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:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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 = () => {
|
||||
|
||||
@@ -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'}`}>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
server: {
|
||||
host: true,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'https://localhost:3456',
|
||||
|
||||
Reference in New Issue
Block a user