# Navi Web Client — Technical Specification (existing client)

This document describes the existing web client located at `/home/gmikcon/Projects/navi-1/client/`. It serves as a reference baseline for building the new client.

---

## 1. Technology stack

| Concern | Solution |
|---------|----------|
| Language | Vanilla JavaScript (ES modules, no transpilation) |
| Build | None — served as static files directly |
| Markdown rendering | `marked.js` via esm.sh CDN |
| Syntax highlighting | `highlight.js` via esm.sh CDN |
| Styling | Plain CSS with custom properties (no framework) |
| Server | Served from `GET /static/**` by the FastAPI backend |

No bundler, no TypeScript, no framework. Everything is a native browser API or an ES module imported from CDN.

---

## 2. File structure

```
client/
├── index.html          # Single-page app shell
├── style.css           # All styles (968 lines, dark theme)
└── js/
    ├── app.js          # Main entry point — state, routing, WS event dispatch
    ├── chat.js         # DOM component builders (bubbles, cards, etc.)
    ├── sidebar.js      # Sidebar rendering (profiles, session list)
    ├── ws.js           # WebSocket wrapper class
    └── api.js          # REST API wrapper
```

---

## 3. Layout

Two-column layout (no responsive/mobile breakpoints in the existing client):

```
┌──────────────────────────────────────────────────────┐
│  Sidebar (300px, fixed)  │  Chat area (flex: 1)      │
│                          │                            │
│  [Profile selector ▼]    │  ┌──── Chat header ─────┐ │
│                          │  │ Session name | Profile│ │
│  [+ New session]         │  └───────────────────────┘ │
│                          │                            │
│  Session list:           │  Messages:                 │
│  ● Session A      [🗑]   │  (scrollable container)    │
│  ● Session B      [🗑]   │                            │
│  ...                     │                            │
│                          │  ┌──── Input bar ────────┐ │
│                          │  │ [📎] [textarea] [Send]│ │
│                          │  │ [file previews...]    │ │
│                          │  └───────────────────────┘ │
└──────────────────────────────────────────────────────┘
```

### Sidebar (300px, fixed height, flex column)

- **Profile selector**: `<select>` populated from `GET /agents/profiles`. Selecting a profile sets `state.selectedProfileId` and is used when creating a new session.
- **New session button**: Creates a session with `state.selectedProfileId`, then switches to it.
- **Session list**: Rendered by `renderSessions()`. Each item shows session preview text and a delete icon. Click → load session. Pinned sessions appear first (server-sorted).

### Chat header

Shows the current session name (generated from `session_id`) and the active profile name.

### Messages area

Scrollable `<div id="messages">`. All message components are appended here via DOM helpers in `chat.js`. Auto-scrolls to bottom on new content.

### Input bar

- `<textarea>` for text input. Sends on Enter (Shift+Enter for newline).
- Attach button (`📎`) → hidden `<input type="file" multiple>` → uploads via `POST /sessions/{id}/files`.
- File preview area shows pending file names with remove buttons.
- Image paste: intercepts `paste` event, reads image files via `FileReader`, converts to base64.
- Send button submits the message.
- Disabled during streaming (`state.streaming = true`).

---

## 4. Application state (`app.js`)

All mutable state is held in a single object:

```js
const state = {
  profiles: [],              // AgentProfile[] from GET /agents/profiles
  sessions: [],              // Session[] from GET /sessions
  currentId: null,           // Active session_id
  ws: null,                  // WsClient instance
  streaming: false,          // True while agent is running
  pendingImages: [],         // Base64 strings waiting to be sent
  pendingFiles: [],          // File objects from POST /sessions/{id}/files
  uploadCount: 0,            // Tracks in-flight uploads
  pendingToolCard: null,     // DOM element for the in-progress tool call
  pendingSubStep: null,      // DOM element for the in-progress subagent step
  selectedProfileId: null,   // Profile chosen in the selector (for new sessions)
};
```

Draft persistence: the textarea content is saved to `localStorage` keyed by `session_id` and restored when the session is reloaded.

---

## 5. Routing

Session ID is stored in the URL hash: `http://host/#<session_id>`.

- On load: read `location.hash`. If valid → load that session. Else → load most recent from `GET /sessions`.
- On session switch: update `location.hash` without page reload.
- Browser back/forward: `hashchange` event triggers session switch.

No server-side routing — the server always serves `index.html` from `/`.

---

