Skip to content

tui: surface the operator's messages as a typewriter monolog over the rain#379

Merged
edwin-zvs merged 1 commit into
mainfrom
operator-monolog-typewriter
Jun 6, 2026
Merged

tui: surface the operator's messages as a typewriter monolog over the rain#379
edwin-zvs merged 1 commit into
mainfrom
operator-monolog-typewriter

Conversation

@edwin-zvs

Copy link
Copy Markdown
Contributor

The operator surfaces genuinely useful lines — e.g. "'run using zarvis' is waiting at the folder trust prompt — press Enter/Yes or exit" — but they only landed in the daemon-owned orchestrator panel, which is collapsed by default. Unless the user opens it, they never see the message.

Design (per discussion)

The operator now surfaces two ways, and it chooses by which event it emits — no new protocol needed:

  • Findings to act on / keep → an Operator widget (UiPanel, the existing path that renders in the matrix area). Persistent.
  • Monolog / "what I just did" → a typewriter line over the matrix rain. When the orchestrator finishes a turn with substantive text, the rain body briefly becomes a monochrome terminal: the line types out, holds, fades, and the rain resumes. Ambient, visible without opening anything, self-dismissing.
┌─ operator · watching ─────────────────────────┐
│ operator ▸                                     │
│ 'run using zarvis' is waiting at the folder    │
│ trust prompt — press Enter/Yes or exit.▌       │
│  · · (types out, holds, fades → rain resumes)  │
└────────────────────────────────────────────────┘

Why TUI-only (no protocol change)

Assistant Message events are emitted per-delta (streaming fragments). Rather than add a SessionEvent variant, the TUI consolidates the orchestrator's deltas across a turn into one finalized string at turn end (AgentStatus active=false), drops the internal noted/empty no-op token, and plays it once — exactly the "emit once + typewriter" behavior. Message (stream) vs UiPanel (widget) already distinguishes monolog from finding.

