Vancetope Web-UI — Specification

Status: v1 Initial Draft. This spec is binding for the development of the Web-UI under repos/vance/client/ (symlink client_web/).

1. Goals and Scope

Vancetope will receive a browser-based UI as a supplement to the existing CLI clients (vance-cli v1, vance-foot v2). The Web-UI is a separate client that does not aim for feature parity with the CLIs. It uses the existing backend (vance-brain) via REST and WebSocket.

What the Web-UI is:

  • A multi-page app set of independent editor/viewer pages (one HTML entry per entity type).
  • Focus on visual overviews (Inbox, Process-Tree, Document list) and on Chat as a live experience.
  • Gateway for non-technical co-users in a Tenant.

What the Web-UI explicitly is not (v1):

  • Not a real-time status mirror of the entire backend world — live updates are exclusively in Chat. Other editors show the state at the time of page load and are manually refreshed.
  • No cross-tab state sharing. Each browser tab is isolated.
  • No offline capabilities, no service worker, no PWA manifest.
  • No proprietary auth system. Exclusively JWT from the /brain/{tenant}/access/{username} endpoint.

The deliberate asymmetry to the CLI: vance-foot has live updates for everything via WebSocket. The Web-UI only has this for Chat. This is v1 scope and may be extended in subsequent versions.

2. Architecture

2.1 Repository Layout

repos/vance/client/                          (Workbench symlink: vance-wb/client_web)
├── pnpm-workspace.yaml
├── package.json                             (Root, devDeps + Workspace Scripts)
├── tsconfig.base.json                       (common compiler options)
├── tsconfig.json                            (Project References)
└── packages/
    ├── generated/        @vance/generated
    ├── shared/           @vance/shared
    └── vance-face/       @vance/vance-face

Workspace Tool: pnpm. No Turbo, no Nx, no build orchestration outside of pnpm’s own filters (pnpm --filter @vance/shared build). Build order results from dependencies entries.

Reference: nimbus-wb/client_world implements the same pattern (without the Engine part) and serves as a practical template.

2.2 Package Responsibilities

Package Scope Content
@vance/generated DTOs from Java TypeScript interfaces, generated by the Maven plugin from vance-api. Manually maintained index.ts. No logic.
@vance/shared Client logic (UI-free) JWT storage, refresh flow, REST client, WS connection manager, persistence helpers. No Vue, no Tailwind. Reusable for a later Electron/mobile client.
@vance/vance-face Web-UI Vue 3 + Vite + Tailwind + DaisyUI. Multi-page app: one HTML entry per editor. Components and composables reused internally.

2.3 Dependency Rules (strict)

Package May depend on
@vance/generated nothing (no runtime deps)
@vance/shared @vance/generated
@vance/vance-face @vance/shared (transitively @vance/generated)

@vance/vance-face must not contain direct REST or WebSocket code. All network calls go through @vance/shared. This ensures @vance/shared remains the sole location for auth header setting, reconnect logic, and URL constants.

3. Editor Inventory and Channel Mapping

Each editor is its own top-level HTML with its own Vue app. The binding list for v1:

Editor HTML Channel Live Purpose
Login / Landing index.html REST Login, Tenant/User input, Token mint, Forward
Chat chat.html WS + REST (History) Session Chat with the Brain. Picker and Live Mode (see §6). One WS connection per tab. Streaming response. History snapshot on mount via REST.
Inbox inbox.html REST Three-column layout (Sidebar: Inbox / Tags / Archive / Team Inbox per Team · Item list · Item detail with type-specific Action Bar — APPROVAL: Yes/No · DECISION: option-buttons · FEEDBACK: textarea+Send · plus Insufficient-Info / Undecidable / Delegate-Modal / Archive / Dismiss). Team Inbox shows items of other team members (own items live in “Inbox”).
Sessions session-editor.html REST Session list per Project, Session details, status snapshot.
Processes process-editor.html REST Process-Tree-View (Marvin), status snapshot, no live.
Documents document-editor.html REST Document list and detail view. Editing if backend API allows it.
Workspace workspace.html REST Read-only filesystem browser of the Project Home Pod (/projects/{project}/workspace/tree + /file). Project picker on the left, lazy Tree in the middle (depth-1 initial, loads Children on expand), File preview on the right (Markdown via MarkdownView, Text via Read-only CodeEditor, images as <img>, otherwise download). Pod forwarding is transparently handled by the server via WorkspaceRoutingCache.
Settings settings-editor.html REST Settings CRUD over Scope cascade.
Recipes recipe-editor.html REST Recipe list and edit.
Projects project-editor.html REST Project CRUD.
Cortex cortex.html WS + REST ✅ (Chat) Unified Chat + Document + Execute working environment per Project. Entry exclusively from chat.html via “Open in Cortex”. File-Tree + Tabs with dynamic Doc-Type-Dispatch (kind-registry + hand-rolled Bindings) + persistent Chat panel with Help sub-tab. kind: application-manifests open directly as an immersive App View (Calendar / Kanban / Slideshow Federation-Bundles via application:<type>-Kind-Registry entry) — no separate MPA entry anymore, see doc-kind-application §7. Run button for .js/.py with Live-Log; Validate + Hactar as JS-only actions. Spec: cortex.md. Replaces the former scripts.html, which has been removed without replacement.
Profile profile.html REST Self-service profile of the logged-in user: identity, per-user preferences (webui.* + chat.language + display.timezone), Speech settings, Team memberships (read-only) as well as admin-triggered actions (Model-Catalog Refresh/Discover). Details in §3.1.

