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

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