diff --git a/site/src/pages/AgentsPage/AgentChatPage.test.ts b/site/src/pages/AgentsPage/AgentChatPage.test.ts index 83de6462bd..e19fc7a3bf 100644 --- a/site/src/pages/AgentsPage/AgentChatPage.test.ts +++ b/site/src/pages/AgentsPage/AgentChatPage.test.ts @@ -1,11 +1,9 @@ import { act, renderHook } from "@testing-library/react"; import { createRef } from "react"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import type * as TypesGen from "#/api/typesGenerated"; import { clearPersistedSidebarTabId, draftInputStorageKeyPrefix, - filterWorkspaceOptionsByOrganization, getPersistedDraftInputValue, getPersistedSidebarTabId, lastActiveSidebarTabStorageKeyPrefix, @@ -128,32 +126,6 @@ describe("waitForPendingChatSettingsSyncs", () => { }); }); -describe("filterWorkspaceOptionsByOrganization", () => { - const makeWorkspace = (id: string, organizationID: string) => - ({ id, organization_id: organizationID }) as TypesGen.Workspace; - - it("returns only workspaces from the active chat organization", () => { - const workspaces = [ - makeWorkspace("workspace-1", "org-a"), - makeWorkspace("workspace-2", "org-b"), - makeWorkspace("workspace-3", "org-a"), - ]; - - expect(filterWorkspaceOptionsByOrganization(workspaces, "org-a")).toEqual([ - workspaces[0], - workspaces[2], - ]); - }); - - it("returns an empty list until the chat organization is known", () => { - const workspaces = [makeWorkspace("workspace-1", "org-a")]; - - expect(filterWorkspaceOptionsByOrganization(workspaces, undefined)).toEqual( - [], - ); - }); -}); - describe("getPersistedDraftInputValue", () => { const chatID = "chat-abc-123"; const expectedKey = `${draftInputStorageKeyPrefix}${chatID}`; diff --git a/site/src/pages/AgentsPage/AgentChatPage.tsx b/site/src/pages/AgentsPage/AgentChatPage.tsx index abe3d5f28c..ea48eb389a 100644 --- a/site/src/pages/AgentsPage/AgentChatPage.tsx +++ b/site/src/pages/AgentsPage/AgentChatPage.tsx @@ -710,10 +710,7 @@ const AgentChatPage: FC = () => { const userDebugLoggingQuery = useQuery(userChatDebugLogging()); const mcpServersQuery = useQuery(mcpServerConfigs()); const workspacesQuery = useQuery(workspaces({ q: "owner:me", limit: 0 })); - const workspaceOptions = filterWorkspaceOptionsByOrganization( - workspacesQuery.data?.workspaces ?? [], - chatQuery.data?.organization_id, - ); + const workspaceOptions = workspacesQuery.data?.workspaces ?? []; const desktopEnabled = desktopEnabledQuery.data?.enable_desktop ?? false; const debugLoggingEnabled = userDebugLoggingQuery.data?.debug_logging_enabled ?? false; diff --git a/site/src/pages/AgentsPage/AgentChatPageView.tsx b/site/src/pages/AgentsPage/AgentChatPageView.tsx index ad7dec71d1..30f5634578 100644 --- a/site/src/pages/AgentsPage/AgentChatPageView.tsx +++ b/site/src/pages/AgentsPage/AgentChatPageView.tsx @@ -541,6 +541,7 @@ export const AgentChatPageView: FC = ({ onPlanModeToggle={onPlanModeToggle} isModelCatalogLoading={isModelCatalogLoading} workspaceOptions={workspaceOptions} + chatOrganizationId={organizationId} selectedWorkspaceId={selectedWorkspaceId} onWorkspaceChange={onWorkspaceChange} isWorkspaceLoading={isWorkspaceLoading} diff --git a/site/src/pages/AgentsPage/components/AgentChatInput.stories.tsx b/site/src/pages/AgentsPage/components/AgentChatInput.stories.tsx index c9a936f1df..792530c4a6 100644 --- a/site/src/pages/AgentsPage/components/AgentChatInput.stories.tsx +++ b/site/src/pages/AgentsPage/components/AgentChatInput.stories.tsx @@ -720,6 +720,7 @@ export const DetailPageWorkspacePicker: Story = { id: "ws-detail", name: "agents-workspace", owner_name: "mike", + organization_id: "org-1", }, ], selectedWorkspaceId: "ws-detail", @@ -810,7 +811,12 @@ export const OverflowBadges: Story = { pagerdutyMCP.id, ], workspaceOptions: [ - { id: "ws-1", name: "my-long-workspace-name", owner_name: "admin" }, + { + id: "ws-1", + name: "my-long-workspace-name", + owner_name: "admin", + organization_id: "org-1", + }, ], selectedWorkspaceId: "ws-1", onWorkspaceChange: fn(), diff --git a/site/src/pages/AgentsPage/components/AgentChatInput.tsx b/site/src/pages/AgentsPage/components/AgentChatInput.tsx index a6c1721a94..0876eb0068 100644 --- a/site/src/pages/AgentsPage/components/AgentChatInput.tsx +++ b/site/src/pages/AgentsPage/components/AgentChatInput.tsx @@ -122,9 +122,13 @@ interface AgentChatInputProps { id: string; name: string; owner_name: string; + organization_id: string; }>; selectedWorkspaceId?: string | null; onWorkspaceChange?: (id: string | null) => void; + // Organization ID of the current chat. When set, workspaces from + // other organizations are shown as disabled in the picker. + chatOrganizationId?: string; isWorkspaceLoading?: boolean; // Queued user messages rendered above the textarea. queuedMessages?: readonly ChatQueuedMessage[]; @@ -298,6 +302,7 @@ export const AgentChatInput: FC = ({ workspaceOptions, selectedWorkspaceId, onWorkspaceChange, + chatOrganizationId, isWorkspaceLoading, queuedMessages = [], onDeleteQueuedMessage, @@ -882,35 +887,15 @@ export const AgentChatInput: FC = ({ Back - - - - - No workspaces found - - - {workspaceOptions?.map((workspace) => ( - { - onWorkspaceChange?.(workspace.id); - setPlusMenuOpen(false); - }} - > - {workspace.name} - {selectedWorkspaceId === workspace.id && ( - - )} - - ))} - - - + { + onWorkspaceChange?.(id); + setPlusMenuOpen(false); + }} + /> ) : ( <> @@ -983,36 +968,16 @@ export const AgentChatInput: FC = ({ sideOffset={8} className="w-64 p-0" > - - - - - No workspaces found - - - {workspaceOptions.map((workspace) => ( - { - onWorkspaceChange(workspace.id); - setWorkspacePickerOpen(false); - setPlusMenuOpen(false); - }} - > - {workspace.name} - {selectedWorkspaceId === workspace.id && ( - - )} - - ))} - - - + { + onWorkspaceChange(id); + setWorkspacePickerOpen(false); + setPlusMenuOpen(false); + }} + /> ))} @@ -1283,3 +1248,82 @@ export const AgentChatInput: FC = ({ ); }; + +/** + * Shared workspace picker used by both the mobile and desktop + * "Attach workspace" menus. Workspaces from a different organization + * than the chat are rendered as disabled items with a tooltip. + */ +interface WorkspacePickerListProps { + workspaceOptions: + | ReadonlyArray<{ + id: string; + name: string; + organization_id: string; + }> + | undefined; + selectedWorkspaceId?: string | null; + chatOrganizationId?: string; + onSelect: (id: string) => void; +} + +const WorkspacePickerList: FC = ({ + workspaceOptions, + selectedWorkspaceId, + chatOrganizationId, + onSelect, +}) => { + return ( + + + + No workspaces found + + {workspaceOptions?.map((workspace) => { + const isCrossOrg = + !!chatOrganizationId && + workspace.organization_id !== chatOrganizationId; + + const item = ( + { + if (!isCrossOrg) { + onSelect(workspace.id); + } + }} + > + {workspace.name} + {selectedWorkspaceId === workspace.id && ( + + )} + + ); + + if (isCrossOrg) { + return ( + + +
{item}
+
+ + Chat and workspace must be in the same organization + +
+ ); + } + + return item; + })} +
+
+
+ ); +}; diff --git a/site/src/pages/AgentsPage/components/ChatPageContent.tsx b/site/src/pages/AgentsPage/components/ChatPageContent.tsx index 3fe58140d0..840833c4e9 100644 --- a/site/src/pages/AgentsPage/components/ChatPageContent.tsx +++ b/site/src/pages/AgentsPage/components/ChatPageContent.tsx @@ -201,6 +201,7 @@ interface ChatPageInputProps { onMCPAuthComplete?: (serverId: string) => void; lastInjectedContext?: readonly TypesGen.ChatMessagePart[]; workspaceOptions: readonly TypesGen.Workspace[]; + chatOrganizationId?: string; selectedWorkspaceId: string | null; onWorkspaceChange: (workspaceId: string | null) => void; isWorkspaceLoading: boolean; @@ -252,6 +253,7 @@ export const ChatPageInput: FC = ({ onMCPAuthComplete, lastInjectedContext, workspaceOptions, + chatOrganizationId, selectedWorkspaceId, onWorkspaceChange, isWorkspaceLoading, @@ -481,6 +483,7 @@ export const ChatPageInput: FC = ({ onPlanModeToggle={onPlanModeToggle} isModelCatalogLoading={isModelCatalogLoading} workspaceOptions={workspaceOptions} + chatOrganizationId={chatOrganizationId} selectedWorkspaceId={selectedWorkspaceId} onWorkspaceChange={onWorkspaceChange} isWorkspaceLoading={isWorkspaceLoading}