Rule: New editors follow the same pattern. If a new editor needs live updates, it becomes WS-capable — and it must be justified why REST is not sufficient. Without justification, it remains REST.

Consequence: @vance/shared/ws is only imported by the Chat editor. Other editors do not pull the WS lib into their bundle at all (Vite/Rollup performs tree-shaking per entry).

3.1 Profile Editor (profile.html)

The self-service profile page (ProfileApp.vue). Authorization model: exclusively the user’s own profile — each endpoint resolves the subject from the JWT claim; there is no username path parameter. Cross-user management is handled separately via /brain/{tenant}/admin/users (requires ADMIN). The page exclusively uses the mandatory shell + mandatory components from §7 (EditorShell, VCard, VInput, VSelect, VCheckbox, VButton, VAlert).

Sections (VCards):

Section Controls Persistence
Identity tenantId + name (read-only), Display Name (title), Email (email) PUT /profile (Batch with Save button; status/roles are not changeable here — no self-escalation)
Preferences UI Language (webui.language), Assistant Language (chat.language), Timezone (display.timezone), Theme (webui.theme), Surface/UI Level (webui.uiLevel), Open documents in new tab (webui.document.openInNewTab) per control immediately via PUT/DELETE /profile/settings/{key}
Speech & Audio Voice (webui.speech.voiceUri), Rate (webui.speech.rate), Volume (webui.speech.volume) — only visible if the browser supports SpeechSynthesis; voices are filtered by the resolved chat.language per control immediately via PUT/DELETE
Actions (admin) “Refresh AI Model Catalog” (POST admin/ai-models/refresh), “Discover AI Models” (POST admin/ai-models/discover) direct trigger; the server enforces Action.ADMIN, client-gating is deliberately permissive (a 403 is displayed as an error). See recipes / Model-Catalog.
Teams List of team memberships read-only

REST Contract (all under /brain/{tenant}/profile, subject = JWT user):

  • GET /profileProfileDto (tenantId, name, title, email, teams, webUiSettings). webUiSettings contains all webui.* keys plus the allowlist extra keys (chat.language, display.timezone).
  • PUT /profile — patch identity (title, email), body ProfileUpdateRequest.
  • PUT /profile/settings/{key} — write a single setting, body ProfileSettingWriteRequest{value}.
  • DELETE /profile/settings/{key} — delete setting.

Self-Service Allowlist: PUT/DELETE settings/{key} only accepts keys with webui. prefix or from the extra allowlist (chat.language, display.timezone). Anything else → 400. This prevents the profile page from becoming a backdoor to arbitrary user-scope settings (which AdminSettingsController secures with Admin permission).

Conventions:

  • Default = Delete. The default value of a setting is stored as its absence: theme=auto, uiLevel=standard, UI language “Browser Default” (""), openInNewTab=trueDELETE. A specific non-default → PUT. Keeps cookie and DB lean.
  • Cookie Refresh. Each mutation renews the server-side (non-HttpOnly) vance_data cookie, from which getActiveTheme/getActiveLanguage/getActiveUiLevel read on the next page load; Theme/Locale are additionally applied immediately in the DOM, without reload.
  • Timezone — Browser Default Seed. If display.timezone is not set on first load, the page persistently stores the browser zone once (Intl.DateTimeFormat().resolvedOptions().timeZone), so that headless turns (Scheduler, Auto-Wakeup) and the Current-Date block also receive the actual zone — analogous to the Foot /timezone seed. The selector lists the full IANA list (Intl.supportedValuesOf('timeZone')). Semantics + Consumers: settings-system (display.timezone), prompt-caching §5b, scheduler §10c.

4. REST/WS Split

WS Architecture: Client WS on /brain/{tenant}/ws with multi-channel envelope (LiveEnvelope, v1 only session-channel active). Wire format and cross-pod routing are specified separately in live-ws.md; the WS sections in this Web-UI document focus on browser lifecycle and editor shell integration.

4.1 General Rule

WebSocket is the primary channel of the backend — vance-foot (CLI v2) is WS-only and the entire CLI functionality is accessible via WS. REST endpoints are an additional convenience layer for the Web-UI:

  • REST may duplicate functionality that is also available in parallel via WS.
  • REST is the preferred channel for static lists, large payloads, inventory queries without a live aspect.
  • WebSocket remains the only channel for streaming, live status, push events, and everything the CLI client needs.
  • In case of conflicts, WS wins — new backend features are designed WS-first, REST is optionally added later.

4.2 Specific Endpoints (v1)

REST (Brain → Web-UI):

  • POST /brain/{tenant}/access/{username} — JWT Mint (exists)
  • POST /brain/{tenant}/refresh — JWT Refresh (new, see §5)
  • GET /brain/{tenant}/admin/settings/... — Settings CRUD (exists)
  • GET|PUT /brain/{tenant}/profile + PUT|DELETE /brain/{tenant}/profile/settings/{key} — Self-service profile (identity + per-user settings), see §3.1
  • GET /brain/{tenant}/sessions/{sessionId}/messages — Chat History Pull for the Chat editor (new, see §6.5). Also used by the Picker for Last-Message-Preview.
  • Further REST endpoints for editors will be added as needed, each with DTOs in vance-api.

WebSocket (Brain ↔ Chat Editor): Complete frame list in specification/websocket-protokoll.md §6. The Chat editor uses a deliberately restricted subset — see §10.

5. Authentication

