Files
2026-01-18 22:07:39 -05:00

12 KiB

Spiceflow - AI Session Orchestration PWA

Overview

A Progressive Web App for monitoring and interacting with running Claude Code and OpenCode sessions from mobile devices. "The spice must flow."

Architecture

┌─────────────────┐     ┌─────────────────────────┐     ┌─────────────────┐
│  Claude Code    │◀───▶│     Spiceflow Server    │◀───▶│   PWA Client    │
│  (CLI)          │     │       (Clojure)         │     │    (Svelte)     │
└─────────────────┘     │                         │     └─────────────────┘
                        │  ┌─────────────────┐    │
┌─────────────────┐     │  │ SQLite + DB     │    │
│  OpenCode       │◀───▶│  │ Abstraction     │    │
│  (CLI)          │     │  │ WebSocket/SSE   │    │
└─────────────────┘     │  └─────────────────┘    │
                        └─────────────────────────┘

CLI Analysis

Claude Code

  • Session storage: ~/.claude/projects/{encoded-path}/{session-id}.jsonl
  • Resume session: claude --resume <session-id> or claude --session-id <uuid>
  • Programmatic I/O: --output-format stream-json + --input-format stream-json + --print
  • JSONL format:
    {
      "type": "user",
      "sessionId": "uuid",
      "parentUuid": "uuid|null",
      "message": {"role": "user", "content": "..."},
      "uuid": "message-uuid",
      "timestamp": "ISO-8601"
    }
    
  • Key flags: --permission-mode, --model, --continue

OpenCode

  • List sessions: opencode session list
  • Export session: opencode export <session-id> (JSON)
  • Continue session: opencode --session <session-id> or --continue
  • Headless mode: opencode serve (starts server), opencode run <message>
  • JSON format:
    {
      "info": {"id": "ses_xxx", "title": "...", "time": {...}},
      "messages": [{"info": {...}, "parts": [{type, text, ...}]}]
    }
    

Tech Stack

  • Frontend: SvelteKit + TypeScript + Vite
  • PWA: vite-plugin-pwa (Workbox under the hood)
  • Styling: Tailwind CSS (mobile-first)
  • Backend: Clojure + Ring/Jetty + next.jdbc
  • Database: SQLite (via generic interface for future swapping)
  • Real-time: WebSocket (via Ring adapter) or SSE
  • Build: deps.edn (Clojure), pnpm (frontend)

Supported Agentic Runtimes

The UI includes a runtime selector dropdown for choosing which agentic CLI to use:

Runtime Status Notes
Claude Code Default Anthropic's official CLI
OpenCode Supported Open-source alternative
(Future) Planned Aider, Cursor CLI, Continue, etc.

The adapter system is designed to be extensible—adding a new runtime requires implementing the AgentAdapter protocol.

Mobile UI Design

The PWA is mobile-first. Key design decisions:

┌─────────────────────────────┐
│  ☰  Spiceflow    [Claude ▾] │  ← Header + runtime dropdown
├─────────────────────────────┤
│                             │
│  ▶ User: Fix the login...   │  ← Collapsed (tap to expand)
│  ▼ Assistant: I'll fix...   │  ← Expanded message
│    [full content here]      │
│    ...                      │
│  ▶ User: Now add tests      │  ← Collapsed
│  ▶ Assistant: Added 3...    │  ← Collapsed
│                             │
├─────────────────────────────┤
│  [Type message...]    [▶]   │  ← Small fixed input bar
└─────────────────────────────┘
  • Collapsible messages: Each message shows a preview (first line or summary). Tap to expand full content. Keeps history skimmable.
  • Fixed input bar: Small, anchored to bottom. Expands only when focused.
  • Touch-friendly: Large tap targets, swipe gestures for navigation.
  • Session list: Cards showing title, runtime badge, last activity. Pull-to-refresh.

Database Abstraction

Generic interface to allow mocking in tests or swapping providers:

;; src/spiceflow/db/protocol.clj
(defprotocol DataStore
  (get-sessions [this])
  (get-session [this id])
  (save-session [this session])
  (update-session [this id data])
  (delete-session [this id])
  (get-messages [this session-id])
  (save-message [this message]))

;; Implementations:
;; - SQLiteStore (production)
;; - AtomStore (testing/mocking)
;; - Future: PostgresStore, etc.

SQLite schema:

