42861ea7fa
Multi-adapter mobile UI for AI coding assistants. Supports Claude Code, Codex CLI, and Gemini CLI through one interface. Features: - Real-time bidirectional sync via tmux + WebSocket - Cross-AI review (send one AI's output to another for review) - Multi-review tabs with minimize/expand - Push notifications (PWA) with smart session-aware filtering - Three-channel event system (hooks, file watcher, pane monitor) - Voice input, image paste, draft persistence - Terminal-native design (JetBrains Mono, dark theme, pixel art claw) - CLI with --adapter flag on every command - Zero-overhead fire-and-forget hooks
257 lines
10 KiB
Markdown
257 lines
10 KiB
Markdown
# Session ID Unification Design Spec
|
|
|
|
## Problem
|
|
|
|
CodeTap's session management has grown organically and now has several issues:
|
|
|
|
1. **Dual ID system** — Each session has an "internal ID" (`session-{timestamp}` or `desktop-{uuid前8字}`) and a "CLI UUID". The internal ID is meaningless to users but is what the UI displays.
|
|
2. **Dual storage** — `session-map.json` (file) and SQLite (DB) both store session mappings. The file is written by the SessionStart hook but only read on server startup, causing `codetap new` sessions to not appear in Active Sessions until server restart.
|
|
3. **`desktop-` prefix confusion** — Sessions that are "rediscovered" after a non-graceful server restart get a new `desktop-` internal ID, losing their original ID.
|
|
4. **No adapter awareness in IDs** — Internal IDs don't indicate which adapter (Claude/Codex/Gemini) the session belongs to.
|
|
5. **User can't resume from desktop** — The chat header shows the internal ID which can't be used with `claude --resume`.
|
|
|
|
## Design Decisions
|
|
|
|
| Topic | Decision |
|
|
|-------|----------|
|
|
| Scope | All adapters (Claude, Codex, future) |
|
|
| Storage | SQLite only — remove session-map.json |
|
|
| SessionStart hook | POST to server API (like all other hooks) |
|
|
| Internal ID format | `{adapter}-{timestamp}` (e.g., `claude-1774210269126`) |
|
|
| `desktop-` prefix | Removed entirely |
|
|
| Non-graceful restart recovery | Read original internal ID from DB |
|
|
| User-facing display | Chat header: CLI UUID (primary) + internal ID (secondary) |
|
|
| Active Sessions list | Keep showing `firstPrompt` (no change) |
|
|
| DB on shutdown | Clear `sessions` table (tmux windows are killed, records are useless) |
|
|
| CLI `--adapter` flag | Added to `codetap new` and `codetap --continue` |
|
|
| CLI `--resume` | Accepts internal ID or CLI UUID; scans JSONL dirs to detect adapter |
|
|
| CLI `--continue` | Pass through to adapter CLI's native continue command |
|
|
| Adapter selector UI | Not in scope (future work) |
|
|
|
|
## Architecture
|
|
|
|
### Internal ID Format
|
|
|
|
```
|
|
{adapter}-{timestamp}
|
|
|
|
Examples:
|
|
claude-1774210269126
|
|
codex-1774210345678
|
|
gemini-1774210500000
|
|
```
|
|
|
|
Produced by:
|
|
- `startSession()` in each adapter (Web UI new session)
|
|
- `bin/codetap` CLI script (`codetap new`, `codetap --resume`, `codetap --continue`)
|
|
|
|
### Single Source of Truth: SQLite
|
|
|
|
All session mappings go through SQLite. No file-based storage.
|
|
|
|
**SessionStart hook flow (unified for all entry points):**
|
|
|
|
```
|
|
Claude/Codex CLI starts → SessionStart hook fires
|
|
↓
|
|
POST /api/hooks/{adapter}/session-start
|
|
body: { session_id: "<CLI UUID>", cwd: "/path", ... }
|
|
↓
|
|
Server handler:
|
|
1. Find tmux window for this session (by window name or DB lookup)
|
|
2. If session already in memory → update mapping (e.g., /resume changed UUID)
|
|
3. If session NOT in memory → create new entry:
|
|
- Internal ID from tmux window name (e.g., claude-1774210269126)
|
|
- Map CLI UUID → internal ID
|
|
- Write to DB
|
|
4. Session appears in Active Sessions immediately
|
|
```
|
|
|
|
**Shutdown flow:**
|
|
|
|
```
|
|
SIGTERM/SIGINT received
|
|
↓
|
|
1. adapter.destroy() → tmuxManager.killSession() → all tmux windows killed
|
|
2. dbSessions.clearAll() → clear sessions table
|
|
3. closeDB()
|
|
```
|
|
|
|
### DB Schema
|
|
|
|
```sql
|
|
CREATE TABLE sessions (
|
|
id TEXT PRIMARY KEY, -- internal ID (claude-1774210269126)
|
|
cli_session TEXT, -- CLI native UUID
|
|
adapter TEXT, -- 'claude' / 'codex' / 'gemini'
|
|
cwd TEXT,
|
|
window_id TEXT,
|
|
permission_mode TEXT,
|
|
created_at DATETIME DEFAULT (datetime('now')),
|
|
last_activity DATETIME DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE INDEX idx_sessions_cli ON sessions(cli_session);
|
|
CREATE INDEX idx_sessions_adapter ON sessions(adapter);
|
|
```
|
|
|
|
Migration: rename `claude_session` → `cli_session`, add `adapter` column (default `'claude'` for existing rows), remove `is_active` column.
|
|
|
|
### Chat Header Display
|
|
|
|
```
|
|
┌──────────────────────────────────────────────────┐
|
|
│ ← code-tap 625c60d0-aedb-4e0b... [copy icon] │
|
|
│ claude-1774210269126 │
|
|
└──────────────────────────────────────────────────┘
|
|
```
|
|
|
|
- Primary: CLI UUID (truncated), click copy icon to copy full UUID → usable with `claude --resume <uuid>`
|
|
- Secondary: internal ID → usable with `tmux select-window -t codetap:claude-1774210269126`
|
|
|
|
`SESSION_CREATED` message updated to include both `sessionId` (internal) and `cliSessionId` (UUID).
|
|
|
|
### Hook Config Change
|
|
|
|
In `hook-config.ts`, SessionStart changes from file-writing script to `fireAndForget` API POST (same pattern as all other hooks):
|
|
|
|
```typescript
|
|
// Before:
|
|
SessionStart: [{ hooks: [{ type: 'command', command: hookPath, timeout: 2 }] }]
|
|
|
|
// After:
|
|
SessionStart: [{ hooks: [{ type: 'command', command: fireAndForget('session-start'), timeout: 2 }] }]
|
|
```
|
|
|
|
### SESSION_CREATED Message Payload
|
|
|
|
```typescript
|
|
// Before:
|
|
{ type: 'session-created', sessionId: string }
|
|
|
|
// After:
|
|
{ type: 'session-created', sessionId: string, cliSessionId: string }
|
|
```
|
|
|
|
`useChat` hook stores both IDs. `ChatView` header displays `cliSessionId` (primary) and `sessionId` (secondary).
|
|
|
|
### Recovery from Non-Graceful Shutdown
|
|
|
|
If the server crashes (kill -9, power loss) without running the shutdown flow:
|
|
|
|
1. Tmux session `codetap` may still be alive with running CLI instances
|
|
2. On next server start, DB still has session records (SQLite persists)
|
|
3. When hooks fire from surviving CLI instances → `resolveSessionId`:
|
|
- Finds the session in DB by `cli_session` UUID
|
|
- Restores the **original** internal ID from DB (e.g., `claude-1774210269126`)
|
|
- Re-creates in-memory mapping
|
|
- Session reappears in Active Sessions with its original ID
|
|
|
|
If the CLI session hasn't fired a SessionStart hook yet (e.g., Codex before first interaction):
|
|
- Session stays in DB with `cli_session = NULL`
|
|
- Once hook fires → DB record updated with CLI UUID
|
|
- UI shows UUID after hook fires (brief grace period showing internal ID only)
|
|
|
|
### CLI Changes (`bin/codetap`)
|
|
|
|
**`codetap new [--adapter <name>]`**
|
|
|
|
```bash
|
|
codetap new # WINDOW_NAME="claude-$(date +%s)", runs: claude
|
|
codetap new --adapter codex # WINDOW_NAME="codex-$(date +%s)", runs: codex
|
|
codetap new --adapter gemini # WINDOW_NAME="gemini-$(date +%s)", runs: gemini
|
|
```
|
|
|
|
Default adapter: `claude`. The `--adapter` flag determines both the window name prefix and the CLI command to run.
|
|
|
|
**`codetap --resume <id>`**
|
|
|
|
```
|
|
Input: internal ID or CLI UUID
|
|
↓
|
|
Is it internal ID format? ({adapter}-{digits})
|
|
├─ Yes → extract adapter from prefix, query DB for CLI UUID
|
|
│ ├─ Found → run: {adapter} --resume {uuid}
|
|
│ └─ Not found → error
|
|
└─ No (UUID format) → query DB by cli_session
|
|
├─ Found → get adapter from DB → run: {adapter} --resume {uuid}
|
|
└─ Not found → scan JSONL directories per adapter
|
|
├─ Found → detected adapter → run: {adapter} --resume {uuid}
|
|
└─ Not found → error: "Session not found"
|
|
```
|
|
|
|
**`codetap --continue [--adapter <name>]`**
|
|
|
|
Pass through to adapter CLI's native continue command:
|
|
- `claude --continue`
|
|
- `codex resume --last`
|
|
|
|
Window name: `{adapter}-{timestamp}` (same format as `new`).
|
|
|
|
SessionStart hook handles the mapping automatically when the CLI starts — even if the continued session was never managed by CodeTap before.
|
|
|
|
Default adapter: `claude`. With `--adapter codex`, runs codex's native continue command instead.
|
|
|
|
**`codetap -a / -A`**
|
|
|
|
Enhanced display:
|
|
|
|
```
|
|
Active sessions for code-tap:
|
|
|
|
1) claude-1774210269126
|
|
UUID: 625c60d0-aedb-4e0b-b78e-c9fbf0405e67
|
|
reply pong...
|
|
|
|
2) codex-1774210345678
|
|
UUID: abc12345-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
|
fix the login bug...
|
|
|
|
Select (1-2):
|
|
```
|
|
|
|
### Files to Modify
|
|
|
|
**Server:**
|
|
- `server/db.ts` — Schema migration, rename column, add `adapter` field, add `clearAll()`, remove session-map.json migration
|
|
- `server/adapters/claude/tmux-adapter.ts` — Remove `desktop-` logic from `resolveSessionId`, change `session-` to `claude-` in `startSession`, add `session-start` handler, restore original ID on recovery
|
|
- `server/adapters/claude/hook-config.ts` — Change SessionStart from `hookPath` script to `fireAndForget('session-start')`
|
|
- `server/adapters/claude/index.ts` — Add `session-start` hook route
|
|
- `server/adapters/codex/codex-tmux-adapter.ts` — Same pattern: `codex-` prefix, unified session-start handling
|
|
- `server/adapters/codex/index.ts` — Add `session-start` hook route if missing
|
|
- `server/adapters/interface.ts` — Add `adapter` field to `ActiveSessionInfo`
|
|
- `server/session-manager.ts` — Pass `cliSessionId` in `SESSION_CREATED` message
|
|
- `server/index.ts` — Call `dbSessions.clearAll()` in shutdown
|
|
- `server/config.ts` — Remove `sessionMap` path config
|
|
|
|
**Client:**
|
|
- `src/hooks/useChat.ts` — Store `cliSessionId` from `SESSION_CREATED`
|
|
- `src/components/ChatView.tsx` — Header shows CLI UUID (primary) + internal ID (secondary)
|
|
- `src/components/SessionsView.tsx` — No change (already shows `firstPrompt`)
|
|
|
|
**CLI:**
|
|
- `bin/codetap` — Add `--adapter` flag, change window naming, update resume/continue logic, enhance `-a`/`-A` display
|
|
- `bin/codetap-hook` — Delete (replaced by API POST)
|
|
|
|
### E2E Spec Updates (`tests/e2e-spec.feature`)
|
|
|
|
The following scenarios need to be updated to reflect the new session ID architecture:
|
|
|
|
1. **Chat header display** (L247): Update to show CLI UUID (primary) + internal ID (secondary) with copy icon
|
|
2. **CLI `--adapter` flag** (L1168-1475): Add scenarios for `codetap new --adapter`, `codetap --continue --adapter`
|
|
3. **Active sessions `-a`/`-A` display** (L1212): Update to show UUID + internal ID format
|
|
4. **session-map.json references** (L1308): Remove; update to DB-based recovery
|
|
5. **Session Deduplication regression** (L1829): Update to reflect Connect button fix (claudeSessionId → sessionId)
|
|
6. **SessionStart hook**: Add scenario documenting API POST flow (replaces file-writing script)
|
|
7. **tmux window naming** (L1176): Specify `{adapter}-{timestamp}` format
|
|
8. **Non-graceful restart recovery** (L1308): Add scenario for restoring original ID from DB
|
|
9. **Active session card UUID field** (L1548): Clarify where UUIDs appear (title vs expanded view)
|
|
|
|
### What Gets Removed
|
|
|
|
- `bin/codetap-hook` script
|
|
- `session-map.json` mechanism (writing, reading, migration)
|
|
- `desktop-` prefix logic in `resolveSessionId`
|
|
- `is_active` column from sessions table
|
|
- `sessionMap` path in config
|