Skip to content

Commit 978443c

Browse files
committed
Fix terminal state loss when switching workspaces
Fixes issue where TUI applications (vim, htop, opencode, etc.) would lose terminal state when switching between workspaces. This caused inability to scroll and display corruption. Root cause: Workspace switching was destroying all tab views including terminals, then recreating them from cache. This destroyed xterm.js instances and lost their state. Solution: Cache tab views across workspace switches instead of destroying them. Tab views are positioned off-screen but kept alive, preserving: - Terminal buffer state (normal and alternate screen modes) - Scrollback history and scrolling capability - Running processes and their output - Cursor position and all terminal modes Memory management: Cached views kept alive until tab closed or window closed. Note: This PR includes the StreamCancelFn type fix from #2716 to ensure the branch builds correctly.
1 parent 90011a7 commit 978443c

File tree

2 files changed

+882
-288
lines changed

2 files changed

+882
-288
lines changed

emain/emain-window.ts

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ export class WaveBrowserWindow extends BaseWindow {
139139
waveWindowId: string;
140140
workspaceId: string;
141141
allLoadedTabViews: Map<string, WaveTabView>;
142+
allTabViewsCache: Map<string, WaveTabView>; // Cache for preserving tab views across workspace switches
142143
activeTabView: WaveTabView;
143144
private canClose: boolean;
144145
private deleteAllowed: boolean;
@@ -218,6 +219,7 @@ export class WaveBrowserWindow extends BaseWindow {
218219
this.waveWindowId = waveWindow.oid;
219220
this.workspaceId = waveWindow.workspaceid;
220221
this.allLoadedTabViews = new Map<string, WaveTabView>();
222+
this.allTabViewsCache = new Map<string, WaveTabView>();
221223
const winBoundsPoller = setInterval(() => {
222224
if (this.isDestroyed()) {
223225
clearInterval(winBoundsPoller);
@@ -352,6 +354,11 @@ export class WaveBrowserWindow extends BaseWindow {
352354
}
353355
tabView?.destroy();
354356
}
357+
// Also destroy any cached views
358+
for (const tabView of this.allTabViewsCache.values()) {
359+
tabView?.destroy();
360+
}
361+
this.allTabViewsCache.clear();
355362
}
356363

357364
async switchWorkspace(workspaceId: string) {
@@ -576,8 +583,27 @@ export class WaveBrowserWindow extends BaseWindow {
576583
return;
577584
}
578585
console.log("processActionQueue switchworkspace newWs", newWs);
579-
this.removeAllChildViews();
580-
console.log("destroyed all tabs", this.waveWindowId);
586+
// Move current tab views to cache instead of destroying them
587+
// This preserves terminal state (including alternate screen mode) across workspace switches
588+
for (const [tabId, tabView] of this.allLoadedTabViews.entries()) {
589+
// Position off-screen but don't destroy
590+
if (!this.isDestroyed()) {
591+
const bounds = this.getContentBounds();
592+
tabView.positionTabOffScreen(bounds);
593+
}
594+
// If tabId already in cache (edge case with rapid workspace switching), destroy the old cached view first
595+
const existingCachedView = this.allTabViewsCache.get(tabId);
596+
if (existingCachedView) {
597+
existingCachedView.destroy();
598+
}
599+
this.allTabViewsCache.set(tabId, tabView);
600+
}
601+
console.log("cached", this.allLoadedTabViews.size, "tabs for workspace", this.workspaceId, this.waveWindowId);
602+
// Note: Cached views are kept alive indefinitely and only destroyed when:
603+
// 1. The tab is explicitly closed by the user
604+
// 2. The window is closed (via removeAllChildViews)
605+
// This matches how traditional terminal apps work and prevents terminal state loss
606+
581607
this.workspaceId = entry.workspaceId;
582608
this.allLoadedTabViews = new Map();
583609
tabId = newWs.activetabid;
@@ -586,7 +612,15 @@ export class WaveBrowserWindow extends BaseWindow {
586612
if (tabId == null) {
587613
return;
588614
}
589-
const [tabView, tabInitialized] = await getOrCreateWebViewForTab(this.waveWindowId, tabId);
615+
// Check cache first to reuse existing tab views across workspace switches
616+
let tabView = this.allTabViewsCache.get(tabId);
617+
let tabInitialized = true;
618+
if (tabView) {
619+
console.log("reusing cached tab view", tabId, this.waveWindowId);
620+
this.allTabViewsCache.delete(tabId);
621+
} else {
622+
[tabView, tabInitialized] = await getOrCreateWebViewForTab(this.waveWindowId, tabId);
623+
}
590624
const primaryStartupTabFlag = entry.op === "switchtab" ? (entry.primaryStartupTab ?? false) : false;
591625
await this.setTabViewIntoWindow(tabView, tabInitialized, primaryStartupTabFlag);
592626
} catch (e) {
@@ -618,14 +652,23 @@ export class WaveBrowserWindow extends BaseWindow {
618652
console.log("cannot remove active tab", tabId, this.waveWindowId);
619653
return;
620654
}
621-
const tabView = this.allLoadedTabViews.get(tabId);
655+
let tabView = this.allLoadedTabViews.get(tabId);
622656
if (tabView == null) {
623-
console.log("removeTabView -- tabView not found", tabId, this.waveWindowId);
624-
// the tab was never loaded, so just return
625-
return;
657+
// Check cache - tab might be from a different workspace
658+
tabView = this.allTabViewsCache.get(tabId);
659+
if (tabView == null) {
660+
console.log("removeTabView -- tabView not found in loaded or cache", tabId, this.waveWindowId);
661+
return;
662+
}
663+
console.log("removeTabView -- removing from cache", tabId, this.waveWindowId);
664+
this.allTabViewsCache.delete(tabId);
665+
} else {
666+
this.allLoadedTabViews.delete(tabId);
667+
}
668+
// Remove from contentView (cached views are still children, just positioned off-screen)
669+
if (!this.isDestroyed()) {
670+
this.contentView.removeChildView(tabView);
626671
}
627-
this.contentView.removeChildView(tabView);
628-
this.allLoadedTabViews.delete(tabId);
629672
tabView.destroy();
630673
}
631674

0 commit comments

Comments
 (0)