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
12 KiB
Remaining Session Fixes — Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task.
Goal: Complete session architecture cleanup: remove pending guessing, remove desktop-discovery, add session API endpoints, update bin/codetap.
Spec: docs/superpowers/specs/2026-03-24-remaining-session-fixes.md
Task 1: Codex handleSessionStart — remove pending matching, add _pendingHookBodies
Files: server/adapters/codex/codex-tmux-adapter.ts
- Step 1: Add _pendingHookBodies field
private _pendingHookBodies: Map<string, CodexHookBody> = new Map();
- Step 2: Rewrite handleSessionStart (line 275)
Replace the entire method body:
handleSessionStart(body: CodexHookBody): void {
const codexUuid = body.session_id;
if (!codexUuid) return;
// 1. Already managed
if (this.sessions.has(codexUuid)) {
this._applySessionStartBody(codexUuid, body);
return;
}
// 2. Has pending sessions → store hook body, let _watchForTranscript match later
const hasPending = [...this.sessions.values()].some(s => s._watcherPending);
if (hasPending) {
this._pendingHookBodies.set(codexUuid, body);
return;
}
// 3. Not our session → ignore
}
Remove the old pendingSessions.length === 1 block (lines 297-308).
Remove the "desktop/unknown origin" block (lines 309-330).
- Step 3: Update _watchForTranscript to read _pendingHookBodies after rekey
In the scanOnce function, after _rekeyAndRename(sessionId, uuid) succeeds, check for stored hook body:
const hookBody = this._pendingHookBodies.get(uuid);
if (hookBody) {
this._applySessionStartBody(uuid, hookBody);
this._pendingHookBodies.delete(uuid);
}
- Step 4: Add cleanup for _pendingHookBodies
In _startSessionCleanup interval, add a sweep:
// Clean up stale pending hook bodies (older than 60s)
const now = Date.now();
for (const [uuid, body] of this._pendingHookBodies) {
// Use a timestamp field or just clean up all entries periodically
this._pendingHookBodies.delete(uuid);
}
Actually simpler: clean up in _pendingHookBodies when adding — if size > 10, delete oldest. Or just clear all entries older than 60s by storing a timestamp alongside.
- Step 5: Commit
git commit -m "refactor: Codex handleSessionStart uses _pendingHookBodies, no pending guessing"
Task 2: Remove desktop-discovery from both adapters
Files: server/adapters/claude/tmux-adapter.ts, server/adapters/codex/codex-tmux-adapter.ts
- Step 1: Claude — simplify handleSessionStart (line 512)
Current code (lines 512-539) does:
sessions.has(cliUuid)→ update lastActivity → return- List tmux windows → search for
w.command.includes('claude') && !sessions.has(w.name)→ create session
Remove step 2 entirely. The method becomes:
async handleSessionStart(body: HookBody): Promise<void> {
const cliUuid = body.session_id;
if (!cliUuid) return;
if (this.sessions.has(cliUuid)) {
this.sessions.get(cliUuid)!.lastActivity = Date.now();
return;
}
// Unknown UUID — not our session, ignore
}
Also remove the await tmuxManager.listWindows() call (no longer needed).
- Step 2: Codex — verify desktop-discovery already removed in Task 1
Check that Task 1's rewrite of handleSessionStart has no desktop-discovery path.
- Step 3: Commit
git commit -m "refactor: remove desktop-discovery from both adapters"
Task 3: Add POST /api/sessions/start and /resume endpoints
Files: server/index.ts
- Step 1: Add POST /api/sessions/start
Place after the existing session endpoints (after DELETE /api/active-sessions/:id):
app.post('/api/sessions/start', authMiddleware, async (req: Request, res: Response) => {
try {
const { adapter: adapterName, cwd, model, permissionMode } = req.body;
if (!cwd) return res.status(400).json({ error: 'cwd required' });
const adapter = getAdapter(adapterName || DEFAULT_ADAPTER);
if (!adapter) return res.status(400).json({ error: `Unknown adapter: ${adapterName}` });
const handle = await adapter.startSession(cwd, { model, permissionMode });
// Register in sessionAdapterMap so events route correctly
sessionAdapterMap.set(handle.sessionId, adapterName || DEFAULT_ADAPTER);
res.json({ sessionId: handle.sessionId });
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
Note: import sessionAdapterMap — check if it's already accessible. It's a module-level variable in session-manager.ts. May need to export a helper function registerSessionAdapter(sessionId, adapterName) from session-manager.
Actually, looking at the code: sessionAdapterMap is defined in session-manager.ts as a module-level const. It's NOT exported. The handleQuery function accesses it directly because it's in the same file.
For server/index.ts to set it, we need either:
a) Export sessionAdapterMap from session-manager.ts
b) Add a registerSessionAdapter(id, name) helper exported from session-manager.ts
c) Have startSession trigger an event that session-manager listens to
Option (b) is cleanest.
- Step 2: Add POST /api/sessions/resume
app.post('/api/sessions/resume', authMiddleware, async (req: Request, res: Response) => {
try {
const { sessionId, adapter: adapterName, cwd } = req.body;
if (!sessionId) return res.status(400).json({ error: 'sessionId required' });
// Determine adapter if not provided
let resolvedAdapterName = adapterName;
if (!resolvedAdapterName) {
// Try to detect from JSONL file location
// ... (use existing findSessionFile logic from each adapter's jsonl-store)
resolvedAdapterName = DEFAULT_ADAPTER;
}
const adapter = getAdapter(resolvedAdapterName);
if (!adapter) return res.status(400).json({ error: `Unknown adapter: ${resolvedAdapterName}` });
const handle = await adapter.resumeSession(sessionId, cwd || process.cwd());
registerSessionAdapter(handle.sessionId, resolvedAdapterName);
res.json({ sessionId: handle.sessionId });
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
- Step 3: Export registerSessionAdapter from session-manager.ts
export function registerSessionAdapter(sessionId: string, adapterName: string): void {
sessionAdapterMap.set(sessionId, adapterName);
}
- Step 4: Commit
git commit -m "feat: add POST /api/sessions/start and /resume endpoints"
Task 4: Update bin/codetap to use API endpoints
Files: bin/codetap
NOTE: sqlite3 references were already removed in Fix 5. This task replaces direct tmux new-window calls with API calls.
- Step 1: Add authentication function
Near the top of the script (after the server-running check):
get_auth_token() {
curl -sk -X POST "https://localhost:$PORT/api/auth/login" \
-H "Content-Type: application/json" \
-d "{\"password\":\"$CLAUDE_UI_PASSWORD\"}" 2>/dev/null | \
python3 -c 'import sys,json; print(json.load(sys.stdin).get("token",""))' 2>/dev/null
}
- Step 2: Replace
newsession creation
Find the block that does tmux new-window ... "$COMMAND". Replace with:
AUTH_TOKEN=$(get_auth_token)
if [ -z "$AUTH_TOKEN" ]; then
echo "Error: Failed to authenticate with CodeTap server"
exit 1
fi
RESULT=$(curl -sk -X POST "https://localhost:$PORT/api/sessions/start" \
-H "Authorization: Bearer $AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"adapter\":\"$ADAPTER\",\"cwd\":\"$(pwd)\"}")
SESSION_ID=$(echo "$RESULT" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("sessionId",""))' 2>/dev/null)
if [ -z "$SESSION_ID" ] || [ "$SESSION_ID" = "null" ]; then
echo "Error: Failed to create session"
echo "$RESULT"
exit 1
fi
tmux select-window -t "$TMUX_SESSION:$SESSION_ID"
- Step 3: Replace
--resumewith API call
AUTH_TOKEN=$(get_auth_token)
RESULT=$(curl -sk -X POST "https://localhost:$PORT/api/sessions/resume" \
-H "Authorization: Bearer $AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"sessionId\":\"$RESUME_ID\",\"adapter\":\"$ADAPTER\",\"cwd\":\"$(pwd)\"}")
SESSION_ID=$(echo "$RESULT" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("sessionId",""))' 2>/dev/null)
tmux select-window -t "$TMUX_SESSION:$SESSION_ID"
- Step 4: Replace
--continue
Find most recent tmux window, resume it:
LATEST=$(tmux list-windows -t "$TMUX_SESSION" -F '#{window_activity} #{window_name}' 2>/dev/null | sort -rn | head -1 | awk '{print $2}')
if [ -n "$LATEST" ] && [ "$LATEST" != "main" ]; then
tmux select-window -t "$TMUX_SESSION:$LATEST"
else
echo "No active sessions to continue"
exit 1
fi
- Step 5: Verify -a listing uses tmux directly
Should already be tmux-based (from Fix 5). Verify no remaining sqlite3 references.
- Step 6: Commit
git commit -m "refactor: bin/codetap uses API endpoints for session creation"
Self-Review Checklist
Compilation safety
- Task 1 changes only Codex adapter internals → compiles independently ✅
- Task 2 simplifies Claude handleSessionStart → compiles independently ✅
- Task 3 adds new endpoints, needs
registerSessionAdapterexport → export first, then add endpoints ✅ - Task 4 is shell script only → no compilation ✅
Codex _watchForTranscript flow after Task 1
1. startSession → temp key in Map, _watcherPending = true
2. pasteToSession → marker + prompt pasted
3. SessionStart hook fires → has pending → stored in _pendingHookBodies
4. _watchForTranscript detects JSONL → reads marker → matches temp key
5. _rekeyAndRename(tempKey, uuid) → rekey + rename
6. Read _pendingHookBodies(uuid) → apply transcript_path, cwd
7. Start JSONL watcher
All steps covered ✅
Claude handleSessionStart after Task 2
handleSessionStart(body):
sessions.has(uuid) → true → update → return
→ false → ignore
Two lines. Very simple. No matching, no discovery. ✅
bin/codetap after Task 4
new: API call → tmux select-window ✅--resume: API call → tmux select-window ✅--continue: tmux list-windows → select most recent ✅-a: tmux list-windows directly ✅- No sqlite3 references ✅
- Requires server running + password (already a requirement) ✅
Edge cases
- bin/codetap when server is down: API calls fail → script shows error → user knows server needs to be running. This is acceptable since CodeTap server is required for all functionality.
- Multiple pending sessions with same UUID in _pendingHookBodies: Won't happen — UUIDs are unique per CLI session.
- _pendingHookBodies grows unbounded: Mitigated by cleanup in _startSessionCleanup (60s sweep).
- bin/codetap Codex new session — temp key returned: Script does
tmux select-window -t codetap:codex-{timestamp}. After rekey, window renamed to UUID. User is already inside — unaffected.
No changes needed
server/session-manager.ts— only needsregisterSessionAdapterexport (Task 3)server/db.ts— no changes- Frontend — no changes
server/adapters/claude/tmux-manager.ts— no changes
Verification
- Server starts cleanly
- New Codex session from Web UI → hook stored in _pendingHookBodies → _watchForTranscript matches → rekey
- New Claude session from Web UI → works (no matching needed)
bin/codetap new --adapter claude→ API call → session created → window selectedbin/codetap new --adapter codex→ API call → session created → window selectedbin/codetap --resume UUID→ API call → session resumedbin/codetap -a→ lists sessions from tmux- Desktop-started sessions (user runs
claude/codexdirectly) → hooks ignored by CodeTap (expected)