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

Navi Webclient — Technical Specification

1. Stack

Concern Solution
Framework Vue 3 (Composition API)
Build Vite
Language JavaScript (ES modules)
State Pinia
UI kit gnexus-ui-kit (SCSS source + JS utilities)
Markdown marked.js
Syntax highlight highlight.js + summerfruit-dark theme (from UI kit)
Icons Phosphor Icons (font, from UI kit)
Virtual scroll vue-virtual-scroller
Deployment Vite builds to dist/, served via FastAPI /static/

2. Project structure

navi-webclient/
├── index.html
├── vite.config.js
├── package.json
├── public/
│   └── assets/          # copied from gnexus-ui-kit/public/assets (fonts, icons)
├── src/
│   ├── main.js           # app entry: Vue, Pinia, global styles
│   ├── App.vue           # root: layout shell, sidebar + chat area
│   ├── components/
│   │   ├── sidebar/
│   │   │   ├── AppSidebar.vue
│   │   │   ├── SessionList.vue
│   │   │   └── SessionItem.vue
│   │   ├── chat/
│   │   │   ├── ChatArea.vue
│   │   │   ├── ChatHeader.vue
│   │   │   ├── MessageList.vue
│   │   │   ├── InputBar.vue
│   │   │   └── FilePreviewStrip.vue
│   │   ├── messages/
│   │   │   ├── UserMessage.vue
│   │   │   ├── AssistantMessage.vue
│   │   │   ├── ThinkingCard.vue
│   │   │   ├── ToolCard.vue
│   │   │   ├── SubagentStep.vue
│   │   │   ├── SummaryCard.vue
│   │   │   └── CompressionNotice.vue
│   │   └── ui/
│   │       ├── WelcomeScreen.vue
│   │       ├── ContextBar.vue     # token indicator
│   │       └── ProfileBadge.vue
│   ├── stores/
│   │   ├── sessions.js
│   │   ├── profiles.js
│   │   └── chat.js
│   ├── composables/
│   │   ├── useWebSocket.js        # WS connection lifecycle
│   │   └── useFileUpload.js       # drag-drop + file/image handling
│   ├── api/
│   │   └── index.js               # REST API wrapper (fetch-based)
│   └── styles/
│       ├── main.scss              # imports kit + overrides
│       └── overrides.scss         # project-specific deviations from kit

3. Routing

Hash-based, no router library needed:

  • /# or /#<session_id> — load session by ID
  • On app init: read location.hash → load session or show welcome
  • On session switch: location.hash = session_id (no page reload)
  • hashchange event → switch active session

4. Pinia stores

useSessionsStore

state: {
  sessions: [],          // Session[] — full list from GET /sessions
  currentId: null,       // active session_id
}
actions:
  fetchSessions()        // GET /sessions
  createSession(profileId)  // POST /sessions
  deleteSession(id)      // DELETE /sessions/{id}
  pinSession(id, pinned) // PATCH /sessions/{id}/pin
  setCurrentId(id)

Designed to support pagination later: sessions will become a partial list when the API adds pagination. Keep fetch logic isolated here.

useProfilesStore

state: {
  profiles: [],          // AgentProfile[]
  selectedProfileId: null
}
actions:
  fetchProfiles()        // GET /agents/profiles

useChatStore

state: {
  messages: [],          // rendered message list for current session
  streaming: false,
  streamingMessageId: null,
  pendingImages: [],     // base64 strings
  pendingFiles: [],      // uploaded file objects {name, path, size}
  contextTokens: 0,
  maxContextTokens: 0,
}
actions:
  loadSession(id)        // GET /sessions/{id} → populate messages
  clearChat()
  appendMessage(msg)
  updateStreamingMessage(delta)
  finalizeStreaming(data)
  setContext(tokens, max)

5. WebSocket — useWebSocket.js

Composable (not a store) — manages one connection per active session.

Connection lifecycle

connect(sessionId)
  → open ws://localhost:8000/ws/sessions/{id}
  → on open: clear reconnect state
  → on message: dispatch to chat store
  → on close/error: start reconnect sequence

