From 5be2fa7974adcafc3cb56b6ed77231445106e4d4 Mon Sep 17 00:00:00 2001 From: TJ Date: Wed, 19 Aug 2026 14:02:11 -0700 Subject: [PATCH] feat(site/src/pages/AgentsPage/components/ChatsSidebar): move PR icon to diff line, center unread dot, persist kebab (#28326) Three chat-sidebar row updates cherry-picked from #28317 (that PR stays open as the full riff), plus one removal: - **PR icon moves to the diff line.** The linked pull request icon (open / draft / merged / closed) no longer replaces the status icon when the chat is idle. It renders on the second line next to the `+additions -deletions` stats, in both the sidebar tree and the chat search results, regardless of chat status. - **Unread dot alignment.** The unread dot is centered in a box matching the kebab's width, so it sits on the same axis as the actions trigger instead of shifting when the hover swap happens. - **Persistent kebab on the selected chat.** The row actions trigger stays visible on the active chat (no hover required, reachable on touch devices); the timestamp is hidden there. Other rows keep the hover swap. - **Subagent count badge removed.** The `N` + bot icon added to the metadata line in #28234 no longer renders, making room for the PR icon and diff stats. The "Show subagents" menu toggle and expand behavior are unchanged. Status and PR icons carry `role="img"` with accessible labels ("Idle", "Pull request merged", ...).
Implementation notes - `statusConfig.ts`: `getChatDisplayConfig(chat)` now returns the status icon plus a separate `prIcon` config (icon, className, accessible label) instead of letting the PR icon take precedence over the status. Labels added for all status and PR states. - `ChatTreeNode.tsx` / `ChatSearchResults.tsx`: render `prIcon` on the second line before the line stats. - Kebab persistence reuses the existing `isActiveChat` value: `opacity-100` on the trigger, `hidden` on the timestamp for the active row. - `ChatTreeNode.tsx`: the on-row subagent count badge (`childIDs.length` + `BotIcon`) is removed; the `SubagentsMenuToggle` story only asserts the menu toggle, so it still passes. - Not included from #28317: the subagent status icon variants (`CheckCheckIcon` / `SubagentsLoaderIcon`, the `hasSubagents` param, and the `WithSubagentStatusIcons` story). Validation: - `pnpm run lint` (biome, tsc, knip, circular deps, compiler): pass - Story tests: 96 passed (4 files) including the updated `WithPRStateIcons` label assertions and the new `ActiveChatKebabPersistent` story - Unit suite for the sidebar area: 65 passed - `make pre-commit` (via git hook): pass
_Generated by Coder Agents on behalf of @tracyjohnsonux._ --- .../ChatsSidebar/ChatsSidebar.stories.tsx | 47 ++++++++++++++ .../dialogs/ChatSearchResults.tsx | 16 ++++- .../ChatsSidebar/tree/ChatTreeNode.tsx | 40 +++++++----- .../ChatsSidebar/tree/statusConfig.ts | 63 ++++++++++++++----- 4 files changed, 131 insertions(+), 35 deletions(-) diff --git a/site/src/pages/AgentsPage/components/ChatsSidebar/ChatsSidebar.stories.tsx b/site/src/pages/AgentsPage/components/ChatsSidebar/ChatsSidebar.stories.tsx index cbcbd3b35f1..ada70da5e03 100644 --- a/site/src/pages/AgentsPage/components/ChatsSidebar/ChatsSidebar.stories.tsx +++ b/site/src/pages/AgentsPage/components/ChatsSidebar/ChatsSidebar.stories.tsx @@ -1860,6 +1860,53 @@ export const WithPRStateIcons: Story = { routing: agentsRouting, }), }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await waitFor(() => { + expect(canvas.getByLabelText("Pull request open")).toBeInTheDocument(); + expect(canvas.getByLabelText("Draft pull request")).toBeInTheDocument(); + expect(canvas.getByLabelText("Pull request merged")).toBeInTheDocument(); + expect(canvas.getByLabelText("Pull request closed")).toBeInTheDocument(); + }); + }, +}; + +export const ActiveChatKebabPersistent: Story = { + args: { + chats: [ + buildChat({ + id: "active-chat", + title: "Active chat", + updated_at: recentTimestamp, + }), + buildChat({ + id: "other-chat", + title: "Other chat", + updated_at: recentTimestamp, + }), + ], + }, + parameters: { + reactRouter: reactRouterParameters({ + location: { + path: "/agents/active-chat", + pathParams: { agentId: "active-chat" }, + }, + routing: agentsRouting, + }), + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const activeTrigger = await canvas.findByLabelText( + "Open actions for Active chat", + ); + // The active chat keeps its actions trigger visible without hover. + await waitFor(() => { + expect(window.getComputedStyle(activeTrigger).opacity).toBe("1"); + }); + const otherTrigger = canvas.getByLabelText("Open actions for Other chat"); + expect(window.getComputedStyle(otherTrigger).opacity).toBe("0"); + }, }; export const WithUnreadChats: Story = { diff --git a/site/src/pages/AgentsPage/components/ChatsSidebar/dialogs/ChatSearchResults.tsx b/site/src/pages/AgentsPage/components/ChatsSidebar/dialogs/ChatSearchResults.tsx index 6e6edc2e477..0fbdf3fdab1 100644 --- a/site/src/pages/AgentsPage/components/ChatsSidebar/dialogs/ChatSearchResults.tsx +++ b/site/src/pages/AgentsPage/components/ChatsSidebar/dialogs/ChatSearchResults.tsx @@ -255,8 +255,11 @@ const ChatSearchResultRow: FC = ({ const { icon: StatusIcon, className: statusClassName, + label: statusLabel, + prIcon, diffStatus, } = getChatDisplayConfig(chat); + const PRIcon = prIcon?.icon; const additions = diffStatus?.additions ?? 0; const deletions = diffStatus?.deletions ?? 0; const changedFiles = diffStatus?.changed_files ?? 0; @@ -285,12 +288,23 @@ const ChatSearchResultRow: FC = ({ isSelected && "bg-surface-tertiary/40 text-content-primary", )} > - +
{chat.title}
+ {PRIcon && prIcon && ( + + )} {hasLineStats && ( +{additions} diff --git a/site/src/pages/AgentsPage/components/ChatsSidebar/tree/ChatTreeNode.tsx b/site/src/pages/AgentsPage/components/ChatsSidebar/tree/ChatTreeNode.tsx index 04ed47cc7a0..ce2bacc0f9b 100644 --- a/site/src/pages/AgentsPage/components/ChatsSidebar/tree/ChatTreeNode.tsx +++ b/site/src/pages/AgentsPage/components/ChatsSidebar/tree/ChatTreeNode.tsx @@ -1,5 +1,4 @@ import { - BotIcon, ChevronDownIcon, ChevronRightIcon, EllipsisVerticalIcon, @@ -127,8 +126,11 @@ export const ChatTreeNode: FC = ({ chat, isChildNode }) => { const { icon: StatusIcon, className: statusClassName, + label: statusLabel, + prIcon, diffStatus, } = getChatDisplayConfig(chat); + const PRIcon = prIcon?.icon; const hasLinkedDiffStatus = Boolean(diffStatus?.url); const changedFiles = diffStatus?.changed_files ?? 0; const additions = diffStatus?.additions ?? 0; @@ -199,6 +201,8 @@ export const ChatTreeNode: FC = ({ chat, isChildNode }) => { ? `agents-tree-executing-${chat.id}` : undefined } + role="img" + aria-label={statusLabel} className={cn("size-3.5 shrink-0", statusClassName)} />
@@ -242,16 +246,12 @@ export const ChatTreeNode: FC = ({ chat, isChildNode }) => { )}
- {hasChildren && ( - - {childIDs.length} - + {PRIcon && prIcon && ( + )} {hasLinkedDiffStatus && hasLineStats && ( = ({ chat, isChildNode }) => { // keep the timestamp visible. hasMenuActions && "[@media(hover:hover)]:group-hover:hidden group-has-[[data-state=open]]:hidden", + hasMenuActions && isActiveChat && "hidden", )} > {chat.has_unread && !isActiveChat ? ( -