## 6. WebSocket event handling

One WebSocket connection per active session. Managed by `WsClient` (`ws.js`):

```js
class WsClient {
  connect(sessionId)    // Opens WS, sets up handlers
  send(payload)         // JSON.stringify + ws.send
  disconnect()          // ws.close
  get ready()           // ws.readyState === WebSocket.OPEN
}
```

Events dispatched in `app.js` in a `switch` on `event.type`:

| Event | Handler |
|-------|---------|
| `stream_start` | Disable input, `state.streaming = true`, create streaming bubble |
| `thinking_delta` | Accumulate delta in thinking card, auto-scroll |
| `thinking_end` | Collapse thinking card, mark as done |
| `turn_thinking` | Render a collapsible "turn thinking" card (complete block) |
| `plan_ready` | Render a collapsible plan card |
| `tool_started` | Create pending tool card (spinner) or subagent step |
| `tool_call` | Finalize tool card with result and success/failure indicator |
| `stream_delta` | Append delta to streaming bubble, parse markdown, auto-scroll |
| `stream_end` | Finalize bubble content, re-enable input, update token indicator |
| `stream_stopped` | Re-enable input |
| `profile_switched` | Update chat header profile indicator |
| `context_compressed` | Append compression notice to messages |
| `error` | Append error message bubble |

---

## 7. Message components (`chat.js`)

All components are built as DOM nodes and appended to `#messages`.

### User bubble

Plain text (no markdown). Shows role label "You". Optionally contains:
- Image thumbnails (base64, click to view full size)
- File pills (filename + size)

### Assistant streaming bubble

Created on `stream_start`. Text content is accumulated from `stream_delta` events and rendered with `marked.parse()` on each delta. On `stream_end` the bubble is finalized: markdown re-rendered once cleanly, code blocks highlighted with `hljs.highlightAll()`.

### Thinking card (`appendThinkingCard`)

Collapsible `<details>` element. Created on first `thinking_delta`. Delta text is accumulated as plain text. On `thinking_end` the card is auto-closed (collapsed). User can re-open it at any time.

### Turn thinking card (`appendTurnThinkingCard`)

Same visual as thinking card but arrives as a single complete block (`turn_thinking` event). Collapsed by default. `is_subagent: true` → nested under the parent tool card.

### Plan card (`appendPlanCard`)

Collapsible `<details>` element. Shows the plan text from `plan_ready` event, rendered as markdown. Collapsed by default after a brief open period (or immediately, depending on UX preference).

### Tool call card (`appendPendingToolCard` → `finalizeToolCard`)

Two-phase render:
1. `tool_started` → create card with tool name, arguments (JSON), and a spinner.
2. `tool_call` → update card: remove spinner, show result, color-code success/failure.

Collapsed by default, expandable to see arguments and result. Tool-specific icon shown (dict of tool name → emoji/icon in `chat.js`).

### Subagent step (`appendSubagentStep`)

Nested card inside a `spawn_agent` tool card. Created on `tool_started` with `is_subagent: true`. Same finalization flow as a regular tool card. Multiple subagent steps stack vertically inside the parent card.

### Summary card (`appendSummaryCard`)

Rendered when a message has `is_summary: true` in the history. Shows "[Context compressed — summary]" header with the summary text collapsed inside.

### Compression notice (`appendCompressionNotice`)

Inline notice: "Context compressed (42 → 12 messages)". Appended after `stream_end` when `context_compressed` event arrives.

---

## 8. Tool icons

`chat.js` contains a dictionary mapping tool names to display icons:

```js
const TOOL_ICONS = {
  web_search: "🔍",
  filesystem: "📁",
  code_exec:  "⚙️",
  terminal:   "💻",
  ssh_exec:   "🖥️",
  spawn_agent:"🤖",
  // ... etc
};
```

Unknown tools fall back to a generic icon.

---

## 9. File and image attachment flow

### Images

1. User pastes an image or selects via file picker (image types only).
2. `FileReader.readAsDataURL()` → base64 string added to `state.pendingImages`.
3. Preview thumbnail shown in input area with remove button.
4. On send: base64 strings included in `images` array of the WebSocket message.
5. Server strips `data:image/...;base64,` prefix automatically.

### Files

1. User clicks attach button → file picker (any non-blocked type).
2. File uploaded via `POST /sessions/{id}/files` (multipart).
3. Server returns `{ name, path, size, content_type }`.
4. File object added to `state.pendingFiles`. Preview pill shown in input area.
5. On send: file objects included in `files` array of the WebSocket message.
6. Server appends file paths to message content so agent can find them.

