tui: surface the operator's messages as a typewriter monolog over the rain#379
Merged
Conversation
… 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
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.
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
UiPanel, the existing path that renders in the matrix area). Persistent.Why TUI-only (no protocol change)
Assistant
Messageevents are emitted per-delta (streaming fragments). Rather than add aSessionEventvariant, the TUI consolidates the orchestrator's deltas across a turn into one finalized string at turn end (AgentStatus active=false), drops the internalnoted/empty no-op token, and plays it once — exactly the "emit once + typewriter" behavior.Message(stream) vsUiPanel(widget) already distinguishes monolog from finding.Changes
app.rs:OperatorMonologstate +operator_utteranceaccumulator; capture inon_notification(accumulate the orchestrator'sAssistanttext, finalize on turn end);operator_monolog_text()filter.ui.rs:render_operator_monolog()— typewriter (type → hold → fade → self-clear), hooked intorender_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_textfilter (dropsnoted/empty, keeps real lines).TestBackendrender test: draws theoperator ▸prompt + revealed text mid-cycle, returns showing=true; after the full cycle returns false and clears the state.0023added.Verification note
Logic + rendering are covered by tests, but the animation is best seen live — the TUI picks up the new
agentbinary 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.