From 488d2790bec2e808b0c0b9922b128eeb6113e169 Mon Sep 17 00:00:00 2001 From: Tracy Johnson Date: Wed, 19 Aug 2026 20:04:25 +0000 Subject: [PATCH 1/4] feat(site/src/pages/AgentsPage/components/ChatsSidebar): move PR icon to diff line, center unread dot, persist kebab The linked pull request icon (open, draft, merged, closed) now renders on the second line next to the diff stats in both the sidebar tree and the chat search results, instead of replacing the status icon when idle. The unread dot is centered on the kebab axis so it stops jumping when the actions trigger swaps in. The row actions trigger stays visible on the selected chat without hovering, and the timestamp is hidden there. Cherry-picked from #28317 without the subagent status icon variants. --- .../ChatsSidebar/ChatsSidebar.stories.tsx | 47 ++++++++++++++ .../dialogs/ChatSearchResults.tsx | 16 ++++- .../ChatsSidebar/tree/ChatTreeNode.tsx | 36 ++++++++-- .../ChatsSidebar/tree/statusConfig.ts | 65 ++++++++++++++----- 4 files changed, 140 insertions(+), 24 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..cd40901417c 100644 --- a/site/src/pages/AgentsPage/components/ChatsSidebar/tree/ChatTreeNode.tsx +++ b/site/src/pages/AgentsPage/components/ChatsSidebar/tree/ChatTreeNode.tsx @@ -127,8 +127,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 +202,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)} />
@@ -253,6 +258,13 @@ export const ChatTreeNode: FC = ({ chat, isChildNode }) => {
- {hasChildren && ( - - {childIDs.length} - - )} {PRIcon && prIcon && ( Date: Wed, 19 Aug 2026 20:43:45 +0000 Subject: [PATCH 3/4] chore(site/src/pages/AgentsPage/components/ChatsSidebar): drop redundant comments in ChatTreeNode --- .../components/ChatsSidebar/tree/ChatTreeNode.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/site/src/pages/AgentsPage/components/ChatsSidebar/tree/ChatTreeNode.tsx b/site/src/pages/AgentsPage/components/ChatsSidebar/tree/ChatTreeNode.tsx index 81f1e7353ba..ce2bacc0f9b 100644 --- a/site/src/pages/AgentsPage/components/ChatsSidebar/tree/ChatTreeNode.tsx +++ b/site/src/pages/AgentsPage/components/ChatsSidebar/tree/ChatTreeNode.tsx @@ -297,14 +297,10 @@ export const ChatTreeNode: FC = ({ chat, isChildNode }) => { // keep the timestamp visible. hasMenuActions && "[@media(hover:hover)]:group-hover:hidden group-has-[[data-state=open]]:hidden", - // The active chat keeps the actions trigger visible, so - // the timestamp stays hidden there. hasMenuActions && isActiveChat && "hidden", )} > {chat.has_unread && !isActiveChat ? ( - // The w-3.5 box matches the kebab icon's width so the - // dot centers on the same axis as the ellipsis glyph. = ({ chat, isChildNode }) => { variant="subtle" className={cn( "absolute inset-0 flex h-6 w-7 min-w-0 justify-end rounded-none px-0 opacity-0 text-content-secondary hover:text-content-primary [@media(hover:hover)]:group-hover:opacity-100 data-[state=open]:opacity-100", - // Keep the trigger visible on the active chat so it is - // reachable without hovering. isActiveChat && "opacity-100", )} aria-label={`Open actions for ${chat.title}`} From 1ad93c9bc064b38c9fb829ad5dc11cae5d7e38d1 Mon Sep 17 00:00:00 2001 From: Tracy Johnson Date: Wed, 19 Aug 2026 20:51:11 +0000 Subject: [PATCH 4/4] chore(site/src/pages/AgentsPage/components/ChatsSidebar): trim getChatDisplayConfig doc comment --- .../AgentsPage/components/ChatsSidebar/tree/statusConfig.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/site/src/pages/AgentsPage/components/ChatsSidebar/tree/statusConfig.ts b/site/src/pages/AgentsPage/components/ChatsSidebar/tree/statusConfig.ts index 842ffddb577..4a2449e46ae 100644 --- a/site/src/pages/AgentsPage/components/ChatsSidebar/tree/statusConfig.ts +++ b/site/src/pages/AgentsPage/components/ChatsSidebar/tree/statusConfig.ts @@ -91,9 +91,7 @@ const getChatDiffStatus = (chat: Chat): ChatDiffStatus | undefined => { /** * Returns the icons and styling that represent a chat's current state. * - * The status icon always reflects the chat status. Any linked pull - * request is surfaced separately via `prIcon` so rows can render it - * next to the diff stats. + * The status icon always reflects the chat status. */ export const getChatDisplayConfig = ( chat: Chat,