Skip to content

refactor(desktop): use serialize addon to serialize/restore for terminal tab switching#656

Merged
Kitenite merged 4 commits intomainfrom
tabby-explore
Jan 7, 2026
Merged

refactor(desktop): use serialize addon to serialize/restore for terminal tab switching#656
Kitenite merged 4 commits intomainfrom
tabby-explore

Conversation

@Kitenite
Copy link
Copy Markdown
Collaborator

@Kitenite Kitenite commented Jan 7, 2026

Summary

  • Fix terminal escape sequence artifacts on tab switching by replacing raw scrollback byte persistence with xterm's SerializeAddon for clean state restoration
  • Simplify terminal architecture by removing disk-based history persistence, DataBatcher, and escape sequence filtering (~1800 lines deleted)

Architecture Change

Before: Raw scrollback bytes stored on disk → replayed on reattach → escape sequence corruption

After (Tabby-style):

  1. On detach: renderer serializes parsed terminal content via SerializeAddon
  2. Main process stores serializedState in memory on TerminalSession
  3. On reattach: main returns serializedState, renderer writes to new xterm instance

Files Changed

Change Files
Added SerializeAddon Terminal.tsx, helpers.ts
Updated types types.ts (serializedState replaces scrollback+wasRecovered)
Simplified session.ts, manager.ts, terminal.ts (tRPC router)
Deleted terminal-history.ts, data-batcher.ts, terminal-escape-filter.ts, tests

Testing

  • Manual testing: switch between terminal tabs, verify no escape sequence artifacts
  • Verify port detection still works
  • Verify initial commands execute correctly

Summary by CodeRabbit

  • New Features

    • Terminal detach/reattach now uses serialized terminal state (faster, GPU-backed serialization).
  • Changes

    • Terminal history persistence removed — scrollback is no longer saved to disk.
    • Killing a terminal no longer accepts a history-delete option.
    • Detach now preserves serialized state; reattach restores from serialized state.
    • Clear scrollback now also clears any preserved serialized state.
  • Removals

    • Automatic port-hint scanning from terminal output and related heuristics removed.

✏️ Tip: You can customize this high-level summary in your review settings.

…ab switching

Replace disk-based scrollback persistence with xterm's SerializeAddon for clean
terminal state restoration when switching tabs. This eliminates escape sequence
artifacts that occurred when replaying raw terminal bytes.

Key changes:
- Add SerializeAddon to renderer, serialize state on detach, restore on reattach
- Remove DataBatcher (direct PTY→IPC emission - xterm handles rendering efficiently)
- Remove HistoryWriter and disk persistence (terminal-history.ts)
- Remove terminal-escape-filter.ts (no longer needed)
- Simplify session.ts (~120 lines vs ~200 before)
- Update types: serializedState replaces scrollback+wasRecovered in SessionResult

Architecture (Tabby-style):
1. On detach: renderer serializes parsed terminal content via SerializeAddon
2. Main process stores serializedState in memory on TerminalSession
3. On reattach: main returns serializedState, renderer writes to new xterm instance

Benefits:
- No escape sequence corruption on tab switch
- Simpler codebase (~1800 lines deleted)
- No disk I/O for terminal history
- Direct data flow (PTY → IPC → xterm)
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 7, 2026

📝 Walkthrough

Walkthrough

Replaces filesystem-backed terminal history and batching with serialized in-memory state. Sessions now exchange serializedState on create/attach/detach; history, DataBatcher, escape-filter, and related tests/modules are removed. Renderer integrates xterm SerializeAddon to capture and restore terminal state.

Changes

