diff --git a/.env.example b/.env.example index 22f1388..0c181a6 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,8 @@ CLAWTAP_PASSWORD=your-password-here PORT=3456 + +# Optional: enables Whisper voice transcription (higher accuracy) +# OPENAI_API_KEY=sk-... + +# Optional: contact email for Web Push VAPID identification +# VAPID_EMAIL=you@example.com diff --git a/README.md b/README.md index c3c4098..91255f6 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,8 @@ clawtap Open the URL on your phone. That's it. +> **Mobile access?** ClawTap needs HTTPS for PWA install and push notifications. The easiest way is [Tailscale](https://tailscale.com): `tailscale serve --bg 3456` gives you a trusted HTTPS URL instantly. See [PWA & Push Notifications](#-pwa--push-notifications) for details. + ClawTap auto-detects which AI CLIs you have installed (`claude`, `codex`, `gemini`) and enables them automatically.
@@ -124,9 +126,16 @@ Every tool call renders as an expandable card: Send follow-up messages while the AI is still responding. They appear as "Queued" with Edit/Cancel and auto-send when the AI finishes. Paste images from clipboard with thumbnail preview. +### Task Progress + +When your AI creates tasks (via `TaskCreate`/`TaskUpdate`), a floating progress ring appears in the bottom-right corner showing completion (e.g., 2/5). Tap it to expand a bottom sheet with full task details. Tasks are grouped by rounds — a new round starts when all previous tasks complete. The ring auto-fades 3 seconds after all tasks finish. + ### Voice Input -Tap the mic icon to dictate coding instructions. Uses the Web Speech API with real-time interim transcription. Works in any language. +Tap the mic icon to dictate coding instructions. Supports two backends: + +- **Web Speech API** (default) — real-time interim transcription, currently configured for Traditional Chinese (`zh-TW`) +- **OpenAI Whisper** — higher accuracy, requires `OPENAI_API_KEY` (see [Configuration](#-configuration)) ### Smart Input @@ -151,6 +160,8 @@ clawtap hooks install [--adapter claude] # Install hooks (all or one adapter) clawtap hooks uninstall [--adapter gemini] # Remove hooks clawtap cert # Generate HTTPS certificate clawtap stop # Graceful shutdown +clawtap --version # Show version +clawtap --help # Show help ``` The `--adapter` flag works with every command. Session lists show colored `[Claude]`/`[Codex]`/`[Gemini]` labels with first-prompt previews. @@ -175,7 +186,7 @@ clawtap cert **2. Install PWA:** Open the URL in Safari → Share → **Add to Home Screen**. -**3. Enable notifications:** Open ClawTap from home screen → tap the **bell icon** → Allow. +**3. Enable notifications:** On first login in standalone mode, ClawTap automatically prompts for notification permission. You can also toggle notifications manually in **Settings**. ### Smart Notifications @@ -195,6 +206,8 @@ The app icon badge shows how many sessions have unread notifications. Entering a |----------|---------|-------------| | `CLAWTAP_PASSWORD` | *(required)* | Login password | | `PORT` | `3456` | Server port | +| `OPENAI_API_KEY` | *(optional)* | Enables Whisper voice transcription (higher accuracy than Web Speech API) | +| `VAPID_EMAIL` | `noreply@clawtap.local` | Contact email for Web Push VAPID identification | HTTPS is enabled automatically when `~/.clawtap/cert.pem` and `~/.clawtap/key.pem` exist. Otherwise the server runs on HTTP. Tailscale Serve is the easiest path to HTTPS. diff --git a/package.json b/package.json index eb61375..682d297 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kuannnn/clawtap", - "version": "0.3.0", + "version": "0.3.1", "description": "Mobile UI for AI coding assistants. Real-time sync with Claude Code, Codex CLI, and Gemini CLI via tmux.", "type": "module", "bin": { diff --git a/src/App.tsx b/src/App.tsx index 51ae123..f2454fb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,7 @@ import { useState, useCallback, useEffect, useRef } from 'react'; import { STORAGE } from './lib/storage-keys'; import { isAuthenticated, clearToken } from './lib/api'; +import { usePushNotifications } from './hooks/usePushNotifications'; import { LoginView } from './components/LoginView'; import { SessionsView } from './components/SessionsView'; import { ChatView } from './components/ChatView'; @@ -59,6 +60,7 @@ export function App() { const [deviceOnline, setDeviceOnline] = useState(navigator.onLine); const consecutiveFails = useRef(0); const initialized = useRef(false); + const { supported: pushSupported, subscribed: pushSubscribed, subscribe: pushSubscribe } = usePushNotifications(); const [installPrompt, setInstallPrompt] = useState(null); const [installDismissed, setInstallDismissed] = useState( @@ -120,6 +122,43 @@ export function App() { return () => navigator.serviceWorker?.removeEventListener('controllerchange', handleControllerChange); }, []); + // PWA iOS: Sync --app-height CSS variable to the true viewport height. + // WebKit bug: after a keyboard open/close cycle, viewport-fit=cover's layout + // extension is lost, causing 100dvh to shrink permanently. We track the max + // observed innerHeight and lock it as the app height. + useEffect(() => { + let maxHeight = window.innerHeight; + const sync = () => { + const h = window.innerHeight; + if (h > maxHeight) maxHeight = h; + document.documentElement.style.setProperty('--app-height', `${maxHeight}px`); + }; + sync(); + window.visualViewport?.addEventListener('resize', sync); + window.addEventListener('resize', sync); + return () => { + window.visualViewport?.removeEventListener('resize', sync); + window.removeEventListener('resize', sync); + }; + }, []); + + // PWA: Auto-prompt notification permission on first login in standalone mode + useEffect(() => { + if (!authed || !pushSupported || pushSubscribed) return; + if (localStorage.getItem(STORAGE.PUSH_PROMPTED)) return; + const isStandalone = window.matchMedia('(display-mode: standalone)').matches + || (navigator as any).standalone === true; + if (!isStandalone) return; + if (typeof Notification !== 'undefined' && Notification.permission === 'denied') return; + + // Small delay so the UI has time to settle after login + const timer = setTimeout(() => { + localStorage.setItem(STORAGE.PUSH_PROMPTED, '1'); + pushSubscribe().catch(() => {}); + }, 1500); + return () => clearTimeout(timer); + }, [authed, pushSupported, pushSubscribed, pushSubscribe]); + // PWA: Clear app badge on focus useEffect(() => { const handleVisibility = () => { diff --git a/src/components/ChatView.tsx b/src/components/ChatView.tsx index 6bc7be2..0920dea 100644 --- a/src/components/ChatView.tsx +++ b/src/components/ChatView.tsx @@ -490,7 +490,7 @@ export function ChatView({ if (!selectedAdapter) { return ( -
+
); @@ -498,7 +498,8 @@ export function ChatView({ return (
{/* Header — overlays content, slides up when scrolling */}
diff --git a/src/components/SessionsView.tsx b/src/components/SessionsView.tsx index d68a318..cf199d5 100644 --- a/src/components/SessionsView.tsx +++ b/src/components/SessionsView.tsx @@ -7,7 +7,7 @@ import { AdapterTabs } from './AdapterTabs'; import { Button } from './ui/button'; import { Badge } from './ui/badge'; import { LoadingAnimation } from './ui/LoadingAnimation'; -import { ChevronLeft, ChevronRight, Plus, RefreshCw, Bell, BellOff, ArrowRightLeft, ClipboardList, MoreVertical } from 'lucide-react'; +import { ChevronLeft, ChevronRight, Plus, RefreshCw, ArrowRightLeft, ClipboardList, MoreVertical } from 'lucide-react'; import { timeAgo, dirName, PERMISSION_MODES } from '@/lib/utils'; import { api } from '@/lib/api'; import { getBrand, ADAPTER_BRANDS } from '@/lib/adapter-brands'; @@ -41,7 +41,7 @@ export function SessionsView({ activeSessionIds, refreshActive, } = useSessions(); - const { supported: pushSupported, subscribed, subscribe, unsubscribe } = usePushNotifications(); + const { subscribed } = usePushNotifications(); const [showBrowser, setShowBrowser] = useState(false); const [showHeaderMenu, setShowHeaderMenu] = useState(false); const [expandedId, setExpandedId] = useState(null); @@ -256,15 +256,6 @@ export function SessionsView({ <>
setShowHeaderMenu(false)} />
- {pushSupported && ( - - )}