Files
spiceflow/PRD.md
Adam Jeniski 66f072a5e6 Add comprehensive PRD and terminal UX improvements
- Add PRD.md with detailed product requirements documentation
- Unify tmux screen size presets to consistent 24-row height
- Add Ctrl+Down keyboard shortcut to scroll terminal to bottom

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 18:04:26 -05:00

45 KiB

Spiceflow Product Requirements Document

Executive Summary

Spiceflow is an AI Session Orchestration Progressive Web App (PWA) for monitoring and interacting with Claude Code and OpenCode CLI sessions from mobile devices or web browsers. It provides a unified interface to manage multiple AI coding assistant sessions, handle permission requests, and interact with tmux terminal sessions.


Table of Contents

  1. Product Overview
  2. System Architecture
  3. Data Models
  4. Session Management
  5. Messaging System
  6. Permission System
  7. Terminal Integration
  8. Push Notifications
  9. API Specification
  10. WebSocket Protocol
  11. User Interface
  12. User Flows
  13. Security Considerations
  14. Performance Requirements

1. Product Overview

1.1 Purpose

Spiceflow bridges the gap between AI coding assistants (Claude Code, OpenCode) and mobile/web interfaces, enabling developers to:

  • Monitor AI coding sessions from any device
  • Respond to permission requests remotely
  • Manage multiple concurrent sessions
  • Interact with shell terminals via tmux

1.2 Supported Providers

Provider Type Interaction Model Permission Handling
Claude Code AI Coding Assistant Chat-based with streaming Manual or Auto-accept
OpenCode AI Coding Assistant Chat-based with streaming Auto-approved by CLI
tmux Terminal Multiplexer Direct shell access N/A

1.3 Key Features

  • Real-time Streaming: Live display of AI responses via WebSocket
  • Permission Interception: Approve, deny, or redirect file/command operations
  • Auto-Accept Edits: Automatic approval of Write/Edit operations for Claude
  • Terminal Emulation: Full tmux session control with keyboard input
  • Push Notifications: Web push alerts for pending permissions
  • PWA Support: Installable app with offline capability

2. System Architecture

2.1 High-Level Architecture

┌─────────────────────────────────────────────────────────────────┐
│                        PWA Client (SvelteKit)                    │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────────────┐ │
│  │ Sessions │  │ Messages │  │ Terminal │  │ Push Notif Store │ │
│  │  Store   │  │  Store   │  │   View   │  │                  │ │
│  └────┬─────┘  └────┬─────┘  └────┬─────┘  └────────┬─────────┘ │
│       │             │             │                  │           │
│       └─────────────┴─────────────┴──────────────────┘           │
│                              │                                   │
│                    ┌─────────┴─────────┐                         │
│                    │   API Client      │                         │
│                    │   WebSocket       │                         │
│                    └─────────┬─────────┘                         │
└──────────────────────────────┼───────────────────────────────────┘
                               │ HTTP / WebSocket
┌──────────────────────────────┼───────────────────────────────────┐
│                    Spiceflow Server (Clojure)                    │
│  ┌─────────────────┬─────────┴─────────┬─────────────────────┐   │
│  │   HTTP Routes   │    WebSocket      │   Push Sender       │   │
│  │   (Reitit)      │    Manager        │   (RFC 8291)        │   │
│  └────────┬────────┴─────────┬─────────┴──────────┬──────────┘   │
│           │                  │                    │              │
│  ┌────────┴──────────────────┴────────────────────┴──────────┐   │
│  │                    Session Manager                         │   │
│  │  ┌──────────────────────────────────────────────────────┐ │   │
│  │  │ Active Processes (ConcurrentHashMap)                 │ │   │
│  │  │ session-id → {process, stdin, stdout, stderr}        │ │   │
│  │  └──────────────────────────────────────────────────────┘ │   │
│  └────────┬──────────────────┬────────────────────┬──────────┘   │
│           │                  │                    │              │
│  ┌────────┴────────┐ ┌───────┴───────┐ ┌─────────┴─────────┐    │
│  │ Claude Adapter  │ │OpenCode Adapt.│ │   Tmux Adapter    │    │
│  │ (JSONL stream)  │ │(JSON stream)  │ │ (raw terminal)    │    │
│  └────────┬────────┘ └───────┬───────┘ └─────────┬─────────┘    │
│           │                  │                   │               │
│  ┌────────┴──────────────────┴───────────────────┴──────────┐   │
│  │                    DataStore (SQLite)                     │   │
│  │  sessions | messages | push_subscriptions | vapid_keys    │   │
│  └───────────────────────────────────────────────────────────┘   │
└──────────────────────────────────────────────────────────────────┘
                               │
        ┌──────────────────────┼──────────────────────┐
        │                      │                      │
