Skip to content

fix(desktop): filter escape sequences when saving terminal history#280

Closed
Kitenite wants to merge 7 commits intomainfrom
unable-peacock-d18f8d
Closed

fix(desktop): filter escape sequences when saving terminal history#280
Kitenite wants to merge 7 commits intomainfrom
unable-peacock-d18f8d

Conversation

@Kitenite
Copy link
Copy Markdown
Collaborator

@Kitenite Kitenite commented Dec 8, 2025

Summary

  • Filter escape sequences in HistoryWriter.init() before persisting initial scrollback to disk
  • Keep filtering on restore to handle legacy unfiltered data
  • Ensures clean terminal history storage and prevents garbage characters on restore

Test plan

  • Existing terminal tests pass
  • Manual test: Open terminal, run commands, restart app, verify no escape sequence artifacts

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Persist terminal scrollback on detach/exit and restore cleaner session histories; renderer now serializes scrollback before cleanup.
  • Bug Fixes
    • More aggressive buffering and stricter handling of incomplete terminal escape sequences to prevent garbled output and orphaned fragments.
  • Tests
    • Expanded coverage for multi-chunk, rapid query-response, and flush scenarios to validate edge-case behavior.

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

Previously, escape sequences were only filtered when restoring terminal
history, not when initially saving recovered scrollback. This could leave
unfiltered data on disk.

Now HistoryWriter.init() filters initial scrollback before persisting,
ensuring clean storage. Filtering on restore is kept for legacy data.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented Dec 8, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
website Ready Ready Preview Comment Dec 8, 2025 1:04am

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 8, 2025

Walkthrough

Adds persistent serialized scrollback support from renderer to main via a new saveScrollback flow (trpc endpoint → TerminalManager → HistoryWriter), integrates xterm SerializeAddon to produce that data, and tightens TerminalEscapeFilter buffering/flush logic and tests to avoid leaking incomplete escape/query sequences across chunk boundaries.

Changes