Cohort / File(s) Summary
tRPC Terminal Router
apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
createOrAttach now returns { paneId, isNew, serializedState } instead of scrollback/wasRecovered; kill input removes deleteHistory; detach accepts optional serializedState; clearScrollback is now synchronous.
Terminal Manager & Session Core
apps/desktop/src/main/lib/terminal/manager.ts, apps/desktop/src/main/lib/terminal/session.ts, apps/desktop/src/main/lib/terminal/types.ts
Removed history recovery, batching, and writer/reader usage. Session result/type changed to use serializedState. Added useFallbackShell path, setupInitialCommands; detach, kill, and clearScrollback signatures/behavior simplified.
Removed History & Batching Modules
apps/desktop/src/main/lib/terminal-history.ts, apps/desktop/src/main/lib/data-batcher.ts
Deleted filesystem-backed history (HistoryWriter/Reader, metadata helpers) and DataBatcher batching implementation.
Removed Escape Filter & Port-Hints
apps/desktop/src/main/lib/terminal-escape-filter.ts, apps/desktop/src/main/lib/terminal-escape-filter.test.ts, apps/desktop/src/main/lib/terminal/port-hints.ts
Deleted escape-sequence clear detection and content-extraction utilities and port-hint regex helper (and its tests).
Removed Tests
apps/desktop/src/main/lib/terminal-history.test.ts, apps/desktop/src/main/lib/terminal/manager.test.ts, apps/desktop/src/main/lib/terminal/session.test.ts
Removed comprehensive tests covering history persistence, manager lifecycle, recovery, and batching.
Renderer: Terminal Serialization
apps/desktop/src/renderer/.../Terminal/Terminal.tsx, .../Terminal/helpers.ts
Integrates SerializeAddon from @xterm/addon-serialize; createTerminalInstance exposes serializeAddon; renderer serializes on detach and applies serializedState on reattach instead of applying recovered scrollback/events.
Port Manager Adjustments
apps/desktop/src/main/lib/terminal/port-manager.ts
Removed output-triggered hint scanning (checkOutputForHint, scanPane) and import of containsPortHint; periodic scanning remains.
UI Cleanup Callsites
apps/desktop/src/renderer/stores/tabs/utils/terminal-cleanup.ts
Updated kill callsites to mutate({ paneId }) (removed deleteHistory payload).

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor User
    participant Renderer as Renderer (Terminal.tsx)
    participant Xterm as Xterm + SerializeAddon
    participant Manager as TerminalManager
    participant Session as TerminalSession

    Note over Renderer,Xterm: Detach flow (capture & store)
    User->>Renderer: request detach
    Renderer->>Xterm: serialize() → serializedState
    Xterm-->>Renderer: serializedState (string)
    Renderer->>Manager: detach({ paneId, serializedState })
    Manager->>Session: attach serializedState to session

    Note over Manager,Renderer: Reattach flow (restore)
    User->>Renderer: request reattach/createOrAttach
    Renderer->>Manager: createOrAttach({ paneId, ... })
    Manager->>Session: lookup session (has serializedState)
    Session-->>Manager: { serializedState }
    Manager-->>Renderer: { paneId, isNew:false, serializedState }
    Renderer->>Xterm: apply(serializedState)
    Xterm-->>Renderer: restored buffer & cursor
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 I nibbled logs and cleared the stash,
Swapped files for strings in a single dash.
Serialize, detach, then hop back in place—
A fluffy state saved, restored with grace. 🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description check ✅ Passed The pull request description provides a clear summary, architecture change explanation, comprehensive file listing, and a testing checklist. However, the testing section lacks completion status and related GitHub issues are not linked.
Title check ✅ Passed The title accurately summarizes the main change: adopting xterm's SerializeAddon for terminal state serialization/restoration during tab switching, replacing disk-based history persistence.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
apps/desktop/src/main/lib/terminal/session.ts (2)

87-95: Potential unbounded scrollback growth.

The session.scrollback accumulates all PTY output for port detection hints but has no upper bound. For long-running sessions with verbose output, this could consume significant memory.

Consider capping the scrollback buffer (e.g., keep only the last N characters or clear it periodically after port detection windows).

♻️ Suggested improvement
+const MAX_SCROLLBACK_SIZE = 100_000; // ~100KB for port detection
+
 ptyProcess.onData((data) => {
   session.scrollback += data;
+  // Trim to prevent unbounded growth (port detection only needs recent output)
+  if (session.scrollback.length > MAX_SCROLLBACK_SIZE) {
+    session.scrollback = session.scrollback.slice(-MAX_SCROLLBACK_SIZE);
+  }
   // Check for hints that a port may have been opened (triggers immediate scan)
   portManager.checkOutputForHint(data, session.paneId);
   // Direct emission to renderer
   onData(paneId, data);
 });

119-136: Extract magic number for shell prompt delay.

The 100ms delay (line 136) for shell prompt readiness is a magic number. Consider extracting it to a named constant for clarity.

♻️ Suggested improvement
+/** Small delay to ensure shell prompt is fully ready before sending commands */
+const SHELL_PROMPT_DELAY_MS = 100;
+
 // Wait for first data (shell prompt ready), then send commands
 const dataHandler = session.pty.onData(() => {
   dataHandler.dispose(); // Only trigger once
 
-  setTimeout(() => {
+  setTimeout(() => {
     if (session.isAlive) {
       // ...
     }
-  }, 100);
+  }, SHELL_PROMPT_DELAY_MS);
 });
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8c0d3ca and de9f025.