┌───────┴───────┐      ┌───────┴───────┐      ┌───────┴───────┐
│  Claude Code  │      │   OpenCode    │      │     tmux      │
│     CLI       │      │     CLI       │      │   sessions    │
└───────────────┘      └───────────────┘      └───────────────┘

2.2 Technology Stack

Layer Technology Purpose
Frontend SvelteKit 2.5, Svelte 4 PWA framework
Styling Tailwind CSS Utility-first CSS
Build Vite, vite-plugin-pwa Asset bundling, service worker
Backend Clojure 1.11, Ring/Jetty HTTP server
Routing Reitit REST API routing
Database SQLite via next.jdbc Persistent storage
State Mount Component lifecycle
Testing Kaocha (unit), Playwright (E2E) Test frameworks

2.3 Data Flow

User Input → HTTP POST → Session Manager → Adapter → CLI stdin
                                                        │
CLI stdout → Adapter.read-stream → Session Manager ─────┤
                                         │              │
                              ┌──────────┴──────────┐   │
                              │                     │   │
                         Save to DB          WebSocket Broadcast
                              │                     │
                              └──────────┬──────────┘
                                         │
                                    PWA Client

3. Data Models

3.1 Session

interface Session {
  id: string;                    // UUID (DB sessions) or tmux name
  provider: 'claude' | 'opencode' | 'tmux';
  external_id?: string;          // CLI-specific session identifier
  title?: string;                // User-friendly name
  working_dir?: string;          // Current working directory
  spawn_dir?: string;            // Original spawn directory
  status: 'idle' | 'processing' | 'awaiting-permission';
  pending_permission?: PermissionRequest;  // JSON blob
  auto_accept_edits?: boolean;   // Auto-approve Write/Edit (Claude only)
  created_at: string;            // ISO timestamp
  updated_at: string;            // ISO timestamp
}

Status State Machine:

          ┌─────────────────────────────────┐
          │                                 │
          ▼                                 │
       ┌──────┐    send_message    ┌────────┴───┐
       │ idle │ ─────────────────▶ │ processing │
       └──────┘                    └─────┬──────┘
          ▲                              │
          │                              │
          │ stream_ends                  │ permission_denied
          │ (no pending)                 │
          │                              ▼
          │                    ┌─────────────────────┐
          │                    │ awaiting-permission │
          │                    └──────────┬──────────┘
          │                               │
          │        respond_to_permission  │
          └───────────────────────────────┘

3.2 Message

interface Message {
  id: string;                    // UUID
  session_id: string;            // FK to sessions
  role: 'user' | 'assistant' | 'system';
  content: string;               // Message text
  metadata?: {
    type?: 'permission-request'; // Permission message
    status?: 'accept' | 'deny' | 'steer';  // Permission response
    auto_accepted?: boolean;     // Was auto-approved
    denials?: PermissionDenial[]; // Tool requests
    message_id?: string;         // For permission tracking
  };
  created_at: string;            // ISO timestamp
}

3.3 Permission Request

interface PermissionRequest {
  tools: string[];               // ['Write', 'Edit', 'Bash', etc.]
  denials: PermissionDenial[];   // Detailed tool requests
}

interface PermissionDenial {
  tool: string;                  // Tool name
  input: ToolInput;              // Tool-specific input
  description: string;           // Human-readable description
}

// Tool Input Types
interface WriteToolInput {
  file_path: string;
  content: string;
}

interface EditToolInput {
  file_path: string;
  old_string: string;
  new_string: string;
}

interface BashToolInput {
  command: string;
}

3.4 Push Subscription

interface PushSubscription {
  id: string;                    // UUID
  endpoint: string;              // Push service URL (unique)
  p256dh: string;                // Client ECDH public key (base64url)
  auth: string;                  // Client auth secret (base64url)
  user_agent?: string;           // Browser info
  created_at: string;            // ISO timestamp
}

3.5 VAPID Keys

interface VapidKeys {
  public_key: string;            // EC P-256 uncompressed point (base64url)
  private_key: string;           // Raw scalar (base64url)
}

4. Session Management

4.1 Session Lifecycle

4.1.1 Creation

Claude/OpenCode:

  1. Client POSTs to /api/sessions with {provider: 'claude'|'opencode'}
  2. Server generates UUID, saves to database
  3. Returns session with status: 'idle'

