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
533 lines
20 KiB
Markdown
533 lines
20 KiB
Markdown
# Unified Session Creation Path 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. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
**Goal:** Unify Cross-AI Review child session creation to use the same WS QUERY path as normal sessions, eliminating the HTTP-creates-session / WS-reconnects split.
|
|
|
|
**Spec:** `docs/superpowers/specs/2026-03-25-unified-session-path-design.md`
|
|
|
|
**Architecture:** Merge `sendMessage` and `pasteToSession` in BOTH adapters (Codex + Claude) so QUERY handles any content size. Move session creation from POST /api/reviews to FloatingReviewPanel's useChat QUERY. POST /api/reviews becomes a registration-only endpoint called after the session exists.
|
|
|
|
---
|
|
|
|
## Edge Cases & Scenarios
|
|
|
|
Before reading the tasks, understand all scenarios this plan must handle:
|
|
|
|
| # | Scenario | Path | Notes |
|
|
|---|----------|------|-------|
|
|
| A | Normal Codex session from WebUI | QUERY → handleQuery → startSession → registerClient → sendMessage | ✅ Already works |
|
|
| B | Cross-AI Review child (same device) | QUERY → handleQuery (same as A) → then POST /api/reviews/register | ✅ New unified path |
|
|
| C | Multi-device: other device connects to parent with active review | RECONNECT → handleReconnect loads active reviews → REVIEW_STARTED → FloatingReviewPanel mounts → RECONNECT to child | ⚠️ RECONNECT path must be preserved |
|
|
| D | Page refresh: reconnect to parent + active review | Same as C | ⚠️ RECONNECT path must be preserved |
|
|
| E | registerReview POST fails after session created | Session exists but no DB record → retry or show error | ⚠️ Error handling needed |
|
|
| F | User clicks End before registerReview completes | reviewId is empty → must not call endReview('') | ⚠️ Guard needed |
|
|
| G | Send-back to Claude parent | Claude sendMessage must handle large multiline text | ⚠️ Claude merge needed |
|
|
| H | Send-back to Codex parent | Codex sendMessage already handles (Task 1) | ✅ |
|
|
| I | CODETAP_REF marker injection | handleQuery injects for non-Claude → sendMessage auto-splits | ✅ |
|
|
|
|
**Key constraint: RECONNECT path must be preserved** for scenarios C and D. FloatingReviewPanel must support BOTH:
|
|
- New path: `initialPrompt` provided, no `childSessionId` → useChat QUERY (creates session)
|
|
- Reconnect path: `childSessionId` provided, no `initialPrompt` → useChat RECONNECT (joins existing session)
|
|
|
|
---
|
|
|
|
### Task 1: Merge sendMessage and pasteToSession in BOTH adapters
|
|
|
|
**Files:**
|
|
- `server/adapters/codex/codex-tmux-adapter.ts`
|
|
- `server/adapters/codex/index.ts`
|
|
- `server/adapters/claude/tmux-adapter.ts`
|
|
- `server/adapters/claude/index.ts`
|
|
|
|
This task is standalone — makes `sendMessage` handle all content sizes in both adapters without breaking anything.
|
|
|
|
- [ ] **Step 1: Rewrite Codex `sendMessage()` (lines 204-221)**
|
|
|
|
Merge the logic from `pasteToSession()` (lines 223-258) into `sendMessage()`:
|
|
|
|
```typescript
|
|
async sendMessage(sessionId: string, text: string, options: QueryOptions = {}): Promise<void> {
|
|
const session = this.sessions.get(sessionId);
|
|
if (!session) throw new Error(`Session ${sessionId} not found`);
|
|
|
|
session._promptSenderClientId = options.clientId || null;
|
|
session.isProcessing = true;
|
|
|
|
// Restart pane monitor if it was stopped
|
|
if (!session.monitor) {
|
|
this._startMonitor(sessionId, session.windowId);
|
|
}
|
|
|
|
// Large or multiline content: use pasteBuffer (fast, handles newlines)
|
|
if (text.length > 500 || text.includes('\n')) {
|
|
const singleLine = text.replace(/\n/g, '\\n');
|
|
|
|
// Fresh Codex sessions have TUI placeholder text. If content starts with
|
|
// CODETAP_REF marker, send marker via sendKeys first (clears placeholder),
|
|
// then pasteBuffer the rest.
|
|
const markerMatch = singleLine.match(/^\[CODETAP_REF:[^\]]+\]/);
|
|
if (markerMatch) {
|
|
const marker = markerMatch[0];
|
|
const rest = singleLine.substring(marker.length);
|
|
await tmuxManager.sendKeys(session.windowId, marker, false);
|
|
await new Promise<void>(r => setTimeout(r, 200));
|
|
if (rest) {
|
|
await tmuxManager.pasteBuffer(session.windowId, rest, false);
|
|
}
|
|
} else {
|
|
await tmuxManager.pasteBuffer(session.windowId, singleLine, false);
|
|
}
|
|
await new Promise<void>(r => setTimeout(r, 300));
|
|
await tmuxManager.sendControl(session.windowId, 'Enter');
|
|
} else {
|
|
// Short text: sendKeys (character-by-character)
|
|
await tmuxManager.sendKeys(session.windowId, text, false);
|
|
await new Promise<void>(r => setTimeout(r, 200));
|
|
await tmuxManager.sendControl(session.windowId, 'Enter');
|
|
}
|
|
|
|
// If there are pending hook bodies waiting for marker matching, try now
|
|
if (this._pendingHookBodies.size > 0 && session._watcherPending) {
|
|
this._tryMatchPending(sessionId);
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Remove Codex `pasteToSession()` method (lines 223-258)**
|
|
|
|
Delete the entire method from `CodexTmuxAdapter`.
|
|
|
|
- [ ] **Step 3: Update `CodexAdapter.pasteToSession` in `server/adapters/codex/index.ts`**
|
|
|
|
Delegate to sendMessage (keeps public API working until Task 3 removes callers):
|
|
|
|
```typescript
|
|
async pasteToSession(sid: string, content: string): Promise<void> {
|
|
return this._tmux.sendMessage(sid, content);
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Update Claude `sendMessage()` in `server/adapters/claude/tmux-adapter.ts`**
|
|
|
|
Currently Claude's `sendMessage` always uses `sendKeys(text, true)`. Add large content handling:
|
|
|
|
```typescript
|
|
async sendMessage(sessionId: string, text: string, options: QueryOptions = {}): Promise<void> {
|
|
const session = this.sessions.get(sessionId);
|
|
if (!session) throw new Error(`Session ${sessionId} not found`);
|
|
session._promptSenderClientId = options.clientId || null;
|
|
if (!session.monitor) {
|
|
this._startMonitor(sessionId, session.windowId);
|
|
}
|
|
|
|
// Large or multiline content: use pasteBuffer (fast)
|
|
if (text.length > 500 || text.includes('\n')) {
|
|
await tmuxManager.pasteBuffer(session.windowId, text);
|
|
} else {
|
|
await tmuxManager.sendKeys(session.windowId, text, true);
|
|
}
|
|
}
|
|
```
|
|
|
|
Note: Claude's `pasteBuffer` already handles Enter (sendEnter defaults to true in tmux-manager). Claude doesn't need `\n` → `\\n` replacement or CODETAP_REF marker splitting (Claude generates its own UUID upfront, no placeholder issue).
|
|
|
|
- [ ] **Step 5: Update `ClaudeAdapter.pasteToSession` in `server/adapters/claude/index.ts`**
|
|
|
|
Delegate to sendMessage:
|
|
|
|
```typescript
|
|
async pasteToSession(sid: string, content: string): Promise<void> {
|
|
return this._tmux.sendMessage(sid, content);
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 6: Verify TypeScript compilation**
|
|
|
|
```bash
|
|
npx tsc --noEmit
|
|
```
|
|
|
|
- [ ] **Step 7: Commit**
|
|
|
|
```bash
|
|
git add server/adapters/codex/codex-tmux-adapter.ts server/adapters/codex/index.ts server/adapters/claude/tmux-adapter.ts server/adapters/claude/index.ts
|
|
git commit -m "refactor: merge sendMessage and pasteToSession in both adapters — auto-detect large content"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 2: Add registerReview API endpoint + update frontend
|
|
|
|
**Files:**
|
|
- `server/index.ts` — add POST /api/reviews/register
|
|
- `src/lib/api.ts` — add `registerReview()` function
|
|
- `src/components/ChatView.tsx` — handleReviewSelect uses local state, calls registerReview after session created
|
|
- `src/components/FloatingReviewPanel.tsx` — accept `initialPrompt`, auto-send via QUERY, support RECONNECT for multi-device
|
|
- `src/hooks/useChat.ts` — support `initialPrompt` for auto-sending first message
|
|
|
|
All files change together to maintain compilation.
|
|
|
|
- [ ] **Step 1: Add `registerReview` to `api.ts`**
|
|
|
|
```typescript
|
|
registerReview: (parentCliSessionId: string, childSessionId: string, targetAdapter: string, anchorMessageId: string, prompt: string, title: string) =>
|
|
request<{ reviewId: string }>('/api/reviews/register', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ parentCliSessionId, childSessionId, targetAdapter, anchorMessageId, prompt, title }),
|
|
}),
|
|
```
|
|
|
|
- [ ] **Step 2: Add POST /api/reviews/register endpoint in `server/index.ts`**
|
|
|
|
```typescript
|
|
app.post('/api/reviews/register', authMiddleware, async (req: Request, res: Response) => {
|
|
try {
|
|
const { parentCliSessionId, childSessionId, targetAdapter, anchorMessageId, prompt, title } = req.body;
|
|
if (!parentCliSessionId || !childSessionId) {
|
|
return res.status(400).json({ error: 'parentCliSessionId and childSessionId required' });
|
|
}
|
|
|
|
const parentAdapterName = sessionAdapterMap.get(parentCliSessionId) || DEFAULT_ADAPTER;
|
|
const reviewId = crypto.randomUUID();
|
|
sessionReviews.create(reviewId, parentCliSessionId, childSessionId, targetAdapter, parentAdapterName, anchorMessageId, prompt, title);
|
|
|
|
if (!sessionAdapterMap.has(childSessionId)) {
|
|
sessionAdapterMap.set(childSessionId, targetAdapter);
|
|
}
|
|
|
|
broadcastReviewStarted(parentCliSessionId, {
|
|
reviewId, childSessionId, childCliSessionId: childSessionId,
|
|
childAdapter: targetAdapter, anchorMessageId, reviewTitle: title,
|
|
});
|
|
|
|
res.json({ reviewId });
|
|
} catch (error) {
|
|
res.status(500).json({ error: (error as Error).message });
|
|
}
|
|
});
|
|
```
|
|
|
|
- [ ] **Step 3: Update FloatingReviewPanel — dual-path support**
|
|
|
|
**File:** `src/components/FloatingReviewPanel.tsx`
|
|
|
|
Update interface to support both paths:
|
|
|
|
```typescript
|
|
interface FloatingReviewPanelProps {
|
|
reviewId?: string; // empty until registerReview completes (new path)
|
|
childSessionId?: string; // empty for new session (QUERY), set for reconnect (RECONNECT)
|
|
childAdapter: string;
|
|
reviewTitle?: string;
|
|
panelState: 'expanded' | 'minimized' | 'hidden';
|
|
onPanelStateChange: (state: 'expanded' | 'minimized' | 'hidden') => void;
|
|
onEnd: () => void;
|
|
// New path only:
|
|
initialPrompt?: string; // review context to auto-send as first QUERY
|
|
cwd?: string;
|
|
onSessionCreated?: (childSessionId: string) => void;
|
|
}
|
|
```
|
|
|
|
useChat call:
|
|
|
|
```typescript
|
|
const {
|
|
messages, streaming, liveStatus, toolStatuses,
|
|
sendMessage: chatSendMessage, abort, sessionId: chatSessionId,
|
|
} = useChat(
|
|
childSessionId || undefined, // undefined → new session (QUERY); set → reconnect
|
|
initialPrompt, // auto-send as first message (new path only)
|
|
childAdapter,
|
|
cwd,
|
|
);
|
|
|
|
// Notify parent when session is created via QUERY (new path)
|
|
const notifiedRef = useRef(false);
|
|
useEffect(() => {
|
|
if (chatSessionId && !childSessionId && onSessionCreated && !notifiedRef.current) {
|
|
notifiedRef.current = true;
|
|
onSessionCreated(chatSessionId);
|
|
}
|
|
}, [chatSessionId, childSessionId, onSessionCreated]);
|
|
```
|
|
|
|
- [ ] **Step 4: Update useChat — support `initialPrompt` parameter**
|
|
|
|
**File:** `src/hooks/useChat.ts`
|
|
|
|
Update signature:
|
|
|
|
```typescript
|
|
export function useChat(
|
|
existingSessionId?: string,
|
|
initialPrompt?: string,
|
|
adapterOverride?: string,
|
|
cwdOverride?: string,
|
|
) {
|
|
```
|
|
|
|
Add ref and auto-send in WS onopen:
|
|
|
|
```typescript
|
|
const initialPromptSent = useRef(false);
|
|
|
|
// In the WS onopen handler, after connection established:
|
|
if (initialPrompt && !existingSessionId && !initialPromptSent.current) {
|
|
initialPromptSent.current = true;
|
|
actualSend(initialPrompt);
|
|
}
|
|
```
|
|
|
|
**Important:** `actualSend` must pass `adapter: adapterOverride` and `cwd: cwdOverride` in the QUERY options so handleQuery uses the correct adapter and directory.
|
|
|
|
- [ ] **Step 5: Update ChatView `handleReviewSelect` — local mount + registerReview**
|
|
|
|
**File:** `src/components/ChatView.tsx`
|
|
|
|
Add state:
|
|
|
|
```typescript
|
|
const [reviewInitialPrompt, setReviewInitialPrompt] = useState<string | null>(null);
|
|
const [reviewCwd, setReviewCwd] = useState<string | null>(null);
|
|
```
|
|
|
|
Replace `api.createReview()` call in handleReviewSelect:
|
|
|
|
```typescript
|
|
// Instead of api.createReview, set local state to mount panel
|
|
setActiveReview({
|
|
reviewId: '',
|
|
childSessionId: '',
|
|
childCliSessionId: '',
|
|
childAdapter: targetAdapter,
|
|
anchorMessageId: anchorMsgId,
|
|
reviewTitle: title,
|
|
});
|
|
setReviewInitialPrompt(cappedContext);
|
|
setReviewCwd(/* parent session's cwd from adapterConfig or session state */);
|
|
setReviewPanelState('expanded');
|
|
```
|
|
|
|
Update FloatingReviewPanel props:
|
|
|
|
```tsx
|
|
<FloatingReviewPanel
|
|
reviewId={activeReview.reviewId || undefined}
|
|
childSessionId={activeReview.childSessionId || undefined}
|
|
childAdapter={activeReview.childAdapter}
|
|
reviewTitle={activeReview.reviewTitle}
|
|
panelState={reviewPanelState}
|
|
onPanelStateChange={setReviewPanelState}
|
|
onEnd={async () => {
|
|
// Guard: only call endReview if reviewId exists (edge case F)
|
|
if (activeReview.reviewId) {
|
|
try { await api.endReview(activeReview.reviewId); } catch {}
|
|
}
|
|
// Always destroy child session if it exists
|
|
if (activeReview.childSessionId) {
|
|
// session cleanup happens server-side when session ends
|
|
}
|
|
setActiveReview(null);
|
|
setReviewPanelState('hidden');
|
|
setReviewInitialPrompt(null);
|
|
}}
|
|
initialPrompt={reviewInitialPrompt || undefined}
|
|
cwd={reviewCwd || undefined}
|
|
onSessionCreated={async (childSid) => {
|
|
try {
|
|
const result = await api.registerReview(
|
|
sessionId, childSid, activeReview.childAdapter,
|
|
activeReview.anchorMessageId, activeReview.reviewTitle || '', ''
|
|
);
|
|
setActiveReview(prev => prev ? {
|
|
...prev,
|
|
reviewId: result.reviewId,
|
|
childSessionId: childSid,
|
|
childCliSessionId: childSid,
|
|
} : null);
|
|
} catch (err) {
|
|
// Edge case E: registerReview failed
|
|
console.error('Failed to register review:', err);
|
|
// Session exists but no DB record — user can still chat, just won't persist
|
|
}
|
|
setReviewInitialPrompt(null);
|
|
}}
|
|
/>
|
|
```
|
|
|
|
- [ ] **Step 6: Verify RECONNECT path still works (scenarios C/D)**
|
|
|
|
The RECONNECT path is preserved because:
|
|
- When `childSessionId` is provided (from REVIEW_STARTED broadcast on reconnect), useChat sends RECONNECT
|
|
- When `initialPrompt` is NOT provided, no auto-send happens
|
|
- FloatingReviewPanel renders ChatBody normally with messages from HISTORY_LOAD
|
|
|
|
Verify by checking: `handleReconnect` in session-manager.ts sends active reviews → useChat REVIEW_STARTED handler sets `activeReview` with `childSessionId` → FloatingReviewPanel mounts with childSessionId → useChat RECONNECT.
|
|
|
|
- [ ] **Step 7: Verify TypeScript compilation**
|
|
|
|
```bash
|
|
npx tsc --noEmit
|
|
```
|
|
|
|
- [ ] **Step 8: Commit**
|
|
|
|
```bash
|
|
git add server/index.ts src/lib/api.ts src/components/ChatView.tsx src/components/FloatingReviewPanel.tsx src/hooks/useChat.ts
|
|
git commit -m "feat: unified session path — review child uses QUERY, registerReview after session created"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 3: Clean up — remove old review session creation + pasteToSession
|
|
|
|
**Files:**
|
|
- `server/index.ts`
|
|
- `src/lib/api.ts`
|
|
- `server/adapters/interface.ts`
|
|
- `server/adapters/codex/index.ts`
|
|
- `server/adapters/claude/index.ts`
|
|
- `server/adapters/claude/tmux-adapter.ts`
|
|
|
|
- [ ] **Step 1: Remove old POST /api/reviews session creation logic**
|
|
|
|
In `server/index.ts` POST /api/reviews handler (lines 249-319):
|
|
- Remove `adapter.startSession()` call
|
|
- Remove `adapter.pasteToSession()` call
|
|
- Remove marker injection logic
|
|
- Keep only: DB record creation + broadcast (same as /api/reviews/register)
|
|
- Or remove the entire endpoint and redirect to /api/reviews/register
|
|
|
|
Check frontend callers:
|
|
```bash
|
|
grep -rn "createReview\|/api/reviews'" src/ --include="*.ts" --include="*.tsx"
|
|
```
|
|
|
|
Remove `createReview` from `api.ts` if no longer called.
|
|
|
|
- [ ] **Step 2: Update send-back to use sendMessage**
|
|
|
|
In `POST /api/reviews/:id/send-back` (server/index.ts lines 369-371):
|
|
|
|
```typescript
|
|
// OLD:
|
|
await parentAdapter.pasteToSession(parentSessionId, formatted);
|
|
|
|
// NEW:
|
|
await parentAdapter.sendMessage(parentSessionId, formatted);
|
|
```
|
|
|
|
Both Claude and Codex `sendMessage` now handle large content (Task 1).
|
|
|
|
- [ ] **Step 3: Remove `pasteToSession` from adapter interface**
|
|
|
|
Check remaining callers:
|
|
```bash
|
|
grep -rn "pasteToSession" server/ --include="*.ts"
|
|
```
|
|
|
|
If no remaining callers after Steps 1-2, remove from:
|
|
- `server/adapters/interface.ts` — base class method
|
|
- `server/adapters/codex/index.ts` — delegation
|
|
- `server/adapters/codex/codex-tmux-adapter.ts` — if any leftover
|
|
- `server/adapters/claude/index.ts` — delegation
|
|
- `server/adapters/claude/tmux-adapter.ts` — implementation
|
|
|
|
- [ ] **Step 4: Verify TypeScript compilation**
|
|
|
|
```bash
|
|
npx tsc --noEmit
|
|
```
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add server/ src/lib/api.ts
|
|
git commit -m "refactor: remove old review session creation and pasteToSession from adapter interface"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 4: E2E Verification
|
|
|
|
- [ ] **Step 1: Start server**
|
|
```bash
|
|
CLAUDE_UI_PASSWORD=TEST npm run dev
|
|
```
|
|
|
|
- [ ] **Step 2: Test normal Codex session (scenario A)**
|
|
New Project → code-tap → Codex → send message → verify response + icon buttons.
|
|
|
|
- [ ] **Step 3: Test normal Claude session**
|
|
New Project → code-tap → Claude → send message → verify response.
|
|
|
|
- [ ] **Step 4: Test Cross-AI Review unified path (scenario B)**
|
|
1. Claude session → send message → get response
|
|
2. Click send icon → select "Direct send"
|
|
3. Verify FloatingReviewPanel opens
|
|
4. Verify panel shows Codex response (via QUERY, same as normal)
|
|
5. Verify session ID updates to real UUID
|
|
|
|
- [ ] **Step 5: Test send-back (scenario H)**
|
|
In review panel, click send-back icon → verify message appears in parent chat.
|
|
|
|
- [ ] **Step 6: Test end review**
|
|
Click "End" → verify panel closes, markers appear.
|
|
|
|
- [ ] **Step 7: Test end review before registerReview (scenario F)**
|
|
Quick-click End immediately after review starts (before Codex responds) → verify no crash.
|
|
|
|
- [ ] **Step 8: Test page refresh reconnect (scenario D)**
|
|
1. Start a review
|
|
2. Refresh page
|
|
3. Reconnect to parent session
|
|
4. Verify FloatingReviewPanel re-appears with child session (RECONNECT path)
|
|
|
|
---
|
|
|
|
## Self-Review Checklist
|
|
|
|
### Flow comparison after all tasks
|
|
|
|
```
|
|
Normal session (Codex or Claude):
|
|
useChat.actualSend("Hi") → WS QUERY → handleQuery → startSession → registerClient → sendMessage
|
|
|
|
Review child (same device, scenario B):
|
|
useChat.actualSend(reviewContext) → WS QUERY → handleQuery → startSession → registerClient → sendMessage
|
|
→ SESSION_CREATED → POST /api/reviews/register → DB record + broadcast
|
|
|
|
Review child (other device/reconnect, scenarios C/D):
|
|
REVIEW_STARTED from server → FloatingReviewPanel mounts with childSessionId
|
|
→ useChat RECONNECT → handleReconnect → registerClient → HISTORY_LOAD
|
|
|
|
All three paths work. Scenarios B and normal use IDENTICAL QUERY flow.
|
|
```
|
|
|
|
### Adapter sendMessage unification
|
|
| Adapter | Short text | Long/multiline text |
|
|
|---------|-----------|-------------------|
|
|
| Codex | sendKeys | `\n`→`\\n` + pasteBuffer (with CODETAP_REF marker split) |
|
|
| Claude | sendKeys | pasteBuffer (no `\n` replacement needed, no marker split) |
|
|
|
|
### Error handling
|
|
- registerReview failure → catch, log, session continues (no DB record but chat works) ✅
|
|
- End with empty reviewId → guard, skip endReview API call ✅
|
|
- initialPrompt double-send → ref guard prevents ✅
|
|
|
|
### Files changed
|
|
| File | Change |
|
|
|------|--------|
|
|
| `server/adapters/codex/codex-tmux-adapter.ts` | Merge sendMessage + pasteToSession |
|
|
| `server/adapters/codex/index.ts` | pasteToSession delegates to sendMessage |
|
|
| `server/adapters/claude/tmux-adapter.ts` | sendMessage handles large content |
|
|
| `server/adapters/claude/index.ts` | pasteToSession delegates to sendMessage |
|
|
| `server/adapters/interface.ts` | Remove pasteToSession (Task 3) |
|
|
| `server/index.ts` | Add /api/reviews/register, remove old POST /api/reviews session creation |
|
|
| `src/lib/api.ts` | Add registerReview(), remove createReview() |
|
|
| `src/components/ChatView.tsx` | handleReviewSelect → local state + registerReview callback |
|
|
| `src/components/FloatingReviewPanel.tsx` | Dual-path: initialPrompt (QUERY) or childSessionId (RECONNECT) |
|
|
| `src/hooks/useChat.ts` | Support initialPrompt auto-send |
|