Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/hooks/tools.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,16 @@ If a value is too large for the environment, it may be omitted (not set). Mux al

</details>

<details>
<summary>complete_goal (2)</summary>

| Env var | JSON path | Type | Description |
| ------------------------ | --------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `MUX_TOOL_INPUT_GOAL_ID` | `goalId` | string | Optional optimistic-concurrency token. Pass the `goalId` returned by `get_goal` to ensure the completion is rejected with a typed conflict error if the user clears or replaces the goal mid-stream. |
| `MUX_TOOL_INPUT_SUMMARY` | `summary` | string | Required 1-2 sentence justification for completing the current goal. |

</details>

<details>
<summary>desktop_click (3)</summary>

Expand Down
4 changes: 4 additions & 0 deletions src/browser/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ import { MULTI_PROJECT_SIDEBAR_SECTION_ID } from "@/common/constants/multiProjec
import { WORKSPACE_DEFAULTS } from "@/constants/workspaceDefaults";
import { isDesktopMode } from "@/browser/hooks/useDesktopTitlebar";
import { prependInitialAppProxyBasePath } from "@/browser/utils/frontendBasePath";
import { WorkspaceActiveGoalsWarningToast } from "@/browser/components/ActiveGoalsWarningToast/ActiveGoalsWarningToast";
import { LoadingScreen } from "@/browser/components/LoadingScreen/LoadingScreen";

