Skip to content

Sharper TUI rendering: sprite glyphs, default theme, terminal-query callbacks - #8

Merged
charliek merged 10 commits into
mainfrom
fix/box-drawing-sprites
Apr 28, 2026
Merged

Sharper TUI rendering: sprite glyphs, default theme, terminal-query callbacks#8
charliek merged 10 commits into
mainfrom
fix/box-drawing-sprites

Conversation

@charliek

@charliek charliek commented Apr 28, 2026

Copy link
Copy Markdown
Owner

Summary

Four commits that bring Roost's rendering and terminal-protocol behaviour up to parity with cmux / Ghostty / Cursor for AI-agent TUIs (Codex, Claude Code, OpenCode):

  • 0dad9c8 — Custom Cairo geometric renderer for box-drawing (U+2500–U+257F) and block-element (U+2580–U+259F) glyphs, ported from Ghostty's font/sprite/draw/{box,block}.zig. Pango still handles all other glyphs. Fixes the OpenCode wordmark seams and Codex/Claude bordered chrome.
  • 1a4b7b4 — Default dark theme installed on every new terminal via libghostty's OPT_COLOR_FOREGROUND/BACKGROUND/CURSOR/PALETTE. Mirrors cmux's dark palette for indices 0–15; generates indices 16–255 from libghostty's standard xterm cube + gray ramp algorithm. Also wires cursor + selection ribbon colours through the same Theme value type so a future LoadTheme(path string) (Theme, error) is a drop-in.
  • 138233eWRITE_PTY / DEVICE_ATTRIBUTES / COLOR_SCHEME callbacks wired into libghostty-vt. Programs that probe terminal state (DA1, DSR, kitty keyboard, color scheme) now get answers instead of silence. Single shared cgo.Handle per terminal carries the callback state through libghostty's userdata slot.
  • 15c8b60 — Synthesise OSC 10/11/12 query responses in the existing internal/osc scanner. libghostty-vt's color-operation handler explicitly drops the .query arm (stream_terminal.zig:616-618), so without this Codex's OSC 11 query ("what is your background?") got silence and Codex skipped emitting its prompt-row gray-bar BG SGR. The scanner runs alongside libghostty in the per-session pty pump and writes the response back via the same Session.QueueWrite keystrokes use. Becomes a no-op once libghostty fills in .query upstream.

Visual before/after

  • Codex: header box now has cleanly closed corners, prompt row shows gray card fill matching cmux.
  • OpenCode: wordmark renders as solid connected blocks (no seams), prompt row has visible gray bg.
  • Claude Code: input divider renders as a clean rule.

Known remaining issue

Cells with explicit BG fills now reveal a pre-existing cell-height issue: session.go's measureCells() uses layout.PixelSize("M") which is 1–2px shy of the font's full ascent+descent, so capital letters and descenders extend slightly outside the cell box. Visible as "clipping" wherever a cell has an explicit BG (Codex's prompt row, OpenCode's prompt). Fixing this is a focused follow-up — switch to Pango font metrics (ascent + descent + leading) for cell height. Out of scope for this PR.

Test plan

  • go test ./... — all pass (sprite geometry tests, OSC scanner regression + new query tests, internal/ghostty build)
  • ./build/build.sh build — clean cgo compile, no missing symbols
  • Codex: codex from a Roost shell — header box closed, prompt row has gray card
  • Claude Code: claude — input divider renders cleanly
  • OpenCode: opencode — wordmark solid, prompt row has gray bg
  • Regression: vim, htop, git log --graph, tree — unchanged; standard ANSI colors render correctly; selection ribbon and cursor unchanged
  • Notification regression: existing OSC 9 / OSC 777 paths still fire (covered by internal/osc/scanner_test.go)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Built‑in theme with 256‑color dark palette and terminal color query responses
    • High‑quality sprite renderer for box‑drawing and block glyphs
    • Per‑session theme and serialized, buffered PTY write pipeline for reliable I/O
  • Bug Fixes

    • Removed initial frame flash and reduced rendering artifacts
    • Selection overlay now uses theme colors
  • Improvements

    • Two‑pass rendering for cleaner glyph/cursor compositing; improved cell measurement
  • Tests

    • Added sprite rendering and OSC query unit tests

