Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions apps/server/src/terminal/Layers/Manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,23 @@ async function defaultSubprocessChecker(terminalPid: number): Promise<boolean> {
return checkPosixSubprocessActivity(terminalPid);
}

/**
* Strip terminal query responses (OSC color reports, DA responses) that
* should be consumed by the terminal emulator but leak into the saved
* history when the session is restored. These appear as visible gibberish
* like `10;rgb:f5f5/f5f5/f5f5` to the user.
*/
function stripTerminalQueryResponses(data: string): string {
// OSC responses: \x1b] ... ST where ST is \x1b\\ or \x07
// Examples: \x1b]10;rgb:f5f5/f5f5/f5f5\x1b\\ (foreground color report)
// \x1b]11;rgb:1616/1616/1616\x07 (background color report)
// DA responses: \x1b[ ... c (Device Attributes)
// Example: \x1b[?1;2c
return data
.replace(/\x1b\][^\x07\x1b]*(?:\x1b\\|\x07)/g, "")
Comment on lines +252 to +258
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

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

The OSC-stripping regex currently removes any OSC sequence (ESC ] ... ST/BEL), not just color report responses for OSC 10/11 as described in the PR. This will also drop other OSC features (e.g., OSC 8 hyperlinks, title updates, OSC 7 cwd hints) from restored history, which is a behavior change/regression. Consider narrowing the pattern to only match OSC 10 and 11 responses (and ideally the expected response payload forms) so other OSC sequences remain intact in persisted history.

Suggested change
// OSC responses: \x1b] ... ST where ST is \x1b\\ or \x07
// Examples: \x1b]10;rgb:f5f5/f5f5/f5f5\x1b\\ (foreground color report)
// \x1b]11;rgb:1616/1616/1616\x07 (background color report)
// DA responses: \x1b[ ... c (Device Attributes)
// Example: \x1b[?1;2c
return data
.replace(/\x1b\][^\x07\x1b]*(?:\x1b\\|\x07)/g, "")
// OSC responses (color reports only): \x1b]10;...ST or \x1b]11;...ST
// ST is \x1b\\ or \x07
// Examples: \x1b]10;rgb:f5f5/f5f5/f5f5\x1b\\ (foreground color report)
// \x1b]11;rgb:1616/1616/1616\x07 (background color report)
// DA responses: \x1b[ ... c (Device Attributes)
// Example: \x1b[?1;2c
return data
.replace(/\x1b\](1[01]);[^\x07\x1b]*(?:\x1b\\|\x07)/g, "")

Copilot uses AI. Check for mistakes.
.replace(/\x1b\[\?[\d;]*c/g, "");
}

function capHistory(history: string, maxLines: number): string {
if (history.length === 0) return history;
const hasTrailingNewline = history.endsWith("\n");
Expand Down Expand Up @@ -694,7 +711,8 @@ export class TerminalManagerRuntime extends EventEmitter<TerminalManagerEvents>
}

private onProcessData(session: TerminalSessionState, data: string): void {
session.history = capHistory(`${session.history}${data}`, this.historyLineLimit);
const cleanData = stripTerminalQueryResponses(data);
session.history = capHistory(`${session.history}${cleanData}`, this.historyLineLimit);
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

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

Since sanitization is only applied to new incoming data, any existing persisted history that already contains OSC/DA response gibberish (from older versions) will continue to show up on restore until the user clears history. If the goal is to fix the symptom immediately after upgrade, consider also stripping these sequences when reading history (and rewriting the capped/cleaned history file), or performing a one-time migration/cleanup on open.

Suggested change
session.history = capHistory(`${session.history}${cleanData}`, this.historyLineLimit);
const combinedHistory = stripTerminalQueryResponses(`${session.history}${cleanData}`);
session.history = capHistory(combinedHistory, this.historyLineLimit);

Copilot uses AI. Check for mistakes.
Comment on lines 713 to +715
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

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

Stripping is applied per onData chunk (stripTerminalQueryResponses(data)), but PTY data events can split escape sequences across chunks. If an OSC/DA response is split, the first fragment will be persisted (and the terminator fragment won’t match either), so the gibberish can still show up after restore. A more reliable approach is to sanitize on the concatenated stream (e.g., apply stripping to the full history right before persisting / when reading history), or keep a small per-session buffer for incomplete trailing escape sequences between chunks.

Copilot uses AI. Check for mistakes.
Comment on lines +714 to +715
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

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

This change introduces new sanitization logic for persisted terminal history, but there doesn’t appear to be a unit test covering it (the repo already has extensive TerminalManagerRuntime history persistence tests). Adding tests that (1) verify OSC 10/11 + DA responses are removed from the persisted history, (2) verify non-target OSC sequences are preserved (if intended), and (3) verify emitted live output events still contain the original data would help prevent regressions.

Copilot uses AI. Check for mistakes.
session.updatedAt = new Date().toISOString();
this.queuePersist(session.threadId, session.terminalId, session.history);
this.emitEvent({
Expand Down Expand Up @@ -897,7 +915,7 @@ export class TerminalManagerRuntime extends EventEmitter<TerminalManagerEvents>
const nextPath = this.historyPath(threadId, terminalId);
try {
const raw = await fs.promises.readFile(nextPath, "utf8");
const capped = capHistory(raw, this.historyLineLimit);
const capped = capHistory(stripTerminalQueryResponses(raw), this.historyLineLimit);
if (capped !== raw) {
await fs.promises.writeFile(nextPath, capped, "utf8");
}
Expand Down