Files
clawtap/src/components/SettingsView.tsx
T
izackp 2ded310472 feat: plan-approval overlay, auth-expiry handling, cert download, login reset
- Add a dedicated plan-approval bottom sheet (interactive-prompt) driven by
  ExitPlanMode's now-4-option layout (Claude Code v2.1.177), with
  approve/YOLO/reject/feedback wired through respondPlan(requestId).
- PermissionManager: stop auto-expiring AskUserQuestion/plan prompts and stop
  releasing pending prompts on client disconnect — they reflect real CLI
  state and must survive reconnects.
- Detect server-rejected tokens (401/403 or failed WS handshake) and bounce
  back to the login screen via a new AUTH_EXPIRED_EVENT.
- Add `/cert` endpoint + Settings entry so phones can download/trust the
  self-signed HTTPS cert directly; fix cert IP detection on non-macOS hosts.
- Add `clawtap reset-login` command and `/api/auth/reset-attempts` endpoint
  to clear login rate-limit lockouts.
- Codex adapter: only forward recognized keystrokes/option indices to tmux,
  never the synthetic "deny" dismissal signal.
- PWA: actively poll for service-worker updates (iOS standalone apps don't
  reliably check on navigation).
- update-service.sh: install tmux if missing, prompt to create the env file
  instead of failing, add --skip-firewall, and improve logging.
2026-06-15 16:30:24 -04:00

151 lines
6.0 KiB
TypeScript

import { useState, useEffect } from 'react';
import { ChevronLeft, ChevronRight, ClipboardList, Bell, Info, ShieldCheck } from 'lucide-react';
import { api } from '@/lib/api';
import { AdapterIcon } from './AdapterIcon';
import { SavedInstructionsView } from './SavedInstructionsView';
import { AdapterSettingsSection } from './AdapterSettingsSection';
import { usePushNotifications } from '@/hooks/usePushNotifications';
interface Adapter {
id: string;
displayName: string;
available: boolean;
}
export function SettingsView({ onBack }: { onBack: () => void }) {
const [subView, setSubView] = useState<'main' | 'instructions' | string>('main');
const [adapters, setAdapters] = useState<Adapter[]>([]);
const [version, setVersion] = useState<string>('');
const [certAvailable, setCertAvailable] = useState(false);
const { supported, subscribed, subscribe, unsubscribe } = usePushNotifications();
const [toggling, setToggling] = useState(false);
useEffect(() => {
api.adapters().then(setAdapters).catch(() => {});
fetch('/health')
.then(r => r.json())
.then((data: { version: string }) => setVersion(data.version))
.catch(() => {});
fetch('/cert', { method: 'HEAD' })
.then(r => setCertAvailable(r.ok))
.catch(() => {});
}, []);
if (subView === 'instructions') {
return <SavedInstructionsView onBack={() => setSubView('main')} />;
}
const matchedAdapter = adapters.find(a => a.id === subView);
if (matchedAdapter) {
return <AdapterSettingsSection adapter={subView} onBack={() => setSubView('main')} />;
}
const handleNotificationToggle = async () => {
if (toggling) return;
setToggling(true);
try {
if (subscribed) {
await unsubscribe();
} else {
await subscribe();
}
} finally {
setToggling(false);
}
};
const availableAdapters = adapters.filter(a => a.available);
return (
<div className="flex flex-col h-full bg-bg">
{/* Header */}
<div className="flex items-center px-4 py-3 border-b border-border safe-top">
<button onClick={onBack} className="text-text-dim hover:text-text mr-2"><ChevronLeft className="w-5 h-5" /></button>
<span className="text-lg font-medium text-text font-mono tracking-wide">Settings</span>
</div>
{/* Main list */}
<div className="flex-1 overflow-y-auto">
{/* Saved Instructions */}
<button
onClick={() => setSubView('instructions')}
className="w-full flex items-center px-4 min-h-[48px] border-b border-border hover:bg-surface-hover transition-colors"
>
<span className="mr-3"><ClipboardList className="w-5 h-5 text-text-dim" /></span>
<span className="flex-1 text-left text-text text-sm">Saved Instructions</span>
<ChevronRight className="w-4 h-4 text-text-dim" />
</button>
{/* Per-adapter rows */}
{availableAdapters.map(adapter => (
<button
key={adapter.id}
onClick={() => setSubView(adapter.id)}
className="w-full flex items-center px-4 min-h-[48px] border-b border-border hover:bg-surface-hover transition-colors"
>
<span className="mr-3">
<AdapterIcon adapterId={adapter.id} size={20} />
</span>
<span className="flex-1 text-left text-text text-sm">{adapter.displayName}</span>
<ChevronRight className="w-4 h-4 text-text-dim" />
</button>
))}
{/* Install HTTPS certificate (only relevant when serving over a self-signed cert) */}
{window.location.protocol === 'https:' && (
certAvailable ? (
<a
href="/cert"
download
className="w-full flex items-center px-4 min-h-[48px] border-b border-border hover:bg-surface-hover transition-colors"
>
<span className="mr-3"><ShieldCheck className="w-5 h-5 text-text-dim" /></span>
<span className="flex-1 text-left text-text text-sm">Install HTTPS Certificate</span>
<ChevronRight className="w-4 h-4 text-text-dim" />
</a>
) : (
<div className="w-full flex items-center px-4 min-h-[48px] border-b border-border">
<span className="mr-3"><ShieldCheck className="w-5 h-5 text-text-dim" /></span>
<div className="flex-1 text-left">
<div className="text-text text-sm">Install HTTPS Certificate</div>
<div className="text-text-dim text-xs font-mono">No cert found run `clawtap cert` on the server</div>
</div>
</div>
)
)}
{/* Notifications */}
{supported && (
<div className="w-full flex items-center px-4 min-h-[48px] border-b border-border">
<span className="mr-3"><Bell className="w-5 h-5 text-text-dim" /></span>
<span className="flex-1 text-text text-sm">Notifications</span>
<button
onClick={handleNotificationToggle}
disabled={toggling}
className={`relative w-11 h-6 rounded-full transition-colors ${
subscribed ? 'bg-accent' : 'bg-white/20'
} ${toggling ? 'opacity-50' : ''}`}
aria-label="Toggle notifications"
>
<span
className={`absolute top-0.5 left-0.5 w-5 h-5 rounded-full bg-white transition-transform ${
subscribed ? 'translate-x-5' : 'translate-x-0'
}`}
/>
</button>
</div>
)}
{/* About */}
<div className="w-full flex items-center px-4 min-h-[48px] border-b border-border">
<span className="mr-3"><Info className="w-5 h-5 text-text-dim" /></span>
<span className="flex-1 text-text text-sm">About</span>
<span className="text-text-dim text-xs font-mono">
{version ? `ClawTap v${version}` : 'ClawTap'}
</span>
</div>
</div>
</div>
);
}