chore: release 0.3.1 — iOS PWA fixes, notification improvements, docs update

- fix(pwa): iOS keyboard gap caused by WebKit viewport-fit=cover bug.
  After keyboard open/close, 100dvh permanently shrinks. Track max
  innerHeight in --app-height CSS variable as stable replacement.
- feat(pwa): auto-prompt notification permission on first login in
  standalone mode (once only, skips if denied).
- refactor: remove duplicate notification toggle from header menu
  (already in Settings).
- feat(dev): expose Vite dev server on network (host: true) for
  mobile testing via Tailscale.
- docs: update README — add Task Progress FAB, fix notification
  flow description, document OPENAI_API_KEY / VAPID_EMAIL env vars,
  clarify voice input backends, add CLI --version/--help, update
  .env.example.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
kuannnn
2026-03-30 05:58:56 +08:00
parent a1079766bd
commit 35b4519b94
8 changed files with 68 additions and 16 deletions
+6
View File
@@ -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
+15 -2
View File
@@ -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.
<details>
@@ -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.
+1 -1
View File
@@ -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": {
+39
View File
@@ -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<BeforeInstallPromptEvent | null>(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 = () => {
+3 -2
View File
@@ -490,7 +490,7 @@ export function ChatView({
if (!selectedAdapter) {
return (
<div className="flex flex-col h-dvh bg-bg items-center justify-center">
<div className="flex flex-col bg-bg items-center justify-center" style={{ height: 'var(--app-height, 100dvh)' }}>
<LoadingAnimation size="md" label="Connecting..." />
</div>
);
@@ -498,7 +498,8 @@ export function ChatView({
return (
<div
className="flex flex-col h-dvh bg-bg relative overflow-hidden safe-top"
className="flex flex-col bg-bg relative overflow-hidden safe-top"
style={{ height: 'var(--app-height, 100dvh)' }}
>
{/* Header — overlays content, slides up when scrolling */}
<div className={`absolute top-0 left-0 right-0 flex items-center gap-2 px-4 py-3 border-b border-border bg-bg z-10 safe-top transition-transform duration-200 ${headerHidden ? '-translate-y-full' : 'translate-y-0'}`}>
+2 -11
View File
@@ -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<string | null>(null);
@@ -256,15 +256,6 @@ export function SessionsView({
<>
<div className="fixed inset-0 z-40" onClick={() => setShowHeaderMenu(false)} />
<div className="absolute right-0 top-full mt-1 z-50 bg-surface border border-border rounded-lg py-1 min-w-[180px] shadow-lg">
{pushSupported && (
<button
onClick={() => { (subscribed ? unsubscribe() : subscribe()).catch(() => {}); setShowHeaderMenu(false); }}
className="w-full flex items-center gap-2 px-3 py-2 text-xs font-mono text-text-dim hover:text-text hover:bg-white/5 transition-colors"
>
{subscribed ? <Bell className="w-3.5 h-3.5" /> : <BellOff className="w-3.5 h-3.5" />}
{subscribed ? 'Disable notifications' : 'Enable notifications'}
</button>
)}
<button
onClick={() => { onOpenSettings(); setShowHeaderMenu(false); }}
className="w-full flex items-center gap-2 px-3 py-2 text-xs font-mono text-text-dim hover:text-text hover:bg-white/5 transition-colors"
+1
View File
@@ -6,6 +6,7 @@ export const STORAGE = {
DRAFT: 'clawtap:draft',
INSTALL_DISMISSED: 'clawtap:install-dismissed',
SESSIONS_TAB: 'clawtap:sessionsTab',
PUSH_PROMPTED: 'clawtap:push-prompted',
adapterPrefs: (id: string) => `clawtap:adapterPrefs:${id}` as const,
} as const;
+1
View File
@@ -49,6 +49,7 @@ export default defineConfig({
},
},
server: {
host: true,
proxy: {
'/api': {
target: 'https://localhost:3456',