📒 Files selected for processing (14)
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
  • apps/desktop/src/main/lib/data-batcher.ts
  • apps/desktop/src/main/lib/terminal-escape-filter.test.ts
  • apps/desktop/src/main/lib/terminal-escape-filter.ts
  • apps/desktop/src/main/lib/terminal-history.test.ts
  • apps/desktop/src/main/lib/terminal-history.ts
  • apps/desktop/src/main/lib/terminal/manager.test.ts
  • apps/desktop/src/main/lib/terminal/manager.ts
  • apps/desktop/src/main/lib/terminal/session.test.ts
  • apps/desktop/src/main/lib/terminal/session.ts
  • apps/desktop/src/main/lib/terminal/types.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts
  • apps/desktop/src/renderer/stores/tabs/utils/terminal-cleanup.ts
💤 Files with no reviewable changes (7)
  • apps/desktop/src/main/lib/data-batcher.ts
  • apps/desktop/src/main/lib/terminal/manager.test.ts
  • apps/desktop/src/main/lib/terminal-escape-filter.test.ts
  • apps/desktop/src/main/lib/terminal-escape-filter.ts
  • apps/desktop/src/main/lib/terminal-history.ts
  • apps/desktop/src/main/lib/terminal-history.test.ts
  • apps/desktop/src/main/lib/terminal/session.test.ts
🧰 Additional context used
📓 Path-based instructions (6)
apps/desktop/**/*.{ts,tsx}

📄 CodeRabbit inference engine (apps/desktop/AGENTS.md)

apps/desktop/**/*.{ts,tsx}: For Electron interprocess communication, ALWAYS use tRPC as defined in src/lib/trpc
Use alias as defined in tsconfig.json when possible
Prefer zustand for state management if it makes sense. Do not use effect unless absolutely necessary.
For tRPC subscriptions with trpc-electron, ALWAYS use the observable pattern from @trpc/server/observable instead of async generators, as the library explicitly checks isObservable(result) and throws an error otherwise

Files:

  • apps/desktop/src/renderer/stores/tabs/utils/terminal-cleanup.ts
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx
  • apps/desktop/src/main/lib/terminal/session.ts
  • apps/desktop/src/main/lib/terminal/types.ts
  • apps/desktop/src/main/lib/terminal/manager.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use object parameters for functions with 2+ parameters instead of positional arguments
Functions with 2+ parameters should accept a single params object with named properties for self-documentation and extensibility
Use prefixed console logging with context pattern: [domain/operation] message
Extract magic numbers and hardcoded values to named constants at module top
Use lookup objects/maps instead of repeated if (type === ...) conditionals
Avoid using any type - maintain type safety in TypeScript code
Never swallow errors silently - at minimum log them with context
Import from concrete files directly when possible - avoid barrel file abuse that creates circular dependencies
Avoid deep nesting (4+ levels) - use early returns, extract functions, and invert conditions
Use named properties in options objects instead of boolean parameters to avoid boolean blindness

Files:

  • apps/desktop/src/renderer/stores/tabs/utils/terminal-cleanup.ts
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx
  • apps/desktop/src/main/lib/terminal/session.ts
  • apps/desktop/src/main/lib/terminal/types.ts
  • apps/desktop/src/main/lib/terminal/manager.ts
apps/desktop/src/renderer/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Never import Node.js modules (fs, path, os, net) in renderer process or shared code - they are externalized for browser compatibility

Files:

  • apps/desktop/src/renderer/stores/tabs/utils/terminal-cleanup.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx
apps/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Drizzle ORM for all database operations - never use raw SQL

Files:

  • apps/desktop/src/renderer/stores/tabs/utils/terminal-cleanup.ts
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx
  • apps/desktop/src/main/lib/terminal/session.ts
  • apps/desktop/src/main/lib/terminal/types.ts
  • apps/desktop/src/main/lib/terminal/manager.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Biome for formatting and linting - run at root level with bun run lint:fix or biome check --write

Files:

  • apps/desktop/src/renderer/stores/tabs/utils/terminal-cleanup.ts
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx
  • apps/desktop/src/main/lib/terminal/session.ts
  • apps/desktop/src/main/lib/terminal/types.ts
  • apps/desktop/src/main/lib/terminal/manager.ts
**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

One component per file - do not create multi-component files

Files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx
🧠 Learnings (3)
📚 Learning: 2025-12-21T04:39:28.543Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: apps/desktop/AGENTS.md:0-0
Timestamp: 2025-12-21T04:39:28.543Z
Learning: Applies to apps/desktop/**/*.{ts,tsx} : For Electron interprocess communication, ALWAYS use tRPC as defined in `src/lib/trpc`

