Skip to content

feat: answer OSC colour queries and track dynamic colours - #95

Open
Ayman Bagabas (aymanbagabas) wants to merge 1 commit into
feat/terminal-configfrom
feat/osc-colors
Open

feat: answer OSC colour queries and track dynamic colours#95
Ayman Bagabas (aymanbagabas) wants to merge 1 commit into
feat/terminal-configfrom
feat/osc-colors

Conversation

@aymanbagabas

@aymanbagabas Ayman Bagabas (aymanbagabas) commented Aug 5, 2026

Copy link
Copy Markdown
Member

Second of a three PR stack: #94#95#96, based on #94 so the diff here is only this layer.

A terminal is supposed to answer when a program asks what colors it is using. Ours never did, so every program that asked had to time out and guess.

Before: the query goes unanswered

A program asking for the background (OSC 11) and cursor (OSC 12) color:

before — no reply, twice after — answered
before after

That silence is not harmless. Programs use OSC 11 to decide whether they are on a light or dark background, so a timeout means an editor or a diff tool picks a theme by guessing, and an agent driving the terminal sees whichever colors the guess produced rather than the ones actually configured. It also costs a real second of wall clock per query while the program waits.

After: setting works too

OSC 10, OSC 11, OSC 12 set the foreground, background, and cursor; OSC 4 sets a palette entry. OSC 110, OSC 111, OSC 112, and OSC 104 reset them.

$ printf '\033]11;#1c2833\a'      # background
$ printf '\033]10;#eaeaea\a'      # foreground
$ printf '\033]4;1;#ff5f87\a'     # palette entry 1, red

$ printf '\033]111\a\033]110\a\033]104;1\a'   # put them all back
default after the three sets after the three resets
default set reset

Background #000000#1c2833#000000, red #800000#ff5f87#800000. The reset returns to the profile from #94, not to a second hardcoded default.

How it resolves