Reconnect strategy

  1. Attempt 1–3: retry with 1s / 2s / 4s backoff (exponential)
  2. After 3 failures: show toast "Connection lost. Reconnecting in background…"
  3. Continue retrying every 15s silently
  4. On success: dismiss toast, continue normally

Auth extension point

// composables/useWebSocket.js
function getAuthHeaders() {
  // TODO: return auth token when multi-user is ready
  return {};
}

The WS URL and any future auth token injection go through this single function.

WS event dispatch (mirrors existing client)

Event Action
stream_start chat.streaming = true, create streaming message slot
thinking_delta append to thinking card text
thinking_end collapse thinking card
turn_thinking render complete thinking block (collapsed)
plan_ready render plan card (collapsed)
tool_started create pending tool card with spinner
tool_call finalize tool card (result, success/failure color)
stream_delta append to streaming bubble, re-render markdown
stream_end finalize bubble, streaming = false, update context tokens
stream_stopped streaming = false, re-enable input
profile_switched update active profile in header
context_compressed append compression notice
error append inline error message to chat

6. Component details

AppSidebar.vue

  • Desktop: fixed panel, 280px wide
  • Mobile: full-screen drawer (uses gnexus-ui-kit Drawer component)
  • Toggle button in ChatHeader on mobile
  • Contains: logo + app name, profile <select>, "+ New Chat" button, SessionList

SessionList.vue

Uses vue-virtual-scroller (RecycleScroller) for performance. Each item is a SessionItem.

  • Pinned sessions rendered first (server sorts them)
  • Active session highlighted
  • Per-item: session preview text, pin icon, delete button
  • Draft saved to localStorage keyed by session_id (restored on session switch)

MessageList.vue

Uses vue-virtual-scroller with variable item height (DynamicScroller).

  • Auto-scroll to bottom during streaming
  • Maintains scroll position when switching sessions
  • Scroll anchor: when user scrolls up manually, stop auto-scroll; resume on new message if within 100px of bottom

Message type routing:

// each item in messages[] has a `role` and optional `type`
// role: 'user' | 'assistant' | 'system'
// type: 'summary' | 'compression_notice' | undefined

ThinkingCard.vue

  • <details> element
  • Auto-closes on thinking_end
  • Italic text style, distinct background from tool cards
  • Re-openable

ToolCard.vue

  • Two phases: pending (spinner) → finalized (result)
  • Left border accent: kit's $color-secondary (pending) → $color-success / $color-error
  • Collapsed by default, expand to see args (JSON, monospace) + result
  • Tool icon from TOOL_ICONS map; unknown → ph-wrench
  • Subagent steps (SubagentStep.vue) nested inside spawn_agent cards

InputBar.vue

  • Auto-resize textarea (1–6 rows)
  • Enter → send, Shift+Enter → newline
  • Disabled during streaming (show stop button instead of send)
  • File attach button → hidden <input type="file" multiple>
  • Drag-and-drop: dragover/drop on the entire ChatArea — highlight drop zone on drag enter
  • Image paste: intercept paste event → base64
  • Stop button → POST /sessions/{id}/stop
  • Upload progress bar (shown during file upload)

ContextBar.vue

Shown in ChatHeader. Visible once first stream_end received.

[████████░░░░] 12,450 / 32,000 tokens (38%)
  • Bar fill: color transitions: green → yellow (>60%) → orange (>80%)
  • Compression fires at ≥80% (server-side), show visual warning
  • Tooltip on hover with exact numbers

WelcomeScreen.vue

Shown when no sessions exist (first launch). Contains:

  • Navi logo + name
  • Tagline (e.g. "Your personal AI assistant")
  • Profile cards: one per active profile, icon + name + short description
  • "Start a conversation" button → creates session with selected profile → loads chat

7. gnexus-ui-kit integration

SCSS

Import kit source directly (not compiled) so updates propagate:

// src/styles/main.scss
@use '../../node_modules/gnexus-ui-kit/src/scss/kit' as *;
@use './overrides';

