Files
orca/src/main/runtime/terminal-command-paint.ts
T
hwantageandNeil 1187abdbc2 fix(windows): mark PowerShell startup command boundaries with OSC 133 C/D (#18182)
A startup command embedded in the PowerShell -EncodedCommand payload runs
before the first prompt, and the prompt wrapper suppresses 133;D until it
has printed once. An agent launched this way exited with no
command-finished boundary, so its pane was never retired.

Emit 133;C before the embedded startup command and prime the prompt latch
so the first prompt reports 133;D with the exit code. Stdin-typed startup
commands keep readline's own marks. The encoded-command budget grows by
the mark's size so real commands keep the prior limit.

Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
2026-10-26 05:14:19 -07:00

59 lines
2.1 KiB
TypeScript

import { stripAnsiEscapeSequences } from '../../shared/ansi-escape-sequences'
import { createOsc133CommandFinishedScanner } from '../../shared/terminal-osc133-command-finished'
import { normalizeTerminalChunk } from './terminal-ansi-normalization'
// eslint-disable-next-line no-control-regex -- control bytes are exactly what is not visible.
const VISIBLE_CHARACTER_RE = /[^\s\u0000-\u001f\u007f-\u009f]/
function hasVisibleText(normalizedText: string): boolean {
return VISIBLE_CHARACTER_RE.test(stripAnsiEscapeSequences(normalizedText))
}
/** OSC 133;C follows the prompt/echo; only visible bytes after it prove the running command painted. */
export class TerminalCommandPaint {
private awaitingPaint = false
private markerEndInChunk = -1
private readonly scanner = createOsc133CommandFinishedScanner(
() => {},
(endInChunk) => {
this.markerEndInChunk = endInChunk
}
)
observe(data: string, normalizedText: string): void {
this.markerEndInChunk = -1
this.scanner.scan(data)
if (this.markerEndInChunk !== -1) {
// Why the rest of this chunk counts: a fast binary can paint in the same read as the marker.
this.awaitingPaint = !hasVisibleText(
normalizeTerminalChunk(data.slice(this.markerEndInChunk)).text
)
return
}
if (this.awaitingPaint && hasVisibleText(normalizedText)) {
this.awaitingPaint = false
}
}
hasPainted(): boolean {
return !this.awaitingPaint
}
}
export type TerminalCommandPaintRecord = { commandPaint?: TerminalCommandPaint }
/** `normalizedText` is the chunk as the tail buffer received it (pending escapes resolved). */
export function observeTerminalCommandPaint(
record: TerminalCommandPaintRecord,
data: string,
normalizedText: string
): void {
record.commandPaint ??= new TerminalCommandPaint()
record.commandPaint.observe(data, normalizedText)
}
/** Without markers (older fish, relay zsh, unwrapped shells, cmd.exe /K startup args), any output is the only paint evidence. */
export function hasTerminalCommandPainted(record: TerminalCommandPaintRecord): boolean {
return record.commandPaint?.hasPainted() ?? true
}