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
This commit is contained in:
kuannnn
2026-03-18 10:24:45 +08:00
commit 42861ea7fa
151 changed files with 33897 additions and 0 deletions
@@ -0,0 +1,566 @@
# 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):
```sql
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:
```typescript
instructionCreate: BetterSqlite3.Statement;
instructionGetAll: BetterSqlite3.Statement;
instructionDelete: BetterSqlite3.Statement;
```
In the `stmts()` function, add:
```typescript
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:
```typescript
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:
```typescript
// --- 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**
```bash
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):
```typescript
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**
```bash
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:
```typescript
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**
```bash
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:
```typescript
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:
```typescript
const [saveToast, setSaveToast] = useState<{ instruction: string; label: string } | null>(null);
```
- [ ] **Step 2: Update ReviewActionMenu JSX**
Replace the current `<ReviewActionMenu>` with:
```tsx
<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>`:
```tsx
{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**
```bash
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`:
```tsx
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:
```tsx
onOpenSettings={() => setView({ name: 'settings' })}
```
- [ ] **Step 3: Commit**
```bash
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:
```tsx
<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**
```bash
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**
```bash
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**
```bash
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**
```bash
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**