The kit is added as a git submodule or npm-linked local package. Do not copy-paste kit SCSS into this project.

Override only via src/styles/overrides.scss — never edit kit files directly.

JS utilities

Called imperatively where needed, not wrapped in Vue components:

// example: show toast from any composable or component
import { Toasts } from 'gnexus-ui-kit'
Toasts.createError('Connection lost', 'Reconnecting…').show()

Components that require DOM init (Accordion, etc.) call .init() in onMounted.

Icons

<i class="ph ph-paper-plane-tilt"></i>  <!-- send -->
<i class="ph ph-sidebar"></i>            <!-- toggle sidebar -->
<i class="ph ph-stop-circle"></i>        <!-- stop generation -->

8. Responsive layout

Desktop (≥768px)

┌─────────────┬──────────────────────────────┐
│  Sidebar    │  Chat area                   │
│  280px      │  flex: 1                     │
│  fixed      │                              │
└─────────────┴──────────────────────────────┘

Mobile (<768px)

┌──────────────────────────────────────────┐
│  [☰]  Session title          [tokens]   │  ← ChatHeader
├──────────────────────────────────────────┤
│                                          │
│  Messages                                │
│                                          │
├──────────────────────────────────────────┤
│  [📎]  [textarea...]          [send ↑]  │  ← InputBar
└──────────────────────────────────────────┘

Sidebar: full-screen drawer, opens over content

9. Animations

Using kit motion tokens ($motion-fast: 0.15s, $motion-base: 0.2s, $motion-slow: 0.28s).

Element Animation
Session switch Chat area fade-out → fade-in (0.15s)
New message appear Slide up + fade in (0.2s)
Thinking/tool card expand Height transition via <details> + CSS (0.2s)
Sidebar drawer (mobile) Slide in from left (0.28s ease)
Toast Kit's built-in a-show / a-hide classes
Streaming text No animation — raw append for performance

Vue <Transition> components used for session switch and message appearance.


10. Error handling

Error type Display
Network / WS disconnect Toast (kit Toasts.createError)
WS reconnect failed (3 attempts) Persistent toast with status
Agent error event from server Inline in chat (red error bubble)
REST API failure (create session, delete, etc.) Toast
File upload failure Toast + remove file from preview strip

11. File and image attachment

Images

  1. Paste (paste event) or file picker (image types)
  2. FileReader.readAsDataURL() → base64 → pendingImages[]
  3. Preview thumbnail in FilePreviewStrip with remove button
  4. On send: included in WS message { images: [...base64] }

Files

  1. File picker (any type) or drag-and-drop onto ChatArea
  2. Upload via POST /sessions/{id}/files (multipart)
  3. Response { name, path, size, content_type }pendingFiles[]
  4. Preview pill in FilePreviewStrip
  5. On send: included in WS message { files: [...] }

Drag-and-drop: visual highlight on ChatArea when dragging over, handled in useFileUpload.js composable shared between drag events and the attach button.


12. Build and deployment

// vite.config.js
export default {
  build: {
    outDir: '../navi-1/client',   // outputs directly into FastAPI's static dir
    emptyOutDir: true
  }
}

FastAPI serves the built files from GET /static/**. index.html is served from GET / by the existing catch-all route.


13. Initialization sequence

1. main.js → mount App, init Pinia
2. App.vue onMounted:
   a. profilesStore.fetchProfiles()    → populate sidebar profile selector
   b. sessionsStore.fetchSessions()    → populate session list
   c. Read location.hash
      ├─ valid session_id → chatStore.loadSession(id) → connect WS
      └─ no sessions at all → show WelcomeScreen
3. loadSession(id):
   a. GET /sessions/{id} → render message history
   b. Update location.hash
   c. useWebSocket.connect(id)
   d. Restore draft from localStorage

14. Out of scope (this version)

  • Keyboard shortcuts (planned for later)
  • Session search/filter (server-side feature, planned)
  • Authentication UI (extension point in code only)
  • Light theme
  • Session rename / export
  • Message search