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

1692 lines
54 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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](#1-product-overview)
2. [System Architecture](#2-system-architecture)
3. [Data Models](#3-data-models)
4. [Session Management](#4-session-management)
5. [Messaging System](#5-messaging-system)
6. [Permission System](#6-permission-system)
7. [Terminal Integration](#7-terminal-integration)
8. [Push Notifications](#8-push-notifications)
9. [API Specification](#9-api-specification)
10. [WebSocket Protocol](#10-websocket-protocol)
11. [User Interface](#11-user-interface)
12. [User Flows](#12-user-flows)
13. [Security Considerations](#13-security-considerations)
14. [Performance Requirements](#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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```clojure
{: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
```clojure
(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)
```json
{"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)
```json
{"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
```clojure
(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:
```typescript
{
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
```bash
# 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
```clojure
(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:
```javascript
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
```clojure
;; 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
```json
{
"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
```json
{
"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:**
```json
[
{
"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:**
```json
{
"provider": "claude" | "opencode" | "tmux",
"working-dir": "/optional/path"
}
```
**Response:** Created session object
#### GET /api/sessions/:id
**Response:**
```json
{
"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:**
```json
{
"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:**
```json
{
"message": "User message text"
}
```
**Response:**
```json
{
"status": "sent"
}
```
#### POST /api/sessions/:id/permission
**Request:**
```json
{
"response": "accept" | "deny" | "steer",
"message": "Optional steer instructions"
}
```
**Response:**
```json
{
"status": "permission-response-sent"
}
```
### 9.3 Terminal Endpoints
#### GET /api/sessions/:id/terminal
**Query Parameters:**
- `fresh=true` - Invalidate cache
**Response:**
```json
{
"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:**
```json
{
"input": "ls -la\r"
}
```
**Response:**
```json
{
"status": "sent"
}
```
#### POST /api/sessions/:id/terminal/resize
**Request:**
```json
{
"mode": "landscape" | "portrait" | "desktop" | "fullscreen"
}
```
**Response:**
```json
{
"status": "resized",
"mode": "landscape"
}
```
### 9.4 Push Endpoints
#### GET /api/push/vapid-key
**Response:**
```json
{
"publicKey": "base64url-encoded-public-key"
}
```
#### POST /api/push/subscribe
**Request:**
```json
{
"endpoint": "https://push.service.com/...",
"keys": {
"p256dh": "base64url-client-key",
"auth": "base64url-auth-secret"
}
}
```
**Response:**
```json
{
"id": "subscription-uuid"
}
```
#### POST /api/push/unsubscribe
**Request:**
```json
{
"endpoint": "https://push.service.com/..."
}
```
**Response:** 204 No Content
### 9.5 Utility Endpoints
#### GET /api/health
**Response:**
```json
{
"status": "ok",
"service": "spiceflow"
}
```
#### GET /api/tmux/external
**Response:**
```json
[
{
"name": "my-session",
"working-dir": "/home/user"
}
]
```
#### POST /api/tmux/import
**Request:**
```json
{
"name": "my-session"
}
```
**Response:**
```json
{
"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:**
```json
{
"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
```json
{
"type": "subscribe",
"session-id": "uuid"
}
```
#### Unsubscribe from Session
```json
{
"type": "unsubscribe",
"session-id": "uuid"
}
```
#### Heartbeat
```json
{
"type": "ping"
}
```
### 10.3 Server Messages
#### Connection Established
```json
{
"type": "connected"
}
```
#### Subscription Confirmed
```json
{
"type": "subscribed",
"session-id": "uuid"
}
```
#### Content Delta
```json
{
"event": "content-delta",
"session-id": "uuid",
"text": "streamed text chunk"
}
```
#### Permission Request
```json
{
"event": "permission-request",
"session-id": "uuid",
"permission-request": {
"tools": ["Write"],
"denials": [...]
},
"message-id": "uuid",
"message": {
"id": "uuid",
"content": "...",
"metadata": {...}
},
"auto-accepted": false
}
```
#### Message Stop
```json
{
"event": "message-stop",
"session-id": "uuid"
}
```
#### Terminal Update
```json
{
"event": "terminal-update",
"session-id": "uuid",
"content": "full terminal content",
"diff": {...}
}
```
#### Working Directory Update
```json
{
"event": "working-dir-update",
"session-id": "uuid",
"working-dir": "/new/path"
}
```
#### Error
```json
{
"event": "error",
"session-id": "uuid",
"message": "Error description"
}
```
#### Pong
```json
{
"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
```sql
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
```bash
claude --output-format stream-json \
--input-format stream-json \
--verbose --print \
--resume <session-id> \
--allowedTools Write Edit \
-- <working-dir>
```
### OpenCode
```bash
script -qc "opencode run --format json --session <id> <message>" /dev/null
```
### tmux
```bash
# 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>"
```