Cohort / File(s) Summary
Escape filtering logic
apps/desktop/src/main/lib/terminal-escape-filter.ts
Broadened CPR regex to allow omitted parameters; treat lone ESC and partial CSI (ESC[, ESC[;, etc.) as bufferable; improved isIncomplete checks; flush() discards incomplete query responses instead of emitting them.
Escape filter tests
apps/desktop/src/main/lib/terminal-escape-filter.test.ts
Updated expectations to assert buffering of incomplete sequences across chunks, renamed/adjusted flush tests to expect discarded buffered data, and added TUI-focused multi-chunk scenarios to prevent orphaned CPR parts.
Scrollback persistence API (main)
apps/desktop/src/main/lib/terminal-manager.ts
Added serializedScrollback to TerminalSession; removed in-session escapeFilter; added saveScrollback({tabId, serialized}) to persist renderer-provided serialized scrollback; changed attach/reattach logic to prefer serialized scrollback and to only filter legacy recovered scrollback when needed.
History persistence
apps/desktop/src/main/lib/terminal-history.ts
Added saveSnapshot(serialized: string) to close/reopen write stream and write provided serialized scrollback; handles stream lifecycle and errors during replace.
trpc router (renderer → main)
apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
Added saveScrollback mutation endpoint that forwards { tabId, serialized } to terminalManager.saveScrollback.
Renderer terminal wiring
apps/desktop/src/renderer/screens/.../Terminal/helpers.ts
apps/desktop/src/renderer/screens/.../Terminal/Terminal.tsx
Integrated SerializeAddon from @xterm/addon-serialize into terminal instance creation; exposed addon from createTerminalInstance; on unmount/teardown serialize scrollback (excluding alt screen) and call trpcClient.terminal.saveScrollback (fire-and-forget with logging).
Terminal manager tests
apps/desktop/src/main/lib/terminal-manager.test.ts
Added tests exercising the new saveScrollback API and updated test flows to simulate renderer persisting per-tab serialized scrollback before detach/exit.

Sequence Diagram(s)

sequenceDiagram
    participant Renderer
    participant TRPC
    participant TerminalManager
    participant HistoryWriter
    participant Filesystem

    Renderer->>Renderer: SerializeAddon.serialize(excludeAlt=true)
    Renderer->>TRPC: terminal.saveScrollback(tabId, serialized) [mutation]
    TRPC->>TerminalManager: saveScrollback({tabId, serialized})
    TerminalManager->>TerminalManager: set session.serializedScrollback
    TerminalManager->>HistoryWriter: saveSnapshot(serialized)
    HistoryWriter->>Filesystem: replace scrollback file (close, write, reopen append)
    Filesystem-->>HistoryWriter: ack / errors
    HistoryWriter-->>TerminalManager: result
    TerminalManager-->>TRPC: completion
    TRPC-->>Renderer: response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Areas needing extra attention:
    • TerminalEscapeFilter regex and buffering logic (edge cases: omitted parameters, multi-chunk sequences).
    • saveScrollback lifecycle: correctness of stream close/reopen in HistoryWriter.saveSnapshot and error handling.
    • TerminalManager attach/reattach changes (ensure legacy recovered scrollback is still safely filtered and no double-filtering occurs).
    • Renderer serialization timing on unmount (race conditions if terminal already exited) and fire-and-forget TRPC usage.

Possibly related PRs

  • fix terminal token filtering #246 — Prior changes to TerminalEscapeFilter and its integration with terminal-manager/terminal-history; relevant for comparing buffering/regex evolution.
  • add xterm addons #124 — Integration of @xterm/addon-serialize into renderer terminal flow; overlaps in SerializeAddon wiring.
  • Set up hooks for notifications #121 — Modifications to terminal-manager session handling and attach/reattach behavior; related to serializedScrollback introduction.

Poem

🐰 I nibble bytes split across the night,
Buffer the ESC, hold sequences tight —
I serialize hops and stash them in store,
Flush what’s broken, leak no more. ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(desktop): filter escape sequences when saving terminal history' accurately captures the main objective: preventing escape sequence artifacts in saved terminal history through filtering.
Description check ✅ Passed The description covers key aspects (summary of changes, test plan status) but lacks complete alignment with the template; missing explicit 'Type of Change' selection, 'Related Issues' link, and 'Screenshots' section.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch unable-peacock-d18f8d

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.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Dec 8, 2025

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch

Thank you for your contribution! 🎉

…back

When ESC appears at the end of a data chunk, it must be buffered to check
if the next chunk forms a query response. Previously, lone ESC passed
through, causing sequences like "[2R" to leak into scrollback.

Also discard incomplete query responses on flush to prevent garbage
characters (e.g., "2R1R12;2$y") from appearing after TUI apps like lazygit.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Buffer ESC[ alone at chunk end to prevent ";1R" leaking through
- Match CPR with omitted row/column (ESC[;1R, ESC[R)
- Buffer ESC[; for CPR with omitted parameters

Fixes garbage like ";1R2;1R" appearing after TUI apps.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/desktop/src/main/lib/terminal-manager.ts (1)

118-124: Potential duplicate filtering of recovered scrollback.

The recoveredScrollback is filtered here before being passed to historyWriter.init(). However, HistoryWriter.init() (lines 61-64 in terminal-history.ts) also applies the same filtering to its initialScrollback parameter.

This results in the recovered scrollback being filtered twice:

  1. Here in terminal-manager.ts (lines 121-123)
  2. In HistoryWriter.init() (lines 62-64)

While double-filtering is safe (idempotent), it's redundant. Consider removing filtering from one location.

-		// Filter escape sequences from recovered scrollback
-		// This handles legacy unfiltered data and ensures clean display
-		if (recoveredScrollback) {
-			const recoveryFilter = new TerminalEscapeFilter();
-			recoveredScrollback =
-				recoveryFilter.filter(recoveredScrollback) + recoveryFilter.flush();
-		}
+		// Note: HistoryWriter.init() filters escape sequences from initialScrollback
+		// to handle legacy unfiltered data and ensure clean storage

Alternatively, keep filtering here for display purposes and remove it from HistoryWriter.init() if storage doesn't need its own sanitization pass.

🧹 Nitpick comments (1)
apps/desktop/src/main/lib/terminal-escape-filter.test.ts (1)

420-430: Multiple assertions in single test.

This test has multiple expect calls testing different aspects of the filter's behavior. Per coding guidelines, tests should have one assert per test. However, since this tests stateful behavior where the second assertion depends on the first chunk's processing, the multi-assertion pattern is reasonable here.

Consider splitting into two tests if strict adherence is required:

  1. Test that ESC alone is buffered (returns "text")
  2. Test that subsequent chunk completing a query response is filtered
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9810396 and 011c05a.

📒 Files selected for processing (4)
  • apps/desktop/src/main/lib/terminal-escape-filter.test.ts (2 hunks)
  • apps/desktop/src/main/lib/terminal-escape-filter.ts (4 hunks)
  • apps/desktop/src/main/lib/terminal-history.ts (2 hunks)
  • apps/desktop/src/main/lib/terminal-manager.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
apps/desktop/**/*.{ts,tsx,js,jsx}

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

For Electron interprocess communication, ALWAYS use tRPC as defined in src/lib/trpc

Files:

  • apps/desktop/src/main/lib/terminal-manager.ts
  • apps/desktop/src/main/lib/terminal-escape-filter.ts
  • apps/desktop/src/main/lib/terminal-history.ts
  • apps/desktop/src/main/lib/terminal-escape-filter.test.ts
apps/desktop/**/*.{ts,tsx}

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

apps/desktop/**/*.{ts,tsx}: Please 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

Files:

  • apps/desktop/src/main/lib/terminal-manager.ts
  • apps/desktop/src/main/lib/terminal-escape-filter.ts
  • apps/desktop/src/main/lib/terminal-history.ts
  • apps/desktop/src/main/lib/terminal-escape-filter.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Co-locate component dependencies (utils, hooks, constants, config, tests, stories) next to the file using them
Avoid any type in TypeScript unless absolutely necessary

Files:

  • apps/desktop/src/main/lib/terminal-manager.ts
  • apps/desktop/src/main/lib/terminal-escape-filter.ts
  • apps/desktop/src/main/lib/terminal-history.ts
  • apps/desktop/src/main/lib/terminal-escape-filter.test.ts
apps/desktop/src/{main,renderer,shared}/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Use type-safe IPC system for communication between Electron main and renderer processes

Files:

  • apps/desktop/src/main/lib/terminal-manager.ts
  • apps/desktop/src/main/lib/terminal-escape-filter.ts
  • apps/desktop/src/main/lib/terminal-history.ts
  • apps/desktop/src/main/lib/terminal-escape-filter.test.ts
apps/desktop/**/*.test.{ts,tsx,js,jsx}

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

apps/desktop/**/*.test.{ts,tsx,js,jsx}: Tests should have one assert per test
Tests should be readable
Tests should be fast
Tests should be independent
Tests should be repeatable

Files:

  • apps/desktop/src/main/lib/terminal-escape-filter.test.ts
🧠 Learnings (2)
📚 Learning: 2025-11-24T21:33:13.267Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: apps/desktop/AGENTS.md:0-0
Timestamp: 2025-11-24T21:33:13.267Z
Learning: Applies to apps/desktop/**/*.test.{ts,tsx,js,jsx} : Tests should be repeatable

Applied to files:

  • apps/desktop/src/main/lib/terminal-escape-filter.test.ts
📚 Learning: 2025-11-24T21:33:13.267Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: apps/desktop/AGENTS.md:0-0
Timestamp: 2025-11-24T21:33:13.267Z
Learning: Applies to apps/desktop/**/*.test.{ts,tsx,js,jsx} : Tests should be independent

Applied to files:

  • apps/desktop/src/main/lib/terminal-escape-filter.test.ts
🧬 Code graph analysis (2)
apps/desktop/src/main/lib/terminal-history.ts (1)
apps/desktop/src/main/lib/terminal-escape-filter.ts (2)
  • filter (117-143)
  • TerminalEscapeFilter (110-259)
apps/desktop/src/main/lib/terminal-escape-filter.test.ts (1)
apps/desktop/src/main/lib/terminal-escape-filter.ts (2)
  • filter (117-143)
  • TerminalEscapeFilter (110-259)
⏰ 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 (6)
apps/desktop/src/main/lib/terminal-escape-filter.test.ts (2)

486-509: LGTM on flush behavior tests.

The updated flush behavior tests correctly verify that incomplete query responses are discarded rather than passed through. The test cases cover:

  • Incomplete DA1 sequences being discarded
  • Lone ESC being discarded on flush

This aligns with the PR objective to prevent garbage characters when restoring history.


512-545: Good regression test coverage for the chunk boundary bug.

These TUI scenario tests directly address the bug where ESC at chunk boundaries caused orphaned sequence parts like "[2R" to leak through. The explicit comment "This was the bug" at line 534 documents the regression scenario well.

apps/desktop/src/main/lib/terminal-escape-filter.ts (4)

21-27: LGTM on CPR pattern update.

The updated regex \d*(?:;\d*)?R correctly handles CPR variants:

  • ESC[24;1R - standard CPR
  • ESC[2R - row only
  • ESC[;1R - omitted row
  • ESC[R - both omitted (edge case)

The \d* allows zero or more digits, and (?:;\d*)? makes the semicolon and column optional.


150-169: LGTM on enhanced buffering logic.

The changes correctly handle chunk boundary cases:

  • Line 152: Buffering lone ESC prevents orphaned sequence parts like "[2R" from leaking
  • Line 164: Buffering ESC[ alone allows proper handling of sequences starting with ;
  • Lines 168-169: Buffering ESC[; handles CPR with omitted row parameter

This directly addresses the bug described in the PR.


240-251: LGTM on flush discard behavior.

The updated flush() correctly discards incomplete query responses rather than passing them through. This is the right approach because:

  1. Incomplete sequences at session end are likely query responses that didn't complete
  2. Passing them through would display garbage characters on restore
  3. The fallback replace(COMBINED_PATTERN, "") handles any complete sequences in non-query-response buffers

202-204: LGTM on isIncomplete handling.

The comment clarifies that lone ESC is treated as incomplete, which is consistent with looksLikeQueryResponse now buffering lone ESC. The str.length < 2 check correctly identifies this case.

Comment thread apps/desktop/src/main/lib/terminal-history.ts Outdated
Replace raw PTY byte storage with xterm.js serialize addon output for
terminal scrollback persistence. This properly solves the escape sequence
garbage problem because:

1. The serialize addon captures the *rendered* terminal buffer state
2. Query responses (DA1, CPR, OSC colors) never appear in the visual buffer
3. TUI apps in alternate screen mode are excluded via excludeAltBuffer option

Architecture change:
- Renderer serializes terminal state on detach via SerializeAddon.serialize()
- Calls new saveScrollback tRPC procedure to persist to main process
- Main process no longer filters/accumulates data for disk - only for in-memory
- HistoryWriter.saveSnapshot() replaces file with clean serialized data

Legacy support:
- TerminalEscapeFilter kept for migrating old unfiltered scrollback files
- Old files are filtered on first load, then replaced with clean data on save

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When reattaching to a live terminal session, return the last serialized
snapshot instead of raw accumulated scrollback. This prevents query
response garbage from appearing on reattach.

Added serializedScrollback field to TerminalSession to track the clean
serialized state separately from the raw accumulated data.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
For live terminal reattach, combine the last serialized snapshot with
filtered new data that arrived since serialization. This ensures:

1. No garbage from the serialized snapshot (already clean)
2. New data since serialization is filtered through TerminalEscapeFilter
3. No gap in terminal history between detach and reattach

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
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: 1

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

287-317: Serialization flow is sound, but consider removing debug logs for production.

The cleanup serialization logic is well-structured:

  • Null-checks prevent errors if addon isn't loaded
  • excludeAltBuffer: true correctly avoids saving TUI app state
  • Fire-and-forget with error logging is reasonable for unmount context

However, the console.log statements at lines 295-300 and 313-316 appear to be debug logging. Consider removing or gating these behind a debug flag before merging.

-			if (serializeAddon && !isExitedRef.current) {
-				try {
-					const serialized = serializeAddon.serialize({
-						excludeAltBuffer: true, // Don't save TUI app alternate screen
-					});
-					console.log(
-						"[Terminal] Serialized scrollback:",
-						serialized?.length,
-						"chars, first 200:",
-						serialized?.slice(0, 200),
-					);
-					if (serialized) {
+			if (serializeAddon && !isExitedRef.current) {
+				try {
+					const serialized = serializeAddon.serialize({
+						excludeAltBuffer: true, // Don't save TUI app alternate screen
+					});
+					if (serialized) {
						// Fire and forget - don't block unmount on save
						trpcClient.terminal.saveScrollback
							.mutate({ tabId: paneId, serialized })
							.catch((err) => {
								console.error("[Terminal] Failed to save scrollback:", err);
							});
					}
				} catch (err) {
					console.error("[Terminal] Failed to serialize terminal:", err);
				}
-			} else {
-				console.log(
-					"[Terminal] Skipping serialize:",
-					!serializeAddon ? "no addon" : "exited",
-				);
			}
apps/desktop/src/main/lib/terminal-manager.ts (1)

328-335: Consider removing debug logging for production.

Similar to the renderer-side code, these console.log statements appear to be debug output. Consider removing or gating behind a debug flag.

-		console.log(
-			"[TerminalManager] saveScrollback:",
-			tabId,
-			"length:",
-			serialized.length,
-			"first 200:",
-			serialized.slice(0, 200),
-		);
 		session.serializedScrollback = serialized;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 011c05a and ba64c71.

📒 Files selected for processing (6)
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts (1 hunks)
  • apps/desktop/src/main/lib/terminal-history.ts (2 hunks)
  • apps/desktop/src/main/lib/terminal-manager.test.ts (3 hunks)
  • apps/desktop/src/main/lib/terminal-manager.ts (5 hunks)
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx (4 hunks)
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts (4 hunks)
🧰 Additional context used
📓 Path-based instructions (7)
apps/desktop/**/*.{ts,tsx,js,jsx}

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

For Electron interprocess communication, ALWAYS use tRPC as defined in src/lib/trpc

Files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts
  • apps/desktop/src/main/lib/terminal-manager.test.ts
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
  • apps/desktop/src/main/lib/terminal-history.ts
  • apps/desktop/src/main/lib/terminal-manager.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx
apps/desktop/**/*.{ts,tsx}

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

apps/desktop/**/*.{ts,tsx}: Please 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

Files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts
  • apps/desktop/src/main/lib/terminal-manager.test.ts
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
  • apps/desktop/src/main/lib/terminal-history.ts
  • apps/desktop/src/main/lib/terminal-manager.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Co-locate component dependencies (utils, hooks, constants, config, tests, stories) next to the file using them
Avoid any type in TypeScript unless absolutely necessary

Files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts
  • apps/desktop/src/main/lib/terminal-manager.test.ts
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
  • apps/desktop/src/main/lib/terminal-history.ts
  • apps/desktop/src/main/lib/terminal-manager.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx
apps/desktop/src/{renderer,shared,preload}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Never import Node.js modules in renderer process code or shared code in the desktop app

Files:

  • 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,renderer,shared}/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Use type-safe IPC system for communication between Electron main and renderer processes

Files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts
  • apps/desktop/src/main/lib/terminal-manager.test.ts
  • apps/desktop/src/main/lib/terminal-history.ts
  • apps/desktop/src/main/lib/terminal-manager.ts
apps/desktop/**/*.test.{ts,tsx,js,jsx}

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

apps/desktop/**/*.test.{ts,tsx,js,jsx}: Tests should have one assert per test
Tests should be readable
Tests should be fast
Tests should be independent
Tests should be repeatable

Files:

  • apps/desktop/src/main/lib/terminal-manager.test.ts
**/{components,pages}/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

**/{components,pages}/**/*.tsx: Use one folder per component with structure ComponentName/ComponentName.tsx plus index.ts for barrel export
Create one component per file - do not use multi-component files

Files:

  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx
🧠 Learnings (3)
📚 Learning: 2025-11-24T21:33:13.267Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: apps/desktop/AGENTS.md:0-0
Timestamp: 2025-11-24T21:33:13.267Z
Learning: Applies to apps/desktop/**/*.test.{ts,tsx,js,jsx} : Tests should be repeatable

Applied to files:

  • apps/desktop/src/main/lib/terminal-manager.test.ts
📚 Learning: 2025-11-24T21:33:13.267Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: apps/desktop/AGENTS.md:0-0
Timestamp: 2025-11-24T21:33:13.267Z
Learning: Applies to apps/desktop/**/*.test.{ts,tsx,js,jsx} : Tests should be readable

Applied to files:

  • apps/desktop/src/main/lib/terminal-manager.test.ts
📚 Learning: 2025-11-24T21:33:13.267Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: apps/desktop/AGENTS.md:0-0
Timestamp: 2025-11-24T21:33:13.267Z
Learning: Applies to apps/desktop/**/*.test.{ts,tsx,js,jsx} : Tests should be independent

Applied to files:

  • apps/desktop/src/main/lib/terminal-manager.test.ts
🧬 Code graph analysis (3)
apps/desktop/src/lib/trpc/routers/terminal/terminal.ts (1)
apps/desktop/src/main/lib/terminal-manager.ts (1)
  • terminalManager (588-588)
apps/desktop/src/main/lib/terminal-manager.ts (1)
apps/desktop/src/main/lib/terminal-escape-filter.ts (2)
  • filter (117-143)
  • TerminalEscapeFilter (110-259)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx (2)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts (1)
  • createTerminalInstance (53-135)
apps/desktop/src/renderer/lib/trpc-client.ts (1)
  • trpcClient (9-11)
⏰ 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 (6)
apps/desktop/src/main/lib/terminal-history.ts (1)

89-112: LGTM! Well-structured snapshot persistence method.

The saveSnapshot method correctly handles the stream lifecycle: closing the existing stream before replacing the file, then reopening in append mode. Error handling is appropriately defensive.

One minor observation: if fs.writeFile at line 104 throws after the stream is closed (line 100), the method will propagate the error without reopening the stream. Subsequent write() calls will silently fail since stream is null. This seems acceptable since a failed snapshot save is likely a critical error, but consider whether you want to attempt stream recovery.

apps/desktop/src/main/lib/terminal-manager.test.ts (2)

375-379: LGTM! Tests correctly simulate the new renderer-driven serialization flow.

The test updates accurately reflect the new architecture where the renderer calls saveScrollback before detach/exit. This ensures the persistence flow is exercised end-to-end.


762-766: Multi-session persistence tests properly updated.

The multi-session test now correctly simulates cumulative scrollback persistence across sessions, matching the expected renderer behavior of serializing the complete terminal state.

Also applies to: 799-803

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

85-95: LGTM! SerializeAddon integrated following existing addon patterns.

The addon is correctly instantiated before xterm.open() and loaded after, consistent with other addons. Exposing it in the return object enables the serialization flow in Terminal.tsx.

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

149-162: LGTM! Clean tRPC endpoint following existing patterns.

The saveScrollback mutation correctly:

  • Uses zod for input validation
  • Awaits the async terminalManager.saveScrollback call
  • Includes a descriptive JSDoc comment

As per coding guidelines, this properly uses tRPC for Electron IPC communication.

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

308-342: LGTM! saveScrollback method correctly integrates with HistoryWriter.

The method properly:

  • Guards against missing sessions
  • Updates the in-memory serialized snapshot
  • Persists to disk via historyWriter.saveSnapshot

Comment on lines +76 to 110
// For live sessions, use the last serialized snapshot as the base.
// If raw scrollback has grown since then, filter and append the new data.
let scrollback = existing.serializedScrollback || "";
const rawLen = existing.scrollback.length;
const serializedLen = existing.serializedScrollback.length;
console.log(
"[TerminalManager] reattach:",
tabId,
"serializedLen:",
serializedLen,
"rawLen:",
rawLen,
"delta:",
rawLen - serializedLen,
);
if (rawLen > serializedLen) {
// There's new data since last serialization - filter it
const newData = existing.scrollback.slice(serializedLen);
console.log(
"[TerminalManager] filtering delta, first 200:",
newData.slice(0, 200),
);
const filter = new TerminalEscapeFilter();
scrollback += filter.filter(newData) + filter.flush();
}
console.log(
"[TerminalManager] returning scrollback, first 200:",
scrollback.slice(0, 200),
);
return {
isNew: false,
scrollback: existing.scrollback,
scrollback,
wasRecovered: existing.wasRecovered,
};
}
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.

⚠️ Potential issue | 🟡 Minor

Potential logic issue with delta calculation based on string length.

The delta filtering logic at lines 91-99 assumes that serializedScrollback.length can be used as an offset into scrollback. However, these two strings may have different content:

  • scrollback contains raw PTY output (may include escape sequences)
  • serializedScrollback contains filtered/serialized content (escape sequences removed)

If the serialized length differs from the raw content it represents, slicing at serializedLen could produce incorrect results (either missing data or including already-processed data).

Consider tracking the raw scrollback length at the time of serialization instead:

 interface TerminalSession {
 	// ...
 	scrollback: string;
 	serializedScrollback: string;
+	/** Raw scrollback length when serializedScrollback was last saved */
+	scrollbackLengthAtLastSerialize: number;
 	// ...
 }

Then in saveScrollback:

 session.serializedScrollback = serialized;
+session.scrollbackLengthAtLastSerialize = session.scrollback.length;

And in reattach:

-if (rawLen > serializedLen) {
-	const newData = existing.scrollback.slice(serializedLen);
+if (rawLen > existing.scrollbackLengthAtLastSerialize) {
+	const newData = existing.scrollback.slice(existing.scrollbackLengthAtLastSerialize);

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/desktop/src/main/lib/terminal-manager.ts around lines 76 to 110, the
code uses serializedScrollback.length as an index into the raw scrollback which
is incorrect because serialization/filtering can change length; update the
serialization flow to store the raw scrollback length at the time of
serialization (e.g. save a rawScrollbackLength alongside serializedScrollback in
saveScrollback), then in the reattach path use that stored rawScrollbackLength
as the slice offset when computing the delta (fall back to 0 if the stored raw
length is missing or larger than current raw length), and keep existing
filtering/flush logic unchanged.

@Kitenite Kitenite closed this Dec 8, 2025
@Kitenite Kitenite deleted the unable-peacock-d18f8d branch December 8, 2025 17:26
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