Both pending images and files are cleared after send.

---

## 10. Session management

### Create

`POST /sessions` with `{ profile_id }` → get `session_id`. Add to session list, switch to it, update URL hash, open WebSocket.

### Load

`GET /sessions/{id}` → render full message history by iterating `messages[]` and calling the appropriate `chat.js` helper for each message role/type. Then open WebSocket.

### Delete

`DELETE /sessions/{id}`. Remove from sidebar. If it was the active session, switch to most recent remaining session (or show empty state).

### Pin

`PATCH /sessions/{id}/pin` with `{ pinned: true/false }`. Re-render session list.

### Stop generation

`POST /sessions/{id}/stop` via `fetch()` (not WebSocket). Called when user clicks the stop button shown during streaming.

---

## 11. Profile switching (mid-session)

The agent can switch profiles via the `switch_profile` tool. When the client receives `profile_switched`:

1. Update the chat header to show the new profile name.
2. The session object is not recreated — only the profile indicator changes.
3. No WebSocket reconnect needed — the server handles it transparently.

---

## 12. Context token indicator

`stream_end` includes `context_tokens` and `max_context_tokens`. The existing client does not render a visible progress bar, but these values are available for use. Compression fires automatically at ≥80% fill and emits `context_compressed`.

---

## 13. Design system

### Color palette (CSS custom properties)

```css
--bg-primary:   #1a1a1a   /* Page background */
--bg-secondary: #252525   /* Sidebar, cards */
--bg-tertiary:  #2d2d2d   /* Input field, hover states */
--bg-elevated:  #333333   /* Dropdowns, tooltips */

--text-primary:   #e8e8e8
--text-secondary: #a0a0a0
--text-muted:     #666666

--accent:         #6b8cff   /* Primary action color */
--accent-hover:   #5a7ae8

--success: #4caf50
--error:   #f44336
--warning: #ff9800

--border: #383838
--border-focus: #6b8cff

--radius-sm: 6px
--radius-md: 12px
--radius-lg: 16px
```

### Typography

- Font: system-ui stack (`-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif`)
- Base size: 15px
- Monospace (code blocks): `"JetBrains Mono", "Fira Code", Consolas, monospace`

### Message bubbles

- User messages: right-aligned, `--bg-elevated` background, rounded corners
- Assistant messages: left-aligned, no background (inline), full width
- Both have `--radius-lg` rounding

### Tool cards

- Background: `--bg-secondary`
- Left border accent: `--accent` (pending), `--success` or `--error` (completed)
- Collapsed by default: only tool name + icon + status visible
- Expanded: shows arguments block (monospace JSON) + result block

### Thinking cards

- Background: slightly different from tool cards (`--bg-tertiary`)
- Italic text style to visually distinguish from response
- Auto-collapsed on `thinking_end`

### Scrollbar

Custom thin scrollbar via `::-webkit-scrollbar` (4px wide, rounded, `--bg-tertiary` track, `--border` thumb).

---

## 14. Markdown rendering

- Library: `marked.js` (latest, via esm.sh CDN)
- Sanitization: none (trusted source — local agent only)
- Code highlighting: `highlight.js` with `github-dark` theme
- Code blocks: rendered with language label + copy button
- Applied to: assistant message content only (user messages shown as plain text)

---

## 15. Initialization sequence

```
1. Fetch GET /agents/profiles → populate profile selector
2. Fetch GET /sessions       → populate session list
3. Read location.hash
   ├── Hash present & matches a known session_id → load that session
   └── No hash or unknown → load sessions[0] (most recent)
4. loadSession(id):
   ├── GET /sessions/{id} → render message history
   ├── Update location.hash to #id
   ├── Connect WebSocket ws://host/ws/sessions/{id}
   └── Restore draft from localStorage
```

---

## 16. Known limitations / improvement areas

- No responsive/mobile layout
- No keyboard shortcuts (except Enter to send)
- No search across sessions or messages
- No context token progress bar (data available but not shown)
- Session names are raw UUID substrings (no user-editable titles)
- No light theme
- No session rename or export
- File attachment only works through the file picker (drag-and-drop not implemented)
- Error handling is minimal (network errors logged to console)
- No reconnection logic on WebSocket disconnect