5.1 JWT Lifecycle

  • Mint: POST /brain/{tenant}/access/{username} with password provides token (AccessTokenResponse: token, expiresAtTimestamp). Exists.
  • Lifetime: 24 hours (Hardcoded in AccessController).
  • Refresh: New endpoint POST /brain/{tenant}/refresh — see §5.4. Accepts a valid, unexpired token in the Authorization: Bearer header and returns a fresh token.
  • Storage in Browser: localStorage under the keys
    • vance.jwt — the token string
    • vance.tenantId — for /brain/{tenant}/... paths
    • vance.username — for UI display
    • vance.activeSessionId — the last active Chat Session (see §6)

localStorage is accepted for v1. XSS hardening via strict CSP headers and no dangerouslySetInnerHTML usage. HttpOnly-Cookie migration is a later option and is not prepared here.

5.2 Usage in REST Calls

@vance/shared/rest automatically sets Authorization: Bearer <jwt> from localStorage. In case of 401, the wrapper attempts a refresh once; if the refresh fails, localStorage is cleared and redirects to index.html?next=<currentUrl>.

5.3 Usage in WebSocket

JWT is included in the HTTP upgrade request (header or query parameter — already implemented in BrainAccessFilter). The WS handshake validates the token; expiration during an active connection does not lead to disconnect, but new frames may be rejected by the server. The Chat editor polls the token expiration date locally and triggers a refresh and a reconnect before expiration.

5.4 Refresh Endpoint (new)

Request: POST /brain/{tenant}/refresh, Authorization: Bearer <currentJwt>, empty body. Response: RefreshTokenResponse { token, expiresAtTimestamp } (same form as AccessTokenResponse). Errors: 401 if token expired or tenant mismatch, 403 if tenant in path ≠ tid-claim.

Associated DTO in de.mhus.vance.api.access:

  • RefreshTokenResponse — structurally like AccessTokenResponse, own class for endpoint symmetry and cleaner TS generation. Annotated with @GenerateTypeScript("access").

No request body and thus no RefreshTokenRequest — the endpoint identifies the caller exclusively via the Authorization header token.

6. Session Handling in the Chat Editor

The Chat editor operates in two clearly separated modes. Picker Mode (URL without sessionId) shows a selection of own and Project Sessions, without a WS connection being bound to a Session. Live Mode (URL with sessionId) binds the WS connection to exactly one Session and streams Chat. The switch happens via routing within the same HTML entry.

Reason for the sub-page separation: before binding, the picker list with bound-flags (see websocket-protokoll.md §5.1) must be visible, so that the user recognizes an occupied Session without first receiving a 409 resume attempt. Furthermore, the picker URL is bookmarkable as an entry point, and deep links to a specific Session as a second form.

6.1 Persistence in the Browser

localStorage keys that the Chat editor reads and writes:

  • vance.activeSessionId — the last actively called Session ID (for auto-redirect from the picker)
  • vance.lastPickerProject — last Project Card selected in the picker (for reorder/highlight)

A Session ID in localStorage is not authoritative state, but only a UX hint for picker pre-selection. The source of truth is always the session-list response from the server.

6.2 Picker Mode (chat.html, without sessionId)

On mount:

  1. Check JWT. If expired / missing: Redirect to index.html?next=....
  2. Connect WS — the connection remains in the unbound state.
  3. In parallel: Query project-list and session-list (without filter).
  4. Render <EditorShell> with Picker View. Layout:
    • Sidebar: Project list (DataList). First element is the pseudo-group “Personal” with the user’s own Session _user_<username> (auto-created on first click if not yet existing). Below that, all visible Projects.
    • Main: Sessions of the selected Project (Card list). Visible per Session: displayName or sessionId-suffix as title, lastActivityAt relative (“3h ago”), bound-status as <VStatusDot variant="red"> with tooltip “occupied by another client”, otherwise gray (= available). Last bound Session is at the top.
    • Footer of the Main column: Button “New session” (Primary) and optional Recipe dropdown — passed as recipeName-param to session-bootstrap on click.
  5. Selection of an available Session → Routing to chat.html?sessionId=<id> (within the same Vue app, no full-page reload). Selection of an occupied Session is disabled; an additional “Force resume” path is not planned for v1.
  6. “New session” click → session-bootstrap with the active Project + selected Recipe → Server assigns sessionId → URL update.

The Picker View is a separate Vue sub-view within the Chat editor bundle, not a second HTML entry — Vue Router (or a simple watch on URLSearchParams) is sufficient.

6.3 Live Mode (chat.html?sessionId=<id>)

On mount:

  1. Check JWT as in 6.2.
  2. Connect WS.
  3. Send session-resume with the URL sessionId.
  4. Server responses:
    • Success: WS is bound, connection dot turns green. Parallel REST call for Chat History (see §6.5), then process-progress and chat-message-* Live Frames are welcome.
    • 404 / 403: Session no longer exists / foreign → User-facing error toast and redirect to the picker (remove ?sessionId from URL).
    • 409 (already bound): Connection dot turns red. UI shows a banner action “Session occupied — pick another / wait” in the Right-Panel slot with a Live Re-Try button (new session-resume attempt every ~10s optionally). The basic path remains: user returns to the picker.
  5. Update vance.activeSessionId with the successfully bound ID.

WS Disconnect during runtime (network lost, server restart): Connection dot turns gray, re-connect attempts in the background with backoff. On successful re-connect, another session-resume — if the Session becomes 409 in the meantime (e.g., another tab took it over), the dot jumps to red with the same banner action as above.

Multiple tabs on the same Session: Prevented by the bound-flag — the second tab will get 409. Multiple tabs on different Sessions are explicitly allowed; each tab maintains its own WS connection.

