feat(web): mobile-first chat — native UI, voice dictation, installable PWA - #50140
feat(web): mobile-first chat — native UI, voice dictation, installable PWA#50140alexzehe wants to merge 1 commit into
Conversation
…e PWA Replaces the terminal-in-the-browser with a native chat experience on phones and makes the dashboard installable as a PWA. Desktop is unchanged. - MobileChat: on screens <=640px, render a native chat (message bubbles, streamed markdown, collapsible reasoning above the answer, inline ordered tool cards with readable args, image/PDF attachments, resume-from-history) driven by the tui_gateway JSON-RPC WebSocket, instead of the xterm/PTY terminal. Desktop keeps the terminal. - Voice dictation: the mic records on the device and POSTs to the existing /api/audio/transcribe endpoint (local faster-whisper by default); the transcript fills the composer. - Claude-style empty state (time-based greeting) + composer (attach, model pill, mic, send) with auto-growing input. - Installable PWA: web manifest, app icons, Apple meta tags, and a minimal pass-through service worker (no app-code caching, so updates aren't stale). PWA static assets are added to the dashboard-auth public allowlist so the OS can fetch the home-screen icon during install (that fetch carries no cookie). - Standalone/iOS polish: safe-area top inset on the mobile header, 100lvh in standalone to fill the screen, full-bleed chat, hide the chat page-header on phones, theme-color the page canvas, lock pinch-zoom. - pdf.attach: fall back to PyMuPDF (fitz) when poppler/pdftoppm is unavailable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
teknium1
left a comment
There was a problem hiding this comment.
Thank you for the substantial mobile dashboard contribution. The native-chat goal is still unmet on current main, but this version needs correctness work before it can safely replace the phone PTY surface.
Problems
web/src/components/MobileChat.tsx:334callsapproval.respondwithoutsession_id; currenttui_gateway/server.py:10215-10219requires a session before resolving the approval.- The phone branch mounts
MobileChatwithout the selected profile. Currentweb/src/pages/ChatPage.tsx:299scopes dashboard chat throughscopedProfile, whileMobileChataccepts but never receives that prop. MobileChat.tsx:218-223clears its live session on reconnect; with no initial?resume=,MobileChat.tsx:209-212creates a fresh session instead of recovering the active conversation.- Root-absolute PWA URLs at
web/index.html:10-11andmanifest.webmanifest:5,12are not rewritten by the prefix handling inhermes_cli/web_server.py:15790-15798.
Suggested changes
- Carry
session_idfor approvals, propagatescopedProfile, and adopt a reconnect strategy that preserves the durable session identity. - Make PWA URLs prefix-aware and add focused protocol, profile, reconnect, and prefix tests.
Automated hermes-sweeper review.
|
|
||
| const stop = useCallback(() => { if (sidRef.current) sendRpc("session.interrupt", { session_id: sidRef.current }); }, [sendRpc]); | ||
| const respondApproval = useCallback((item: ApprovalItem, choice: "approve" | "deny") => { | ||
| sendRpc("approval.respond", { approval_id: item.approvalId, choice }); |
There was a problem hiding this comment.
approval.respond requires the live session_id: current tui_gateway/server.py:10215-10219 calls _sess(params, rid) before resolving the approval. Include session_id: sidRef.current, otherwise every inline approval returns a session error.
| if (unmounting.current) return; | ||
| sidRef.current = ""; curText.current = null; curReason.current = null; | ||
| setRunning(false); setConn("closed"); | ||
| reconnectTimer.current = setTimeout(() => void connect(), 1500); |
There was a problem hiding this comment.
This reconnect drops sidRef, and the gateway-ready path creates a new session whenever the original resume prop is absent. A transient phone-network disconnect therefore abandons a newly created conversation; preserve and resume its durable session identity.
| // Key by the resume target so picking a different chat from history | ||
| // remounts MobileChat and resumes that specific session (not the first). | ||
| return <MobileChat key={resumeParam ?? "new"} resume={resumeParam} />; | ||
| } |
There was a problem hiding this comment.
Pass the selected scopedProfile here. Main scopes the terminal chat via scopedProfile (ChatPage.tsx:299,914), but this branch creates/resumes the native chat without it, so a selected management profile is ignored on phone-sized screens.
| content="width=device-width, initial-scale=1.0, viewport-fit=cover" | ||
| content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" | ||
| /> | ||
| <link rel="manifest" href="/manifest.webmanifest" /> |
There was a problem hiding this comment.
This root-absolute manifest URL bypasses an X-Forwarded-Prefix deployment. Current _serve_index rewrites /assets, favicon, fonts, and ds-assets only (hermes_cli/web_server.py:15790-15798); make this URL prefix-aware or extend that rewrite with coverage.
Summary
Makes the dashboard a first-class experience on phones: on small screens the chat renders as a native messaging UI instead of the terminal-in-the-browser, and the dashboard is now an installable PWA. Desktop is unchanged.
This came out of a real deployment — a family running Hermes on a home Mac, reached over Tailscale from their phones — so the focus was making it genuinely pleasant to use day-to-day from an iPhone.
What's included
Native mobile chat —
web/src/components/MobileChat.tsx, rendered on viewports ≤640px (desktop keeps the xterm/PTY terminal):tool.startcloses the current text segment so the final answer renders after the tools; args/results are JSON-stringified (no more[object Object])?resume=), interrupt/stop, inline approvalstui_gatewayJSON-RPC WebSocket (session.create/prompt.submit+ streamed events)Voice dictation — the mic records on the device and POSTs to the existing
POST /api/audio/transcribeendpoint (localfaster-whisperby default); the transcript fills the composer.Claude-style UI — time-based greeting empty state; composer card (attach · model pill · mic · send) with an auto-growing input.
Installable PWA — web manifest, app icons (reusing
apps/desktop/assets/icon.png), Apple meta tags, and a minimal pass-through service worker (no app-code caching, so updates are never served stale). PWA static assets (/icons,/manifest.webmanifest,/sw.js) are added to the dashboard-auth public allowlist so the OS can fetch the home-screen icon during "Add to Home Screen" (that fetch carries no session cookie).Standalone / iOS polish —
env(safe-area-inset-top)on the mobile header,100lvhin standalone mode to fill the screen, full-bleed chat, hide the chat page-header on phones, theme-colored page canvas, locked pinch-zoom.Backend —
pdf.attachfalls back to PyMuPDF (fitz) whenpdftoppm/poppler is unavailable (e.g. macOS without Homebrew).Scope / safety
Testing
tsc+vite buildpass on top of currentmain.Screenshots
Native mobile chat on iOS — ordered inline tool cards, streamed reasoning, and the composer (attach · model pill · mic · send):