diff --git a/site/src/pages/AgentsPage/components/AgentChatInput.stories.tsx b/site/src/pages/AgentsPage/components/AgentChatInput.stories.tsx index e049a49c2fa..40fe6c53c23 100644 --- a/site/src/pages/AgentsPage/components/AgentChatInput.stories.tsx +++ b/site/src/pages/AgentsPage/components/AgentChatInput.stories.tsx @@ -1479,6 +1479,21 @@ export const OverflowBadges: Story = { ], selectedWorkspaceId: "ws-1", onWorkspaceChange: fn(), + attachedWorkspace: { + id: "ws-1", + name: "my-long-workspace-name", + route: "/@admin/my-long-workspace-name", + statusIcon: , + statusLabel: "Workspace running", + }, + workspace: { + ...MockWorkspace, + id: "ws-1", + name: "my-long-workspace-name", + owner_name: "admin", + }, + workspaceAgent: MockWorkspaceAgent, + chatId: "overflow-chat-id", }, parameters: { viewport: { defaultViewport: "mobile2" }, @@ -1546,12 +1561,19 @@ export const ContextNearLimit: Story = { }, }; -/** Long workspace name at iPhone SE width — verifies truncation. */ +/** Long workspace name at iPhone SE width collapses into +N overflow. */ export const LongWorkspaceNameMobile: Story = { args: { ...mcpDefaults, mcpServers: [githubMCPConnected], selectedMCPServerIds: [githubMCPConnected.id], + attachedWorkspace: { + id: MockWorkspace.id, + name: "my-super-extremely-long-workspace-name-that-overflows", + route: `/@${MockWorkspace.owner_name}/my-super-extremely-long-workspace-name-that-overflows`, + statusIcon: , + statusLabel: "Workspace running", + }, workspace: { ...MockWorkspace, name: "my-super-extremely-long-workspace-name-that-overflows", @@ -1565,15 +1587,23 @@ export const LongWorkspaceNameMobile: Story = { }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); - // The workspace pill button should be present. - const pill = await canvas.findByRole("button", { - name: /workspace menu/, + // Too narrow for the pill's minimum width: it must collapse into + // the overflow popover instead of clipping to a tiny pill. + const overflowPill = await canvas.findByRole("button", { + name: /more item/, }); await waitFor(() => { - expect(pill).toBeVisible(); + expect(overflowPill).toBeVisible(); }); + await userEvent.click(overflowPill); + const popover = await within(document.body).findByRole("dialog"); + expect( + within(popover).getByText( + "my-super-extremely-long-workspace-name-that-overflows", + ), + ).toBeInTheDocument(); // The toolbar row should not cause horizontal overflow. - const toolbar = pill.closest( + const toolbar = overflowPill.closest( ".flex.items-center.justify-between", ) as HTMLElement; if (toolbar?.parentElement) { diff --git a/site/src/pages/AgentsPage/components/AgentChatInput.tsx b/site/src/pages/AgentsPage/components/AgentChatInput.tsx index cad62b611b6..4d48c3d254b 100644 --- a/site/src/pages/AgentsPage/components/AgentChatInput.tsx +++ b/site/src/pages/AgentsPage/components/AgentChatInput.tsx @@ -585,16 +585,25 @@ export const AgentChatInput: FC = ({ const shouldOverflowPlanningBadge = planModeEnabled && contextUsage !== undefined; + // When workspace data is available the badge renders as the + // interactive WorkspacePill; its overflow-popover fallback uses the + // richer attachedWorkspace data when present. + let workspacePillBadge: ToolBadgeData | undefined; + if (workspace && workspaceAgent && chatId) { + workspacePillBadge = attachedWorkspace + ? { kind: "attached-workspace", ...attachedWorkspace } + : { kind: "workspace", name: workspace.name }; + } + // Ordered list of active tool badge data so we can determine // which ones ended up in the overflow popover. const allBadges: ToolBadgeData[] = []; if (shouldOverflowPlanningBadge) { 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. - if (!(workspace && workspaceAgent && chatId) && attachedWorkspace) { + if (workspacePillBadge) { + allBadges.push(workspacePillBadge); + } else if (attachedWorkspace) { allBadges.push({ kind: "attached-workspace", ...attachedWorkspace }); } if (shouldShowSelectedWorkspaceBadge && selectedWorkspace) { @@ -1447,24 +1456,37 @@ export const AgentChatInput: FC = ({ * hide and reorder via CSS. The pill is invisible * when there's no overflow but still occupies * layout space, preventing measurement flicker. */} - {workspace && workspaceAgent && chatId && ( - - - - )}
{allBadges.map((badge, i) => { const isOverflow = overflowCount > 0 && i >= visibleCount; + if ( + badge === workspacePillBadge && + workspace && + workspaceAgent && + chatId + ) { + return ( + + + + ); + } return ( = ({ role="combobox" type="button" variant="subtle" + // min-w floor: ~8ch of the label plus 3.125rem of fixed chrome + // stay visible before truncation kicks in. className={cn( - "h-7 min-w-0 shrink justify-start gap-1 rounded-full border-0 bg-surface-secondary px-2 py-0.5 text-xs font-medium shadow-none transition-colors hover:bg-surface-tertiary hover:text-content-primary focus:ring-0 focus-visible:ring-2 focus-visible:ring-content-link [&>svg]:!size-3.5 [&>svg]:p-0 [&>svg]:shrink-0 [&>svg]:transition [&>svg]:hover:text-content-primary [&>img]:!size-3 [&>img]:!p-0", + "h-7 min-w-[calc(8ch_+_3.125rem)] shrink justify-start gap-1 rounded-full border-0 bg-surface-secondary px-2 py-0.5 text-xs font-medium shadow-none transition-colors hover:bg-surface-tertiary hover:text-content-primary focus:ring-0 focus-visible:ring-2 focus-visible:ring-content-link [&>svg]:!size-3.5 [&>svg]:p-0 [&>svg]:shrink-0 [&>svg]:transition [&>svg]:hover:text-content-primary [&>img]:!size-3 [&>img]:!p-0", className, )} onTouchStart={onTriggerTouchStart} @@ -196,11 +198,9 @@ export const ModelSelector: FC = ({ contentClassName, )} onOpenAutoFocus={(event) => { - // On touch devices, auto-focusing the search input pops the - // software keyboard as soon as the picker opens, hiding the - // model list behind it. Only keep the WAI-ARIA combobox - // focus-into-input behavior for fine pointers (keyboard and - // mouse users on desktop). + // Touch devices auto-focus search on open, and the software + // keyboard hides the model list. Keep the WAI-ARIA combobox + // focus-into-input behavior for fine pointers only. if (matchMedia("(pointer: coarse)").matches) { event.preventDefault(); } diff --git a/site/src/pages/AgentsPage/components/WorkspacePill.tsx b/site/src/pages/AgentsPage/components/WorkspacePill.tsx index 04e5dff8469..9737a8cb531 100644 --- a/site/src/pages/AgentsPage/components/WorkspacePill.tsx +++ b/site/src/pages/AgentsPage/components/WorkspacePill.tsx @@ -125,7 +125,10 @@ export const WorkspacePill: FC = ({ } }} > - + {/* Floor of ~8ch of name + 3.125rem chrome (padding, status icon, + * gaps, chevron). Below the floor the overflow system moves the + * pill into the +N popover instead of shrinking it further. */} + setTooltipOpen(v && !open)} @@ -136,29 +139,22 @@ export const WorkspacePill: FC = ({ type="button" aria-label={`${workspace.name} workspace menu`} className={cn( - "inline-flex min-w-0 cursor-pointer items-center justify-center gap-1 rounded-full border-0 bg-transparent p-0 text-xs font-medium text-content-secondary transition-colors hover:bg-surface-tertiary hover:text-content-primary", - "size-7 md:size-auto md:max-w-[200px] md:justify-start md:px-2 md:py-0.5", + "inline-flex min-w-0 cursor-pointer items-center justify-start gap-1 rounded-full border-0 bg-transparent p-0 text-xs font-medium text-content-secondary transition-colors hover:bg-surface-tertiary hover:text-content-primary", + "h-7 w-full max-w-[200px] px-2 py-0.5", )} > - - - {workspace.name} - + + {workspace.name} - - {statusLabel} - + {statusLabel}