Tmux:

  1. Client POSTs to /api/sessions with {provider: 'tmux'}
  2. Server generates readable name: spiceflow-{adjective}-{noun}-{4digits}
  3. Creates tmux session via tmux new-session -d -s {name}
  4. Sets up output capture via pipe-pane
  5. Returns session (ID = tmux session name)

4.1.2 Message Sending

send_message_to_session(session_id, message)
  │
  ├─▶ Save user message to database
  │
  ├─▶ Session not running?
  │     └─▶ start_session(session_id)
  │           ├─▶ Get adapter for provider
  │           ├─▶ adapter.spawn_session(session_id, opts)
  │           └─▶ Store handle in active_processes map
  │
  ├─▶ adapter.send_message(handle, message)
  │
  └─▶ Return handle for streaming

4.1.3 Response Streaming

stream_session_response(session_id, callback)
  │
  ├─▶ Get session and active handle
  │
  ├─▶ adapter.read_stream(handle, event_callback)
  │     │
  │     ├─▶ On 'init': Extract external_id, spawn_dir
  │     │
  │     ├─▶ On 'content-delta':
  │     │     ├─▶ Accumulate text
  │     │     └─▶ Broadcast via WebSocket
  │     │
  │     ├─▶ On 'result':
  │     │     ├─▶ Save assistant message
  │     │     ├─▶ Check for permission_denials
  │     │     │     ├─▶ If auto-accept eligible:
  │     │     │     │     ├─▶ Mark message status='accept'
  │     │     │     │     ├─▶ Broadcast with auto_accepted=true
  │     │     │     │     └─▶ Recurse after responding
  │     │     │     └─▶ Else:
  │     │     │           ├─▶ Set pending_permission
  │     │     │           ├─▶ Broadcast permission-request
  │     │     │           └─▶ Schedule push notification (15s)
  │     │     └─▶ Extract working_dir from tool results
  │     │
  │     └─▶ On 'message-stop': Finalize
  │
  └─▶ On stream end:
        ├─▶ If auto-accepted: respond_to_permission(:accept)
        ├─▶ Update session status
        └─▶ Remove from active_processes

4.1.4 Session Discovery

