Files
clawtap/docs/superpowers/plans/2026-03-24-window-name-to-uuid.md
kuannnn 42861ea7fa feat: ClawTap v0.1.0 — initial release
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
2026-03-26 10:40:26 +08:00

410 lines
15 KiB
Markdown

# Window Name to CLI UUID + Backward Compat Cleanup Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Two things: (1) Use CLI UUID as tmux window name, eliminating `window_name` column and all name-mapping. (2) Delete all backward-compat code (old migrations, deprecated aliases) since the app is pre-release.
**Architecture:** 5 tasks: (1) add renameWindow to TmuxManager, (2) update adapters to use CLI UUID as window name, (3) clean up DB — remove window_name + delete old migrations + simplify schema, (4) remove ActiveSessionInfo.cliSessionId from public API, (5) update bin/codetap.
**Tech Stack:** TypeScript, SQLite, tmux, Shell
**After this plan completes, the session ID system is fully clean:**
- Single ID everywhere: CLI UUID
- tmux window name = CLI UUID
- No translations, no mappings, no deprecated aliases
- DB has minimal schema with no migration chain
---
### Task 1: Add renameWindow to TmuxManager
**Files:**
- Modify: `server/adapters/claude/tmux-manager.ts`
- [ ] **Step 1: Add renameWindow method**
After `killWindow()`, add:
```typescript
async renameWindow(windowId: string, newName: string): Promise<void> {
const target = `${SESSION_NAME}:${windowId}`;
await exec(TMUX, ['rename-window', '-t', target, newName]);
}
```
- [ ] **Step 2: Commit**
```bash
git add server/adapters/claude/tmux-manager.ts
git commit -m "feat: add renameWindow to TmuxManager"
```
---
### Task 2: Use CLI UUID as tmux window name in both adapters
**Files:**
- Modify: `server/adapters/claude/tmux-adapter.ts`
- Modify: `server/adapters/codex/codex-tmux-adapter.ts`
**Claude adapter:**
- [ ] **Step 1: startSession — use CLI UUID as window name**
Remove `const windowName = ...`. Pass `sessionId` (CLI UUID) directly:
```typescript
const windowId = await tmuxManager.createWindow(sessionId, cwd, parts.join(' '));
```
Update `dbSessions.upsert` — pass `undefined` for windowName (removed in Task 3):
```typescript
dbSessions.upsert(sessionId, cwd, windowId, undefined, 'claude');
```
- [ ] **Step 2: resumeSession — use CLI UUID as window name**
```typescript
const windowId = await tmuxManager.createWindow(cliUuid, cwd, command);
```
- [ ] **Step 3: attachSession — same pattern**
Remove windowName from upsert calls, pass `undefined`.
- [ ] **Step 4: Update _handleSessionStart discovery**
Replace `w.name.startsWith('claude-')` with:
```typescript
if (w.command.includes('claude') && !this.sessions.has(w.name)) {
```
This works because: CodeTap-created windows have CLI UUID names (which are in the sessions Map if managed). Desktop-started windows have arbitrary names (not in the Map). Either way, checking `!this.sessions.has(w.name)` correctly identifies unmanaged windows.
- [ ] **Step 5: Simplify _findWindowForSession**
```typescript
private async _findWindowForSession(sessionId: string, windowList?: TmuxWindow[]): Promise<string | null> {
const windows = windowList || await tmuxManager.listWindows();
// Primary: check DB for stored window_id
const dbRow = dbSessions.get(sessionId);
if (dbRow?.window_id && windows.some(w => w.id === dbRow.window_id)) {
return dbRow.window_id;
}
// Fallback: match by name (window name = CLI UUID = sessionId)
const match = windows.find(w => w.name === sessionId);
return match?.id || null;
}
```
Note: `listWindows()` is called once and reused for both checks.
**Codex adapter:**
- [ ] **Step 6: startSession — temp name, then rename**
```typescript
const tempName = `codex-${Date.now()}`;
const windowId = await tmuxManager.createWindow(tempName, cwd, parts.join(' '));
// ... _waitForReady, _watchForTranscript ...
const cliUUID = await this._waitForCliUUID(tempName);
// Rename tmux window to CLI UUID
const session = this.sessions.get(cliUUID);
if (session?.windowId) {
await tmuxManager.renameWindow(session.windowId, cliUUID);
}
return { sessionId: cliUUID };
```
- [ ] **Step 7: resumeSession — use CLI UUID as window name**
```typescript
const windowId = await tmuxManager.createWindow(codexUuid, cwd, parts.join(' '));
```
- [ ] **Step 8: Pass undefined for windowName in all upsert calls**
Both adapters: `dbSessions.upsert(id, cwd, windowId, undefined, adapter)`.
- [ ] **Step 9: Commit**
```bash
git add server/adapters/claude/tmux-adapter.ts server/adapters/codex/codex-tmux-adapter.ts
git commit -m "refactor: use CLI UUID as tmux window name"
```
---
### Task 3: Clean up DB — remove window_name, delete old migrations, simplify
**Files:**
- Modify: `server/db.ts`
This task does 3 things: (a) remove `window_name` column, (b) delete ALL old schema migrations, (c) delete `migrateJsonToSqlite`. Since the app is pre-release, no backward compat needed.
- [ ] **Step 1: Replace entire initDB() migration section with a single clean schema**
Delete ALL migration code in initDB():
- `claude_session → cli_session` rename (~line 83-85)
- `cli_session → id + window_name` table rebuild (~line 93-117)
- Any `PRAGMA table_info` checks
Replace the CREATE TABLE with the FINAL clean schema:
```sql
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
cwd TEXT NOT NULL,
window_id TEXT,
adapter TEXT DEFAULT 'claude',
permission_mode TEXT DEFAULT 'default',
created_at TEXT DEFAULT (datetime('now')),
last_activity TEXT DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_sessions_window ON sessions(window_id);
```
No `cli_session`, no `window_name`, no `claude_session`. Just the final schema.
- [ ] **Step 2: Delete migrateJsonToSqlite function**
Remove the entire `migrateJsonToSqlite` function (~line 282-309) and its exported types (`JsonPushSub`, etc.). Also remove its caller — grep for `migrateJsonToSqlite` in `server/index.ts`.
- [ ] **Step 3: Update SessionRow interface**
```typescript
export interface SessionRow {
id: string; // CLI UUID
cwd: string;
window_id: string | null;
adapter: string;
permission_mode: string;
created_at: string;
last_activity: string;
}
```
Remove `window_name` and `cli_session` fields entirely.
- [ ] **Step 4: Update upsert signature and SQL**
```typescript
upsert(id: string, cwd: string, windowId?: string, adapter?: string): void {
stmts().sessionsUpsert.run(id, cwd, windowId || null, adapter || 'claude');
},
```
SQL:
```sql
INSERT INTO sessions (id, cwd, window_id, adapter)
VALUES (?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
cwd = excluded.cwd,
window_id = excluded.window_id,
last_activity = datetime('now')
```
- [ ] **Step 5: Update ALL dbSessions.upsert callers**
Grep entire codebase. Change from 5-param to 4-param signature. Locations:
- `server/adapters/claude/tmux-adapter.ts` — all upsert calls (~4-5 sites)
- `server/adapters/codex/codex-tmux-adapter.ts` — all upsert calls (~4-5 sites)
- `server/adapters/codex/codex-tmux-adapter.ts``_waitForCliUUID` upsert
- [ ] **Step 6: Handle existing DB with old schema**
Since we deleted all migrations, if an old DB exists with `cli_session` or `window_name` columns, it will be incompatible. Add a simple destructive migration:
```typescript
// If old schema detected, just drop and recreate
const tableInfo = d.prepare("PRAGMA table_info('sessions')").all() as { name: string }[];
const hasOldColumns = tableInfo.some(c => c.name === 'cli_session' || c.name === 'window_name' || c.name === 'claude_session');
if (hasOldColumns) {
d.exec('DROP TABLE sessions');
// Table will be recreated by the CREATE TABLE IF NOT EXISTS above
d.exec(`CREATE TABLE sessions (...final schema...)`);
console.log('[db] Dropped old sessions table (pre-release cleanup)');
}
```
This is safe because the app is pre-release and `clearAll()` deletes all rows on shutdown anyway.
- [ ] **Step 7: Remove migrateJsonToSqlite caller from server/index.ts**
Grep for `migrateJsonToSqlite` in `server/index.ts` and remove the call.
- [ ] **Step 8: Commit**
```bash
git add server/db.ts server/index.ts server/adapters/claude/tmux-adapter.ts server/adapters/codex/codex-tmux-adapter.ts
git commit -m "refactor: clean DB schema — remove window_name, delete old migrations"
```
---
### Task 4: Remove ActiveSessionInfo.cliSessionId from public API
**Files:**
- Modify: `server/adapters/interface.ts`
- Modify: `server/adapters/claude/tmux-adapter.ts`
- Modify: `server/adapters/codex/codex-tmux-adapter.ts`
- Modify: `src/hooks/useSessions.ts`
- Modify: `src/components/SessionsView.tsx`
- Modify: `server/index.ts`
- [ ] **Step 1: Remove cliSessionId from ActiveSessionInfo**
In `server/adapters/interface.ts`, remove:
```typescript
/** @deprecated Use sessionId instead — same value after unification */
cliSessionId: string;
```
- [ ] **Step 2: Remove cliSessionId from getActiveSessions in both adapters**
In Claude's `getActiveSessions()`: remove `cliSessionId: session.cliSessionId` from the returned object.
In Codex's `getActiveSessions()`: same.
- [ ] **Step 3: Update frontend useSessions.ts**
Change `if (s.cliSessionId) ids.add(s.cliSessionId)` to `if (s.sessionId) ids.add(s.sessionId)`. (May already be done — verify.)
- [ ] **Step 4: Update SessionsView.tsx**
Remove any remaining `session.cliSessionId` references. Use `session.sessionId` everywhere.
- [ ] **Step 5: Update server/index.ts active-sessions endpoint**
The active-sessions handler may still reference `s.cliSessionId` for child filtering. Change to `s.sessionId`.
- [ ] **Step 6: TypeScript compilation check**
`npx tsc --noEmit` — zero errors.
- [ ] **Step 7: Commit**
```bash
git add server/adapters/interface.ts server/adapters/claude/tmux-adapter.ts server/adapters/codex/codex-tmux-adapter.ts src/hooks/useSessions.ts src/components/SessionsView.tsx server/index.ts
git commit -m "refactor: remove deprecated cliSessionId from public API"
```
---
### Task 5: Update bin/codetap
**Files:**
- Modify: `bin/codetap`
- [ ] **Step 1: get_project_sessions() — query id**
```bash
# Before: SELECT window_name FROM sessions WHERE cwd=...
# After: SELECT id FROM sessions WHERE cwd=...
```
tmux window names are now CLI UUIDs = DB `id`.
- [ ] **Step 2: -a listing — match by id**
```bash
# Before: SELECT id, adapter, window_name, cwd FROM sessions WHERE window_name IN (...)
# After: SELECT id, adapter, cwd FROM sessions WHERE id IN (...)
```
- [ ] **Step 3: --resume — simplified**
```bash
# Before: WHERE id='...' OR window_name='...'
# After: WHERE id='...'
```
- [ ] **Step 4: Window name generation for new/continue**
Generate UUID for Claude (use `--session-id` value):
```bash
SESSION_UUID=$(python3 -c 'import uuid; print(uuid.uuid4())')
WINDOW_NAME="$SESSION_UUID"
# For Claude: pass --session-id $SESSION_UUID
```
For Codex: use temp name, server will rename after UUID discovery.
- [ ] **Step 5: Remove any window_name references**
Grep entire script for `window_name` — should be zero after above changes.
- [ ] **Step 6: Commit**
```bash
git add bin/codetap
git commit -m "refactor: bin/codetap uses CLI UUID as tmux window name"
```
---
## Self-Review Checklist
### Compilation Safety
- Task 2 passes `undefined` for windowName param → Task 3 removes the param. Between Tasks 2 and 3, the code compiles because `undefined` is valid for an optional `string?` param. ✅
- Task 4 removes `cliSessionId` from `ActiveSessionInfo`. All consumers updated in same task. ✅
### Codex _waitForCliUUID Flow
- Session starts under temp name `codex-{timestamp}` → stored in Map under temp key
- Hook/watcher sets `session.cliSessionId``_waitForCliUUID` polls and detects it
- `_waitForCliUUID` re-keys Map: delete temp key, set CLI UUID key
- **NEW**: `renameWindow(windowId, cliUUID)` renames the tmux window
- After this: window name = Map key = DB id = CLI UUID ✅
- `session.cliSessionId` field kept in `CodexSessionState` (needed for _waitForCliUUID polling). Not exposed publicly. ✅
### handleReconnect
- User clicks session → `registerClient(conn, sessionId)` where sessionId = CLI UUID
- `hasActiveWindow(sessionId)` checks if tmux window exists for this session
- After window name change: `_findWindowForSession(sessionId)` finds by `w.name === sessionId` (window name = CLI UUID) ✅
- Desktop later opens same session → events broadcast to CLI UUID → mobile receives ✅
### DB Schema Final State
```sql
sessions: id(PK/UUID), cwd, window_id(@N), adapter, permission_mode, created_at, last_activity
session_reviews: id, parent_cli_session_id, child_cli_session_id, child_adapter, ...
```
No `cli_session`, no `window_name`, no `claude_session`. Clean. ✅
### Old DB Handling
- If old DB exists with legacy columns → DROP TABLE + recreate. Data loss is fine (pre-release). ✅
- `session_reviews` table is not touched — it was created with the correct schema. ✅
### bin/codetap
- `-a` mode: tmux window names are now UUIDs, DB `id` is UUID → direct IN clause match ✅
- `--resume`: accepts UUID → `WHERE id='...'`
- `new` mode: generates UUID as window name ✅
- `--continue`: queries most recent session by `id` → resume it ✅
### Things NOT changed (correct to leave alone)
- `SessionState.cliSessionId` in both adapters — needed internally for Codex UUID discovery. Not exposed publicly after Task 4. ✅
- `session_reviews` table column names (`parent_cli_session_id`, `child_cli_session_id`) — these are just column names, not related to the internal ID concept. They store CLI UUIDs. ✅
- `tmux-manager.ts` `TmuxWindow.name` field — still populated from `#{window_name}` tmux format. Now contains CLI UUID. ✅
### Potential Issues
- **UUID as tmux tab name is long (36 chars)** — cosmetic only, tmux truncates display. Not a functional issue.
- **Desktop-started sessions (not via CodeTap)** — their tmux window name won't be a UUID. But `handleSessionStart` uses `w.command.includes('claude')` for discovery, not window name format. Hook body provides the CLI UUID. ✅
- **`python3 -c 'import uuid; print(uuid.uuid4())'` in bin/codetap** — requires Python 3. Could use `uuidgen` instead (available on macOS). Safer: `uuidgen | tr '[:upper:]' '[:lower:]'`
---
## Verification
1. Delete `~/.codetap/codetap.db` to start fresh (or let migration drop old table)
2. `CLAUDE_UI_PASSWORD=test npm run dev` — server starts cleanly
3. `tmux list-windows -t codetap` — windows named with CLI UUIDs
4. Click historical session → history loads immediately
5. New Claude session → window named with UUID, messages work
6. New Codex session → starts with temp name, renamed to UUID
7. `bin/codetap -a` → lists sessions
8. `bin/codetap --resume <UUID>` → works
9. Active sessions tab → shows sessions, no `cliSessionId` references
10. `grep -rn "window_name\|cli_session\|cliSessionId\|claude_session" server/ src/` → zero results (except internal `SessionState.cliSessionId` in adapters)