From 66f072a5e64f0391660f442b59c5ce0efd9f137e Mon Sep 17 00:00:00 2001 From: Adam Jeniski Date: Tue, 20 Jan 2026 18:03:59 -0500 Subject: [PATCH] Add comprehensive PRD and terminal UX improvements - Add PRD.md with detailed product requirements documentation - Unify tmux screen size presets to consistent 24-row height - Add Ctrl+Down keyboard shortcut to scroll terminal to bottom Co-Authored-By: Claude Opus 4.5 --- PRD.md | 1424 +++++++++++++++++ client/src/lib/components/TerminalView.svelte | 13 +- server/src/spiceflow/adapters/tmux.clj | 6 +- 3 files changed, 1437 insertions(+), 6 deletions(-) create mode 100644 PRD.md diff --git a/PRD.md b/PRD.md new file mode 100644 index 0000000..7ec456d --- /dev/null +++ b/PRD.md @@ -0,0 +1,1424 @@ +# 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.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 + +#### 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` | 40x24 | Phone portrait | +| `landscape` | 65x24 | Phone landscape | +| `desktop` | 100x24 | Split screen | +| `fullscreen` | 180x24 | Full terminal | + +--- + +## 8. Push Notifications + +### 8.1 VAPID Authentication (RFC 8292) + +#### 8.1.1 Key Generation + +```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" +} +``` + +--- + +## 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 + +--- + +## 11. User Interface + +### 11.1 Page Structure + +#### Home Page (`/`) + +``` +┌─────────────────────────────────────────┐ +│ ☰ Spiceflow 🔔 + ↻ │ +├─────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────┐ │ +│ │ ● My Claude Session claude │ │ +│ │ /home/user/project 2h ago │ │ +│ └─────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────┐ │ +│ │ ○ Terminal Session tmux │ │ +│ │ /home/user 1d ago │ │ +│ └─────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────┘ +``` + +**Elements:** +- Header with branding and action buttons +- Session cards with status indicators +- Provider badges (color-coded) +- Relative timestamps + +#### Session Page (`/session/:id`) + +**Chat Mode (Claude/OpenCode):** +``` +┌─────────────────────────────────────────┐ +│ ← Session Title ⚙️ CLAUDE │ +├─────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────┐ │ +│ │ User: Hello │ │ +│ └─────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────┐ │ +│ │ Assistant: Hi! How can I help? │ │ +│ │ ● ● ● (thinking) │ │ +│ └─────────────────────────────────┘ │ +│ │ +├─────────────────────────────────────────┤ +│ [Type a message... ] [➤] │ +└─────────────────────────────────────────┘ +``` + +**Terminal Mode (tmux):** +``` +┌─────────────────────────────────────────┐ +│ ← brave-fox-0042 ⚙️ TERMINAL │ +├─────────────────────────────────────────┤ +│ $ pwd │ +│ /home/user │ +│ $ ls -la │ +│ total 32 │ +│ drwxr-xr-x 5 user user 4096 Jan 1 00:0│ +│ -rw-r--r-- 1 user user 123 Jan 1 00:0│ +│ $ │ +├─────────────────────────────────────────┤ +│ [^] [^C] [^D] | [y] [n] | [1-4] | [⇥] │ +│ [-] 100% [+] | [📱] [📺] [🖥️] [⬜] [↓] │ +└─────────────────────────────────────────┘ +``` + +### 11.2 Permission UI + +``` +┌─────────────────────────────────────────┐ +│ ⚠️ Claude needs permission │ +├─────────────────────────────────────────┤ +│ │ +│ Write: /home/user/foo.md │ +│ ┌─────────────────────────────────┐ │ +│ │ + 1 # My Haiku │ │ +│ │ + 2 │ │ +│ │ + 3 Code flows like water │ │ +│ │ + 4 Tests catch bugs silently │ │ +│ │ + 5 Green lights bring peace │ │ +│ └─────────────────────────────────┘ │ +│ │ +│ [Accept] [Deny] [No, and...] │ +└─────────────────────────────────────────┘ +``` + +### 11.3 Component Inventory + +| Component | Purpose | Props | +|-----------|---------|-------| +| `SessionCard` | Session list item | `session` | +| `MessageList` | Chat history display | `messages, streamingContent, isThinking` | +| `InputBar` | Message input | `disabled, placeholder, autoFocus` | +| `PermissionRequest` | Permission approval UI | `permission, assistantName` | +| `FileDiff` | File change visualization | `tool, input, filePath` | +| `TerminalView` | Tmux display/input | `sessionId, autoScroll` | +| `SessionSettings` | Settings dropdown | `autoAcceptEdits, autoScroll, provider` | +| `PushToggle` | Notification toggle | (uses store) | + +### 11.4 Responsive Behavior + +| Breakpoint | Header | Layout | +|------------|--------|--------| +| Portrait | Full header | Vertical stack | +| Landscape mobile | Hamburger menu | Compact header | +| Desktop | Full header | All controls visible | + +--- + +## 12. User Flows + +### 12.1 Create and Chat with Claude + +1. User opens app → sees session list +2. User taps **+** button → provider menu appears +3. User selects **Claude Code** → navigates to new session +4. User sees "No messages yet" state +5. User types message in input bar +6. User taps send → message appears immediately (optimistic) +7. Thinking indicator (●●●) appears +8. Streaming text appears incrementally +9. Response completes → thinking indicator disappears +10. User can continue conversation + +### 12.2 Handle Permission Request + +1. User sends message requesting file operation +2. Claude attempts Write/Edit/Bash +3. **Permission UI appears** with tool details +4. User reviews file diff (for Write/Edit) +5. User chooses: + - **Accept** → grants permission, streaming continues + - **Deny** → rejects, Claude finds alternative + - **No, and...** → enters steer mode +6. If steer: user types redirect instructions +7. Permission UI disappears +8. New response streams + +### 12.3 Enable Auto-Accept + +1. User opens session with Claude +2. User taps **⚙️** (settings gear) +3. User enables **Auto-accept edits** checkbox +4. Server saves preference (`auto-accept-edits: true`) +5. Future Write/Edit operations auto-approved +6. Permission messages appear with green ✓ status +7. No interruption for file operations + +### 12.4 Use Terminal Session + +1. User creates tmux session +2. User sees terminal with prompt +3. User taps terminal to focus +4. User types command (keyboard or quick buttons) +5. Input sent to tmux session +6. Output appears in terminal view +7. User can resize terminal (portrait/landscape/desktop/fullscreen) +8. User can use quick buttons (Ctrl+C, y/n, etc.) + +### 12.5 Receive Push Notification + +1. User enables push notifications (bell icon) +2. Browser prompts for permission +3. User grants permission +4. Subscription saved to server +5. Later: permission request pending for 15+ seconds +6. Server sends push notification +7. User's device shows notification +8. User taps notification → app opens to session +9. User sees permission UI, responds + +--- + +## 13. Security Considerations + +### 13.1 Permission Model + +- **No auto-execute:** All tool operations require explicit approval (or opt-in auto-accept) +- **Auto-accept scope:** Limited to Write/Edit only, never Bash/network +- **Steer capability:** User can redirect AI without accepting dangerous operations + +### 13.2 Push Security + +- **VAPID authentication:** Server identity verified to push services +- **End-to-end encryption:** RFC 8291 AES-128-GCM encryption +- **Subscription cleanup:** Invalid subscriptions auto-removed + +### 13.3 Data Protection + +- **Local SQLite:** Data stored locally on server +- **No cloud sync:** Sessions not transmitted externally +- **Process isolation:** CLI processes run with server user permissions + +### 13.4 API Security + +- **CORS:** Configured for allowed origins +- **Input validation:** Session IDs, permission responses validated +- **No secrets in URLs:** Sensitive data in request bodies + +--- + +## 14. Performance Requirements + +### 14.1 Response Times + +| Operation | Target | Maximum | +|-----------|--------|---------| +| Page load | <1s | 3s | +| Session list | <500ms | 2s | +| Message send | <100ms (optimistic) | 1s | +| WebSocket connect | <1s | 5s | +| Terminal input | <50ms | 200ms | + +### 14.2 Streaming Performance + +- **Content delta frequency:** Real-time as received +- **Terminal diff threshold:** >50% change triggers full refresh +- **Frame ordering:** Prevents out-of-order updates + +### 14.3 Resource Usage + +- **Active processes:** ConcurrentHashMap for thread-safe access +- **WebSocket connections:** Efficient broadcast to subscribers only +- **Terminal cache:** Per-session diff state + +### 14.4 Scalability + +- **Concurrent sessions:** Limited by server resources +- **WebSocket subscribers:** Multiple clients per session supported +- **Database:** SQLite (single-writer, multiple-reader) + +--- + +## Appendix A: Configuration Reference + +| Variable | Default | Description | +|----------|---------|-------------| +| `SPICEFLOW_PORT` | 3000 | Server port | +| `SPICEFLOW_HOST` | 0.0.0.0 | Server host | +| `SPICEFLOW_DB` | spiceflow.db | SQLite database path | +| `CLAUDE_SESSIONS_DIR` | ~/.claude/projects | Claude sessions directory | +| `OPENCODE_CMD` | opencode | OpenCode binary name | + +## Appendix B: Database Schema + +```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 \ + --allowedTools Write Edit \ + -- +``` + +### OpenCode + +```bash +script -qc "opencode run --format json --session " /dev/null +``` + +### tmux + +```bash +# Create session +tmux new-session -d -s -c + +# Send keys +tmux send-keys -t [-l] "" [Enter] + +# Capture pane +tmux capture-pane -t -p -e -S -1000 + +# Resize window +tmux resize-window -t -x -y + +# Pipe output +tmux pipe-pane -t "cat >> " +``` diff --git a/client/src/lib/components/TerminalView.svelte b/client/src/lib/components/TerminalView.svelte index 015dafe..c4c9bda 100644 --- a/client/src/lib/components/TerminalView.svelte +++ b/client/src/lib/components/TerminalView.svelte @@ -198,6 +198,13 @@ } async function handleKeydown(event: KeyboardEvent) { + // Ctrl+Down scrolls to bottom (don't send to tmux) + if (event.ctrlKey && event.key === 'ArrowDown') { + event.preventDefault(); + scrollToBottom(); + return; + } + if (!isAlive) return; // Prevent default for all keys we handle @@ -450,7 +457,7 @@ class="px-1 py-0.5 rounded-sm text-[10px] font-mono transition-colors {screenMode === 'portrait' ? 'bg-cyan-600 text-white' : 'bg-zinc-700 hover:bg-zinc-600 text-zinc-200'} disabled:opacity-50" title="Portrait (50x60)" > - + @@ -470,7 +477,7 @@ class="px-1 py-0.5 rounded-sm text-[10px] font-mono transition-colors {screenMode === 'desktop' ? 'bg-cyan-600 text-white' : 'bg-zinc-700 hover:bg-zinc-600 text-zinc-200'} disabled:opacity-50" title="Split screen (100x40)" > - + @@ -481,7 +488,7 @@ class="px-1 py-0.5 rounded-sm text-[10px] font-mono transition-colors {screenMode === 'fullscreen' ? 'bg-cyan-600 text-white' : 'bg-zinc-700 hover:bg-zinc-600 text-zinc-200'} disabled:opacity-50" title="Fullscreen (180x60)" > - + diff --git a/server/src/spiceflow/adapters/tmux.clj b/server/src/spiceflow/adapters/tmux.clj index d2a1473..3a85885 100644 --- a/server/src/spiceflow/adapters/tmux.clj +++ b/server/src/spiceflow/adapters/tmux.clj @@ -339,10 +339,10 @@ ;; Screen size presets for different device orientations (def ^:private screen-sizes - {:fullscreen {:width 180 :height 60} - :desktop {:width 100 :height 40} + {:fullscreen {:width 180 :height 24} + :desktop {:width 100 :height 24} :landscape {:width 65 :height 24} - :portrait {:width 40 :height 35}}) + :portrait {:width 40 :height 24}}) (defn resize-session "Resize a tmux session window to a preset size.