- 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>
54 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
- Product Overview
- System Architecture
- Data Models
- Session Management
- Messaging System
- Permission System
- Terminal Integration
- Push Notifications
- API Specification
- WebSocket Protocol
- User Interface
- User Flows
- Security Considerations
- 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:
- Client POSTs to
/api/sessionswith{provider: 'claude'|'opencode'} - Server generates UUID, saves to database
- Returns session with
status: 'idle'
Tmux:
- Client POSTs to
/api/sessionswith{provider: 'tmux'} - Server generates readable name:
spiceflow-{adjective}-{noun}-{4digits} - Creates tmux session via
tmux new-session -d -s {name} - Sets up output capture via
pipe-pane - 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:
- List External:
GET /api/tmux/externalreturns sessions withoutspiceflow-prefix - Import:
POST /api/tmux/importwith session name - Rename: Server renames
{name}tospiceflow-{name} - Setup: Enables pipe-pane capture for imported session
- 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: trueand 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:
- Rename tmux session from
spiceflow-{name}to{name} - Delete session from Spiceflow database
- 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→:initassistant→:content-delta(aggregated)result→:resultwith 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→:inittext→:content-deltastep_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 creationEdit- File modification
Ineligible Tools:
Bash- Shell commandsWebFetch- HTTP requestsWebSearch- Web searchesNotebookEdit- Jupyter notebooksTask- Sub-agent spawningSkill- 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.orientationAPI (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:
- User clicks "Eject session" in settings menu
- Server renames session from
spiceflow-{name}to{name} - Session removed from Spiceflow database
- 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-htmllibrary - 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:
- Input received via
POST /api/sessions/:id/terminal/input - Input sent to tmux immediately
- 100ms delay for command execution
- Fresh terminal content captured
- Diff computed and broadcast via WebSocket
- 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=truerequest - 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
- User opens app → sees session list
- User taps + button → provider menu appears
- User selects Claude Code → navigates to new session
- User sees "No messages yet" state
- User types message in input bar
- User taps send → message appears immediately (optimistic)
- Thinking indicator (●●●) appears
- Streaming text appears incrementally
- Response completes → thinking indicator disappears
- User can continue conversation
12.2 Handle Permission Request
- User sends message requesting file operation
- Claude attempts Write/Edit/Bash
- Permission UI appears with tool details
- User reviews file diff (for Write/Edit)
- User chooses:
- Accept → grants permission, streaming continues
- Deny → rejects, Claude finds alternative
- No, and... → enters steer mode
- If steer: user types redirect instructions
- Permission UI disappears
- New response streams
12.3 Enable Auto-Accept
- User opens session with Claude
- User taps ⚙️ (settings gear)
- User enables Auto-accept edits checkbox
- Server saves preference (
auto-accept-edits: true) - Future Write/Edit operations auto-approved
- Permission messages appear with green ✓ status
- No interruption for file operations
12.4 Use Terminal Session
- User creates tmux session
- User sees terminal with prompt
- User taps terminal to focus
- User types command (keyboard or quick buttons)
- Input sent to tmux session
- Output appears in terminal view
- User can resize terminal (portrait/landscape/desktop/fullscreen)
- User can use quick buttons (Ctrl+C, y/n, etc.)
12.5 Receive Push Notification
- User enables push notifications (bell icon)
- Browser prompts for permission
- User grants permission
- Subscription saved to server
- Later: permission request pending for 15+ seconds
- Server sends push notification
- User's device shows notification
- User taps notification → app opens to session
- User sees permission UI, responds
12.6 Import External Tmux Session
- User has existing tmux session running (e.g.,
dev-session) - User opens Spiceflow home page
- "Import tmux session" button appears
- User clicks import → dropdown shows available sessions
- User selects
dev-session - Server renames to
spiceflow-dev-session - Session appears in Spiceflow list
- User can now manage session via Spiceflow
12.7 Eject Tmux Session
- User opens tmux session in Spiceflow
- User clicks settings gear (⚙️)
- User clicks "Eject session"
- Confirmation alert shows reattach command
- Session renamed from
spiceflow-{name}to{name} - User redirected to home page
- Session removed from Spiceflow but continues running
- User can reattach via
tmux attach -t {name}
12.8 Rotate Phone (Terminal)
- User viewing terminal session on phone
- User rotates phone from portrait to landscape
screen.orientationchange event fires- App detects orientation is now landscape
- Terminal automatically resizes to landscape dimensions (88x24)
- Fresh terminal content fetched after 150ms
- 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>"