From 5211d6664708819e584385fef8aa299486aa24fb Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Tue, 21 Jul 2026 22:59:42 +0300 Subject: [PATCH] fix(ui): default project sorting to manual --- packages/ui/src/stores/DOCUMENTATION.md | 2 ++ .../src/stores/useSessionDisplayStore.test.ts | 22 ++++++++++++ .../ui/src/stores/useSessionDisplayStore.ts | 35 ++++++++++++------- 3 files changed, 46 insertions(+), 13 deletions(-) create mode 100644 packages/ui/src/stores/useSessionDisplayStore.test.ts diff --git a/packages/ui/src/stores/DOCUMENTATION.md b/packages/ui/src/stores/DOCUMENTATION.md index e88d0186..9d6db4bb 100644 --- a/packages/ui/src/stores/DOCUMENTATION.md +++ b/packages/ui/src/stores/DOCUMENTATION.md @@ -67,6 +67,8 @@ Shared safe storage treats durable failures per key. A quota or access failure c Project and UI settings use successful settings synchronization as authority. Omitted fields in a complete snapshot reset to canonical client defaults, including an omitted project list becoming empty; transport or settings-load failure dispatches no synchronization event and preserves current state. Settings save responses are partial patches and must not clear unrelated in-memory preferences or local mirrors. +Project ordering defaults to manual. Session display persistence v3 migrates the previously shipped `recent` project order to `manual` while preserving every other explicit sort mode. + Session folders persist in runtime-specific v2 browser keys without silently evicting older runtime namespaces. Runtime switch, page hide, app freeze, and unload synchronously flush the pending browser snapshot before lifecycle suspension or namespace replacement. A runtime switch then cancels stale old-runtime disk work and starts generation-owned disk hydration. Missing or malformed server files are not authoritative empty snapshots; disk data may replace browser state only when it carries a real revision and no newer local folder mutation occurred. Server writes are serialized and reject non-newer revisions so delayed or duplicate requests cannot overwrite the current state. File-search cache and in-flight keys include runtime plus directory and are cleared on endpoint reset. Persisted session todos use a bounded composite key of runtime, normalized directory, and session ID. Ambiguous legacy todo entries are discarded rather than claimed by whichever runtime starts first. Authoritative deletion uses an explicit runtime identity, and session-folder deletion scans every scope in the active runtime so archived assignments cannot survive after their session is gone. diff --git a/packages/ui/src/stores/useSessionDisplayStore.test.ts b/packages/ui/src/stores/useSessionDisplayStore.test.ts new file mode 100644 index 00000000..71a58199 --- /dev/null +++ b/packages/ui/src/stores/useSessionDisplayStore.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, test } from 'bun:test'; +import { migrateSessionDisplayState, useSessionDisplayStore } from './useSessionDisplayStore'; + +describe('useSessionDisplayStore project sorting', () => { + test('defaults to manual ordering', () => { + expect(useSessionDisplayStore.getState().projectSortOrder).toBe('manual'); + }); + + test('migrates the v2 recent default to manual', () => { + const migrated = migrateSessionDisplayState({ projectSortOrder: 'recent' }, 2); + + expect(migrated.projectSortOrder).toBe('manual'); + }); + + for (const projectSortOrder of ['manual', 'a-z', 'z-a', 'date-added'] as const) { + test(`preserves the v2 ${projectSortOrder} sort order`, () => { + const migrated = migrateSessionDisplayState({ projectSortOrder }, 2); + + expect(migrated.projectSortOrder).toBe(projectSortOrder); + }); + } +}); diff --git a/packages/ui/src/stores/useSessionDisplayStore.ts b/packages/ui/src/stores/useSessionDisplayStore.ts index 3e579a4d..b3ab9876 100644 --- a/packages/ui/src/stores/useSessionDisplayStore.ts +++ b/packages/ui/src/stores/useSessionDisplayStore.ts @@ -18,6 +18,23 @@ type SessionDisplayStore = { setProjectSortOrder: (order: ProjectSortOrder) => void; }; +export const migrateSessionDisplayState = ( + persisted: unknown, + version: number, +): Partial => { + const state = (persisted ?? {}) as Partial; + if (version < 1) { + return { ...state, displayMode: 'minimal', projectSortOrder: 'manual' }; + } + if (version < 2) { + return { ...state, projectSortOrder: 'manual' }; + } + if (version < 3 && state.projectSortOrder === 'recent') { + return { ...state, projectSortOrder: 'manual' }; + } + return state; +}; + export const useSessionDisplayStore = create()( persist( (set) => ({ @@ -28,7 +45,7 @@ export const useSessionDisplayStore = create()( // disappear once the persisted preference rehydrates. Users who opted into // showing archived have `true` persisted, which is preserved on rehydrate. showArchivedSessions: false, - projectSortOrder: 'recent', + projectSortOrder: 'manual', setDisplayMode: (mode) => set({ displayMode: mode }), setShowRecentSection: (show) => set({ showRecentSection: show }), setShowArchivedSessions: (show) => set({ showArchivedSessions: show }), @@ -38,21 +55,13 @@ export const useSessionDisplayStore = create()( }), { name: 'session-display-mode', - version: 2, + version: 3, // v0 shipped 'default' as the only/initial mode, so most existing users // have it persisted by accident rather than choice. Nudge everyone onto // minimal once so the mode can be evaluated before removing it entirely. - // v1→v2 adds projectSortOrder defaulting to 'recent'. - migrate: (persisted, version) => { - const state = (persisted ?? {}) as Partial; - if (version < 1) { - return { ...state, displayMode: 'minimal', projectSortOrder: 'recent' }; - } - if (version < 2) { - return { ...state, projectSortOrder: 'recent' }; - } - return state; - }, + // v1→v2 adds projectSortOrder using the canonical manual ordering. + // v2→v3 replaces the previously shipped recent default with manual. + migrate: migrateSessionDisplayState, }, ), );