From ab6b4d1f3fd259f1b14a029da3fb62d4387f8fc5 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Tue, 2 Jun 2026 13:52:10 +0000 Subject: [PATCH] fix(site/src/pages/AgentsPage): prevent planning pill overlap --- .../components/AgentChatInput.stories.tsx | 70 +++++++++++++++++++ .../AgentsPage/components/AgentChatInput.tsx | 55 +++++++++++---- 2 files changed, 110 insertions(+), 15 deletions(-) diff --git a/site/src/pages/AgentsPage/components/AgentChatInput.stories.tsx b/site/src/pages/AgentsPage/components/AgentChatInput.stories.tsx index 375cf88430..b7cf64715a 100644 --- a/site/src/pages/AgentsPage/components/AgentChatInput.stories.tsx +++ b/site/src/pages/AgentsPage/components/AgentChatInput.stories.tsx @@ -842,6 +842,76 @@ export const PlanningIndicator: Story = { }, }; +const narrowPlanningContextUsage: AgentContextUsage = { + usedTokens: 100_000, + contextLimitTokens: 200_000, +}; + +const narrowPlanningModelOptions = [ + { + id: "long-model-name", + provider: "anthropic", + model: "claude-sonnet-4-5-long-name", + displayName: "Claude Sonnet 4.5 Extended Thinking", + }, +] as const; + +export const PlanningIndicatorNarrow: Story = { + args: { + planModeEnabled: true, + onPlanModeToggle: fn(), + contextUsage: narrowPlanningContextUsage, + selectedModel: narrowPlanningModelOptions[0].id, + modelOptions: [...narrowPlanningModelOptions], + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const composer = await canvas.findByTestId("chat-composer"); + const sendButton = canvas.getByRole("button", { name: "Send" }); + const contextUsageButton = canvas.getByRole("button", { + name: /Context usage/, + }); + const planningBadge = canvasElement.querySelector( + "[data-testid='planning-badge']", + ); + const isVisible = (element: HTMLElement) => { + const style = getComputedStyle(element); + const rect = element.getBoundingClientRect(); + return ( + style.display !== "none" && + style.visibility !== "hidden" && + rect.width > 0 && + rect.height > 0 + ); + }; + + await waitFor(() => { + const composerRect = composer.getBoundingClientRect(); + const sendButtonRect = sendButton.getBoundingClientRect(); + const contextUsageRect = contextUsageButton.getBoundingClientRect(); + + expect(contextUsageRect.left).toBeGreaterThanOrEqual(composerRect.left); + expect(sendButtonRect.right).toBeLessThanOrEqual(composerRect.right); + + if (planningBadge && isVisible(planningBadge)) { + expect(planningBadge.getBoundingClientRect().right).toBeLessThanOrEqual( + contextUsageRect.left + 1, + ); + return; + } + + expect(canvas.getByRole("button", { name: "1 more item" })).toBeVisible(); + }); + }, +}; + export const DisablePlanModeFromBadge: Story = { args: { planModeEnabled: true, diff --git a/site/src/pages/AgentsPage/components/AgentChatInput.tsx b/site/src/pages/AgentsPage/components/AgentChatInput.tsx index 3377cab6c6..4ec59f37a2 100644 --- a/site/src/pages/AgentsPage/components/AgentChatInput.tsx +++ b/site/src/pages/AgentsPage/components/AgentChatInput.tsx @@ -196,7 +196,8 @@ export interface AttachedWorkspaceInfo { type ToolBadgeData = | { kind: "workspace"; name: string } | ({ kind: "attached-workspace" } & AttachedWorkspaceInfo) - | { kind: "mcp"; server: TypesGen.MCPServerConfig }; + | { kind: "mcp"; server: TypesGen.MCPServerConfig } + | { kind: "planning" }; // Small `X` button rendered inside pill-style badges (attached // workspace, MCP server, planning indicator) to dismiss or disable @@ -224,13 +225,38 @@ const ToolBadge: FC<{ badge: ToolBadgeData; onRemoveWorkspace?: () => void; onRemoveMcp?: (serverId: string) => void; + onRemovePlanning?: () => void; + isDisabled?: boolean; className?: string; -}> = ({ badge, onRemoveWorkspace, onRemoveMcp, className }) => { +}> = ({ + badge, + onRemoveWorkspace, + onRemoveMcp, + onRemovePlanning, + isDisabled, + className, +}) => { const badgeCls = cn( "inline-flex shrink-0 items-center gap-1 rounded-full bg-surface-secondary px-2 py-0.5 text-xs font-medium text-content-secondary", className, ); + if (badge.kind === "planning") { + return ( + + + Planning + {onRemovePlanning && ( + + )} + + ); + } + if (badge.kind === "attached-workspace") { return ( @@ -525,6 +551,9 @@ export const AgentChatInput: FC = ({ // Ordered list of active tool badge data so we can determine // which ones ended up in the overflow popover. const allBadges: ToolBadgeData[] = []; + if (planModeEnabled) { + allBadges.push({ kind: "planning" }); + } // When workspace data is available, WorkspacePill handles // the display (including app dropdown). Otherwise fall back // to the simple attached-workspace ToolBadge. @@ -1339,24 +1368,12 @@ export const AgentChatInput: FC = ({ disabled={isDisabled} placeholder={modelSelectorPlaceholder} formatProviderLabel={formatProviderLabel} + className="md:shrink" dropdownSide="top" dropdownAlign="center" enableMobileFullWidthDropdown /> )} - {planModeEnabled && ( - - - Planning - {onPlanModeToggle && ( - - )} - - )}{" "} {/* Badge row; all badges and the pill always * render so the DOM structure never changes. * Overflow badges use invisible + order-1 to @@ -1387,6 +1404,10 @@ export const AgentChatInput: FC = ({ badge={badge} onRemoveWorkspace={removeWorkspaceHandler} onRemoveMcp={handleRemoveMcp} + onRemovePlanning={ + onPlanModeToggle ? handleDisablePlanMode : undefined + } + isDisabled={isDisabled} className={isOverflow ? "invisible order-1" : undefined} /> ); @@ -1427,6 +1448,10 @@ export const AgentChatInput: FC = ({ badge={badge} onRemoveWorkspace={removeWorkspaceHandler} onRemoveMcp={handleRemoveMcp} + onRemovePlanning={ + onPlanModeToggle ? handleDisablePlanMode : undefined + } + isDisabled={isDisabled} /> ))}