Files
Adam Jeniski a2e10688bf Add terminal big mode, keyboard shortcuts menu, and UX refinements
- Reduce mobile terminal widths by 2 chars (portrait 42x24, landscape 86x24)
- Add "Big mode" for mobile: desktop sizing (120x36) at 70% zoom
- Click zoom percentage to reset to 100%
- Add keyboard shortcuts submenu in session settings
- Update PRD with all terminal features and documentation

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

54 KiB
Raw Permalink Blame History

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.1.5 External Tmux Session Import

Users can import existing tmux sessions not managed by Spiceflow:

  1. List External: GET /api/tmux/external returns sessions without spiceflow- prefix
  2. Import: POST /api/tmux/import with session name
  3. Rename: Server renames {name} to spiceflow-{name}
  4. Setup: Enables pipe-pane capture for imported session
  5. Return: Session available in Spiceflow with new prefixed ID

4.1.6 Session Rename

Sessions can be renamed via PATCH /api/sessions/:id:

Claude/OpenCode:

  • Updates title in database
  • Session ID remains unchanged

Tmux:

  • Renames tmux session via tmux rename-session
  • Session ID changes to new spiceflow-{name} format
  • Response includes idChanged: true and new session object
  • Client navigates to new URL with replaceState

4.1.7 Session Eject (Tmux only)

Removes session from Spiceflow management while keeping it running:

  1. Rename tmux session from spiceflow-{name} to {name}
  2. Delete session from Spiceflow database
  3. Session continues running, attachable via tmux attach -t {name}

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

Name Generation:

  • 30 adjectives × 30 nouns = 900 base combinations
  • 4-digit random suffix (0000-9999)
  • Docker-style naming convention

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 42x24 Phone portrait
landscape 86x24 Phone landscape
desktop 120x36 Split screen
fullscreen 260x36 Full terminal

7.5 Auto Orientation Detection

On mobile devices, the terminal automatically resizes when the user rotates their phone:

screen.orientation.addEventListener('change', () => {
  const type = screen.orientation.type;
  if (type.includes('portrait')) {
    resizeScreen('portrait');
  } else if (type.includes('landscape')) {
    resizeScreen('landscape');
  }
});

Trigger Conditions:

  • Only on mobile (width < 640px or height < 450px)
  • Uses screen.orientation API (modern browsers)

7.6 Font Zoom Control

Terminal text size can be adjusted from 50% to 150% in 5% increments:

Control Action
- button Decrease font scale
+ button Increase font scale
Percentage display Shows current zoom; click to reset to 100%

Visibility: Zoom controls hidden on mobile portrait, visible on landscape/desktop.

7.7 Big Mode

Mobile users can enable "Big mode" from the settings menu to view more terminal content:

  • Resize: Sets terminal to desktop dimensions (120x36)
  • Zoom: Sets font scale to 70%
  • Access: Settings menu (gear icon) → "Big mode" button
  • Visibility: Only shown for tmux sessions on mobile devices

7.8 Session Eject

Tmux sessions can be "ejected" from Spiceflow management while keeping them running:

  1. User clicks "Eject session" in settings menu
  2. Server renames session from spiceflow-{name} to {name}
  3. Session removed from Spiceflow database
  4. User can reattach manually via tmux attach -t {name}

Use Case: Transfer session to local terminal for continued work.

7.9 Keyboard Shortcuts

Shortcut Action
Ctrl+Down Scroll to bottom
Ctrl+Shift+V Paste from clipboard
Shift+Enter Send literal newline
Shift+Tab Send reverse-tab escape sequence
^ toggle Enable Ctrl mode (next letter sends control character)

7.10 Quick Action Buttons

Button Function Color
^ Toggle Ctrl mode Gray (cyan ring when active)
^C Send interrupt Red
^D Send EOF Amber
y Send 'y' Green
n Send 'n' Red
1-4 Send number Gray
Send Tab Cyan
Send Shift+Tab Cyan
Send Enter Green
📋 Paste clipboard Violet

Visibility: ^, y, n, 1-4, and paste hidden on mobile portrait.

7.11 ANSI Color Rendering

