* fix(ui): hide unusable models from picker
* refactor(ui): remove stale model availability helper
* refactor(ui): simplify catalog state guards
* style: format provider catalog imports
* chore: refresh plugin SDK API baseline
* refactor(core): break provider catalog type cycle
* chore(protocol): refresh models list Swift output
* chore: refresh plugin SDK API baseline after rebase
* fix(gateway): preserve full catalog preload semantics
* fix(ui): keep model status within startup budget
* fix(ui): preserve provider status within startup budget
* fix(models): scope live catalog outcomes
* test(ui): expect agent-scoped model refresh
* test(ui): align model refresh e2e fixtures
* fix(macos): make the whole AI candidate row clickable and clear stale exhausted verdicts
Live-testing pick-during-testing on 2026-08-11 showed clicks on a candidate
row's blank stretch (between the title/subtitle texts or over the spacer)
silently doing nothing: plain-style buttons only hit-test opaque label
pixels. A user trying to pick Claude Code while Codex auto-tests can click
the visually highlighted row and get no outcome. .contentShape(Rectangle())
makes the full row hit-test.
Also, after auto-candidates exhaust, a user-picked retest left the stale
"None of the found options worked" card up while the new test visibly ran;
userSelect now clears exhaustedAutoCandidates when a fresh attempt begins.
* test(tooling): drop stateless poll tests from stateful-helper fixtures
Main went red when #121923 rewrote the outbound poll tests to be
order-independent and removed their stateful helper import/file; this
lane-config test hardcoded both as stateful-helper classification
fixtures. Folded into this PR per red-main landing policy.
* fix(mac): scope managed CLI install and detection to the active app profile
Under an active OPENCLAW_PROFILE the onboarding ready page detected the
host-global managed CLI (~/.openclaw/bin/openclaw) and its update/install
flow rewrote the operator's real managed copy. The managed install prefix
now follows the profile state directory (~/.openclaw-<name>), preferred
paths exclude other profiles' managed trees (including stale validated
executables and inherited shell PATH entries), and external CLIs stay
detectable read-only. Default-profile behavior is unchanged.
* chore(mac): refresh native i18n inventory for shifted source lines
* feat(agents): rename spawn_task tool to suggest_task
Models across vendors read the name spawn_task as 'starts background
work' and refuse to call it when the user has not asked to start
anything (Claude Sonnet 5, verbatim: 'spawn_task (which actively starts
background work)... Since you didn't ask me to start work, I'm flagging
this as a follow-up instead'). The tool only records a suggestion card,
so the name defeated the feature.
Rename the model-facing tool to suggest_task everywhere (catalog,
descriptors, presets, display config, OpenClawKit tool-display, docs)
and migrate persisted operator state via doctor: config tool policies
and cron SQLite toolsAllow entries that reference spawn_task are
rewritten to suggest_task. No runtime alias remains; runtime exposes
only the canonical name.
Live clean-room matrix on a dev gateway (virgin session per cell):
suggested-task-card phrasing now yields exactly one card on
anthropic/claude-sonnet-5, claude-haiku-4-5, openai/gpt-5.6-sol,
gpt-5.6-luna, and google/gemini-3.1-pro; pre-rename the same bait
produced prose-only follow-ups.
* chore(doctor): keep legacy tool-name predicate module-local
isLegacyTaskSuggestionToolName has no external consumers; the exported
form tripped the knip unused-export gate (deadcode:exports).
* fix(doctor): keep tool-name migration out of plugin-owned config
The rename migration traversed the whole raw config, so any object
carrying a toolsAllow key was rewritten - including opaque
plugins.entries.*.config, which core must not mutate. Scope the apply
pass to the typed core roots (tools, agents, channels, gateway) and pin
plugin config preservation in the test. Plugin-owned tool lists migrate
via the owning plugin's doctor contract instead.
* fix(ci): heal main deadcode and stale Swift protocol mirror
Unrelated main breakage blocking this PR's merge gate, healed here per
landing policy:
- #121653 left dead exports in src/skills/workshop: make
removeSkillCollectionDirectory and SkillCollectionRestoreResult
module-local; register collection-review.ts under the documented knip
ignoreIssues test-only-export convention (production runs it via the
scheduled maintenance loop).
- #121673 added ApprovalResolveParams.reviewer without regenerating the
Swift mirror; regenerate GatewayModels.swift (additive only).
* fix(ci): satisfy generic approval-runtime request signature in resolver test
Third main-heal carried by this PR: #121673 (9935ca3b30) left the
scoped-request mock's concrete inferred type unassignable to the generic
GatewayNativeApprovalRuntime request signature, breaking check-test-types
on main. Cast the mock at the runtime literal; assertions keep the Mock.
* chore(ci): drop approval-test cast superseded by main's typed mock rewrite
* chore(ci): drop duplicate knip entry superseded by main's heal
* fix(workers): persist placement terminal failures
* fix(workers): refresh placement protocol clients
* refactor(workers): isolate error formatting
* fix: integrate cloud terminal state with current main
* chore(plugin-sdk): refresh API baseline
* refactor(ui): inline one-use cloud terminal-reason banner helper
Keeps the Control UI startup JS bundle inside its 317 KiB gzip budget
(the helper + type-only import tipped it by 16 bytes).
* refactor(ui): trim terminal-reason lookup to type-erased optional access
Recovers the last gzip byte of the Control UI startup budget
(324609 B vs the 324608 B limit).
* feat(gateway): task-suggestion acceptance modes with compact split-button card
- taskSuggestions.accept gains additive mode (worktree default | local | cloud | session) + cloudProfileId; operator.admin unchanged; worktree requests stay {taskId}-only for older gateways
- local reuses the plain-cwd create; cloud mirrors the Control UI create-empty -> sessions.dispatch -> send sequence with pre-claim profile validation and full rollback; session delivers the prompt into the source session (steer single active run, plain turn when idle, ambiguous refused) and can only cancel the chip on failure, never touch the source session
- Control UI card redesigned per owner reference: repo-basename eyebrow, clamped summary, instructions expander, split button with per-profile cloud items or a disabled "No cloud environment configured" hint
- TUI gains the same modes as selector rows with capability gating and double-Enter arming
Release-note context: operators now choose where an accepted follow-up runs — isolated worktree (default), the project checkout, a configured cloud worker, or as the next instruction in the current session — from a compact suggestion card.
* fix(gateway): negotiate task-suggestion acceptance modes and enforce steerable-run invariant
- Gate non-worktree task-suggestion actions on the negotiated Gateway capability.
- Require exactly one steerable run ID before delivering into an active source session.
* feat(gateway): live desktop observer for cloud workers
Adds live observation for cloud worker desktops through the gateway and Crabbox plugin, including desktop provisioning, persisted desktop metadata, tunneled WebSocket proxying, and the worker.desktop.observe protocol method.
The gateway, Crabbox plugin, and gateway protocol surfaces remain off by default behind the cloudWorkers.desktop Labs flag.
* feat(ui): Desktop panel for cloud worker observation
* docs(gateway): document cloud worker desktop lab
* fix(ci): regenerate contract baselines after rebase
* fix(protocol): regenerate Android gateway methods
* fix(ci): align rebased SDK and lint baselines
* fix(gateway): enforce view-only RFB boundary and fence desktop teardown
* fix(gateway): tighten RFB filter surface
* fix(state): keep pre-desktop databases readable and harden view-only RFB
* fix(gateway): fence desktop observer upgrades behind work admission
* fix(gateway): bind desktop observer tokens to their owner epoch
* fix(ci): regenerate config and SDK baselines after rebase
* fix(ci): regenerate native protocol and SDK baselines
* fix(ci): regenerate contracts after main rebase
* fix(state): register desktop metadata as lazy additive
* fix(ci): regenerate SDK baseline after final direct-merge rebase