Files
orca/src/shared/terminal-bracketed-paste-text.ts
T
bfa0dc01ba fix(mobile): preserve long terminal and chat pastes (#31473)
* fix(mobile): preserve long terminal and chat pastes

Agents can keep only the last ~1 KiB read of unframed input (#13332).
Frame long single-line chat messages like multi-line ones, and frame Live
input appends over 1 KiB (an OS paste) with the terminal's paste rule,
including held text flushed before a terminal click.

Co-authored-by: Carlos Baraza Haro <carlos.baraza@ieee.org>

* fix(mobile): end the live input session after a framed paste

Agents such as Codex collapse a framed paste into one token, so later
per-character erases from the field mirror desynced the line. A long
append to a terminal in bracketed paste mode now commits the whole field
in one frame and clears the field, recording it like other session ends
so a late native echo cannot send the paste twice; without that mode the
paste stays unframed and editable. Chat sends measure the sanitized bytes
when deciding to frame.

---------

Co-authored-by: Carlos Baraza Haro <carlos.baraza@ieee.org>
Co-authored-by: Kelvin Amoaba <97001695+AmoabaKelvin@users.noreply.github.com>
2026-10-29 03:02:08 +00:00

36 lines
1.3 KiB
TypeScript

/** Keep renderer pastes and host launch prompts framed identically. */
const ESCAPE = '\u001b'
export const BRACKETED_PASTE_START = `${ESCAPE}[200~`
export const BRACKETED_PASTE_END = `${ESCAPE}[201~`
// Agents can keep only the last read of unframed input longer than this (#13332).
export const UNFRAMED_AGENT_INPUT_MAX_BYTES = 1024
// Replace embedded ESC with printable ␛ (U+241B) so pasted framing bytes cannot close the frame and execute its tail.
export function sanitizeBracketedPasteText(text: string): string {
let escapeIndex = text.indexOf(ESCAPE)
if (escapeIndex === -1) {
return text
}
let sanitized = ''
let start = 0
while (escapeIndex !== -1) {
sanitized += `${text.slice(start, escapeIndex)}␛`
start = escapeIndex + ESCAPE.length
escapeIndex = text.indexOf(ESCAPE, start)
}
return sanitized + text.slice(start)
}
export function normalizeTerminalPasteLineEndings(text: string): string {
// Why: xterm's native paste path converts every clipboard newline to CR.
// Direct frames must match it or ConPTY TUIs can treat raw LF as submit.
return text.replace(/\r?\n/g, '\r')
}
export function wrapTerminalBracketedPasteText(text: string): string {
const normalizedText = normalizeTerminalPasteLineEndings(text)
return `${BRACKETED_PASTE_START}${sanitizeBracketedPasteText(normalizedText)}${BRACKETED_PASTE_END}`
}