Files
clawtap/docs/superpowers/plans/2026-03-26-send-to-menu-settings.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

19 KiB
Raw Permalink Blame History

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_instructions table 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 savedInstructions export 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 shown
  • selectedAdapter: string | null — chosen adapter ID
  • adapterConfig: loaded from api.adapterConfig(selectedAdapter) when adapter is chosen
  • selectedModel: string — from adapterConfig.models, default to first item
  • instructionsExpanded: boolean — toggle for With Instructions section
  • savedInstructions: loaded from api.getInstructions() on mount
  • customText: 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 from adapterConfig.models
  • "Direct Send" button (icon ↗) — calls onDirectSend(selectedAdapter, selectedModel) then onClose()
  • "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)

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 onOpenSettings prop 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 from api.adapters() on mount
  • version: fetched from server health endpoint or read from a constant

Sub-view routing:

  • subView === 'instructions' → render <SavedInstructionsView onBack={() => setSubView('main')} />
  • subView matches 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 usePushNotifications hook)
  • "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 from api.getInstructions() on mount
  • showAddForm: boolean
  • newLabel: string
  • newInstruction: 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?'), then api.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 from api.adapterConfig(adapter) on mount (contains models, permissionModes, effortLevels, effortLabel)
  • prefs: loaded from loadAdapterPrefs(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 has value + label)
    • "Permission Mode" → options from config.permissionModes
    • Effort label from config.effortLabel (e.g. "Thinking" for Claude, "Effort" for Codex) → options from config.effortLevels
  • 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:

  1. Settings icon visible in project list header
  2. Settings page loads with all sections
  3. Saved Instructions: add an instruction, verify it appears in list, delete it
  4. Adapter settings: all dropdowns populate with correct adapter-specific options
  5. In a chat, click send icon → two-step menu opens
  6. Step 1 shows adapter icons + names (no model text)
  7. Step 2 shows model dropdown + Direct Send + With Instructions
  8. With Instructions has expand/collapse chevron
  9. Direct Send sends only raw response text
  10. With Instructions sends instruction + raw text
  11. Save toast appears and works after custom instruction send
  • Step 4: Final commit if any fixes needed