# UI fixes & updates plan

## Context

`ui/` is the in-tree single-page web client (vanilla JS + HTML + CSS) that consumes the same REST/SSE/WS API documented under `docs/api/`. It's used **primarily as a debugging surface** for the runtime — not a polished end-user product. The plan below is organized around that reality.

## Guiding principles

1. **No truncations.** This UI exists to inspect what the runtime actually does. Every site that currently truncates a payload (`truncate()`, `slice(0, N)`, `outputPreview`-only, etc.) needs to be replaced by either (a) a full-content render, or (b) a "show more / show full" toggle that loads the full text on demand. No silent clipping.
2. **Surface every event.** If the runtime emits an SSE/WS event, the feed and the relevant view should display it. Unknown event types fall back to a generic JSON dump rather than being dropped.
3. **Mirror server state precisely.** Field names and shapes must match the server. Where the audit found a name mismatch (e.g. daemon `cron`), the fix is "match the server" — never alias on the UI side.
4. **Add capability before polish.** Each gap below is either `[BUG]` (currently broken / wrong field), `[MISSING]` (capability not exposed), `[NO-TRUNC]` (a clipping site), or `[POLISH]` (UX nicety). Priority order: BUG > NO-TRUNC > MISSING > POLISH.
5. **No hidden state.** Anything the server tracks (budgets, compaction counters, trace events, sender-wrapping prefix, mid-run system-reminder) should be visible somewhere in the UI.

---

## Bugs (must fix)

### B1 — `ui/views/agents.js:169` reads wrong daemon field

The agent detail panel does:
```js
if (modeConf.schedule) details.push(`cron: ${modeConf.schedule}`);
```

But the actual JSON field on `agent.json` is `cron`, not `schedule` (see `infrastructure/scheduler.js:26`: `agent.modes?.daemon?.cron`). Result: cron expressions never display in agent detail.

**Fix.** Change to `if (modeConf.cron) details.push(\`cron: ${modeConf.cron}\`);`. Confirm `daemons.js:63` (which displays `d?.schedule` from the daemon record returned by `GET /daemons`) — that one is reading from the scheduler's normalized response object, which IS `cron` per `infrastructure/scheduler.js:113`. Spot-check `daemons.js:63` and align it to `cron` if it's also wrong.

---

## De-truncation pass (cross-cutting)

The current UI clips content at multiple sites. For a debug surface this is exactly wrong — the cap usually hides the bytes you care about. Each site below needs to be either fully expanded by default or given an explicit "show full" toggle.

### N1 — Tool result rendering

- **`ui/views/tasks.js:301`** renders `outputPreview` (server-side preview, capped to ~500 chars on the trace event). The full tool result is persisted in the `messages` table as `role: 'tool'` rows. The UI currently shows ONLY the preview.
  - **Fix.** Below the `outputPreview` line, add a "show full result" expander that fetches the corresponding `tool` message via `GET /sessions/:id/messages` (filter by `tool_call_id`) and renders the entire `content` string verbatim in a `<pre>` block. No client-side truncation.
- **`ui/views/sessions.js:281`** — `content.slice(0, 200) + '…'` for message preview rows.
  - **Fix.** Render full content by default (debug UI; rows can scroll). If the full render proves too long for the list view, replace the truncation with a `<details>` element where the summary is the first 200 chars and the body is the full content. Either way, no information is lost.

### N2 — Thinking / reasoning visibility

Thinking is currently **invisible** in the UI: no SSE handler for `thinking.chunk`, no rendering of `message.thinking_content` on persisted assistant messages.

- **Fix in `ui/views/chat.js`.** Add a `thinking.chunk` SSE branch that appends content to a "thinking" lane attached to the in-flight assistant turn. Live-render it as it streams. When `done` fires, also render the final accumulated `thinking_content` from the `message` event so refreshed views see the same text.
- **Fix in `ui/views/sessions.js` message renderer.** For each assistant row, if `m.thinking_content` is non-null, render it as a collapsible `<details>` block above the visible content. Show the full text — never clip.
- **Fix in `ui/api.js`** SSE event-name list to include `thinking.chunk`.