Terminal output preserves ANSI escape sequences:

  • Converted to HTML via ansi-to-html library
  • Default foreground: green (#22c55e)
  • Background: transparent
  • Supports standard terminal colors

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

POST /api/sessions/:id/eject

Ejects a tmux session from Spiceflow management (tmux only).

Response:

{
  "status": "ejected",
  "message": "Session ejected. Reattach with: tmux attach -t {name}",
  "session-name": "my-session"
}

Errors:

  • 400: Not a tmux session
  • 404: Session not found

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

10.5 Terminal Update Broadcasting

After tmux input, server broadcasts terminal updates:

  1. Input received via POST /api/sessions/:id/terminal/input
  2. Input sent to tmux immediately
  3. 100ms delay for command execution
  4. Fresh terminal content captured
  5. Diff computed and broadcast via WebSocket
  6. Broadcast always sent (even if unchanged) to ensure client sync

10.6 Full Frame Refresh

To handle potential drift, the server periodically sends full frames:

  • Every 5 seconds during active streaming
  • On explicit fresh=true request
  • After terminal resize operations

11. User Interface

11.1 Page Structure

Home Page (/)

┌─────────────────────────────────────────┐
│ ☰ Spiceflow                    🔔 + ↻  │
├─────────────────────────────────────────┤
│  ┌─────────────────────────────────┐   │
│  │ ● 2 sessions processing         │   │  (green badge, pulsing)
│  └─────────────────────────────────┘   │
│                                         │
│  ┌─────────────────────────────────┐   │
│  │ ● My Claude Session     claude  │   │
│  │   /home/user/project    2h ago  │   │
│  └─────────────────────────────────┘   │
│                                         │
│  ┌─────────────────────────────────┐   │
│  │ ○ Terminal Session      tmux    │   │
│  │   /home/user            1d ago  │   │
│  └─────────────────────────────────┘   │
│                                         │
│  ┌─────────────────────────────────┐   │
│  │ + Import tmux session           │   │  (if external sessions exist)
│  └─────────────────────────────────┘   │
└─────────────────────────────────────────┘

Elements:

  • Header with branding and action buttons
  • Processing sessions counter (green pulsing badge)
  • Session cards with status indicators
  • Provider badges (color-coded)
  • Relative timestamps
  • Import button for external tmux sessions

Session Page (/session/:id)

Chat Mode (Claude/OpenCode):

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

Mobile Keyboard Toggle:

  • button shows/hides mobile keyboard
  • Addresses issue where keyboard can hide input field
  • Toggles between up/down arrow indicators

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          .md  │  (file extension badge)
│  ┌─────────────────────────────────┐   │
│  │ + 1  # My Haiku                 │   │
│  │ + 2                             │   │
│  │ + 3  Code flows like water      │   │
│  │ + 4  Tests catch bugs silently  │   │
│  │ + 5  Green lights bring peace   │   │
│  └─────────────────────────────────┘   │
│                                         │
│  [Accept]  [Deny]  [No, and...]        │
└─────────────────────────────────────────┘

File Diff Viewer:

  • Write operations: All lines shown as green additions (+)
  • Edit operations: Old lines in red (-), new lines in green (+)
  • Line numbers: Shown for both old and new content
  • File extension badge: Displayed in top-right corner
  • Hover highlighting: Lines highlight on mouse over
  • Tab handling: Tabs rendered as 4 spaces

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 (<640px width, >450px height) Full header Vertical stack
Landscape mobile (<450px height) Hamburger menu Compact header
Desktop (≥640px width, ≥450px height) Full header All controls visible
XL Desktop (≥1280px width) Full header Mobile orientation buttons hidden

11.5 Mobile-Specific UI Classes

CSS Class Behavior
portrait-hide Hidden on mobile portrait (width < 640px AND height > 450px)
desktop-only Hidden on mobile (width < 640px OR height < 450px)
mobile-only Hidden on XL desktop (width ≥ 1280px)
landscape-mobile:hidden Hidden when height < 450px
landscape-menu Shown only when height < 450px

11.6 Message Condensing

Long messages (5+ lines) can be collapsed:

  • Threshold: 5 lines minimum to show collapse toggle
  • Preview: Shows first 3 lines when collapsed
  • Toggle: Chevron indicator expands/collapses
  • Bulk action: "Condense all" in settings menu

11.7 Thinking Indicator

Animated indicator when Claude is processing:

  • Three bouncing dots (●●●)
  • Separate from streaming content display
  • Disappears when response completes or permission requested

11.8 Auto-Scroll Control

  • Default: Enabled
  • Persistence: Saved to localStorage (spiceflow-auto-scroll)
  • Toggle: Available in session settings menu
  • Behavior: Scrolls to bottom on new content
  • Override: Ctrl+Down forces scroll regardless of setting

11.9 Session Status Indicators

Status Indicator
Idle Gray dot (static)
Processing Green dot (pulsing)
Awaiting Permission Amber dot (pulsing)
Tmux Alive Green dot
Tmux Dead Gray dot

11.10 Markdown Rendering

Assistant messages render Markdown with:

  • Headings (h1-h6) with appropriate sizing
  • Code blocks with monospace font and background
  • Inline code with background highlight
  • Lists (ordered and unordered)
  • Blockquotes with left border
  • Links styled in orange (spice color)
  • Line breaks preserved (breaks: true)

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

12.6 Import External Tmux Session

  1. User has existing tmux session running (e.g., dev-session)
  2. User opens Spiceflow home page
  3. "Import tmux session" button appears
  4. User clicks import → dropdown shows available sessions
  5. User selects dev-session
  6. Server renames to spiceflow-dev-session
  7. Session appears in Spiceflow list
  8. User can now manage session via Spiceflow

12.7 Eject Tmux Session

  1. User opens tmux session in Spiceflow
  2. User clicks settings gear (⚙️)
  3. User clicks "Eject session"
  4. Confirmation alert shows reattach command
  5. Session renamed from spiceflow-{name} to {name}
  6. User redirected to home page
  7. Session removed from Spiceflow but continues running
  8. User can reattach via tmux attach -t {name}

12.8 Rotate Phone (Terminal)

  1. User viewing terminal session on phone
  2. User rotates phone from portrait to landscape
  3. screen.orientation change event fires
  4. App detects orientation is now landscape
  5. Terminal automatically resizes to landscape dimensions (88x24)
  6. Fresh terminal content fetched after 150ms
  7. UI updates to show landscape-specific controls

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