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,558 @@
# PWA Optimization 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:** Bring CodeTap's PWA to production-grade quality with proper viewport handling, splash screens, install prompts, SW updates, badge management, draft persistence, navigation history, and social meta tags.
**Architecture:** All changes are additive — native Web APIs with feature detection, no new dependencies. App.tsx gains PWA lifecycle effects. ShimmerInput gains draft persistence. Manifest and HTML get richer metadata.
**Tech Stack:** Vite + vite-plugin-pwa + native Web APIs (beforeinstallprompt, History API, Network Information API)
**Spec:** `docs/superpowers/specs/2026-03-26-pwa-optimization-design.md`
---
## File Structure
| File | Responsibility | Action |
|------|---------------|--------|
| `index.html` | Viewport, splash images, OG tags | Modify |
| `vite.config.ts` | Manifest shortcuts, screenshots | Modify |
| `src/App.tsx` | Install prompt, SW update, badge clear, history API | Modify |
| `src/components/SessionsView.tsx` | Install banner UI | Modify |
| `src/components/ShimmerInput.tsx` | Draft auto-save | Modify |
| `src/components/StatusBar.tsx` | Slow network indicator | Modify |
| `src/index.css` | Safe area utilities | Modify |
| `public/splash/` | iOS splash screen images | Create |
| `public/screenshots/` | Manifest screenshots | Create |
---
## Task 1: Viewport & Safe Areas
**Files:**
- Modify: `index.html`
- Modify: `src/index.css`
- [ ] **Step 1: Add viewport-fit=cover to index.html**
Change the viewport meta tag:
```html
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
```
- [ ] **Step 2: Add safe area utilities to index.css**
After the existing `.safe-bottom` rule:
```css
.safe-top {
padding-top: env(safe-area-inset-top);
}
.safe-x {
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
}
```
- [ ] **Step 3: Verify — open in iOS simulator or DevTools, check notch area**
- [ ] **Step 4: Commit**
```bash
git add index.html src/index.css
git commit -m "feat(pwa): viewport-fit=cover and full safe area utilities"
```
---
## Task 2: iOS Splash Screens
**Files:**
- Create: `public/splash/` directory
- Modify: `index.html`
- [ ] **Step 1: Create splash screen SVG generator script**
Create a simple inline SVG splash as a data URI approach in index.html. This avoids needing to generate multiple PNGs. Use `apple-mobile-web-app-startup-image` with media queries for major iPhone sizes.
Add to `<head>` in index.html, after the apple-touch-icon link:
```html
<!-- iOS Splash Screens -->
<meta name="apple-mobile-web-app-title" content="CodeTap" />
<link rel="apple-touch-startup-image"
media="screen and (device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3)"
href="/splash/splash-1290x2796.png" />
<link rel="apple-touch-startup-image"
media="screen and (device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3)"
href="/splash/splash-1179x2556.png" />
<link rel="apple-touch-startup-image"
media="screen and (device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3)"
href="/splash/splash-1284x2778.png" />
<link rel="apple-touch-startup-image"
media="screen and (device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3)"
href="/splash/splash-1170x2532.png" />
<link rel="apple-touch-startup-image"
media="screen and (device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3)"
href="/splash/splash-1125x2436.png" />
<link rel="apple-touch-startup-image"
media="screen and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3)"
href="/splash/splash-1242x2688.png" />
<link rel="apple-touch-startup-image"
media="screen and (device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2)"
href="/splash/splash-750x1334.png" />
```
- [ ] **Step 2: Generate splash screen PNGs via Node.js canvas script**
Create `scripts/generate-splash.mjs` that uses the built-in `node:canvas` or a simple HTML-to-PNG approach. Simplest method: create a single-use Node script that writes minimal HTML to a temp file and uses `sharp` or pure SVG-to-PNG. Actually, the most practical approach for a CLI tool: use a simple Node script that generates SVG strings and writes them as `.svg` files that Safari can use (Safari accepts SVG for startup images). If SVG doesn't work, use a single high-res PNG and reference it without media queries as a universal fallback.
Practical fallback: Create `public/splash/` with a single `splash.svg` (dark bg + centered "CodeTap" text), referenced without media queries. Remove per-device media queries from Step 1 and use a single universal link tag instead:
```html
<link rel="apple-touch-startup-image" href="/splash/splash.svg" />
```
If SVG is not supported by Safari for startup images (it isn't), generate a single 1290x2796 PNG using a canvas script at build time, or manually create one. The key requirement is: dark background (#09090b), centered CodeTap text or mascot.
- [ ] **Step 3: Verify — add to home screen on iOS, check splash appears**
- [ ] **Step 4: Commit**
```bash
git add -f public/splash/ index.html
git commit -m "feat(pwa): iOS splash screens for major iPhone sizes"
```
---
## Task 3: Android Install Prompt
**Files:**
- Modify: `src/App.tsx`
- Modify: `src/components/SessionsView.tsx`
- [ ] **Step 1: Add install prompt state to App.tsx**
Add state and effect near the top of `App()`:
```tsx
const [installPrompt, setInstallPrompt] = useState<any>(null);
const [installDismissed, setInstallDismissed] = useState(
() => localStorage.getItem('codetap:install-dismissed') === 'true'
);
useEffect(() => {
const handler = (e: Event) => {
e.preventDefault();
setInstallPrompt(e);
};
window.addEventListener('beforeinstallprompt', handler);
const installedHandler = () => {
setInstallPrompt(null);
setInstallDismissed(true);
localStorage.setItem('codetap:install-dismissed', 'true');
};
window.addEventListener('appinstalled', installedHandler);
return () => {
window.removeEventListener('beforeinstallprompt', handler);
window.removeEventListener('appinstalled', installedHandler);
};
}, []);
```
- [ ] **Step 2: Pass install props to SessionsView**
Update the SessionsView rendering in App.tsx:
```tsx
<SessionsView
onOpenChat={openChat}
onLogout={handleLogout}
onOpenSettings={() => setView({ name: 'settings' })}
installPrompt={!installDismissed ? installPrompt : null}
onInstall={async () => {
if (installPrompt) {
installPrompt.prompt();
const result = await installPrompt.userChoice;
if (result.outcome === 'accepted') {
setInstallPrompt(null);
setInstallDismissed(true);
localStorage.setItem('codetap:install-dismissed', 'true');
}
}
}}
onDismissInstall={() => {
setInstallDismissed(true);
localStorage.setItem('codetap:install-dismissed', 'true');
}}
/>
```
- [ ] **Step 3: Add install banner to SessionsView**
Add to SessionsView props interface and render a banner below the header when `installPrompt` is truthy:
```tsx
// Add to props
installPrompt?: any;
onInstall?: () => void;
onDismissInstall?: () => void;
// Render below the header, before the tab bar
{installPrompt && (
<div className="flex items-center gap-3 px-4 py-2.5 bg-accent/10 border-b border-accent/20">
<span className="text-xs text-text flex-1 font-mono">Install CodeTap for a better experience</span>
<button onClick={onInstall} className="text-xs font-medium text-accent hover:text-accent-light">Install</button>
<button onClick={onDismissInstall} className="text-xs text-text-dim hover:text-text">Dismiss</button>
</div>
)}
```
- [ ] **Step 4: Verify — open in Chrome Android (or DevTools Application panel), check install banner appears**
- [ ] **Step 5: Commit**
```bash
git add src/App.tsx src/components/SessionsView.tsx
git commit -m "feat(pwa): Android install prompt with dismissible banner"
```
---
## Task 4: Service Worker Update Notification
**Files:**
- Modify: `src/App.tsx`
- [ ] **Step 1: Add SW update detection and toast state**
Add near other effects in App():
```tsx
const [swUpdateAvailable, setSwUpdateAvailable] = useState(false);
useEffect(() => {
const handleControllerChange = () => setSwUpdateAvailable(true);
navigator.serviceWorker?.addEventListener('controllerchange', handleControllerChange);
return () => navigator.serviceWorker?.removeEventListener('controllerchange', handleControllerChange);
}, []);
```
- [ ] **Step 2: Render update toast**
Add before the closing `</div>` or at the bottom of the main render:
```tsx
{swUpdateAvailable && (
<div className="fixed bottom-6 left-4 right-4 bg-surface border border-accent/30 rounded-md px-4 py-3 flex items-center justify-between z-50 shadow-lg">
<span className="text-sm text-text font-mono">New version available</span>
<div className="flex gap-2">
<button
onClick={() => window.location.reload()}
className="text-sm font-medium text-accent hover:text-accent-light"
>Refresh</button>
<button
onClick={() => setSwUpdateAvailable(false)}
className="text-sm text-text-dim hover:text-text"
>Later</button>
</div>
</div>
)}
```
- [ ] **Step 3: Verify — modify SW file, rebuild, check toast appears**
- [ ] **Step 4: Commit**
```bash
git add src/App.tsx
git commit -m "feat(pwa): service worker update notification toast"
```
---
## Task 5: Badge Clear on Focus
**Files:**
- Modify: `src/App.tsx`
- [ ] **Step 1: Add visibility change listener**
Add with other effects in App():
```tsx
useEffect(() => {
const handleVisibility = () => {
if (document.visibilityState === 'visible') {
navigator.clearAppBadge?.();
}
};
document.addEventListener('visibilitychange', handleVisibility);
return () => document.removeEventListener('visibilitychange', handleVisibility);
}, []);
```
- [ ] **Step 2: Commit**
```bash
git add src/App.tsx
git commit -m "feat(pwa): clear app badge when app becomes visible"
```
---
## Task 6: Manifest Shortcuts & Screenshots
**Files:**
- Modify: `vite.config.ts`
- Create: `public/screenshots/` (placeholder)
- [ ] **Step 1: Add shortcuts to manifest in vite.config.ts**
Add after the `icons` array in the manifest config:
```ts
shortcuts: [
{
name: 'New Chat',
short_name: 'New',
url: '/?action=newchat',
icons: [{ src: '/pwa-192x192.png', sizes: '192x192' }],
},
],
categories: ['developer-tools', 'productivity'],
```
- [ ] **Step 2: Handle ?action=newchat in App.tsx**
Add after the existing `?session=` URL handler:
```tsx
useEffect(() => {
const params = new URLSearchParams(window.location.search);
if (params.get('action') === 'newchat' && authed) {
window.history.replaceState({}, '', '/');
// Shortcut just brings user to sessions view — they pick a project from there
setView({ name: 'sessions' });
}
}, [authed]);
```
- [ ] **Step 3: Add screenshots placeholder to manifest**
Add to manifest in vite.config.ts:
```ts
screenshots: [
{
src: '/screenshots/narrow.png',
sizes: '1080x1920',
type: 'image/png',
form_factor: 'narrow',
label: 'CodeTap Chat View',
},
{
src: '/screenshots/wide.png',
sizes: '1920x1080',
type: 'image/png',
form_factor: 'wide',
label: 'CodeTap Sessions View',
},
],
```
Create `public/screenshots/` directory with placeholder images (can be actual screenshots later).
- [ ] **Step 4: Commit**
```bash
git add vite.config.ts src/App.tsx
git add -f public/screenshots/ 2>/dev/null || true
git commit -m "feat(pwa): manifest shortcuts, categories, and screenshots config"
```
---
## Task 7: Input Draft Auto-Save
**Files:**
- Modify: `src/components/ShimmerInput.tsx`
- [ ] **Step 1: Add draft persistence to ShimmerInput**
ShimmerInput doesn't receive a sessionId prop, so use a global draft key. Add after the existing state declarations:
```tsx
const DRAFT_KEY = 'codetap:draft';
// Restore draft on mount
useEffect(() => {
if (!initialText) {
const saved = localStorage.getItem(DRAFT_KEY);
if (saved) setText(saved);
}
}, []);
// Debounce-save draft on text change
const saveTimer = useRef<ReturnType<typeof setTimeout>>();
useEffect(() => {
clearTimeout(saveTimer.current);
if (text.trim()) {
saveTimer.current = setTimeout(() => {
localStorage.setItem(DRAFT_KEY, text);
}, 500);
} else {
localStorage.removeItem(DRAFT_KEY);
}
return () => clearTimeout(saveTimer.current);
}, [text]);
```
- [ ] **Step 2: Clear draft on send**
In the existing send handler, add `localStorage.removeItem(DRAFT_KEY)` after `onSend(...)`:
```tsx
// Find the handleSend function and add after onSend call:
localStorage.removeItem(DRAFT_KEY);
```
- [ ] **Step 3: Verify — type text, close tab, reopen, check draft restored**
- [ ] **Step 4: Commit**
```bash
git add src/components/ShimmerInput.tsx
git commit -m "feat(pwa): auto-save input draft to localStorage with debounce"
```
---
## Task 8: Slow Network Detection
**Files:**
- Modify: `src/components/StatusBar.tsx`
- [ ] **Step 1: Add network quality detection**
Add a hook at the top of StatusBar component:
```tsx
const [slowNetwork, setSlowNetwork] = useState(false);
useEffect(() => {
const conn = (navigator as any).connection;
if (!conn) return;
const check = () => {
setSlowNetwork(conn.effectiveType === '2g' || conn.effectiveType === 'slow-2g');
};
check();
conn.addEventListener('change', check);
return () => conn.removeEventListener('change', check);
}, []);
```
- [ ] **Step 2: Render slow network indicator**
Add inside the status bar, before or after the model display:
```tsx
{slowNetwork && (
<span className="text-warning text-[10px] font-mono">Slow</span>
)}
```
- [ ] **Step 3: Commit**
```bash
git add src/components/StatusBar.tsx
git commit -m "feat(pwa): slow network indicator via Network Information API"
```
---
## Task 9: History API Navigation
**Files:**
- Modify: `src/App.tsx`
- [ ] **Step 1: Push state on view changes**
Modify the `saveView` function to also push history state:
```tsx
function saveView(view: View) {
sessionStorage.setItem('currentView', JSON.stringify(view));
const url = view.name === 'chat' && view.sessionId
? `/?view=chat&session=${view.sessionId}`
: view.name === 'settings'
? '/?view=settings'
: '/';
window.history.pushState({ view }, '', url);
}
```
- [ ] **Step 2: Listen for popstate (back button/gesture)**
Add effect in App():
```tsx
useEffect(() => {
const handlePopState = (event: PopStateEvent) => {
if (event.state?.view) {
setView(event.state.view);
sessionStorage.setItem('currentView', JSON.stringify(event.state.view));
} else {
setView({ name: 'sessions' });
sessionStorage.setItem('currentView', JSON.stringify({ name: 'sessions' }));
}
};
window.addEventListener('popstate', handlePopState);
return () => window.removeEventListener('popstate', handlePopState);
}, []);
```
- [ ] **Step 3: Use replaceState for initial load (avoid double entry)**
In the existing `loadView()` function, after loading the view, replace the current history entry:
```tsx
// At the end of App() initialization, after first render:
useEffect(() => {
window.history.replaceState({ view }, '', window.location.pathname + window.location.search);
}, []); // only on mount
```
- [ ] **Step 4: Verify — navigate sessions → chat → back gesture returns to sessions**
- [ ] **Step 5: Commit**
```bash
git add src/App.tsx
git commit -m "feat(pwa): History API navigation for back gesture support"
```
---
## Task 10: OpenGraph Meta Tags
**Files:**
- Modify: `index.html`
- [ ] **Step 1: Add OG and Twitter meta tags to index.html**
Add before `</head>`:
```html
<!-- OpenGraph -->
<meta property="og:title" content="CodeTap" />
<meta property="og:description" content="Use Claude Code from your phone. Real-time mobile UI synced with your desktop terminal." />
<meta property="og:type" content="website" />
<meta property="og:image" content="/pwa-512x512.png" />
<meta name="twitter:card" content="summary" />
<meta name="description" content="Use Claude Code from your phone. Real-time mobile UI synced with your desktop terminal." />
```
- [ ] **Step 2: Commit**
```bash
git add index.html
git commit -m "feat(pwa): OpenGraph and Twitter Card meta tags"
```
---
## Verification
1. Run `npm run dev` in the worktree
2. Open in Chrome DevTools → Application panel:
- Check manifest loads correctly with shortcuts, screenshots, categories
- Check service worker is registered
3. Mobile testing (iOS):
- Add to Home Screen → check splash screen appears
- Verify content extends properly behind notch (viewport-fit=cover)
- Test back gesture navigates correctly
4. Mobile testing (Android / Chrome):
- Check install banner appears in SessionsView
- Dismiss and verify it doesn't reappear
- Install and verify banner disappears
5. Draft persistence:
- Type text in input, close tab, reopen → text should be restored
- Send message → draft should be cleared
6. Badge:
- Receive push notification with badge → switch to app → badge clears
7. Network:
- Throttle to 2G in DevTools → "Slow" indicator appears in StatusBar