Applied to files:

  • apps/desktop/src/renderer/stores/tabs/utils/terminal-cleanup.ts
📚 Learning: 2026-01-02T06:50:28.671Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-02T06:50:28.671Z
Learning: Applies to apps/desktop/src/renderer/**/*.{ts,tsx} : Never import Node.js modules (fs, path, os, net) in renderer process or shared code - they are externalized for browser compatibility

Applied to files:

  • apps/desktop/src/main/lib/terminal/session.ts
📚 Learning: 2026-01-02T06:50:28.671Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-02T06:50:28.671Z
Learning: Applies to apps/desktop/src/shared/ipc-channels.ts : Define IPC channel types in apps/desktop/src/shared/ipc-channels.ts before implementing handlers

Applied to files:

  • apps/desktop/src/main/lib/terminal/types.ts
🧬 Code graph analysis (3)
apps/desktop/src/renderer/stores/tabs/utils/terminal-cleanup.ts (1)
apps/desktop/src/renderer/lib/trpc-client.ts (1)
  • trpcClient (9-11)
apps/desktop/src/main/lib/terminal/session.ts (2)
apps/desktop/src/main/lib/terminal/types.ts (1)
  • TerminalSession (3-19)
apps/desktop/src/main/lib/terminal/port-manager.ts (1)
  • portManager (281-281)
apps/desktop/src/main/lib/terminal/manager.ts (3)
apps/desktop/src/main/lib/terminal/types.ts (2)
  • CreateSessionParams (40-51)
  • TerminalSession (3-19)
apps/desktop/src/main/lib/terminal/index.ts (1)
  • CreateSessionParams (3-3)
apps/desktop/src/main/lib/terminal/session.ts (1)
  • setupInitialCommands (104-138)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build
🔇 Additional comments (16)
apps/desktop/src/renderer/stores/tabs/utils/terminal-cleanup.ts (1)

3-10: LGTM!

The simplification aligns well with the PR's removal of disk-based history. The mutation now only requires paneId, error handling logs with context, and uses the standalone tRPC client appropriately for non-React contexts.

apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts (1)

6-6: LGTM!

SerializeAddon integration follows the established pattern for other xterm addons. The addon is created, loaded, and exposed correctly. The lifecycle is consistent with other addons like ClipboardAddon and Unicode11Addon which also rely on xterm.dispose() for cleanup.

Also applies to: 109-109, 123-123, 133-133, 192-192

apps/desktop/src/main/lib/terminal/types.ts (2)

11-14: LGTM!

Clear separation of concerns: scrollback for port detection (raw PTY output), serializedState for renderer's parsed terminal content. The comments effectively document the purpose of each field.


34-38: LGTM!

SessionResult now returns serializedState instead of scrollback/wasRecovered. The type is non-optional (always returns a string, empty for new sessions), which provides a consistent API contract.

apps/desktop/src/lib/trpc/routers/terminal/terminal.ts (3)

94-98: LGTM!

The return type correctly exposes serializedState for the renderer to restore terminal content on reattachment.


146-160: LGTM!

The detach mutation now accepts serializedState from the renderer's SerializeAddon, enabling clean reattachment. Good documentation in the comment.


172-174: Synchronous mutation is correct.

The removal of async/await is appropriate since terminalManager.clearScrollback() is now synchronous (returns void). The mutation handler can remain async-compatible since tRPC handles both.

apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx (3)

474-488: LGTM! Serialization options are well-chosen.

  • excludeAltBuffer: true - Correctly excludes alternate screen buffer (vim, less, etc.)
  • excludeModes: true - Avoids capturing terminal mode states
  • scrollback: 1000 - Reasonable limit to prevent large serialized states

The cleanup properly serializes before detaching and clears the ref.


212-217: Simplified stream handling looks correct.

With Tabby-style serialize/restore, events no longer need queuing—the terminal subscription is enabled immediately after xterm is ready. The guard for xtermRef.current prevents writes before initialization.


310-316: Serialization approach is correct and follows xterm.js documentation.

SerializeAddon.serialize() outputs VT/ANSI escape sequences that encode framebuffer content (text, colors, styles), and Terminal.write() is the documented method to restore state by replaying those sequences. The implementation at lines 310-316 matches the standard xterm.js pattern. No changes needed.

apps/desktop/src/main/lib/terminal/session.ts (1)

72-85: LGTM!

