* feat(gateway): typed structured questions on openclaw.chat
The chat result gains an additive optional question field (2-4 options, one
recommended max, per-option reply text). Producers: the two onboarding welcome
variants (apply-setup yes/ask, ready next-step) and hosted wizard select/confirm
steps, which mirror the awaited step so card clients render real wizard choices.
Prose replies always stand alone for text-only clients (macOS app, TUI). The
custodian page consumes the typed field and drops the PR1 string-marker parser
(never shipped in a release); cards send the option reply while the transcript
shows the label.
* chore: keep OnboardingWelcome type module-local
* chore(protocol): regenerate Swift bindings for the chat question field
* fix(macos): stop Canvas presents from stealing focus
Agent-driven canvas.present/canvas.navigate activated the app with
NSApp.activate(ignoringOtherApps:), yanking keyboard focus from
whatever the user was doing. The Canvas panel's elevated window level
already keeps it visible without app activation, and user entry points
(AppNavigationActions) activate explicitly, so presentation now orders
the panel front without switching apps.
* fix(macos): order Canvas front without taking key focus
* fix(macos): drop showWindow from Canvas window presentation
* fix(macos): keep Canvas hidden for agent content operations
Agent canvas.evalJS, canvas.snapshot, and canvas.a2ui.* commands routed
through CanvasManager.show, which re-presented the panel and called
NSApp.activate(ignoringOtherApps:) on every content update. After a
gateway restart the hosted A2UI page is dead, so the next agent push
always re-opened a panel the user had closed and stole focus.
Content operations now use a non-presenting ensureController/prepare
path; only canvas.present, canvas.navigate, and user actions reveal the
panel. Snapshot is read-only: it requires a visible matching panel and
returns CANVAS_HIDDEN instead of mutating panel state (WebKit cannot
render ordered-out windows).
* chore(i18n): refresh native inventory line offsets
* feat(mac): Quick Chat v2 — enable toggle, agent avatars and switching, zoom entrance, window screenshots
- Settings toggle (openclaw.quickChatEnabled) gating hotkey, menu item, recorder
- agent chip renders avatar image (data URIs only; remote URLs deliberately not
fetched to avoid SSRF), emoji, or tinted monogram; native NSMenu agent picker
routes via canonical agent:<id>:<mainKey> or global+agentId per server contract
- 'main session' capsule removed; placeholder carries the identity
- fade+zoom presentation (96% scale in, 97% out) via frame+alpha animators
- window screenshot picker: labeled overlays on renderable windows (Peekaboo
filtering + z-order hit tests), overlays torn down pre-capture, SCK window
capture, ChatImageProcessor JPEG, auto-send as chat.send attachment with the
draft or a default caption; ownership-token pipeline bound to presentation
and route; user-initiated Screen Recording alert; cancellable bounded discovery
- ⌘Return opens the accepted route's session; global-scope agent threading is a
filed follow-up; locale artifacts refreshed for new strings
* chore(mac): regenerate i18n inventory after rebase onto main
* feat(linux): add Quick Chat floating composer to the Tauri companion
- frameless transparent always-on-top quickchat window (on-demand, canvas
pattern), positioned top-third on the cursor monitor, CSS fade+zoom in/out
- global shortcut Ctrl+Shift+Space on X11 (GNOME reserves Alt+Space); tray
'Quick Chat' item is the Wayland entry point; blur/Esc/close-request hide
- sends via the CLI agent turn with --message-file staged 0600 (argv is
world-readable procfs) and cleaned up after the turn; failures surface as
desktop notifications; Ctrl+Enter also opens the dashboard
- default-agent identity chip (emoji/monogram) from agents list --json, 60s cache
- window-scoped capability + permission set; window-state denylist; docs
* chore(linux): register quickchat.js as a knip native-asset entry
* feat(chat): replay the active plan snapshot to reconnecting clients
Plan checklists were live-event-only: a client that connected or
refreshed mid-run saw nothing until the next update_plan call, and iOS
adoptRun actively wiped retained plan state on reconnect. The gateway
now records the latest normalized plan snapshot per run beside the
existing chat text buffers and replays it through the established
chat.history/chat.startup inFlightRun recovery contract (runId > plan >
text under the payload budget; a budget-dropped plan is sent as an
explicit empty snapshot so clients never preserve a stale checklist).
Web, iOS, and Android seed and reconcile the snapshot through one shared
contract riding each client's existing inFlightRun adoption gate: present
plan replaces, absent plan preserves (legacy gateway), a different or
terminated run clears, and stale history responses cannot clobber a newer
live plan. Verified live gap: openclaw/openclaw#108675 comment 5001475066.
* fix(ios): extract run-snapshot adoption below the file-length lint ceiling
ChatViewModel.swift crossed SwiftLint's 1500-line limit; the in-flight
snapshot adoption and plan reconciliation move to a focused extension.
Also refresh the native i18n inventory for shifted line anchors.
* fixup: regenerate i18n anchors and test formatting after rebase
* ci: retrigger after wedged queued runs
* fix(ui): use Object.hasOwn for the plan field presence probe
* fix(ios): annotate the plan-snapshot fixture init for periphery
* feat(clients): session detail parity — row actions, unread auto-clear, and subagent tree in the native chat sidebar
* fix(clients): overlay pending explicit unread values over stale session snapshots
* chore(i18n): translate new session action strings and regenerate Apple/Android catalogs
* chore(clients): delete dead pin helper and worktree memberwise init
* feat(macos): honor the selected microphone in Talk and surface live level, transcript, and mic picker in chat
* chore(i18n): translate new talk activity strings and regenerate Apple/Android catalogs
Both flakes (CI run 29573675642, macos-swift) were the same mechanism:
fixture timestamps anchored to wall clock at test start, raced against
the optimistic user-echo timestamp captured later at send time. The
pending-run drain (clearPendingRunIfAssistantMessagePresent ->
assistantHapticEvent(after:)) only matches assistant rows timestamped
at or after the user echo, and the fake transports' default
waitForRunCompletion returns .unavailable, so this comparison is the
only drain path in both tests.
ChatHapticsTests "durable assistant failure fires run failed": the
durable failure row was stamped test start + 1000ms. Under CI load a
>1s stall between fixture creation and send() left the row permanently
older than the user echo, so .runFailed never fired for either
parameterized case. Fix: HapticsTestTransport now takes a history
closure and the test stamps the failure row at requestHistory time,
which is always post-send by event order.
ChatStreamReplayTests "reconnect mid-run converges via history
refetch": the completed transcript rows were stamped test start +
100/900ms; the scripted payload has no inFlightRun/sessionInfo, so the
foreground refetch cannot drain and only the pending-run owner's
timestamp comparison can. Bootstrap plus send taking >900ms made the
assistant row older than the echo and the run never drained. Fix:
anchor the reconnect transcript to a timestamp captured after the send
completed.
Proof: injecting a 1.5s/1.2s stall before send reproduced both CI
failures exactly (timeout after ~16.5s/16.2s, both haptics cases
failing); with the fixes the tests pass even with the stalls injected.
Full ChatHapticsTests + ChatStreamReplayTests suites green; both fixed
tests stressed 10x with zero failures.
* feat(mac): add Quick Chat floating composer with global shortcut
- Spotlight-style non-activating key panel on Option+Space (KeyboardShortcuts
3.0.1 exact-pinned, recorder row in Settings -> General) plus a menu bar item
- shows the main-session agent identity via agent.identity.get with a
'main session' chip; Return sends via chat.send reusing the idempotency key
on ambiguous retries; Cmd+Return also opens full chat; Shift+Return newline
- targeted permission strip (notifications/accessibility/screen recording)
with a scoped 1s status poll while visible; grant flow keeps the bar alive
and avoids PermissionMonitor's AppleScript probe
- extract shared ChatSendStatus acceptance mapping; TalkModeRuntime adopts it
- rename anchored-panel 'quick chat' wording to 'compact chat panel'; docs
* chore(mac): regenerate docs map and native i18n inventory for Quick Chat
* ci: retrigger checks after stalled push event
* chore(mac): refresh native locale artifacts for Quick Chat strings
* chore(android): regenerate locale strings for refreshed shared translations
* feat(android): add Wear OS companion
Co-authored-by: Sebastian Schubotz <git@sibbl.net>
Co-authored-by: IWhatsskill <284122573+IWhatsskill@users.noreply.github.com>
* style(android): format Wear release gate test
* test(android): cover Wear release CI contracts
* test(android): import Wear JSON fixture type
* fix(android): harden Wear proxy event actor
* test(android): stabilize Wear proxy actor tests
* test(android): isolate Wear proxy actor lifecycles
* test(android): own Wear actor dispatchers
* test(android): use real time for Wear actor tests
* test(android): own Wear actors in test scope
* test(android): stop Wear actors explicitly
* test(android): drain Wear actor cancellation
* test(android): isolate Wear actors from test scheduler
* test(android): inject Wear test clock
---------
Co-authored-by: IWhatsskill <284122573+IWhatsskill@users.noreply.github.com>
* feat(gateway): add sessions.files.reveal to open a session workspace on the gateway host
Resolves the session's workspace root server-side (same resolution as
sessions.files.list), refuses exec-node and cloud-worker placements, and
opens the directory with the platform opener. The opener helpers move out
of config.ts into a shared open-path module that config.openFile now uses.
* feat(ui): always-on chat title bar with inline rename and workspace menu
Every chat pane now renders its header: click the session title to rename
it inline (Enter commits via sessions.patch, Escape cancels), a workspace
chip names the session's checkout with reveal/copy-path/copy-branch
actions, and cloud-worker sessions show a globe. The single-pane floating
toggle cluster and the transcript's reserved titlebar drag band fold into
the header, which keeps native macOS window dragging.
* fix(ui): resolve title-bar session aliases and clear the collapsed-nav overlay
Live verification found two gaps: route aliases like ?session=main showed
the generic key-derived title instead of the session's label (resolve via
hello defaults + key equivalence, matching the pane), and the floating
sidebar-expand pill overlapped the header title on plain web while the nav
was collapsed (52px inset, mirroring the native-shell clearances).
* fix(ui): keep the protocol schema layer out of the Control UI startup bundle
The title-bar change made session-row-badges runtime-import the
gateway-protocol barrel for one classifier, dragging typebox and every
schema into startup JS (395 KiB gzip vs the 370 KiB budget). The placement
vocabulary now lives in a dependency-free session-placement-state module:
the schema union derives from it, the barrel re-exports it, and the UI
deep-imports it (startup back to 361.8 KiB).
* fix(gateway-protocol): keep SessionPlacementStateSchema statically typed
Type.Union over a mapped array loses tuple inference and collapses Static
to never; the literal list stays explicit with a compile-time guard tying
it to the shared SESSION_PLACEMENT_STATES vocabulary.
* chore(protocol): regenerate Swift/Kotlin bindings for sessions.files.reveal
* chore: narrow new exports flagged by the deadcode gate
* chore(i18n): leave generated locale artifacts to the locale-refresh workflow
The generated-artifact isolation gate (067635cb51) landed mid-flight;
source PRs now carry en.ts only.
The watch inbox's empty and waiting surface gets a 72pt Clawd: sleepy
(nightcap and all) when nothing is waiting, thinking while the watch
waits for iPhone sync, attentive when an approval needs a decision.
The Always-On display renders a static pose so the animation loop
never runs dimmed, and the active loop is throttled to 15fps for
battery. The shared mascot gains a defaulted minimum frame interval;
mascot sources compile directly into the watch target following the
TalkWaveformView pattern.
* feat(android): add Wear proxy protocol
Introduce a bounded, versioned phone/watch RPC contract and wire its test, lint, and build gates before either runtime endpoint lands.
Co-authored-by: Sebastian Schubotz <git@sibbl.de>
* test(android): lock Wear wire names
Cover every supported RPC method and event discriminator so phone and watch cannot silently drift.
Co-authored-by: Sebastian Schubotz <git@sibbl.net>
* fix(android): bound Wear JSON depth
Reject excessively nested Data Layer envelopes before kotlinx.serialization can recurse through them.
* fix(android): enforce Wear depth on encode
Keep outbound messages inside the same JSON nesting contract enforced by the decoder.
* fix(android): preflight Wear payload depth
Traverse arbitrary JSON payloads iteratively before serialization so deeply nested local trees fail safely.
* feat(android): add Wear phone proxy
Co-authored-by: Sebastian Schubotz <git@sibbl.net>
* fix(ci): repair Wear and docs sync checks
---------
Co-authored-by: Sebastian Schubotz <git@sibbl.de>
Co-authored-by: Sebastian Schubotz <git@sibbl.net>
Ports the deterministic mascot animator to Android: full pose channels
(face, gaze, droop, hard hat, effects), seeded mood loops including the
hard-hat working hammer cycle, entrance gestures, and static poses when
system animations are off. Onboarding heroes now feel the flow — working
while the gateway connects, celebrating once paired, thinking during
node approval, curious on permissions, sad on visible errors. Tinted
silhouette marks keep their ambient idle loop and stay silent for
TalkBack outside the welcome hero.
The standalone chat window stacked a visible title bar (traffic lights +
"OpenClaw" title) above the SwiftUI toolbar row, wasting a full band of
vertical space. Apply the Dashboard window's merged chrome: hidden title,
transparent titlebar, no separator, movable-by-background — the unified
toolbar (sidebar toggle, search, pickers, session actions) now shares its row
with the traffic lights. A window subclass pins titleVisibility to hidden
because SwiftUI's toolbar bridge can restore visible title chrome, and scene
bridging is reset to toolbars-only after controller attachment (attaching an
NSHostingController resets it to .all).
The companion now rings the doorbell for gateway approvals: it polls pending
node and device pairing requests over the existing CLI seam on the connected
watchdog cadence, fires one native notification per new request when the
window is unfocused, and shows a pending count in the tray status line.
Approval itself stays in the dashboard/CLI (owner boundary).
Adds a CmdOrCtrl+Shift+O summon shortcut with a tray toggle whose opt-out
persists as a marker file (no config schema). The X11-only global-hotkey
backend is gated off on native Wayland sessions — the feature is omitted
rather than half-registered through XWayland; a portal implementation can
follow.
Single-instance + openclaw:// deep links (openclaw://dashboard opens and
connects the dashboard; the tiny URL contract is routed entirely in Rust), a
Start at Login tray toggle (tauri-plugin-autostart, default off), native
notifications for update-ready/update-available when the window is unfocused,
and window-state persistence (canvas window excluded). All official Tauri v2
plugins; cross-platform deps so the macOS/Windows test builds keep compiling.