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
26 changes: 26 additions & 0 deletions apps/desktop/e2e/sidebar-project-row.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,29 @@ test('project navigation and actions remain adjacent keyboard controls', async (
await page.getByRole('button', { name: '关闭', exact: true }).click();
await expect(action).toBeFocused();
});

test('rail grouping survives a renderer reload', async ({ projectSidebarWindow: page }) => {
await page.keyboard.press('Escape');
await expect(page.locator('[data-maka-contract="search-modal"]')).not.toBeVisible();

const sidebar = page.getByRole('navigation', { name: '任务列表' });
const byTime = sidebar.getByRole('radio', { name: '按时间', exact: true });
const byProject = sidebar.getByRole('radio', { name: '按项目', exact: true });

await expect(byTime).toBeChecked();
await byProject.click();
await expect(byProject).toBeChecked();
await expect
.poll(() => page.evaluate(() => localStorage.getItem('maka-chat-list-view-mode-v1')))
.toBe('project');

await page.reload();
await expect(page.locator('[data-maka-contract="search-modal"]')).toBeVisible();
await page.keyboard.press('Escape');
await expect(page.locator('[data-maka-contract="search-modal"]')).not.toBeVisible();

await expect(sidebar.getByRole('radio', { name: '按项目', exact: true })).toBeChecked();
await expect
.poll(() => page.evaluate(() => localStorage.getItem('maka-chat-list-view-mode-v1')))
.toBe('project');
});
79 changes: 79 additions & 0 deletions apps/desktop/src/main/__tests__/session-list-layout.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import assert from 'node:assert/strict';
import { afterEach, describe, it } from 'node:test';
import {
readSessionListViewMode,
writeSessionListViewMode,
} from '../../renderer/session-list-layout.js';

const VIEW_MODE_KEY = 'maka-chat-list-view-mode-v1';

function installMemoryLocalStorage(initial: Record<string, string> = {}) {
const store = new Map<string, string>(Object.entries(initial));
const previous = Object.getOwnPropertyDescriptor(globalThis, 'localStorage');
const memory: Storage = {
get length() {
return store.size;
},
clear() {
store.clear();
},
getItem(key) {
return store.has(key) ? store.get(key)! : null;
},
key(index) {
return [...store.keys()][index] ?? null;
},
removeItem(key) {
store.delete(key);
},
setItem(key, value) {
store.set(key, String(value));
},
};
Object.defineProperty(globalThis, 'localStorage', {
configurable: true,
writable: true,
value: memory,
});
return {
store,
restore() {
if (previous) Object.defineProperty(globalThis, 'localStorage', previous);
else Reflect.deleteProperty(globalThis, 'localStorage');
},
};
}

