Web UI Design
Contents
- Purpose
- Primary Surfaces
- UI Principle
- Approval UX
- Contextual Loading and Empty States
- Real-Time Updates
- System One (Jev) Section
Purpose
The web UI is AI-first Home: the visual conversation workspace for the local harness. It should keep the user in the current thread, make the searchable session rail and central conversation pane the default surfaces, and surface details, trace, and capabilities without forcing a mode switch.
Implementation status.
src/ui/src/components/ConversationHome.vueimplements the AI-first Home shell: searchable session rail, central conversation pane, details inspector, and the composer-driven conversation flow.HomeSessionRail.vue,HomeTimeline.vue,HomeComposer.vue,HomeCapabilityDrawer.vue, andHomeTraceDrawer.vueprovide the rail, timeline, composer, capability, and trace surfaces. Repository intake is not a Home mode:vf initasks its questionnaire when stdin is a TTY, and--no-askskips it. On a LAN bind, legacy mutations require the authorized LAN page session plus CSRF token. Conversation Home uses a separate conversation session that is issued only on loopback, so its JSON, artifact, and stream-token routes fail closed on LAN even after page bootstrap (seeSECURITY_MODEL.md). The control center is the single workspace configuration surface. OpenControl centerfrom the Home top bar to detect a repository, initialize the harness with or without AI, initialize an agent, enable or disable CLI engines, toggle Memory/CodeGraph/LSP, inspect capabilities and skills, and view configured MCP server names. Harness detection reads the existing/api/detectcontract; settings persist through/api/settings; initialization uses/api/init. The existing Capabilities drawer remains the reviewed install/repair surface; the control center links users to it instead of duplicating mutation actions. Unchecked engines are not dispatched by control-center init, but their binaries remain installed. Empty MCP inventory is an honest state, not a simulated server list.
Primary surfaces
1. AI-first Home
The default surface is the searchable session rail plus the central conversation pane.
It keeps the current conversation visible, shows participant state and live stream
status, and makes new conversation creation obvious. Search filters sessions in place;
selecting a result opens it directly without a resume dialog. The rail collapses to
zero width via its collapse toggle (.home-rail__collapse), hiding it with
aria-hidden=true so keyboard focus skips it; the toggle restores it.
Engine picker
The composer’s engine chip (above the message queue) opens a menu with Auto plus one row per installed-or-known CLI: claude, copilot, codex, opencode, antigravity.
- Auto probes each CLI’s readiness and picks a live engine for the conversation
(the engine chip shows the resolved pick, e.g.
Engine: Copilot). - A concrete pick applies to new conversations (the chip reads
localStorage vf-engine; values:auto | claude | copilot | codex | opencode | antigravity). The currently open conversation keeps its bound engine until it ends. - A CLI whose binary is missing shows a disabled row with the install hint
(
agy CLI not found — install the Antigravity CLI …) and is skipped by Auto. - The menu re-checks engine binaries when opened; the row subtitle shows
Readyor the probe detail. - Model selection is not exposed in Home: the engine runs its default/configured model (per-role model ranks apply to coordinator roles, not the Home engine picker).
Opening a conversation (restore)
Selecting a session in the rail verifies its active head, public trace, and durable
queue before the transcript unlocks. The restore placeholder (“Verifying the active
head …”) shows while that read completes; a completed/failed conversation has no live
engine stream, so the transcript renders from the verified timeline alone. Once the
timeline is loaded the composer unlocks (terminal conversations can still be extended
with a follow-up message). A conversation that failed mid-turn shows the terminal
failure in the thread — [<engine> failed: <reason>] — instead of a silent empty
“Complete”; the thread keeps the failure notice and state labels (*Failed*,
*Complete*) in both the rail and the transcript.
New conversation button
+ New conversation in the rail always returns to the composer on the home surface;
typing the first message creates the conversation with the picked engine. The composer
toolbar (agent, remove participant, attach, mention, private range, capabilities,
engine chip, send) is available before a session exists — the creation flow needs
+ Agent, private range, and the engine picker before the first send.
2. Composer
The composer is conversation-first, not form-first. It owns the durable FIFO queue, ArrowUp editing for the latest queued human message, private file range capture, typed capability selection, and file attachments. Typing +/-/@ (no space) opens a suggestion listbox (agents, participants, commands). Before any conversation exists there are no participants, so a lone @ or - shows a dashed hint — “Chưa có tác nhân để nhắc — thêm bằng nút +Agent trước” — instead of a dead-empty listbox; the hint disappears as soon as a participant exists, and Escape closes suggestions without touching the draft. Sends made while an agent is busy queue automatically. Real-user QA of the composer must exercise token suggestions, attachment upload/remove, private-range source selection, drawer focus/Escape, 320-390px widths, and 200% zoom — asserting a stable composer height, zero horizontal overflow, no console leakage, and a clean aXe pass. ArrowUp starts an edit only for the latest queued human message; Escape cancels, and a lost dispatch/edit race preserves the draft for explicit send-as-new. Typed add-participant actions promote a direct route into coordinate through proposal/review/commit, and removing the last executor collapses it back to direct. Picking a chip from the toolbar or suggestion list always appends a trailing space, so typing after a mention keeps the token separate (the chip never merges with following text); the toolbar menus render through a Teleport so the composer wrap’s overflow cannot clip them. Suggestion listboxes also render through a Teleport to body as fixed viewport overlays positioned from live composer bounds; this avoids the scrollable timeline stacking context intercepting pointer events. Opening or picking an agent does not add listbox height to form or push conversation upward. Real-user QA must hit-test first option with elementFromPoint and perform actual click, not only assert visibility. Mention chips render as bordered, tinted pills colored per kind (--agent amber, participant/mention variants) and display the agent label rather than the raw token — no special characters in the visible chip. The draft itself keeps the raw token (e.g. +web_ui@codex), so the highlight overlay shows labels while the textarea holds the token; while a token is being edited (backspace/typing), the chip keeps the full label as long as the token is still a prefix of a known agent/participant key, so it never flickers into raw text. The private-range panel offers a git-diff-style preview. By default, attach a text file first, then choose it in the panel’s Private range file selector; preview reads .vibeflow/attachments/<name> through guarded GET /api/file?preview=1, and selecting lines stages only requested line window. Images/documents stay excluded because they have no text lines. A visible Path fallback remains for repo files not uploaded. Missing previews answer {ok:false, reason:"not found"} with HTTP 200, so a mistyped path does not spam the browser console. Numbered lines show around selection; clicking line picks start then end (auto-swapping inverted pair). The Attach button sits in toolbar and is hidden without engine support: claude accepts text (--append-system-prompt-file), copilot image+document (--attachment), codex image (--image), opencode any (--file), antigravity none. Auto file dialog accepts union of all engine kinds, then per-file gate picks first capable ready engine or support-matrix fallback while probe runs. Picked files upload through POST /api/upload, render removable chips, and delete through DELETE /api/upload; argv uses .vibeflow/attachments/<name> before prompt (copilot flags before -p).
Attachment support matrix
Uploads are stored as .vibeflow/attachments/<name>. The UI classifies allowed
extensions into text, image, and document kinds, then projects the repo-relative
path into engine argv. Auto mode accepts the union of supported kinds, chooses a
ready capable engine for one file, and keeps one engine capable of every selected
kind for mixed files. Mixed image + text uses OpenCode when available. Readiness
gates dispatch, not picker visibility. Private ranges require text: select an
uploaded text file from Private range file, preview numbered lines, select the
range, and stage VF-PRIVATE-FILE-RANGES/1; Path remains explicit fallback for
repo files not uploaded. Image/document attachments cannot provide line ranges.
3. Details inspector
The details inspector shows the active conversation’s participants, continuity, lineage, and health so the user can verify who is involved before adding more context or changing route authority. Participants can be mentioned or removed from this surface. The visible − action prepares -@participant in the composer so removal remains a chat event instead of hidden settings.
4. Trace / capabilities drawers
The trace drawer shows ordered public trace and evidence. The capabilities drawer shows typed capability actions and their current state without leaving the thread.
5. Message interactions
Users can quote one through eight currently visible messages from one or more sources. Ordered quote chips can move earlier/later, be removed, and jump back to their source. Messages accept only 👍, 👎, ❤️, 🎉, 👀, 🤔, ✅, and ❗ reactions. Reactions are typed records rather than prompt syntax; an agent may add at most three distinct non-self reactions so the affordance does not become noise.
6. Repository intake
Run vf init in a TTY for first-run repository intake; it asks the goal, engine, sources,
and Definition of Done before generating canonical context. vf init --no-ask is the
non-interactive path. vf ui always stays on AI-first Home.
7. Secondary workflow/review surfaces
The work-unit board, orchestration dashboard, generated instructions, review surfaces, and skill-evolution panels remain available, but they are secondary to the home-first conversation flow.
UI principle
The UI should reduce user burden. It should not ask “What should I do next?” or send the user away from the current conversation unless it is necessary.
It should show:
Recommended next action
Reason
Evidence
Risk
Safety control
Approval button if required
Approval UX
Approval, cancellation, install, repair, and lifecycle proposals render as typed action cards
in the central timeline. A card shows the target, reason, bounded evidence, risk, and exact
operation id. The browser only resolves a guarded decision endpoint; it never installs or
mutates capability state directly. Approve remains disabled for HIGH/CRITICAL findings,
Reject keeps an explicit gap, errors are assertive, and completion returns focus to the
composer. A 409 triggers a state refresh because another operation already won.
Skill-acquisition cards additionally name the pinned registry commit and bounded
security scan result; rejection or a blocked install leaves an explicit skill gap.
Approval prompts should support:
Approve once
Approve for this task
Approve for this repo policy
Reject
Edit policy
Contextual Loading and Empty States
Loading copy names the operation that is actually pending: reconnecting a stream, loading a session, waiting for an agent, applying a queued edit, resolving an action, searching capabilities, or fetching trace. The active conversation stays visible while a scoped region loads. New users see a simple session rail plus composer prompt; empty drawers explain what will appear there and do not block chat. Reduced-motion preference disables decorative motion without removing status text or progress semantics.
10. Diff Preview (#641)
The Diff Preview shows code changes at the workflow or work-unit level, synchronized with the selected pipeline node.
Workflow-level summary: changed-file count, additions/deletions totals,
and baseline label (dispatch checkpoint when available, otherwise HEAD).
Binary files are flagged; untracked files are reported separately from
git status --porcelain.
Work-unit preview: scope-limited unified diff filtered to the selected
unit’s declared paths. Capped at 200 KB / 2,000 lines with truncated: true
and a local-command hint on overflow. No-diff, unsupported, binary, and
truncated states are clearly labeled.
API contract (GET /api/dashboard/diff):
- Validates repo is a registry member,
workflowIdmatchestask_id, unit exists. - Uses
git diff --no-ext-diff --binary <baseline> -- <validated scope>— never shell-interpolates input. - Rendition via
{{ }}interpolation, neverv-html. - Baseline defaults to the pre-dispatch checkpoint’s base ref; falls back
to
HEAD.
Integration points:
- Workflow Dashboard (secondary view): diff panel above the pipeline graph. Selecting a pipeline node filters to that unit’s scope.
- Verify screen (stage 4): full workflow diff summary above the task table.
Pipeline dashboard (ADR-006)
The workflow dashboard is a secondary view surfaced from the home shell; the default stage 0 surface remains AI-first Home. The dashboard still lists every registered workflow. Each card displays repo, task ID, goal, done/total, running/blocked count, and latest activity. Selecting a card reveals:
-
A dependency pipeline (CSS Grid + SVG) with one column per wave. Nodes are keyboard-focusable
<button>elements with status-based coloring: pending (neutral), running (animated blue), verifying (animated amber), done (green), blocked (red). An ordered text list provides screen-reader access. No external graph library is used. -
A scoped log drawer showing only events for that workflow. When a unit is selected, filters narrow to that unit while retaining workflow-level lifecycle events. The existing active-session log pane (
/api/logs/stream) is unchanged.
Dashboard polling interval: 2 s while any workflow is running, 15 s otherwise. One selected workflow gets a durable-log SSE stream.
Layout
Desktop: pipeline graph on the left, log drawer on the right (lg breakpoint). Mobile: stacked vertically. The “Recent projects” section (Resume/Reuse/Delete) remains but is secondary to the active workflow cards.
11. Interactive Plan Review (PR1)
The Plan Review panel (Stage 2 of the legacy repository workflow surface, not AI-first Home) is a file-backed plan-markdown review surface with three components:
PlanReview.vue — parent container. Loads revisions via store.loadRevisions()
when repoPath resolves (watches store.repoPath). Renders a split layout:
revision rail on the left, canvas on the right.
PlanRevisionRail.vue — left sidebar listing all stored revisions by creator name and timestamp. Click to select; the initial state shows a textarea for creating the first draft. When an anchor is active, displays the anchor blockId + quote preview with the note “Comment storage not implemented” (PR2).
PlanCanvas.vue — right content area rendering typed blocks via {{ }}
interpolation (never v-html). Each block type renders distinctly:
- heading → styled by level (h1-h3 mapped to size classes)
- paragraph →
<p>with relaxed leading - list-run →
<ul><li>with disc markers - fenced-code →
<pre><code>with monospace - fenced-mermaid → fallback label +
<pre><code>source (no mermaid runtime)
Each block has a hover-reveal “Comment” button and mouseup selection handler — both
emit a BlockAnchor (blockId, quote, selection range) as groundwork for threaded
comments (PR2).
API surface (docs/adr/ADR-007-interactive-plan-review.md):
GET /api/plan-review?repoPath=&workflowId=— fetch current revision + blocksPOST /api/plan-review/revisions— create new revision from markdown (CSRF-guarded)
Deferred to PR2: threaded comment storage, dispatch gate, revision diff. Deferred to PR3: AI replan from review feedback.
See src/ui/src/components/PlanReview.vue, PlanCanvas.vue, PlanRevisionRail.vue,
src/ui/src/lib/plan-render.ts, src/ui/src/lib/plan-anchor.ts.
Real-time updates
Use Server-Sent Events for:
- command logs
- agent status
- queued send and edit reconciliation
- participant add/remove proposal/review/commit and collapse events
- typed quote and reaction changes
- inline approval/capability operation state
- contextual reconnect/loading state
- hook decisions
- skill usage
- diff updates
- verification progress
- (dashboard) selected workflow durable log tail
System One (Jev) section
The Home Control Center drawer carries one System One (Jev) section, labelled Optional decision judge, for the optional TypeSafe judge. It is always rendered (it is the on-demand disclosure surface) and it renders these states in priority order, so the most consequential one wins:
- loading -
Loading System One settings...,role="status",aria-busy="true"; - view request failed -
System One connection failed - <error>,role="alert"; - unconfigured -
No System One key configured - key missing: set the environment variable or run vf config typesafe key; - breaker open -
Circuit open - judge calls are paused until <cooldownUntil>,role="alert"; - otherwise - the read-only list below.
The read-only list is enabled, configured, state (carrying a data-state
attribute), key source, model, timeout, and last call (caller, status, ms) when a
call has been recorded. The controls are the enable toggle, the two confidence thresholds
(Run judge at confidence, Accept verdict at confidence, both 0..1 step 0.05 with
an inline threshold error), a toggle per call site with a one-line description of what
that site does, Test connection in the section heading, and a save button. The breaker
tuning numbers (failStreakLimit, cooldownBaseMs, cooldownCapMs, hookTimeoutMs,
hookBusLockRetries) are settings-only: they are visible in vf config typesafe status
and in the DOM-less settings view, not as UI fields.
settingsView returns a redacted typesafe object: enabled, configured, state,
keySource, model, timeoutMs, and lastCall. The API key is never part of the
response, so it can never reach the DOM; the section states that the key stays on the
machine. The one-line note at the top of the section carries the authority rule, in
product language: the judge can only reject a change sooner or raise a risk tier; it never
opens a gate or skips a review, and it can only suggest an engine from the pool preflight
already admitted.
Related: Architecture · Workflow Edit this page on GitHub