Interactive Prompts: - Unified InteractivePrompt type across all 3 adapters (Claude/Codex/Gemini) - InteractivePromptOverlay component with options, text input, countdown - Gemini + Codex pane monitors detect tool confirmation, ask user, plan approval - respondInteractivePrompt routing: permission → respondPermission, options → _selectOption - Claude AskUserQuestion nested questions[0] structure parsing Cross-AI Review: - Client-generated reviewId, removed pendingReview state - FloatingReviewPanel uses CSS display:none instead of unmount (keeps hooks alive) - Child review sessions default to YOLO/bypass permission mode - Send back to parent, send to existing/new review, tab switching, end review - Collapsed review cards with read-only panel for ended reviews - Full reconnect support: active + ended reviews restore correctly AskUserQuestion Tool Card UI: - Dedicated renderer replaces raw JSON display - Options shown with selected (green) / unselected (gray) indicators - Free text answers shown in quoted format with green border - Collapsed summary: question → answer - Shared parseAskQuestionInput utility (client + server) - Historical tool results attached via _result on tool_use blocks Adapter Fixes: - Session→adapter mapping persisted in SQLite (survives server restart) - SESSION_CREATED deferred for pendingRekey adapters (Codex/Gemini) - session-rekeyed handler sends complete SESSION_CREATED with adapter + cwd - Gemini: auto-accept folder trust, privacy notice, IDE nudge, YOLO * prompt - Claude: auto-accept bypass permissions confirmation (v2.1.85+) - Port fallback (EADDRINUSE → try +1), statusLine shell script wrapper Other: - Desktop Enter sends / Shift+Enter newline; Mobile Enter newline - Strip CLAWTAP_REF marker from session list - Active sessions tab shows adapter badge - Rename CLAUDE_UI_PASSWORD → CLAWTAP_PASSWORD Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
5.7 KiB
Adapter Identity Fix — Design Spec
Date: 2026-03-26 Status: Draft
Problem
When opening a historical session without explicit adapter info (URL direct access, browser refresh, push notification), the frontend guesses the adapter from localStorage and initializes model/permissionMode/effort from the wrong adapter's prefs. The server later corrects the adapter via SESSION_CREATED, but model/prefs are never corrected → wrong UI state.
Root Cause
useChat eagerly initializes everything (adapter, model, permissionMode, effort, adapterConfig) at mount time before knowing the correct adapter. When the adapter is unknown, it guesses wrong.
Design Principle
Don't guess. Wait.
If the adapter is known (from prop or URL) → initialize immediately. If the adapter is unknown → show loading → wait for server SESSION_CREATED → then initialize.
Server is the single source of truth for adapter identity.
Changes
Change 1: useChat — defer initialization until adapter is confirmed
File: src/hooks/useChat.ts
Current:
const resolvedAdapter = initialAdapter || localStorage.getItem(STORAGE.ADAPTER) || 'claude';
const initialPrefs = loadAdapterPrefs(resolvedAdapter);
const [model, setModel] = useState(initialPrefs.model || '');
// ... all initialized immediately with possibly wrong adapter
New:
// adapter is either known from prop/URL, or null (wait for server)
const [selectedAdapter, setSelectedAdapter] = useState<string | null>(initialAdapter || null);
// model/prefs initialized only after adapter is confirmed
const [model, setModel] = useState<string>('');
const [permissionMode, setPermissionMode] = useState<string>('default');
const [effort, setEffort] = useState<string>('');
When SESSION_CREATED arrives:
case WS.SESSION_CREATED:
setSessionId(msg.sessionId);
if (msg.adapter) {
setSelectedAdapter(msg.adapter);
// Load correct prefs now that we know the adapter
const prefs = loadAdapterPrefs(msg.adapter);
setModel(prefs.model || '');
setPermissionMode(msg.permissionMode || prefs.permissionMode || 'default');
setEffort(prefs.effort || '');
}
break;
If initialAdapter was provided (session list click), everything initializes immediately at mount — no waiting, no loading state.
Change 2: ChatView — show loading when adapter unknown
File: src/components/ChatView.tsx
If selectedAdapter is null (waiting for server), render a loading indicator instead of the chat UI:
if (!selectedAdapter) {
return <LoadingAnimation size="md" label="Connecting..." />;
}
This only happens for path B (URL) and C (notification). Path A (session list) always has adapter → no loading.
Change 3: URL includes adapter when available
File: src/App.tsx
navigateTo() includes adapter in URL when known:
const url = view.sessionId
? `/?view=chat&session=${view.sessionId}${view.adapter ? `&adapter=${view.adapter}` : ''}`
: '/';
URL parser reads adapter from URL:
const sessionId = params.get('session');
const adapter = params.get('adapter');
if (sessionId) {
openChat(sessionId, undefined, adapter || undefined);
}
Push notification service worker also includes adapter in URL if available.
Change 4: SERVER SESSION_CREATED includes adapter + cwd
File: server/session-manager.ts
sendSessionCreated() includes adapter name (already done) and cwd:
send(conn, {
type: WS.SESSION_CREATED,
sessionId,
adapter: adapterName,
cwd: sessionObj?.cwd || resolvedCwd,
permissionMode: sessionObj?.permissionMode,
});
The cwd allows the frontend to show the correct project name in the header even when opened from URL (which doesn't have cwd).
Change 5: handleQuery verifies adapter on resumeSession
File: server/session-manager.ts
When handleQuery receives a QUERY with a sessionId (resume path), verify the adapter:
if (sessionId) {
const resolvedName = await resolveAdapterForSession(sessionId);
if (resolvedName) adapterName = resolvedName; // override client's adapter with truth
handle = await adapter.resumeSession(sessionId, cwd, { permissionMode });
}
Change 6: Active sessions tab shows adapter badge
File: src/components/SessionsView.tsx
Add adapter brand badge to active session cards (same pattern as project sessions list).
Flow After Fix
Path A (Session List Click):
adapter = 'gemini' (from server API) → useChat initializes immediately → no loading
→ WS RECONNECT → server confirms → messages load → render
Path B (URL Direct) / Path C (Push Notification):
adapter = null (unknown) → useChat does NOT initialize prefs → ChatView shows loading
→ WS RECONNECT → server SESSION_CREATED { adapter: 'gemini', cwd: '...' }
→ useChat sets adapter, loads correct prefs → loading disappears → messages load → render
Path B with adapter in URL:
URL: ?session=UUID&adapter=gemini
adapter = 'gemini' (from URL) → useChat initializes immediately → no loading
→ WS RECONNECT → server confirms → messages load → render
Verification
- Session list → Gemini session: Loads immediately, correct adapter/model/messages
- URL
?session=UUID(no adapter): Shows loading → then correct adapter/messages - URL
?session=UUID&adapter=gemini: Loads immediately, no flash - Browser refresh on chat: Adapter from URL → immediate load
- Push notification click: Loading → then correct session
- Two tabs same session: Both show correct adapter, messages sync
- Send from Tab A: Tab B sees user message + thinking + response
- Active sessions tab: Shows adapter badge
- New Chat (all adapters): Still works correctly