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

303 lines
12 KiB
Markdown

# 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**:
```json
{
"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**:
```json
{
"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:
```clojure
;; 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:
```sql
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
```