CREATE TABLE sessions (
  id TEXT PRIMARY KEY,
  provider TEXT NOT NULL,        -- 'claude' | 'opencode'
  external_id TEXT,              -- original session ID from CLI
  title TEXT,
  working_dir TEXT,
  status TEXT DEFAULT 'idle',    -- 'idle' | 'running' | 'completed'
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE messages (
  id TEXT PRIMARY KEY,
  session_id TEXT REFERENCES sessions(id),
  role TEXT NOT NULL,            -- 'user' | 'assistant' | 'system'
  content TEXT,
  metadata TEXT,                 -- JSON blob for tool calls, etc.
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

Project Structure

spiceflow/
├── server/                      # Clojure backend
│   ├── deps.edn
│   ├── src/spiceflow/
│   │   ├── core.clj             # Entry point
│   │   ├── config.clj           # Configuration
│   │   ├── db/
│   │   │   ├── protocol.clj     # DataStore protocol
│   │   │   ├── sqlite.clj       # SQLite implementation
│   │   │   └── memory.clj       # In-memory impl for tests
│   │   ├── adapters/
│   │   │   ├── protocol.clj     # AgentAdapter protocol
│   │   │   ├── claude.clj       # Claude Code adapter
│   │   │   └── opencode.clj     # OpenCode adapter
│   │   ├── api/
│   │   │   ├── routes.clj       # REST endpoints
│   │   │   └── websocket.clj    # WebSocket handlers
│   │   └── session/
│   │       └── manager.clj      # Session lifecycle
│   ├── test/spiceflow/
│   │   ├── db_test.clj
│   │   └── adapters_test.clj
│   └── resources/
│       └── migrations/          # SQL migrations
│
├── client/                      # SvelteKit PWA
│   ├── package.json
│   ├── svelte.config.js
│   ├── vite.config.ts
│   ├── src/
│   │   ├── routes/
│   │   │   ├── +layout.svelte
│   │   │   ├── +page.svelte     # Session list
│   │   │   └── session/
│   │   │       └── [id]/
│   │   │           └── +page.svelte
│   │   ├── lib/
│   │   │   ├── stores/
│   │   │   │   ├── runtime.ts       # Selected runtime (persisted)
│   │   │   │   └── sessions.ts
│   │   │   ├── components/
│   │   │   │   ├── RuntimeSelector.svelte  # Dropdown for agentic runtime
│   │   │   │   ├── SessionCard.svelte      # Session list card
│   │   │   │   ├── MessageList.svelte      # Scrollable message container
│   │   │   │   ├── CollapsibleMessage.svelte # Expandable message item
│   │   │   │   └── InputBar.svelte         # Fixed bottom input
│   │   │   └── api.ts           # Server API client
│   │   └── app.html
│   ├── static/
│   │   └── manifest.json
│   └── tests/
│
└── README.md

Core Flow: Continuing a Session

  1. User opens PWA → sees list of tracked sessions from SQLite
  2. User selects session → sees message history, status
  3. User types message → POST to /api/sessions/:id/send
  4. Server receives message:
    • Saves message to SQLite
    • Spawns CLI process: claude --resume <external_id> --print --output-format stream-json --input-format stream-json
    • Pipes user message to stdin
    • Streams stdout back via WebSocket
  5. Client receives streamed response → renders in real-time
  6. Process completes → server saves assistant message to SQLite

Implementation Phases

Phase 1: Project Scaffolding

  1. Initialize Clojure project with deps.edn
  2. Initialize SvelteKit project with TypeScript
  3. Set up Tailwind CSS
  4. Create basic directory structure

Phase 2: Database Layer

  1. Define DataStore protocol
  2. Implement SQLite store with next.jdbc
  3. Implement in-memory store for tests
  4. Write migrations
  5. Write tests for both implementations

Phase 3: Server Core

  1. Ring server setup with Jetty
  2. REST API: GET/POST /sessions, GET /sessions/:id
  3. WebSocket setup for real-time streaming
  4. Configuration management

Phase 4: Agent Adapters

  1. Define AgentAdapter protocol (discover, spawn, send, stream)
  2. Claude Code adapter:
    • Discover sessions from ~/.claude/projects/
    • Spawn with --resume --print --output-format stream-json --input-format stream-json
    • Parse JSONL output
  3. OpenCode adapter:
    • Discover via opencode session list
    • Spawn with opencode run --session <id>
    • Parse JSON output

Phase 5: Client Core

  1. SvelteKit routing setup
  2. Runtime selector dropdown (Claude Code default, OpenCode, extensible for future runtimes)
  3. Session list page filtered by selected runtime
  4. Session detail page with message history
  5. Real-time message streaming via WebSocket
  6. Message input component

Phase 6: PWA Setup

  1. Configure vite-plugin-pwa
  2. Web app manifest (icons, theme color, display)
  3. Service worker for offline caching
  4. Install prompt handling

Phase 7: Interaction Features

  1. Send message to session
  2. Display tool calls and outputs
  3. Session status indicators
  4. Error handling and reconnection

Key Files to Create

File Purpose
server/deps.edn Clojure dependencies
server/src/spiceflow/db/protocol.clj DataStore protocol
server/src/spiceflow/db/sqlite.clj SQLite implementation
server/src/spiceflow/adapters/claude.clj Claude CLI adapter
server/src/spiceflow/api/routes.clj REST API
client/src/routes/+page.svelte Session list
client/src/lib/components/RuntimeSelector.svelte Runtime dropdown (Claude default)
client/src/lib/stores/sessions.ts Session state
client/static/manifest.json PWA manifest

Verification

  1. Database test: Run clj -M:test → all DataStore tests pass
  2. Server test: clj -M:run, hit GET /api/sessions → returns []
  3. Adapter test: Claude adapter discovers existing sessions from ~/.claude
  4. Client test: pnpm dev, open in browser → see session list
  5. PWA test: Lighthouse audit → PWA criteria met
  6. E2E test: Send message via PWA → see response streamed back

API Endpoints

GET  /api/runtimes              List available runtimes (claude=default, opencode, etc.)
GET  /api/sessions              List all tracked sessions (?runtime=claude filter)
POST /api/sessions              Import/track a new session
GET  /api/sessions/:id          Get session details + messages
POST /api/sessions/:id/send     Send message to session
WS   /api/ws                    WebSocket for real-time updates

GET  /api/discover/claude       Discover Claude sessions
GET  /api/discover/opencode     Discover OpenCode sessions