Skip to content

Fix terminal reattach scroll jumps#955

Closed
Kitenite wants to merge 4 commits into
mainfrom
terminal-reattach-fix-clean
Closed

Fix terminal reattach scroll jumps#955
Kitenite wants to merge 4 commits into
mainfrom
terminal-reattach-fix-clean

Conversation

@Kitenite
Copy link
Copy Markdown
Collaborator

@Kitenite Kitenite commented Jan 26, 2026

Summary:

  • preserve viewport position across reattach
  • skip redundant resize/SIGWINCH on attach
  • filter ESC[3J in renderer + persistence to keep scrollback

Testing: not run

Summary by CodeRabbit

  • Bug Fixes

    • Terminal scrollback history is now preserved when commands execute clear-scrollback operations, ensuring command output remains visible for reference.
  • Improvements

    • Enhanced terminal responsiveness when switching between tabs and restoring terminal sessions.

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

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 26, 2026

📝 Walkthrough

Walkthrough

The PR introduces removeClearScrollbackSequences to remove ANSI clear-scrollback sequences (ESC[3J) and updates terminal subsystems across backend and renderer layers to use this simplified approach. It replaces the previous extractContentAfterClear logic in session and history management, adds sanitization hooks in renderer components, and makes terminal resize operations unconditional on visibility changes.

Changes

Cohort / File(s) Summary
Clear-scrollback filtering API
apps/desktop/src/main/lib/terminal-escape-filter.ts, apps/desktop/src/main/lib/terminal-escape-filter.test.ts
Added new global-flag regex pattern CLEAR_SCROLLBACK_GLOBAL_PATTERN and exported function removeClearScrollbackSequences(data: string) to strip ED3 sequences; includes comprehensive test coverage for sequence removal, collapsing multiple sequences, and preserving non-ED3 sequences.
Backend session and history management
apps/desktop/src/main/lib/terminal/daemon/history-manager.ts, apps/desktop/src/main/lib/terminal/session.ts, apps/desktop/src/main/lib/terminal-host.ts
Replaced extractContentAfterClear with removeClearScrollbackSequences for scrollback sanitization; simplified history-manager's clear-scrollback event handling by removing session reinitialization logic; removed non-fatal error handling from terminal-host resize operations.
Terminal manager and agent session detection
apps/desktop/src/main/lib/terminal/manager.ts
Added isAgentSession derived flag from initialCommands using agentCommandPattern and assigned it to shouldAwaitAgentHooks.
Renderer terminal UI and resize behavior
apps/desktop/src/renderer/.../Terminal/Terminal.tsx
Modified visibilitychange handler to always call resize() unconditionally with current xterm dimensions instead of conditionally based on dimension changes.
Renderer terminal restoration and streaming
apps/desktop/src/renderer/.../Terminal/hooks/useTerminalRestore.ts, apps/desktop/src/renderer/.../Terminal/hooks/useTerminalStream.ts, apps/desktop/src/renderer/.../Terminal/utils.ts
Added new stripClearScrollback(data: string) utility function; integrated sanitization across terminal data write paths in restoration and streaming hooks to prevent clear-scrollback sequences from being written to xterm.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Suggested reviewers

  • AviPeltz

Poem

🐰 Scrollback whispers fade away,
Sequences scrubbed bright and clean,
History kept, resets removed,
Terminal flows now serene.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.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 Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Fix terminal reattach scroll jumps' directly and clearly describes the main change: preventing scroll position loss when clients reattach to terminals by filtering ESC[3J sequences and preserving viewport state.

✏️ 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: 1

🤖 Fix all issues with AI agents
In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/utils.ts`:
- Around line 23-26: stripClearScrollback currently does a stateless replaceAll
which fails when the ESC sequence "\x1b[3J" is split across chunks; change it to
a stateful stripper that preserves trailing partial sequences and removes
completed "\x1b[3J" only when fully seen. Implement this by turning
stripClearScrollback into a factory that returns a function/closure which keeps
a small carry-over buffer (tracking "", "\x1b", "\x1b[", "\x1b[3") between
calls, or update the stream hook to buffer joins before calling the existing
function; update all call sites to use the returned function (or the buffered
input) and ensure any leftover partial sequence is kept for the next invocation
to avoid passing incomplete ESC sequences to xterm.
🧹 Nitpick comments (2)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/useTerminalRestore.ts (1)

162-164: Consider passing false directly for clarity.

Since this code is in the else branch of if (result.isNew), result.isNew is always false here. Passing it explicitly as false would make this more self-documenting, though the current approach maintains callback signature consistency.

♻️ Optional: More explicit parameter
 				} else {
-					restoreViewportRef.current?.(xterm, result.isNew);
+					restoreViewportRef.current?.(xterm, false /* isNew */);
 				}
apps/desktop/src/main/lib/terminal/daemon/history-manager.ts (1)

84-111: Remove the unused _getSession parameter from writeToHistory.

The _getSession parameter is never referenced in the method body. The single call site in daemon-manager.ts unnecessarily passes a callback, creating maintenance burden. Since this is internal code with no API compatibility constraints, removing the parameter and updating the call site is straightforward.

Comment on lines +23 to +26
export function stripClearScrollback(data: string): string {
const ESC = "\x1b";
return data.replaceAll(`${ESC}[3J`, "");
}
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 | 🟠 Major

Stateless ED3 stripping can miss split escape sequences.

If \x1b and [3J arrive in different chunks, replaceAll won’t match and the scrollback clear still reaches xterm. Consider a stateful stripper (carry-over of trailing ESC, ESC[, ESC[3) or implement the buffering in the stream hook.

💡 Example stateful stripper (requires updating the call site to keep the returned function)
-export function stripClearScrollback(data: string): string {
-	const ESC = "\x1b";
-	return data.replaceAll(`${ESC}[3J`, "");
-}
+export function createClearScrollbackStripper(): (data: string) => string {
+	const ESC = "\x1b";
+	let carry = "";
+	return (data: string) => {
+		const input = carry + data;
+		const cleaned = input.replaceAll(`${ESC}[3J`, "");
+		const carryMatch = cleaned.match(/(\x1b|\x1b\[|\x1b\[3)$/);
+		carry = carryMatch?.[0] ?? "";
+		return carry ? cleaned.slice(0, -carry.length) : cleaned;
+	};
+}
🤖 Prompt for AI Agents
In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/utils.ts`
around lines 23 - 26, stripClearScrollback currently does a stateless replaceAll
which fails when the ESC sequence "\x1b[3J" is split across chunks; change it to
a stateful stripper that preserves trailing partial sequences and removes
completed "\x1b[3J" only when fully seen. Implement this by turning
stripClearScrollback into a factory that returns a function/closure which keeps
a small carry-over buffer (tracking "", "\x1b", "\x1b[", "\x1b[3") between
calls, or update the stream hook to buffer joins before calling the existing
function; update all call sites to use the returned function (or the buffered
input) and ensure any leftover partial sequence is kept for the next invocation
to avoid passing incomplete ESC sequences to xterm.

@Kitenite Kitenite closed this Jan 26, 2026
@github-actions
Copy link
Copy Markdown
Contributor

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

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

Thank you for your contribution! 🎉

@Kitenite Kitenite deleted the terminal-reattach-fix-clean branch January 26, 2026 22:08
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