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
+
+
+
+ Persist workspace sidebar selections and filters
+
+
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
-
-
-
- Persist workspace sidebar selections and filters
-
-
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
+
+
+
+ Persist workspace sidebar selections and filters
+
+
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
- Persist workspace sidebar selections and filters
+ Persist workspace sidebar state and open workspaces
-
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');
}