function RootRouteShell(props: {
Expand Down Expand Up @@ -186,6 +187,7 @@ function AppInner() {
);

const [isMultiProjectWorkspaceModalOpen, setMultiProjectWorkspaceModalOpen] = useState(false);
const goalsEnabled = useExperimentValue(EXPERIMENT_IDS.GOALS);
const multiProjectWorkspacesEnabled = useExperimentValue(EXPERIMENT_IDS.MULTI_PROJECT_WORKSPACES);

// Left sidebar is drag-resizable (mirrors RightSidebar). Width is persisted globally;
Expand Down Expand Up @@ -714,6 +716,7 @@ function AppInner() {
onStartWorkspaceCreation: openNewWorkspaceFromPalette,
onStartMultiProjectWorkspaceCreation: openNewMultiProjectWorkspaceFromPalette,
multiProjectWorkspacesEnabled,
goalsEnabled,
onArchiveMergedWorkspacesInProject: archiveMergedWorkspacesInProjectFromPalette,
getBranchesForProject,
onSelectWorkspace: selectWorkspaceFromPalette,
Expand Down Expand Up @@ -1216,6 +1219,7 @@ function AppInner() {
)}
</div>
</div>
<WorkspaceActiveGoalsWarningToast enabled={goalsEnabled} />
<CommandPalette getSlashContext={() => ({ workspaceId: selectedWorkspace?.workspaceId })} />
<ProjectCreateModal
initialPath={projectCreateInitialPath}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { cleanup, render, waitFor } from "@testing-library/react";
import { installDom } from "../../../../tests/ui/dom";

import { ActiveGoalsWarningToast } from "./ActiveGoalsWarningToast";

describe("ActiveGoalsWarningToast", () => {
let cleanupDom: (() => void) | null = null;

beforeEach(() => {
cleanupDom = installDom();
});

afterEach(() => {
cleanup();
cleanupDom?.();
cleanupDom = null;
});

test("fires once on a rising edge above three active goals", async () => {
const { queryByRole, rerender } = render(<ActiveGoalsWarningToast activeGoalCount={3} />);

expect(queryByRole("status")).toBeNull();

rerender(<ActiveGoalsWarningToast activeGoalCount={4} />);
await waitFor(() => expect(queryByRole("status")?.textContent).toContain("4 active goals"));

rerender(<ActiveGoalsWarningToast activeGoalCount={5} />);
expect(queryByRole("status")?.textContent).toContain("4 active goals");
});

test("re-arms after the active-goal count falls to three", async () => {
const { queryByRole, rerender } = render(<ActiveGoalsWarningToast activeGoalCount={4} />);

await waitFor(() => expect(queryByRole("status")?.textContent).toContain("4 active goals"));

rerender(<ActiveGoalsWarningToast activeGoalCount={3} />);
await waitFor(() => expect(queryByRole("status")).toBeNull());

rerender(<ActiveGoalsWarningToast activeGoalCount={4} />);
await waitFor(() => expect(queryByRole("status")?.textContent).toContain("4 active goals"));
});

test("announces warnings politely", async () => {
const { getByRole } = render(<ActiveGoalsWarningToast activeGoalCount={4} />);

await waitFor(() => expect(getByRole("status").getAttribute("aria-live")).toBe("polite"));
});

test("does not fire when the GOALS experiment is disabled", () => {
// Coder-agents-review P3 DEREM-49: pin the experiment-off short-circuit
// so a regression that removed the `enabled === false` guard would fail.
// Without it, users who toggled the experiment off mid-session would get
// spurious warnings whenever active goals exceed the threshold.
const { queryByRole } = render(<ActiveGoalsWarningToast activeGoalCount={5} enabled={false} />);

expect(queryByRole("status")).toBeNull();
});

test("clears any showing toast when the experiment is toggled off mid-session", async () => {
// The experiment-off branch also clears a *currently showing* warning,
// not just suppresses new ones. Render with enabled=true above the
// threshold (which fires the toast), then flip to enabled=false and
// assert the toast disappears.
const { queryByRole, rerender } = render(<ActiveGoalsWarningToast activeGoalCount={4} />);
await waitFor(() => expect(queryByRole("status")?.textContent).toContain("4 active goals"));

rerender(<ActiveGoalsWarningToast activeGoalCount={4} enabled={false} />);
await waitFor(() => expect(queryByRole("status")).toBeNull());
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { useEffect, useRef, useState } from "react";
import { useActiveGoalCount } from "@/browser/stores/WorkspaceStore";

const ACTIVE_GOAL_WARNING_THRESHOLD = 3;
const AUTO_DISMISS_MS = 5_000;
const wrapperClassName =
"pointer-events-none fixed top-4 right-4 z-[10000] max-w-[min(420px,calc(100vw-2rem))] [&>*]:pointer-events-auto";

interface ActiveGoalsWarningToastProps {
activeGoalCount: number;
enabled?: boolean;
}

export function ActiveGoalsWarningToast(props: ActiveGoalsWarningToastProps) {
const [toastCount, setToastCount] = useState<number | null>(null);
const wasAboveThresholdRef = useRef(false);

useEffect(() => {
if (props.enabled === false) {
wasAboveThresholdRef.current = false;
setToastCount(null);
return;
}

const isAboveThreshold = props.activeGoalCount > ACTIVE_GOAL_WARNING_THRESHOLD;
if (!isAboveThreshold) {
wasAboveThresholdRef.current = false;
setToastCount(null);
return;
}

// Warn on the rising edge only so several stream-end updates during the same elevated
// active-goal period do not spam the user.
if (wasAboveThresholdRef.current) {
return;
}

wasAboveThresholdRef.current = true;
setToastCount(props.activeGoalCount);
}, [props.activeGoalCount, props.enabled]);

useEffect(() => {
if (toastCount == null || typeof window === "undefined") {
return;
}

const timeoutId = window.setTimeout(() => setToastCount(null), AUTO_DISMISS_MS);
return () => window.clearTimeout(timeoutId);
}, [toastCount]);

if (toastCount == null) {
return null;
}

return (
<div className={wrapperClassName}>
<div
role="status"
aria-live="polite"
className="bg-background-secondary border-warning text-warning flex animate-[toastSlideIn_0.2s_ease-out] items-start gap-2 rounded border px-3 py-2 text-xs shadow-[0_4px_12px_rgba(0,0,0,0.3)]"
>
<span className="bg-warning mt-1 inline-block h-2 w-2 shrink-0 rounded-full" />
<span>
You have {toastCount} active goals running concurrently. Goal continuations will fire
serially across workspaces.
</span>
</div>
</div>
);
}

export function WorkspaceActiveGoalsWarningToast(props: { enabled?: boolean }) {
const activeGoalCount = useActiveGoalCount();

return <ActiveGoalsWarningToast activeGoalCount={activeGoalCount} enabled={props.enabled} />;
}
79 changes: 79 additions & 0 deletions src/browser/components/AgentListItem/AgentListItem.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import {
getWorkspaceLastReadKey,
} from "@/common/constants/storage";
import type { AgentRowRenderMeta } from "@/browser/utils/ui/workspaceFiltering";
import { EXPERIMENT_IDS, getExperimentKey } from "@/common/constants/experiments";
import type { GoalStatus } from "@/common/types/goal";
import type { WorkspaceActivitySnapshot } from "@/common/orpc/types";

const meta: Meta<typeof AgentListItem> = {
title: "Components/AgentListItem",
Expand Down Expand Up @@ -82,8 +85,10 @@ function StoryScaffold(props: {
activeWorkspaceId?: string;
workspaces?: ReadonlyArray<(typeof STORY_WORKSPACES)[number]>;
rowContainerClassName?: string;
workspaceActivitySnapshots?: Record<string, WorkspaceActivitySnapshot>;
}) {
const api = createMockORPCClient({
workspaceActivitySnapshots: props.workspaceActivitySnapshots,
onChat: (workspaceId, emit) => {
emit({ type: "caught-up", hasOlderHistory: false });
if (workspaceId === "ws-active") {
Expand Down Expand Up @@ -293,6 +298,49 @@ function renderIdleState(isUnread: boolean) {
return renderSingleWorkspaceState(2);
}

function renderGoalState(
status: GoalStatus,
accounting?: { budgetCents: number | null; costCents: number }
) {
const workspace = STORY_WORKSPACES[2];
updatePersistedState(getExperimentKey(EXPERIMENT_IDS.GOALS), true);
return (
<StoryScaffold
activeWorkspaceId={workspace.id}
workspaceActivitySnapshots={{
[workspace.id]: {
recency: NOW,
streaming: false,
lastModel: null,
lastThinkingLevel: null,
goal: {
goalId: "11111111-1111-4111-8111-111111111111",
status,
objective: "Ship the goal primitive",
budgetCents: accounting?.budgetCents ?? null,
costCents: accounting?.costCents ?? 0,
turnsUsed: 0,
turnCap: null,
completionSummary: status === "complete" ? "The goal primitive shipped." : undefined,
startedAtMs: NOW,
},
},
}}
>
<AgentListItem
metadata={workspace}
projectPath={PROJECT_PATH}
projectName={PROJECT_NAME}
isSelected={false}
onSelectWorkspace={() => undefined}
onForkWorkspace={() => Promise.resolve()}
onArchiveWorkspace={() => Promise.resolve()}
onCancelCreation={() => Promise.resolve()}
/>
</StoryScaffold>
);
}

function renderDraftState() {
return (
<StoryScaffold>
Expand Down Expand Up @@ -530,6 +578,37 @@ export const Question: Story = {
render: () => renderSingleWorkspaceState(4),
};

export const GoalActive: Story = {
args: undefined as never,
render: () => renderGoalState("active"),
};

export const GoalActiveBudgeted: Story = {
args: undefined as never,
render: () => renderGoalState("active", { budgetCents: 500, costCents: 123 }),
};

export const GoalActiveUnbudgeted: Story = {
args: undefined as never,
render: () => renderGoalState("active", { budgetCents: null, costCents: 123 }),
};

export const GoalPaused: Story = {
args: undefined as never,
render: () => renderGoalState("paused"),
};

export const GoalBudgetLimited: Story = {
args: undefined as never,
name: "Goal/Budget Limited",
render: () => renderGoalState("budget_limited", { budgetCents: 500, costCents: 525 }),
};

export const GoalComplete: Story = {
args: undefined as never,
render: () => renderGoalState("complete"),
};

export const Draft: Story = {
args: undefined as never,
render: renderDraftState,
Expand Down
22 changes: 22 additions & 0 deletions src/browser/components/AgentListItem/AgentListItem.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,28 @@ describe("AgentListItem", () => {
expect(rowView.queryByTestId(`workspace-secondary-row-${TEST_WORKSPACE_ID}`)).toBeNull();
});

test("renders budget-limited goal pill with stateful aria label", () => {
mockWorkspaceHeartbeatsEnabled = true;
mockWorkspaceSidebarState = createWorkspaceSidebarState({
goal: {
goalId: "11111111-1111-4111-8111-111111111111",
status: "budget_limited",
objective: "Ship goal budgets",
budgetCents: 500,
costCents: 525,
turnsUsed: 4,
turnCap: null,
startedAtMs: Date.now(),
},
});

const { row } = renderWorkspaceItem();
const pill = within(row).getByTestId(`workspace-goal-pill-${TEST_WORKSPACE_ID}`);

expect(pill.textContent).toContain("Target budget limited");
expect(pill.getAttribute("aria-label")).toBe("Goal budget limited, $5.25 of $5.00 spent");
});

test("renders a heartbeat icon directly in the leading slot for seen rows when the heartbeat experiment is enabled", () => {
mockWorkspaceHeartbeatsEnabled = true;

Expand Down
Loading
Loading