Session initialization is clean. scrollback starts empty and serializedState is intentionally omitted (set later by renderer on detach).

apps/desktop/src/main/lib/terminal/manager.ts (5)

33-37: LGTM!

Correctly returns stored serializedState for existing sessions, enabling clean reattachment. Falls back to empty string if no state is stored yet.


71-76: LGTM!

Initial commands are properly set up only for new sessions, with conditional agent hooks waiting. This replaces the previous complex data handler setup.


224-238: LGTM!

Detach correctly stores the serializedState from the renderer for later reattachment. The conditional store (only if truthy) prevents accidentally clearing state with an empty string.


240-255: LGTM!

Clearing both scrollback and serializedState ensures a clean slate for the terminal buffer, consistent with the user's clear command intention.


208-222: LGTM!

The simplified kill method removes the deleteHistory parameter, aligning with the removal of disk-based history persistence. The method correctly handles both alive and dead sessions.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In @apps/desktop/src/main/lib/terminal/session.ts:
- Around line 122-124: Wrap the session.pty.write(initialCommandString) call in
a try/catch so any failure when the PTY has closed or errors is not swallowed;
inside the catch log the error with context (include session id or other
identifying info and the initialCommandString) using the existing logging
facility and avoid rethrowing unless caller expects it; keep the isAlive check
(session.isAlive) but still guard the write with try/catch to surface and record
failures.
- Around line 96-100: The function setupInitialCommands currently takes three
positional parameters (session, initialCommands, beforeInitialCommands);
refactor it to accept a single params object (e.g., { session, initialCommands,
beforeInitialCommands }) by updating the function signature, destructuring those
named properties inside the function, and preserving the existing types and
optionality. Update all call sites to pass an object with those named keys
instead of positional args, and ensure any exported type annotations or usages
(e.g., in TerminalSession contexts) are adjusted to the new param shape so
TypeScript signatures remain correct.
🧹 Nitpick comments (5)
apps/desktop/src/main/lib/terminal/port-manager.ts (2)

11-11: Remove unused constant.

The _HINT_SCAN_DELAY_MS constant is declared but never used after the removal of hint-based scanning. The underscore prefix suggests it's intended to be internal/unused.

🧹 Proposed cleanup
-// Delay before running an immediate scan triggered by output hint (in ms)
-// This gives the server time to fully bind the port
-const _HINT_SCAN_DELAY_MS = 500;
-
 // Ports to ignore (common system ports that are usually not dev servers)
 const IGNORED_PORTS = new Set([22, 80, 443, 5432, 3306, 6379, 27017]);

25-25: Consider removing unused pendingHintScans infrastructure.

The pendingHintScans map is no longer populated after removing checkOutputForHint. It's only cleared in unregisterSession but never has entries added, making it effectively dead code.

