From 79a28bad728ffa33045d700a91c18b4246bd735d Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Fri, 12 Jun 2026 12:39:53 +0100 Subject: [PATCH] feat(site): group shared agents in sidebar (#26328) --- docs/ai-coder/agents/chat-sharing.md | 6 +- site/src/pages/AgentsPage/AgentsPage.tsx | 1 + .../AgentsPage/AgentsPageView.stories.tsx | 1 + site/src/pages/AgentsPage/AgentsPageView.tsx | 3 + .../ChatsSidebar/ChatsSidebar.stories.tsx | 1 + .../ChatsSidebar/ChatsSidebar.test.tsx | 73 +++++++++++++++++++ .../components/ChatsSidebar/ChatsSidebar.tsx | 3 + .../ChatsSidebar/chats/ChatsPanel.tsx | 43 ++++++++++- 8 files changed, 125 insertions(+), 6 deletions(-) diff --git a/docs/ai-coder/agents/chat-sharing.md b/docs/ai-coder/agents/chat-sharing.md index 89a9391d0e..0eeb4f9495 100644 --- a/docs/ai-coder/agents/chat-sharing.md +++ b/docs/ai-coder/agents/chat-sharing.md @@ -11,13 +11,13 @@ Chat sharing lets you give other users or groups read-only access to a Coder Age 1. Click **Add member** to grant **Read** access. 1. Copy the chat URL from your browser and send it to the recipients. -Coder does not create a separate share link or notify recipients. They must open the chat from the URL you send them. +Coder does not create a separate share link or notify recipients. Recipients need the chat URL for initial access. ## Shared chat access -Viewers can open the chat from a direct link, view messages, stream live updates, and download chat attachments. They reach sub-agent chats by following sub-agent links inside the parent chat or by opening a direct URL. +Viewers can open the chat from a direct link, view messages, stream live updates, and download chat attachments. Chats shared by other users can appear in the sidebar under **Shared with you** when they are in the chat list. Pinned shared chats appear under **Pinned**. Viewers reach sub-agent chats by following sub-agent links inside the parent chat or by opening a direct URL. -Shared chats do not appear in the viewer's normal chat list. Viewers have read-only access: they cannot send or edit messages, regenerate the chat title, archive the chat, or change its sharing settings. +Viewers have read-only access: they cannot send or edit messages, regenerate the chat title, archive the chat, or change its sharing settings. ## Disable chat sharing diff --git a/site/src/pages/AgentsPage/AgentsPage.tsx b/site/src/pages/AgentsPage/AgentsPage.tsx index a42505bcae..0584a6bf64 100644 --- a/site/src/pages/AgentsPage/AgentsPage.tsx +++ b/site/src/pages/AgentsPage/AgentsPage.tsx @@ -679,6 +679,7 @@ const AgentsPage: FC = () => { = { agentId: undefined, chatList: [], + currentUserId: MockUserOwner.id, catalogModelOptions: defaultModelOptions, modelConfigs: defaultModelConfigs, handleNewAgent: fn(), diff --git a/site/src/pages/AgentsPage/AgentsPageView.tsx b/site/src/pages/AgentsPage/AgentsPageView.tsx index 817c0f7c29..6df47b4b1e 100644 --- a/site/src/pages/AgentsPage/AgentsPageView.tsx +++ b/site/src/pages/AgentsPage/AgentsPageView.tsx @@ -40,6 +40,7 @@ export interface AgentsOutletContext { interface AgentsPageViewProps { agentId: string | undefined; chatList: TypesGen.Chat[]; + currentUserId: string; catalogModelOptions: readonly ModelSelectorOption[]; modelConfigs: readonly TypesGen.ChatModelConfig[]; handleNewAgent: () => void; @@ -83,6 +84,7 @@ interface AgentsPageViewProps { export const AgentsPageView: FC = ({ agentId, chatList, + currentUserId, catalogModelOptions, modelConfigs, handleNewAgent, @@ -180,6 +182,7 @@ export const AgentsPageView: FC = ({ > = { onSearchDialogOpenChange: fn(), isCreating: false, regeneratingTitleChatIds: [], + currentUserId: MockUserOwner.id, sidebarFilters: defaultSidebarFilters, isPersonalModelOverridesEnabled: true, onSidebarFiltersChange: fn(), diff --git a/site/src/pages/AgentsPage/components/ChatsSidebar/ChatsSidebar.test.tsx b/site/src/pages/AgentsPage/components/ChatsSidebar/ChatsSidebar.test.tsx index e341d82e74..1912cee1a7 100644 --- a/site/src/pages/AgentsPage/components/ChatsSidebar/ChatsSidebar.test.tsx +++ b/site/src/pages/AgentsPage/components/ChatsSidebar/ChatsSidebar.test.tsx @@ -134,10 +134,83 @@ const defaultProps: React.ComponentProps = { isCreating: false, sidebarFilters: defaultSidebarFilters, onSidebarFiltersChange: vi.fn(), + currentUserId: MockUserOwner.id, }; // ---- Tests ---- +describe("ChatsSidebar sections", () => { + it("renders unpinned shared chats in Shared with you before date sections", () => { + render( + + + , + ); + + const pinnedSection = screen.getByTestId("agents-section-toggle-Pinned"); + const pinnedSharedNode = screen.getByTestId( + "agents-tree-node-pinned-shared-chat", + ); + const sharedSection = screen.getByTestId( + "agents-section-toggle-Shared-with-you", + ); + const sharedNode = screen.getByTestId("agents-tree-node-shared-chat"); + const todaySection = screen.getByTestId("agents-section-toggle-Today"); + const ownedNode = screen.getByTestId("agents-tree-node-owned-chat"); + + expect(pinnedSection).toHaveTextContent("Pinned (1)"); + expect(sharedSection).toHaveTextContent("Shared with you (1)"); + expect(todaySection).toHaveTextContent("Today (2)"); + expect( + pinnedSection.compareDocumentPosition(pinnedSharedNode) & + Node.DOCUMENT_POSITION_FOLLOWING, + ).toBeTruthy(); + expect( + pinnedSharedNode.compareDocumentPosition(sharedSection) & + Node.DOCUMENT_POSITION_FOLLOWING, + ).toBeTruthy(); + expect( + sharedSection.compareDocumentPosition(sharedNode) & + Node.DOCUMENT_POSITION_FOLLOWING, + ).toBeTruthy(); + expect( + sharedNode.compareDocumentPosition(todaySection) & + Node.DOCUMENT_POSITION_FOLLOWING, + ).toBeTruthy(); + expect( + todaySection.compareDocumentPosition(ownedNode) & + Node.DOCUMENT_POSITION_FOLLOWING, + ).toBeTruthy(); + }); +}); + describe("ChatsSidebar filters", () => { it("calls the sidebar filter change callback after Apply is clicked", async () => { const user = userEvent.setup(); diff --git a/site/src/pages/AgentsPage/components/ChatsSidebar/ChatsSidebar.tsx b/site/src/pages/AgentsPage/components/ChatsSidebar/ChatsSidebar.tsx index 042c1417c0..07cd483b44 100644 --- a/site/src/pages/AgentsPage/components/ChatsSidebar/ChatsSidebar.tsx +++ b/site/src/pages/AgentsPage/components/ChatsSidebar/ChatsSidebar.tsx @@ -43,6 +43,7 @@ interface ChatsSidebarProps { onCollapse?: () => void; isPersonalModelOverridesEnabled?: boolean; isAdmin?: boolean; + currentUserId: string; } export const ChatsSidebar: FC = (props) => { @@ -77,6 +78,7 @@ export const ChatsSidebar: FC = (props) => { onCollapse, isPersonalModelOverridesEnabled = false, isAdmin = false, + currentUserId, } = props; const { agentId, chatId } = useParams<{ agentId?: string; @@ -136,6 +138,7 @@ export const ChatsSidebar: FC = (props) => { isSettingsPanel={isSettingsPanel} isChatsActive={!activeChatId && sidebarView.panel === "chats"} location={location} + currentUserId={currentUserId} /> = ({ @@ -131,6 +133,7 @@ export const ChatsPanel: FC = ({ isSettingsPanel, isChatsActive, location, + currentUserId, }) => { const locationSearch = normalizeLocationSearch(location.search); const [expandedById, setExpandedById] = useState>({}); @@ -156,6 +159,12 @@ export const ChatsPanel: FC = ({ const unpinnedChats = visibleRootIDs .map((id) => chatById.get(id)) .filter((chat): chat is Chat => chat !== undefined && chat.pin_order === 0); + const sharedWithYouChats = unpinnedChats.filter( + (chat) => chat.shared && chat.owner_id !== currentUserId, + ); + const unpinnedOwnedChats = unpinnedChats.filter( + (chat) => !chat.shared || chat.owner_id === currentUserId, + ); const hasAppliedResultFilters = sidebarFilters.prStatuses.length > 0 || sidebarFilters.chatStatuses.length !== AGENT_CHAT_STATUS_ORDER.length || @@ -312,18 +321,18 @@ export const ChatsPanel: FC = ({ { key: UNREAD_SECTION_KEY, label: UNREAD_SECTION_KEY, - chats: unpinnedChats.filter((chat) => chat.has_unread), + chats: unpinnedOwnedChats.filter((chat) => chat.has_unread), }, { key: READ_SECTION_KEY, label: READ_SECTION_KEY, - chats: unpinnedChats.filter((chat) => !chat.has_unread), + chats: unpinnedOwnedChats.filter((chat) => !chat.has_unread), }, ] : TIME_GROUPS.map((group) => ({ key: group, label: group, - chats: unpinnedChats.filter( + chats: unpinnedOwnedChats.filter( (chat) => getTimeGroup(chat.updated_at) === group, ), })) @@ -559,6 +568,34 @@ export const ChatsPanel: FC = ({ ))} )} + {sharedWithYouChats.length > 0 && ( +
+ + toggleSection(SHARED_WITH_YOU_SECTION_KEY) + } + testId={getSectionToggleTestId( + SHARED_WITH_YOU_SECTION_KEY, + )} + /> + {!collapsedSections[SHARED_WITH_YOU_SECTION_KEY] && ( +
+ {sharedWithYouChats.map((chat) => ( + + ))} +
+ )} +
+ )} {chatSections.map((section) => { const isSectionExpanded = !collapsedSections[section.key];