feat(web): persist collapsed workspace groups to localStorage (#1045)

This commit is contained in:
qer 2026-06-23 22:45:24 +08:00 committed by GitHub
parent c240bfab7d
commit ac1882fe28
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 50 additions and 1 deletions

View file

@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---
Persist the collapsed state of workspace groups in the web sidebar across page reloads.

View file

@ -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<Set<string>>(new Set());
const collapsedIds = ref<Set<string>>(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);
}
// ---------------------------------------------------------------------------

View file

@ -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<string, boolean>): 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<unknown>(STORAGE_KEYS.collapsedWorkspaces);
if (!Array.isArray(parsed)) return [];
return parsed.filter((id): id is string => typeof id === 'string');
}
export function saveCollapsedWorkspaces(ids: Iterable<string>): void {
safeSetJson(STORAGE_KEYS.collapsedWorkspaces, Array.from(ids));
}

View file

@ -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([]);
});
});