fix(tui): stop writing OSC 133 prompt markers in regular mode (#11565)

In regular TUI mode, user and assistant messages wrote OSC 133
(`A`/`B`/`C`) shell-integration markers straight to the terminal.
Terminals that display OSC 7501 program status (Rex, iTerm2 3.7.5+) read
`133;A` as a new shell prompt and drop the current status. As a result,
the `working` indicator disappeared right after a run started and only
`done` showed up at the end. Fullscreen mode was not affected because it
already strips these markers before writing.

## Change
- `TuiMainScreen.doRender()` (`packages/tui/src/tui-main-screen.ts`) now
strips OSC 133 markers from the start of each rendered line before
compositing and writing. It uses the same pattern as fullscreen
(`OSC133_ZONE_PREFIX`).
- The message components still produce the markers. Fullscreen uses them
internally for prompt navigation and already strips them before writing,
so its behavior is unchanged.
- In regular mode, the terminal's own jump-to-prompt shortcuts no longer
stop at pi messages (added in #1805).

## Testing
- New test in `packages/tui/test/tui-render.test.ts`: no `ESC]133;`
reaches the terminal across a full render and a differential render,
with both BEL and ST terminators. It fails without the change and passes
with it.
- These pass:
  - `packages/tui` `npm test`
- `test/user-message.test.ts` and `test/assistant-message.test.ts` in
coding-agent
  - `npm run check`
- I ran `pi -ne --tui-mode regular` with `PI_PROGRAM_STATUS=1` and a
streaming test provider, capturing all terminal output with `script`.
- Before: `7501 working`, then repeated `133;A`/`133;B`/`133;C`, then
`7501 done`.
  - After: only `7501 idle` → `working` → `done`, and no OSC 133.

Fixes #11519

Requested by @davidbrai

Co-authored-by: triagent <triagent@localhost>
This commit is contained in:
robopipi[bot] 2026-10-29 14:00:16 +01:00 • committed by GitHub
parent ba7bdb9afc
commit 19ce8059c3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 25 additions and 1 deletions

View file

@ -10,6 +10,7 @@
### Fixed
- Fixed regular-mode `TUI` writing OSC 133 prompt markers from rendered lines to the terminal. Terminals treat `133;A` as a new shell prompt and drop the OSC 7501 program status, so the working indicator disappeared during runs. Fullscreen still uses the markers for prompt navigation and already stripped them ([#11519](https://github.com/earendil-works/pi/issues/11519))
- Fixed fullscreen double-click selection splitting paths and tokens at `~`, `.`, `\`, and `+`. Word characters now match iTerm2's defaults ([#10980](https://github.com/earendil-works/pi/issues/10980))
- Fixed `fuzzyMatch()` ranking a query scattered over an earlier part of the text above a contiguous match later in it. It now also scores each contiguous occurrence of the query and keeps the best alignment ([#11220](https://github.com/earendil-works/pi/issues/11220), [#11296](https://github.com/earendil-works/pi/pull/11296))
- Fixed `ProcessTerminal.setTitle()` writing control characters from the title, which could end the title sequence and send the rest to the terminal as escape sequences

View file

@ -5,6 +5,9 @@ import { deleteKittyImage, isImageLine } from "./terminal-image.ts";
import { type TUI, TuiBase, type TuiStopOptions } from "./tui.ts";
import { visibleWidth } from "./utils.ts";
/** OSC 133 prompt markers at the start of a rendered line. Fullscreen uses them for prompt navigation. */
const OSC133_ZONE_PREFIX = /^(?:\x1b\]133;[ABC](?:\x07|\x1b\\))+/;
const KITTY_SEQUENCE_PREFIX = "\x1b_G";
const MAX_RENDER_WRITE_CHARS = 1024 * 1024;
@ -262,7 +265,8 @@ export class TuiMainScreen extends TuiBase implements TUI {
// Render all components to get new lines
// Resolve fake cursors before compositing so overlays and line slicing see plain SGR codes
let newLines = this.resolveFakeCursors(this.render(width));
// Drop OSC 133 markers: the terminal reads 133;A as a new shell prompt, which ends program status (OSC 7501)
let newLines = this.resolveFakeCursors(this.render(width).map((line) => line.replace(OSC133_ZONE_PREFIX, "")));
// Composite overlays into the rendered lines (before differential compare)
if (this.hasOverlayEntries) {

View file

@ -209,6 +209,24 @@ describe("TUI bounded render output", () => {
});
});
describe("TUI OSC 133 markers", () => {
it("does not write OSC 133 prompt markers to the terminal", () => {
// Terminals read 133;A as a new shell prompt and drop the OSC 7501 program status.
const terminal = new BoundedWriteTerminal();
const tui = new TuiMainScreen(terminal);
const component = new TestComponent();
tui.addChild(component);
component.lines = ["\x1b]133;A\x07prompt", "\x1b]133;B\x07\x1b]133;C\x07output"];
tui.renderNow();
component.lines = [...component.lines, "\x1b]133;A\x1b\\reply", "\x1b]133;B\x1b\\\x1b]133;C\x1b\\done"];
tui.renderNow();
const output = terminal.writes.join("");
assert.ok(!output.includes("\x1b]133;"), "regular mode must not emit OSC 133 markers");
for (const text of ["prompt", "output", "reply", "done"]) assert.ok(output.includes(text));
});
});
/** Set each environment variable to `value`, returning a function that restores the previous state. */
function overrideEnv(names: readonly string[], value: string): () => void {
const previousValues = names.map((name) => [name, process.env[name]] as const);