6.4 Connection Dot — Three States

The <VStatusDot> component in the <EditorShell> Topbar (right next to the User menu) is the only indicator of the connection status. Three states, uniform for all WS editors (v1 only Chat):

Color Meaning Tooltip
🟢 green WS connected, Session bound, Live stream active “Connected — live”
⚪ gray Picker mode (unbound) OR Disconnect with re-connect attempt “Reconnecting…” / “Pick a session to start”
🔴 red Last bind attempt resulted in 409 (occupied by another connection) “Session is occupied”

No yellow state and no movement in the dot component — animation is distracting. Pure color change is enough.

6.5 Chat History vs. Live Stream

The Web-UI renders two separate data sources in the Chat editor:

Source Channel Frame / Endpoint Content Rendering
History REST GET /brain/{tenant}/sessions/{id}/messages?limit=N (new, see below) persisted ChatMessageDocuments, loaded once on mount Static in main scroll, from top to bottom
Live Stream WS chat-message-stream-chunk + chat-message-appended Streaming tokens and new persisted messages Appended to the end of the main scroll; chunks optimistically, append frame replaces them
Live Progress WS process-progress (metrics/plan/status) ephemeral status pings, no conversation content Right-Panel Slot, NOT in Chat stream — see user-progress-channel.md

History and Live Stream must not overlap during the transition: chat-message-appended frames whose message ID is already in the loaded history are deduped by the client (ID match). Optimistic stream chunks belonging to a non-existent message are buffered and replaced by the first chat-message-appended.

Raw thoughts (“Gedanken”). For reasoning models (Qwen3, DeepSeek-R1, GPT-OSS), the live channel streams the raw thought narration during the turn; with chat-message-appended, the chunk is replaced by the final content (from the action message) and the narration would otherwise disappear. To keep the thoughts readable, the backend encapsulates the raw narration verbatim (unfiltered, including <think>-markup) and persists it separately as a thinking-field (see llm-resource-management §4.1 and websocket-protokoll §6) — available on both the History ChatMessageDto and ChatMessageAppendedData. <MessageBubble> renders thinking (if not empty) as an expandable, by default collapsed “Gedanken” area below the response text (native <details>), verbatim (no Markdown render, so <think>-markup is not swallowed by the sanitizer). Responses without streamed free text (e.g., pure action call) do not provide thinking → no area.

New REST Endpoint (to be implemented in vance-brain, spec will then move to websocket-protokoll.md §0 or a separate rest-protokoll.md):

  • GET /brain/{tenant}/sessions/{sessionId}/messages?limit=200&before=<messageId> — Page through Chat History backwards, newest first. Auth: JWT Bearer like all REST endpoints. Response: List<ChatMessageDto> (new DTO in vance-api with @GenerateTypeScript("chat")). Fields: messageId, processId, role (user/assistant/system/tool), content, createdAt, optional thinking (raw thought narration, verbatim — see above), optional toolCallId/toolName for Tool bubbles.

Reason against a WS-based History Pull: REST is for static lists with paging (§4.1), and the picker preview can use the same endpoint — a snippet of the last message per Session is shown in the picker as a sub-title. WS pull would burden the connection without a live aspect.

6.6 Slash Command Subset for the Web-UI

In vance-foot, slash commands are interpreted locally and sent as WS frames to the server. On the web, this is server-side identical (frames are the same) — the question is only which commands the Web-UI offers to the user in the input line.

Allowed in Web v1:

Command Affects
/help purely client-side, lists the following commands
/clear scrolls UI; does not delete server state
/skill <on\|off> <name> maps to process-skill
/compact maps to process-compact

Not allowed in Web v1 (would be lifecycle commands managed by the picker or the EditorShell menu):

  • /session-create, /session-resume, /session-list, /logout
  • /process-create, /process-list, /process-steer (steering is normal input without /)
  • /connect, /disconnect (WS connection is editor-implicit)

User input without / goes directly as process-steer to the active Think Process of the Session. The bootstrap (Recipe / Engine) is parameterized once at the picker and sent by the client as session-bootstrap — it is not a user command.

6.7 URL Schema — Overview

URL Mode Behavior
chat.html Picker §6.2
chat.html?sessionId=<id> Live §6.3, with bookmark and deep-link suitability
chat.html?projectId=<id> Picker with pre-selected Project optional, Sidebar selection is set

7. UI Style Guide

Basic Assumption: A style guide as plain Markdown text is not enough — as soon as three editors are developed in parallel, the appearance will drift. Consistency is enforced here by a mandatory shell and a mandatory component library, not by discipline alone. Direct use of DaisyUI classes outside the component library is a code review block.

7.1 Theme

  • Tailwind 3 + DaisyUI, configuration analogous to nimbus-wb/client_world/packages/controls/tailwind.config.js.
  • Themes: light and dark. Default: System preference (prefers-color-scheme). Toggle in the Topbar.
  • Accent color: DaisyUI default primary for v1. Custom branding will be determined later.
  • Typography: Inter for UI text, JetBrains Mono for code/IDs/token displays.
  • Spacing: Tailwind default scale (4px grid).
  • Global CSS: packages/vance-face/src/style/app.css — imported by every main.ts. Tailwind directives, DaisyUI theme selection.

7.2 Mandatory Shell — <EditorShell>

Every editor (except index.html, which does not know a Tenant before login) must render its top-level layout via <EditorShell>. No one builds their own <header> or <main>.