🧹 Proposed cleanup
 class PortManager extends EventEmitter {
 	private ports = new Map<string, DetectedPort>();
 	private sessions = new Map<string, RegisteredSession>();
 	private scanInterval: ReturnType<typeof setInterval> | null = null;
-	private pendingHintScans = new Map<string, ReturnType<typeof setTimeout>>();
 	private isScanning = false;
 	unregisterSession(paneId: string): void {
 		this.sessions.delete(paneId);
 		this.removePortsForPane(paneId);
-
-		// Cancel any pending hint scan for this pane
-		const pendingTimeout = this.pendingHintScans.get(paneId);
-		if (pendingTimeout) {
-			clearTimeout(pendingTimeout);
-			this.pendingHintScans.delete(paneId);
-		}
 	}
 	stopPeriodicScan(): void {
 		if (this.scanInterval) {
 			clearInterval(this.scanInterval);
 			this.scanInterval = null;
 		}
-
-		for (const timeout of this.pendingHintScans.values()) {
-			clearTimeout(timeout);
-		}
-		this.pendingHintScans.clear();
 	}

Also applies to: 47-52, 80-83

apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx (1)

308-312: Consider validating serializedState before writing.

The applySerializedState function writes serialized content directly to xterm without validation. If serializedState contains unexpected control sequences or is corrupted, it could cause terminal rendering issues.

🛡️ Add validation
 const applySerializedState = (serializedState: string) => {
 	if (serializedState) {
-		xterm.write(serializedState);
+		try {
+			xterm.write(serializedState);
+		} catch (error) {
+			console.warn(`[Terminal] Failed to apply serialized state for pane ${paneId}:`, error);
+		}
 	}
 };
apps/desktop/src/main/lib/terminal/session.ts (2)

105-106: Move initialCommandString construction after early return.

The initialCommandString is constructed before checking if initialCommands is empty, wasting computation when commands aren't needed.

⚡ Optimize construction order
 	if (!initialCommands || initialCommands.length === 0) {
 		return;
 	}

 	const initialCommandString = `${initialCommands.join(" && ")}\n`;

110-110: Extract magic number to named constant.

The 100ms delay before running initial commands is a hardcoded magic number. It should be extracted to a named constant at the module top for clarity and maintainability.

📝 Extract to named constant

At the top of the file after AGENT_HOOKS_TIMEOUT_MS:

 const DEFAULT_COLS = 80;
 const DEFAULT_ROWS = 24;
 /** Max time to wait for agent hooks before running initial commands */
 const AGENT_HOOKS_TIMEOUT_MS = 2000;
+/** Delay after first shell output before running initial commands */
+const INITIAL_COMMAND_DELAY_MS = 100;

Then use it:

-		setTimeout(() => {
+		setTimeout(() => {
 			if (session.isAlive) {
 				void (async () => {
 					if (beforeInitialCommands) {
 						const timeout = new Promise<void>((resolve) =>
 							setTimeout(resolve, AGENT_HOOKS_TIMEOUT_MS),
 						);
 						await Promise.race([beforeInitialCommands, timeout]).catch(
 							() => {},
 						);
 					}
 
 					if (session.isAlive) {
 						session.pty.write(initialCommandString);
 					}
 				})();
 			}
-		}, 100);
+		}, INITIAL_COMMAND_DELAY_MS);

As per coding guidelines: Extract magic numbers and hardcoded values to named constants at module top.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between de9f025 and 1b6b7e4.

📒 Files selected for processing (7)
  • apps/desktop/src/main/lib/terminal/manager.ts
  • apps/desktop/src/main/lib/terminal/port-hints.ts
  • apps/desktop/src/main/lib/terminal/port-manager.ts
  • apps/desktop/src/main/lib/terminal/session.ts
  • apps/desktop/src/main/lib/terminal/types.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx
  • apps/desktop/src/renderer/stores/tabs/utils/terminal-cleanup.ts
💤 Files with no reviewable changes (1)
  • apps/desktop/src/main/lib/terminal/port-hints.ts
🧰 Additional context used
📓 Path-based instructions (6)
apps/desktop/**/*.{ts,tsx}

📄 CodeRabbit inference engine (apps/desktop/AGENTS.md)

apps/desktop/**/*.{ts,tsx}: For Electron interprocess communication, ALWAYS use tRPC as defined in src/lib/trpc
Use alias as defined in tsconfig.json when possible
Prefer zustand for state management if it makes sense. Do not use effect unless absolutely necessary.
For tRPC subscriptions with trpc-electron, ALWAYS use the observable pattern from @trpc/server/observable instead of async generators, as the library explicitly checks isObservable(result) and throws an error otherwise

Files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx
  • apps/desktop/src/renderer/stores/tabs/utils/terminal-cleanup.ts
  • apps/desktop/src/main/lib/terminal/types.ts
  • apps/desktop/src/main/lib/terminal/port-manager.ts
  • apps/desktop/src/main/lib/terminal/session.ts
  • apps/desktop/src/main/lib/terminal/manager.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use object parameters for functions with 2+ parameters instead of positional arguments
Functions with 2+ parameters should accept a single params object with named properties for self-documentation and extensibility
Use prefixed console logging with context pattern: [domain/operation] message
Extract magic numbers and hardcoded values to named constants at module top
Use lookup objects/maps instead of repeated if (type === ...) conditionals
Avoid using any type - maintain type safety in TypeScript code
Never swallow errors silently - at minimum log them with context
Import from concrete files directly when possible - avoid barrel file abuse that creates circular dependencies
Avoid deep nesting (4+ levels) - use early returns, extract functions, and invert conditions
Use named properties in options objects instead of boolean parameters to avoid boolean blindness

Files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx
  • apps/desktop/src/renderer/stores/tabs/utils/terminal-cleanup.ts
  • apps/desktop/src/main/lib/terminal/types.ts
  • apps/desktop/src/main/lib/terminal/port-manager.ts
  • apps/desktop/src/main/lib/terminal/session.ts
  • apps/desktop/src/main/lib/terminal/manager.ts
apps/desktop/src/renderer/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Never import Node.js modules (fs, path, os, net) in renderer process or shared code - they are externalized for browser compatibility

Files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx
  • apps/desktop/src/renderer/stores/tabs/utils/terminal-cleanup.ts
**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

One component per file - do not create multi-component files

Files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx
apps/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Drizzle ORM for all database operations - never use raw SQL

Files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx
  • apps/desktop/src/renderer/stores/tabs/utils/terminal-cleanup.ts
  • apps/desktop/src/main/lib/terminal/types.ts
  • apps/desktop/src/main/lib/terminal/port-manager.ts
  • apps/desktop/src/main/lib/terminal/session.ts
  • apps/desktop/src/main/lib/terminal/manager.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Biome for formatting and linting - run at root level with bun run lint:fix or biome check --write

Files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx
  • apps/desktop/src/renderer/stores/tabs/utils/terminal-cleanup.ts
  • apps/desktop/src/main/lib/terminal/types.ts
  • apps/desktop/src/main/lib/terminal/port-manager.ts
  • apps/desktop/src/main/lib/terminal/session.ts
  • apps/desktop/src/main/lib/terminal/manager.ts
🧠 Learnings (2)
📚 Learning: 2025-12-21T04:39:28.543Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: apps/desktop/AGENTS.md:0-0
Timestamp: 2025-12-21T04:39:28.543Z
Learning: Applies to apps/desktop/**/*.{ts,tsx} : For Electron interprocess communication, ALWAYS use tRPC as defined in `src/lib/trpc`

Applied to files:

  • apps/desktop/src/renderer/stores/tabs/utils/terminal-cleanup.ts
📚 Learning: 2026-01-02T06:50:28.671Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-02T06:50:28.671Z
Learning: Applies to apps/desktop/src/shared/ipc-channels.ts : Define IPC channel types in apps/desktop/src/shared/ipc-channels.ts before implementing handlers

Applied to files:

  • apps/desktop/src/main/lib/terminal/types.ts
🧬 Code graph analysis (2)
apps/desktop/src/main/lib/terminal/session.ts (1)
apps/desktop/src/main/lib/terminal/types.ts (1)
  • TerminalSession (3-16)
apps/desktop/src/main/lib/terminal/manager.ts (2)
apps/desktop/src/main/lib/terminal/types.ts (2)
  • CreateSessionParams (37-48)
  • TerminalSession (3-16)
apps/desktop/src/main/lib/terminal/session.ts (1)
  • setupInitialCommands (96-129)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build
🔇 Additional comments (5)
apps/desktop/src/main/lib/terminal/types.ts (1)

11-11: Type inconsistency between TerminalSession.serializedState and SessionResult.serializedState.

TerminalSession.serializedState is optional (string?) while SessionResult.serializedState is required (string). All code paths returning SessionResult already provide serializedState (using empty string fallback), so the implementation is sound. However, consider aligning the type definitions for better consistency: either make SessionResult.serializedState optional to match TerminalSession, or document why SessionResult always requires a value.

apps/desktop/src/main/lib/terminal/manager.ts (4)

6-6: LGTM - Clean import refactoring.

The shift from legacy history-based functions to createSession and setupInitialCommands aligns with the serialized-state approach and simplifies the session lifecycle.


17-91: Session lifecycle correctly refactored to use serialized state.

The migration from scrollback-based recovery to in-memory serializedState is implemented correctly:

  • Existing sessions return stored serializedState (line 35)
  • New sessions initialize with empty state (line 89)
  • setupInitialCommands properly replaces the old history-based initialization

Note: Serialized terminal state is now held in memory per session. For very long-running sessions with extensive output, this could grow, but it's the intended tradeoff for eliminating disk I/O and escape-sequence artifacts.


237-250: Verify clearScrollback clears both state and display.

The method now only clears the in-memory serializedState (line 248) without sending a clear sequence to the terminal's pty. This means:

  • State tracking is reset ✓
  • Terminal display content is not cleared

If the intent is to also clear the visible scrollback in the terminal, the implementation should additionally send the appropriate control sequence (e.g., \x1b[2J\x1b[3J\x1b[H) via session.pty.write().

Confirm whether the renderer handles display clearing separately or if this method should do both.


222-235: No action neededserializedState is consistently provided in all detach calls from the renderer. The Terminal component always serializes terminal state using serializeAddon.serialize() before calling detach, so the optional handling in the method is defensive but not indicative of an actual issue.

Likely an incorrect or invalid review comment.

Comment on lines +96 to 100
export function setupInitialCommands(
session: TerminalSession,
initialCommands: string[] | undefined,
wasRecovered: boolean,
onHistoryReinit: () => Promise<void>,
beforeInitialCommands?: Promise<void>,
): void {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Use object parameter for function with 3 parameters.

setupInitialCommands has 3 parameters and should accept a single params object per coding guidelines.

♻️ Refactor to object parameter
 /**
  * Set up initial commands to run after shell prompt is ready.
  * Commands are only sent for new sessions (not reattachments).
  */
-export function setupInitialCommands(
-	session: TerminalSession,
-	initialCommands: string[] | undefined,
-	beforeInitialCommands?: Promise<void>,
-): void {
+export function setupInitialCommands(params: {
+	session: TerminalSession;
+	initialCommands: string[] | undefined;
+	beforeInitialCommands?: Promise<void>;
+}): void {
+	const { session, initialCommands, beforeInitialCommands } = params;
 	if (!initialCommands || initialCommands.length === 0) {
 		return;
 	}

As per coding guidelines: Functions with 2+ parameters should accept a single params object with named properties for self-documentation and extensibility.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function setupInitialCommands(
session: TerminalSession,
initialCommands: string[] | undefined,
wasRecovered: boolean,
onHistoryReinit: () => Promise<void>,
beforeInitialCommands?: Promise<void>,
): void {
export function setupInitialCommands(params: {
session: TerminalSession;
initialCommands: string[] | undefined;
beforeInitialCommands?: Promise<void>;
}): void {
const { session, initialCommands, beforeInitialCommands } = params;
🤖 Prompt for AI Agents
In @apps/desktop/src/main/lib/terminal/session.ts around lines 96 - 100, The
function setupInitialCommands currently takes three positional parameters
(session, initialCommands, beforeInitialCommands); refactor it to accept a
single params object (e.g., { session, initialCommands, beforeInitialCommands })
by updating the function signature, destructuring those named properties inside
the function, and preserving the existing types and optionality. Update all call
sites to pass an object with those named keys instead of positional args, and
ensure any exported type annotations or usages (e.g., in TerminalSession
contexts) are adjusted to the new param shape so TypeScript signatures remain
correct.

Comment thread apps/desktop/src/main/lib/terminal/session.ts
@Kitenite Kitenite changed the title refactor(desktop): adopt Tabby-style serialize/restore for terminal tab switching refactor(desktop): adopt use serialize addonserialize/restore for terminal tab switching Jan 7, 2026
@Kitenite Kitenite changed the title refactor(desktop): adopt use serialize addonserialize/restore for terminal tab switching refactor(desktop): adopt use serialize addon to serialize/restore for terminal tab switching Jan 7, 2026
@Kitenite Kitenite changed the title refactor(desktop): adopt use serialize addon to serialize/restore for terminal tab switching refactor(desktop): use serialize addon to serialize/restore for terminal tab switching Jan 7, 2026
@Kitenite Kitenite merged commit ccf9cdb into main Jan 7, 2026
4 checks passed
@Kitenite Kitenite deleted the tabby-explore branch January 7, 2026 23:50
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jan 7, 2026

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ⚠️ Neon database branch
  • ⚠️ Electric Fly.io app

Thank you for your contribution! 🎉

andreasasprou added a commit to andreasasprou/superset that referenced this pull request Jan 8, 2026
@Kitenite Kitenite restored the tabby-explore branch January 8, 2026 23:22
@Kitenite Kitenite deleted the tabby-explore branch January 8, 2026 23:56
saddlepaddle pushed a commit that referenced this pull request Jan 10, 2026
…nal tab switching (#656)

* refactor(desktop): adopt Tabby-style serialize/restore for terminal tab switching

Replace disk-based scrollback persistence with xterm's SerializeAddon for clean
terminal state restoration when switching tabs. This eliminates escape sequence
artifacts that occurred when replaying raw terminal bytes.

Key changes:
- Add SerializeAddon to renderer, serialize state on detach, restore on reattach
- Remove DataBatcher (direct PTY→IPC emission - xterm handles rendering efficiently)
- Remove HistoryWriter and disk persistence (terminal-history.ts)
- Remove terminal-escape-filter.ts (no longer needed)
- Simplify session.ts (~120 lines vs ~200 before)
- Update types: serializedState replaces scrollback+wasRecovered in SessionResult

Architecture (Tabby-style):
1. On detach: renderer serializes parsed terminal content via SerializeAddon
2. Main process stores serializedState in memory on TerminalSession
3. On reattach: main returns serializedState, renderer writes to new xterm instance

Benefits:
- No escape sequence corruption on tab switch
- Simpler codebase (~1800 lines deleted)
- No disk I/O for terminal history
- Direct data flow (PTY → IPC → xterm)

* clean up and remove port hints
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant