* test(macos): add OPENCLAW_DEBUG_OPEN_MENU screenshot hook
* feat(macos): add live execution approval queue
* refactor(macos): replace menu injectors with owned status menu
* fix(macos): keep unconfigured menu header calm
* fix(macos): route status-item right-clicks through a local event monitor
NSControl's send-action mask ignores right mouse buttons, so the previous
sendAction(on: [.rightMouseUp]) wiring never fired and the menu was
unreachable by pointer. A local monitor now owns pointer routing (left =
dashboard, right = menu) — the same mechanism the shipped
StatusItemMouseRouter used — and menuWillOpen gained the re-entrancy
guard the old injector carried, since reconciling tracked rows can
re-enter the callback without a close.
* chore(i18n): refresh native inventory for status menu strings
* chore(macos): remove menu-refactor dead code
Periphery flagged the orphans the status-menu refactor left behind: the
ExecApprovalQuickMode enum and AppState's entire quick-mode read/retry
surface (its only consumer was the deleted menu picker; the Settings
pane owns exec-approval policy UI), SessionMenuLabelView,
TrackingAreaSupport, NodeMenuMultilineView, UpdateStatus.disabled, and
two fixture-only initializers. StatusMenuController.stop() is now wired
into applicationWillTerminate. The menu-highlight environment key moved
from the deleted view file into MenuItemHighlightColors.
* chore(macos): fix status-menu lint style and refresh i18n inventory
* fix(macos): converge approval cards after losing a resolution race
The status-menu queue and the modal prompter intentionally share the
gateway approval event stream: the gateway resolves each approval
exactly once, the resolved broadcast removes the card, and the modal
stays the active presentation owner while the menu is the passive,
ambient one. What was missing: when the menu's resolve loses the race
(modal or another client answered first), the gateway rejection left a
zombie card if the resolved event was dropped. Resolve failures now
re-list from the authoritative queue. Regression test simulates the
race at the socket boundary and fails pre-fix.
* fix(gateway): refresh provider usage asynchronously
* fix(ui): report a stalled provider-usage refresh on Model Providers
The page observed the incomplete-usage marker but discarded the exhausted
outcome, so once the retry budget was spent it rendered ordinary provider
cards with no usage and no explanation — indistinguishable from providers
that report no usage at all. Keep the outcome and render the warning the
Usage page already owns, reusing usage.providerUsage.stalled rather than
minting a Model Providers key so no locale baseline churns.
A user-initiated refresh now restarts the retry budget. The notice tells the
operator to refresh, so the button has to hand back attempts to spend; only
the forced path resets it, or the budget could never exhaust.
Also fixes tsgo:core:test on the current head: createStore's inferred literal
had no usageStats, so the run-bookkeeping case could not stamp it, and
view.test.ts needed the new prop.
Closes the ClawSweeper P2 at model-providers-page.ts:169-175.
* fix(ui): keep the stalled usage notice when usage.status starts rejecting
loadModelProvidersData turned a rejected usage.status into providerUsage:
null, which the page read as a completed load. observe(false) then reset the
retry budget and cleared the stalled callout, so a permanently broken usage
endpoint rendered as ordinary cards with no usage and no explanation — the
same silent failure the callout was added to prevent. The reset also fired
mid-cycle: one incomplete response followed by one rejection restarted the
budget, so the notice could be deferred indefinitely.
Record the failure at its producer instead of inferring it downstream. A null
providerUsage also means "not loaded yet", and no caller can tell the two
apart, so load.ts now reports providerUsageFailed explicitly and the page
treats a failed read as unresolved rather than resolved-empty.
Found by a Codex review of 417d43b65d.
* revert(gateway): drop the opportunistic model-catalog fast path
It broke two chat.history tests on main — both assert the cold catalog loader
runs exactly once, and reading the prepared snapshot first means it never does.
checks-node-compact-small-10 was red for that reason.
The change was a separate-surface latency fix that this PR picked up in passing,
and the body already offered to split it. Dropping it is the honest resolution:
rewriting main's assertions to accommodate a drive-by optimization would trade
one concern's proof for another's convenience. optional-model-catalog.ts,
server-model-catalog-auth.ts and their test return to the merge-base.
This PR is now only the usage.status non-blocking contract and its clients.
* fix(usage): preserve incomplete retry state
* perf(ui): keep usage capability startup-neutral
* fix(ui): restore provider usage retry convergence
* fix(usage): restore retry and cache invariants
* fix(usage): stabilize provider convergence
* test(ui): exercise provider recovery path
* test(ui): remove stale usage route fixture field
* fix(macos): show provider usage errors
* fix(macos): bound usage retries per menu open
* fix(macos): end usage retries on menu close
---------
Co-authored-by: Josh Lehman <550978+jalehman@users.noreply.github.com>