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
19 KiB
Send-to Menu Redesign + Settings Page 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: Redesign the Send-to menu as a two-step bottom sheet with model selection, add a Settings page with saved instructions management and per-adapter preferences.
Architecture: Three layers of changes: (1) Server — new saved_instructions DB table + API endpoints, (2) Client API — new instruction CRUD methods, (3) UI — rewritten ReviewActionMenu, new SettingsView with sub-pages, updated App routing and SessionsView header.
Tech Stack: React + TypeScript + Vite (client), Express + SQLite + better-sqlite3 (server), Tailwind CSS (styling)
Spec: docs/superpowers/specs/2026-03-26-send-to-menu-settings-design.md
File Map
| File | Action | Responsibility |
|---|---|---|
server/db.ts |
Modify | Add saved_instructions table + prepared statements + savedInstructions operations |
server/index.ts |
Modify | Add 3 instruction API endpoints |
src/lib/api.ts |
Modify | Add instruction CRUD client methods |
src/components/ReviewActionMenu.tsx |
Rewrite | Two-step bottom sheet with adapter/model/instructions |
src/components/ChatView.tsx |
Modify | Simplify handleReviewSelect — no more context assembly |
src/App.tsx |
Modify | Add settings view to routing |
src/components/SessionsView.tsx |
Modify | Add settings icon in header |
src/components/SettingsView.tsx |
Create | Main settings page with sections |
src/components/SavedInstructionsView.tsx |
Create | Instruction list with add/delete |
src/components/AdapterSettingsSection.tsx |
Create | Per-adapter model/permission/effort dropdowns |
Task 1: Saved Instructions — Server DB + API
Files:
-
Modify:
server/db.ts -
Modify:
server/index.ts -
Step 1: Add
saved_instructionstable to DB
In server/db.ts, add to the db.exec() block (after session_reviews table):
CREATE TABLE IF NOT EXISTS saved_instructions (
id TEXT PRIMARY KEY,
label TEXT NOT NULL,
instruction TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now'))
);
- Step 2: Add prepared statements
In the PreparedStatements interface, add:
instructionCreate: BetterSqlite3.Statement;
instructionGetAll: BetterSqlite3.Statement;
instructionDelete: BetterSqlite3.Statement;
In the stmts() function, add:
instructionCreate: d.prepare(
`INSERT INTO saved_instructions (id, label, instruction) VALUES (?, ?, ?)`
),
instructionGetAll: d.prepare(
`SELECT * FROM saved_instructions ORDER BY created_at ASC`
),
instructionDelete: d.prepare(
`DELETE FROM saved_instructions WHERE id = ?`
),
- Step 3: Add
savedInstructionsexport object
After the sessionReviews export, add:
export const savedInstructions = {
create(id: string, label: string, instruction: string): void {
stmts().instructionCreate.run(id, label, instruction);
},
getAll(): { id: string; label: string; instruction: string; created_at: string }[] {
return stmts().instructionGetAll.all() as any[];
},
delete(id: string): void {
stmts().instructionDelete.run(id);
},
};
- Step 4: Add API endpoints in
server/index.ts
Import savedInstructions from ./db.js. Add 3 routes after the review routes:
// --- Saved Instructions API ---
app.get('/api/instructions', authMiddleware, (_req: Request, res: Response) => {
try {
res.json(savedInstructions.getAll());
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
app.post('/api/instructions', authMiddleware, (req: Request, res: Response) => {
try {
const { label, instruction } = req.body;
if (!label || !instruction) return res.status(400).json({ error: 'label and instruction required' });
const id = randomUUID();
savedInstructions.create(id, label, instruction);
res.json({ id, label, instruction });
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
app.delete('/api/instructions/:id', authMiddleware, (req: Request, res: Response) => {
try {
savedInstructions.delete(req.params.id);
res.json({ ok: true });
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
Note: randomUUID is already imported in server/index.ts.
- Step 5: Verify TypeScript compiles
Run: npx tsc --noEmit
Expected: No errors
- Step 6: Commit
git add server/db.ts server/index.ts
git commit -m "feat: add saved_instructions DB table and API endpoints"
Task 2: Client API — Instruction Methods
Files:
-
Modify:
src/lib/api.ts -
Step 1: Add instruction API methods
Add to the api object, following the existing pattern (see registerReview, endReview for reference):
async getInstructions(): Promise<{ id: string; label: string; instruction: string; created_at: string }[]> {
return request('/api/instructions');
},
async createInstruction(label: string, instruction: string): Promise<{ id: string; label: string; instruction: string }> {
return request('/api/instructions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ label, instruction }),
});
},
async deleteInstruction(id: string): Promise<void> {
return request(`/api/instructions/${id}`, { method: 'DELETE' });
},
- Step 2: Verify TypeScript compiles
Run: npx tsc --noEmit
- Step 3: Commit
git add src/lib/api.ts
git commit -m "feat: add instruction CRUD methods to client API"
Task 3: ReviewActionMenu — Two-Step Bottom Sheet
Files:
- Rewrite:
src/components/ReviewActionMenu.tsx
This is the core UI change. The component goes from a simple template picker to a two-step bottom sheet with adapter selection, model picker, and expandable instructions panel.
- Step 1: Rewrite ReviewActionMenu.tsx
New props interface:
interface ReviewActionMenuProps {
visible: boolean;
adapters: { id: string; displayName: string }[];
onDirectSend: (adapter: string, model: string) => void;
onSendWithInstruction: (adapter: string, model: string, instruction: string, isCustom: boolean) => void;
onClose: () => void;
}
Component state:
step:'adapter' | 'action'— which step is shownselectedAdapter:string | null— chosen adapter IDadapterConfig: loaded fromapi.adapterConfig(selectedAdapter)when adapter is chosenselectedModel:string— from adapterConfig.models, default to first iteminstructionsExpanded:boolean— toggle for With Instructions sectionsavedInstructions: loaded fromapi.getInstructions()on mountcustomText:string— free text input value
Step 1 UI (adapter selection):
- Backdrop overlay (click to close)
- Bottom sheet with drag handle
- "Send to…" title
- Adapter rows:
<AdapterIcon>+ adapter name, no model text, tap → set selectedAdapter + go to step 2
Step 2 UI (action selection):
‹ {AdapterName}header with colored adapter name (back arrow returns to step 1)- Model:
<select>with options fromadapterConfig.models - "Direct Send" button (icon ↗) — calls
onDirectSend(selectedAdapter, selectedModel)thenonClose() - "With Instructions" button (icon ✎) with ▼/▲ chevron — toggles
instructionsExpanded - When expanded:
- Saved instructions list (tap → calls
onSendWithInstruction(adapter, model, item.instruction, false)) - Divider "或輸入新的"
- Text input + ↑ send button → calls
onSendWithInstruction(adapter, model, customText, true)(isCustom=true triggers save toast) - Text input + ↑ send button → calls
onSendWithInstruction(adapter, model, customText)
- Saved instructions list (tap → calls
Use AdapterIcon component from ./AdapterIcon for adapter icons.
Use getBrand from @/lib/adapter-brands for adapter colors.
Reset step to 'adapter' whenever visible changes to true.
- Step 2: Verify it compiles
Run: npx tsc --noEmit
- Step 3: Commit
git add src/components/ReviewActionMenu.tsx
git commit -m "feat: rewrite ReviewActionMenu as two-step bottom sheet"
Task 4: ChatView — Simplify Review Flow + Save Toast
Files:
-
Modify:
src/components/ChatView.tsx -
Step 1: Replace handleReviewSelect with two new handlers
Delete the old handleReviewSelect callback, the promptTemplates record, and the context assembly code.
Add two new handlers:
const handleDirectSend = useCallback((adapter: string, model: string) => {
const anchorId = reviewMenuMessageId;
setReviewMenuMessageId(null);
setReviewTargetAdapter(null);
if (!anchorId) return;
// Save selected model to adapter prefs so child session uses it
patchAdapterPrefs(adapter, { model });
const anchorMsg = messages.find(m => m.id === anchorId);
const rawText = anchorMsg ? extractTextFromBlocks(anchorMsg.content) : '';
setHistoryReview(null);
setActiveReview({
reviewId: '', childSessionId: '', childCliSessionId: '',
childAdapter: adapter, anchorMessageId: anchorId, reviewTitle: 'direct',
});
setReviewInitialPrompt(rawText);
setReviewCwd(cwd || null);
setActiveReviewPanel('expanded');
}, [reviewMenuMessageId, messages, cwd]);
const handleSendWithInstruction = useCallback((adapter: string, model: string, instruction: string, isCustom: boolean) => {
const anchorId = reviewMenuMessageId;
setReviewMenuMessageId(null);
setReviewTargetAdapter(null);
if (!anchorId) return;
// Save selected model to adapter prefs so child session uses it
patchAdapterPrefs(adapter, { model });
const anchorMsg = messages.find(m => m.id === anchorId);
const rawText = anchorMsg ? extractTextFromBlocks(anchorMsg.content) : '';
const prompt = `${instruction}\n\n${rawText}`;
const title = instruction.substring(0, 30);
setHistoryReview(null);
setActiveReview({
reviewId: '', childSessionId: '', childCliSessionId: '',
childAdapter: adapter, anchorMessageId: anchorId, reviewTitle: title,
});
setReviewInitialPrompt(prompt);
setReviewCwd(cwd || null);
setActiveReviewPanel('expanded');
// Show save toast only for custom (typed) instructions, not saved ones
if (isCustom) {
setSaveToast({ instruction, label: title });
setTimeout(() => setSaveToast(null), 3000);
}
}, [reviewMenuMessageId, messages, cwd]);
Add state:
const [saveToast, setSaveToast] = useState<{ instruction: string; label: string } | null>(null);
- Step 2: Update ReviewActionMenu JSX
Replace the current <ReviewActionMenu> with:
<ReviewActionMenu
visible={!!reviewMenuMessageId}
adapters={sendTargets.map(t => ({ id: t.adapter, displayName: t.label }))}
onDirectSend={handleDirectSend}
onSendWithInstruction={handleSendWithInstruction}
onClose={() => { setReviewMenuMessageId(null); setReviewTargetAdapter(null); }}
/>
- Step 3: Add save toast UI
Render at the bottom of the ChatView return, before the closing </div>:
{saveToast && (
<div className="fixed bottom-20 left-4 right-4 bg-[#1a1a1a] border border-[#333] rounded-xl p-3 flex items-center justify-between z-30">
<span className="text-sm text-[#888]">存成常用?</span>
<button
className="text-sm text-blue-400 font-medium px-3 py-1 rounded-lg hover:bg-blue-400/10"
onClick={() => {
api.createInstruction(saveToast.label, saveToast.instruction);
setSaveToast(null);
}}
>Save</button>
</div>
)}
- Step 4: Verify TypeScript compiles
Run: npx tsc --noEmit
- Step 5: Commit
git add src/components/ChatView.tsx
git commit -m "feat: simplify review flow — direct send raw text, instructions without context"
Task 5: App Routing — Add Settings View
Files:
-
Modify:
src/App.tsx -
Step 1: Add settings to View type and rendering
Add | { name: 'settings' } to the View union type.
Add rendering branch before the default SessionsView:
if (view.name === 'settings') {
return <SettingsView onBack={() => setView({ name: 'sessions' })} />;
}
Add import: import { SettingsView } from './components/SettingsView';
- Step 2: Pass onOpenSettings to SessionsView
Add prop to the <SessionsView> JSX:
onOpenSettings={() => setView({ name: 'settings' })}
- Step 3: Commit
git add src/App.tsx
git commit -m "feat: add settings view to app routing"
Task 6: SessionsView — Settings Icon in Header
Files:
-
Modify:
src/components/SessionsView.tsx -
Step 1: Add
onOpenSettingsprop and gear icon
Add onOpenSettings: () => void to the component props.
In the header's button group (the <div className="flex items-center gap-2"> block), add before the Logout button:
<button
onClick={onOpenSettings}
className="p-2 text-text-dim hover:text-text transition-colors"
title="Settings"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="3"/>
<path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-2 2 2 2 0 01-2-2v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83 0 2 2 0 010-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 01-2-2 2 2 0 012-2h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 010-2.83 2 2 0 012.83 0l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 012-2 2 2 0 012 2v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 0 2 2 0 010 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 012 2 2 2 0 01-2 2h-.09a1.65 1.65 0 00-1.51 1z"/>
</svg>
</button>
- Step 2: Commit
git add src/components/SessionsView.tsx
git commit -m "feat: add settings gear icon to project list header"
Task 7: SettingsView — Main Settings Page
Files:
-
Create:
src/components/SettingsView.tsx -
Step 1: Create SettingsView component
Props: onBack: () => void
State:
subView:'main' | 'instructions' | string(string = adapter id)adapters: fetched fromapi.adapters()on mountversion: fetched from server health endpoint or read from a constant
Sub-view routing:
subView === 'instructions'→ render<SavedInstructionsView onBack={() => setSubView('main')} />subViewmatches an adapter id → render<AdapterSettingsSection adapter={subView} onBack={() => setSubView('main')} />- Default → render main list
Main list: dark themed rows with chevrons, each tappable:
- "Saved Instructions" →
setSubView('instructions') - One row per adapter (icon + name) →
setSubView(adapterId) - "Notifications" → inline push toggle (reuse existing
usePushNotificationshook) - "About" → show
CodeTap v{version}inline
Header: back arrow + "Settings" title, same style as existing headers.
- Step 2: Verify TypeScript compiles
Run: npx tsc --noEmit
- Step 3: Commit
git add src/components/SettingsView.tsx
git commit -m "feat: create SettingsView with section navigation"
Task 8: SavedInstructionsView — Instruction Management
Files:
-
Create:
src/components/SavedInstructionsView.tsx -
Step 1: Create SavedInstructionsView component
Props: onBack: () => void
State:
instructions: array, fetched fromapi.getInstructions()on mountshowAddForm: booleannewLabel: stringnewInstruction: string
UI:
-
Header: back arrow + "Saved Instructions" +
[+ Add]button -
List: each item shows label (bold) + instruction preview (truncated, dim) + ✕ delete button
-
Delete: show
window.confirm('Delete this instruction?'), thenapi.deleteInstruction(id), remove from local state -
Add form (when showAddForm): label input + instruction textarea + [Save] + [Cancel] buttons
-
Save:
api.createInstruction(label, instruction), append to local state, hide form -
Step 2: Verify TypeScript compiles
Run: npx tsc --noEmit
- Step 3: Commit
git add src/components/SavedInstructionsView.tsx
git commit -m "feat: create SavedInstructionsView with add/delete"
Task 9: AdapterSettingsSection — Per-Adapter Preferences
Files:
-
Create:
src/components/AdapterSettingsSection.tsx -
Step 1: Create AdapterSettingsSection component
Props: adapter: string; onBack: () => void
State:
config: fetched fromapi.adapterConfig(adapter)on mount (contains models, permissionModes, effortLevels, effortLabel)prefs: loaded fromloadAdapterPrefs(adapter)(model, permissionMode, effort)
UI:
- Header: back arrow + AdapterIcon + adapter display name (from
getBrand) - Three
<select>dropdowns, each with a label:- "Model" → options from
config.models(each hasvalue+label) - "Permission Mode" → options from
config.permissionModes - Effort label from
config.effortLabel(e.g. "Thinking" for Claude, "Effort" for Codex) → options fromconfig.effortLevels
- "Model" → options from
- On change:
patchAdapterPrefs(adapter, { [field]: value })to persist to localStorage
Import loadAdapterPrefs, patchAdapterPrefs from @/lib/adapter-prefs.
Import getBrand from @/lib/adapter-brands.
Import AdapterIcon from ./AdapterIcon.
- Step 2: Verify TypeScript compiles
Run: npx tsc --noEmit
- Step 3: Commit
git add src/components/AdapterSettingsSection.tsx
git commit -m "feat: create AdapterSettingsSection with per-adapter dropdowns"
Task 10: E2E Verification
- Step 1: Full TypeScript check
Run: npx tsc --noEmit
Expected: No errors
- Step 2: Start server (without watch mode) and Vite
Start server and Vite in separate processes. Ensure tmux session exists first.
- Step 3: Visual verification in browser
Verify the following scenarios:
- Settings icon visible in project list header
- Settings page loads with all sections
- Saved Instructions: add an instruction, verify it appears in list, delete it
- Adapter settings: all dropdowns populate with correct adapter-specific options
- In a chat, click send icon → two-step menu opens
- Step 1 shows adapter icons + names (no model text)
- Step 2 shows model dropdown + Direct Send + With Instructions
- With Instructions has expand/collapse chevron
- Direct Send sends only raw response text
- With Instructions sends instruction + raw text
- Save toast appears and works after custom instruction send
- Step 4: Final commit if any fixes needed