Skip to content

fix: resolve #802 — broker: add VT grid via alacritty_terminal (steal from ht, don't use libghostty)#836

Merged
willwashburn merged 5 commits into
AgentWorkforce:mainfrom
SlncTrZ:CntrB/feat/add-alacritty-terminal-dependency
May 17, 2026
Merged

fix: resolve #802 — broker: add VT grid via alacritty_terminal (steal from ht, don't use libghostty)#836
willwashburn merged 5 commits into
AgentWorkforce:mainfrom
SlncTrZ:CntrB/feat/add-alacritty-terminal-dependency

Conversation

@SlncTrZ

@SlncTrZ SlncTrZ commented May 10, 2026

Copy link
Copy Markdown
Contributor

Summary

fix: resolve #802 — broker: add VT grid via alacritty_terminal (steal from ht, don't use libghostty)

Problem

Severity: Medium | File: Cargo.toml

Add the alacritty_terminal crate 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

  • Existing tests pass
  • Manual review completed
  • No new warnings/errors introduced"

SlncTrZ added 2 commits May 11, 2026 02:00
…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>
@coderabbitai

coderabbitai Bot commented May 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5e24d657-371d-47d3-b5e0-4eb86ac6c8e0

📥 Commits

Reviewing files that changed from the base of the PR and between 4a4c05a and 9283abc.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (1)
  • Cargo.toml
🚧 Files skipped from review as they are similar to previous changes (1)
  • Cargo.toml

📝 Walkthrough

Walkthrough

Integrates 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.

Changes

Terminal State Tracking Integration

Layer / File(s) Summary
New Dependency
Cargo.toml
Adds alacritty_terminal = "0.26" to crate dependencies.
Terminal API & GridSize
src/pty.rs
Imports alacritty_terminal types and introduces GridSize implementing Dimensions for Term sizing and clamps zero dimensions.
PtySession fields
src/pty.rs
Adds term: Arc<Mutex<Term<VoidListener>>> and processor: Arc<Mutex<Processor>> to PtySession.
Spawn initialization & reader loop
src/pty.rs
Creates Term/Processor at spawn from computed GridSize, clones into reader thread; reader advances Processor/Term per PTY chunk while forwarding raw bytes to subscribers.
Struct wiring
src/pty.rs
Wires initialized term and processor into the returned PtySession instance.
Resize and query methods
src/pty.rs
resize updates Term size under locks; public queries (cursor_position, screen_text, cell_at, grid_size) read from Term under mutex.
Offline test helpers
src/pty.rs
Adds parse_into and render test utilities to build and render a standalone Term/Processor.
Offline terminal parsing & live tests
src/pty.rs
Unit tests for parsing/rendering behaviors and live PTY tests updated to assert on PtySession::screen_text and grid_size; adds regression tests for zero-dimension clamping and resize non-panics.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Suggested reviewers:
    • willwashburn

Poem

🐰 I nibbled bytes and leapt with glee,

The grid now holds each cursor's plea,
Processor hums, Term paints the screen,
Raw bytes still flow—both worlds convene,
A rabbit hops where terminals gleam.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: adding the alacritty_terminal dependency to the codebase to replace VT grid functionality, directly addressing issue #802.
Description check ✅ Passed The description includes a summary and mentions testing checklist items, though it lacks complete fill-out of the provided template structure and some sections are minimal.
Linked Issues check ✅ Passed The PR implementation successfully addresses all primary coding objectives from issue #802: adds alacritty_terminal dependency, implements per-session Term and Processor, handles resize operations with proper lock ordering, implements queryable grid methods (cursor_position, screen_text, cell_at, grid_size), and includes comprehensive test coverage.
Out of Scope Changes check ✅ Passed All changes are directly aligned with issue #802 requirements: dependency addition, PTY parser integration, grid querying, and resize handling. No unrelated or extraneous modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/pty.rs (1)

30-31: 💤 Low value

Consider whether parser field 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. Only term needs to be stored for grid/cursor queries (per PR objectives).

The parser field 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

📥 Commits

Reviewing files that changed from the base of the PR and between bffd6b2 and 195f55f.

📒 Files selected for processing (2)
  • Cargo.toml
  • src/pty.rs

Comment thread Cargo.toml
Comment thread src/pty.rs Outdated
Comment thread src/pty.rs Outdated
devin-ai-integration[bot]

This comment was marked as resolved.

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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 win

Serialize resize() with parser advancement.

master.resize() can trigger an immediate redraw from the child, but term.resize() happens afterwards and without the processor lock. 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

📥 Commits

Reviewing files that changed from the base of the PR and between 195f55f and 43389b0.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (1)
  • src/pty.rs

@cubic-dev-ai cubic-dev-ai 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.

No issues found across 3 files

Re-trigger cubic

@cubic-dev-ai cubic-dev-ai 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.

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

Comment thread src/pty.rs Outdated
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>
@willwashburn

Copy link
Copy Markdown
Member

Pushed 4a4c05ad addressing the remaining review feedback. Recap by reviewer:

@coderabbitai

  • (Major) Serialize resize() with parser advancement — Fixed. resize now acquires (processor, term) in the same order the reader thread uses, holds both through master.resize() and term.resize(). Matching lock order also forecloses the deadlock class. Inline comment on the reader loop now documents the convention.
  • (Critical) vte::Parser vs Processor mismatch — addressed in 43389b0 (switched to alacritty_terminal::vte::ansi::Processor).
  • (Critical) Term::new signature + by-value SizeInfo — addressed in 43389b0 (3-arg Term::new(Config::default(), &size, VoidListener); replaced SizeInfo with a local GridSize implementing Dimensions).
  • ⏭️ Add vte as an explicit Cargo.toml dep — N/A in current code. No direct use vte::... anywhere; we go through alacritty_terminal::vte::ansi::Processor (alacritty re-exports vte at its module path), so there's no transitive-dep fragility to guard against.
  • ⏭️ (Nitpick) drop the processor field — kept on purpose. The field is now load-bearing for the resize-race fix: resize takes the same lock the reader uses, which requires the lock to live somewhere accessible from &self. Doc comment updated to reflect this.

@cubic-dev-ai

  • (P1) Validate/clamp VT grid dimensions — Fixed. New GridSize::from_pty(rows, cols) clamps each axis to max(1, n). Used by both spawn and resize. Two regression tests: grid_size_clamps_zero_dimensions_to_one (unit) and resize_to_zero_does_not_panic (live PTY).

@devin-ai-integration

  • SizeInfo zero cell_width/cell_height overflow — already self-resolved when the code switched to a Dimensions-based GridSize (no float math involved anymore). Confirmed in your follow-up comment.

cargo fmt clean. Full broker suite: 581 passed, 0 failed (was 579 before this commit — +2 for the new clamp tests).

@coderabbitai

coderabbitai Bot commented May 17, 2026

Copy link
Copy Markdown
Contributor

Review rate limit: 9/10 reviews remaining, refill in 5 minutes and 50 seconds.

@willwashburn willwashburn merged commit 89c2c36 into AgentWorkforce:main May 17, 2026
47 checks passed
lezacky pushed a commit to lezacky/relay that referenced this pull request May 19, 2026
…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>
lezacky pushed a commit to lezacky/relay that referenced this pull request May 19, 2026
…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>
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.

broker: add VT grid via alacritty_terminal (steal from ht, don't use libghostty)

2 participants