Newer
Older
navi-1 / webclient / docs / current-client-spec.md

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...]    │ │
│                          │  └───────────────────────┘ │
└──────────────────────────────────────────────────────┘
  • 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:

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):

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 (appendPendingToolCardfinalizeToolCard)

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:

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)

--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