describe('session list view mode persistence', () => {
const cleanups: Array<() => void> = [];
afterEach(() => {
while (cleanups.length > 0) cleanups.pop()?.();
});

it('defaults to conversation when nothing is stored', () => {
cleanups.push(installMemoryLocalStorage().restore);
assert.equal(readSessionListViewMode(), 'conversation');
});

it('round-trips a project grouping through the same key the shell hydrates', () => {
const memory = installMemoryLocalStorage();
cleanups.push(memory.restore);
writeSessionListViewMode('project');
assert.equal(memory.store.get(VIEW_MODE_KEY), 'project');
assert.equal(readSessionListViewMode(), 'project');
});

it('keeps conversation when that is what was written', () => {
cleanups.push(installMemoryLocalStorage({ [VIEW_MODE_KEY]: 'project' }).restore);
writeSessionListViewMode('conversation');
assert.equal(readSessionListViewMode(), 'conversation');
});

it('fails open to conversation for garbage or empty stored values', () => {
for (const stored of ['', 'time', 'true', 'PROJECT', 'conversation\n']) {
const memory = installMemoryLocalStorage({ [VIEW_MODE_KEY]: stored });
assert.equal(readSessionListViewMode(), 'conversation', stored);
memory.restore();
}
});
});
8 changes: 7 additions & 1 deletion apps/desktop/src/renderer/app-shell-effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ import type { UiLocale } from '@maka/core/ui-locale';
import { generalizedErrorMessageChinese } from '@maka/core/redaction';
import { sessionExpectsEventStream } from '@maka/core/session-event-health';
import { type ShellRunUpdate } from '@maka/core/events';
import type { LiveTurnProjection, NavSelection } from '@maka/ui';
import type { LiveTurnProjection, NavSelection, SessionViewMode } from '@maka/ui';
import { messageReadErrorMessage } from './app-shell-copy';
import { getDesktopConversationCopy } from './locales/conversation-copy.js';
import { getShellRemainingCopy } from './locales/shell-remaining-copy.js';
import { applyTheme, applyThemePalette } from './theme';
import { safeLocalStorageSet } from './browser-storage';
import type { NavigationState } from './nav-selection.js';
import { writeSessionListViewMode } from './session-list-layout.js';
import {
createSessionEventStreamSubscription,
evaluateSessionEventStreamSnapshot,
Expand Down Expand Up @@ -118,6 +119,7 @@ export function useAppShellPersistenceEffects(options: {
navigationState: NavigationState;
sessionListCollapsed: boolean;
sessionListWidth: number;
sessionListViewMode: SessionViewMode;
workbarCollapsed: boolean;
workbarWidth: number;
bottomPanelOpen: boolean;
Expand Down Expand Up @@ -161,6 +163,10 @@ export function useAppShellPersistenceEffects(options: {
safeLocalStorageSet('maka-chat-list-collapsed-v1', options.sessionListCollapsed ? 'true' : 'false');
}, [options.sessionListCollapsed]);

useEffect(() => {
writeSessionListViewMode(options.sessionListViewMode);
}, [options.sessionListViewMode]);

useEffect(() => {
const handle = window.setTimeout(() => {
safeLocalStorageSet('maka-session-workbar-width-v1', String(options.workbarWidth));
Expand Down
4 changes: 3 additions & 1 deletion apps/desktop/src/renderer/app-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ import { useActiveExecutionBoundary } from './use-active-execution-boundary';
import {
SESSION_LIST_EXPANDED_MAX_WIDTH,
SESSION_LIST_EXPANDED_MIN_WIDTH,
readSessionListViewMode,
} from './session-list-layout';
import { modelSetupToastCopy } from './model-connection-errors';
import type { AppShellCommandListOptions } from './app-shell-command-actions';
Expand Down Expand Up @@ -602,7 +603,7 @@ function AppShellContent({
const persistedComposerDefaults = loadComposerDefaults();
const [helpOpen, closeHelp, openHelp] = useKeyboardHelp();
const [paletteOpen, openPalette, closePalette] = useCommandPalette();
const [viewMode, setViewMode] = useState<SessionViewMode>('conversation');
const [viewMode, setViewMode] = useState<SessionViewMode>(() => readSessionListViewMode());
const composerRef = useRef<ComposerHandle>(null);
// The rail's toggle has to reach Astryx's resizable state, not just this
// boolean — see the prop's note on SessionListPanel. The sidenav is mounted
Expand Down Expand Up @@ -2343,6 +2344,7 @@ function AppShellContent({
navigationState,
sessionListCollapsed,
sessionListWidth,
sessionListViewMode: viewMode,
workbarCollapsed,
workbarWidth,
bottomPanelOpen,
Expand Down
15 changes: 14 additions & 1 deletion apps/desktop/src/renderer/session-list-layout.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
import { safeLocalStorageGet } from './browser-storage.js';
import type { SessionViewMode } from '@maka/ui';
import { safeLocalStorageGet, safeLocalStorageSet } from './browser-storage.js';

export const SESSION_LIST_EXPANDED_DEFAULT_WIDTH = 260;
export const SESSION_LIST_EXPANDED_MIN_WIDTH = 180;
export const SESSION_LIST_EXPANDED_MAX_WIDTH = 480;

const SESSION_LIST_VIEW_MODE_KEY = 'maka-chat-list-view-mode-v1';

export function readSessionListViewMode(): SessionViewMode {
const stored = safeLocalStorageGet(SESSION_LIST_VIEW_MODE_KEY);
if (stored === 'project' || stored === 'conversation') return stored;
return 'conversation';
}

export function writeSessionListViewMode(mode: SessionViewMode): void {
safeLocalStorageSet(SESSION_LIST_VIEW_MODE_KEY, mode);
}

export function readSessionListWidth(): number {
const stored = Number(safeLocalStorageGet('maka-chat-list-width-v1'));
if (Number.isFinite(stored) && stored > 0) return clampSessionListWidth(stored);
Expand Down
Loading