Skip to content

feat(live-room): inline working card widget in the chat stream#392

Merged
spytensor merged 1 commit into
mainfrom
feat/v0.10-381-working-card-widget
May 26, 2026
Merged

feat(live-room): inline working card widget in the chat stream#392
spytensor merged 1 commit into
mainfrom
feat/v0.10-381-working-card-widget

Conversation

@spytensor
Copy link
Copy Markdown
Owner

Closes #381.

Summary

Implements the v0.10 WorkingCard widget per the ADR (docs/v0.10-chat-stream-vs-dashboard.md) Frame B mockup. Each Working-state spawn instance now renders as a multi-line ASCII card spliced into the chat scrollback at the spawning event's chat-time position. The widget reads from SpawnInstance only — no kernel access.

┌─ @security · audit README claims ─────── working · 14s ┐
│ ✓ read README.md §2.4 security model                    │
│ ∴ cross-checking claims against src/permissions/        │
└─ 1 step done · [e]xpand [i]nterrupt [f]ocus ────────────┘

Design choices that needed picking

Card title source. The issue offered two options. I picked adding a title: String field to SpawnInstance (default empty) and wiring CrepEvent::WorkTitle through SpawnLifecycleTracker::apply_event to populate it. Rationale: the tracker is already the single ingest point for the kernel event stream, so this stays consistent with the rest of #380's model and avoids a parallel pending_titles map on the renderer. When no WorkTitle has landed yet the card top border shows the locked (no title) placeholder.

Chat position. Added a chat_position: usize field to SpawnInstance. RoomRuntimeState::apply_event stamps it via a new SpawnLifecycleTracker::set_chat_position(SpawnId, usize) setter at TurnDispatched time, using the current scrollback.len() as the splice index. This keeps the tracker renderer-agnostic — it owns the storage but never reads the value.

Splice rather than overlay. render_scrollback now builds a merged Vec<Line> that interleaves card lines into state.scrollback at each working spawn's chat_position. The existing slicing / scroll_offset / ↓ N new follow indicator (v0.9.16 #371) all continue to work because cards live INSIDE the merged scrollback rather than being rendered as a separate overlay.

Elapsed time. Rendered with Duration::as_secs() granularity. Sub-second drift cannot change the displayed string, so AC-5 (at most once per second) holds without an explicit timestamp gate.

Acceptance criteria

  • AC-1: New widget WorkingCard renders one Working spawn per the visual. Reads SpawnInstance only.
  • AC-2: Card sits at the spawning event's chat-time position, not pinned at the bottom. Cards scroll with history (ADR Q1).
  • AC-3: Identity color from tui_style::role_color — no hard-coded literals for role identity.
  • AC-4: Long tool-call summaries are middle-truncated to fit the card width.
  • AC-5: Elapsed updates at whole-second granularity; sub-second drift does not change the rendered string.
  • AC-6: Renderer tests cover the three required cases (see below).
  • AC-7: Hotkey hint string [e]xpand [i]nterrupt [f]ocus renders but pressing the keys does nothing — v0.9.16: per-card hotkeys for working cards — [e]xpand [i]nterrupt [f]ocus #385's surface.

Validation

cargo fmt --all -- --check                  clean
cargo build --lib --quiet                   clean
cargo test --lib spawn_lifecycle --quiet    17 passed (was 14)
cargo test --lib working_card --quiet       20 passed (new module)
cargo test --lib console_room_runtime --quiet  71 passed (was 65)
cargo test --test console_terminal_qa_test --quiet  5 passed
cargo test --lib --quiet                    737 passed; 1 ignored
cargo clippy --all-targets --all-features -- -D warnings  clean

Required evidence — three rendered scenes

Scene A (AC-6a) — zero tool calls, card shows just title:

┌Room──────────────────────────────────────────────────────────────────────────────────────────────┐
│             ┌─ ◆ @security · audit README claims ── working · 0s ───────────────────────────────┐│
│             └─ 0 steps done · [e]xpand [i]nterrupt [f]ocus ─────────────────────────────────────┘│
│  @security ─────────────────────────────────────────────────────────── starting                  │
│                                                                                                  │
…
└──────────────────────────────────────────────────────────────────────────────────────────────────┘

Scene B (AC-6b) — one done + one in-progress:

┌Room──────────────────────────────────────────────────────────────────────────────────────────────┐
│             ┌─ ◆ @security · audit README claims ── working · 0s ───────────────────────────────┐│
│             │ ✓ read README.md §2.4 security model                                              ││
│             │ ∴ Grep                                                                            ││
│             └─ 1 step done · [e]xpand [i]nterrupt [f]ocus ──────────────────────────────────────┘│
│  @security ─────────────────────────────────────────────────────────── starting                  │
…

