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/11] 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/11] 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/11] 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/11] 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/11] 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');
}
From aecde56bcc22efb5d9bf50726513eb75ab5dd79a Mon Sep 17 00:00:00 2001
From: web3dev1337
Date: Sat, 18 Jul 2026 12:02:39 +1000
Subject: [PATCH 11/11] fix: include workspaceTabState in the static defaults
template
The programmatic getDefaultSettings() and the tests already carry
ui.experimental.workspaceTabState; the static user-settings.default.json
template was missing it, leaving the two default sources out of sync.
Co-Authored-By: Claude Fable 5
---
user-settings.default.json | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/user-settings.default.json b/user-settings.default.json
index 8b16d9f8..bf22af6c 100644
--- a/user-settings.default.json
+++ b/user-settings.default.json
@@ -228,7 +228,11 @@
},
"experimental": {
"persistWorkspaceSidebarState": false,
- "workspaceSidebarStateByWorkspace": {}
+ "workspaceSidebarStateByWorkspace": {},
+ "workspaceTabState": {
+ "openWorkspaceIds": [],
+ "activeWorkspaceId": ""
+ }
},
"worktrees": {
"autoCreateExtraWhenBusy": true,