From e3e14eea8f18721ed3efbed77bf5d6e9376b7cf1 Mon Sep 17 00:00:00 2001 From: AnrokX <192667251+AnrokX@users.noreply.github.com> Date: Tue, 24 Mar 2026 13:50:35 -0600 Subject: [PATCH 01/10] feat: persist workspace sidebar state experimentally --- client/app.js | 219 +++++++++++++++++++++++- client/index.html | 11 ++ client/workspace-tab-manager.js | 5 + server/userSettingsService.js | 37 +++- tests/unit/userSettingsDefaults.test.js | 26 +++ user-settings.default.json | 4 + 6 files changed, 296 insertions(+), 6 deletions(-) diff --git a/client/app.js b/client/app.js index 1133fa74..33680bf7 100644 --- a/client/app.js +++ b/client/app.js @@ -57,6 +57,9 @@ class ClaudeOrchestrator { this.settings = this.loadSettings(); this.appInfo = null; this.userSettings = null; // Will be loaded from server + this.workspaceSidebarStatePersistTimer = null; + this.workspaceSidebarStatePersistChain = Promise.resolve(); + this.pendingWorkspaceSidebarStateSaves = new Map(); this.simpleModeStartupTriggered = false; this.desktopDevtoolsKeydownHandler = null; this.currentLayout = '2x4'; @@ -497,6 +500,179 @@ class ClaudeOrchestrator { this.saveSessionVisibilityOverrides(); } + isWorkspaceSidebarStatePersistenceEnabled() { + return this.userSettings?.global?.ui?.experimental?.persistWorkspaceSidebarState === true; + } + + normalizeWorkspaceSidebarStateViewMode(value) { + const normalized = String(value || '').trim().toLowerCase(); + return ['all', 'claude', 'server'].includes(normalized) ? normalized : 'all'; + } + + normalizeWorkspaceSidebarStateTierFilter(value) { + const raw = String(value ?? '').trim().toLowerCase(); + if (!raw || raw === 'all') return 'all'; + if (raw === 'none') return 'none'; + const tier = Number.parseInt(raw, 10); + return (tier >= 1 && tier <= 4) ? String(tier) : 'all'; + } + + getPersistedWorkspaceSidebarState(workspaceId = null) { + const workspaceKey = String(workspaceId || this.currentWorkspace?.id || '').trim(); + if (!workspaceKey) return null; + const states = this.userSettings?.global?.ui?.experimental?.workspaceSidebarStateByWorkspace; + if (!states || typeof states !== 'object') return null; + const snapshot = states[workspaceKey]; + return snapshot && typeof snapshot === 'object' ? snapshot : null; + } + + captureWorkspaceSidebarStateSnapshot({ workspaceId = null } = {}) { + const workspaceKey = String(workspaceId || this.currentWorkspace?.id || '').trim(); + if (!workspaceKey) return null; + + const worktreeKeys = new Map(); + for (const [sessionId, session] of this.sessions) { + if (!sessionId || !session) continue; + const sessionWorkspaceId = String(session?.workspace || this.currentWorkspace?.id || '').trim(); + if (workspaceKey && sessionWorkspaceId && sessionWorkspaceId !== workspaceKey) continue; + const worktreeKey = this.getSessionWorktreeKey(sessionId, session); + if (!worktreeKey) continue; + if (!worktreeKeys.has(worktreeKey)) worktreeKeys.set(worktreeKey, []); + worktreeKeys.get(worktreeKey).push(String(sessionId)); + } + + const hiddenWorktreeKeys = Array.from(worktreeKeys.entries()) + .filter(([, sessionIds]) => sessionIds.every((sessionId) => !this.visibleTerminals.has(sessionId))) + .map(([worktreeKey]) => worktreeKey) + .sort((a, b) => a.localeCompare(b)); + + return { + viewMode: this.normalizeWorkspaceSidebarStateViewMode(this.viewMode), + tierFilter: this.normalizeWorkspaceSidebarStateTierFilter(this.tierFilter), + hiddenWorktreeKeys + }; + } + + queueWorkspaceSidebarStatePersistence({ workspaceId = null, snapshot = null, immediate = false } = {}) { + if (!this.isWorkspaceSidebarStatePersistenceEnabled()) return; + + const workspaceKey = String(workspaceId || this.currentWorkspace?.id || '').trim(); + if (!workspaceKey) return; + + const nextSnapshot = snapshot || this.captureWorkspaceSidebarStateSnapshot({ workspaceId: workspaceKey }); + if (!nextSnapshot) return; + + this.pendingWorkspaceSidebarStateSaves.set(workspaceKey, nextSnapshot); + + const flush = () => { + if (!this.pendingWorkspaceSidebarStateSaves.size) return; + const queuedEntries = Array.from(this.pendingWorkspaceSidebarStateSaves.entries()); + this.pendingWorkspaceSidebarStateSaves.clear(); + this.workspaceSidebarStatePersistChain = this.workspaceSidebarStatePersistChain + .catch(() => {}) + .then(async () => { + for (const [queuedWorkspaceId, queuedSnapshot] of queuedEntries) { + await this.persistWorkspaceSidebarStateSnapshot(queuedWorkspaceId, queuedSnapshot); + } + }); + }; + + if (this.workspaceSidebarStatePersistTimer) { + clearTimeout(this.workspaceSidebarStatePersistTimer); + this.workspaceSidebarStatePersistTimer = null; + } + + if (immediate) { + flush(); + return; + } + + this.workspaceSidebarStatePersistTimer = setTimeout(() => { + this.workspaceSidebarStatePersistTimer = null; + flush(); + }, 150); + } + + async persistWorkspaceSidebarStateSnapshot(workspaceId, snapshot) { + const workspaceKey = String(workspaceId || '').trim(); + if (!workspaceKey || !snapshot || typeof snapshot !== 'object') return; + if (!this.isWorkspaceSidebarStatePersistenceEnabled()) return; + + const currentMap = (this.userSettings?.global?.ui?.experimental?.workspaceSidebarStateByWorkspace + && typeof this.userSettings.global.ui.experimental.workspaceSidebarStateByWorkspace === 'object') + ? this.userSettings.global.ui.experimental.workspaceSidebarStateByWorkspace + : {}; + + const normalizedSnapshot = { + viewMode: this.normalizeWorkspaceSidebarStateViewMode(snapshot.viewMode), + tierFilter: this.normalizeWorkspaceSidebarStateTierFilter(snapshot.tierFilter), + hiddenWorktreeKeys: Array.isArray(snapshot.hiddenWorktreeKeys) + ? Array.from(new Set(snapshot.hiddenWorktreeKeys.map((value) => String(value || '').trim()).filter(Boolean))).sort((a, b) => a.localeCompare(b)) + : [] + }; + + const existing = currentMap[workspaceKey]; + if (existing && JSON.stringify(existing) === JSON.stringify(normalizedSnapshot)) { + return; + } + + const nextMap = { + ...currentMap, + [workspaceKey]: normalizedSnapshot + }; + + if (this.userSettings?.global?.ui?.experimental) { + this.userSettings.global.ui.experimental.workspaceSidebarStateByWorkspace = nextMap; + } + + await this.updateGlobalUserSetting('ui.experimental.workspaceSidebarStateByWorkspace', nextMap); + } + + applyPersistedWorkspaceSidebarState({ workspaceId = null } = {}) { + if (!this.isWorkspaceSidebarStatePersistenceEnabled()) return false; + + const workspaceKey = String(workspaceId || this.currentWorkspace?.id || '').trim(); + if (!workspaceKey) return false; + + const snapshot = this.getPersistedWorkspaceSidebarState(workspaceKey); + if (!snapshot) return false; + + const hiddenWorktreeKeys = new Set( + Array.isArray(snapshot.hiddenWorktreeKeys) + ? snapshot.hiddenWorktreeKeys.map((value) => String(value || '').trim()).filter(Boolean) + : [] + ); + + const nextVisibleTerminals = new Set(); + let matchedSession = false; + for (const [sessionId, session] of this.sessions) { + if (!sessionId || !session) continue; + const sessionWorkspaceId = String(session?.workspace || this.currentWorkspace?.id || '').trim(); + if (workspaceKey && sessionWorkspaceId && sessionWorkspaceId !== workspaceKey) continue; + const worktreeKey = this.getSessionWorktreeKey(sessionId, session); + if (!worktreeKey) { + nextVisibleTerminals.add(sessionId); + continue; + } + matchedSession = true; + if (!hiddenWorktreeKeys.has(worktreeKey)) { + nextVisibleTerminals.add(sessionId); + } + } + + this.viewMode = this.normalizeWorkspaceSidebarStateViewMode(snapshot.viewMode); + const normalizedTier = this.normalizeWorkspaceSidebarStateTierFilter(snapshot.tierFilter); + this.tierFilter = normalizedTier === 'all' || normalizedTier === 'none' + ? normalizedTier + : Number.parseInt(normalizedTier, 10); + + if (matchedSession) { + this.visibleTerminals = nextVisibleTerminals; + } + + return true; + } + isSessionVisibleByWorktreeSelection(sessionId, session = null) { const sid = String(sessionId || '').trim(); if (!sid) return false; @@ -2580,6 +2756,23 @@ class ClaudeOrchestrator { }); } + const experimentalPersistSidebarState = document.getElementById('experimental-persist-sidebar-state'); + if (experimentalPersistSidebarState) { + experimentalPersistSidebarState.addEventListener('change', async (e) => { + const enabled = !!e.target.checked; + await this.updateGlobalUserSetting('ui.experimental.persistWorkspaceSidebarState', enabled); + if (!enabled) { + if (this.workspaceSidebarStatePersistTimer) { + clearTimeout(this.workspaceSidebarStatePersistTimer); + this.workspaceSidebarStatePersistTimer = null; + } + this.pendingWorkspaceSidebarStateSaves.clear(); + return; + } + this.queueWorkspaceSidebarStatePersistence({ immediate: true }); + }); + } + // Discord services auto-start (server-persisted) const discordAutoEnsure = document.getElementById('discord-auto-ensure-services'); if (discordAutoEnsure) { @@ -2988,7 +3181,11 @@ class ClaudeOrchestrator { this.updateTerminalGrid(); if (persist) { - this.updateGlobalUserSetting('ui.terminals.viewMode', normalized); + if (this.isWorkspaceSidebarStatePersistenceEnabled()) { + this.queueWorkspaceSidebarStatePersistence(); + } else { + this.updateGlobalUserSetting('ui.terminals.viewMode', normalized); + } } } @@ -3022,7 +3219,11 @@ class ClaudeOrchestrator { if (persist) { const next = (normalized === 'all' || normalized === 'none') ? String(normalized) : String(Number(normalized)); - this.updateGlobalUserSetting('ui.terminals.tierFilter', next); + if (this.isWorkspaceSidebarStatePersistenceEnabled()) { + this.queueWorkspaceSidebarStatePersistence(); + } else { + this.updateGlobalUserSetting('ui.terminals.tierFilter', next); + } } } @@ -3951,6 +4152,10 @@ class ClaudeOrchestrator { this.lastSessionsWorkspaceId = currentWorkspaceId; + if (!preserveVisibility) { + this.applyPersistedWorkspaceSidebarState({ workspaceId: currentWorkspaceId }); + } + // Hide loading message FIRST const loadingMessage = document.getElementById('loading-message'); if (loadingMessage) { @@ -5392,6 +5597,7 @@ class ClaudeOrchestrator { } this.updateTerminalGrid(); this.buildSidebar(); + this.queueWorkspaceSidebarStatePersistence(); } toggleWorktreeVisibility(worktreeIdOrKey) { @@ -5463,6 +5669,7 @@ class ClaudeOrchestrator { // This will re-render with correct data-visible-count and apply proper CSS grid this.updateTerminalGrid(); this.buildSidebar(); + this.queueWorkspaceSidebarStatePersistence(); } showWorktree(worktreeIdOrKey) { @@ -5475,6 +5682,7 @@ class ClaudeOrchestrator { this.updateTerminalGrid(); this.buildSidebar(); + this.queueWorkspaceSidebarStatePersistence(); } showAllTerminals() { @@ -5485,6 +5693,7 @@ class ClaudeOrchestrator { this.updateTerminalGrid(); this.buildSidebar(); + this.queueWorkspaceSidebarStatePersistence(); } /** @@ -5787,6 +5996,7 @@ class ClaudeOrchestrator { }); this.updateTerminalGrid(); this.buildSidebar(); + this.queueWorkspaceSidebarStatePersistence(); } renderTerminals(sessionIds) { @@ -17962,6 +18172,11 @@ class ClaudeOrchestrator { } this.applySimpleModeConfig(); + const experimentalPersistSidebarState = document.getElementById('experimental-persist-sidebar-state'); + if (experimentalPersistSidebarState) { + experimentalPersistSidebarState.checked = this.userSettings.global?.ui?.experimental?.persistWorkspaceSidebarState === true; + } + const desktopDevtoolsEnabled = document.getElementById('desktop-devtools-enabled'); if (desktopDevtoolsEnabled) { desktopDevtoolsEnabled.checked = this.userSettings.global?.ui?.desktop?.devtools?.enabled === true; diff --git a/client/index.html b/client/index.html index 2e88a612..26ef7486 100644 --- a/client/index.html +++ b/client/index.html @@ -255,6 +255,17 @@

General Settings

+
+

Experimental

+
+ +

Off by default. Saves per-workspace worktree visibility plus the left-sidebar Tier and View filters across tabs, reloads, and app/server restarts.

+
+
+

Branch Labels

diff --git a/client/workspace-tab-manager.js b/client/workspace-tab-manager.js index 2541365b..3e17b901 100644 --- a/client/workspace-tab-manager.js +++ b/client/workspace-tab-manager.js @@ -510,6 +510,11 @@ class WorkspaceTabManager { // Persist UI/filter state (visible terminals, filters, etc.) for later restoration this.saveTabUIState(tabState); + this.orchestrator.queueWorkspaceSidebarStatePersistence?.({ + workspaceId: tabWorkspaceId, + snapshot: this.orchestrator.captureWorkspaceSidebarStateSnapshot?.({ workspaceId: tabWorkspaceId }), + immediate: true + }); for (const [sessionId, termData] of Array.from(tabState.terminals.entries())) { if (currentSessionIds.has(sessionId)) continue; diff --git a/server/userSettingsService.js b/server/userSettingsService.js index ff849dd4..064d0369 100644 --- a/server/userSettingsService.js +++ b/server/userSettingsService.js @@ -303,6 +303,10 @@ class UserSettingsService { // 'all' | 'none' | '1' | '2' | '3' | '4' tierFilter: 'all' }, + experimental: { + persistWorkspaceSidebarState: false, + workspaceSidebarStateByWorkspace: {} + }, worktrees: { autoCreateExtraWhenBusy: true, autoCreateMinNumber: 9, @@ -806,10 +810,10 @@ class UserSettingsService { }; } - if (ui.workflow) { - const defaultsWorkflow = (uiDefaults.workflow || {}); - const wf = ui.workflow || {}; - merged.global.ui.workflow = { + if (ui.workflow) { + const defaultsWorkflow = (uiDefaults.workflow || {}); + const wf = ui.workflow || {}; + merged.global.ui.workflow = { ...defaultsWorkflow, ...wf, focus: { @@ -823,6 +827,19 @@ class UserSettingsService { }; } + if (ui.experimental && typeof ui.experimental === 'object') { + const defaultsExperimental = (uiDefaults.experimental || {}); + const nextExperimental = ui.experimental || {}; + merged.global.ui.experimental = { + ...defaultsExperimental, + ...nextExperimental, + workspaceSidebarStateByWorkspace: { + ...(defaultsExperimental.workspaceSidebarStateByWorkspace || {}), + ...(nextExperimental.workspaceSidebarStateByWorkspace || {}) + } + }; + } + if (ui.tasks) { const defaultsTasks = tasksDefaults || {}; const tasks = ui.tasks || {}; @@ -1083,6 +1100,18 @@ class UserSettingsService { ...newGlobal.ui.diffViewer }; } + if (newGlobal.ui.experimental) { + const defaultsExperimental = this.getDefaultSettings().global.ui.experimental || {}; + const nextExperimental = newGlobal.ui.experimental || {}; + this.settings.global.ui.experimental = { + ...defaultsExperimental, + ...nextExperimental, + workspaceSidebarStateByWorkspace: { + ...(defaultsExperimental.workspaceSidebarStateByWorkspace || {}), + ...(nextExperimental.workspaceSidebarStateByWorkspace || {}) + } + }; + } } const saved = this.saveSettings(); diff --git a/tests/unit/userSettingsDefaults.test.js b/tests/unit/userSettingsDefaults.test.js index c9a399d9..76e8c98a 100644 --- a/tests/unit/userSettingsDefaults.test.js +++ b/tests/unit/userSettingsDefaults.test.js @@ -46,6 +46,15 @@ describe('UserSettingsService defaults', () => { expect(defaults.global.ui.workflow.notifications.mode).toBeTruthy(); }); + test('includes ui.experimental workspace sidebar persistence defaults', () => { + const defaults = UserSettingsService.prototype.getDefaultSettings.call({}); + const experimental = defaults?.global?.ui?.experimental; + expect(experimental).toBeTruthy(); + expect(experimental.persistWorkspaceSidebarState).toBe(false); + expect(experimental.workspaceSidebarStateByWorkspace).toBeTruthy(); + expect(typeof experimental.workspaceSidebarStateByWorkspace).toBe('object'); + }); + test('includes ui.worktrees auto-create defaults', () => { const defaults = UserSettingsService.prototype.getDefaultSettings.call({}); expect(defaults?.global?.ui?.worktrees).toBeTruthy(); @@ -137,6 +146,15 @@ describe('UserSettingsService defaults', () => { simpleMode: { startupOpen: true }, + experimental: { + workspaceSidebarStateByWorkspace: { + alpha: { + viewMode: 'claude', + tierFilter: '3', + hiddenWorktreeKeys: ['repo-work4'] + } + } + }, workflow: { mode: 'focus' }, @@ -193,6 +211,14 @@ describe('UserSettingsService defaults', () => { expect(merged.global.ui.simpleMode.startupOpen).toBe(true); expect(merged.global.ui.simpleMode.enabled).toBe(false); expect(merged.global.ui.simpleMode.hotkeys).toBe(true); + // Keeps experimental defaults while allowing workspace-scoped sidebar state overrides. + expect(merged.global.ui.experimental).toBeTruthy(); + expect(merged.global.ui.experimental.persistWorkspaceSidebarState).toBe(false); + expect(merged.global.ui.experimental.workspaceSidebarStateByWorkspace.alpha).toEqual({ + viewMode: 'claude', + tierFilter: '3', + hiddenWorktreeKeys: ['repo-work4'] + }); // Does not drop process.status defaults when only one cap is provided. expect(merged.global.process.status.lookbackHours).toBeTruthy(); diff --git a/user-settings.default.json b/user-settings.default.json index 438f1435..8b16d9f8 100644 --- a/user-settings.default.json +++ b/user-settings.default.json @@ -226,6 +226,10 @@ "viewMode": "all", "tierFilter": "all" }, + "experimental": { + "persistWorkspaceSidebarState": false, + "workspaceSidebarStateByWorkspace": {} + }, "worktrees": { "autoCreateExtraWhenBusy": true, "autoCreateMinNumber": 9, From d0299b0776f4293ef47da1e1ce6cdf0de7350b1a Mon Sep 17 00:00:00 2001 From: AnrokX <192667251+AnrokX@users.noreply.github.com> Date: Tue, 24 Mar 2026 13:50:46 -0600 Subject: [PATCH 02/10] docs: note experimental sidebar persistence --- CODEBASE_DOCUMENTATION.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CODEBASE_DOCUMENTATION.md b/CODEBASE_DOCUMENTATION.md index bbcaa9f1..45db7033 100644 --- a/CODEBASE_DOCUMENTATION.md +++ b/CODEBASE_DOCUMENTATION.md @@ -233,6 +233,7 @@ client/app.js - Config pre-fetching & caching client/app.js - Main client application ├─ Manages: UI state, socket connections, terminal grid ├─ Features: 16-terminal layout, real-time updates, session switching +├─ Experimental workspace sidebar persistence: optional per-workspace restore for hidden worktrees plus View/Tier filters across reloads and app restarts ├─ Command Palette: header `⌘ Commands` button + `Ctrl/Cmd+K` searchable command launcher for command-catalog actions ├─ Intent hints: compact "intent haiku" strip above each agent terminal, refreshed from `POST /api/sessions/intent-haiku` ├─ Projects + Chats automation: `project-chats-new` Commander/voice action supports explicit workspace + repository targeting @@ -377,6 +378,7 @@ package.json - Node.js dependencies and scripts user-settings.json - User preferences and workspace settings user-settings.default.json - Default user settings template +├─ Experimental UI state: `global.ui.experimental.persistWorkspaceSidebarState` gates per-workspace sidebar snapshots stored in `workspaceSidebarStateByWorkspace` ``` ### Workspace Templates & Scripts From c3a8dde9279b54a60d918b060852ccc4dec6443d Mon Sep 17 00:00:00 2001 From: AnrokX <192667251+AnrokX@users.noreply.github.com> Date: Tue, 24 Mar 2026 14:00:08 -0600 Subject: [PATCH 03/10] fix: restore experimental sidebar state after reopen --- client/app.js | 16 ++++++++++++++++ client/index.html | 22 +++++++++++----------- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/client/app.js b/client/app.js index 33680bf7..da670053 100644 --- a/client/app.js +++ b/client/app.js @@ -673,6 +673,21 @@ class ClaudeOrchestrator { return true; } + restoreCurrentWorkspaceSidebarStateFromPersistence() { + const workspaceId = String(this.currentWorkspace?.id || '').trim(); + if (!workspaceId || this.sessions.size === 0) return false; + + const applied = this.applyPersistedWorkspaceSidebarState({ workspaceId }); + if (!applied) return false; + + this.updateViewModeButtons(); + this.updateTierFilterButtons(); + this.ensureFilterToggleExists(); + this.updateTerminalGrid(); + this.buildSidebar(); + return true; + } + isSessionVisibleByWorktreeSelection(sessionId, session = null) { const sid = String(sessionId || '').trim(); if (!sid) return false; @@ -17862,6 +17877,7 @@ class ClaudeOrchestrator { this.applyUiVisibility(); this.refreshBranchLabels(); this.updateTierFilterButtons(); + this.restoreCurrentWorkspaceSidebarStateFromPersistence(); } else { console.error('Failed to load user settings:', response.statusText); } diff --git a/client/index.html b/client/index.html index 26ef7486..cb23f351 100644 --- a/client/index.html +++ b/client/index.html @@ -255,17 +255,6 @@

General Settings

-
-

Experimental

-
- -

Off by default. Saves per-workspace worktree visibility plus the left-sidebar Tier and View filters across tabs, reloads, and app/server restarts.

-
-
-

Branch Labels

@@ -482,6 +471,17 @@
Per-Terminal Overrides
+ +
+

Experimental

+
+ +

Off by default. Saves per-workspace worktree visibility plus the left-sidebar Tier and View filters across tabs, reloads, and app/server restarts.

+
+
From df3531de4b64c903353eb13694d1be52d34aac01 Mon Sep 17 00:00:00 2001 From: AnrokX <192667251+AnrokX@users.noreply.github.com> Date: Tue, 24 Mar 2026 14:05:40 -0600 Subject: [PATCH 04/10] fix: restore all open workspace tabs on reopen --- client/app.js | 7 ++- client/workspace-tab-manager.js | 75 ++++++++++++++++++++++++++++++++- 2 files changed, 76 insertions(+), 6 deletions(-) diff --git a/client/app.js b/client/app.js index da670053..de9bd4d6 100644 --- a/client/app.js +++ b/client/app.js @@ -1658,10 +1658,9 @@ class ClaudeOrchestrator { if (mainContainer) mainContainer.classList.remove('hidden'); if (sidebar) sidebar.classList.remove('hidden'); - // Create tab for the active workspace - // Note: sessions will come later in the 'sessions' event - const tabId = this.tabManager.createTab(active, []); - console.log(`Created initial tab ${tabId} for workspace ${active.name}`); + const restoredTabId = this.tabManager.restorePersistedTabs(available, { activeWorkspaceId: active.id }); + const tabId = restoredTabId || this.tabManager.createTab(active, []); + console.log(`Created/restored initial tab set, focusing ${tabId} for workspace ${active.name}`); // Set currentTabId so subsequent sessions event knows which tab to use this.currentTabId = tabId; diff --git a/client/workspace-tab-manager.js b/client/workspace-tab-manager.js index 3e17b901..38cd4feb 100644 --- a/client/workspace-tab-manager.js +++ b/client/workspace-tab-manager.js @@ -30,6 +30,70 @@ class WorkspaceTabManager { this.setupKeyboardShortcuts(); } + getTabPersistenceStorageKey() { + return 'agent-workspace-open-workspace-tabs-v1'; + } + + loadPersistedTabState() { + try { + const raw = localStorage.getItem(this.getTabPersistenceStorageKey()); + const parsed = raw ? JSON.parse(raw) : null; + if (!parsed || typeof parsed !== 'object') return { workspaceIds: [], activeWorkspaceId: '' }; + const workspaceIds = Array.isArray(parsed.workspaceIds) + ? Array.from(new Set(parsed.workspaceIds.map((value) => String(value || '').trim()).filter(Boolean))) + : []; + const activeWorkspaceId = String(parsed.activeWorkspaceId || '').trim(); + return { workspaceIds, activeWorkspaceId }; + } catch { + return { workspaceIds: [], activeWorkspaceId: '' }; + } + } + + savePersistedTabState() { + try { + const workspaceIds = Array.from(this.tabs.values()) + .map((tab) => String(tab?.workspaceId || '').trim()) + .filter(Boolean); + const activeWorkspaceId = String(this.getActiveTab()?.workspaceId || '').trim(); + localStorage.setItem(this.getTabPersistenceStorageKey(), JSON.stringify({ + workspaceIds, + activeWorkspaceId + })); + } catch { + // ignore storage failures + } + } + + restorePersistedTabs(availableWorkspaces = [], { activeWorkspaceId = '' } = {}) { + const availableById = new Map( + (Array.isArray(availableWorkspaces) ? availableWorkspaces : []) + .map((workspace) => [String(workspace?.id || '').trim(), workspace]) + .filter(([workspaceId, workspace]) => workspaceId && workspace) + ); + + const persisted = this.loadPersistedTabState(); + const orderedWorkspaceIds = Array.from(new Set([ + ...persisted.workspaceIds.filter((workspaceId) => availableById.has(workspaceId)), + ...(activeWorkspaceId && availableById.has(activeWorkspaceId) ? [activeWorkspaceId] : []) + ])); + + if (!orderedWorkspaceIds.length) { + return null; + } + + orderedWorkspaceIds.forEach((workspaceId) => { + const workspace = availableById.get(workspaceId); + if (!workspace) return; + this.createTab(workspace, [], { autoActivate: false }); + }); + + if (activeWorkspaceId) { + return this.findTabByWorkspaceId(activeWorkspaceId)?.id || null; + } + + return this.findTabByWorkspaceId(orderedWorkspaceIds[0])?.id || null; + } + createTabBarContainer() { const mainContainer = document.querySelector('.main-container'); if (!mainContainer) { @@ -97,7 +161,7 @@ class WorkspaceTabManager { /** * Create a new tab for a workspace */ - createTab(workspace, sessions = []) { + createTab(workspace, sessions = [], { autoActivate = null } = {}) { // Prevent duplicate tabs for the same workspace const existingTab = this.findTabByWorkspaceId(workspace?.id); if (existingTab) { @@ -175,11 +239,13 @@ class WorkspaceTabManager { // Store tab this.tabs.set(tabId, tabState); + this.savePersistedTabState(); console.log(`Created tab ${tabId} for workspace ${workspace.name}`); // If this is the first tab, activate it - if (this.tabs.size === 1) { + const shouldAutoActivate = autoActivate === null ? this.tabs.size === 1 : !!autoActivate; + if (shouldAutoActivate) { this.switchTab(tabId); } @@ -436,6 +502,7 @@ class WorkspaceTabManager { // Update active state this.activeTabId = tabId; + this.savePersistedTabState(); // Update UI this.updateTabUI(); @@ -883,6 +950,7 @@ class WorkspaceTabManager { // Remove from tabs map this.tabs.delete(tabId); + this.savePersistedTabState(); // If we closed the active tab, switch to another if (wasActive) { @@ -926,6 +994,8 @@ class WorkspaceTabManager { } } + this.savePersistedTabState(); + return tabsToRemove.length; } @@ -1104,6 +1174,7 @@ class WorkspaceTabManager { // Remove from tabs map this.tabs.delete(tabState.id); + this.savePersistedTabState(); } /** From deea02b113063c08b2dae51f32cd2006fdd48ed3 Mon Sep 17 00:00:00 2001 From: AnrokX <192667251+AnrokX@users.noreply.github.com> Date: Tue, 24 Mar 2026 14:11:47 -0600 Subject: [PATCH 05/10] fix: persist open workspaces across app restarts --- CODEBASE_DOCUMENTATION.md | 2 +- client/app.js | 119 +++++++++++++++++++++++- client/index.html | 4 +- client/workspace-tab-manager.js | 46 +++++++-- server/userSettingsService.js | 14 ++- tests/unit/userSettingsDefaults.test.js | 11 +++ 6 files changed, 183 insertions(+), 13 deletions(-) diff --git a/CODEBASE_DOCUMENTATION.md b/CODEBASE_DOCUMENTATION.md index 45db7033..661b8579 100644 --- a/CODEBASE_DOCUMENTATION.md +++ b/CODEBASE_DOCUMENTATION.md @@ -233,7 +233,7 @@ client/app.js - Config pre-fetching & caching client/app.js - Main client application ├─ Manages: UI state, socket connections, terminal grid ├─ Features: 16-terminal layout, real-time updates, session switching -├─ Experimental workspace sidebar persistence: optional per-workspace restore for hidden worktrees plus View/Tier filters across reloads and app restarts +├─ Experimental workspace UI persistence: optional per-workspace restore for hidden worktrees plus View/Tier filters, and optional restore of the open workspace tab set across reloads and app restarts ├─ Command Palette: header `⌘ Commands` button + `Ctrl/Cmd+K` searchable command launcher for command-catalog actions ├─ Intent hints: compact "intent haiku" strip above each agent terminal, refreshed from `POST /api/sessions/intent-haiku` ├─ Projects + Chats automation: `project-chats-new` Commander/voice action supports explicit workspace + repository targeting diff --git a/client/app.js b/client/app.js index de9bd4d6..c740cf75 100644 --- a/client/app.js +++ b/client/app.js @@ -60,6 +60,9 @@ class ClaudeOrchestrator { this.workspaceSidebarStatePersistTimer = null; this.workspaceSidebarStatePersistChain = Promise.resolve(); this.pendingWorkspaceSidebarStateSaves = new Map(); + this.workspaceTabStatePersistTimer = null; + this.workspaceTabStatePersistChain = Promise.resolve(); + this.pendingWorkspaceTabStateSave = null; this.simpleModeStartupTriggered = false; this.desktopDevtoolsKeydownHandler = null; this.currentLayout = '2x4'; @@ -526,6 +529,89 @@ class ClaudeOrchestrator { return snapshot && typeof snapshot === 'object' ? snapshot : null; } + normalizeWorkspaceTabState(snapshot) { + const rawOpenWorkspaceIds = Array.isArray(snapshot?.openWorkspaceIds) + ? snapshot.openWorkspaceIds + : (Array.isArray(snapshot?.workspaceIds) ? snapshot.workspaceIds : []); + const openWorkspaceIds = Array.from(new Set( + rawOpenWorkspaceIds.map((value) => String(value || '').trim()).filter(Boolean) + )); + const activeWorkspaceId = String(snapshot?.activeWorkspaceId || '').trim(); + return { + openWorkspaceIds, + activeWorkspaceId + }; + } + + getPersistedWorkspaceTabState() { + if (!this.isWorkspaceSidebarStatePersistenceEnabled()) return null; + const snapshot = this.userSettings?.global?.ui?.experimental?.workspaceTabState; + if (!snapshot || typeof snapshot !== 'object') return null; + return this.normalizeWorkspaceTabState(snapshot); + } + + captureWorkspaceTabStateSnapshot() { + if (!this.tabManager) { + return this.normalizeWorkspaceTabState(null); + } + + const openWorkspaceIds = Array.from(this.tabManager.tabs.values()) + .map((tab) => String(tab?.workspaceId || '').trim()) + .filter(Boolean); + const activeWorkspaceId = String(this.tabManager.getActiveTab?.()?.workspaceId || this.currentWorkspace?.id || '').trim(); + return this.normalizeWorkspaceTabState({ + openWorkspaceIds, + activeWorkspaceId + }); + } + + queueWorkspaceTabStatePersistence({ snapshot = null, immediate = false } = {}) { + if (!this.isWorkspaceSidebarStatePersistenceEnabled()) return; + + const nextSnapshot = this.normalizeWorkspaceTabState(snapshot || this.captureWorkspaceTabStateSnapshot()); + this.pendingWorkspaceTabStateSave = nextSnapshot; + + const flush = () => { + if (!this.pendingWorkspaceTabStateSave) return; + const queuedSnapshot = this.pendingWorkspaceTabStateSave; + this.pendingWorkspaceTabStateSave = null; + this.workspaceTabStatePersistChain = this.workspaceTabStatePersistChain + .catch(() => {}) + .then(() => this.persistWorkspaceTabStateSnapshot(queuedSnapshot)); + }; + + if (this.workspaceTabStatePersistTimer) { + clearTimeout(this.workspaceTabStatePersistTimer); + this.workspaceTabStatePersistTimer = null; + } + + if (immediate) { + flush(); + return; + } + + this.workspaceTabStatePersistTimer = setTimeout(() => { + this.workspaceTabStatePersistTimer = null; + flush(); + }, 150); + } + + async persistWorkspaceTabStateSnapshot(snapshot) { + if (!this.isWorkspaceSidebarStatePersistenceEnabled()) return; + + const normalizedSnapshot = this.normalizeWorkspaceTabState(snapshot); + const existingSnapshot = this.normalizeWorkspaceTabState(this.userSettings?.global?.ui?.experimental?.workspaceTabState); + if (JSON.stringify(existingSnapshot) === JSON.stringify(normalizedSnapshot)) { + return; + } + + if (this.userSettings?.global?.ui?.experimental) { + this.userSettings.global.ui.experimental.workspaceTabState = normalizedSnapshot; + } + + await this.updateGlobalUserSetting('ui.experimental.workspaceTabState', normalizedSnapshot); + } + captureWorkspaceSidebarStateSnapshot({ workspaceId = null } = {}) { const workspaceKey = String(workspaceId || this.currentWorkspace?.id || '').trim(); if (!workspaceKey) return null; @@ -688,6 +774,29 @@ class ClaudeOrchestrator { return true; } + async restoreWorkspaceTabsFromPersistence() { + if (!this.tabManager || !this.currentWorkspace) return false; + if (!Array.isArray(this.availableWorkspaces) || this.availableWorkspaces.length === 0) return false; + if (!this.isWorkspaceSidebarStatePersistenceEnabled()) return false; + + const persistedTabState = this.getPersistedWorkspaceTabState(); + if (!persistedTabState || persistedTabState.openWorkspaceIds.length === 0) return false; + + const activeWorkspaceId = persistedTabState.activeWorkspaceId || String(this.currentWorkspace?.id || '').trim(); + const restoredTabId = this.tabManager.restorePersistedTabs(this.availableWorkspaces, { activeWorkspaceId }); + if (!restoredTabId) return false; + + const targetTab = this.tabManager.getTab(restoredTabId); + if (!targetTab) return false; + + if (this.currentTabId !== restoredTabId || this.tabManager.activeTabId !== restoredTabId) { + await this.tabManager.switchTab(restoredTabId); + } + + this.currentTabId = restoredTabId; + return true; + } + isSessionVisibleByWorktreeSelection(sessionId, session = null) { const sid = String(sessionId || '').trim(); if (!sid) return false; @@ -1601,7 +1710,7 @@ class ClaudeOrchestrator { this.showClaudeUpdateRequired(updateInfo); }); - this.socket.on('user-settings-updated', (settings) => { + this.socket.on('user-settings-updated', async (settings) => { console.log('User settings updated:', settings); this.userSettings = settings; this.syncUserSettingsUI(); @@ -1610,6 +1719,7 @@ class ClaudeOrchestrator { this.applyDesktopDevtoolsConfig(); this.maybeAutoOpenSimpleMode(); this.applyUiVisibility(); + await this.restoreWorkspaceTabsFromPersistence(); }); // Workspace events @@ -2780,10 +2890,16 @@ class ClaudeOrchestrator { clearTimeout(this.workspaceSidebarStatePersistTimer); this.workspaceSidebarStatePersistTimer = null; } + if (this.workspaceTabStatePersistTimer) { + clearTimeout(this.workspaceTabStatePersistTimer); + this.workspaceTabStatePersistTimer = null; + } this.pendingWorkspaceSidebarStateSaves.clear(); + this.pendingWorkspaceTabStateSave = null; return; } this.queueWorkspaceSidebarStatePersistence({ immediate: true }); + this.queueWorkspaceTabStatePersistence({ immediate: true }); }); } @@ -17877,6 +17993,7 @@ class ClaudeOrchestrator { this.refreshBranchLabels(); this.updateTierFilterButtons(); this.restoreCurrentWorkspaceSidebarStateFromPersistence(); + await this.restoreWorkspaceTabsFromPersistence(); } else { console.error('Failed to load user settings:', response.statusText); } diff --git a/client/index.html b/client/index.html index cb23f351..1741cbb8 100644 --- a/client/index.html +++ b/client/index.html @@ -477,9 +477,9 @@

Experimental

-

Off by default. Saves per-workspace worktree visibility plus the left-sidebar Tier and View filters across tabs, reloads, and app/server restarts.

+

Off by default. Saves per-workspace worktree visibility, the left-sidebar Tier and View filters, and the set of open workspace tabs across tabs, reloads, and app/server restarts.

diff --git a/client/workspace-tab-manager.js b/client/workspace-tab-manager.js index 38cd4feb..1e32c731 100644 --- a/client/workspace-tab-manager.js +++ b/client/workspace-tab-manager.js @@ -34,7 +34,34 @@ class WorkspaceTabManager { return 'agent-workspace-open-workspace-tabs-v1'; } + isTabPersistenceEnabled() { + return this.orchestrator?.isWorkspaceSidebarStatePersistenceEnabled?.() === true; + } + + buildPersistedTabStateSnapshot() { + const openWorkspaceIds = Array.from(this.tabs.values()) + .map((tab) => String(tab?.workspaceId || '').trim()) + .filter(Boolean); + const activeWorkspaceId = String(this.getActiveTab()?.workspaceId || '').trim(); + return { + openWorkspaceIds: Array.from(new Set(openWorkspaceIds)), + activeWorkspaceId + }; + } + loadPersistedTabState() { + if (!this.isTabPersistenceEnabled()) { + return { workspaceIds: [], activeWorkspaceId: '' }; + } + + const persistedFromSettings = this.orchestrator?.getPersistedWorkspaceTabState?.(); + if (persistedFromSettings) { + return { + workspaceIds: Array.isArray(persistedFromSettings.openWorkspaceIds) ? persistedFromSettings.openWorkspaceIds : [], + activeWorkspaceId: String(persistedFromSettings.activeWorkspaceId || '').trim() + }; + } + try { const raw = localStorage.getItem(this.getTabPersistenceStorageKey()); const parsed = raw ? JSON.parse(raw) : null; @@ -50,18 +77,21 @@ class WorkspaceTabManager { } savePersistedTabState() { + const snapshot = this.buildPersistedTabStateSnapshot(); + try { - const workspaceIds = Array.from(this.tabs.values()) - .map((tab) => String(tab?.workspaceId || '').trim()) - .filter(Boolean); - const activeWorkspaceId = String(this.getActiveTab()?.workspaceId || '').trim(); - localStorage.setItem(this.getTabPersistenceStorageKey(), JSON.stringify({ - workspaceIds, - activeWorkspaceId - })); + if (this.isTabPersistenceEnabled()) { + localStorage.setItem(this.getTabPersistenceStorageKey(), JSON.stringify(snapshot)); + } else { + localStorage.removeItem(this.getTabPersistenceStorageKey()); + } } catch { // ignore storage failures } + + if (this.isTabPersistenceEnabled()) { + this.orchestrator?.queueWorkspaceTabStatePersistence?.({ snapshot }); + } } restorePersistedTabs(availableWorkspaces = [], { activeWorkspaceId = '' } = {}) { diff --git a/server/userSettingsService.js b/server/userSettingsService.js index 064d0369..92610e9f 100644 --- a/server/userSettingsService.js +++ b/server/userSettingsService.js @@ -305,7 +305,11 @@ class UserSettingsService { }, experimental: { persistWorkspaceSidebarState: false, - workspaceSidebarStateByWorkspace: {} + workspaceSidebarStateByWorkspace: {}, + workspaceTabState: { + openWorkspaceIds: [], + activeWorkspaceId: '' + } }, worktrees: { autoCreateExtraWhenBusy: true, @@ -836,6 +840,10 @@ class UserSettingsService { workspaceSidebarStateByWorkspace: { ...(defaultsExperimental.workspaceSidebarStateByWorkspace || {}), ...(nextExperimental.workspaceSidebarStateByWorkspace || {}) + }, + workspaceTabState: { + ...(defaultsExperimental.workspaceTabState || {}), + ...(nextExperimental.workspaceTabState || {}) } }; } @@ -1109,6 +1117,10 @@ class UserSettingsService { workspaceSidebarStateByWorkspace: { ...(defaultsExperimental.workspaceSidebarStateByWorkspace || {}), ...(nextExperimental.workspaceSidebarStateByWorkspace || {}) + }, + workspaceTabState: { + ...(defaultsExperimental.workspaceTabState || {}), + ...(nextExperimental.workspaceTabState || {}) } }; } diff --git a/tests/unit/userSettingsDefaults.test.js b/tests/unit/userSettingsDefaults.test.js index 76e8c98a..0382de5c 100644 --- a/tests/unit/userSettingsDefaults.test.js +++ b/tests/unit/userSettingsDefaults.test.js @@ -53,6 +53,9 @@ describe('UserSettingsService defaults', () => { expect(experimental.persistWorkspaceSidebarState).toBe(false); expect(experimental.workspaceSidebarStateByWorkspace).toBeTruthy(); expect(typeof experimental.workspaceSidebarStateByWorkspace).toBe('object'); + expect(experimental.workspaceTabState).toBeTruthy(); + expect(Array.isArray(experimental.workspaceTabState.openWorkspaceIds)).toBe(true); + expect(experimental.workspaceTabState.activeWorkspaceId).toBe(''); }); test('includes ui.worktrees auto-create defaults', () => { @@ -153,6 +156,10 @@ describe('UserSettingsService defaults', () => { tierFilter: '3', hiddenWorktreeKeys: ['repo-work4'] } + }, + workspaceTabState: { + openWorkspaceIds: ['alpha', 'beta'], + activeWorkspaceId: 'beta' } }, workflow: { @@ -219,6 +226,10 @@ describe('UserSettingsService defaults', () => { tierFilter: '3', hiddenWorktreeKeys: ['repo-work4'] }); + expect(merged.global.ui.experimental.workspaceTabState).toEqual({ + openWorkspaceIds: ['alpha', 'beta'], + activeWorkspaceId: 'beta' + }); // Does not drop process.status defaults when only one cap is provided. expect(merged.global.process.status.lookbackHours).toBeTruthy(); From 2b8bf75729fca2f94ab838426df60537b1372b1e Mon Sep 17 00:00:00 2001 From: AnrokX <192667251+AnrokX@users.noreply.github.com> Date: Tue, 24 Mar 2026 14:16:53 -0600 Subject: [PATCH 06/10] fix: restore sidebar filters for reopened workspaces --- client/app.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/client/app.js b/client/app.js index c740cf75..20013070 100644 --- a/client/app.js +++ b/client/app.js @@ -4206,10 +4206,15 @@ class ClaudeOrchestrator { // receive a sessions refresh for the SAME workspace (e.g. after adding a // worktree). Never carry visibility between different workspaces. const currentWorkspaceId = this.currentWorkspace?.id || null; - const previousWorkspaceId = this.lastSessionsWorkspaceId || null; - const preserveVisibility = !!(currentWorkspaceId && previousWorkspaceId && currentWorkspaceId === previousWorkspaceId); const previousSessionIds = new Set(this.sessions.keys()); const previousVisibleSessionIds = new Set(this.visibleTerminals); + const previousWorkspaceId = this.lastSessionsWorkspaceId || null; + const preserveVisibility = !!( + currentWorkspaceId + && previousWorkspaceId + && currentWorkspaceId === previousWorkspaceId + && previousSessionIds.size > 0 + ); const nextSessionIds = new Set(Object.keys(sessionStates || {})); const removedSessionIds = Array.from(previousSessionIds).filter((sessionId) => !nextSessionIds.has(sessionId)); From 020e16dc71c0af702dd708ee3b07e6572038e71e Mon Sep 17 00:00:00 2001 From: AnrokX <192667251+AnrokX@users.noreply.github.com> Date: Tue, 24 Mar 2026 14:20:05 -0600 Subject: [PATCH 07/10] fix: rehydrate restored workspace sidebar state --- client/app.js | 10 +++++++++- client/workspace-tab-manager.js | 7 ++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/client/app.js b/client/app.js index 20013070..7b70c8e0 100644 --- a/client/app.js +++ b/client/app.js @@ -1828,6 +1828,7 @@ class ClaudeOrchestrator { } if (existingTab) { + const shouldForcePersistedSidebarRestore = existingTab.needsPersistedWorkspaceSidebarRestore === true; // Switch to existing tab console.log(`Workspace ${nextWorkspace.name} already open, switching to tab`); existingTab.workspace = nextWorkspace; @@ -1855,7 +1856,7 @@ class ClaudeOrchestrator { // CRITICAL: Set lastSessionsWorkspaceId BEFORE handleInitialSessions so that // it treats this as a same-workspace refresh and preserves the worktree // visibility state that was just restored from the tab (fix #786). - this.lastSessionsWorkspaceId = nextWorkspace.id; + this.lastSessionsWorkspaceId = shouldForcePersistedSidebarRestore ? null : nextWorkspace.id; await this.maybePlanWorkspaceRecovery(nextWorkspace.id, { interactive: true }); this.handleInitialSessions(sessions); @@ -4285,6 +4286,13 @@ class ClaudeOrchestrator { this.pruneIntentHaikuState(new Set(Object.keys(sessionStates))); + if (this.tabManager && this.currentTabId) { + const currentTab = this.tabManager.getTab(this.currentTabId); + if (currentTab) { + currentTab.needsPersistedWorkspaceSidebarRestore = false; + } + } + this.lastSessionsWorkspaceId = currentWorkspaceId; if (!preserveVisibility) { diff --git a/client/workspace-tab-manager.js b/client/workspace-tab-manager.js index 1e32c731..7c7d2f23 100644 --- a/client/workspace-tab-manager.js +++ b/client/workspace-tab-manager.js @@ -114,7 +114,11 @@ class WorkspaceTabManager { orderedWorkspaceIds.forEach((workspaceId) => { const workspace = availableById.get(workspaceId); if (!workspace) return; - this.createTab(workspace, [], { autoActivate: false }); + const tabId = this.createTab(workspace, [], { autoActivate: false }); + const tabState = this.getTab(tabId); + if (tabState && tabState.sessions.size === 0) { + tabState.needsPersistedWorkspaceSidebarRestore = true; + } }); if (activeWorkspaceId) { @@ -227,6 +231,7 @@ class WorkspaceTabManager { // UI/filter state uiState: this.createDefaultUIState(), + needsPersistedWorkspaceSidebarRestore: false, // Observer for resize handling resizeObserver: null, From a7d26b80e9af05e8ab3324488508fa3360f94cbe Mon Sep 17 00:00:00 2001 From: AnrokX <192667251+AnrokX@users.noreply.github.com> Date: Tue, 24 Mar 2026 14:24:37 -0600 Subject: [PATCH 08/10] fix: avoid overriding experimental view filters --- client/app.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/app.js b/client/app.js index 7b70c8e0..dcf61ebb 100644 --- a/client/app.js +++ b/client/app.js @@ -3541,6 +3541,10 @@ class ClaudeOrchestrator { } syncTerminalFiltersFromUserSettings() { + if (this.isWorkspaceSidebarStatePersistenceEnabled()) { + return; + } + const t = this.userSettings?.global?.ui?.terminals || {}; const viewMode = String(t?.viewMode || '').trim().toLowerCase(); const tierRaw = t?.tierFilter; From 05a140d83c86b4e935cb0a62f424731a671e226f Mon Sep 17 00:00:00 2001 From: AnrokX <192667251+AnrokX@users.noreply.github.com> Date: Tue, 24 Mar 2026 14:31:46 -0600 Subject: [PATCH 09/10] fix: avoid recovery prompt during experimental startup restore --- client/app.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/client/app.js b/client/app.js index dcf61ebb..4f4ff0a8 100644 --- a/client/app.js +++ b/client/app.js @@ -57,6 +57,7 @@ class ClaudeOrchestrator { this.settings = this.loadSettings(); this.appInfo = null; this.userSettings = null; // Will be loaded from server + this.hasCompletedStartupRecoveryCheck = false; this.workspaceSidebarStatePersistTimer = null; this.workspaceSidebarStatePersistChain = Promise.resolve(); this.pendingWorkspaceSidebarStateSaves = new Map(); @@ -206,6 +207,13 @@ class ClaudeOrchestrator { return this.dashboard.pendingRecovery; } + if (!this.hasCompletedStartupRecoveryCheck) { + this.hasCompletedStartupRecoveryCheck = true; + if (this.isWorkspaceSidebarStatePersistenceEnabled()) { + return null; + } + } + try { const recoveryPlan = await this.dashboard.planRecoveryForWorkspace(targetWorkspaceId, { interactive }); if (recoveryPlan?.pending) { @@ -1498,6 +1506,8 @@ class ClaudeOrchestrator { this.applyTheme(); this.syncSettingsUI(); this.applySimpleModeConfig(); + await this.loadUserSettings(); + this.applySidebarDesktopCollapsedFromPrefs(); // Connect to server await this.connectToServer(); @@ -1506,9 +1516,6 @@ class ClaudeOrchestrator { // Hook panels that depend on socket events this.activityFeedPanel?.onSocketConnected?.(this.socket); - // Load user settings from server - await this.loadUserSettings(); - this.applySidebarDesktopCollapsedFromPrefs(); this.refreshLicenseStatus?.().catch(() => {}); this.syncTerminalFiltersFromUserSettings(); this.syncWorkflowModeFromUserSettings(); From 117149d00c4631bb28e6d04373c723d58dafed87 Mon Sep 17 00:00:00 2001 From: AnrokX <192667251+AnrokX@users.noreply.github.com> Date: Tue, 24 Mar 2026 14:33:52 -0600 Subject: [PATCH 10/10] fix: reapply experimental filters after recovery --- client/app.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/client/app.js b/client/app.js index 4f4ff0a8..85e0e85d 100644 --- a/client/app.js +++ b/client/app.js @@ -57,7 +57,6 @@ class ClaudeOrchestrator { this.settings = this.loadSettings(); this.appInfo = null; this.userSettings = null; // Will be loaded from server - this.hasCompletedStartupRecoveryCheck = false; this.workspaceSidebarStatePersistTimer = null; this.workspaceSidebarStatePersistChain = Promise.resolve(); this.pendingWorkspaceSidebarStateSaves = new Map(); @@ -207,13 +206,6 @@ class ClaudeOrchestrator { return this.dashboard.pendingRecovery; } - if (!this.hasCompletedStartupRecoveryCheck) { - this.hasCompletedStartupRecoveryCheck = true; - if (this.isWorkspaceSidebarStatePersistenceEnabled()) { - return null; - } - } - try { const recoveryPlan = await this.dashboard.planRecoveryForWorkspace(targetWorkspaceId, { interactive }); if (recoveryPlan?.pending) { @@ -1698,6 +1690,9 @@ class ClaudeOrchestrator { // Hide + persist dismissal so it doesn't resurrect on refresh/worktree-add this.hideStartupUI(sessionId); this.scheduleAutoPromptFallback(sessionId, 'claude'); + if (this.isWorkspaceSidebarStatePersistenceEnabled()) { + setTimeout(() => this.restoreCurrentWorkspaceSidebarStateFromPersistence(), 0); + } // Enable the start button now that Claude has started const startBtn = document.getElementById(`claude-start-btn-${sessionId}`); @@ -1711,6 +1706,9 @@ class ClaudeOrchestrator { this.socket.on('agent-started', ({ sessionId, config }) => { this.hideStartupUI(sessionId); this.scheduleAutoPromptFallback(sessionId, config?.agentId); + if (this.isWorkspaceSidebarStatePersistenceEnabled()) { + setTimeout(() => this.restoreCurrentWorkspaceSidebarStateFromPersistence(), 0); + } }); this.socket.on('claude-update-required', (updateInfo) => { @@ -17069,6 +17067,12 @@ class ClaudeOrchestrator { } } + if (this.isWorkspaceSidebarStatePersistenceEnabled()) { + setTimeout(() => { + this.restoreCurrentWorkspaceSidebarStateFromPersistence(); + }, 600); + } + this.showTemporaryMessage(`Recovered ${recovery.sessions.length} session(s)`, 'success'); }