feat(site): group shared agents in sidebar (#26328)

This commit is contained in:
Danielle Maywood
2026-06-12 12:39:53 +01:00
committed by GitHub
parent 4debd23cbb
commit 79a28bad72
8 changed files with 125 additions and 6 deletions
+3 -3
View File
@@ -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
+1
View File
@@ -679,6 +679,7 @@ const AgentsPage: FC = () => {
<AgentsPageView
agentId={agentId}
chatList={chatList}
currentUserId={user.id}
catalogModelOptions={catalogModelOptions}
modelConfigs={chatModelConfigsQuery.data ?? []}
handleNewAgent={handleNewAgent}
@@ -316,6 +316,7 @@ const agentsWithChatTopBarRouting = {
const defaultArgs: ComponentProps<typeof AgentsPageView> = {
agentId: undefined,
chatList: [],
currentUserId: MockUserOwner.id,
catalogModelOptions: defaultModelOptions,
modelConfigs: defaultModelConfigs,
handleNewAgent: fn(),
@@ -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<AgentsPageViewProps> = ({
agentId,
chatList,
currentUserId,
catalogModelOptions,
modelConfigs,
handleNewAgent,
@@ -180,6 +182,7 @@ export const AgentsPageView: FC<AgentsPageViewProps> = ({
>
<ChatsSidebar
chats={chatList}
currentUserId={currentUserId}
chatErrorReasons={sidebarChatErrorReasons}
modelOptions={catalogModelOptions}
modelConfigs={modelConfigs}
@@ -117,6 +117,7 @@ const meta: Meta<typeof ChatsSidebar> = {
onSearchDialogOpenChange: fn(),
isCreating: false,
regeneratingTitleChatIds: [],
currentUserId: MockUserOwner.id,
sidebarFilters: defaultSidebarFilters,
isPersonalModelOverridesEnabled: true,
onSidebarFiltersChange: fn(),
@@ -134,10 +134,83 @@ const defaultProps: React.ComponentProps<typeof ChatsSidebar> = {
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(
<Wrapper>
<ChatsSidebar
{...defaultProps}
chats={[
buildChat({
id: "pinned-shared-chat",
title: "Pinned shared chat",
shared: true,
pin_order: 1,
}),
buildChat({
id: "shared-chat",
title: "Shared chat",
owner_id: "sharing-user-id",
shared: true,
}),
buildChat({
id: "owned-shared-chat",
title: "Owned shared chat",
shared: true,
updated_at: new Date().toISOString(),
}),
buildChat({
id: "owned-chat",
title: "Owned chat",
updated_at: new Date().toISOString(),
}),
]}
/>
</Wrapper>,
);
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();
@@ -43,6 +43,7 @@ interface ChatsSidebarProps {
onCollapse?: () => void;
isPersonalModelOverridesEnabled?: boolean;
isAdmin?: boolean;
currentUserId: string;
}
export const ChatsSidebar: FC<ChatsSidebarProps> = (props) => {
@@ -77,6 +78,7 @@ export const ChatsSidebar: FC<ChatsSidebarProps> = (props) => {
onCollapse,
isPersonalModelOverridesEnabled = false,
isAdmin = false,
currentUserId,
} = props;
const { agentId, chatId } = useParams<{
agentId?: string;
@@ -136,6 +138,7 @@ export const ChatsSidebar: FC<ChatsSidebarProps> = (props) => {
isSettingsPanel={isSettingsPanel}
isChatsActive={!activeChatId && sidebarView.panel === "chats"}
location={location}
currentUserId={currentUserId}
/>
<SettingsPanel
isSettingsPanel={isSettingsPanel}
@@ -63,6 +63,7 @@ import { UserSidebarFooter } from "./UserSidebarFooter";
const UNREAD_SECTION_KEY = "Unread";
const READ_SECTION_KEY = "Read";
const SHARED_WITH_YOU_SECTION_KEY = "Shared with you";
interface ChatsPanelProps {
readonly chats: readonly Chat[];
@@ -98,6 +99,7 @@ interface ChatsPanelProps {
readonly isSettingsPanel: boolean;
readonly isChatsActive: boolean;
readonly location: Location;
readonly currentUserId: string;
}
export const ChatsPanel: FC<ChatsPanelProps> = ({
@@ -131,6 +133,7 @@ export const ChatsPanel: FC<ChatsPanelProps> = ({
isSettingsPanel,
isChatsActive,
location,
currentUserId,
}) => {
const locationSearch = normalizeLocationSearch(location.search);
const [expandedById, setExpandedById] = useState<Record<string, boolean>>({});
@@ -156,6 +159,12 @@ export const ChatsPanel: FC<ChatsPanelProps> = ({
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<ChatsPanelProps> = ({
{
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<ChatsPanelProps> = ({
))}
</div>
)}
{sharedWithYouChats.length > 0 && (
<div className="[&:not(:first-child)]:mt-3">
<ChatSectionHeader
label={SHARED_WITH_YOU_SECTION_KEY}
count={sharedWithYouChats.length}
expanded={
!collapsedSections[SHARED_WITH_YOU_SECTION_KEY]
}
onToggle={() =>
toggleSection(SHARED_WITH_YOU_SECTION_KEY)
}
testId={getSectionToggleTestId(
SHARED_WITH_YOU_SECTION_KEY,
)}
/>
{!collapsedSections[SHARED_WITH_YOU_SECTION_KEY] && (
<div className="flex flex-col gap-0.5">
{sharedWithYouChats.map((chat) => (
<ChatTreeNode
key={chat.id}
chat={chat}
isChildNode={false}
/>
))}
</div>
)}
</div>
)}
{chatSections.map((section) => {
const isSectionExpanded =
!collapsedSections[section.key];