From 89267776ee2f451626ecee865de88ccd5191dcda Mon Sep 17 00:00:00 2001 From: Krzysztof Wende Date: Thu, 9 Apr 2026 23:19:27 +0200 Subject: [PATCH 1/3] OpenClaw streaming, gateway auth, and UI improvements Overhaul the OpenClaw runtime to track agent lifecycle phases and prevent premature finalization on intermediate chat:final events. Add gateway device-auth with token caching, session bridging, and v3 protocol handshake. Improve history reconciliation with a wider optimistic dedup window and stable ID assignment. Introduce tool result plugin injection, turn activity box, detached document-scroll chat mode for iOS Safari, and numerous chat input and message rendering refinements. --- AGENTS.md | 19 +- CHANGELOG.md | 13 + CLAUDE.md | 19 +- CONCEPTS.md | 5 +- app/globals.css | 13 + app/layout.tsx | 21 +- app/page.tsx | 96 ++- components/ChatInput.tsx | 217 +++--- components/MessageRow.tsx | 504 ++++++++++---- components/SlideContent.tsx | 2 +- components/ToolCallPill.tsx | 160 ++++- components/TurnActivityBox.tsx | 347 ++++++++++ components/chat/ChatComposerBar.tsx | 71 +- components/chat/ChatViewport.tsx | 224 +++++- ...os-safari-detached-document-scroll-chat.md | 75 ++ ...6-04-09-streaming-regression-postmortem.md | 76 ++ docs/plugin-system.md | 2 +- hooks/chat/useDemoRuntime.ts | 13 +- hooks/chat/useModeBootstrap.ts | 2 +- hooks/chat/useNativeBridgeMessage.ts | 10 +- hooks/chat/useOpenClawRuntime.ts | 652 +++++++++++++++++- hooks/useAppMode.ts | 38 +- hooks/useIsMobileViewport.ts | 25 + hooks/useScrollManager.ts | 203 +++--- hooks/useSessionSwitcher.ts | 9 +- ios/MobileClaw/Bridge/BridgeMessages.swift | 9 + ios/MobileClaw/Bridge/WebViewBridge.swift | 40 +- .../Networking/DeviceIdentity.swift | 32 +- ios/MobileClaw/Resources/contextPrefixes.json | 2 +- lib/chat/chatEventUpsert.ts | 108 ++- lib/chat/historyResponse.ts | 260 ++++++- lib/chat/layout.ts | 36 +- lib/chat/layoutMode.ts | 43 ++ lib/chat/messageTransforms.ts | 47 +- lib/chat/pluginTagParser.ts | 77 +++ lib/chat/streamMutations.ts | 258 +++++-- lib/chat/thinkingUtils.ts | 60 ++ lib/chat/toolEventUtils.ts | 56 ++ lib/chat/toolResultPlugins.ts | 214 ++++++ lib/constants.ts | 29 + lib/demoMode.ts | 300 ++++++-- lib/deviceIdentity.ts | 34 +- lib/gatewayAuth.ts | 226 ++++++ lib/gatewayClientMetadata.ts | 67 ++ lib/nativeBridge.ts | 46 +- lib/toolDisplay.ts | 17 + lib/widgetContext.ts | 6 + next-env.d.ts | 2 +- package.json | 2 +- pnpm-lock.yaml | 10 +- shared/contextPrefixes.json | 2 +- tests/MessageRow.test.tsx | 72 +- tests/ToolCallPill.test.tsx | 67 ++ tests/TurnActivityBox.test.tsx | 29 + tests/chatStreamMutations.test.ts | 153 ++++ tests/constants.test.ts | 77 +++ tests/demoMode.test.ts | 4 +- tests/gatewayAuth.test.ts | 123 ++++ tests/historyResponse.test.ts | 74 ++ tests/layout.test.ts | 42 +- tests/layoutMode.test.ts | 121 ++++ tests/pluginTagParser.test.ts | 97 +++ tests/toolEventUtils.test.ts | 52 ++ tests/useAppMode.test.ts | 43 ++ tests/useNativeBridgeMessage.test.tsx | 18 +- tests/useOpenClawRuntime.test.tsx | 537 +++++++++++++++ types/chat.ts | 142 +++- widget.tsx | 24 +- 68 files changed, 5762 insertions(+), 712 deletions(-) create mode 100644 components/TurnActivityBox.tsx create mode 100644 dev-notes/2026-03-25_ios-safari-detached-document-scroll-chat.md create mode 100644 docs/dev-notes/2026-04-09-streaming-regression-postmortem.md create mode 100644 hooks/useIsMobileViewport.ts create mode 100644 lib/chat/layoutMode.ts create mode 100644 lib/chat/pluginTagParser.ts create mode 100644 lib/chat/thinkingUtils.ts create mode 100644 lib/chat/toolEventUtils.ts create mode 100644 lib/chat/toolResultPlugins.ts create mode 100644 lib/gatewayAuth.ts create mode 100644 lib/gatewayClientMetadata.ts create mode 100644 tests/ToolCallPill.test.tsx create mode 100644 tests/TurnActivityBox.test.tsx create mode 100644 tests/gatewayAuth.test.ts create mode 100644 tests/layoutMode.test.ts create mode 100644 tests/pluginTagParser.test.ts create mode 100644 tests/toolEventUtils.test.ts create mode 100644 tests/useAppMode.test.ts create mode 100644 tests/useOpenClawRuntime.test.tsx diff --git a/AGENTS.md b/AGENTS.md index 39859d5..a5bf871 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -125,8 +125,17 @@ MobileClaw supports three backend modes, selectable in the setup dialog: MobileClaw connects to OpenClaw's gateway WebSocket. Protocol frames: 1. **Server sends** `event:connect.challenge` with nonce -2. **Client responds** with `req:connect` including auth token, capabilities -3. **Server responds** with `res:hello-ok` including server info, session snapshot -4. **Client requests** `req:chat.history` to load message history -5. **Messages flow** via `event:chat` (delta/final/aborted/error) and `event:agent` (content/tool/reasoning/lifecycle streams) -6. **Client sends** `req:chat.send` with user messages +2. **Client responds** with `req:connect` using protocol `3`, a v3 device-auth signature, and either a shared auth token or a cached `auth.deviceToken` +3. **Server responds** with `res:hello-ok` including server info, session snapshot, `features`, `policy`, and optional auth state +4. **Client persists** `hello-ok.auth.deviceToken` per normalized gateway URL and reuses it on later connects when the shared token hash still matches +5. **Client requests** `req:chat.history` to load message history +6. **Messages flow** via `event:chat` (delta/final/aborted/error) and `event:agent` (content/tool/reasoning/lifecycle streams) +7. **Client optionally subscribes** to `sessions.subscribe` and `sessions.messages.subscribe` when advertised, but still reconciles transcript state from `chat.history` +8. **Client sends** `req:chat.send` with user messages + +### Protocol Notes + +- Keep `lib/deviceIdentity.ts` and `ios/MobileClaw/Networking/DeviceIdentity.swift` aligned. They must sign the same v3 auth payload and normalize `platform` / `deviceFamily` the same way. +- Device-token cache is implemented in `lib/gatewayAuth.ts` under `mc-openclaw-device-auth-v1`. Native storage goes through `lib/nativeBridge.ts` and `ios/MobileClaw/Bridge/WebViewBridge.swift`. +- `session.message`, `session.tool`, and `sessions.changed` are used as invalidation signals. They should not directly mutate the rendered transcript. +- `AUTH_TOKEN_MISMATCH` may trigger one reconnect with cached device approval. `DEVICE_AUTH_*` errors should surface immediately. diff --git a/CHANGELOG.md b/CHANGELOG.md index 923baaa..93a564f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes to MobileClaw are documented in this file. +## 2026-04-07 + +### Added +- OpenClaw gateway-auth cache (`mc-openclaw-device-auth-v1`) for persisted `hello-ok.auth.deviceToken` reuse across reconnects on web and in the native iOS shell +- Native bridge support for gateway-auth storage (`gatewayAuth:get`, `gatewayAuth:set`, `gatewayAuth:delete`) backed by Keychain on iOS +- Runtime tests covering hello-ok auth persistence, cached-device-token reuse, bounded `AUTH_TOKEN_MISMATCH` retry, and session invalidation events + +### Changed +- OpenClaw handshake now signs the v3 device-auth payload with `platform` and `deviceFamily` on both web and iOS; MobileClaw no longer silently downgrades when device signing fails +- `types/chat.ts` now models `hello-ok` auth/features/policy payloads plus newer gateway method and event names without hard-coding the full surface area +- `useOpenClawRuntime.ts` now capability-gates `sessions.subscribe` / `sessions.messages.subscribe`, treats `session.message` / `session.tool` / `sessions.changed` as invalidation signals, and surfaces gateway shutdown/auth guidance more explicitly +- Session switcher now marks session lists dirty and refreshes immediately when session-change events arrive + ## 2026-03-03 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index a3a65ef..7e8f41a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -52,11 +52,20 @@ MobileClaw has a native iOS app that wraps the webapp in a WKWebView. See [`ios/ MobileClaw connects to OpenClaw's gateway WebSocket. Protocol frames: 1. **Server sends** `event:connect.challenge` with nonce -2. **Client responds** with `req:connect` including auth token, capabilities -3. **Server responds** with `res:hello-ok` including server info, session snapshot -4. **Client requests** `req:chat.history` to load message history -5. **Messages flow** via `event:chat` (delta/final/aborted/error) and `event:agent` (content/tool/reasoning/lifecycle streams) -6. **Client sends** `req:chat.send` with user messages +2. **Client responds** with `req:connect` using protocol `3`, a v3 device-auth signature, and either a shared auth token or a cached `auth.deviceToken` +3. **Server responds** with `res:hello-ok` including server info, session snapshot, `features`, `policy`, and optional `auth.deviceToken` state +4. **Client persists** `hello-ok.auth.deviceToken` per normalized gateway URL and reuses it on later connects when the current shared token hash still matches +5. **Client requests** `req:chat.history` to load message history +6. **Messages flow** via `event:chat` (delta/final/aborted/error) and `event:agent` (content/tool/reasoning/lifecycle streams) +7. **Client optionally subscribes** to `sessions.subscribe` and `sessions.messages.subscribe` when the gateway advertises those methods, but still uses `chat.history` as the source of truth for transcript reconciliation +8. **Client sends** `req:chat.send` with user messages + +### Protocol Notes + +- `lib/deviceIdentity.ts` and `ios/MobileClaw/Networking/DeviceIdentity.swift` must stay aligned. Both build the same v3 auth payload: `v3|deviceId|clientId|clientMode|role|scopes|signedAtMs|token|nonce|platform|deviceFamily`. +- Device-token cache lives in `lib/gatewayAuth.ts` under `mc-openclaw-device-auth-v1`. Web uses `localStorage`; native uses the bridge-backed Keychain store. +- MobileClaw treats `session.message`, `session.tool`, and `sessions.changed` as invalidation signals only. The realtime UI still renders from `chat` and `agent` events. +- On `AUTH_TOKEN_MISMATCH`, MobileClaw retries once with a cached device token if the gateway says `canRetryWithDeviceToken`. `DEVICE_AUTH_*` errors are treated as client bugs and are surfaced directly. **ALWAYS use cicada-mcp tools for Elixir and Python code searches. NEVER use Grep/Find for these tasks.** diff --git a/CONCEPTS.md b/CONCEPTS.md index 7a1de0c..7068106 100644 --- a/CONCEPTS.md +++ b/CONCEPTS.md @@ -24,6 +24,7 @@ - **Hook**: `hooks/useAppMode.ts` — provides `isNative`, `isNativeRef` - **Activation**: URL param `?native=true` or `window.__nativeMode === true` - **Effect**: Adds `"native"` class to body, hides chrome, transparent background, uses `lib/nativeBridge.ts` for Swift ↔ WebView communication +- **Auth/storage bridge**: Native handles device signing and gateway-auth cache persistence through `identity:sign`, `gatewayAuth:get`, `gatewayAuth:set`, and `gatewayAuth:delete` - **Bootstrap**: Native bridge handler registered in `hooks/chat/useModeBootstrap.ts` --- @@ -142,7 +143,9 @@ All blocks are rendered inside `MessageRow.tsx`. Block data is represented as `C ### OpenClaw (Default) - **Runtime**: `hooks/chat/useOpenClawRuntime.ts` - **Protocol**: WebSocket (`lib/useWebSocket.ts`) -- **Flow**: Challenge → connect → hello-ok → history → event streams (`event:chat`, `event:agent`) +- **Flow**: Challenge → signed protocol-3 connect → hello-ok → history → event streams (`event:chat`, `event:agent`) +- **Auth**: `lib/deviceIdentity.ts` signs v3 connect challenges; `lib/gatewayAuth.ts` persists per-gateway device tokens and handles one-shot retry on `AUTH_TOKEN_MISMATCH` +- **Session freshness**: Uses `sessions.subscribe` / `sessions.messages.subscribe` when the gateway advertises them, but only as invalidation signals for `sessions.list` and `chat.history` - **Features**: Session switching, model selection, slash commands, full agent protocol ### LM Studio diff --git a/app/globals.css b/app/globals.css index 4b616a9..ccac33b 100644 --- a/app/globals.css +++ b/app/globals.css @@ -128,6 +128,16 @@ --color-sidebar-ring: var(--sidebar-ring); } +[data-mobileclaw-embedded] { + --primary: oklch(0.313 0 0); + --primary-foreground: oklch(0.985 0 0); +} + +.dark [data-mobileclaw-embedded] { + --primary: oklch(0.258 0 0); + --primary-foreground: oklch(0.911 0 0); +} + @theme { /* Font-size scale — boosted so text-sm = 16px body baseline */ --text-2xs: 0.75rem; /* 12px */ @@ -148,6 +158,9 @@ * { @apply border-border outline-ring/50; } + html { + @apply bg-background text-foreground; + } body { @apply bg-background text-foreground; overflow: hidden; diff --git a/app/layout.tsx b/app/layout.tsx index b9f52c8..eec0ce5 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -32,9 +32,15 @@ export const metadata: Metadata = { // Also registers the service worker for PWA support const headScript = ` (function() { + var isDev = ${JSON.stringify(process.env.NODE_ENV !== 'production')}; var params = new URLSearchParams(location.search); var detached = params.has('detached'); var detachedMode = detached ? params.get('mode') : null; + var host = location.hostname; + var isIpV4 = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(host); + var isLocalHost = host === 'localhost' || host === '127.0.0.1' || host === '0.0.0.0' || host.endsWith('.local'); + var isPrivateLanIp = /^10\\./.test(host) || /^192\\.168\\./.test(host) || /^172\\.(1[6-9]|2\\d|3[0-1])\\./.test(host); + var shouldDisableServiceWorker = isDev || isLocalHost || (isIpV4 && isPrivateLanIp); try { if (detachedMode === 'dark') { document.documentElement.classList.add('dark'); @@ -53,8 +59,19 @@ const headScript = ` if (detached) { document.documentElement.classList.add('detached-loading'); } - if ('serviceWorker' in navigator && location.hostname !== 'localhost' && !window.__nativeMode) { + if ('serviceWorker' in navigator && !window.__nativeMode) { window.addEventListener('load', function() { + if (shouldDisableServiceWorker) { + navigator.serviceWorker.getRegistrations().then(function(registrations) { + registrations.forEach(function(registration) { registration.unregister(); }); + }); + if (window.caches && caches.keys) { + caches.keys().then(function(keys) { + keys.forEach(function(key) { caches.delete(key); }); + }); + } + return; + } navigator.serviceWorker.register('/sw.js'); }); } @@ -67,7 +84,7 @@ export default function RootLayout({ children: React.ReactNode }>) { return ( - +