mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-10-07 13:18:19 +08:00
fix: always show all workspace folders in grouped view regardless of pagination (#16444)
This commit is contained in:
+86
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string, LocalWorkspace>();
|
||||
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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user