mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-11 13:41:53 +00:00
Brainstormed design (glitch 2026-06-13). TUI-only, no core gateway/agent changes — builds on existing events (notification.show/clear, background.complete, subagent.*, agents.list, process.stop, session.interrupt). Approach 1: shared substrate + two surfaces + multi-channel notifications (inline card + ambient badge + OSC) + input-density pass. Phased P1–P4 with per-phase gates.
10 KiB
10 KiB
OpenTUI — Background Activity: agents inspection, background panel, notifications + density
Status: SPEC (brainstormed with glitch 2026-06-13) · target branch feat/opentui-native-engine
Hard constraint: TUI-LAYER ONLY (ui-opentui/). Zero changes to tui_gateway/server.py or
run_agent.py core. Build only on gateway events/RPCs that already exist. Everything below was
feasibility-checked against the live gateway surface (see "Gateway surface" §).
Why
Dogfeedback (screenshots iznq/qxpe/rpiw/rplj):
- Agents dashboard is too crowded (
rplj) — master rows dump each subagent's full multi-line prompt; the trace pane is squished. Inspection + transcript reading is "not great." - Background processes are basically invisible (
qxpe) — completions leak into the transcript as plain lines that read like model output; no panel, no badge, notifications are non-existent. - Input zone is too crowded (
rpiw) — status bar + composer + agents tray + completion menu + shell note stack under the transcript.
Design decisions (from the brainstorm)
- Two SEPARATE surfaces, ONE shared substrate. Background agents (delegated subagents) and background work (detached runs + OS processes) are visually/feature-wise distinct, but share the underlying tracking + notification + badge plumbing.
- Notifications are multi-channel on every relevant state change:
- (C) inline card in the transcript — a distinct, colored, collapsed system card, clearly NOT model output (replaces today's plain-line leak).
- (A) ambient badge — a live count in chrome (status-bar
bg:/the⚡ N agentstray) that flashes on change; you pull-to-inspect. Stays visible while things run. - OSC desktop — reuse the EXISTING
boundary/termChrome.ts(notify, OSC 9/99/777, already focus-gated so it only fires when the terminal is blurred).
- Agents surface = inspection only. No foregrounding / "become the subagent" (that would change core subagent UX — explicitly out of scope). Scannable list + a faithful render of the already- tracked live activity (goal/model/reasoning/tool calls/progress/summary). No new fetch.
- Background surface = view + stop. List runs + OS processes with status/uptime; cancel a run
(
session.interrupt/subagent.interrupt); stop-all OS processes (process.stop). Per-process kill and per-process logs are NOT exposed as RPCs → out of scope under the no-core rule (noted). - Input density is in scope (own phase).
Gateway surface we build on (verified — all already exist)
| Need | Mechanism (existing) |
|---|---|
| Background-run lifecycle | prompt.background (start), background.complete (event) |
| Notifications | notification.show / notification.clear events — payload {text, level, kind, ttl_ms, key, id} |
| Subagent stream | subagent.spawn_requested/start/thinking/tool/progress/complete events (store already consumes) |
| List OS processes | agents.list RPC → {processes:[{session_id, command, status, uptime_seconds}]} |
| Stop OS processes | process.stop RPC → kill_all() (all, not per-process) |
| Cancel a run / subagent | session.interrupt, subagent.interrupt |
| List active sessions/runs | session.active_list, session.status |
| Subagent trace (archived) | spawn_tree.list/load (already used by /replay) |
| OSC desktop notify | boundary/termChrome.ts notify(TermNotification) |
Honest limits (no-core constraint): OS processes get list + stop-all only — no per-process kill
(process_registry.kill_process exists but isn't an RPC) and no per-process log tail
(read_log isn't an RPC). If the no-core rule is ever relaxed, each is a ~5-line additive @method.
Architecture (Approach 1 — substrate-first)
gateway events ──► store: backgroundActivity slice ──► derived counts/state
│ │
├─► notificationDispatcher ─────────┼─► (C) inline card (transcript)
│ (card + badge + OSC) ├─► (A) ambient badge (statusBar/tray)
│ └─► OSC via termChrome.notify
├─► Surface 1: AgentsDashboard (revamp) — list + rich activity pane
└─► Surface 2: BackgroundPanel (new) — runs + processes, stop
Shared substrate (the "underneath" both surfaces use)
logic/backgroundActivity.ts(new) — pure model + reducers. Types:BackgroundRun(fromprompt.background/background.complete/session.active_list):{ id, label, status: 'running'|'complete'|'failed'|'cancelled', startedAt, summary? }BackgroundProcess(fromagents.list):{ sessionId, command, status, uptimeSeconds }Notification(fromnotification.show):{ id, key?, text, level, kind, ttlMs?, at }- Pure helpers:
applyNotification,clearNotification(key), counts (runningCount),mergeProcessList, dedupe bykey/id. Fully unit-testable (no renderer).
store.ts— abackgroundActivityslice + event handlers fornotification.show/clear,background.complete, and a polledagents.listsnapshot (poll only while a panel/badge is live, or piggyback existing cadence). Existingsubagent.*handling is untouched.logic/notificationDispatcher.ts(new, pure) — given a state-change, decide the channels: returns{ card?: SystemCard, badge: delta, osc?: TermNotification }. The boundary callstermChrome.notifyfor the OSC part; the store appends the card + bumps the badge.
Surface 1 — Agents inspection overlay (revamp view/overlays/agentsDashboard.tsx)
- Master list rows = ONE line each:
<statusGlyph> <truncated goal (truncRight to width)> · <model>. No multi-line prompt dump. Selected row highlighted (existing▸+ accent). - Detail pane = faithful activity transcript of the selected agent, styled like the main
transcript (not flat dumped lines): goal+model header, then the trace rendered by type
(reasoning / tool-call+result / progress / final summary), newest last, sticky-bottom, PgUp/PgDn.
- Requires giving
SubagentInfo.tracelight typing ({ kind:'tool'|'reasoning'|'progress'|'summary', text }) instead ofstring[], populated wheresubagent.*events are reduced. Internal data-shape change only; no gateway change.
- Requires giving
- Keep Esc/q close, ↑↓ select. Reuse theme +
truncRightfrom statusBar.
Surface 2 — Background panel (new view/overlays/backgroundPanel.tsx)
- Two sections: Runs (background agent runs) and Processes (OS processes from
agents.list). - Each row: status glyph + label/command (truncated) + uptime/elapsed + status.
- Actions:
↑↓select; on a run →ccancel (session.interrupt/subagent.interrupt); global stop-all processes (x→process.stop, confirm). Esc/q close. - Access: new client slash
/bg(alias/background,/jobs) inlogic/slash.tsCLIENT set →store.openBackgroundPanel(). Also reachable from the ambient badge. - Poll
agents.liston open + on a light interval while open; stop polling on close.
Notifications (the (C)+(A)+OSC wiring)
- (C) inline card — a new transcript element
view/notificationCard.tsx: a bordered/colored,selectable:falsesystem card keyed bynotification.id, level-tinted (info/warn/error), collapsed to one line by default with thekind+text; clearable bynotification.clearkey. Appended into the message stream as a distinct row type (NOT a plainsystemtext line). Replaces the current plain-line leak. (/detailsinterplay: cards are chrome, always shown, never windowed.) - (A) ambient badge —
statusBar.tsxbg: Nsegment (already reserved) bound torunningCount(); theagentsTray.tsxcount already exists — extend it to "agents + background." Flash/recolor on a fresh notification (brief). - OSC — on
notification.showwith a terminal level (complete/failed), calltermChrome.notify({title, body})(already focus-gated). No new escape-sequence code.
Input-zone density pass (view/composer.tsx / view/App.tsx)
- Audit what stacks under the transcript and collapse/gate: the
⚡ N agentstray line folds into the ambient badge (shrinks one line); ensure the shell-mode note, completion menu, and status bar don't co-stack more than necessary. Concrete rules decided with a tmux density pass (ASCII-mocked, approved) — kept minimal; no behavior change, just fewer competing chrome lines.
Phases (implementation order — each gated + tmux-smoked + committed)
- P1 — Notification substrate (
backgroundActivity.ts+notificationDispatcher.ts+ store slice +notificationCard.tsx+ badge wiring + OSC call). Highest visible win; the shared core. - P2 — Agents inspection revamp (
agentsDashboard.tsx+ typedtrace). De-crowdsrplj. - P3 — Background panel (
backgroundPanel.tsx+/bg+ actions). New surface. - P4 — Input density pass. Folds the tray into the badge; trims co-stacked chrome.
Testing / gates (per phase)
- Pure logic (
backgroundActivity,notificationDispatcher, slash/bgrouting, trace-typing) → vitest unit tests, TDD where natural. - Views → headless frame tests (
renderProbe) for the card, the de-crowded dashboard row format, the background panel sections; + live tmux smoke (tmux-pane-screenshot) for each surface using a seeded-store harness (theuxSmokepattern:store.apply/applyInfo/commitSnapshot+ canned events). - Gate
cd ui-opentui && npm run checkgreen (judge by real exit, not a piped tail) after each phase; rebuilddist/main.js; commitopentui(v6): …(no attribution) and push per standing instr.
Out of scope (explicit)
- Foregrounding / "becoming" a subagent (B/C from the brainstorm) — would change core subagent UX.
- Per-process kill + per-process log tail for OS processes — needs additive gateway RPCs (no-core veto).
- "Collect result into transcript" for finished runs — deferred (Q6=B, view+stop only).
- Any change to
tui_gateway/server.py/run_agent.py.