### N3 — Feed event truncation

- **`ui/views/feed.js:168`** truncates event content to 80 chars via `U.truncate`.
  - **Fix.** Change every feed row to render the full event payload. If line length is a problem visually, let the row wrap or use `<details>` (one-click expand). The feed is a debug log, not a chat preview — caps actively hurt.

### N4 — Generic `truncate()` helper at `ui/app.js:64`

This is the helper used by `tasks.js:109`, `feed.js:168`, and others.

- **Fix.** Audit all callers. For each caller that's part of a debug-surfacing view, replace the `truncate(s, n)` call with the raw `U.esc(s)` (or wrap in `<details>` for very long strings). Leave `truncate()` available only for views where space is genuinely constrained (the connection indicator, the session-id pill that already has a tooltip showing the full id) — and in those cases, ensure the full value is reachable via tooltip / clipboard / detail view.

### N5 — Tool stdout/stderr live streaming

`tool.chunk` SSE events stream live tool output (e.g. bash stdout/stderr) before `tool.end` fires. The UI ignores them.

- **Fix.** In `ui/views/chat.js` SSE switch, add a `tool.chunk` branch. Maintain a per-`toolCallId` accumulator and append each chunk verbatim to a "tool stdout" lane attached to the in-flight tool call. On `tool.end`, leave the accumulated stdout visible (don't replace it with the truncated `outputPreview`).

---

## Missing API coverage in `ui/api.js`

### A1 — `PATCH /sessions/:id`

Currently `api.js` only has `getSession` / `listSessions` / `deleteSession`. The PATCH endpoint accepts `title`, `model`, `temperature`, `max_tokens`, `reasoning`, `model_thinking`, plus all `compact_*` fields.

- **Add.** `patchSession(sessionId, body)` returning the updated session row.

### A2 — `POST /sessions/:id/cancel`

- **Add.** `cancelSession(sessionId)`. Idempotent on the server; the UI helper just returns `{ sessionId, status: 'canceled' }`.

### A3 — `GET /sessions/:id/config`

- **Add.** `getSessionConfig(sessionId)` returning `{ effective, overrides, agentDefaults }`. Renders into a session-detail panel (see UI surface S3 below).

### A4 — `GET /sessions/streaming`

- **Add.** `listStreamingSessions()` returning `{ streams: [{ sessionId, agentName, startedAt }] }`. Used to power a "currently running" badge on the sessions list.

### A5 — `POST /sessions/:id/compact`

- **Add.** `compactSession(sessionId)` returning `{ sessionId, compactedCount, newSize, alreadyUpToDate, session }`. Used by the manual compaction button in the session-detail panel.

### A6 — Orchestration endpoints

- **Add three methods:**
  - `getOrchestrationGraph({ depth })` → `GET /orchestration/graph?depth=N`
  - `getTrace(traceId)` → `GET /orchestration/trace/:trace_id`
  - `streamTrace(traceId, onEvent)` → SSE consumer for `GET /orchestration/trace/:trace_id/stream` (handles the `trace.event` event-name)

### A7 — SSE event-name registry

`ui/api.js` SSE consumer (around lines 60–72) currently splits on `event:` and `data:` lines but the chat.js switch only handles a fixed set of event names. Centralize the known event names in one place, document them, and treat unknown names as a generic JSON dump (don't drop). Specifically the consumer must accept: `inference.chunk`, `inference.tool`, `thinking.chunk`, `tool.chunk`, `message`, `done`, `error`, `session`, `chat.tool`, `chat.response`, `chat.event`, `task.event`, `task.status`, `daemon.tick`, `trace.event`.

---

## Missing UI surfaces

### S1 — Cancel button (chat composer + sessions list)

Currently no way to abort an in-flight chat. Add:

- **Chat composer (`ui/views/chat.js`).** While an SSE stream is open, render a small "cancel" button next to the send button that calls `api.cancelSession(currentSessionId)`. Wire it to also abort the local SSE reader so the UI returns to idle promptly.
- **Sessions list (`ui/views/sessions.js`).** For sessions returned by `GET /sessions/streaming` (intersected with the current sessions list), show a "running… [cancel]" badge + button on the row.

### S2 — Cancel surface in `done` payload

`done.cancelled` and `done.cancelReason` are currently ignored.

- **Fix in `ui/views/chat.js`.** When `done.cancelled === true`, show a banner above the chat lane: "Cancelled — reason: {cancelReason}". When the SSE error event is `code: "CANCELLED"`, render the same banner instead of a generic error.

### S3 — Session config panel (`/sessions/:id/config`)

The session-detail view should show what the model will actually use on the next turn.

- **Add.** A "Config" tab on the session detail page that calls `api.getSessionConfig(sid)` and renders three columns: `effective` / `overrides` / `agentDefaults`, each as a key/value table for the 5 keys (`model`, `temperature`, `max_tokens`, `reasoning`, `thinking`). Highlight cells where `overrides` differs from `agentDefaults`.

### S4 — Per-session overrides editor

Right next to S3, add an inline editor that PATCHes the session.

- **Add.** A small form that POSTs `PATCH /sessions/:id` with any combination of `temperature` (number, blank=clear), `max_tokens` (integer, blank=clear), `reasoning` (dropdown: minimal/low/medium/high/blank), `model` (free text), `model_thinking` (JSON textarea). Server enforces validation; surface validation errors literally (don't truncate).

### S5 — Per-call `overrides` in chat composer

The chat composer should let you override LLM params for one turn without persisting.

- **Add.** A collapsible "advanced" panel under the chat composer with the same fields as S4 (`model`, `temperature`, `max_tokens`, `reasoning`, `thinking`). When non-empty, the values are sent under `overrides` in the chat POST body. After the turn the composer's overrides reset to empty (per-call only).

### S6 — Compaction view + manual compact button

Sessions track a rich compaction state that's currently invisible.

- **Fix in `ui/views/sessions.js` detail.** Add a "Compaction" section showing: `compact_summary` (full text — N1 applies), `compact_size`, `compact_auto_threshold`, `compact_count`, `compact_model`, `compact_custom_instructions`. Beside it, a "Run compaction now" button that calls `api.compactSession(sid)` and refreshes the section with the new state. Surface `alreadyUpToDate: true` as a clear "no-op" badge so the user knows the call didn't fail.
- **Fix in `ui/views/agents.js`.** In the agent-detail card, render the `defaultCompaction` block (`enabled`, `compactionCount`, `autoThreshold`, `model`, `customInstructions`) with the same no-truncation rule for the customInstructions textarea.

### S7 — 3-axis budget governor in settings

The new `settings.budget.{max_tokens, max_wall_seconds, max_spawn_depth}` block (default null = unlimited) is currently invisible in the settings editor.

- **Fix in `ui/views/settings.js`.** Add a guided form section labeled "Budget governor" with three numeric inputs (each accepts blank = unlimited). When the user saves, write into `settings.budget` rather than mutating top-level fields. Display the legacy `maxSubAgentDepth` as a read-only "legacy task-mode field" line so users don't think they're equivalent.

### S8 — Summarizer settings UI

- **Add.** Two more inputs in settings: `summarizerModel` (free text, blank = use config default) and `summarizerMaxCallsPerMinute` (integer, blank = unlimited). Mention in the description that a non-null cap surfaces `<rate-limited>` in the `agent_control` get-summary response.

### S9 — Attachment upload (with 8 MB cap)

Multimodal chat is supported server-side but not exposed in the UI.

- **Fix in `ui/views/chat.js`.** Add a paperclip-style attachment chooser to the composer. Each attached file:
  - For images / video / file: read as base64 in the browser, push into `attachments[]` as `{ type, mediaType, data }`. Verify size <= 8 MB before sending; if larger, surface a clear error referencing the cap (don't silently truncate).
  - For audio: same, but only `data` (base64) is allowed — no URL form per server doc.
- Render attached items as chips below the input box with a remove button.
- On `MODEL_MODALITY_UNSUPPORTED` error response, render the attachment chip(s) with a red "claude-cli engine doesn't accept attachments — switch agent or model" overlay.

### S10 — Multi-agent governance surfaces

`agent_spawn`, `agent_message`, `agent_control` calls are currently visible only as generic tool calls in the chat / task views.

- **Fix in `ui/views/feed.js`.** Recognize tool-call events for these three tools and render them with their semantic shape:
  - `agent_spawn`: show child instance_name + sessionId; if `async_inform: true`, label as "(async dispatch)".
  - `agent_message`: show target sessionId + sync vs async + msg#1 vs msg#2+ if known (not always derivable client-side; cite `targetSessionId` + `async_inform` body fields).
  - `agent_control`: show action (`get-state` / `get-summary` / `stop`).
- **Mid-run reminder.** When a chat message comes in whose content starts with `<system-reminder>This is a mid-run reply from the sub-agent`, render the reminder as a styled banner above the actual `[Message from <agent> (session: <sid>)]:` line so it's visually obvious this is an auto-delivery, not a final reply. Don't strip the reminder — show it verbatim.
- **Sender wrapping.** When a user-row's content matches `^\[Message from <agent> \(session: <sid>\)\]: `, render the bracketed prefix as a small label above the actual content, with the full prefix copyable.

### S11 — Orchestration trace viewer

A new top-level "Trace" route should render the orchestration tree.

- **Add a new view `ui/views/trace.js`.** Routes:
  - `#trace` — list active traces (graph from `GET /orchestration/graph`)
  - `#trace/:traceId` — full event tree from `GET /orchestration/trace/:traceId`. Render as a collapsible tree. Each node shows `tool_name`, `action`, `agent_name`, token counts, latency, full `envelope_summary` (no truncation).
  - Live tail mode: subscribes to `GET /orchestration/trace/:traceId/stream` and appends new `trace.event` rows in real time.
- **Wire into `ui/index.html` + `ui/app.js`** as a new top-level nav entry.

### S12 — `session.budget_exceeded` + `trace.event` in feed

`ui/views/feed.js` currently filters on a known list of event types. Add explicit handlers:

- `session.budget_exceeded` — render with the `budgetType`, `limit`, `actual` fields visible.
- `trace.event` — render the full trace row (no clipping); maybe with a filter toggle so the feed isn't dominated by trace noise.

### S13 — `memory.js` write-semantics warning

- **Fix.** Add a one-line note in the memory editor: "HTTP `PUT` overwrites the file as-is. The `memory_write` tool used by agents skips empty content (`skipped_empty`) and exact-duplicate appends (`skipped_duplicate`). Use it from inside an agent run if you need those guards." Link to `docs/api/07-memory.md`.

### S14 — Tool count

- **Fix.** Anywhere the UI mentions "24 tools" or similar (likely none — verify), update to 28. Add a check: render `KNOWN_BUILTIN_TOOLS.length` if such a constant exists, otherwise hardcode 28 with a code comment pointing at `tools/`.

---

## Implementation order

Group into batches that can ship independently. Each batch ends with a smoke test (manual or scripted).

### Batch 1 — Bugs + de-truncation (no new endpoints)

- B1: cron field name fix in `agents.js:169`
- N1: full tool result rendering (replace `outputPreview`-only)
- N2: `thinking_content` rendering on persisted assistant rows
- N3: feed full content
- N4: audit `truncate()` callers
- N5: `tool.chunk` SSE handler + accumulator

**Why first:** every one of these is either a true bug or a debug-affordance regression. Zero new endpoints needed; works against today's server.

### Batch 2 — Cancel UX + chat-composer overrides

- A1, A2, A3: `patchSession`, `cancelSession`, `getSessionConfig` in api.js
- A7: SSE event-name registry centralized
- S1: cancel button (chat composer + sessions list)
- S2: cancel banner in done payload
- S3: session config panel
- S4: PATCH session form
- S5: per-call overrides in chat composer

**Why second:** unblocks all session-level debugging. Builds on Batch 1's cleaner SSE pipeline.

### Batch 3 — Compaction + budget + summarizer settings

- A5: `compactSession` in api.js
- S6: compaction view + manual button
- S7: budget governor settings UI
- S8: summarizer settings UI

**Why third:** smaller surface, depends on Batch 2's session-detail layout.

### Batch 4 — Multi-agent observability

- S10: governance surfaces (spawn / message / control + mid-run reminder + sender wrapping)
- S12: feed handlers for `session.budget_exceeded` + `trace.event`

### Batch 5 — Orchestration trace viewer + attachments

- A4, A6: streaming sessions + orchestration endpoints in api.js
- S9: attachment upload with 8 MB cap
- S11: orchestration trace view (new top-level route)

### Batch 6 — Polish

- S13: memory write-semantics note
- S14: tool count fix

---

## Files touched (summary)

| File | Batches that touch it |
|---|---|
| `ui/api.js` | 2, 3, 5 (new methods + event-name registry) |
| `ui/app.js` | 1 (audit `truncate()` callers), 5 (new route) |
| `ui/index.html` | 5 (new nav entry) |
| `ui/views/chat.js` | 1 (N2, N5), 2 (S1, S2, S5), 5 (S9), 4 (mid-run reminder rendering) |
| `ui/views/sessions.js` | 1 (N1, N2), 2 (S3, S4), 3 (S6) |
| `ui/views/agents.js` | 1 (B1), 3 (S6 defaultCompaction) |
| `ui/views/tasks.js` | 1 (N1, N4) |
| `ui/views/memory.js` | 6 (S13) |
| `ui/views/daemons.js` | 1 (verify `cron` field name in display) |
| `ui/views/feed.js` | 1 (N3), 4 (S10), 4 (S12) |
| `ui/views/settings.js` | 3 (S7, S8) |
| `ui/views/trace.js` | 5 (new file) |
| `ui/views/connection.js` | none |
| `ui/views/models.js` | none |

---

## Verification

For each batch, manual smoke tests against the running server:

- **Batch 1.** Open chat with `assistant`, ask it to bash a long command; confirm tool stdout streams live AND the full result is rendered (no `outputPreview` cap). Open a chat with cc-sonnet + thinking enabled; confirm `thinking.chunk` events render live and `thinking_content` is preserved on the persisted message.
- **Batch 2.** Start a long-running chat, click the cancel button mid-flight; confirm the chat ends with a "Cancelled" banner showing `cancelReason`. PATCH a session via the form; confirm `GET /sessions/:id/config` reflects the change. Submit a chat with per-call `overrides`; confirm session row stays unchanged after.
- **Batch 3.** Click "Run compaction now" on a session with > 10 messages; confirm `compactedCount` updates and the summary appears. Edit `settings.budget.max_spawn_depth` to 1 and confirm a deep agent_spawn returns `BUDGET_EXCEEDED` (visible somewhere — task.error or chat error).
- **Batch 4.** Run an `agent_spawn` + `agent_message` chain; confirm the feed shows semantic events and the chat lane shows the system-reminder banner on msg#2+ replies.
- **Batch 5.** Drag-drop an image > 8 MB into the composer; confirm a clear cap error. Start a multi-agent run; confirm the orchestration trace view shows the full tree.
- **Batch 6.** Memory editor shows the write-semantics note.

No automated UI tests in scope — VeilCLI doesn't ship a UI test harness today. Recommend revisiting that as a separate effort.
