From 5b8b8d4e99c1c7bca2bd7ca54dd480d3eb532514 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Mon, 8 Jun 2026 22:09:20 -0700 Subject: [PATCH] Skip hidden title terminal renderer writes --- .../hidden-renderer-skip-eligibility.test.ts | 42 ++++++++++++++++++- .../hidden-renderer-skip-eligibility.ts | 39 +++++++++++++++-- .../terminal-pane/pty-connection.test.ts | 38 +++++++++++++++++ .../terminal-pane/pty-transport.test.ts | 19 +++++++++ ...icial-opencode-hidden-pressure-scenario.ts | 2 +- ...ificial-opencode-hidden-pressure-script.ts | 10 +++-- .../artificial-opencode-terminal-load.spec.ts | 16 ++++++- 7 files changed, 156 insertions(+), 10 deletions(-) diff --git a/src/renderer/src/components/terminal-pane/hidden-renderer-skip-eligibility.test.ts b/src/renderer/src/components/terminal-pane/hidden-renderer-skip-eligibility.test.ts index 4ab90d6ecdd..1076a985cfe 100644 --- a/src/renderer/src/components/terminal-pane/hidden-renderer-skip-eligibility.test.ts +++ b/src/renderer/src/components/terminal-pane/hidden-renderer-skip-eligibility.test.ts @@ -35,6 +35,32 @@ describe('shouldSkipHiddenRendererOutput', () => { ).toBe(false) }) + it('skips complete hidden title OSC chunks when a snapshot restore is available', () => { + for (const data of ['\x1b]0;window title\x07', '\x1b]1;icon title\x07', '\x1b]2;both\x1b\\']) { + expect( + shouldSkipHiddenRendererOutput({ + foreground: false, + canRestoreHiddenOutput: true, + startupRendererQueryWindowActive: false, + synchronizedOutputActive: false, + data + }) + ).toBe(true) + } + }) + + it('skips hidden title OSC mixed with otherwise restorable plain output', () => { + expect( + shouldSkipHiddenRendererOutput({ + foreground: false, + canRestoreHiddenOutput: true, + startupRendererQueryWindowActive: false, + synchronizedOutputActive: false, + data: 'line before\r\n\x1b]0;next title\x07line after\r\n' + }) + ).toBe(true) + }) + it('keeps startup query windows and terminal-control chunks live', () => { expect( shouldSkipHiddenRendererOutput({ @@ -58,7 +84,7 @@ describe('shouldSkipHiddenRendererOutput', () => { shouldSkipHiddenRendererOutput({ foreground: false, canRestoreHiddenOutput: true, - startupRendererQueryWindowActive: false, + startupRendererQueryWindowActive: true, synchronizedOutputActive: false, data: '\x1b]0;title\x07' }) @@ -74,6 +100,20 @@ describe('shouldSkipHiddenRendererOutput', () => { ).toBe(false) }) + it('keeps non-title OSC and incomplete title OSC chunks live', () => { + for (const data of ['\x1b]52;c;clipboard\x07', '\x1b]9;notify\x07', '\x1b]0;partial-title']) { + expect( + shouldSkipHiddenRendererOutput({ + foreground: false, + canRestoreHiddenOutput: true, + startupRendererQueryWindowActive: false, + synchronizedOutputActive: false, + data + }) + ).toBe(false) + } + }) + it('keeps rewrite and unicode chunks live', () => { expect( shouldSkipHiddenRendererOutput({ diff --git a/src/renderer/src/components/terminal-pane/hidden-renderer-skip-eligibility.ts b/src/renderer/src/components/terminal-pane/hidden-renderer-skip-eligibility.ts index 328e817e748..123d22b60ff 100644 --- a/src/renderer/src/components/terminal-pane/hidden-renderer-skip-eligibility.ts +++ b/src/renderer/src/components/terminal-pane/hidden-renderer-skip-eligibility.ts @@ -13,18 +13,51 @@ function isAllowedPlainHiddenOutputCode(code: number): boolean { return code >= 0x20 && code <= 0x7e } -function containsOnlyPlainHiddenOutput(data: string): boolean { - for (let index = 0; index < data.length; index++) { +function findTitleOscEnd(data: string, startIndex: number): number | null { + const command = data.charCodeAt(startIndex + 2) + if ( + data.charCodeAt(startIndex) !== 0x1b || + data.charCodeAt(startIndex + 1) !== 0x5d || + (command !== 0x30 && command !== 0x31 && command !== 0x32) || + data.charCodeAt(startIndex + 3) !== 0x3b + ) { + return null + } + + for (let index = startIndex + 4; index < data.length; index++) { const code = data.charCodeAt(index) + if (code === 0x07) { + return index + 1 + } + if (code === 0x1b) { + return data.charCodeAt(index + 1) === 0x5c ? index + 2 : null + } + } + return null +} + +function containsOnlyRestorableHiddenOutput(data: string): boolean { + for (let index = 0; index < data.length; ) { + const code = data.charCodeAt(index) + if (code === 0x1b) { + const nextIndex = findTitleOscEnd(data, index) + if (nextIndex === null) { + return false + } + index = nextIndex + continue + } if (code === 0x0d) { if (data.charCodeAt(index + 1) !== 0x0a) { return false } + index += 1 continue } if (!isAllowedPlainHiddenOutputCode(code)) { return false } + index += 1 } return true } @@ -45,5 +78,5 @@ export function shouldSkipHiddenRendererOutput({ ) { return false } - return containsOnlyPlainHiddenOutput(data) + return containsOnlyRestorableHiddenOutput(data) } diff --git a/src/renderer/src/components/terminal-pane/pty-connection.test.ts b/src/renderer/src/components/terminal-pane/pty-connection.test.ts index d98531931a9..450688ddcda 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection.test.ts @@ -3358,6 +3358,44 @@ describe('connectPanePty', () => { disposable.dispose() }) + it('skips hidden title OSC renderer writes while keeping pane title handling wired', async () => { + const { connectPanePty } = await import('./pty-connection') + const transport = createMockTransport('pty-id') + const capturedDataCallback: { + current: ((data: string, meta?: { seq?: number; rawLength?: number }) => void) | null + } = { current: null } + transport.connect.mockImplementation(async ({ callbacks }: { callbacks: ConnectCallbacks }) => { + capturedDataCallback.current = callbacks.onData ?? null + return 'pty-id' + }) + transportFactoryQueue.push(transport) + + const pane = createPane(1) + const manager = createManager(1) + const deps = createDeps({ + isVisibleRef: { current: false } + }) + const disposable = connectPanePty(pane as never, manager as never, deps as never) + await flushAsyncTicks(6) + + const hiddenTitle = '\x1b]0;hidden title\x07' + capturedDataCallback.current?.(hiddenTitle, { + seq: hiddenTitle.length, + rawLength: hiddenTitle.length + }) + + expect(pane.terminal.write).not.toHaveBeenCalledWith(hiddenTitle, expect.any(Function)) + const titleHandler = createdTransportOptions[0]?.onTitleChange as + | ((title: string, rawTitle: string) => void) + | undefined + if (!titleHandler) { + throw new Error('Expected onTitleChange to be registered') + } + titleHandler('hidden title', 'hidden title') + expect(deps.setRuntimePaneTitle).toHaveBeenCalledWith('tab-1', pane.id, 'hidden title') + disposable.dispose() + }) + it('restores plain hidden remote runtime output from its serialized snapshot', async () => { const { connectPanePty } = await import('./pty-connection') const transport = createMockTransport('remote:env-1@@terminal-1') diff --git a/src/renderer/src/components/terminal-pane/pty-transport.test.ts b/src/renderer/src/components/terminal-pane/pty-transport.test.ts index 1c4cd98a750..7ff205f25aa 100644 --- a/src/renderer/src/components/terminal-pane/pty-transport.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-transport.test.ts @@ -93,6 +93,25 @@ describe('createIpcPtyTransport', () => { transport.disconnect() }) + it('runs title side effects even when the data callback does not render the chunk', async () => { + const { createIpcPtyTransport } = await import('./pty-transport') + const onTitleChange = vi.fn() + const onDataCallback = vi.fn() + const transport = createIpcPtyTransport({ onTitleChange }) + + await transport.connect({ url: '', callbacks: { onData: onDataCallback } }) + + onData?.({ id: 'pty-1', data: '\u001b]0;hidden-title\u0007' }) + + expect(onDataCallback).toHaveBeenCalledWith('\u001b]0;hidden-title\u0007') + expect(onTitleChange).not.toHaveBeenCalled() + + await flushPtySideEffects() + + expect(onTitleChange).toHaveBeenCalledWith('hidden-title', 'hidden-title') + transport.disconnect() + }) + it('does not schedule PTY side-effect drains for ordinary output with no working title', async () => { vi.useFakeTimers() try { diff --git a/tests/e2e/artificial-opencode-hidden-pressure-scenario.ts b/tests/e2e/artificial-opencode-hidden-pressure-scenario.ts index e6cf4cae555..b34e8b0c606 100644 --- a/tests/e2e/artificial-opencode-hidden-pressure-scenario.ts +++ b/tests/e2e/artificial-opencode-hidden-pressure-scenario.ts @@ -171,7 +171,7 @@ export async function runHiddenRealPtyPressureScenario< ackGate ) - if (pressureOutputMode === 'plain') { + if (pressureOutputMode === 'plain' || pressureOutputMode === 'title') { expect(debug?.hiddenRendererSkipCount ?? 0).toBeGreaterThan(0) expect(debug?.hiddenRendererSkippedChars ?? 0).toBeGreaterThan(0) } else { diff --git a/tests/e2e/artificial-opencode-hidden-pressure-script.ts b/tests/e2e/artificial-opencode-hidden-pressure-script.ts index c0e9a9b0576..c9887e4c465 100644 --- a/tests/e2e/artificial-opencode-hidden-pressure-script.ts +++ b/tests/e2e/artificial-opencode-hidden-pressure-script.ts @@ -1,15 +1,17 @@ import { mkdirSync, writeFileSync } from 'node:fs' import path from 'node:path' -export type HiddenPressureOutputMode = 'tui' | 'plain' +export type HiddenPressureOutputMode = 'tui' | 'plain' | 'title' export function pressureOutputScript(runId: string, mode: HiddenPressureOutputMode): string { - const headerPrefix = mode === 'plain' ? '' : '\\x1b[0m' - const donePrefix = mode === 'plain' ? '' : '\\x1b[0m' + const headerPrefix = mode === 'tui' ? '\\x1b[0m' : '' + const donePrefix = mode === 'tui' ? '\\x1b[0m' : '' const chunkExpression = mode === 'plain' ? "'plain pressure pane=' + paneIndex + ' frame=' + frame + ' ' + chunkBody + '\\n'" - : "'\\x1b[?2026h\\x1b[1;1Hpressure pane=' + paneIndex + ' frame=' + frame + ' ' + chunkBody + '\\x1b[?2026l\\n'" + : mode === 'title' + ? "'\\x1b]0;title pressure pane=' + paneIndex + ' frame=' + frame + ' ' + chunkBody + '\\x07'" + : "'\\x1b[?2026h\\x1b[1;1Hpressure pane=' + paneIndex + ' frame=' + frame + ' ' + chunkBody + '\\x1b[?2026l\\n'" return ` const paneIndex = process.argv[2] ?? '0' const targetChars = Number(process.argv[3] ?? '0') diff --git a/tests/e2e/artificial-opencode-terminal-load.spec.ts b/tests/e2e/artificial-opencode-terminal-load.spec.ts index dabff6a534e..df37fee6dc9 100644 --- a/tests/e2e/artificial-opencode-terminal-load.spec.ts +++ b/tests/e2e/artificial-opencode-terminal-load.spec.ts @@ -21,6 +21,7 @@ import { waitForTerminalOutput } from './helpers/terminal' import { runHiddenRealPtyPressureScenario } from './artificial-opencode-hidden-pressure-scenario' +import type { HiddenPressureOutputMode } from './artificial-opencode-hidden-pressure-script' import { runMainPressureScenario } from './artificial-opencode-main-pressure-scenario' import { startSyntheticOpenCodeInjection } from './artificial-opencode-synthetic-injection' @@ -728,7 +729,7 @@ test.describe('Artificial OpenCode terminal load', () => { testInfo: TestInfo, hiddenPaneCount: number, annotationSuffix?: string, - pressureOutputMode?: 'tui' | 'plain' + pressureOutputMode?: HiddenPressureOutputMode ): Promise { await runHiddenRealPtyPressureScenario({ orcaPage, @@ -779,6 +780,19 @@ test.describe('Artificial OpenCode terminal load', () => { 'plain' ) }) + test('skips renderer writes for title-only hidden PTY output while preserving restore', async ({ + orcaPage, + testRepoPath + }, testInfo) => { + await runConfiguredHiddenRealPtyPressureScenario( + orcaPage, + testRepoPath, + testInfo, + HIDDEN_PRESSURE_PANES, + '-title', + 'title' + ) + }) for (const paneCount of SCALE_HIDDEN_PRESSURE_PANES) { test(`keeps hidden restore responsive with ${paneCount} ACK-backpressured real PTYs`, async ({ orcaPage,