fix: resolve #802 — broker: add VT grid via alacritty_terminal (steal from ht, don't use libghostty)#836
Conversation
…erminal (steal from ht, don't use libghostty) Fixes AgentWorkforce#802 Signed-off-by: Dinh Truong (SlncTrZ) <46520299+SlncTrZ@users.noreply.github.com>
…erminal (steal from ht, don't use libghostty) Fixes AgentWorkforce#802 Signed-off-by: Dinh Truong (SlncTrZ) <46520299+SlncTrZ@users.noreply.github.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughIntegrates alacritty_terminal into PtySession: each session now owns a shared Term and Processor advanced on PTY output, exposing grid text, cell lookup, cursor position, and proper resize handling; tests added for offline parsing and updated live session assertions. ChangesTerminal State Tracking Integration
Sequence Diagram(s)sequenceDiagram
participant Spawn as PtySession::spawn
participant PTY as PTY device
participant Reader as Reader thread
participant Processor as vte::ansi::Processor
participant Term as alacritty_terminal::term::Term
participant Subs as Subscribers (mpsc)
Spawn->>Processor: create & Arc<Mutex>
Spawn->>Term: create with GridSize & Arc<Mutex>
PTY->>Reader: bytes(chunk)
Reader->>Processor: lock, Processor::advance(&mut Term, chunk)
Processor->>Term: update grid, cursor, cells
Reader->>Subs: forward raw bytes
Other->>PtySession: queries (screen_text, cell_at, cursor_position) read Term under mutex
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
src/pty.rs (1)
30-31: 💤 Low valueConsider whether
parserfield is needed after fixing the integration.If switching to
alacritty_terminal::ansi::Processor, it can be instantiated locally in the reader thread since it doesn't need to be shared. Onlytermneeds to be stored for grid/cursor queries (per PR objectives).The
parserfield would become unused unless there's a specific need to access parser state externally.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/pty.rs` around lines 30 - 31, The parser field (Arc<Mutex<Parser>>) appears unnecessary after switching to alacritty_terminal::ansi::Processor; remove the parser field from the struct and any related uses, keep only term: Arc<Mutex<Term>> for shared grid/cursor access, and instantiate a local Processor inside the reader thread (e.g., where the reader loop or spawn_reader is created) instead of sharing parser state; update any constructors, impls, and tests that referenced parser to use a locally created Processor or term access as appropriate.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Cargo.toml`:
- Line 41: The crate directly imports vte::Parser (see src/pty.rs import) but
Cargo.toml only relies on alacritty_terminal's transitive dependency; add an
explicit dependency entry vte = "0.15" to the [dependencies] section of
Cargo.toml so vte is guaranteed for direct use.
In `@src/pty.rs`:
- Around line 17-18: The code currently uses vte::Parser directly with
alacritty_terminal::term::Term, but Parser::advance requires a vte::Perform impl
and Term implements alacritty_terminal::ansi::Handler; replace the Parser usage
with alacritty_terminal::ansi::Processor which adapts a vte::Parser to work with
Term: change the parser field type to Processor (instead of vte::Parser), update
imports (remove vte::Parser import and add alacritty_terminal::ansi::Processor),
initialize the Processor at spawn (wrapping a new vte::Parser inside Processor),
and in the parsing loop replace the call parser_guard.advance(&mut *term_guard,
*byte) with processor.advance(&mut *term_guard, *byte) so the types align.
- Around line 125-127: Term::new is called with the wrong signature and a
by-value SizeInfo; change the call to pass a reference and a third event proxy
implementing EventListener (e.g. create or reuse an EventListener/EventProxy
type), i.e. construct SizeInfo as before, then call
Term::new(Default::default(), &size_info, event_proxy) where event_proxy
implements the alacritty_terminal::event::EventListener trait (or use an
existing adapter in your crate), and keep the Arc::new(Mutex::new(...)) wrapping
the resulting Term.
---
Nitpick comments:
In `@src/pty.rs`:
- Around line 30-31: The parser field (Arc<Mutex<Parser>>) appears unnecessary
after switching to alacritty_terminal::ansi::Processor; remove the parser field
from the struct and any related uses, keep only term: Arc<Mutex<Term>> for
shared grid/cursor access, and instantiate a local Processor inside the reader
thread (e.g., where the reader loop or spawn_reader is created) instead of
sharing parser state; update any constructors, impls, and tests that referenced
parser to use a locally created Processor or term access as appropriate.
🪄 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 Plus
Run ID: 38117230-7360-42de-93df-213e2b92ffc0
📒 Files selected for processing (2)
Cargo.tomlsrc/pty.rs
The starter scaffold added the dependency and a Term/Parser stub but
didn't compile against alacritty_terminal 0.26 — wrong `vte::Parser`
import, missing `EventListener` type param, two-arg `Term::new`, and
the now-removed `SizeInfo` shape. Replaces all of that with the
0.26 API and wires the grid up well enough for callers to query.
- `alacritty_terminal::event::VoidListener` instead of an absent
listener — keeps `Term` parameterized on `<VoidListener>` so we
pay nothing for event dispatch we don't need.
- `alacritty_terminal::vte::ansi::Processor` (the chunk-based
wrapper) instead of raw `vte::Parser` — one `advance(&mut term,
&chunk)` per PTY read instead of byte-by-byte.
- Local `GridSize { columns, screen_lines }` implementing
`Dimensions` instead of the test-helper `TermSize` from
`term::test::*`.
- Resize plumbs through to `Term::resize` correctly (the main
implementation risk AgentWorkforce#802 flagged).
Adds query methods to `PtySession`:
- `cursor_position() -> (row, col)` — 1-indexed, matching how
`WaitCondition::Cursor` (AgentWorkforce#800 / PR AgentWorkforce#837) and the public API
talk about cells.
- `screen_text() -> String` — visible viewport rendered as
plain text, trailing blanks trimmed per row.
- `cell_at(row, col) -> Option<char>` — point query, 1-indexed.
- `grid_size() -> (rows, cols)` — current dimensions.
These unblock the `Cursor` wait variant from AgentWorkforce#800 / PR AgentWorkforce#837 and
give AgentWorkforce#864's `view`/`drive`/`relay` clients a way to redraw current
screen state on attach instead of starting blank.
Tests (`src/pty.rs::tests`, 7 new offline + 2 new live-PTY):
- plain-text writes land in the grid
- CUP `ESC[3;5H` lands the cursor at line 2 col 4 (0-indexed)
- `\r` returns to col 0 and subsequent text overwrites
- CSI color sequences leave only the visible chars in the grid
- `ESC[2J ESC[H` clears + homes correctly
- chunked vs one-shot byte streams produce identical grids
(Processor state survives between advance() calls)
- live PTY echoing "hello-grid" populates the visible screen
- resize updates grid dimensions
Full broker suite: 579 passed, 0 failed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/pty.rs (1)
215-229:⚠️ Potential issue | 🟠 Major | ⚡ Quick winSerialize
resize()with parser advancement.
master.resize()can trigger an immediate redraw from the child, butterm.resize()happens afterwards and without theprocessorlock. That leaves a race where the reader thread can parse post-resize bytes against the old grid dimensions.Suggested fix
pub fn resize(&self, rows: u16, cols: u16) -> Result<()> { + let _processor = self.processor.lock(); + let mut term = self.term.lock(); + self.master .resize(PtySize { rows, cols, pixel_width: 0, pixel_height: 0, }) .context("failed to resize pty")?; let size = GridSize { columns: cols as usize, screen_lines: rows as usize, }; - self.term.lock().resize(size); + term.resize(size); Ok(()) }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/pty.rs` around lines 215 - 229, The resize race happens because master.resize() can cause the child to emit data that the reader thread parses against the old grid; fix it by serializing resize with the terminal parser/processor: acquire the processor lock (the same lock used by the reader/parser) before calling master.resize(), perform master.resize(), then call term.lock().resize(size) while still holding that processor lock (or immediately advance the parser while holding the lock) and only then release the processor lock so the reader cannot parse bytes against the old dimensions; refer to the resize() method, master.resize(), term.lock().resize(), and the processor/parser lock to implement this ordering.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@src/pty.rs`:
- Around line 215-229: The resize race happens because master.resize() can cause
the child to emit data that the reader thread parses against the old grid; fix
it by serializing resize with the terminal parser/processor: acquire the
processor lock (the same lock used by the reader/parser) before calling
master.resize(), perform master.resize(), then call term.lock().resize(size)
while still holding that processor lock (or immediately advance the parser while
holding the lock) and only then release the processor lock so the reader cannot
parse bytes against the old dimensions; refer to the resize() method,
master.resize(), term.lock().resize(), and the processor/parser lock to
implement this ordering.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 1fa1d423-57ac-43ee-bddb-b076987e8af7
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (1)
src/pty.rs
There was a problem hiding this comment.
1 issue found across 3 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/pty.rs">
<violation number="1" location="src/pty.rs:158">
P1: Validate/clamp VT grid dimensions before `Term::new`/`resize`; zero rows/columns can trigger alacritty grid underflow/panic paths.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
Fix all with cubic | Re-trigger cubic
Two issues surfaced by PR AgentWorkforce#836 review: 1. coderabbit (Major): the reader thread holds `(processor, term)` while advancing the parser, but `resize()` only locked `term` — and only AFTER `master.resize()`. The child often emits an immediate redraw after resize; the reader could parse those bytes against the OLD grid dimensions before `term.resize()` ran. Fix: take `processor` then `term` (matching the reader's lock order) BEFORE `master.resize()`, and hold both through `term.resize()`. Same-order locking also prevents the deadlock class. 2. cubic (P1): zero rows/cols would land in alacritty's grid allocator and either panic or wrap to `usize::MAX`. PTY resize events of 0 do arrive in practice (window minimized, container without a TTY, teardown races). Fix: new `GridSize::from_pty(rows, cols)` clamps each axis to at least 1. Used by both `spawn` and `resize`. Two regression tests cover the clamp directly and the live `resize(0, 0)` path. `processor` field doc updated to reflect that `resize` is now a deliberate second consumer of the lock (it's no longer just held to extend the lifetime of the reader-thread clone). Inline reader-loop comment updated to call out the locking convention. `cargo fmt` clean. Full broker suite: 581 passed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Pushed
|
|
Review rate limit: 9/10 reviews remaining, refill in 5 minutes and 50 seconds. |
…ntWorkforce#869) Adds `src/snapshot.rs` with `Snapshot::capture` / `to_plain` / `to_ansi`, exposes it over `GET /api/spawned/{name}/snapshot?format=plain|ansi`, and ships an `agent-relay-broker dump-pty <name>` admin CLI that wraps the route. Renderers walk the existing alacritty VT grid (AgentWorkforce#836) once under the term lock, then drop it — captured state is self-contained so the PTY reader thread keeps advancing while callers render. The HTTP request flows broker → PTY worker subprocess via a new `snapshot_pty` / `snapshot_response` frame pair keyed by request_id, with a 5s timeout sweep in the broker's reap_tick so a crashed worker can't hang the HTTP oneshot. The `ansi` payload is base64 because the bytes contain control characters; `dump-pty --format ansi` decodes and writes raw bytes to stdout for terminal re-render. 12 new tests (8 snapshot unit + 4 HTTP route), 600 total pass (was 588). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…AgentWorkforce#867) PR AgentWorkforce#836 wired the alacritty VT grid into every PtySession but passed VoidListener to Term::new, so alacritty parsed query sequences (DSR/DA1/DA2/CPR) and discarded the responses. A parallel hand-rolled TerminalQueryParser in helpers.rs answered those queries with a hardcoded 1;1 CPR reply that ignored the real cursor position. Replace VoidListener with RelayEventListener, which owns a std::sync::mpsc::Sender<Vec<u8>>. alacritty's send_event(PtyWrite) hands query response bytes to the channel; a dedicated drainer thread takes the writer lock and pushes them down the PTY. The listener is non-blocking so it is safe to call while processor+term locks are held, and the drainer exits when the listener is dropped at PtySession teardown. CPR responses now reflect the live cursor position (verified by the new unit test: ESC[3;5H ESC[6n yields ESC[3;5R, not ESC[1;1R). Delete the hand-rolled parser and its tests in helpers.rs, plus its three call sites in pty_worker.rs, wrap.rs, and the test module in main.rs. Make Snapshot::from_term generic over EventListener so it accepts both Term<RelayEventListener> from a live PtySession and the Term<VoidListener> used in offline tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
fix: resolve #802 — broker: add VT grid via alacritty_terminal (steal from ht, don't use libghostty)
Problem
Severity:
Medium| File:Cargo.tomlAdd the
alacritty_terminalcrate to enable a real VT100‑compatible parser and grid. This dependency is pure Rust, cross‑platform, and production‑grade (used by Alacritty itself). No additional features are required beyond the default set, as we only need the terminal emulation state, not rendering or windowing.Solution
Under
[dependencies], add:Changes
Cargo.toml(modified)src/pty.rs(modified)Testing