charliek and others added 4 commits April 27, 2026 22:10
Pango leaves visible seams between adjacent block-character cells and produces gappy borders for box-drawing characters. Add a custom Cairo geometric renderer for U+2500-U+257F (box drawing) and U+2580-U+259F (block elements) — ported from Ghostty's font/sprite/draw/{box,block}.zig. Pango still handles all other glyphs.
Roost previously left libghostty-vt's palette and default fg/bg
uncustomised, so cells with palette-indexed backgrounds (e.g. Codex's
gray-card chrome resolved to colors that happened to match the surface
background and rendered invisibly.

Add a Theme value type plus a DefaultTheme mirroring cmux's dark scheme
(16 named ANSI entries; 16-255 generated to match libghostty's built-in
xterm cube and gray ramp). Install it via a new SetTheme cgo wrapper at
NewTerminal time. Wire cursor and selection colors through the same
struct so the future config-file loader has every user-visible color
covered the day it lands. Also flip antialias off for the block-element
sprite path as defensive insurance against AA-drift seams.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
COMMITEOF
)
Programs running in the terminal probe its state via OSC and CSI queries (DA1 to detect capabilities, OSC 10/11 for fg/bg color, CSI ?996n for the light/dark scheme). Without WRITE_PTY wired, libghostty had no way to send any response back to the pty, so probing programs got silence and fell back to degraded behaviour. With it wired, a Roost terminal now reports as VT220 with ANSI color (matching xterm-256color), responds to DSR and kitty keyboard queries, and identifies as a dark scheme. Each callback shares libghostty's single userdata slot via a runtime/cgo.Handle wrapping a per-Terminal callbacks struct, freed in Close. A separate callbacks_export.go isolates the //export functions so cgo's auto-generated _cgo_export.h doesnt collide with the static helpers in callbacks.go. OSC 10/11 query responses still dont surface — confirmed via diagnostic that libghostty-vts color-operation handler explicitly drops the .query arm (stream_terminal.zig). That gap is upstream; a Roost-side workaround will follow as a separate change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
libghostty-vts color-operation handler stubs the .query arm (stream_terminal.zig:616-618), so when programs query the terminal for its current foreground/background/cursor colour they get silence. Codex relies on this query-and-respond loop: it asks for the background via OSC 11, and only emits its prompt-row gray-bar bg SGR when it gets an answer. Without one, no gray bar. Extend the existing OSC scanner (already running in the per-session pty pump alongside libghostty for OSC 9/777 notifications) so it also recognises OSC 10/11/12 queries and synthesises the standard rgb:RRRR/GGGG/BBBB response from DefaultThemes fg/bg/cursor. The response is queued through the same Session.QueueWrite that handles keystrokes, so it serialises correctly. Becomes dead code once libghostty-vt fills in its .query arm upstream — at which point both responses race and the program reads the first identical one. Easy to delete then.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Apr 28, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a Theme model and applies it to rendering and libghostty; implements a Cairo sprite renderer and tests for box/block glyphs; wires libghostty callbacks and cgo-exported hooks; replaces per-call PTY write goroutines with a single writer goroutine; extends OSC scanner to synthesize color-query responses.

Changes

Cohort / File(s) Summary
Theme Definition
cmd/roost/theme.go
Adds Theme type, DefaultTheme dark palette, palette generation (16 + 6×6×6 cube + grayscale), and DefaultDeviceAttrs.
Sprite Renderer & Tests
cmd/roost/sprite.go, cmd/roost/sprite_test.go
Adds Cairo-based renderer for U+2500–U+259F and block elements with antialiasing and junction logic; tests validate pixel geometry, tiling, and connectivity.
Rendering Integration
cmd/roost/render.go
Initial background now from DefaultTheme.Background; rendering split into background and glyph passes; geometric glyphs dispatched to drawCellSprite with Pango fallback; selection tint uses theme; adds cellColors helper and targeted logging.
Session & Writer Serialization
cmd/roost/session.go
Session holds per-session theme; libghostty configured (theme, device attrs, dark mode); replaces per-call goroutines+mutex with single FIFO writer (writeChan, QueueWrite, runWriter) and registers it as PTY writer; OSC handling synthesizes color-query replies; measureCells uses Pango recommended line-height.
Ghostty Callbacks (Go & cgo)
internal/ghostty/callbacks.go, internal/ghostty/callbacks_export.go, internal/ghostty/terminal.go
Adds DeviceAttrs, persistent callback state with cgo.Handle lifecycle, SetPtyWriter, SetDeviceAttributes, SetColorSchemeDark, and SetTheme; cgo-exported functions bridge C→Go for PTY writes, device attributes, and color-scheme queries; ensures cleanup on Close.
OSC Scanner & Tests
internal/osc/scanner.go, internal/osc/scanner_test.go
Replaces single-callback API with Handler (OnNotification, OnQueryResponse, QueryColors); recognizes OSC 10/11/12 "?" queries, synthesizes rgb: responses, normalizes terminators, and routes responses; tests cover query synthesis and terminator behaviors.
Docs / Comments
cmd/roost/app.go
Updates inline documentation describing the new per-session buffered writer goroutine and serialization model.

Sequence Diagram(s)

sequenceDiagram
    participant Session
    participant Terminal
    participant GhosttyC as "libghostty C"
    participant Writer

    Session->>Terminal: SetTheme(fg,bg,cursor,palette)
    Terminal->>GhosttyC: set foreground/background/cursor/palette
    GhosttyC-->>Terminal: OK

    Session->>Terminal: SetDeviceAttributes(attrs)
    Terminal->>GhosttyC: register device attributes callback
    GhosttyC-->>Terminal: OK

    Session->>Terminal: SetPtyWriter(QueueWrite)
    Terminal->>GhosttyC: register write callback
    GhosttyC-->>Writer: write events -> QueueWrite

    Session->>Writer: QueueWrite(bytes)
    Writer->>Writer: enqueue and perform ordered pty.Write
Loading
sequenceDiagram
    participant OSCInput
    participant Scanner
    participant Handler
    participant Session

    OSCInput->>Scanner: Feed(OSC 11 "?" BEL)
    Scanner->>Handler: QueryColors()
    Handler->>Session: obtain theme colors
    Session-->>Handler: fg,bg,cursor RGB
    Handler-->>Scanner: synthesized "rgb:..." bytes
    Scanner->>Handler: OnQueryResponse("rgb:... BEL")
    Handler->>Session: QueueWrite(response bytes)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐇 I hop through pixels, draw each line and block,

Themes tuck colors in, callbacks keep the clock.
Sprites stitch seams, queries answer bright,
One writer queues, and bytes take flight. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.42% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title comprehensively covers the three main changes: sprite-based glyph rendering, a default theme system, and terminal-query callbacks. It accurately reflects the primary objectives and is specific enough to convey the changeset's significance.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/box-drawing-sprites

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/ghostty/callbacks_export.go`:
- Around line 21-27: The resolveCallbacks function currently calls
cgo.Handle(uintptr(userdata)).Value() directly and can panic if the handle was
deleted; update resolveCallbacks to guard the Value() call by wrapping it in a
defer/recover so any panic from Value() is caught and the function returns nil
instead of crashing; specifically, around the
cgo.Handle(uintptr(userdata)).Value() invocation in resolveCallbacks, add a
defer that recovers from a panic and sets the returned *terminalCallbacks to
nil, leaving the existing userdata==nil check intact and ensuring callers safely
receive nil for stale/deleted handles.

In `@internal/ghostty/callbacks.go`:
- Around line 97-99: The conditional that clears callbacks calls
C.ghostty_terminal_set(t.c, C.GHOSTTY_TERMINAL_OPT_WRITE_PTY, nil) and then
returns nil unconditionally, swallowing any failure; change both occurrences
(the blocks that pass nil to ghostty_terminal_set when clearing callbacks) to
check the C function's return value and return a non-nil Go error when it
indicates failure (convert the C return code to an error via fmt.Errorf or a
helper like errorFromGhostty) instead of always returning nil so callers can
observe and handle ghostty_terminal_set failures.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 87d3a817-9c91-456e-9bb3-19e03e9980d9

📥 Commits

Reviewing files that changed from the base of the PR and between cc8e7fa and 15c8b60.

📒 Files selected for processing (10)
  • cmd/roost/render.go
  • cmd/roost/session.go
  • cmd/roost/sprite.go
  • cmd/roost/sprite_test.go
  • cmd/roost/theme.go
  • internal/ghostty/callbacks.go
  • internal/ghostty/callbacks_export.go
  • internal/ghostty/terminal.go
  • internal/osc/scanner.go
  • internal/osc/scanner_test.go

Comment thread internal/ghostty/callbacks_export.go Outdated
Comment thread internal/ghostty/callbacks.go
charliek and others added 2 commits April 28, 2026 00:51
…ell height

Two related bugs were combining to clip glyph descenders inside multi-row prompt boxes (opencode and codex gray bars) and visibly compress text like ls output. First, measureCells took cell height from layout.PixelSize("M"), which is the M-glyph's tight box — M has no descender, so per-line cell height was undercounted and descender ink on g/p/q/y bled into the row below. Second, the render walk in drawTerminal painted each cell as BG-fill-then-glyph in row-major order, so even when a descender extended into row N+1, that descender ink was painted before row N+1's BG fill and got immediately overwritten by it.

measureCells now uses Pango's metrics.Height() (recommended baseline-to-baseline distance, includes any line-gap the font specifies; falls back to ascent+descent if the font reports Height = 0). This matches what Ghostty derives from Freetype face metrics and what xterm/kitty use.

drawTerminal splits the cell walk into two sequential passes: pass A paints BG fills only, pass B paints glyphs (and captures cursor info). Doing all backgrounds first means a Pass B descender lands on top of any row N+1 BG fill that was already committed in Pass A, instead of being stamped underneath it. This is the canonical CPU-renderer pattern (xterm.js does the same on Canvas); Ghostty avoids it by composing on the GPU instead. Cellcolors lifted out as a small helper since both passes need to resolve a cell's effective fg/bg/inverse identically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ensureCallbacks was bridging a cgo.Handle into libghostty's void* userdata slot via unsafe.Pointer(uintptr(handle)) — the canonical pattern when the C API takes void*, but go vet's unsafeptr check correctly flags the uintptr->unsafe.Pointer conversion as suspicious because it can't tell that cgo.Handle is GC-pinned. CI lint and Linux/macOS test jobs both fail on it. Add a small static C helper roost_register_userdata that takes a uintptr_t and does the (void*)h cast on the C side. The Go call becomes C.roost_register_userdata(t.c, C.uintptr_t(t.cbsHandle)) — same wire bits, no Go-side unsafe conversion, no vet finding. Drop the now-unused unsafe import.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/roost/session.go`:
- Around line 337-342: QueryColors currently returns hard-coded DefaultTheme
colors; change it to read the live session theme/state so OSC 10/11/12 reflect
current colors. Update the QueryColors closure used in session.go to call
toOSCRGB on the session's current color fields (e.g., s.Theme.Foreground,
s.Theme.Background, s.Theme.Cursor or the equivalent s.State.* fields) instead
of DefaultTheme, so QueryColors returns the session's current colors.
- Around line 192-203: term.SetPtyWriter currently causes each libghostty
callback to spawn a writer goroutine that blocks on writeMu; instead create a
single per-session buffered byte channel (e.g. ptyWriteChan) and a dedicated
per-session writer goroutine that drains that channel and performs the actual
PTY writes (the existing per-tab writer loop used with glib.IdleAdd). Change
term.SetPtyWriter to enqueue raw byte slices into this channel (use a
non-blocking/drop policy or bounded buffer to avoid unbounded growth) and remove
any code paths that spawn new writer goroutines per callback; reference
term.SetPtyWriter, QueueWrite, writeMu and the per-session writer loop when
implementing this switch.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6c2ba291-ff08-481c-bb90-77da44a41b02

📥 Commits

Reviewing files that changed from the base of the PR and between 15c8b60 and cc28d31.

📒 Files selected for processing (2)
  • cmd/roost/render.go
  • cmd/roost/session.go

Comment thread cmd/roost/session.go
Comment thread cmd/roost/session.go

@coderabbitai coderabbitai Bot 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.

♻️ Duplicate comments (1)
internal/ghostty/callbacks.go (1)

102-104: ⚠️ Potential issue | 🟠 Major

Check and propagate clear-path ghostty_terminal_set failures.

Line 103 and Line 121 ignore libghostty return codes when clearing callbacks. That swallows failures and can leave callback state unexpectedly active.

