* feat(ui): redesign Channels page with guided channel setup wizard
Adds wizard.start flow=channels gateway RPC (additive protocol change) that
drives the shared channel-setup wizard (openclaw channels add) over the
session step protocol. Control UI Channels page becomes a card hub with
plugin-art covers, guided per-channel setup modal (WhatsApp QR pairing via
web.login.*, BotFather/Slack/Discord helper links), and a detail overlay
hosting the full schema config form.
* test(ui): cover channel wizard controller; refresh channels view test props
* chore(mock): use non-token-shaped wizard placeholder
* test(gateway): scanner-safe auth local in channels wizard test
* fix(ui): cancel gateway wizard session on channels page disconnect
* fix(ui): adopt browse-all channel pick and guard dirty config before setup
* fix(ui): cancel gateway session created by a stale wizard.start
* fix(ui): adopt multiselect channel picks; drop redundant String()
* fix(ui): track all wizard-adopted channels so WhatsApp QR runs regardless of pick order
* feat(gateway): report configured channels on terminal channel-wizard result
Replaces the Control UI's channel-adoption heuristic with the authoritative
outcome: the channels flow reports its actual selection after config commit,
the wizard session surfaces it as an additive channels field on the terminal
wizard.next result, and the UI keys WhatsApp QR linking off that.
* refactor(ui): align channels hub with the unified settings design language
Hub becomes settings-language rows (44px art tiles, status dots, uppercase
section headings, hairline groups) instead of a card gallery; the detail
overlay hosts the migrated per-channel settings sections with Run setup in
the header. Wizard dialog and tile/cover styles move to spacing tokens.
* chore(i18n): translate channels hub/setup strings; refresh raw-copy baseline
* refactor: satisfy LOC ratchet and dead-export gates
Split mock-dev channel/plugin fixtures into their own modules, move wizard
runner types/defaults to server-methods/wizard.ts, extract the channels page
wizard host and nostr profile HTTP ops, and unexport internal-only types
(incl. the unused config-form SECTION_META barrel re-export).
* chore(i18n): retranslate channels strings on rebased locale bundles
* fix(test): drop redundant String() in channels wizard e2e
* fix: address channel-wizard review findings
- lock wizard cancellation before durable installs/config writes
- report configured channel accounts on the terminal wizard result and
start WhatsApp QR pairing for that account (web.login accountId)
- forward request options through the browser gateway client so wizard
RPC timeouts apply
- render skipped setup as a no-change completion
- keep detail/advanced config reachable for unconfigured channels
- surface the dirty-config warning inside the detail overlay
- adopt the picked channel for wizard title/links in browse-all flows
* fix(ui): local wizard RPC timeout; translate no-change completion strings
* fix(deepgram): reject malformed and non-http(s) realtime base URL overrides
* fix(deepgram): accept direct ws(s):// realtime base URL overrides
Preserve released behavior: v2026.6.11 passes wss:// (and ws://) base URL
overrides straight through to the WebSocket URL builder. The prior validator
rejected every non-http(s) scheme, breaking custom Deepgram realtime proxies
that use a direct wss:// endpoint. Accept ws:/wss: alongside http(s):, keep
rejecting malformed and unrelated schemes, and preserve a direct ws(s):
override's protocol/path/port through toDeepgramRealtimeWsUrl.
* fix(deepgram): keep secure ws:// -> wss:// upgrade for realtime base URL
A prior change preserved direct ws:/wss: overrides unchanged, which also
stopped upgrading a plaintext ws:// override to wss://. That regressed the
released behavior where every accepted non-http: scheme maps to wss:, so an
existing ws:// endpoint would begin sending Deepgram realtime audio and
provider auth over an unencrypted socket. Restore the release mapping
(http: -> ws:, all others -> wss:), which still preserves a direct wss://
override and keeps the ws:// -> wss:// secure upgrade.
* fix(deepgram): preserve custom websocket transport
Co-authored-by: dwc1997 <du.wenchi@xydigit.com>
* test(deepgram): use placeholder credential fixture
* test(deepgram): clarify loopback callbacks
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(discord): inherit default_auto_archive_duration in createThreadDiscord
When createThreadForBinding hardcoded autoArchiveMinutes: 60, the
parent channel's default_auto_archive_duration (configured by
operators at 1440, 4320, or 10080) was silently overridden. This is
the same pattern fixed in #103033 for sendMessageDiscord's implicit
forum thread creation.
- Move the auto_archive_duration set after the channel fetch in
createThreadDiscord so channel.default_auto_archive_duration can
serve as a fallback when autoArchiveMinutes is not provided.
- Remove the hardcoded autoArchiveMinutes: 60 from
createThreadForBinding so it inherits the channel default for
forum/media channels and omits the field for text channels
(preserving Discord's server-side default).
- Explicit autoArchiveMinutes from callers (thread-create action,
auto-thread config) still take priority via the ?? operator.
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor(discord): preserve parent thread archive defaults
Co-authored-by: 陈志强0668000989 <chen.zhiqiang1@xydigit.com>
* docs(changelog): credit Discord archive default fix
Co-authored-by: 陈志强0668000989 <chen.zhiqiang1@xydigit.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(codex): read the account email with app-server usage
The Codex-harness usage snapshot now carries the account identity: the
usage fetch runs account/rateLimits/read and account/read over one
app-server session (new withCodexAppServerJsonClient seam, which the
single-request helper now delegates to), so the email is guaranteed to
belong to the same account as the windows. Identity stays best-effort;
rate limits survive an account read failure.
* style(test): scanner-safe app-server marker fixture
* test(codex): keep scanner-neutral hunks around app-server marker literals
* fix(codex): bound the best-effort account read within the usage deadline
Review P2: account/read shared the outer usage deadline, so a slow or hung
identity read could reject the whole snapshot with a timeout instead of
returning the already-fetched rate-limit windows. Race the account read
against a local timer bounded to min(4s, usageTimeoutMs/3) and drop the
identity on timeout; a regression test hangs account/read and asserts the
windows still return.
* fix(codex): send account/read params and bound identity by the remaining deadline
Second review round: account/read (GetAccount) requires a params object per
the app-server protocol, so send an empty {} (refreshToken defaults false);
without it a real app-server rejects the request. Bound the best-effort
identity read by the time left before the shared usage deadline (minus a
margin) instead of the original budget, so a slow rate-limit read can no
longer let the identity read trip the outer timeout and discard the
already-fetched windows.
* fix(codex): reserve the isolated shutdown budget in the usage deadline
Third review round: withCodexAppServerJsonClient awaits the isolated
client's closeAndWait inside the outer timeout, so a hung account/read
could reach the deadline during force-kill/exit cleanup and discard the
already-fetched windows. The usage read now passes a cheap isolatedShutdown
(force-kill 200ms + exit 300ms) and the identity bound reserves that plus a
250ms margin (750ms) of the remaining deadline, so cleanup always finishes
before the outer timeout.
* style(codex): rename scoped-request generic to avoid shadowing
crabbox run rsyncs the invoking repo by default, which blew the setup
budget when the gateway provisioned from a source checkout. Workspace
transfer is owned by the worker tunnel, so setup passes --no-sync.
Live AWS E2E hit the designed fail-stop: worker bootstrap requires Node on
the leased host, and bare AWS leases ship without it. Implement the plan's
provider setup phase as an opt-in `setup` profile key: after the lease is
SSH-ready the provider runs the command via `crabbox run --keep=true`,
gets its own timeout budget on top of provision, and stops the lease
before surfacing a failure so no unbootstrapped box leaks. Setup runs on
every provision attempt (including replay adoption), so commands must be
idempotent.
* fix(workboard): resolve parent dependency status via targeted lookups, not a full-corpus scan
dependencyTargetStatus() called the fully unscoped store.list() (every card,
every board, fully materialized via entries()+readCard()) just to look up the
status of a card's own parent id(s) -- almost always exactly one. dispatch()
calls promoteDependencyReady() once per card in its board-scoped pass, so
every parented card in that pass re-paid for a full-corpus re-scan.
Confirmed via a live CDP CPU profile of a real "openclaw workboard dispatch"
call (--inspect attached to a running gateway, Profiler.start/stop scoped to
the repro, elapsed 161.8s matching a known 160s+ stall): 93.3% of the time
(142s + 8.7s of 162s) was inside native SQLite all()/prepare() bindings, all
funneling through dispatch -> promoteDependencyReady -> dependencyTargetStatus
-> list -> entries -> readCard (readCard issues 4 separate synchronous SQL
statements per card). On a corpus of ~5770 cards, this is O(parented_cards_in_pass
x corpus_size x 4_queries) -- a genuine algorithmic defect, not event-loop
starvation from a concurrent session as originally suspected for this symptom.
Replace the unscoped list()+Map lookup with targeted get(parentId) calls
(already a single indexed-row lookup used everywhere else in this file) --
O(parents.length), normally O(1), instead of O(corpus size). Behavior is
unchanged: parent?.status === "done" on the get() result is exactly what
cards.get(parentId)?.status === "done" computed from the unscoped list().
Adds a regression test asserting the underlying store's entries() is called
at most once while promoting 8 parented cards together in one dispatch pass
(would have been 9 calls before this fix -- 1 outer scoped list() + 1 inner
unscoped list() per parented card).
* fix(workboard): address review feedback on dependency-target-status fastpath
Two findings from the automated PR review, both confirmed real:
- Remove an incomplete, unrelated active-owner fast-path (listActiveOwnerIds()
in store.ts + its cross-board test in store.test.ts) that leaked into this
commit from separate, still-in-progress local work. It imported
WorkboardActiveOwnerQueryable, a type that isn't defined anywhere in this
PR's tree, breaking typecheck and the bundled-extension lint check. Not
part of the dependencyTargetStatus fix this PR is scoped to.
- Replace the comparator-free, mutating .sort() calls in the new dependency
regression test with .toSorted() plus an explicit localeCompare comparator,
per the repo's lint rules.
Re-ran locally against just these two files (the actual PR diff, review WIP
set aside): pnpm lint --threads=8, pnpm format:check, pnpm tsgo:prod, pnpm
check:test-types, pnpm run lint:extensions:bundled all pass; full workboard
extension suite 111/111 (two removed tests belonged to the unrelated
active-owner code, not this fix).
---------
Co-authored-by: ClawBox <clawbox@cayk.ca>