emulator.color(slot) = what a program set with OSC   (runtime, clearable)
                    ?? the session profile           (from #94, read only)
                    ?? the static xterm table        (indices 16-255)

A reset clears the runtime value only. It can never clear a configured one, so OSC 104 from a stray program cannot wipe the palette a test was pinned against.

Notes for review

  • The reply is built from alacritty's Event::ColorRequest, whose formatter already captured the query's prefix and terminator, so a BEL-terminated query gets a BEL-terminated answer and an ST-terminated one gets ST. There is no second parser over the PTY stream, and Term::colors() is readable directly once advance() returns.
  • Replies are queued into the same pending buffer as PtyWrite, so ordering with other terminal output is unchanged.
  • ColorSlot is an enum (Indexed(u8) | Foreground | Background | Cursor). Alacritty's internal 256/257/258 numbering stays inside alacritty.rs and does not leak into the profile type.
  • 22 tests, including conformance cases that run against every backend, so a future backend inherits them.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds support for OSC color query/set/reset behavior to the headless terminal emulator layer, and updates rendering + assertions to resolve colors from the emulator’s effective (runtime-overridden) palette rather than only from the session profile. This makes terminals respond to OSC 10/11/12 and OSC 4 queries and ensures screenshots and expect --fg/--bg reflect what programs actually set during a session.

Changes:

  • Introduces ColorSlot / static xterm 256-color table and exposes Emulator::color(...) + default Emulator::resolve(...) for consistent color resolution across backends.
  • Implements color query capture/answering and runtime override resolution for the alacritty-based backend, wiring the session to provide a Profile to the emulator.
  • Updates screenshot rendering and color assertions to resolve via the emulator (so runtime OSC changes affect screenshots/asserts) and adds extensive conformance + integration coverage.
Show a summary per file
File Description
SKILL.md Documents runtime OSC color set/query/reset behavior and its interaction with profiles/screenshots.
crates/shell-use/src/terminal/emu.rs Extends the emulator trait with color(ColorSlot) and a shared resolve(...) implementation.
crates/shell-use/src/terminal/conformance.rs Updates conformance harness to construct emulators with a Profile and adds OSC color conformance cases.
crates/shell-use/src/terminal/alacritty.rs Captures Event::ColorRequest, answers OSC color queries, and resolves colors via runtime overrides → profile → xterm table.
crates/shell-use/src/session.rs Passes the session Profile into the emulator and removes the stored Session.profile field.
crates/shell-use/src/render/svg.rs Renders via &dyn Emulator so screenshots reflect effective (runtime) colors; adds tests covering set/reset impact.
crates/shell-use/src/profile.rs Adds ColorSlot and a static xterm 256-color table helper (xterm_color); simplifies Colors::rgb.
crates/shell-use/src/engine.rs Updates expect color checking and screenshot rendering to resolve through the live emulator state.
crates/shell-use/src/assert/snapshot.rs Adds tests ensuring snapshots record palette slots (not resolved RGB), while truecolor is recorded literally.
crates/shell-use/src/assert/color.rs Makes matching/description resolve via &dyn Emulator and adds tests for runtime recoloring behavior.
crates/shell-use-cli/tests/session_lifecycle.rs Adds an end-to-end Unix PTY test ensuring OSC queries are answered and set/reset is observable.
crates/shell-use-cli/src/monitor.rs Updates test construction of AlacrittyEmu to pass a Profile.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 12/12 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment on lines +1192 to +1196
session
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.emu

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The structure is as you describe — the reader thread does need this lock to process — so I measured how long it is actually held. Release build, timing render_svg alone:

screenshot rows lock held
typical, 80x24 25 71 µs
--full, 10k scrollback 10001 15.9 ms

The file write is already outside the lock; only the render is inside.

So the common case is 71 µs, and even a full render at the default scrollback is 16 ms — well inside the seconds a program waits for a query reply, and small against a PTY buffer. It scales with scrollback, so a much deeper one would make it worse.

I have not changed it, for a reason worth flagging: the fix you describe is a snapshot of the resolved palette, and this PR deliberately removed exactly that type. Colors resolve through the emulator now precisely so a screenshot shows what OSC set at that moment, and reintroducing a copied palette is the shape we took out. Given 71 µs in the common case I would rather not trade that back without a measurement showing it hurts.

Happy to revisit if you would rather have the shorter critical section — say the word and I will do it in a follow-up so it can be reviewed on its own.

Comment thread crates/shell-use/src/terminal/alacritty.rs
@aymanbagabas

Copy link
Copy Markdown
Member Author

Heads up on CI flakiness that is not from these changes, since it will keep showing up.

Two timing-sensitive end-to-end tests fail intermittently when the suite runs in parallel under load. I reproduced it locally at roughly 1 in 3 full-suite runs, and checked whether it was mine by going back to the commit before the reply-ordering fix and running the same suite six times — a different test flaked there (expect_exit_code_timing_out_does_not_accept_a_stale_code), so it is a pre-existing property of the suite rather than something this PR introduced.

The two seen so far:

  • a_color_query_is_answered_over_the_ptywait command returns before the probe has printed
  • expect_exit_code_timing_out_does_not_accept_a_stale_code

Both pass 6/6 in isolation. Worth a separate issue if it becomes annoying; happy to file one.

@aymanbagabas

Copy link
Copy Markdown
Member Author

Follow-up on the flakiness note above: the JS-side one (echo roundtrip drives a real session) is fixed separately in #99, and the whole class is tracked in #98. The Rust-side one in this PR is fixed here.

Programs ask the terminal what color it is before deciding whether to
draw for a light or a dark background. Nothing answered, so every one of
them blocked until it timed out and guessed.

The emulator answers now, through `take_pending_writes`, which already
exists for exactly this: the replies a terminal owes to device queries.
alacritty parses the sequence, tracks what a program set, and hands back
a formatter with the query's own prefix and terminator already captured,
so the only missing piece was the color itself. That comes from the
session profile, which the emulator is now constructed with.

The alternative was parsing the sequences off the PTY stream, the way
shell integration is tracked. That would have meant reimplementing color
parsing, the runtime table, reply formatting, and terminator tracking,
all of which the emulator already does — and getting the terminator
wrong, since it is only visible to whoever parsed the sequence. Reading
what the emulator already knows is both less code and more faithful.

Colors resolve in three layers: what a program set, else the session
profile, else the table the specification defines. A reset clears only
the first, so the profile is unreachable from the byte stream and there
is always something to restore. That is what the specification asks for,
describing a reset as restoring "the color specified by the corresponding
X resource".

`Emulator` gains `color(slot)`, which every backend answers from its own
state, plus a `palette()` snapshot of all 259 slots. The screenshot
renderer and `expect --fg/--bg` take that snapshot rather than the
emulator, so neither holds the session lock while it renders, and neither
knows which backend produced the colors.

Five conformance cases cover queries, terminator echo, set-then-reset,
unconfigured indices, and that a cell follows whatever its slot now
holds. They run against every backend, so a future one cannot answer
differently. An end-to-end test drives a real program through the whole
path: it reads the configured background, sets its own, resets, and gets
the configured one back.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>

Rebasing onto a main that had since gained window titles folded the
follow-up commits into this one, so it also carries: color resolution
moved into the emulator so a screenshot and an assertion both see what a
program set, `ColorSlot` naming the three dynamic colors instead of
numbering them, snapshot coverage pinning that a snapshot records the
slot rather than the color it resolves to, and the fix that answers
queries in the order they were asked rather than appending every color
reply last.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants