From 50dbb3d2cb55c2a2f8f2b4a0a85286daafd59794 Mon Sep 17 00:00:00 2001 From: david-fraley <67079030+david-fraley@users.noreply.github.com> Date: Thu, 23 Apr 2026 09:19:24 -0500 Subject: [PATCH] feat(site/src/pages/AgentsPage/components): allow disengaging plan mode from Planning badge (#24651) Adds an inline `X` button to the "Planning" indicator so users can disengage plan mode directly from the chat input, without reopening the `+` menu. Reuses the same pattern that already ships on the attached-workspace and MCP-server badges. - When `onPlanModeToggle` is provided and plan mode is on, the pill renders a dismiss `X` next to the label; clicking it calls `onPlanModeToggle(false)`. - When no toggle handler is passed, no `X` renders (matches the other badges). - Extracts `BadgeDismissButton` inside `AgentChatInput.tsx` now that the dismiss pattern lives in three places, collapsing ~24 lines of duplicated markup. - Storybook coverage: tightened `PlanningIndicator`, new `DisablePlanModeFromBadge` (click fires `onPlanModeToggle(false)`), new `PlanningIndicatorWithoutToggle` (no handler, no `X`). ### Demo ![Demo: open + menu, enable Plan first, click X on the Planning pill to disengage](https://github.com/david-fraley/coder/raw/pr-24651-media/plan-mode-dismiss.gif) Higher-quality [MP4](https://github.com/david-fraley/coder/raw/pr-24651-media/plan-mode-dismiss.mp4) also available.
Implementation plan ### Red / Green / Refactor 1. **Red**: Extended `AgentChatInput.stories.tsx` to assert the `X` button exists in the Planning pill, clicking it fires `onPlanModeToggle(false)`, and no `X` renders when `onPlanModeToggle` is absent. Two stories failed as expected. 2. **Green**: Added an inline `
--- _This PR was opened by a Coder agent on behalf of @david-fraley._ --------- Co-authored-by: Jaayden Halko --- .../components/AgentChatInput.stories.tsx | 33 +++++++++++++ .../AgentsPage/components/AgentChatInput.tsx | 49 +++++++++++++------ 2 files changed, 68 insertions(+), 14 deletions(-) diff --git a/site/src/pages/AgentsPage/components/AgentChatInput.stories.tsx b/site/src/pages/AgentsPage/components/AgentChatInput.stories.tsx index b033162163..80b58f30c9 100644 --- a/site/src/pages/AgentsPage/components/AgentChatInput.stories.tsx +++ b/site/src/pages/AgentsPage/components/AgentChatInput.stories.tsx @@ -653,6 +653,39 @@ export const PlanningIndicator: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByText("Planning")).toBeVisible(); + expect( + canvas.getByRole("button", { name: "Disable plan mode" }), + ).toBeVisible(); + }, +}; + +export const DisablePlanModeFromBadge: Story = { + args: { + planModeEnabled: true, + onPlanModeToggle: fn(), + }, + play: async ({ args, canvasElement }) => { + const canvas = within(canvasElement); + const dismiss = canvas.getByRole("button", { + name: "Disable plan mode", + }); + await userEvent.click(dismiss); + expect(args.onPlanModeToggle).toHaveBeenCalledTimes(1); + expect(args.onPlanModeToggle).toHaveBeenCalledWith(false); + }, +}; + +export const PlanningIndicatorWithoutToggle: Story = { + args: { + planModeEnabled: true, + onPlanModeToggle: undefined, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText("Planning")).toBeVisible(); + expect( + canvas.queryByRole("button", { name: "Disable plan mode" }), + ).not.toBeInTheDocument(); }, }; diff --git a/site/src/pages/AgentsPage/components/AgentChatInput.tsx b/site/src/pages/AgentsPage/components/AgentChatInput.tsx index 3d18cb923f..4694aa21d7 100644 --- a/site/src/pages/AgentsPage/components/AgentChatInput.tsx +++ b/site/src/pages/AgentsPage/components/AgentChatInput.tsx @@ -171,6 +171,26 @@ type ToolBadgeData = | ({ kind: "attached-workspace" } & AttachedWorkspaceInfo) | { kind: "mcp"; server: TypesGen.MCPServerConfig }; +// Small `X` button rendered inside pill-style badges (attached +// workspace, MCP server, planning indicator) to dismiss or disable +// the badge without opening the `+` menu. Callers pass the action +// handler and a descriptive aria-label. +const BadgeDismissButton: FC<{ + onClick: () => void; + ariaLabel: string; + isDisabled?: boolean; +}> = ({ onClick, ariaLabel, isDisabled = false }) => ( + +); + const ToolBadge: FC<{ badge: ToolBadgeData; onRemoveWorkspace?: () => void; @@ -210,14 +230,10 @@ const ToolBadge: FC<{ {badge.name} {onRemoveWorkspace && ( - + ariaLabel={`Remove workspace ${badge.name}`} + /> )} ); @@ -237,14 +253,10 @@ const ToolBadge: FC<{ )} {badge.server.display_name} {!isForceOn && onRemoveMcp && ( - + ariaLabel={`Remove ${badge.server.display_name}`} + /> )} ); @@ -445,6 +457,8 @@ export const AgentChatInput: FC = ({ setPlusMenuOpen(false); }; + const handleDisablePlanMode = () => onPlanModeToggle?.(false); + const fileInputRef = useRef(null); const [composerElement, setComposerElement] = useState( null, @@ -1065,6 +1079,13 @@ export const AgentChatInput: FC = ({ Planning + {onPlanModeToggle && ( + + )} )}{" "} {/* Badge row — all badges and the pill always