# 2026-08-04 — Chat interaction overhaul: inline progressive stream (Claude Code style)

**Status:** Planned — not started. (Refocused from the earlier feature-heavy
draft; backend features deferred — see "Deferred".)

## Goal

Streamline agent interactions — thinking, plan, tool usage, responses — into
**one linear progressive inline stream per turn** (the Claude Code / Cline /
Roo pattern), instead of the current split where the transcript shows a
collapsed trace and the real live activity lives in a separate rail timeline.
The right rail becomes **graph-only** (and auto-zooms to fit all entities).

## Locked decisions (operator interview)

| Decision | Choice |
|---|---|
| Live activity layout | **Inline stream (Claude Code)** — one progressive column per turn; rail keeps ONLY the Scope graph; Activity timeline tab removed |
| Tool-call detail | **Per-tool progressive lines** — each tool its own compact live line (spinner → one-line result summary), expandable to raw |
| Feature phases | **Defer** — edit/resubmit, @mentions, attachments are later phases; this plan is interaction-focused + graph auto-zoom |

## Diagnosis (grounded in current code)

- The transcript (`ChatThread` → `AgentTrace`) collapses a whole turn's tool
  calls into one line ("Proposed plan" / "N tool calls"), raw-JSON detail on
  expand. Not progressive; you can't see what's happening without expanding.
- The actual live plan + tool timeline lives in the **right rail**
  (`TaskContextPanel` → `UnifiedTimeline`): newest-first backbone + tool stubs.
  So "what is the agent doing" is in a **second place** — a cognitive split.
- `UnifiedTimeline` is imported **only** by `TaskContextPanel` (grep confirms),
  so removing the Activity pane is self-contained.
- The `activityLog` **store** stays required: it feeds inline labels
  (`toolActivityLabel`), live `run` output (`toolsWithLive`), and the mascot
  (`mascot/stimuli.ts`). Only the timeline *view* is removed.
- Tool events already arrive separately (`tool_use` then `tool_result` in
  `chat.ts`), and the activity log already carries humanized labels + per-tool
  `stepSeq` attribution. So progressive per-tool lines + step grouping are a
  **presentation** change, not a data/model change.
- `run` results are free-form text (e.g. `"run on lxc:caddy: ERROR exit status
  1"`) → one-line result summaries are best-effort text parsing, no backend.

## Design

### D1 — One progressive inline stream per turn
Replace `AgentTrace` (one collapsed blob per turn) with a new
**`TurnTrace.svelte`** rendered inline for each assistant turn, top-to-bottom:
1. **Live plan checklist** (only on the most-recent/running turn — see D3).
2. **Tool lines grouped by plan step** (D2), then orphan tools (no step).
3. **Streamed text answer** (existing `markdown-body prose-chat`), with the
   blinking cursor while streaming (existing).
4. A compact **"Thinking" line** while `working` and before any output: reuses
   the existing `indicatorLabel` (running step → tool → "Agent is thinking…").
   Fades once text/tools arrive; reappears between steps.

