From 2ed792bc77de2b1ef00efff68600e0ec74a3a4c0 Mon Sep 17 00:00:00 2001 From: Mohit Singh <93594454+mohitdebian@users.noreply.github.com> Date: Tue, 18 Aug 2026 03:26:55 +0530 Subject: [PATCH] fix: always show all workspace folders in grouped view regardless of pagination (#16444) --- .../conversation-panel-list-helpers.test.ts | 86 +++++++++++++++++++ .../conversation-panel-list-helpers.ts | 11 +++ .../conversation-panel/conversation-panel.tsx | 53 +++++++++++- 3 files changed, 149 insertions(+), 1 deletion(-) diff --git a/__tests__/components/features/conversation-panel/conversation-panel-list-helpers.test.ts b/__tests__/components/features/conversation-panel/conversation-panel-list-helpers.test.ts index ad2fe58aa7..2c18c9c316 100644 --- a/__tests__/components/features/conversation-panel/conversation-panel-list-helpers.test.ts +++ b/__tests__/components/features/conversation-panel/conversation-panel-list-helpers.test.ts @@ -628,4 +628,90 @@ describe("conversation-panel-list-helpers", () => { ).map((c) => c.id), ).toEqual(["audit", "unnamed"]); }); + + it("pre-seeds workspace groups from knownWorkspaces even when no conversations are loaded for them", () => { + const knownWorkspaces = [ + { id: "/workspace/alpha", name: "alpha", path: "/workspace/alpha" }, + { id: "/workspace/beta", name: "beta", path: "/workspace/beta" }, + ]; + const groups = groupConversations( + [], + "local", + "updated", + { emptyWorkspace: "No workspace", emptyRepository: "No repository" }, + knownWorkspaces, + ); + expect(groups.map((g) => ({ id: g.id, label: g.label }))).toEqual([ + { id: "ws:/workspace/alpha", label: "alpha" }, + { id: "ws:/workspace/beta", label: "beta" }, + ]); + expect(groups.every((g) => g.conversations.length === 0)).toBe(true); + }); + + it("merges known workspaces with conversations from paginated pages into one unified group list", () => { + const knownWorkspaces = [ + { id: "/workspace/alpha", name: "alpha", path: "/workspace/alpha" }, + ]; + const pageTwoConversation: AppConversation = { + ...base, + id: "deep", + title: "deep", + selected_workspace: "/workspace/beta", + updated_at: "2024-01-05T00:00:00.000Z", + }; + const groups = groupConversations( + [pageTwoConversation], + "local", + "updated", + { emptyWorkspace: "No workspace", emptyRepository: "No repository" }, + knownWorkspaces, + ); + const ids = groups.map((g) => g.id); + expect(ids).toContain("ws:/workspace/alpha"); + expect(ids).toContain("ws:/workspace/beta"); + const alpha = groups.find((g) => g.id === "ws:/workspace/alpha"); + expect(alpha?.conversations).toHaveLength(0); + const beta = groups.find((g) => g.id === "ws:/workspace/beta"); + expect(beta?.conversations.map((c) => c.id)).toEqual(["deep"]); + }); + + it("uses the known workspace name for a group whose path is in knownWorkspaces", () => { + const knownWorkspaces = [ + { + id: "/workspace/my-project", + name: "My Project", + path: "/workspace/my-project", + }, + ]; + const convo: AppConversation = { + ...base, + id: "c1", + title: "c1", + selected_workspace: "/workspace/my-project", + updated_at: "2024-01-02T00:00:00.000Z", + }; + const groups = groupConversations( + [convo], + "local", + "updated", + { emptyWorkspace: "No workspace", emptyRepository: "No repository" }, + knownWorkspaces, + ); + const group = groups.find((g) => g.id === "ws:/workspace/my-project"); + expect(group?.label).toBe("My Project"); + }); + + it("ignores knownWorkspaces for cloud backend grouping", () => { + const knownWorkspaces = [ + { id: "/workspace/alpha", name: "alpha", path: "/workspace/alpha" }, + ]; + const groups = groupConversations( + [], + "cloud", + "updated", + { emptyWorkspace: "No workspace", emptyRepository: "No repository" }, + knownWorkspaces, + ); + expect(groups).toHaveLength(0); + }); }); diff --git a/src/components/features/conversation-panel/conversation-panel-list-helpers.ts b/src/components/features/conversation-panel/conversation-panel-list-helpers.ts index b2bc2a61dd..0efba6e7dc 100644 --- a/src/components/features/conversation-panel/conversation-panel-list-helpers.ts +++ b/src/components/features/conversation-panel/conversation-panel-list-helpers.ts @@ -1,5 +1,6 @@ import type { AppConversation } from "#/api/conversation-service/agent-server-conversation-service.types"; import type { BackendKind } from "#/api/backend-registry/types"; +import type { LocalWorkspace } from "#/types/workspace"; import type { Provider } from "#/types/settings"; import { AUTOMATION_NAME_TAG_KEY, @@ -419,6 +420,7 @@ export function groupConversations( backendKind: BackendKind, sortField: ConversationSortField, labels: { emptyWorkspace: string; emptyRepository: string }, + knownWorkspaces?: readonly LocalWorkspace[], ): { id: string; label: string; @@ -430,6 +432,15 @@ export function groupConversations( { label: string; conversations: AppConversation[] } >(); + if (backendKind === "local" && knownWorkspaces) { + for (const ws of knownWorkspaces) { + const normalized = ws.path.trim().replace(/\/+$/, ""); + if (normalized) { + byId.set(`ws:${normalized}`, { label: ws.name, conversations: [] }); + } + } + } + for (const c of items) { const { id, label: rawLabel } = getConversationGroupIdentity( c, diff --git a/src/components/features/conversation-panel/conversation-panel.tsx b/src/components/features/conversation-panel/conversation-panel.tsx index 61889311b3..878118930f 100644 --- a/src/components/features/conversation-panel/conversation-panel.tsx +++ b/src/components/features/conversation-panel/conversation-panel.tsx @@ -6,6 +6,7 @@ import { useNavigation } from "#/context/navigation-context"; import { useActiveBackend } from "#/contexts/active-backend-context"; import { useBackendScopedPath } from "#/hooks/use-backend-scoped-path"; import { usePaginatedConversations } from "#/hooks/query/use-paginated-conversations"; +import { useResolvedWorkspaces } from "#/hooks/query/use-resolved-workspaces"; import { useStartTasks } from "#/hooks/query/use-start-tasks"; import { useDeleteConversation } from "#/hooks/mutation/use-delete-conversation"; import { useUnifiedPauseConversation } from "#/hooks/mutation/use-unified-stop-conversation"; @@ -16,6 +17,7 @@ import { NavigationLink } from "#/components/shared/navigation-link"; import { ExitConversationModal } from "./exit-conversation-modal"; import { useClickOutsideElement } from "#/hooks/use-click-outside-element"; import { Provider } from "#/types/settings"; +import type { LocalWorkspace } from "#/types/workspace"; import { useUpdateConversation } from "#/hooks/mutation/use-update-conversation"; import { displayErrorToast, @@ -278,6 +280,7 @@ export function ConversationPanel({ isFetchingNextPage, fetchNextPage, } = usePaginatedConversations(); + const { workspaces: knownWorkspaces } = useResolvedWorkspaces(); // Fetch in-progress start tasks const { data: startTasks } = useStartTasks(); @@ -342,6 +345,46 @@ export function ConversationPanel({ [conversations], ); + const allWorkspacesForGrouping = React.useMemo< + readonly LocalWorkspace[] + >(() => { + if ( + compact || + organizeMode !== "grouped" || + activeBackend.kind !== "local" + ) { + return []; + } + const normalize = (p: string) => p.trim().replace(/\/+$/, ""); + const byPath = new Map(); + for (const ws of knownWorkspaces) { + const key = normalize(ws.path); + if (key) { + byPath.set(key, ws); + } + } + for (const c of conversations) { + const normalized = c.selected_workspace + ? normalize(c.selected_workspace) + : ""; + if (normalized && !byPath.has(normalized)) { + const label = normalized.split("/").filter(Boolean).pop() ?? normalized; + byPath.set(normalized, { + id: normalized, + name: label, + path: normalized, + }); + } + } + return Array.from(byPath.values()); + }, [ + activeBackend.kind, + compact, + conversations, + knownWorkspaces, + organizeMode, + ]); + const automationFilteredConversations = React.useMemo( () => applyAutomationConversationFilter( @@ -455,12 +498,14 @@ export function ConversationPanel({ activeBackend.kind, conversationSort, groupLabels, + allWorkspacesForGrouping, ); }, [ activeBackend.kind, conversationSort, groupLabels, groupedSourceConversations, + allWorkspacesForGrouping, ]); const groupDiscoveryConversationIds = React.useMemo(() => { @@ -957,13 +1002,19 @@ export function ConversationPanel({ const showInitialSkeleton = isLoading || !isFetched; const showPinnedSection = !compact && !showInitialSkeleton && pinnedConversations.length > 0; + const hasVisibleGroups = + organizeMode === "grouped" && + !compact && + orderedConversationGroups != null && + orderedConversationGroups.length > 0; const showEmptyState = isFetched && !isLoading && !compact && listIsEffectivelyEmpty && !showPinnedSection && - !startTasks?.length; + !startTasks?.length && + !hasVisibleGroups; const showConversationHeader = !compact;