Patch
 func (t *Terminal) SetPtyWriter(fn func([]byte)) error {
@@
 	t.cbs.writePty = fn
 	if fn == nil {
-		C.ghostty_terminal_set(t.c, C.GHOSTTY_TERMINAL_OPT_WRITE_PTY, nil)
+		if rc := C.ghostty_terminal_set(t.c, C.GHOSTTY_TERMINAL_OPT_WRITE_PTY, nil); rc != C.GHOSTTY_SUCCESS {
+			return fmt.Errorf("clear WRITE_PTY: %d", int(rc))
+		}
 		return nil
 	}
@@
 func (t *Terminal) SetDeviceAttributes(d *DeviceAttrs) error {
@@
 	t.cbs.deviceAttrs = d
 	if d == nil {
-		C.ghostty_terminal_set(t.c, C.GHOSTTY_TERMINAL_OPT_DEVICE_ATTRIBUTES, nil)
+		if rc := C.ghostty_terminal_set(t.c, C.GHOSTTY_TERMINAL_OPT_DEVICE_ATTRIBUTES, nil); rc != C.GHOSTTY_SUCCESS {
+			return fmt.Errorf("clear DEVICE_ATTRIBUTES: %d", int(rc))
+		}
 		return nil
 	}

As per coding guidelines, "Return errors instead of logging-and-swallowing; log only at the boundary that handles errors."

Also applies to: 120-122

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/ghostty/callbacks.go` around lines 102 - 104, The calls to
C.ghostty_terminal_set (e.g., the one using t.c and
GHOSTTY_TERMINAL_OPT_WRITE_PTY when fn == nil) ignore and swallow the C return
code; change these to capture the integer return, check for non-zero, and return
a Go error (e.g., fmt.Errorf("ghostty_terminal_set failed: %d") or wrap with a
sentinel) instead of returning nil. Apply the same change to the other
occurrence around lines 120-122 so both clear-path branches propagate failures
from C.ghostty_terminal_set rather than silently ignoring them.
🧹 Nitpick comments (1)
internal/ghostty/callbacks.go (1)

101-108: Apply Go-side callback state only after C registration succeeds.

Each setter currently mutates t.cbs before confirming ghostty_terminal_set success. On failure, the method returns an error but leaves partially-updated Go state.

Refactor sketch
 func (t *Terminal) SetPtyWriter(fn func([]byte)) error {
 	if err := t.ensureCallbacks(); err != nil {
 		return err
 	}
-	t.cbs.writePty = fn
 	if fn == nil {
-		if rc := C.ghostty_terminal_set(t.c, C.GHOSTTY_TERMINAL_OPT_WRITE_PTY, nil); rc != C.GHOSTTY_SUCCESS {
+		if rc := C.ghostty_terminal_set(t.c, C.GHOSTTY_TERMINAL_OPT_WRITE_PTY, nil); rc != C.GHOSTTY_SUCCESS {
 			return fmt.Errorf("clear WRITE_PTY: %d", int(rc))
 		}
+		t.cbs.writePty = nil
 		return nil
 	}
 	if rc := C.roost_register_write_pty(t.c); rc != C.GHOSTTY_SUCCESS {
 		return fmt.Errorf("set WRITE_PTY: %d", int(rc))
 	}
+	t.cbs.writePty = fn
 	return nil
 }
 func (t *Terminal) SetDeviceAttributes(d *DeviceAttrs) error {
@@
-	t.cbs.deviceAttrs = d
 	if d == nil {
 		if rc := C.ghostty_terminal_set(t.c, C.GHOSTTY_TERMINAL_OPT_DEVICE_ATTRIBUTES, nil); rc != C.GHOSTTY_SUCCESS {
 			return fmt.Errorf("clear DEVICE_ATTRIBUTES: %d", int(rc))
 		}
+		t.cbs.deviceAttrs = nil
 		return nil
 	}
 	if rc := C.roost_register_device_attrs(t.c); rc != C.GHOSTTY_SUCCESS {
 		return fmt.Errorf("set DEVICE_ATTRIBUTES: %d", int(rc))
 	}
+	t.cbs.deviceAttrs = d
 	return nil
 }
 func (t *Terminal) SetColorSchemeDark(dark bool) error {
@@
-	t.cbs.hasScheme = true
+	var scheme C.GhosttyColorScheme
 	if dark {
-		t.cbs.colorScheme = C.GHOSTTY_COLOR_SCHEME_DARK
+		scheme = C.GHOSTTY_COLOR_SCHEME_DARK
 	} else {
-		t.cbs.colorScheme = C.GHOSTTY_COLOR_SCHEME_LIGHT
+		scheme = C.GHOSTTY_COLOR_SCHEME_LIGHT
 	}
 	if rc := C.roost_register_color_scheme(t.c); rc != C.GHOSTTY_SUCCESS {
 		return fmt.Errorf("set COLOR_SCHEME: %d", int(rc))
 	}
+	t.cbs.colorScheme = scheme
+	t.cbs.hasScheme = true
 	return nil
 }

Also applies to: 119-126, 137-145

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/ghostty/callbacks.go` around lines 101 - 108, The setter currently
assigns t.cbs.writePty before calling the C registration, which leaves Go state
mutated on C failure; change the logic so the C call (C.ghostty_terminal_set /
C.roost_register_write_pty) is performed first and only on success update
t.cbs.writePty (for the fn==nil branch: clear the C option first and then set
t.cbs.writePty=nil). Apply the same pattern to the other setters referenced (the
blocks around 119-126 and 137-145) so that any C registration/unregistration
completes successfully before mutating t.cbs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@internal/ghostty/callbacks.go`:
- Around line 102-104: The calls to C.ghostty_terminal_set (e.g., the one using
t.c and GHOSTTY_TERMINAL_OPT_WRITE_PTY when fn == nil) ignore and swallow the C
return code; change these to capture the integer return, check for non-zero, and
return a Go error (e.g., fmt.Errorf("ghostty_terminal_set failed: %d") or wrap
with a sentinel) instead of returning nil. Apply the same change to the other
occurrence around lines 120-122 so both clear-path branches propagate failures
from C.ghostty_terminal_set rather than silently ignoring them.

---

Nitpick comments:
In `@internal/ghostty/callbacks.go`:
- Around line 101-108: The setter currently assigns t.cbs.writePty before
calling the C registration, which leaves Go state mutated on C failure; change
the logic so the C call (C.ghostty_terminal_set / C.roost_register_write_pty) is
performed first and only on success update t.cbs.writePty (for the fn==nil
branch: clear the C option first and then set t.cbs.writePty=nil). Apply the
same pattern to the other setters referenced (the blocks around 119-126 and
137-145) so that any C registration/unregistration completes successfully before
mutating t.cbs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 004eb11f-1f44-4e87-9676-f473030d4afe

📥 Commits

Reviewing files that changed from the base of the PR and between cc28d31 and 021aec2.

📒 Files selected for processing (1)
  • internal/ghostty/callbacks.go

charliek and others added 3 commits April 28, 2026 01:09
…lear failures

Two CodeRabbit findings against the WRITE_PTY/DEVICE_ATTRIBUTES/COLOR_SCHEME wiring:

resolveCallbacks called cgo.Handle.Value with no panic guard. Value panics if the handle was already deleted, which can happen if libghostty fires a callback in the brief window between Terminal.Close calling cbsHandle.Delete and the C-side teardown completing. Add a defer-recover that converts the panic into a nil callbacks return; the call sites already check for nil and skip silently, so this turns a potential crash into a benign drop.

SetPtyWriter and SetDeviceAttributes ignored ghostty_terminal_set return codes on the nil-clear path, returning nil unconditionally. If the C call failed, the Go-side state was cleared but libghostty was still pointed at the now-stale callback. Check rc and surface a clear-error so callers can see and react.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ssion

CodeRabbits review pointed out that Session.QueueWrite was spawning a goroutine per call, each blocked on writeMu. Under probe-heavy load (libghostty WRITE_PTY responses + keystrokes + mouse events) those goroutines could pile up serialised on the mutex, growing without bound when the PTY stalls.

Switch to the canonical Go pattern for serialised I/O: a buffered channel feeding one long-lived writer goroutine.

- writeChan (cap 256) carries owned byte slices from senders to the writer.
- runWriter drains writeChan in FIFO order and performs short-write looped pty.Write. One goroutine for the lifetime of the Session.
- QueueWrite becomes a non-blocking select-send. On stop it drops; on full queue it logs and drops. Blocking would freeze the GTK main thread, which is worse than losing one reply during extreme load.
- stopWrite + writerDone chans coordinate teardown: Close signals stopWrite after the pump has exited and waits for writerDone before scheduling libghostty cleanup, so no in-flight pty.Write races with libghostty teardown.
- writeMu and the sync import drop out.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ge global

CodeRabbit flagged that the QueryColors closure resolved each query against DefaultTheme directly. That works today because nothing mutates the colours after construction, but it bakes a coupling: anyone who ever swaps a sessions theme has to remember to update both libghostty (via SetTheme) and the OSC scanner closure separately. Reading from a single per-session source removes that future foot-gun and keeps the answers internally consistent.

Add a theme field to Session, populate it from DefaultTheme in NewSession, push it into libghostty there, and have the QueryColors closure read s.theme.{Foreground,Background,Cursor}. Reading effective colours from libghostty itself would be cleaner still, but ghostty_terminal_get is main-thread-only and the scanner runs in the pump goroutine, so a session-cached theme is the right plumbing for now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
internal/ghostty/callbacks.go (1)

114-121: Own a copy of DeviceAttrs instead of storing caller memory.

roostDeviceAttrsFn reads this later from C callback context, and PrimaryFeatures is a slice. Keeping the caller’s pointer means any later mutation changes live terminal responses and can race with callback reads. Copy the struct and clone PrimaryFeatures in the setter so the terminal owns stable data.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/ghostty/callbacks.go` around lines 114 - 121, SetDeviceAttributes
currently stores the caller's DeviceAttrs pointer directly (t.cbs.deviceAttrs =
d), which can race with C callbacks (roostDeviceAttrsFn) because
DeviceAttrs.PrimaryFeatures is a slice; instead allocate a new DeviceAttrs, copy
all scalar fields and make a fresh slice copy of PrimaryFeatures (and any other
slice or reference fields) and assign that copy to t.cbs.deviceAttrs; handle nil
input by clearing t.cbs.deviceAttrs, and keep the function name
Terminal.SetDeviceAttributes and the field t.cbs.deviceAttrs as the places to
change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/ghostty/callbacks.go`:
- Around line 101-109: The code currently updates t.cbs fields (e.g.,
t.cbs.writePty) before calling the C registration functions
(ghostty_terminal_set / roost_register_write_pty), which can leave Go state
inconsistent on C failure; change these setters so they do the C call first and
only assign t.cbs.writePty (and other callback fields) after the C call
succeeds, and if the C call fails leave the existing t.cbs value unchanged (or
roll back to the previous value) and return the error; apply the same
commit-after-success/rollback pattern to the other affected setters
(SetDeviceAttributes and SetColorSchemeDark) so a failed C call never leaves
Go’s t.cbs out of sync with libghostty.

---

Nitpick comments:
In `@internal/ghostty/callbacks.go`:
- Around line 114-121: SetDeviceAttributes currently stores the caller's
DeviceAttrs pointer directly (t.cbs.deviceAttrs = d), which can race with C
callbacks (roostDeviceAttrsFn) because DeviceAttrs.PrimaryFeatures is a slice;
instead allocate a new DeviceAttrs, copy all scalar fields and make a fresh
slice copy of PrimaryFeatures (and any other slice or reference fields) and
assign that copy to t.cbs.deviceAttrs; handle nil input by clearing
t.cbs.deviceAttrs, and keep the function name Terminal.SetDeviceAttributes and
the field t.cbs.deviceAttrs as the places to change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9060012d-fab5-42c6-b2a5-34055ec1e950

📥 Commits

Reviewing files that changed from the base of the PR and between 021aec2 and 99c4587.

📒 Files selected for processing (4)
  • cmd/roost/app.go
  • cmd/roost/session.go
  • internal/ghostty/callbacks.go
  • internal/ghostty/callbacks_export.go
✅ Files skipped from review due to trivial changes (1)
  • cmd/roost/app.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • cmd/roost/session.go

Comment thread internal/ghostty/callbacks.go Outdated
CodeRabbit follow-up: Set* methods updated t.cbs before calling into libghostty. If the C call failed, Go-side state was already mutated while libghostty was still pointed at the previous handler. Most surfaceable in the clear path: a failed nil-clear would null t.cbs.writePty before libghostty heard about it, so subsequent callbacks would dispatch to a nil function and silently drop bytes — even though the user got a clear-failed error and might assume the old handler was still running.

Reorder all three setters (SetPtyWriter, SetDeviceAttributes, SetColorSchemeDark) to run the C call first and only commit t.cbs.{writePty, deviceAttrs, hasScheme, colorScheme} after success. SetColorSchemeDark builds the scheme value into a local first so the two-field commit happens together.

Safe by construction: roost_register_* registers the function pointer with libghostty, but libghostty does not invoke any callback until vt_write later processes a query. Since these setters run on the GTK main thread alongside vt_write, no callback can fire in the window between C registration succeeding and the Go-side state commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (1)
internal/ghostty/callbacks.go (1)

115-133: Copy DeviceAttrs before storing it.

internal/ghostty/callbacks_export.go:54-82 dereferences cbs.deviceAttrs and PrimaryFeatures directly. Keeping the caller-owned pointer here makes post-registration mutations observable in the callback path and leaves a race-prone footgun in the API.

Patch
 func (t *Terminal) SetDeviceAttributes(d *DeviceAttrs) error {
 	if err := t.ensureCallbacks(); err != nil {
 		return err
 	}
 	if d == nil {
 		if rc := C.ghostty_terminal_set(t.c, C.GHOSTTY_TERMINAL_OPT_DEVICE_ATTRIBUTES, nil); rc != C.GHOSTTY_SUCCESS {
 			return fmt.Errorf("clear DEVICE_ATTRIBUTES: %d", int(rc))
 		}
 		t.cbs.deviceAttrs = nil
 		return nil
 	}
+	attrs := *d
+	attrs.PrimaryFeatures = append([]uint16(nil), d.PrimaryFeatures...)
 	if rc := C.roost_register_device_attrs(t.c); rc != C.GHOSTTY_SUCCESS {
 		return fmt.Errorf("set DEVICE_ATTRIBUTES: %d", int(rc))
 	}
-	t.cbs.deviceAttrs = d
+	t.cbs.deviceAttrs = &attrs
 	return nil
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/ghostty/callbacks.go` around lines 115 - 133, SetDeviceAttributes
currently stores the caller-owned *DeviceAttrs directly (t.cbs.deviceAttrs = d),
which allows post-registration mutation and races; instead make and store an
independent copy: allocate a new DeviceAttrs value, deep-copy any reference
fields (notably PrimaryFeatures and other slices/ptrs) from the incoming d, call
C.roost_register_device_attrs as before, then set t.cbs.deviceAttrs to the newly
copied value so the callback path only sees owned, immutable data.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@internal/ghostty/callbacks.go`:
- Around line 115-133: SetDeviceAttributes currently stores the caller-owned
*DeviceAttrs directly (t.cbs.deviceAttrs = d), which allows post-registration
mutation and races; instead make and store an independent copy: allocate a new
DeviceAttrs value, deep-copy any reference fields (notably PrimaryFeatures and
other slices/ptrs) from the incoming d, call C.roost_register_device_attrs as
before, then set t.cbs.deviceAttrs to the newly copied value so the callback
path only sees owned, immutable data.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: df45855f-a13b-4f26-ace2-ba58e76f27e0

📥 Commits

Reviewing files that changed from the base of the PR and between 99c4587 and bbe0cfc.

📒 Files selected for processing (1)
  • internal/ghostty/callbacks.go

@charliek
charliek merged commit 179ee1c into main Apr 28, 2026
5 checks passed
@charliek
charliek deleted the fix/box-drawing-sprites branch April 28, 2026 06:43
@coderabbitai coderabbitai Bot mentioned this pull request May 17, 2026
12 tasks
charliek added a commit that referenced this pull request May 23, 2026
…ail-hard, entitlements cleanup, PtySupervisor double-emit

Address four findings from the M6-M9 sub-agent review:

**1. Linux reorder events for parity with Mac.** Sub-agent #8
caught that the Mac M9 polish wired `Workspace.tabsReordered` /
`projectsReordered` events but Linux's
`crates/roost-linux/src/daemon/state.rs::reorder_tabs` /
`reorder_projects` mutate positions in-place without emitting.
Added two new `WorkspaceEvent` variants (`TabsReordered`,
`ProjectsReordered`) with the same payload shape as Mac (the
post-reorder display order — supplied prefix + sorted unlisted).
Both reorder methods now `events.send(...)` after persist. The
GTK app.rs match arm currently drops the events with a comment —
the UI's drag-reorder path already updates AdwTabBar inline
before firing the RPC, so re-applying the broadcast would be
double-work. Cross-client convergence is a follow-up slice; the
event is emitted so a future `events.subscribe` consumer (or a
sibling GTK process) can react.

**2. `bundle.sh` codesign fail-hard.** Sub-agent #5 caught that
the previous `codesign … || echo "warn ... (continuing)"` form
swallowed signature failures with exit 0. A botched signature
silently produces a Gatekeeper-rejected app + a notarization
failure at release time. Replaced with a `codesign_or_die`
helper that exits 1 on failure unless `ROOST_ALLOW_UNSIGNED=1`
is set (for the rare dev case where Xcode CLT codesign is
unavailable).

**3. `Roost.entitlements` cleanup.** Sub-agent #6 caught that
`com.apple.security.cs.disable-library-validation` was included
based on a misreading of the docs — library validation governs
in-process `dlopen` / framework loads, not `execve` of a
separately-signed embedded binary (the `Contents/Resources/bin/
roostctl` exec path is governed by Gatekeeper / quarantine,
not library validation). Apple notarization will scrutinize
any unnecessary hardened-runtime entitlements, so removed.
Plist is now an empty dict with a long comment block listing
what we INTENTIONALLY don't need (allow-jit, network.*, sandbox)
and the conditions under which we'd add each.

**4. PtySupervisor double-`.tabExited` race.** Sub-agent #3.b
walked through a scenario where the read source yields `.eof`
and the bg-reap teardown yields `.forcedExit` onto the same
AsyncStream FIFO; the drain task would emit `.tabExited` twice
(once from `reapAndCleanup` on `.eof`, once from `.forcedExit`).
Added an `emittedExit` bool to the drain task that suppresses
the second emit; `reapAndCleanup` is now `@discardableResult ->
Bool` so the drain knows whether it actually emitted (vs. the
session-already-gone-from-close-race case where it returns
false without emitting and the subsequent `.forcedExit` is the
one that should fire). All emit-once paths preserved.

`swift test`: 132/132. `cargo test -p roost-linux`: 20/20
state-machine tests + 34 IPC-dispatch + the rest still green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
charliek added a commit that referenced this pull request May 23, 2026
* fix(linux): Linux IPC robustness sweep (closes #80)

Address the deferred CR items from the inline-core refactor (PR #78).
Reconciled against the merged code: #1 (parse-error reply), #10 (pending
HashSet), #13 (smoke-test ordering) were already done; the rest are fixed
here. Project is early-stage, so the bar is long-term correctness.

PTY supervisor (crates/roost-linux/src/daemon/pty.rs, tab_session.rs):
- #11: wait task now removes the session on child exit (sessions behind
  Arc<Mutex>), so writes to a dead PTY return NotFound instead of
  silently succeeding.
- #12: close() escalates SIGHUP -> SIGKILL after a grace period. The
  cloned portable-pty ChildKiller only sends SIGHUP, so a shell that
  traps/ignores it used to outlive close(); a reaped flag + pid watchdog
  force-kill it, mirroring the Mac teardown.
- #8: send_input/send_resize feed one per-tab serial channel drained by a
  single task, so keystrokes can't reorder (the old per-call tokio::spawn
  could under the multi-thread runtime).

Workspace state (crates/roost-linux/src/daemon/state.rs):
- #4: new project/tab position uses max(position)+1, not len()/count(),
  so a delete-then-create can't collide.
- #5: every mutator now publishes its event while still holding `inner`,
  so broadcast order matches commit order; persist runs after the drop.
  The events-resync Resync path is preserved.

Server framing (crates/roost-ipc/src/server.rs):
- #2: on an envelope decode failure, peel `id` from the raw JSON (string-
  or number-encoded) so the parse-error reply lands at the client's id,
  not 0.

Lifecycle (crates/roost-linux/src/main.rs, ipc.rs, app.rs, messages.rs):
- #7: IPC bind is now a synchronous startup requirement; bind failure
  aborts startup instead of leaving the UI socket-less.
- #6: a second launch dials the new `app.activate` op; the handler
  forwards to the GTK thread which raises the running window.
- #9: events.subscribe returns not-implemented (Linux + Mac) instead of a
  false {} ACK, so clients don't wait forever. Real streaming is deferred
  to its first consumer (roostctl watch).

Tests: write-after-exit -> NotFound, SIGHUP-ignoring child force-killed,
send_input ordering, position max+1 after delete, id-preserving parse
error, bind-failure surfaced, events.subscribe not-implemented. Rust
workspace + Mac build/clippy/tests green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(mac): IPC handler dispatch harness (#80 follow-up)

The Rust handler has tests/ipc_dispatch.rs; the Mac IPCHandlerImpl had
no equivalent, leaving its hand-written cross-cutting logic untested —
strict unknown-field rejection (decodeParams), ipcDim u16 validation,
mapWorkspace/mapPty error-code mapping, the not-implemented/unknown-op
paths, and result encoding. The two handlers must stay behaviorally
convergent over the shared wire contract, so this guards that.

Calls IPCHandlerImpl.handle(op:params:) directly — no socket. Exercises
only non-PTY-spawning ops to avoid the forkpty/swift-testing SIGTRAP that
disables the PTY paths elsewhere (tab.open stays on the manual pass; the
error-mapping ops reach the supervisor only on the lookup-fails path).

Seeds 8 tests: events.subscribe→not-implemented (the #9 path),
unknown-op, unknown-field strictness, project.rename→not-found,
tab.resize out-of-range→invalid-param, tab.resize missing→not-found,
identify profile echo, project.create→tab.list round-trip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(linux): address CodeRabbit review on #80 sweep

Resolves 4 of CodeRabbit's 6 findings on PR #86 (the persistence-ordering
finding is pre-existing/orthogonal and tracked separately).

pty.rs:
- Major: spawn() acquired the master reader/writer AFTER spawn_command,
  so a try_clone_reader/take_writer failure returned with a live child
  and no wait task — an orphaned PTY. Acquire reader+writer BEFORE
  spawning, making spawn_command the last fallible step (no orphan).
- Major: the exit wait task removed the session by tab_id alone, which
  could evict a newer session that reused the same tab_id (close() frees
  the slot synchronously; pty_smoke reuses id 42). Gate the remove on
  Arc::ptr_eq of the per-spawn `reaped` identity so only the owning
  waiter deletes. (My prior "monotonic ids" comment over-assumed caller
  discipline; the supervisor can't rely on it.)
- Major: input and resize were unified at TabSession but re-split into
  the supervisor's two channels (select!), so mixed input/resize wasn't
  FIFO end-to-end. Merge them into one WriterCmd channel drained by a
  single ordered loop — genuine submission-order delivery.

ipc.rs + messages.rs:
- Minor: app.activate ACK'd any payload. Add an empty strict
  AppActivateParams and decode it so the op validates its envelope
  (rejects unknown fields) like every other op.

IPCHandlerTests.swift:
- Minor: replace `as? T != nil` (SwiftLint prefer_type_checking) with a
  meaningful empty-array assertion on a fresh project's tabs.

Rust clippy --all-targets clean; roost-ipc + roost-linux tests green
(A3 ordering + dup-spawn still pass under the new channel/guard). Mac
swift build + 140 tests green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(linux): order state.json persistence by commit seq (CR #3)

Resolves the last CodeRabbit finding on PR #86. persist_async did
synchronous file I/O on the caller thread after dropping `inner`, so two
concurrent mutators could write state.json out of commit order — a slow
earlier commit could clobber a newer one, regressing restart durability.

Keep persistence synchronous (state_persist.rs and restart-reload depend
on it; an async writer thread would break that), but make it ordered:
- Inner.persist_seq: a monotonic commit counter bumped under the lock when
  a snapshot is taken, so the seq reflects commit order.
- snapshot_for_persist now returns (SnapshotFile, seq).
- persist(seq, snapshot) serializes on a new persist_guard mutex and drops
  any snapshot whose seq is <= the highest already written, so the newest
  committed snapshot always wins regardless of which thread races first.

Newest-wins verified by persist_drops_stale_out_of_order_writes. Rust
clippy --all-targets clean; roost-ipc + roost-linux suites green (incl.
state_persist).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* style: rustfmt ipc.rs import block

The AppActivateParams import addition left the use block un-reflowed;
CI's rust-lint (cargo fmt --all -- --check) caught it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
charliek added a commit that referenced this pull request May 28, 2026
…gray bar (#144)

* fix(osc): synthesise OSC 10/11/12 query replies so codex renders its gray bar

Codex (and reportedly claude-code) only emits its highlighted prompt-row
background SGR sequence *after* the terminal answers an OSC 11
background-color query. libghostty-vt's color-operation handler is a
no-op for the .query arm (see stream_terminal.zig upstream), so without
us answering the queries the prompt row renders against the canvas bg
and looks invisible. The legacy Go port fixed this in PR #8 ("Sharper
TUI rendering"); the Rust + Swift ports forwarded the ColorQuery event
to the daemon report path but never synthesised a reply.

Both UIs already emitted `ColorQuery(n)` events from the OSC scanner;
this PR closes the loop:

- `crates/roost-osc/src/lib.rs`: new dependency-free
  `format_color_query_response(n, (r,g,b)) -> Option<Vec<u8>>` that
  produces the standard XTerm `\x1b]N;rgb:RRRR/GGGG/BBBB\x07` form
  (16-bit-per-channel, BEL-terminated) — byte-identical to the legacy
  Go `internal/osc/scanner.go:294-298`. 5 unit tests covering 10/11/12,
  unknown N, and channel order.
- `crates/roost-linux/src/app.rs`: the output-drain task intercepts
  ColorQuery before forwarding to `report_osc_event` and writes the
  reply via `session.send_input(...)` — same per-tab serial channel
  as keystrokes so ordering is preserved relative to typing.
- `mac/Sources/Roost/TerminalView.swift`: `appendBytes` synthesises the
  reply from the current theme and routes it through the existing
  `onKey` callback (same destination as user keystrokes — bytes into
  the PTY's stdin via the tab's keystroke continuation). New
  `formatColorQueryResponse(n:, color:)` static helper mirrors the
  Rust formatter byte-for-byte. 5 swift-testing cases mirror the Rust
  suite.

Verified visually on Mac: codex's "›" prompt row now renders with the
expected gray-card background, matching cmux / legacy Go.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* style: prefer range pattern for OSC color-query number match

* docs: clarify OSC reply ordering + document dynamic-color limitation (#145)

Address Cursor's review feedback on PR #144:
- Update roost-osc module docs to reflect that ColorQuery events now
  have a real synthesised reply path via format_color_query_response.
- Tighten the wiring-site comments on both UIs to make the FIFO
  ordering guarantee precise (serialised with other PTY-input writes
  once enqueued, not against PTY output still draining).
- Document the static-theme vs dynamic-OSC-set limitation inline so a
  future reader sees the trade-off + the link to follow-up #145. Codex
  (the primary use case) only queries, so it's unaffected; vim
  colorscheme-change plugins would hit it.

No behaviour change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

1 participant