app · diff

git:20260702.18f1c90 to git:20260816.ff0cd32

9 added, 415 removed. Audit A to A.

---
name: app
description: Guide for the Kimix web frontend (src/app/) — vanilla TypeScript + Vite, SSE-based chat UI mirroring sse_cli.py. Use when modifying the frontend, adding new UI features, changing the API client, or understanding the message rendering pipeline.
---
# Kimix Web Frontend (`src/app/`)
- ## Overview
-
- ```
- ┌────────────────────────────────────────────────────────────────────┐
- │ src/app/ (Vanilla TypeScript + Vite) │
- │ │
- │ index.html ──► src/main.ts (app glue, DOM, poll loop) │
- │ src/client.ts (HTTP + SSE client) │
- │ src/types.ts (wire format parsing) │
- │ src/renderer.ts (message → HTML) │
- │ src/styles.css (Catppuccin Mocha theme) │
- │ src/parse_test.ts (unit tests for parsing) │
- └────────────────────────────────────────────────────────────────────┘
- ```
+ Vanilla TypeScript + Vite chat UI mirroring `sse_cli.py`. Zero runtime deps; connects to the backend on port 4096 with poll-based SSE message streaming.
- This is a zero-dependency (except Vite + TypeScript dev tooling) web UI that mirrors the Python `sse_cli.py` terminal client. It connects to the backend on port 4096 and provides a chat interface with poll-based SSE message streaming.
+ **Key files**: `src/main.ts` (glue/DOM/poll loop), `src/client.ts` (HTTP + SSE), `src/types.ts` (wire-format parsing), `src/renderer.ts` (message → HTML), `src/styles.css` (Catppuccin Mocha).
- **Start**:
+ **Commands**:
```bash
cd src/app
- npm run dev # Vite dev server on port 5173, opens browser
- npm run build # TypeScript check + Vite production build → dist/
+ npm run dev # Vite dev server on port 5173
+ npm run build # TypeScript check + production build → dist/
npm run preview # Preview production build
```
-
- Or via the monorepo runner:
- ```bash
- uv run kimix gui # backend + Vite dev
- uv run kimix gui --build # build frontend first
- uv run kimix gui --fe-port 3000 # custom frontend port
- uv run kimix gui --no-fe # backend only (no Node.js/Vite required)
- # Backward-compatible wrapper:
- uv run scripts/run_app.py # delegates to `kimix gui`
- ```
-
- ## Environment Setup (Installation Experience)
-
- First-time setup from a clean clone:
-
- ```bash
- cd src/app
- # 1. Check if node_modules exists
- # If missing, dependencies need installation.
-
- # 2. Install npm dependencies
- npm install
- # Output: added 15 packages, audited 16 packages in ~2s
- # 0 vulnerabilities found
- # Installed packages: typescript ^5.7.0 + vite ^6.0.0 (dev only)
-
- # 3. Start Vite dev server
- npm run dev
- # VITE v6.4.3 ready in ~300 ms
- # ➜ Local: http://localhost:5173/
- # ➜ Network: use --host to expose
- ```
-
- **Key observations**:
- - **Zero runtime dependencies** — only `typescript` and `vite` as dev tooling. No framework, no React, no Vue.
- - **Install is fast** — `npm install` completes in ~2 seconds due to minimal dependency tree.
- - **Dev server starts instantly** — Vite ready in ~300ms. Hot module replacement (HMR) is active.
- - **No build step needed for development** — Vite serves TypeScript directly.
- - **The frontend connects to backend on port 4096** — you need the backend server running separately (see `serve` skill).
-
- **If you encounter issues**:
- - `npm install` fails → check Node.js version (requires Node >=18).
- - Port 5173 in use → modify `vite.config.ts` or use `--port` flag.
- - Frontend can't connect → ensure backend is running on port 4096, check the connection panel defaults.
-
- ---
-
- ## File Map
-
- | File | Role | Lines |
- |------|------|-------|
- | `index.html` | Entry HTML — connection panel, command bar, output area, input bar | 66 |
- | `package.json` | npm config — `dev` / `build` / `preview` scripts, TS+Vite deps | 16 |
- | `tsconfig.json` | TS config — ES2022, strict, bundler resolution | 17 |
- | `vite.config.ts` | Vite config — dev port 5173, sourcemaps, dist/ output | 14 |
- | `src/types.ts` | Types, enums, and wire-format parsers (`parseMessage`, `parseMessagePart`, `parseSession`) | 175 |
- | `src/client.ts` | `KimixClient` — `fetch()`-based HTTP client + `EventSource` SSE | 169 |
- | `src/renderer.ts` | `renderMessagePart()` — `MessagePart` → HTML `<span>` element | 134 |
- | `src/main.ts` | Application glue — DOM wiring, connect/disconnect, poll loop, slash commands | 421 |
- | `src/styles.css` | Catppuccin Mocha theme — CSS custom properties, part-type styling | 335 |
- | `src/parse_test.ts` | Standalone parse tests (11 tests, both wire formats) | 343 |
-
- ---
-
- ## Architecture
-
- ### Data Flow
-
- ```
- User types "hello" → sendPrompt()
- → client.sendPromptAsync(sessionId, "hello") // POST /session/{id}/prompt_async → 204
- → startPolling() // setInterval(pollMessages, 500ms)
- ↓
- pollMessages()
- → client.getMessages(sessionId, 50) // GET /session/{id}/message?limit=50
- → resp.json().map(parseMessage) // detects & parses both wire formats
- → for each new message part:
- renderMessagePart(part) → HTML element
- appendPart(el) → add to output area
- → idle detection: 4 empty polls (2s) → check status → stop polling if idle
- ```
-
- ### State Machine
-
- ```
- Disconnected ──[Connect]──► Connected ──[Send]──► Streaming ──[Idle]──► Connected
- ▲ │ ▲
- └──────[Disconnect]───────┘ │
- │
- Error ──────────────────┘
- ```
-
- Key state variables in `main.ts`:
- - `client: KimixClient | null` — HTTP client instance
- - `session: Session | null` — current session
- - `pollTimer` — `setInterval` handle for message polling
- - `seenMessageCount` — cursor for incremental message display
- - `emptyPolls` — consecutive polls with no new messages (idle detection)
- - `connected` / `debugMode` — UI flags
-
- ---
-
- ## Type System (`src/types.ts`)
-
- ### MessagePartType Enum
-
- ```typescript
- enum MessagePartType {
- TEXT = "text",
- THINKING = "thinking",
- TOOL_CALLING = "tool_calling",
- TOOL_CALLING_PART = "tool_calling_part",
- TOOL_RESULT = "tool_result",
- STEP_START = "step-start",
- STEP_FINISH = "step-finish",
- UNKNOWN = "unknown",
- }
- ```
-
- ### Core Interfaces
-
- ```typescript
- interface MessagePart {
- type: MessagePartType;
- text: string | null;
- tool_name: string | null;
- tool_status: string | null;
- tool_state: Record<string, unknown> | null;
- tool_result: string | null;
- call_id: string | null;
- reason: string | null;
- cost: number | null;
- tokens: Record<string, unknown> | null;
- raw_data: Record<string, unknown> | null;
- }
-
- interface Message {
- id: string;
- role: string;
- parts: MessagePart[];
- created_at: number | null;
- text_content: string; // concatenated TEXT parts
- }
-
- interface Session {
- id: string;
- title: string | null;
- created_at: number | null;
- updated_at: number | null;
- parent_id: string | null;
- }
- ```
-
- ### Wire Format Detection
-
- `parseMessage()` detects the backend format:
-
- **Dummy format** (from `DummySessionManager`): `data.info` is absent → uses `DUMMY_TYPE_MAP`:
- ```typescript
- DUMMY_TYPE_MAP = {
- Text → TEXT, Thinking → THINKING, ToolCalling → TOOL_CALLING,
- ToolCallingPart → TOOL_CALLING_PART, ToolResult → TOOL_RESULT
- }
- ```
- ```json
- {"type": "Text", "text": "Hello", "time": 1234567890.123}
- ```
-
- **Opencode format** (from `SessionManager`): `data.info` present → iterates `data.parts`, uses `OPENCODE_TYPE_MAP`:
- ```typescript
- OPENCODE_TYPE_MAP = { tool → TOOL_CALLING, reasoning → THINKING }
- ```
- ```json
- {
- "info": {"id": "msg_001", "role": "assistant", "time": {"created": 123}},
- "parts": [
- {"id": "prt_001", "type": "text", "text": "Hello"},
- {"id": "prt_002", "type": "tool", "tool": "read", "callID": "toolu_001",
- "state": {"status": "running", "input": {"path": "/foo"}}}
- ]
- }
- ```
-
- ---
-
- ## API Client (`src/client.ts`)
-
- `KimixClient` wraps all backend REST endpoints. Base URL: `http://{host}:{port}`.
-
- | Method | HTTP | Endpoint |
- |--------|------|----------|
- | `healthCheck()` | GET | `/global/health` |
- | `createSession(title?)` | POST | `/session` |
- | `getSession(id)` | GET | `/session/{id}` |
- | `deleteSession(id)` | DELETE | `/session/{id}` |
- | `listSessions()` | GET | `/session` |
- | `getMessages(id, limit?)` | GET | `/session/{id}/message?limit=N` |
- | `getSessionStatus(id)` | GET | `/session/{id}/status` |
- | `sendPromptAsync(id, text, agent?)` | POST | `/session/{id}/prompt_async` |
- | `abortSession(id)` | POST | `/session/{id}/abort` |
- | `clearSession(id)` | GET | `/session/{id}/clear` |
- | `compactSession(id, keep?)` | GET | `/session/{id}/compact?keep=N` |
- | `exportSession(id, outputPath?)` | GET | `/session/{id}/export?output_path=P` |
- | `streamEvents(onEvent, onError?)` | SSE | `/event` (EventSource) |
-
- **Pattern**: `fetch(baseUrl + path)` → check `resp.ok` → return JSON or boolean.
-
- ---
-
- ## Message Rendering (`src/renderer.ts`)
-
- ### `renderMessagePart(part: MessagePart): HTMLElement`
-
- Each `MessagePartType` → CSS class + rendering:
-
- | Type | CSS class | Rendering |
- |------|-----------|-----------|
- | `TEXT` | `.part-text` | Inline `<span>` with text content |
- | `THINKING` | `.part-thinking` | Inline `<span>` with text (cyan color) |
- | `TOOL_CALLING` | `.part-tool-calling` | Block: header (`⚡ tool_name`) + detail lines (status, input, output, error) |
- | `TOOL_CALLING_PART` | `.part-tool-calling-part` | Inline text (magenta) |
- | `TOOL_RESULT` | `.part-tool-result` | Block: `✓ result` or `✗ error` prefix |
- | `STEP_START` | `.part-step-start` | Block: `[STEP START]` (yellow) |
- | `STEP_FINISH` | `.part-step-finish` | Block: `[STEP FINISH] reason=...` (yellow) |
- | `UNKNOWN` | `.part-unknown` | JSON dump of `raw_data` (gray) |
-
- ### Utilities
-
- - `fmtArg(s, maxLen=120)` — truncate long strings keeping head + tail
- - `fmtTs(unixT)` — format unix timestamp → `HH:MM:SS`
- - `partTypeClass(type)` — map enum → CSS class name
-
- ---
-
- ## Application Logic (`src/main.ts`)
-
- ### DOM Wiring
-
- All elements are obtained once via `getElementById()` and stored in module-level variables. `setConnected()` enables/disables groups of buttons and inputs.
-
- ### Output Helpers
-
- - `log(text, cls)` — append a `<div>` with optional CSS class (`.info`, `.error`, `.debug`, `.user-input`)
- - `logHtml(html, cls)` — same but with innerHTML
- - `appendPart(el)` — smart append: consecutive TEXT parts chain inline via `.has-inline` wrapper divs
- - `scrollToBottom()` — auto-scroll output area
- - `setStatus(text, ok)` — update status indicator with green/red class
-
- ### Slash Commands
-
- Handled via `handleCommand()` switch. Commands with optional arguments use `:` separator (e.g., `/compact:5`).
-
- | Command | Action |
- |---------|--------|
- | `/help` | Print command list |
- | `/new` | Create new session, clear output |
- | `/abort` | Cancel running prompt |
- | `/status` | Show session status JSON |
- | `/sessions` | List all sessions |
- | `/messages` | Show recent messages |
- | `/clear` | Clear session history + output |
- | `/compact[:N]` | Compact context, optional keep count |
- | `/export[:path]` | Export session to file |
- | `/exit` | Disconnect |
-
- ### Poll Loop
-
- `pollMessages()` runs every 500ms:
- 1. Fetches last 50 messages
- 2. Renders new messages from `seenMessageCount` cursor
- 3. After 4 empty polls (2s), checks session status → if idle/error, stops polling
-
- ---
-
- ## Styling (`src/styles.css`)
-
- Catppuccin Mocha theme via CSS custom properties:
-
- ```css
- :root {
- --bg: #1e1e2e; /* base */
- --fg: #cdd6f4; /* text */
- --surface: #181825; /* mantle */
- --surface2: #313244; /* surface0 */
- --border: #45475a; /* surface1 */
- --accent: #89b4fa; /* blue */
- --green: #a6e3a1;
- --red: #f38ba8;
- --yellow: #f9e2af;
- --cyan: #89dceb;
- --magenta: #cba6f7;
- --gray: #6c7086;
- --font: "Cascadia Code", "Fira Code", "JetBrains Mono", "Consolas", monospace;
- }
- ```
-
- ### Layout
-
- - `#app` — flex column, max-width 1200px, height 100vh
- - `#connection-panel` — host/port inputs, connect/disconnect buttons, debug toggle
- - `#command-bar` — flex row of slash-command shortcut buttons
- - `#output-panel` — flex:1, scrollable output area (min-height 200px)
- - `#input-panel` — flex row with text input + Send button
-
- ### Part Styling
-
- - `.part-text` — `color: var(--fg)` (normal text)
- - `.part-thinking` — `color: var(--cyan)` (thinking text)
- - `.part-tool-calling` — `display: block; color: var(--magenta)` (tool header + details)
- - `.part-tool-calling-part` — `color: var(--magenta)` (inline tool output)
- - `.part-tool-result` — `display: block; color: var(--green)` (success), `.tool-error` variant in red
- - `.part-step-start` / `.part-step-finish` — `display: block; color: var(--yellow)`
- - `.part-unknown` — `color: var(--gray)`
-
- ---
-
- ## How to Add a New UI Feature
-
- ### Step 1: Add backend endpoint (if needed)
-
- Follow the `serve` skill guide: add method to `DummySessionManager` + `SessionManager`, expose route in `dummy_app.py`/`app.py`.
-
- ### Step 2: Add client method
-
- In `src/client.ts`, add to `KimixClient`:
-
- ```typescript
- async myAction(sessionId: string, param: string): Promise<SomeType> {
- const resp = await fetch(`${this.baseUrl}/session/${sessionId}/my-action?param=${param}`);
- if (!resp.ok) throw new Error(`myAction failed: ${resp.status}`);
- return await resp.json();
- }
- ```
-
- ### Step 3: Add UI elements
-
- In `index.html`:
- - Add button in `#command-bar`: `<button id="btn-myaction" disabled>/myaction</button>`
- - Or add input/controls as needed
-
- ### Step 4: Wire in main.ts
-
- ```typescript
- // Get element reference
- const btnMyAction = document.getElementById("btn-myaction") as HTMLButtonElement;
-
- // Add event listener
- btnMyAction.addEventListener("click", () => handleCommand("/myaction"));
-
- // Add case to handleCommand() switch
- case "myaction": {
- const result = await client.myAction(session.id, "value");
- log(`[SSE CLI] MyAction: ${JSON.stringify(result)}`, "info");
- break;
- }
-
- // Add to setConnected() disabled-buttons array
- for (const btn of [..., btnMyAction]) { ... }
- ```
-
- ### Step 5: Add new message type (if needed)
-
- If the backend produces a new `MessageType`, update:
- 1. `src/types.ts` — add to `MessagePartType` enum and `DUMMY_TYPE_MAP` / `OPENCODE_TYPE_MAP`
- 2. `src/renderer.ts` — add case in `partTypeClass()` and `renderMessagePart()`
- 3. `src/styles.css` — add `.part-<newtype>` styling
-
- ---
-
- ## Testing
-
- ### Parse Tests (`src/parse_test.ts`)
-
- Standalone tests for `parseMessage()` and `parseMessagePart()`. Run with:
-
- ```bash
- cd src/app && npx tsx src/parse_test.ts
- ```
-
- 11 tests covering:
- - Dummy format: Text, Thinking, ToolCalling, ToolResult
- - Opencode format: Text, Tool (→tool_calling), Reasoning (→thinking), Step-Start/Finish
- - Edge cases: empty data, unknown types, multi-part messages
-
- ### Browser Debugging
-
- - Open DevTools → Console tab for `fetch` errors and `catch` block logs
- - Toggle Debug checkbox to enable verbose logging (sets `debugMode`)
- - Check Network tab for REST calls to port 4096
-
- ---
+ Monorepo runner: `uv run kimix gui` (backend + Vite dev), `--build`, `--fe-port 3000`, `--no-fe` (backend only); `uv run scripts/run_app.py` delegates to `kimix gui`.
- ## Dependencies
+ **Adding a UI feature**: backend endpoint (serve skill) → `KimixClient` method → `index.html` elements → wire in `main.ts` `handleCommand()` → optional new MessagePartType across `types.ts`/`renderer.ts`/`styles.css`.
- **Runtime**: None (vanilla TypeScript, no framework).
+ **Tests**: `cd src/app && npx tsx src/parse_test.ts` (11 parse tests).
- **Dev only**:
- - `typescript ^5.7.0` — type checking
- - `vite ^6.0.0` — dev server + bundler
+ Full architecture, data flow, API client table, and styling details: read `references/architecture.md`.
- The `package.json` is `"type": "module"` for ESM imports.