Vancetope Web-UI — Specification
Status: v1 Initial Draft. This spec is binding for the development of the Web-UI under
repos/vance/client/(symlinkclient_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 /profile→ProfileDto(tenantId,name,title,email,teams,webUiSettings).webUiSettingscontains allwebui.*keys plus the allowlist extra keys (chat.language,display.timezone).PUT /profile— patch identity (title,email), bodyProfileUpdateRequest.PUT /profile/settings/{key}— write a single setting, bodyProfileSettingWriteRequest{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=true→DELETE. A specific non-default →PUT. Keeps cookie and DB lean. - Cookie Refresh. Each mutation renews the server-side (non-HttpOnly)
vance_datacookie, from whichgetActiveTheme/getActiveLanguage/getActiveUiLevelread on the next page load; Theme/Locale are additionally applied immediately in the DOM, without reload. - Timezone — Browser Default Seed. If
display.timezoneis 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/timezoneseed. 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}/wswith multi-channel envelope (LiveEnvelope, v1 onlysession-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.1GET /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 theAuthorization: Bearerheader and returns a fresh token. - Storage in Browser:
localStorageunder the keysvance.jwt— the token stringvance.tenantId— for/brain/{tenant}/...pathsvance.username— for UI displayvance.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 likeAccessTokenResponse, 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:
- Check JWT. If expired / missing: Redirect to
index.html?next=.... - Connect WS — the connection remains in the unbound state.
- In parallel: Query
project-listandsession-list(without filter). - 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:
displayNameorsessionId-suffix as title,lastActivityAtrelative (“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 tosession-bootstrapon click.
- Sidebar: Project list (DataList). First element is the pseudo-group “Personal” with the user’s own Session
- 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. - “New session” click →
session-bootstrapwith the active Project + selected Recipe → Server assignssessionId→ 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:
- Check JWT as in 6.2.
- Connect WS.
- Send
session-resumewith the URLsessionId. - Server responses:
- Success: WS is bound, connection dot turns green. Parallel REST call for Chat History (see §6.5), then
process-progressandchat-message-*Live Frames are welcome. - 404 / 403: Session no longer exists / foreign → User-facing error toast and redirect to the picker (remove
?sessionIdfrom 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-resumeattempt every ~10s optionally). The basic path remains: user returns to the picker.
- Success: WS is bound, connection dot turns green. Parallel REST call for Chat History (see §6.5), then
- Update
vance.activeSessionIdwith 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 invance-apiwith@GenerateTypeScript("chat")). Fields:messageId,processId,role(user/assistant/system/tool),content,createdAt, optionalthinking(raw thought narration, verbatim — see above), optionaltoolCallId/toolNamefor 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:
lightanddark. Default: System preference (prefers-color-scheme). Toggle in the Topbar. - Accent color: DaisyUI default
primaryfor 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 everymain.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'):
pointerdownon a zone → this zone is focused.focusinon the footer (or a focusable element within it, e.g., the Composer textarea) → Footer is focused.focusininstead ofpointerdown, because a tab-in from outside via keyboard should also address the footer.Escapeglobally → Focus back tomain(unless already there).pointerdownoutside the editor body grid (Topbar, everything around it) → Focus back tomain. 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). |
|
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 ofpackages/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-syntaxblocks direct DaisyUI usage outsidesrc/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
.tsfiles per annotated Java class with Auto-Generated-Header. - Plugin does not delete outdated files. If a Java DTO is renamed or deleted, the old
.tsfile must be manually removed. - Plugin automatically generates cross-file imports: for each referenced type that is also present in the model (e.g.,
InboxItemTypereferenced byInboxItemDto), animport { ... } from './…'is set. The scanner reads the resolved TS types (not the raw Java types), so that mappings likeMap<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.tsinpackages/generated/src/index.tsis 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.xmlandnimbus-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:
mvn -pl vance-api install -am(ensure DTOs are fresh)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.tsnimbus-wb/server/world-shared/pom.xml— Plugin configuration as templatevance/plugins/generate-java-to-ts-maven-plugin/— the generator itselfspecification/websocket-protokoll.md— existing WS framesspecification/architektur-scopes-clients.md— Scope hierarchy and client modelCLAUDE.md— will be supplemented with Web-UI module structure and dependency rules