### D2 — Per-tool progressive lines (the Claude-Code signature)
One `ToolLine.svelte` per tool call (replaces `ToolCallCard`'s row style):
- Left: state icon — spinner while `tool_use`-only, ✓ on result, ✗ on error.
- Label: existing `toolActivityLabel(tool)` (humanized action).
- **One-line result summary** on completion — new `toolResultSummary(tool)`
  in `activity.ts` (see plumbing). E.g.:
  - `run` → `exit 0 · <first line>` (parse "exit status N" / "ERROR")
  - `get_entity` → `host:hubris (healthy)`; `get_health_summary` → `healthy X · degraded Y · down Z`
  - `list_entities`/`list_lxcs` → `N entities`; `get_relations` → `N relations`
  - `search_knowledge` → `N results`; `upsert_knowledge` → `recorded document:…`
  - `update_plan_step` → `step <seq> → <status>`; `propose_plan` → `N steps`
  - default → first non-empty line of stringified result (≤80ch); `done` if empty
- Live `run` output: while streaming, the line auto-expands a pinned-tail mini
  pane (reuse the `liveOutput` path from `toolsWithLive`).
- Click → expand raw args/result (border-driven `<pre>`, cyberspace-square).
- Border-driven, no rounded/shadow (per `border_driven_language`).

### D3 — Live plan checklist (TodoWrite-style)
On the **running/last** turn, render the current-generation `planSteps`
(already generation-aware via `workspace.ts`) as a checklist: pending = hollow,
running = spinner + highlight, done = ✓, failed = ✗, blocked = pause. Steps
check off live as `plan.step.*` events land. This is the unified timeline's
plan view, moved inline and scoped to the active turn. Past turns render only
their tool lines + text (the plan is session-level; the running turn carries
its current state, mirroring how TodoWrite re-displays state each turn). On a
terminal task state (`done`/`failed`), the checklist collapses to one line:
`Plan complete — N steps` / `Plan failed — step K`.

### D4 — Rail → graph only
`TaskContextPanel`: remove the Activity pane and the `UnifiedTimeline` import;
the panel becomes the Scope graph full-height (keep the collapsible "Scope"
header + the `nowTouching` strip). The graph is now the rail's entire job, so
auto-fit (D6) matters more. `activityLog*` stores remain imported only where
the inline stream/mascot need them.

### D5 — Cyberspace cohesion of the stream
Apply alongside the rewrite so the new inline view is on-system from day one:
- Transcript → **terminal log rows** (square, full-width, `YOU`/`NOMOS`
  role-tags, hairline `divide-y` separators; no bubbles, no soft shadow).
  Delete `.user-msg { box-shadow }`.
- Tool lines + expanded `<pre>`: border-driven, square, opaque.
- Composer: opaque `bg-background`, square (remove `rounded-2xl`/`bg-card/50`).
- Rewrite the stale "Art Nouveau" `<style>` comments → "cyberspace/terminal".
- Per `central_css_override`: drive surface styling centrally in `app.css`
  where it's a primitive concern; no ad-hoc `rounded-*`/`shadow-*`/`backdrop-blur`.

### D6 — Graph auto-fit + drag-pan (`SessionGraph.svelte`) (carried over)
- Wrap nodes+links in `<g transform="translate(tx,ty) scale(s)">`; fit the bbox
  of all nodes (radius + label + padding) into `cw`/`ch`; cap `s ∈ [0.2, 2.5]`.
- Re-fit on: mount, node-set change, container resize, sim-settle
  (`alpha > 0.05`), background double-click. **Not** every tick (fights pan).
  A `userPanned` flag pauses auto-follow after a manual pan until next
  membership/resize/double-click.
- Background drag = pan (`tx`/`ty`); node drag converts screen→graph via the
  inverse transform before setting `fx`/`fy`. Dot-grid stays in screen space.
- Keep: open-on-click, `touched` pulse, health-diff label, selection ring.
  Respect `scrollIntoView` pitfall (transform, not scroll).

## Phased task list (each independently shippable; all frontend)

- **P1 — Inline progressive stream.** `TurnTrace.svelte` + `ToolLine.svelte`;
  wire into `ChatThread` per turn; "Thinking" line; tool→step grouping via
  activity-log `stepSeq` matched by tool id; keep `toolsWithLive` for `run`.
- **P2 — Live plan checklist.** Inline current-gen `planSteps` on the running
  turn; collapse-to-summary at terminal state.
- **P3 — Rail → graph only.** Strip Activity pane + `UnifiedTimeline` from
  `TaskContextPanel`; verify no other importers (grep: only TaskContextPanel).
- **P4 — Cyberspace cohesion.** Terminal log rows; remove rounded/shadow/
  translucency; square composer; centralize in `app.css`; fix stale comments.
- **P5 — Graph auto-fit + drag-pan.** D6.
- **Polish (small, frontend-only):** per-message/tool **copy**; **scroll-to-
  bottom** button (uses `container.scrollTo`, never `scrollIntoView`).

## Plumbing specifics (grounded, no backend)

- New `toolResultSummary(t: ToolCallResult): string` in `activity.ts`, beside
  `toolActivityLabel`. Per-name switch (D2 list), graceful fallback.
- Tool→step grouping: build `id → stepSeq` from the activity log once per turn;
  tools with no step render as orphans.
- Reuse: `planSteps` (generation-aware), `indicatorLabel`, `toolsWithLive`,
  `toolActivityLabel`, `liveOutput` streaming path.

## Constraints honored (saved decisions)

- `design_system.central_css_override`, `border_driven_language`: square,
  hairline, opaque, focus-by-color, no soft shadows/glows.
- `chat_thread.pane_layout`: dynamic status (Thinking line, live checklist)
  lives in the **message Pane**, never the input Pane.
- `wmkit.scrollintoview_reflow_pitfall`: `container.scrollTo` for scroll-to-
  bottom; transform (not scroll) for graph pan.

## Risks

- **Removing the rail timeline loses the "overview" view.** Mitigation: the
  inline checklist + per-turn tool lines carry the same info progressively; the
  graph still shows fleet scope. If operators miss the overview, a collapsed
  "full timeline" can return as a toggle (follow-up).
- **Auto-fit vs manual pan** — handled by `userPanned` + settle-alpha gate.
- **Inline stream length on long turns** (15–27 min, many tools) — progressive
  lines can get long; mitigate by auto-collapsing finished steps (keep the
  running step + its tools expanded, prior steps as one-line summaries).
- **Best-effort result summaries** may misformat unusual payloads — fallback is
  always a truncated raw line + expandable raw detail, never a blank.

## Validation

- `npm run lint`, `tsc --noEmit` (no NEW errors beyond the known baseline in
  `ui/*`, `oidc.ts`, `windows.ts`, `workspace.ts`), `vite build`, `vitest`
  (add a `toolResultSummary` unit test per tool name + fallback).
- Manual matrix: (a) start a long task → Thinking line → plan checklist
  appears and checks off live → each tool streams as its own line with a
  one-line summary → text streams; (b) reload mid-turn → working still shows;
  (c) `run` tool → live output pins to tail then collapses to summary; (d)
  graph auto-fits at settle + on new entity + drag-pan + double-click reset;
  (e) no rounded/soft-shadow remains on chat surfaces; (f) rail shows graph only.

## Deferred (later phases, after this lands + validates)

- **Edit-and-resubmit** — `truncateFrom` store method + `POST /sessions/{id}/edit`
  (extract `streamTurn` from `handleChat`); reuse `reopenSession` (already
  exists, store.go:838 — marks prior `session_plan_steps` `replaced`, clears
  outcome) for the reset. Reject edit while the gate is busy (HTTP 409); edit
  cannot queue (truncation must be atomic). Regenerate = no-op-edit case.
- **@entity mentions** — small `GET /api/v1/entities/search?q=` + composer
  autocomplete inserting `type:name` slugs the agent/graph already parse.
- **Attachments** — multipart upload + `agent_attachments` table + configured
  `OIKOS_ATTACHMENTS_DIR` (explicit volume, not relative) + capped text inlining.
- **Continue button** — needs `/resume` to `reopenSession` first for terminal
  sessions (today `/resume` does not reopen `done`/`failed`; `handleChat`'s
  follow-up path does). Small backend tweak.
- **Image vision** pending provider confirmation.

## Out of scope / follow-ups

- A collapsible "full timeline" overview toggle if the rail removal is missed.
- `read_attachment` MCP tool (lazy full-content fetch, lower context than inlining).
- Oldest-first timeline toggle / per-tool `tool.*` events for background turns.
