From ac1882fe28c906904ffaacd8434bb20e689d6677 Mon Sep 17 00:00:00 2001 From: qer Date: Tue, 23 Jun 2026 22:45:24 +0800 Subject: [PATCH] feat(web): persist collapsed workspace groups to localStorage (#1045) --- .changeset/web-persist-workspace-collapse.md | 5 ++++ apps/kimi-web/src/components/Sidebar.vue | 4 ++- apps/kimi-web/src/lib/storage.ts | 16 ++++++++++++ apps/kimi-web/test/storage-logic.test.ts | 26 ++++++++++++++++++++ 4 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 .changeset/web-persist-workspace-collapse.md diff --git a/.changeset/web-persist-workspace-collapse.md b/.changeset/web-persist-workspace-collapse.md new file mode 100644 index 000000000..19b6b01e4 --- /dev/null +++ b/.changeset/web-persist-workspace-collapse.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Persist the collapsed state of workspace groups in the web sidebar across page reloads. diff --git a/apps/kimi-web/src/components/Sidebar.vue b/apps/kimi-web/src/components/Sidebar.vue index ba1ab27e7..65619c682 100644 --- a/apps/kimi-web/src/components/Sidebar.vue +++ b/apps/kimi-web/src/components/Sidebar.vue @@ -7,6 +7,7 @@ import { computed, nextTick, onBeforeUnmount, ref } from 'vue'; import { useI18n } from 'vue-i18n'; import { serverEndpointLabel } from '../api/config'; import { copyTextToClipboard } from '../lib/clipboard'; +import { loadCollapsedWorkspaces, saveCollapsedWorkspaces } from '../lib/storage'; import type { Session, WorkspaceGroup as WorkspaceGroupType, WorkspaceView } from '../types'; import SessionRow from './SessionRow.vue'; import WorkspaceGroup from './WorkspaceGroup.vue'; @@ -89,7 +90,7 @@ function onSelectResult(sessionId: string): void { // --------------------------------------------------------------------------- // Collapse groups // --------------------------------------------------------------------------- -const collapsedIds = ref>(new Set()); +const collapsedIds = ref>(new Set(loadCollapsedWorkspaces())); function isCollapsed(id: string): boolean { return collapsedIds.value.has(id); @@ -107,6 +108,7 @@ function toggleCollapse(id: string): void { next.add(id); } collapsedIds.value = next; + saveCollapsedWorkspaces(next); } // --------------------------------------------------------------------------- diff --git a/apps/kimi-web/src/lib/storage.ts b/apps/kimi-web/src/lib/storage.ts index d1b5d5ca2..f83c86ef7 100644 --- a/apps/kimi-web/src/lib/storage.ts +++ b/apps/kimi-web/src/lib/storage.ts @@ -22,6 +22,7 @@ export const STORAGE_KEYS = { accent: 'kimi-web.accent', colorScheme: 'kimi-web.color-scheme', hiddenWorkspaces: 'kimi-web.hidden-workspaces', + collapsedWorkspaces: 'kimi-web.collapsed-workspaces', betaToc: 'kimi-web.beta-toc', notifyOnComplete: 'kimi-web.notify-on-complete', inputHistory: 'kimi-web.input-history', @@ -122,3 +123,18 @@ export function saveUnread(changes: Record): void { } safeSetString(STORAGE_KEYS.unread, JSON.stringify(merged)); } + +/** + * Collapsed workspace ids in the sidebar. Persisted as a JSON array of ids so + * the fold state of each workspace group survives a page refresh. There is no + * server-side source of truth for this UI-only state. + */ +export function loadCollapsedWorkspaces(): string[] { + const parsed = safeGetJson(STORAGE_KEYS.collapsedWorkspaces); + if (!Array.isArray(parsed)) return []; + return parsed.filter((id): id is string => typeof id === 'string'); +} + +export function saveCollapsedWorkspaces(ids: Iterable): void { + safeSetJson(STORAGE_KEYS.collapsedWorkspaces, Array.from(ids)); +} diff --git a/apps/kimi-web/test/storage-logic.test.ts b/apps/kimi-web/test/storage-logic.test.ts index 1983447ce..79284a3b1 100644 --- a/apps/kimi-web/test/storage-logic.test.ts +++ b/apps/kimi-web/test/storage-logic.test.ts @@ -1,6 +1,8 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { + loadCollapsedWorkspaces, loadUnread, + saveCollapsedWorkspaces, saveUnread, STORAGE_KEYS, draftStorageKey, @@ -169,3 +171,27 @@ describe('loadUnread / saveUnread', () => { expect(loadUnread()).toEqual({ C: true, D: true }); }); }); + +describe('loadCollapsedWorkspaces / saveCollapsedWorkspaces', () => { + it('returns an empty array when the key is missing', () => { + expect(loadCollapsedWorkspaces()).toEqual([]); + }); + + it('round-trips the collapsed ids', () => { + saveCollapsedWorkspaces(['ws-1', 'ws-2']); + expect(loadCollapsedWorkspaces()).toEqual(['ws-1', 'ws-2']); + }); + + it('accepts any iterable of ids', () => { + saveCollapsedWorkspaces(new Set(['ws-1', 'ws-3'])); + expect(loadCollapsedWorkspaces()).toEqual(['ws-1', 'ws-3']); + }); + + it('drops non-string entries and returns [] for malformed values', () => { + safeSetString(STORAGE_KEYS.collapsedWorkspaces, JSON.stringify(['ws-1', 2, null, 'ws-2'])); + expect(loadCollapsedWorkspaces()).toEqual(['ws-1', 'ws-2']); + + safeSetString(STORAGE_KEYS.collapsedWorkspaces, JSON.stringify({ ws: true })); + expect(loadCollapsedWorkspaces()).toEqual([]); + }); +});