Frontend architecture
June 11, 2026 · View on GitHub
Developer reference for the Vue 3 web dashboard. Covers the component tree, store design, WebSocket protocols, and how to add new panels.
Dev loop
Source lives in src/kohakuterrarium-frontend/. Built output lands in
src/kohakuterrarium/web_dist/ (configured in vite.config.js:48),
where the FastAPI app in api/app.py and serving/web.py pick it up
as static files.
# Dev server (hot reload, points at the Python API via proxy)
npm run dev --prefix src/kohakuterrarium-frontend
# Production build (writes into src/kohakuterrarium/web_dist)
npm run build --prefix src/kohakuterrarium-frontend
# Lint / format
npm run lint --prefix src/kohakuterrarium-frontend
npm run format --prefix src/kohakuterrarium-frontend
# Unit tests (vitest + jsdom)
npm run test --prefix src/kohakuterrarium-frontend
When distributing KT, run npm run build so web_dist/ is populated
before pip install -e . or packaging. The Python side ships the
built bundle as part of the installed package.
Stack
- Vue 3.5+ with
<script setup>composition API - Pinia 3 for state management (options API stores for chat, composition API for layout/canvas/palette)
- Vite (rolldown-vite) with UnoCSS, unplugin-auto-import, unplugin-vue-components, unplugin-vue-router
- Element Plus 2.11 for dialogs, dropdowns, selects, tooltips
- Monaco Editor for code editing
- Vditor for rich markdown editing
- xterm.js for the terminal panel
- highlight.js for canvas code viewer
- splitpanes (legacy, used in old SplitPane.vue component only)
Directory structure
src/kohakuterrarium-frontend/src/
├── App.vue # Root: NavRail + router-view + global composables
├── main.js # Pinia + router + panel registration (sync!)
├── style.css # Theme variables, font stacks
├── components/
│ ├── chat/ # ChatPanel, ChatMessage, ToolCallBlock
│ ├── chrome/ # AppHeader, StatusBar, ModelSwitcher,
│ │ CommandPalette, ToastCenter
│ ├── common/ # StatusDot, SplitPane, GemBadge, MarkdownRenderer
│ ├── editor/ # EditorMain, MonacoEditor, VditorEditor,
│ │ FileTree, FileTreeNode, EditorStatus
│ ├── layout/ # WorkspaceShell, LayoutNode, EditModeBanner,
│ │ PanelHeader, PanelPicker, SavePresetModal,
│ │ NavRail, NavItem, Zone*.vue (legacy)
│ ├── panels/ # ActivityPanel, StatePanel, FilesPanel,
│ │ CreaturesPanel, CanvasPanel, SettingsPanel,
│ │ DebugPanel, TerminalPanel
│ │ ├── canvas/ # CodeViewer, MarkdownViewer, HtmlViewer
│ │ ├── debug/ # LogsTab, TraceTab, PromptTab, EventsTab
│ │ └── settings/ # ModelTab, PluginsTab, ExtensionsTab, etc.
│ ├── registry/ # ConfigCard
│ └── status/ # StatusDashboard (large tabbed status panel)
├── composables/
│ ├── useKeyboardShortcuts.js # Ctrl+1..6, Ctrl+Shift+L, Ctrl+K
│ ├── useBuiltinCommands.js # Palette command registry
│ ├── useAutoTriggers.js # Canvas notification, error→debug
│ ├── useArtifactDetector.js # Scans chat for code blocks → canvas store
│ ├── useLogStream.js # /ws/logs WebSocket composable
│ └── useFileWatcher.js # /ws/files WebSocket composable (unused on Windows)
├── stores/
│ ├── chat.js # WebSocket chat, messages, runningJobs, tokenUsage
│ ├── layout.js # Presets, panels, edit mode, split tree mutations
│ ├── layoutPanels.js # Panel + preset registration (called from main.js)
│ ├── canvas.js # Artifact detection + storage
│ ├── files.js # Touched files derived from chat events
│ ├── scratchpad.js # Scratchpad REST client
│ ├── palette.js # Command palette registry + fuzzy search
│ ├── notifications.js # Toast + history
│ ├── instances.js # Running instance list
│ ├── editor.js # Open files, active file, tree
│ ├── theme.js # Dark/light toggle
│ └── ...
├── pages/
│ ├── instances/[id].vue # Main instance view (WorkspaceShell)
│ ├── editor/[id].vue # Editor-focused view (WorkspaceShell)
│ ├── detached/[key].vue # Pop-out single panel
│ ├── panel-debug.vue # Debug page: each panel as a tab
│ ├── index.vue, new.vue, sessions.vue, registry.vue, settings.vue
│ └── ...
└── utils/
├── api.js # Axios HTTP client (all REST endpoints)
└── layoutEvents.js # CustomEvent bus for cross-component actions
Layout system
Binary split tree
The layout is a recursive binary tree where each node is:
// Split: two children with a draggable divider
{ type: "split", direction: "horizontal"|"vertical", ratio: 0-100, children: [Node, Node] }
// Leaf: renders one panel
{ type: "leaf", panelId: "chat" }
LayoutNode.vue is the recursive renderer. For splits, it renders two
children in a flex container with a pointer-captured drag handle. For
leaves, it resolves the panel component from the layout store and
renders it via <component :is>.
Panel registration
Panels are registered in stores/layoutPanels.js at app startup
(synchronous, before app.mount()):
layout.registerPanel({
id: "chat",
label: "Chat",
component: ChatPanel,
});
The component is wrapped in markRaw() internally so Vue reactivity
doesn't wrap it.
Presets
Presets are tree definitions with an id, label, and optional shortcut:
const CHAT_FOCUS = {
id: "chat-focus",
label: "Chat Focus",
shortcut: "Ctrl+1",
tree: hsplit(70, leaf("chat"), vsplit(65, leaf("status-dashboard"), leaf("state"))),
};
Helper functions hsplit(ratio, left, right), vsplit(ratio, top, bottom),
and leaf(panelId) create the tree nodes concisely.
Panel props
Route pages (e.g., pages/instances/[id].vue) provide runtime props to
panels via Vue's provide("panelProps", computed(() => ({...}))). The
LayoutNode injects this and passes the appropriate slice to each leaf
component based on panelId.
Edit mode
layout.enterEditMode() deep-clones the active preset. All tree
mutations (replace, split, close) operate on the clone.
layout.exitEditMode() restores the original. layout.saveEditMode()
persists the clone (user presets only).
WebSocket protocols
Chat (/ws/creatures/{agent_id} or /ws/terrariums/{id})
Existing; managed by stores/chat.js. Streams text chunks, tool
start/done, token usage, session info, compaction events.
Logs (/ws/logs)
Server process log tail. Messages: {type: "meta"|"line"|"error", ...}.
Lines parsed into {ts, level, module, text}.
Terminal (/ws/terminal/{agent_id})
PTY shell in the agent's working directory. Messages:
- Client → Server:
{type: "input", data: "..."},{type: "resize", rows, cols} - Server → Client:
{type: "output", data: "..."},{type: "error", data: "..."}
Files (/ws/files/{agent_id})
File system watcher (watchfiles). Messages:
{type: "ready"|"change"|"error", ...}. Changes include path + action
(added/modified/deleted). Currently unreliable on Windows.
Adding a new panel
- Create
components/panels/MyPanel.vue - Register in
stores/layoutPanels.js:import MyPanel from "@/components/panels/MyPanel.vue"; layout.registerPanel({ id: "my-panel", label: "My Panel", component: MyPanel }); - Add to a preset tree:
tree: hsplit(50, leaf("chat"), leaf("my-panel")) - If the panel needs runtime props (like
instance), add an entry to the route page'spanelPropscomputed.
Theme
stores/theme.js manages dark/light mode. Components use
useThemeStore().dark reactively. CSS uses html.dark class for dark
mode overrides. UnoCSS dark: prefix works throughout.
Vditor and xterm.js have their own theme systems; both watch
themeStore.dark and call their respective theme-switch APIs.