┌─────────────────────────────────────────────────────────────┐
│ Topbar: [Logo][Crumbs Project › Session › ...] [WS] [User] │
├─────────┬───────────────────────────────────────────┬───────┤
│         │                                           │       │
│ Sidebar │            Main Content                   │ Right │
│ (opt.)  │            (Editor-specific)              │ Panel │
│         │                                           │ (opt.)│
├─────────┴───────────────────────────────────────────┴───────┤
│ Footer (opt., spans full width — e.g. chat composer)        │
└─────────────────────────────────────────────────────────────┘

Usage:

<template>
  <EditorShell title="Inbox" :breadcrumbs="['Project foo', 'Inbox']">
    <template #sidebar><InboxFilters /></template>
    <InboxList />
    <template #right-panel><InboxItemDetails /></template>
    <template #footer><InboxReplyComposer /></template>
  </EditorShell>
</template>

<EditorShell> renders the Topbar with fixed geometry (Logo left, Breadcrumbs next to it, optional Connection State Dot, User Menu with Tenant/Logout/Theme Toggle right) and a CSS Grid body with up to four cells — Sidebar (slot #sidebar), Main (default), Right-Panel (slot #right-panel), Footer (slot #footer, spans full width). A cell only exists in the grid if its slot is occupied. Geometry, spacing, and transitions are centralized in the component — no editor adjusts them.

7.2.1 Focus Model — Single-Focus-Zone

Conceptual spec, designer API, and comparison with related layout patterns: specification/responsive-focus-layout.md. This section only describes the <EditorShell> API.

EditorShell operates a Single-Focus Model: exactly one zone (Sidebar, Main, Right, Footer) is “in focus”, the others are “not in focus”. The focused zone gets more space and a light background; the unfocused ones shrink to a compact width and adopt the editor background (DaisyUI base-200). This always makes it clear where the user is currently working, without zones needing to be expanded/collapsed.

Props to <EditorShell>:

Prop Type Default Meaning
focusModel 'off' \| 'auto' 'off' off = columns have fixed width, no background highlight, no reclaim handles. auto = focus mechanism active.
focusZone 'main' \| 'sidebar' \| 'right' \| 'footer' 'main' Currently focused zone. Only effective if focusModel='auto'.

Triggers (belongs to focusModel='auto'):

  • pointerdown on a zone → this zone is focused.
  • focusin on the footer (or a focusable element within it, e.g., the Composer textarea) → Footer is focused. focusin instead of pointerdown, because a tab-in from outside via keyboard should also address the footer.
  • Escape globally → Focus back to main (unless already there).
  • pointerdown outside the editor body grid (Topbar, everything around it) → Focus back to main. The Main zone is the implicit “Home”.

Reclaim Handles — small chips at the edge of each unfocused optional zone. They remain clickable even if the zone has collapsed to width 0 (e.g., on phone viewports), and are thus the only way back into a hidden zone. When a zone is focused, its own handle is hidden (opacity: 0).

Zone Handle Glyph Position
Sidebar right edge of the Sidebar (between Sidebar and Main)
Right left edge of the Right zone (between Main and Right)
Footer top edge of the Footer (between Main-Row and Footer-Row)
Main no handle (Main is always reclaimable via Escape or Topbar click)

Reclaim Handles are the only form of “manual focus operation” — they replace toggle icons in the header or keyboard shortcuts in v1.

Zone Sizing — defined as CSS variables in the scoped style block of <EditorShell>, allowing tuning without code edits. Three values exist per zone: --sidebar-base (default, when zone not focused), --sidebar-expanded (when focused), --sidebar-collapsed (when a sibling zone is focused that pushes the Sidebar away — e.g., when Right is fully open and Sidebar has to make space). Analogous for --right-* and --footer-*. For viewport responsiveness, values may use clamp(min, vw-fraction, max).

Responsive Collapse on narrow viewports — via media queries, --right-base / --sidebar-base / --footer-base can be set to 0, so that the zone completely disappears when Focus≠its-name. Reclaim Handles remain visible and are then the only way to reopen the zone.

Background Highlight — all zones by default show DaisyUI base-200 (= Editor BG, “gray”). The focused zone changes to base-100 (“white”), with a 200ms transition-colors. Transitions on background color and grid track width run synchronously (common --focus-duration-CSS variable).

Transition Status (2026-06-01): Pilot in chat.html runs within ChatView.vue (own CSS Grid, own CSS variables, only Right-Reclaim Handle), not via EditorShell props. The Shell lift transfers the model to EditorShell and adds Sidebar and Footer handles. Until then, focusModel/focusZone on EditorShell have code-side effect (column width), but no listeners/handles/background highlights — editors other than Chat should leave focusModel='off' (default).

7.3 Mandatory Components

All UI elements are built using the following primitives in packages/vance-face/src/components/. No editor directly uses DaisyUI classes like btn, input, alert, card, dialog, select, textarea, file-input. Only Tailwind layout classes (flex, grid, gap-*, p-*, space-*, min-h-*) are allowed — these are layout, not style.

Status: ✅ = implemented in src/components/ and re-exported in index.ts. – = planned, will be created as soon as the first editor needs it.

Layout & Navigation

Component Status Wraps Purpose
<EditorShell> Top-level layout: Topbar (Logo, Title, Breadcrumbs, optional Connection Dot, User Menu with Logout) + optional Sidebar (#sidebar) + Main (default) + optional Right-Panel (#right-panel) + optional Footer (#footer, full-width) + optional Topbar slot (#topbar-extra). Focus model via focusModel/focusZone props — see §7.2.1.
<VBackButton> Consistent back button for sub-pages, with arrow icon. Expects an @click listener; does not use its own routing logic.

Form Inputs

| Component | Status | Wraps | Purpose | |—|—|—|—| | <VButton variant size href loading> | ✅ | DaisyUI btn | Only allowed button. Variants: primary | secondary | ghost | danger | link. Renders as <a> if href is set (anchor-as-button for editor navigation). Own loading prop with spinner. | | <VInput> | ✅ | DaisyUI input | Text input with label, help text, error state. Mandatory for all single-line inputs. | | <VTextarea> | ✅ | DaisyUI textarea | Multi-line plain text input. font-mono by default. For structured content (Markdown / JSON / YAML) use <CodeEditor>. | | <CodeEditor mime-type> | ✅ | CodeMirror 6 | Multi-line editor with syntax highlighting. Language selected via mime-type: text/markdown, application/json, application/yaml (plus common aliases). Other mime types fall back to plain text. Line numbers, folding, bracket matching, undo/redo active. Mandatory for all editors that edit code/config/Markdown content — VTextarea remains for pure plain text. | | <MarkdownView source inline> | ✅ | marked + DOMPurify | Read-only Markdown renderer. GFM-enabled (Tables, Task-Lists, Fenced Code). DOMPurify sanitizes HTML output — mandatory for any user/LLM content that comes into the DOM via v-html. inline-prop for one-line previews (Chat Bubble, List-Row-Preview). In block mode, a token walker dispatches Vancetope-specific Fenced Blocks and vance:-Markdown links to <InlineKindBox>/<EmbeddedKindBox> — see inline-and-embedded-content §11. | | <KindBox> / <InlineKindBox> / <EmbeddedKindBox> | ✅ | — | Frame for rich content artifacts. Inline = Fenced-Body with Kind tag ( mindmap`, `tree, ` list`, `records). Embedded = Markdown link with vance:-URI to a Document. Both channels use the same registry kind→View. Action buttons: Inline = Download + Raw (Toggle); Embedded = Copy + Open. Full spec: [inline-and-embedded-content](/specs/inline-and-embedded-content). | | ` | ✅ | — | Closed UI unit for ASK_USER-Chat-Messages: question text + option buttons in a card. Dispatch occurs in `` via `meta.actionType === 'ASK_USER'` (see [inline-and-embedded-content](/specs/inline-and-embedded-content) §10/§11). | | `` | ✅ | DaisyUI `select` | Dropdown with Optgroup support: `options: { value, label, group?, disabled? }[]`. Consecutive options with the same `group` end up under an `<optgroup>`. Generic over the value type (`string \| number`). | | `` | ✅ | DaisyUI `file-input` | Drag-and-drop zone with file picker as fallback. v-model is always `File[]` (even with `multiple={false}`: 1-element). Multi-mode appended (Dedup by `name+size+lastModified`), picker reset after selection. Per-file `✕`-remove + "clear all". | | `` | – | DaisyUI `checkbox` | Checkbox with label. |

Containers & Dialogs

Component Status Wraps Purpose
<VCard title> DaisyUI card Uniform padding/shadow/rounding. #header/default/#actions-slots.
<VModal v-model close-on-backdrop> DaisyUI <dialog> Two-way bound visibility. ESC + Backdrop click (optional via Prop) + X-button close. #header/default/#actions-slots. Primary action right, Cancel left.

Feedback

Component Status Wraps Purpose
<VAlert variant> DaisyUI alert Banner. Variants: info | warning | error | success.
<VEmptyState headline body> Centered, #icon + Headline + Body + #action-slot.
<VToast> Auto-dismiss notification (top right). 3s success, 5s error. Global stack via useToast()-composable.
<VLoading> DaisyUI skeleton / loading Skeleton for lists, spinner for inline actions.

Display

Component Status Wraps Purpose
<VBadge> DaisyUI badge Status marker.
<VStatusDot variant> Colored dot for connection/process status. Variants: green | grey | red (no yellow, no animation — see §6.4). Tooltip slot for hover text. Mandatory in the Topbar slot of <EditorShell> for every WS editor.

Lists & Paging

Component Status Wraps Purpose
<VDataList items selectable selectedId @select> Card list with default slot per item. Optional selectable (Cursor + Hover + emit select). For Sessions, Processes, Documents — everywhere where metadata should be visible.
<VPagination v-model:page page-size total-count> Zero-based page indicator with « ‹ N/M › »-buttons and “X–Y of Z” display. v-model emits update:page with the new page number.
<VDataTable rows columns> Table for Settings, Recipes, Audit Logs (column structure dominant).
<VTree node> Hierarchical display — only for Marvin Task Tree.

New primitives will be added here before they are used in an editor. If an element is built that only it needs, it is not built as a primitive — it is built in its editor directory. If a second editor needs the same, it becomes a primitive.

7.4 Enforcement

  • Code Review: PRs are checked against style drift. Direct DaisyUI classes (btn, input, alert, card, dialog, modal, select, checkbox, textarea, file-input, badge, tabs, navbar) outside of packages/vance-face/src/components/ are a block.
  • Editor Template: packages/vance-face/src/_template/ contains a skeleton (_TemplateApp.vue + main.ts) that correctly integrates <EditorShell>. New editors are copied from it. (To be created as soon as the second editor emerges — oversized before that.)
  • /preview.html: Own editor HTML as a visual reference, showing each primitive in every state (loading/empty/error/normal). Maintained when adding a new primitive. Serves as a smoke test for theme and DaisyUI updates. Due — the library has reached 13 primitives, a central reference page helps with reviews and DaisyUI updates.
  • ESLint Rule (escalation stage, not v1): no-restricted-syntax blocks direct DaisyUI usage outside src/components/. Will be activated if review discipline is insufficient.

7.5 Dialogs — Modal vs. Sub-Page vs. Side-Panel

Variant Component When Examples
Modal <VModal> Confirmations, quick edits ≤ 5 fields, tool approvals. Not bookmarkable. “Delete Session?”, “Set Setting”, “Approve Tool Call”
Sub-Page own route, same <EditorShell> Longer editors, wizards, anything where the user should be able to share the URL. Document Editor with multiple tabs
Side-Panel #right-panel-slot of <EditorShell> Contextual details, main view remains usable. Process Inspector, Inbox Item Details

Back button on sub-pages: <VBackButton> top left in the content area, not in the Topbar (which remains static). Browser back must work the same way.

7.6 States

State Component Behavior
Loading <VLoading variant="skeleton\|spinner"> Skeleton in lists, spinner inline
Empty <VEmptyState> Centered, Icon + Headline + Action
Error transient <VToast variant="error"> Auto-dismiss 5s
Error permanent <VAlert variant="error"> Stays, closable (e.g., WS disconnected)
Success <VToast variant="success"> Auto-dismiss 3s

7.7 Forms

  • Labels above the field via <VInput label="...">. Help text and errors go into component props, not the template.
  • Inline validation (field level). Submit button (<VButton variant="primary">) is disabled until all required fields are valid.
  • Form Footer (Modal or Form Card): Cancel left (<VButton variant="ghost">), Primary right.

7.7.1 Cancel / Apply / Save — Mandatory Pattern for Edit Dialogs

Edit dialogs (Modal and Sub-Page if the page is a specific detail editor card) use three footer buttons with clearly separated semantics:

Button Variant Effect
Cancel ghost, left Discards changes since the last Apply / Open. Exits the dialog.
Apply secondary, middle Persists changes, stays in the dialog. On server error, the dialog also remains — the error is displayed in the dialog.
Save primary, right Persists + exits the dialog (== Apply ▶ Cancel-on-success). On server error, the dialog remains, the error message appears just like with Apply — the user can correct and press Save or Apply again.

Implementation Convention:

async function apply(): Promise<boolean> {
  // … persist; set editError on server error;
  // returns true if successful.
}

async function save(): Promise<void> {
  if (await apply()) closeDialog();
}

This keeps the logic DRY: save = apply + close-on-success. The dialog component / editor decides what “close” specifically means (Back-To-List, Modal-Close, Side-Panel-Collapse).

Why the separation: Users who edit iteratively (“change title, see if it looks good, change path, see if the list shows it correctly”) want Apply. Users who are done and want to return to the overview press Save. Before this convention, it was a Save button with unclear behavior — some editors closed, some didn’t; this confused users.

Does not apply to:

  • Pure confirmation modals (e.g., “Delete?” → Cancel + Confirm). A single action button is sufficient there.
  • Tool approval modals (e.g., “Allow Tool X to run?” → Cancel + Run). Single-shot.
  • Wizards with Next/Back navigation. Own convention.

7.8 Refresh Convention for REST Editors

Every REST-only editor has a refresh trigger — either as an icon button at the top of the content area or as a <VButton variant="ghost"> with a refresh icon. F5 is always a valid alternative, as editors load their state on-mount.

8. Build and Dev Workflow

8.1 DTO Generation

The generate-java-to-ts-maven-plugin (see vance/plugins/generate-java-to-ts-maven-plugin/) is configured in vance-api/pom.xml. Output directory is relative from the module:

<plugin>
  <groupId>de.mhus.vance.tools</groupId>
  <artifactId>generate-java-to-ts-maven-plugin</artifactId>
  <execution>
    <phase>generate-resources</phase>
    <goals><goal>generate</goal></goals>
    <configuration>
      <inputDirectory>${project.basedir}/src/main/java</inputDirectory>
      <outputDirectory>${project.basedir}/../../client/packages/generated/src</outputDirectory>
    </configuration>
  </execution>
</plugin>

Path justification: vance-api/ is under repos/vance/server/vance-api/, target is repos/vance/client/packages/generated/src/ — hence ../../client/... (server → vance → client).

Convention:

  • Plugin overwrites .ts files per annotated Java class with Auto-Generated-Header.
  • Plugin does not delete outdated files. If a Java DTO is renamed or deleted, the old .ts file must be manually removed.
  • Plugin automatically generates cross-file imports: for each referenced type that is also present in the model (e.g., InboxItemType referenced by InboxItemDto), an import { ... } from './…' is set. The scanner reads the resolved TS types (not the raw Java types), so that mappings like Map<K, V>Record<string, V> do not create phantom imports on the key type. TS built-ins (string, number, Date, Record, Promise, …) are excluded.
  • The index.ts in packages/generated/src/index.ts is manually maintained and re-exports the DTOs that a consumer actually needs. Extended when adding new editors.
  • Nimbus example: nimbus-wb/server/world-shared/pom.xml and nimbus-wb/client_world/packages/shared/src/generated/.

8.2 Build Commands

Java Side (Maven):

cd vance/vance-api && mvn install        # Generates DTOs as a side effect

Frontend Side (pnpm):

cd repos/vance/client
pnpm install
pnpm --filter @vance/generated build     # tsc → dist/
pnpm --filter @vance/shared build        # tsc → dist/
pnpm --filter @vance/vance-face build    # vite build → dist/
# or shorter:
pnpm -r build

wb-Wrapper:

New command wb build face:

  1. mvn -pl vance-api install -am (ensure DTOs are fresh)
  2. cd repos/vance/client && pnpm install && pnpm -r build

Java devs do not need Node installed — the frontend build is only triggered by wb build face. wb build vance continues to build only Java.

8.3 Dev Server

cd repos/vance/client
pnpm --filter @vance/vance-face dev      # vite dev with HMR

Vite proxies API calls to the local Brain (http://localhost:9990 or configurable via VITE_BRAIN_URL). Configuration analogous to nimbus-wb/client_world/packages/controls/vite.config.ts.

8.4 Vite Configuration (Multi-Page)

packages/vance-face/vite.config.ts:

export default defineConfig({
  plugins: [vue()],
  server: { port: 9900 },
  resolve: {
    alias: {
      '@': resolve(__dirname, './src'),
      '@components': resolve(__dirname, './src/components'),
      '@composables': resolve(__dirname, './src/composables'),
      '@vance/shared': resolve(__dirname, '../shared/src'),
      '@vance/generated': resolve(__dirname, '../generated/src'),
    },
  },
  build: {
    rollupOptions: {
      input: {
        index: resolve(__dirname, 'index.html'),
        chat: resolve(__dirname, 'chat.html'),
        inbox: resolve(__dirname, 'inbox-editor.html'),
        session: resolve(__dirname, 'session-editor.html'),
        process: resolve(__dirname, 'process-editor.html'),
        document: resolve(__dirname, 'document-editor.html'),
        workspace: resolve(__dirname, 'workspace.html'),
        settings: resolve(__dirname, 'settings-editor.html'),
        recipe: resolve(__dirname, 'recipe-editor.html'),
        project: resolve(__dirname, 'project-editor.html'),
      },
    },
  },
});

9. Editor Convention

Strictly for each editor:

packages/vance-face/
├── <name>-editor.html                       Top-level HTML with <script type="module" src="/src/<name>/main.ts">
└── src/<name>/
    ├── main.ts                              Auth check → mount Vue app
    └── <Name>App.vue                        Root component of the editor

Sub-components and sub-views of the editor live in src/<name>/components/ and src/<name>/views/. Shared components (Topbar, Sidebar, Modal wrapper) are in src/components/. Shared logic (useAuth, useChat, useDocuments) in src/composables/.

Vue Router: Allowed per editor, but not mandatory. Editors with internal tabs/sub-views (Document Editor: content/metadata/versions) use Vue Router. Single-view editors do without.

9.1 Boilerplate main.ts

import { createApp } from 'vue';
import { createPinia } from 'pinia';
import { ensureAuthenticated } from '@vance/shared/auth';
import App from './<Name>App.vue';
import '@/style/app.css';

await ensureAuthenticated();   // Redirect to index.html if no valid JWT
const app = createApp(App);
app.use(createPinia());
app.mount('#app');

9.2 Auth Check in @vance/shared/auth

export async function ensureAuthenticated(): Promise<void> {
  const jwt = localStorage.getItem('vance.jwt');
  const exp = parseExp(jwt);
  if (!jwt || exp < Date.now()) {
    const next = encodeURIComponent(window.location.pathname + window.location.search);
    window.location.href = `/index.html?next=${next}`;
    return new Promise(() => {}); // Block further execution
  }
}

10. WebSocket Frames in the Chat Editor

specification/websocket-protokoll.md §6 is the source spec; the Web-UI uses the following existing frames, without introducing new top-level types:

Who Frame Purpose in Chat Editor
Client → Server session-bootstrap Picker → Live transition: Session create-or-resume + initial Recipe / Engine
Client → Server session-resume Direct resume on deep link (URL ?sessionId=...)
Client → Server session-unbind Tab closes cleanly, without ending the Session
Client → Server process-steer User input to the Think Process — Chat send path
Client → Server process-skill, process-compact Few allowed slash commands (see §6.6)
Server → Client chat-message-stream-chunk Streaming tokens; optimistic rendering
Server → Client chat-message-appended Persist commit; replaces optimistic chunks
Server → Client process-progress Side-channel updates (metrics / plan / status) — Right-Panel, not Chat stream
Server → Client inbox-item-added, inbox-item-updated Optional, if Chat editor shows an Inbox badge in the Topbar
Server → Client error Bind errors (see websocket-protokoll.md §5.1) and frame-specific errors

Web clients do not use client-tool-register / client-tool-invoke / client-tool-result — Workspace Tools are a CLI/desktop concern.

New requirements from §6.5 arise not in the WS protocol, but in the REST layer (GET /brain/{tenant}/sessions/{id}/messages).

11. What comes later (explicitly not v1)

  • Cross-tab sync via BroadcastChannel or SharedWorker.
  • Live updates in Inbox/Process/Session editors.
  • API versioning in the WS welcome frame.
  • HttpOnly cookie auth.
  • Service Worker, Offline, PWA.
  • Custom branding/custom theme.

These points are deliberately excluded from v1. If a need arises, a v2 section will be added here.

12. References

  • nimbus-wb/client_world/ — pnpm Workspace, MPA pattern, tailwind.config.js, vite.config.ts
  • nimbus-wb/server/world-shared/pom.xml — Plugin configuration as template
  • vance/plugins/generate-java-to-ts-maven-plugin/ — the generator itself
  • specification/websocket-protokoll.md — existing WS frames
  • specification/architektur-scopes-clients.md — Scope hierarchy and client model
  • CLAUDE.md — will be supplemented with Web-UI module structure and dependency rules