plan
This commit is contained in:
@@ -0,0 +1,302 @@
|
||||
# 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
|
||||
```
|
||||
Reference in New Issue
Block a user