mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(site): group shared agents in sidebar (#26328)
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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];
|
||||
|
||||
Reference in New Issue
Block a user