Scene C (AC-6c) — 5 tool calls (>N=3), oldest 2 dropped:

┌Room──────────────────────────────────────────────────────────────────────────────────────────────┐
│             ┌─ ◇ @backend · verify technical claims ── working · 0s ────────────────────────────┐│
│             │ ✓ step 3 of 5                                                                     ││
│             │ ✓ step 4 of 5                                                                     ││
│             │ ✓ step 5 of 5                                                                     ││
│             └─ 5 steps done · [e]xpand [i]nterrupt [f]ocus ─────────────────────────────────────┘│
│  @backend ──────────────────────────────────────────────────────────── starting                  │
…

(Scenes captured from cargo run --example against RoomRuntimeState driven through real CrepEvents — the example file itself is not committed; the equivalent test fixtures live in src/console_room_runtime.rs::tests and src/working_card.rs::tests.)

Files touched

  • src/working_card.rs (new) — the widget plus 14 renderer unit tests (state gating, zero/one+/overflow tool calls, identity color, elapsed label, middle-truncation, marker glyphs).
  • src/spawn_lifecycle.rs — added title and chat_position fields to SpawnInstance; routed CrepEvent::WorkTitle through apply_event; added set_chat_position setter; 3 new lifecycle tests.
  • src/console_room_runtime.rs — stamped chat_position on TurnDispatched; added build_merged_scrollback; 6 new integration tests.
  • src/lib.rs — registered the new module.

Out of scope (per #381)

  • Hotkey wiring (#385).
  • Done and Reported collapsed/report dual emission (#384).
  • Footer narration (#382).
  • Card density auto-collapse (#386).
  • Right rail changes (#383).

Closes #381. Renders each `Working`-state spawn instance as an inline
ASCII card spliced into the chat scrollback at the spawning event's
chat-time position, per ADR Frame B.

- New `working_card` module wraps the visual locked in the
  chat-stream-demo (#379) and reads only from `SpawnInstance`.
  Markers: `✓` done, `∴` in-progress, `⨯` failed. Identity color
  comes from `tui_style::role_color` — no hard-coded literals.
- `SpawnInstance` gains a `title` field (populated from `WorkTitle`
  events the tracker now consumes) and a `chat_position` field
  (stamped by `RoomRuntimeState` at `TurnDispatched` time).
- `render_scrollback` builds a merged scrollback that interleaves
  card lines at each spawn's `chat_position`, then applies the
  existing scroll-offset / wrap logic. Cards therefore scroll with
  history (ADR Q1 — no pinning) and the v0.9.16 `↓ N new` follow
  indicator continues to work.
- Elapsed time renders at whole-second granularity so sub-second
  re-renders never change the rendered string (AC-5 flicker guard).
- Long tool-call summaries middle-truncated to fit `card_width`.
- Hotkey hint `[e]xpand [i]nterrupt [f]ocus` is rendered as a
  non-functional string per AC-7; `handle_key` is untouched (#385
  will wire it).

Addresses @Reviewer must-fix items on the original draft:

1. Rebased onto current main (which now has #390 footer narration and
   #391 rail slim). Tests verify the working card, footer narration
   row, and slim rail coexist at 120×30.
2. `push_scrollback` now calls `spawn_lifecycle.shift_chat_positions`
   after the 1000-row drain so every Working card's anchor index
   shifts left by `overflow` rows (saturating at 0). Without this the
   card would silently slip out of place after long sessions, which
   would have broken AC-2.

Two new regression tests pin both:

- `working_card_position_survives_scrollback_drain` floods 1500 rows
  while one card is Working, then asserts `chat_position` shifted and
  the card still precedes the latest line in the merged scrollback.
- `build_merged_scrollback_is_idempotent` rerenders the same state
  twice and asserts byte-for-byte identical output.

Validation: cargo fmt clean, cargo clippy --all-targets -- -D warnings
clean, cargo test --lib console_room_runtime (77 pass — 65 existing +
6 footer narration + 6 working card integration), cargo test --lib
working_card (21 pass), cargo test --lib spawn_lifecycle (17 pass).
@spytensor spytensor force-pushed the feat/v0.10-381-working-card-widget branch from 6c71226 to 2510ce2 Compare May 26, 2026 09:31
@spytensor spytensor merged commit b25f683 into main May 26, 2026
5 checks passed
@spytensor spytensor deleted the feat/v0.10-381-working-card-widget branch May 26, 2026 09:33
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.

v0.9.16: inline working card widget in the chat stream

1 participant