12 KiB
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>orclaude --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
- User opens PWA → sees list of tracked sessions from SQLite
- User selects session → sees message history, status
- User types message → POST to
/api/sessions/:id/send - 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
- Client receives streamed response → renders in real-time
- Process completes → server saves assistant message to SQLite
Implementation Phases
Phase 1: Project Scaffolding
- Initialize Clojure project with deps.edn
- Initialize SvelteKit project with TypeScript
- Set up Tailwind CSS
- Create basic directory structure
Phase 2: Database Layer
- Define DataStore protocol
- Implement SQLite store with next.jdbc
- Implement in-memory store for tests
- Write migrations
- Write tests for both implementations
Phase 3: Server Core
- Ring server setup with Jetty
- REST API: GET/POST /sessions, GET /sessions/:id
- WebSocket setup for real-time streaming
- Configuration management
Phase 4: Agent Adapters
- Define AgentAdapter protocol (discover, spawn, send, stream)
- Claude Code adapter:
- Discover sessions from
~/.claude/projects/ - Spawn with
--resume --print --output-format stream-json --input-format stream-json - Parse JSONL output
- Discover sessions from
- OpenCode adapter:
- Discover via
opencode session list - Spawn with
opencode run --session <id> - Parse JSON output
- Discover via
Phase 5: Client Core
- SvelteKit routing setup
- Runtime selector dropdown (Claude Code default, OpenCode, extensible for future runtimes)
- Session list page filtered by selected runtime
- Session detail page with message history
- Real-time message streaming via WebSocket
- Message input component
Phase 6: PWA Setup
- Configure vite-plugin-pwa
- Web app manifest (icons, theme color, display)
- Service worker for offline caching
- Install prompt handling
Phase 7: Interaction Features
- Send message to session
- Display tool calls and outputs
- Session status indicators
- 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
- Database test: Run
clj -M:test→ all DataStore tests pass - Server test:
clj -M:run, hitGET /api/sessions→ returns[] - Adapter test: Claude adapter discovers existing sessions from ~/.claude
- Client test:
pnpm dev, open in browser → see session list - PWA test: Lighthouse audit → PWA criteria met
- 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