Changes

  • app.rs: OperatorMonolog state + operator_utterance accumulator; capture in on_notification (accumulate the orchestrator's Assistant text, finalize on turn end); operator_monolog_text() filter.
  • ui.rs: render_operator_monolog() — typewriter (type → hold → fade → self-clear), hooked into render_matrix_rain; widgets still render on top so an open widget isn't hidden; only shows while the rain is visible; rides the existing animation tick.
  • agent.rs: orchestrator system prompt explains its short text shows as a fading monolog and to use a widget for anything persistent/actionable.

Tests

  • operator_monolog_text filter (drops noted/empty, keeps real lines).
  • TestBackend render test: draws the operator ▸ prompt + revealed text mid-cycle, returns showing=true; after the full cycle returns false and clears the state.
  • 251 cli tests + adapter tests pass; full build clean. Spec 0023 added.

Verification note

Logic + rendering are covered by tests, but the animation is best seen live — the TUI picks up the new agent binary on restart, and the operator picks up the new prompt on /agents restart. Then any operator finding (or e.g. a session sitting at a trust prompt) types out over the rain.

… rain

The operator's text replies only landed in the daemon-owned orchestrator panel,
which is collapsed by default -- so a useful line like "'run using zarvis' is
waiting at the folder trust prompt -- press Enter" was invisible unless the user
opened the panel.

Now the operator surfaces two ways, chosen by which event it emits:
- findings to act on/keep -> an Operator widget (existing UiPanel path)
- monolog / "what I just did" -> a typewriter line over the matrix rain: the
  rain body briefly becomes a monochrome terminal, the line types out, holds,
  fades, and the rain resumes. Ephemeral, nothing to open.

The TUI consolidates the orchestrator's streaming assistant Message deltas
across a turn into one finalized string at turn end (AgentStatus active=false),
drops the internal `noted`/empty no-op token, and plays it once. No new
protocol event -- Message (stream) vs UiPanel (widget) already distinguishes
the two, so this is TUI-only.

- app.rs: OperatorMonolog state + operator_utterance accumulator; capture in
  on_notification (accumulate orchestrator Assistant text, finalize on turn
  end); operator_monolog_text() filter.
- ui.rs: render_operator_monolog() typewriter (type -> hold -> fade -> clear),
  hooked into render_matrix_rain; widgets still render on top.
- agent.rs: orchestrator prompt explains monolog (fades) vs widget (persists).

Tests: operator_monolog_text filter + a TestBackend render test (draws the
prompt + revealed text mid-cycle, self-clears after expiry). 251 cli tests
pass. Spec 0023 added.
@edwin-zvs
edwin-zvs merged commit 7c3a075 into main Jun 6, 2026
1 check passed
@edwin-zvs
edwin-zvs deleted the operator-monolog-typewriter branch June 6, 2026 19:43
edwin-zvs added a commit that referenced this pull request Jun 6, 2026
… suppression (#380)

The operator panel showed the operator's response to each ambient/observation
turn ("noted", "Worked", a finding) but not the INPUT that triggered it -- the
observation is a Message event, never rendered to the panel's PTY. So a user
watching the panel saw context-free "noted / Worked / noted ..." and couldn't
tell what was being noted.

Now, before the response streams, the operator echoes the OBSERVATION trigger
into the panel (dim), with the monitor's instruction boilerplate stripped so
only the substance shows -- an ambient monitor finding or a fleet event. The
panel reads question -> answer.

Also removes the dead ambient-turn PTY/message suppression: it keyed on the
stale "ambient operator loop tick" string (the loop's observation became
"ambient fleet monitor flagged" in #376), so it never fired -- and the intent
is now to SHOW responses with their trigger, not hide them. Leaving it was a
trap (fixing its string would have hidden the wanted responses).

The typewriter monolog (#379) is unaffected -- it consumes the response Message
stream; the echo is PTY-only.

- interactive.rs: observation_panel_echo() + emit it before the user Message;
  removed ambient_turn detection + both suppression blocks.
- Test: observation_panel_echo strips the marker/boilerplate, keeps substance,
  returns None for real user input. 38 interactive tests pass. Spec 0024.
edwin-zvs added a commit that referenced this pull request Jun 6, 2026
…re) (#381)

* tui: fix operator monolog showing only the last delta; drop the label

Two issues from dogfooding the typewriter monolog (#379):

1. It displayed only the tail of a message -- e.g. "ed" for "noted" (which
   should be filtered entirely). Cause: `AgentStatus active=true` fires on
   EVERY delta (a per-token "Working" heartbeat), and the TUI cleared the
   utterance accumulator on every active=true. So a message streamed as
   "not"+"ed" lost "not" and finalized as "ed". This truncated ALL multi-delta
   monologs to their last fragment, not just "noted".

   Fix: don't clear on active=true. The accumulator is already cleared at turn
   end (active=false, which fires once per turn), so each turn starts clean.
   Now the full text accumulates -> "noted" is filtered, findings show in full.

2. Dropped the "operator ▸" label -- the matrix panel title already says
   "operator", so it was redundant.

Regression test feeds the exact heartbeat/delta sequence (working, "not",
working, "ed", worked) and asserts "noted" is filtered (not "ed"), plus a
finding across deltas yields the full "session blocked". Render test updated
for the removed label.

* tui: overlay the operator monolog on the rain; skip it when the panel is open

Three refinements from dogfooding:

1. The typewriter took over the rain body (Clear + skip the rain render), so the
   rain froze and restarted from empty when the text cleared. Make it an OVERLAY
   instead: the rain renders every frame (state keeps advancing) and the text
   draws on top, so it never restarts. Moved the call after the rain pass.

2. Skip the overlay while the orchestrator panel (minibuffer) is open -- the
   operator's text is already visible right below, so overlaying it on the rain
   just duplicates it.

3. Widgets are unaffected: render_matrix_widget_viewport runs in its own pass
   after the monolog, independent of the panel/monolog, so the operator can show
   widgets whether the panel is open or collapsed.

Tests: skip-while-panel-open (returns false, draws nothing); existing render +
accumulation tests still pass. 4 monolog tests green.

* tui: match operator monolog bg to the rain backdrop (subtle overlay)

The overlay text used a solid Color::Black background, which read as a highlight
band over the rain. Use Color::Reset (terminal default — the rain's own
backdrop) so the typed line blends in as a subtle overlay.
@edwin-zvs edwin-zvs mentioned this pull request Jun 6, 2026
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