Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/web-persist-workspace-collapse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Persist the collapsed state of workspace groups in the web sidebar across page reloads.
4 changes: 3 additions & 1 deletion apps/kimi-web/src/components/Sidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand All @@ -107,6 +108,7 @@ function toggleCollapse(id: string): void {
next.add(id);
}
collapsedIds.value = next;
saveCollapsedWorkspaces(next);
}

// ---------------------------------------------------------------------------
Expand Down
16 changes: 16 additions & 0 deletions apps/kimi-web/src/lib/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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));
}
26 changes: 26 additions & 0 deletions apps/kimi-web/test/storage-logic.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import {
loadCollapsedWorkspaces,
loadUnread,
saveCollapsedWorkspaces,
saveUnread,
STORAGE_KEYS,
draftStorageKey,
Expand Down Expand Up @@ -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([]);
});
});
Loading