Claude Code:

  • Scans ~/.claude/projects/*/sessions/*.json
  • Decodes URL-encoded paths from filenames
  • Returns list of discovered sessions with external IDs

Tmux:

  • Runs tmux list-sessions -F "#{session_name}:#{pane_current_path}"
  • Filters for spiceflow-* prefix
  • Returns managed sessions only

4.2 Process Handle Structure

{:process   java.lang.Process      ; JVM process reference
 :stdin     java.io.BufferedWriter ; Write messages here
 :stdout    java.io.BufferedReader ; Read JSONL/output here
 :stderr    java.io.BufferedReader ; Error stream
 ;; Tmux-specific:
 :session-name  string             ; Tmux session name
 :output-file   string             ; Pipe-pane log path
 :end-marker    string             ; Command completion marker
 :marker-id     string             ; UUID for marker
 :original-cmd  string             ; User's command (for filtering)
}

4.3 Adapter Protocol

(defprotocol AgentAdapter
  (provider-name [this])
  ;; Returns :claude, :opencode, or :tmux

  (discover-sessions [this])
  ;; Returns [{:external-id :working-dir :title}]

  (spawn-session [this session-id opts])
  ;; Creates CLI process, returns handle

  (send-message [this handle message])
  ;; Writes to stdin, returns handle (possibly updated)

  (read-stream [this handle callback])
  ;; Reads output, calls callback(event) for each

  (kill-process [this handle])
  ;; Terminates process

  (parse-output [this line]))
  ;; Parses output line to event map

5. Messaging System

5.1 Message Flow

┌─────────┐     POST /send      ┌─────────┐      stdin      ┌─────────┐
│  Client │ ──────────────────▶ │  Server │ ──────────────▶ │   CLI   │
└─────────┘                     └─────────┘                 └────┬────┘
     ▲                               │                           │
     │                               │                           │ stdout
     │                          Save user                        │ (JSONL)
     │                          message                          │
     │                               │                           ▼
     │                               │                     ┌─────────┐
     │      WebSocket               ┌┴┐                    │ Adapter │
     │    content-delta         ◀───┤ │◀───────────────────│  Parse  │
     │    message-stop              └┬┘                    └─────────┘
     │    permission-request         │
     │                               │
     │                          Save assistant
     │                          message
     └───────────────────────────────┘

5.2 Stream Event Types

Event Source Payload Purpose
init CLI startup {session-id, cwd} Session initialized
content-delta Assistant response {text} Incremental text
message-stop Response complete {} Stream finalized
permission-request Permission denied {permission-request, message-id, message} Tool blocked
working-dir-update Directory change {working-dir} CWD updated
terminal-update Tmux output {content, diff} Terminal changed
error Any failure {message} Error occurred

5.3 JSONL Parsing (Claude)

{"type":"system","subtype":"init","session_id":"abc","cwd":"/home/user"}
{"type":"assistant","message":{"content":[{"type":"text","text":"Hello"}]}}
{"type":"result","result":"success","permission_denials":[...]}

Event Mapping:

  • system.init:init
  • assistant:content-delta (aggregated)
  • result:result with permission extraction

5.4 JSON Parsing (OpenCode)

{"type":"step_start","sessionID":"abc","cwd":"/home/user"}
{"type":"text","content":"Hello"}
{"type":"step_finish","reason":"complete"}

Event Mapping:

  • step_start:init
  • text:content-delta
  • step_finish:result

6. Permission System

6.1 Permission Flow

┌─────────────────────────────────────────────────────────────────┐
│                    Permission Request Flow                       │
└─────────────────────────────────────────────────────────────────┘

CLI requests tool ──▶ Server detects denial ──▶ Check auto-accept
                                                       │
                          ┌────────────────────────────┴────────┐
                          │                                     │
                    Auto-accept?                           Manual
                          │                                     │
                    ┌─────┴─────┐                               │
                    │           │                               │
               Only Write/   Other                              │
               Edit tools    tools                              │
                    │           │                               │
                    ▼           │                               │
              Auto-accept       └───────────────────────────────┤
              immediately                                       │
                    │                                           │
                    │                                           ▼
                    │                               ┌───────────────────┐
                    │                               │ Set pending_perm  │
                    │                               │ Broadcast to WS   │
                    │                               │ Schedule push     │
                    │                               └─────────┬─────────┘
                    │                                         │
                    │                                         ▼
                    │                               ┌───────────────────┐
                    │                               │ Client displays   │
                    │                               │ permission UI     │
                    │                               └─────────┬─────────┘
                    │                                         │
                    │                    User responds: Accept/Deny/Steer
                    │                                         │
                    │                                         ▼
                    ▼                               ┌───────────────────┐
              ┌───────────────────┐                 │ POST /permission  │
              │ respond_to_perm   │◀────────────────│ {response, msg}   │
              │ :accept, nil      │                 └───────────────────┘
              └─────────┬─────────┘
                        │
                        ▼
              ┌───────────────────┐
              │ Spawn new process │
              │ --resume          │
              │ --allowedTools    │
              └─────────┬─────────┘
                        │
                        ▼
              ┌───────────────────┐
              │ Send message:     │
              │ "continue" (accept)│
              │ "denied" (deny)   │
              │ user text (steer) │
              └─────────┬─────────┘
                        │
                        ▼
              ┌───────────────────┐
              │ Stream new response│
              └───────────────────┘

6.2 Auto-Accept Logic

(defn should-auto-accept? [session perm-req]
  (and (:auto-accept-edits session)
       (every? #{"Write" "Edit"} (:tools perm-req))))

Eligible Tools:

  • Write - File creation
  • Edit - File modification

Ineligible Tools:

  • Bash - Shell commands
  • WebFetch - HTTP requests
  • WebSearch - Web searches
  • NotebookEdit - Jupyter notebooks
  • Task - Sub-agent spawning
  • Skill - Skill invocation

6.3 Permission Response Types

Response Message Sent Behavior
accept "continue" Grants all requested tools
deny "Permission denied. Find another approach without using that tool." Rejects request
steer User-provided text Redirects with instructions

6.4 Permission Message Recording

Permissions are recorded as assistant messages with metadata:

{
  role: 'assistant',
  content: 'I need permission to...',
  metadata: {
    type: 'permission-request',
    status: 'accept' | 'deny' | 'steer',
    auto_accepted?: true,
    denials: [...],
    message_id: 'uuid'
  }
}

7. Terminal Integration

7.1 Tmux Session Management

7.1.1 Session Naming

  • Format: spiceflow-{adjective}-{noun}-{4digits}
  • Examples: spiceflow-brave-fox-0042, spiceflow-calm-owl-1337
  • Purpose: Human-readable, unique identifiers

7.1.2 Output Capture

# Create session
tmux new-session -d -s {session-name} -c {working-dir}

# Capture output to file
tmux pipe-pane -t {session-name} "cat >> /tmp/spiceflow-tmux-{name}.log"

# Capture visible pane
tmux capture-pane -t {session-name} -p -e -S -1000

7.2 Terminal Diff System

7.2.1 Diff Types

Type When Used Payload
full Initial load, >50% changed {lines: [...], hash, frame-id}
diff Partial update {changes: {line: content}, total-lines, hash, frame-id}
unchanged No changes {hash, frame-id}

7.2.2 Frame Ordering

  • Frame ID: Auto-incrementing counter (wraps at 2^53-1)
  • Purpose: Prevent out-of-order updates from network delays
  • Client Logic: Only apply frames with ID > last frame ID

7.2.3 Change Detection

(defn compute-line-diff [old-lines new-lines]
  ;; Compare line-by-line
  ;; Return map of {line-num -> new-content}
  ;; If >50% changed, return nil (signal full refresh))

7.3 Input Handling

7.3.1 Key Mapping

Input tmux send-keys
Regular text -l "{text}" (literal)
Enter Enter (key name)
Newline -l "\n"
Ctrl+C \x03 (raw)
Ctrl+D \x04 (raw)
Tab \t
Escape \x1b
Arrow keys \x1b[A/B/C/D

7.3.2 Input Batching

  • Client-side: 30ms batching for rapid keystrokes
  • Server-side: Immediate send to tmux
  • Feedback: 100ms delayed terminal capture after input

7.4 Screen Size Presets

Mode Dimensions Use Case
portrait 40x24 Phone portrait
landscape 65x24 Phone landscape
desktop 100x24 Split screen
fullscreen 180x24 Full terminal

8. Push Notifications

8.1 VAPID Authentication (RFC 8292)

8.1.1 Key Generation

;; Generate EC P-256 key pair
(defn generate-keypair []
  {:public-key  ;; Uncompressed point (0x04 || x || y), base64url
   :private-key ;; Raw scalar (32 bytes), base64url
  })

8.1.2 JWT Structure

{
  "aud": "https://push.service.com",
  "exp": 1234567890,
  "sub": "mailto:admin@example.com"
}

Signature: ES256 (ECDSA P-256 + SHA-256)

8.2 Web Push Encryption (RFC 8291)

8.2.1 Encryption Flow

1. Decode client keys (p256dh, auth) from base64url
2. Generate ephemeral server EC key pair
3. Generate random 16-byte salt
4. ECDH with client public key → shared secret
5. HKDF-extract with auth secret
6. HKDF-expand for CEK (16 bytes) and nonce (12 bytes)
7. Pad plaintext (2-byte length prefix)
8. AES-128-GCM encrypt

8.2.2 Message Format

salt (16) || record_size (4) || keyid_len (1) || keyid (65) || ciphertext

8.3 Notification Payload

{
  "title": "Permission Required",
  "body": "Claude needs permission for: Write file.txt",
  "sessionId": "uuid",
  "sessionTitle": "My Session",
  "tools": ["Write"]
}

8.4 Notification Timing

  • Delay: 15 seconds after permission request
  • Condition: Permission still pending (same message-id)
  • Auto-cleanup: Subscriptions removed on 404/410 response

9. API Specification

9.1 Session Endpoints

GET /api/sessions

Response:

[
  {
    "id": "uuid",
    "provider": "claude",
    "title": "My Session",
    "status": "idle",
    "working-dir": "/home/user/project",
    "auto-accept-edits": false,
    "created-at": "2024-01-01T00:00:00Z",
    "updated-at": "2024-01-01T00:00:00Z"
  }
]

POST /api/sessions

Request:

{
  "provider": "claude" | "opencode" | "tmux",
  "working-dir": "/optional/path"
}

Response: Created session object

GET /api/sessions/:id

Response:

{
  "id": "uuid",
  "provider": "claude",
  "messages": [
    {
      "id": "uuid",
      "role": "user",
      "content": "Hello",
      "created-at": "2024-01-01T00:00:00Z"
    }
  ],
  "pending-permission": null
}

PATCH /api/sessions/:id

Request:

{
  "title": "New Title",
  "auto-accept-edits": true
}

Response: Updated session object

DELETE /api/sessions/:id

Response: 204 No Content

9.2 Message Endpoints

POST /api/sessions/:id/send

Request:

{
  "message": "User message text"
}

Response:

{
  "status": "sent"
}

POST /api/sessions/:id/permission

Request:

{
  "response": "accept" | "deny" | "steer",
  "message": "Optional steer instructions"
}

Response:

{
  "status": "permission-response-sent"
}

9.3 Terminal Endpoints

GET /api/sessions/:id/terminal

Query Parameters:

  • fresh=true - Invalidate cache

Response:

{
  "content": "terminal output...",
  "alive": true,
  "session-name": "spiceflow-brave-fox-0042",
  "layout": "landscape",
  "diff": {
    "type": "diff",
    "changes": {"5": "new line content"},
    "total-lines": 24,
    "hash": 12345,
    "frame-id": 100
  }
}

POST /api/sessions/:id/terminal/input

Request:

{
  "input": "ls -la\r"
}

Response:

{
  "status": "sent"
}

POST /api/sessions/:id/terminal/resize

Request:

{
  "mode": "landscape" | "portrait" | "desktop" | "fullscreen"
}

Response:

{
  "status": "resized",
  "mode": "landscape"
}

9.4 Push Endpoints

GET /api/push/vapid-key

Response:

{
  "publicKey": "base64url-encoded-public-key"
}

POST /api/push/subscribe

Request:

{
  "endpoint": "https://push.service.com/...",
  "keys": {
    "p256dh": "base64url-client-key",
    "auth": "base64url-auth-secret"
  }
}

Response:

{
  "id": "subscription-uuid"
}

POST /api/push/unsubscribe

Request:

{
  "endpoint": "https://push.service.com/..."
}

Response: 204 No Content

9.5 Utility Endpoints

GET /api/health

Response:

{
  "status": "ok",
  "service": "spiceflow"
}

GET /api/tmux/external

Response:

[
  {
    "name": "my-session",
    "working-dir": "/home/user"
  }
]

POST /api/tmux/import

Request:

{
  "name": "my-session"
}

Response:

{
  "id": "spiceflow-my-session",
  "name": "spiceflow-my-session",
  "working-dir": "/home/user"
}

10. WebSocket Protocol

10.1 Connection

Endpoint: /api/ws

Upgrade: Standard WebSocket handshake

10.2 Client Messages

Subscribe to Session

{
  "type": "subscribe",
  "session-id": "uuid"
}

Unsubscribe from Session

{
  "type": "unsubscribe",
  "session-id": "uuid"
}

Heartbeat

{
  "type": "ping"
}

10.3 Server Messages

Connection Established

{
  "type": "connected"
}

Subscription Confirmed

{
  "type": "subscribed",
  "session-id": "uuid"
}

Content Delta

{
  "event": "content-delta",
  "session-id": "uuid",
  "text": "streamed text chunk"
}

Permission Request

{
  "event": "permission-request",
  "session-id": "uuid",
  "permission-request": {
    "tools": ["Write"],
    "denials": [...]
  },
  "message-id": "uuid",
  "message": {
    "id": "uuid",
    "content": "...",
    "metadata": {...}
  },
  "auto-accepted": false
}

Message Stop

{
  "event": "message-stop",
  "session-id": "uuid"
}

Terminal Update

{
  "event": "terminal-update",
  "session-id": "uuid",
  "content": "full terminal content",
  "diff": {...}
}

Working Directory Update

{
  "event": "working-dir-update",
  "session-id": "uuid",
  "working-dir": "/new/path"
}

Error

{
  "event": "error",
  "session-id": "uuid",
  "message": "Error description"
}

Pong

{
  "type": "pong"
}

10.4 Reconnection Strategy

  • Max Attempts: 5
  • Initial Delay: 1 second
  • Backoff: Exponential (1s, 2s, 4s, 8s, 16s)
  • Heartbeat: Ping every 25 seconds
  • Pong Timeout: 10 seconds

11. User Interface

11.1 Page Structure

Home Page (/)

┌─────────────────────────────────────────┐
│ ☰ Spiceflow                    🔔 + ↻  │
├─────────────────────────────────────────┤
│                                         │
│  ┌─────────────────────────────────┐   │
│  │ ● My Claude Session     claude  │   │
│  │   /home/user/project    2h ago  │   │
│  └─────────────────────────────────┘   │
│                                         │
│  ┌─────────────────────────────────┐   │
│  │ ○ Terminal Session      tmux    │   │
│  │   /home/user            1d ago  │   │
│  └─────────────────────────────────┘   │
│                                         │
└─────────────────────────────────────────┘

Elements:

  • Header with branding and action buttons
  • Session cards with status indicators
  • Provider badges (color-coded)
  • Relative timestamps

Session Page (/session/:id)

Chat Mode (Claude/OpenCode):

┌─────────────────────────────────────────┐
│ ← Session Title            ⚙️ CLAUDE   │
├─────────────────────────────────────────┤
│                                         │
│  ┌─────────────────────────────────┐   │
│  │ User: Hello                     │   │
│  └─────────────────────────────────┘   │
│                                         │
│  ┌─────────────────────────────────┐   │
│  │ Assistant: Hi! How can I help?  │   │
│  │ ● ● ●  (thinking)               │   │
│  └─────────────────────────────────┘   │
│                                         │
├─────────────────────────────────────────┤
│ [Type a message...              ] [➤]  │
└─────────────────────────────────────────┘

Terminal Mode (tmux):

┌─────────────────────────────────────────┐
│ ← brave-fox-0042           ⚙️ TERMINAL │
├─────────────────────────────────────────┤
│ $ pwd                                   │
│ /home/user                              │
│ $ ls -la                                │
│ total 32                                │
│ drwxr-xr-x  5 user user 4096 Jan 1 00:0│
│ -rw-r--r--  1 user user  123 Jan 1 00:0│
│ $                                       │
├─────────────────────────────────────────┤
│ [^] [^C] [^D] | [y] [n] | [1-4] | [⇥]  │
│ [-] 100% [+] | [📱] [📺] [🖥️] [⬜] [↓]  │
└─────────────────────────────────────────┘

11.2 Permission UI

┌─────────────────────────────────────────┐
│ ⚠️  Claude needs permission             │
├─────────────────────────────────────────┤
│                                         │
│  Write: /home/user/foo.md               │
│  ┌─────────────────────────────────┐   │
│  │ + 1  # My Haiku                 │   │
│  │ + 2                             │   │
│  │ + 3  Code flows like water      │   │
│  │ + 4  Tests catch bugs silently  │   │
│  │ + 5  Green lights bring peace   │   │
│  └─────────────────────────────────┘   │
│                                         │
│  [Accept]  [Deny]  [No, and...]        │
└─────────────────────────────────────────┘

11.3 Component Inventory

Component Purpose Props
SessionCard Session list item session
MessageList Chat history display messages, streamingContent, isThinking
InputBar Message input disabled, placeholder, autoFocus
PermissionRequest Permission approval UI permission, assistantName
FileDiff File change visualization tool, input, filePath
TerminalView Tmux display/input sessionId, autoScroll
SessionSettings Settings dropdown autoAcceptEdits, autoScroll, provider
PushToggle Notification toggle (uses store)

11.4 Responsive Behavior

Breakpoint Header Layout
Portrait Full header Vertical stack
Landscape mobile Hamburger menu Compact header
Desktop Full header All controls visible

12. User Flows

12.1 Create and Chat with Claude

  1. User opens app → sees session list
  2. User taps + button → provider menu appears
  3. User selects Claude Code → navigates to new session
  4. User sees "No messages yet" state
  5. User types message in input bar
  6. User taps send → message appears immediately (optimistic)
  7. Thinking indicator (●●●) appears
  8. Streaming text appears incrementally
  9. Response completes → thinking indicator disappears
  10. User can continue conversation

12.2 Handle Permission Request

  1. User sends message requesting file operation
  2. Claude attempts Write/Edit/Bash
  3. Permission UI appears with tool details
  4. User reviews file diff (for Write/Edit)
  5. User chooses:
    • Accept → grants permission, streaming continues
    • Deny → rejects, Claude finds alternative
    • No, and... → enters steer mode
  6. If steer: user types redirect instructions
  7. Permission UI disappears
  8. New response streams

12.3 Enable Auto-Accept

  1. User opens session with Claude
  2. User taps ⚙️ (settings gear)
  3. User enables Auto-accept edits checkbox
  4. Server saves preference (auto-accept-edits: true)
  5. Future Write/Edit operations auto-approved
  6. Permission messages appear with green ✓ status
  7. No interruption for file operations

12.4 Use Terminal Session

  1. User creates tmux session
  2. User sees terminal with prompt
  3. User taps terminal to focus
  4. User types command (keyboard or quick buttons)
  5. Input sent to tmux session
  6. Output appears in terminal view
  7. User can resize terminal (portrait/landscape/desktop/fullscreen)
  8. User can use quick buttons (Ctrl+C, y/n, etc.)

12.5 Receive Push Notification

  1. User enables push notifications (bell icon)
  2. Browser prompts for permission
  3. User grants permission
  4. Subscription saved to server
  5. Later: permission request pending for 15+ seconds
  6. Server sends push notification
  7. User's device shows notification
  8. User taps notification → app opens to session
  9. User sees permission UI, responds

13. Security Considerations

13.1 Permission Model

  • No auto-execute: All tool operations require explicit approval (or opt-in auto-accept)
  • Auto-accept scope: Limited to Write/Edit only, never Bash/network
  • Steer capability: User can redirect AI without accepting dangerous operations

13.2 Push Security

  • VAPID authentication: Server identity verified to push services
  • End-to-end encryption: RFC 8291 AES-128-GCM encryption
  • Subscription cleanup: Invalid subscriptions auto-removed

13.3 Data Protection

  • Local SQLite: Data stored locally on server
  • No cloud sync: Sessions not transmitted externally
  • Process isolation: CLI processes run with server user permissions

13.4 API Security

  • CORS: Configured for allowed origins
  • Input validation: Session IDs, permission responses validated
  • No secrets in URLs: Sensitive data in request bodies

14. Performance Requirements

14.1 Response Times

Operation Target Maximum
Page load <1s 3s
Session list <500ms 2s
Message send <100ms (optimistic) 1s
WebSocket connect <1s 5s
Terminal input <50ms 200ms

14.2 Streaming Performance

  • Content delta frequency: Real-time as received
  • Terminal diff threshold: >50% change triggers full refresh
  • Frame ordering: Prevents out-of-order updates

14.3 Resource Usage

  • Active processes: ConcurrentHashMap for thread-safe access
  • WebSocket connections: Efficient broadcast to subscribers only
  • Terminal cache: Per-session diff state

14.4 Scalability

  • Concurrent sessions: Limited by server resources
  • WebSocket subscribers: Multiple clients per session supported
  • Database: SQLite (single-writer, multiple-reader)

Appendix A: Configuration Reference

Variable Default Description
SPICEFLOW_PORT 3000 Server port
SPICEFLOW_HOST 0.0.0.0 Server host
SPICEFLOW_DB spiceflow.db SQLite database path
CLAUDE_SESSIONS_DIR ~/.claude/projects Claude sessions directory
OPENCODE_CMD opencode OpenCode binary name

Appendix B: Database Schema

CREATE TABLE session_statuses (
  id INTEGER PRIMARY KEY,
  name TEXT NOT NULL UNIQUE
);

INSERT INTO session_statuses VALUES
  (1, 'idle'), (2, 'processing'), (3, 'awaiting-permission');

CREATE TABLE sessions (
  id TEXT PRIMARY KEY,
  provider TEXT NOT NULL,
  external_id TEXT,
  title TEXT,
  working_dir TEXT,
  spawn_dir TEXT,
  status_id INTEGER DEFAULT 1 REFERENCES session_statuses(id),
  pending_permission TEXT,
  auto_accept_edits INTEGER DEFAULT 0,
  created_at TEXT NOT NULL,
  updated_at TEXT NOT NULL
);

CREATE TABLE messages (
  id TEXT PRIMARY KEY,
  session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
  role TEXT NOT NULL,
  content TEXT NOT NULL,
  metadata TEXT,
  created_at TEXT NOT NULL
);

CREATE TABLE push_subscriptions (
  id TEXT PRIMARY KEY,
  endpoint TEXT NOT NULL UNIQUE,
  p256dh TEXT NOT NULL,
  auth TEXT NOT NULL,
  user_agent TEXT,
  created_at TEXT NOT NULL
);

CREATE TABLE vapid_keys (
  id INTEGER PRIMARY KEY CHECK (id = 1),
  public_key TEXT NOT NULL,
  private_key TEXT NOT NULL,
  created_at TEXT NOT NULL
);

Appendix C: CLI Command Reference

Claude Code

claude --output-format stream-json \
       --input-format stream-json \
       --verbose --print \
       --resume <session-id> \
       --allowedTools Write Edit \
       -- <working-dir>

OpenCode

script -qc "opencode run --format json --session <id> <message>" /dev/null

tmux

# Create session
tmux new-session -d -s <name> -c <dir>

# Send keys
tmux send-keys -t <name> [-l] "<input>" [Enter]

# Capture pane
tmux capture-pane -t <name> -p -e -S -1000

# Resize window
tmux resize-window -t <name> -x <width> -y <height>

# Pipe output
tmux pipe-pane -t <name> "cat >> <file>"