* feat(gateway): expose command lane diagnostics
* feat(ui): add live debug busyness overlay
* fix(ui): show newest events in debug overlay and update diagnostics call-list tests
* test(ui): add lane and status fixtures to the mocked dashboard
* feat(ui): add System busyness entry to the account menu
* fix(gateway/ui): bound lane diagnostics, append-only descriptor, fail-visible lanes load
Addresses ClawSweeper review findings on #125591: diagnostics.lanes moves to
the append-only tail of the descriptor table, the Control UI lanes request
fails visibly instead of masking errors, and the RPC exports only static
lane snapshots plus a bounded dynamic-session aggregate composed in the new
command-lane-diagnostics module.
* chore(protocol): regenerate Kotlin gateway methods for diagnostics.lanes
* test(gateway/ui): register diagnostics.lanes in the 2026.8 train and mock it in the debug e2e
* feat(ui): add sidebar Online presence section and Activity session feed
Adds a sidebar "Online" section (visible only when at least one other
identity is connected, idle users dimmed) that links into a new
session-feed default view on the existing Activity page: search, time
window, and per-person facets over day-grouped sessions, with a
per-person identity header (devices, viewing-now) when one person is
selected. Removes the dead footer viewer-facepile roster that the
sidebar-zone refactor (#112936) left unmounted.
Client-only; no gateway/protocol changes.
* test(ui): make activity capture date-boundary safe
* fix(ui): stop the model picker from nagging about refresh failures
A failed background catalog refresh rendered a "Couldn't refresh models"
banner plus a Retry button on top of a complete, working model list, and
replaced the composer trigger's model name with that error text. The
picker already keeps the last-known catalog and re-requests it on every
open, so the operator was being asked to press a button for something the
UI does by itself.
The error stays recorded on the host — it is what drops the stale
availability gate so the composer remains usable — but it is no longer
surfaced while there are models to show. Only a genuinely empty catalog
still says "Models unavailable". The new-session picker gains the chat
picker's open-triggers-revalidate behavior so re-opening it is the retry
there too, instead of dead-ending until a page reload.
Two adjacent simplifications in the same surface:
- Model rows reserve their provider-icon slot as an invisible stem, so
names line up with the provider heading label (same 34px stem grouped
and filtered, nothing shifts while typing).
- The "Using agent default" footer is gone: the default row already
carries a DEFAULT badge and the checkmark. Typing "default" in the
picker search now matches the default model instead.
Proof: scripts/capture-model-picker-proof.mts captures the open picker
against a mocked gateway; alignment delta 0px (was -24px), search
"default" matches the default row (was nothing), and a failed models.list
leaves no catalog-state element with all rows intact.
* fix(ci): register the model-picker proof script and await its picker revalidation
The unused-file scan needs every scripts/ entry point referenced, like the
sibling ui:proof:* recipes. The catalog-reconnect assertion also has to wait
for the picker's own metadata request instead of reading the log the moment
the rows render.
* test(ui): split model-catalog scenarios out of the new-session e2e file
The catalog-reconnect file hit the 1000-line cap. Its model-catalog metadata
failure/recovery pair is a separate surface from CLI-agent targets, terminal
start, and draft reconnect, so it moves to its own file rather than earning a
max-lines suppression.
* test(audit): give the pinned-reader contract test a realistic timeout
It adds a pinned-SHA git worktree and cold-compiles the audit and state
modules under tsx, which takes minutes on a contended runner. The 120s
default made it fail by construction; it timed out on an unrelated PR shard
while passing locally at ~55s.
* fix(gateway): stop chat.startup from blocking on workspace icon discovery
chat.startup awaited workspace icon preparation before responding, so a
cold icon cache (up to the full favicon candidate list) delayed the first
chat-open response for every session. It now starts discovery eagerly and
returns without waiting for it.
That leaves the icon route's readiness 503. The route already awaits an
in-flight preparation, so the only way to reach 503 is having no cache
entry at all: an icon GET that beat its chat.startup across transports, or
a session whose snapshot aged out of the bounded cache. Neither is fixed by
the client asking again for the same missing snapshot, so the producer
answers instead. prepareSessionWorkspaceIcon() publishes cache entry and
waiting requests through one publication point, and a request that misses
waits up to two seconds for that publish before falling back to 503. The
race resolves into a 200 on the original request, and 503 now means only
what it says: nothing is preparing this session.
* fix(gateway): keep icon absence out of 503 and bound the publish wait
The publish waiter resolved with the snapshot promise itself, so `resolve`
adopted it and a workspace that publishes `null` reached the route as a falsy
value indistinguishable from an expired wait: the common no-icon race answered
503 instead of the stable 404. Waiters now carry a discriminated publication,
so timeout and published absence stay separate outcomes.
Admission is also bounded. Every authenticated unknown key used to create a
bucket, callback, timer, and held response for the full wait with no cap and no
disconnect cleanup, which accumulates once sidebar project groups render icons
for sessions that may never get a chat.startup. Waits are now capped per
session and per process, a saturated pool answers 503 immediately, and every
exit path releases the timer, the disconnect listener, and the slot together.
* fix(gateway): bound cached workspace icon waits
* fix(gateway): recover delayed workspace icons
* test(ui): assert workspace icon retry fallback
* fix(gateway): bound workspace icon recovery
* fix(gateway): satisfy workspace icon lint
* fix(ui): scope workspace icon recovery
---------
Co-authored-by: ClawSweeper <steipete+clawsweeper@gmail.com>
Co-authored-by: RoboClaw <309084314+roboclaw-bot@users.noreply.github.com>
* improve(ui): flatten transcript tool activity
* improve(ui): complete tool activity redesign
* improve(ui): normalize activity row typography
* improve(ui): unify activity typography contract
* improve(ui): standardize all activity row text
* improve(ui): increase activity text size
* improve(ui): underline activity filenames
* improve(ui): tighten activity row spacing
* improve(ui): standardize activity chevrons
* improve(ui): reveal child chevrons on interaction
* improve(ui): retain active child chevrons
* improve(ui): space expanded child tools
* improve(ui): remove idle activity fade and gap
* fix(ui): preserve tool activity contracts
* fix(ui): simplify activity basename lookup
* fix(ui): preserve canonical tool row actions
* fix(ui): record tool outcomes and repair transcript row regressions
Neutral collapsed rows are the design, but this branch left failures with
nowhere to land and shipped chrome whose styles it had already deleted.
- Expanded standalone tool messages close with the outcome line, so an inline
rows shape still reports the failure instead of dropping it.
- Progress-card receipts drop the badge and the spinner: this branch removed
both styles, leaving a 0x0 invisible running indicator, and the receipt
label already names the state.
- Single delete patches are non-navigable; their workspace target is gone, so
the file action could only ever fail to load.
- Flush command padding derives from the action-button size instead of a stale
42px constant, and the running text wave covers command rows again.
- Activity lane returns to the tool-shell column width the rule claims.
- Drop CSS nothing references: tool-card tabs, empty, plain-output, activity
scroll masks, secondary command text.
- renderToolDataBlock takes an optional label instead of comparing copy.
- Split the oversized tool-card test file and refresh stale row assertions.
* fix(ui): fold the tool action size into the card header rule
Stylelint no-duplicate-selectors rejects the separate .chat-tool-card__header
block; the custom property belongs in the existing rule.
---------
Co-authored-by: RoboClaw <309084314+roboclaw-bot@users.noreply.github.com>
* fix(ui): coalesce device refreshes during active loads
Nodes and device list loaders dropped every refresh request received while a
previous list RPC was in flight. A successful approve, reject, removal, token
mutation, or Gateway resolved event could therefore be overwritten by the old
pre-mutation response and remain stale until the next poll.
Record one closed pending refresh mode per list owner. When the current request
settles, immediately run the coalesced refresh, preserving whether any queued
caller requested visible error handling. Add order-faithful node and device
regressions that fail when the follow-up list request is dropped.
* test(ui): initialize queued device refresh state
* fix(ui): stop composer mic hover shift and stray red hover
The device-picker chevron collapsed to width 0 while idle and grew to
20px on hover, shoving the right-aligned mic button 20px left under
the cursor on every mouse-over. Detach the picker into its own
fixed-width ghost button beside the mic instead of morphing the mic's
shape into a split pill; hover only fades the picker's opacity, so the
reserved layout never shifts.
Also fixes the mic showing the send button's loud accent-red hover in
dark/openknot themes: those themes overrode .chat-send-btn:hover
directly, which outranked every variant's own hover rule (including
the intentional neutral mic hover and the stop button's danger hover).
Theme overrides now set a --chat-send-hover-background variable that
each variant's hover rule can still take precedence over.
* chore(scripts): register composer mic hover proof script
Missing package.json entry made the deadcode full-tree scan flag the
new capture script as an unused file, same as the existing
ui:proof:workboard sibling.
* fix(ui): show session hovercards for every sidebar row beside the rail
Sidebar session rows were excluded from session-link hovercards, so only
the Home (main) row showed a card, and it overlaid the sidebar itself.
Hovercards now open for all sidebar session rows and position to the
right of the sidebar so navigation stays visible.
* fix(ui): yield sidebar previews to active progress hovercards
Sidebar rows already open a progress hovercard when the session has an
active progress card; the new preview card stacked on top of it. The
preview now probes the shared progress-card store (deduped with the
progress provider's own load) and opens only when the row has none.
The /new page renders its own composer, which never wired the `$skill` mention menu owned by the chat composer, so typing `$` there did nothing while /chat offered completions. Narrow the skill-menu module to a small host contract shared by both composers, drive it from the new-session textarea, and refresh commands.list on the first `$`. Also moves `paneDomId` to the composer DOM helpers to break a types→skill-menu→slash-menu→state import cycle.
* feat(ui): show worktrees only for Git group folders
* feat(ui): enrich group environment picker
* fix(ui): preserve worktree defaults on probe errors
* fix(ui): reject stale group repository probes
* test(ui): type repository inspection helper
Restore the block-axis padding reset lost in #125368 so Web Awesome tab labels remain vertically legible while retaining logical inline spacing. Add LTR and RTL browser geometry coverage for the visible label boundary.
* feat(control-ui): move the Ask OpenClaw toggle to the sidebar footer
The sidebar bottom now mirrors its top: big agent selector with inline
chevron + small new-session square above, big account identity card with
inline chevron + small lobster toggle below. The account chevron moves
from the row's far right to directly after the name, matching the top
header treatment; the freed edge hosts the toggle. The old top-left
shell-chrome placement is removed along with its four-button width
reservation (which read-scoped clients paid for without seeing the
button). Scope gating, palette entry, and panel wiring are unchanged;
gating regressions moved to the sidebar cases.
* fix(control-ui): keep Ask OpenClaw reachable while the nav is collapsed
Address the ClawSweeper P1: desktop collapse hides the sidebar entirely
(display:none, no rail), which would have removed the toggle's only
persistent surface. The lobster now joins the chrome strip exactly while
the nav is collapsed — the footer stays its home — and the strip's
collapsed width reserves four controls for that state. Regression covers
collapsed-present/expanded-absent/read-scope-absent.
The panel from #125199 rendered raw wire enums in code tags, used
form-grid/field markup whose styles the agents page never loads (bare
unstyled inputs outside the card), referenced non-existent avatar
classes, and put callouts inside the settings group. Rebuild it on the
canonical settings primitives: status rows with dot status and friendly
source/evidence labels, a System/This Agent segmented scope control,
the settings secret input (gains a disabled prop), a danger status row
for errors, and a quiet-inherit + primary-save action row. Retitle to
"GitHub Identity" and move it below Tool Access and Available Right
Now. Add a .settings-account primitive for the 20px round avatar.
Live-tested on an isolated dev gateway: native-credential verify via
the GitHub API, segmented scope switching, and the empty-token error
row. Controller behavior unchanged.
* feat(ui): preserve composer drafts across restarts
Persist draft text and attachment Blobs in browser-local IndexedDB, scoped to Gateway credentials and session or New Session targets. Clean up durable data after successful send or creation, attachment removal, and session deletion.
* fix(ui): preserve text when draft attachments exceed cap
preserve text-only durable fallback for oversized attachments; serialize visible Incognito transition with its storage fence; document retention and disposal.
* test(ui): make draft retirement proof isolate-safe
The shared isolate:false UI suite exposed an order-dependent module mock; exercise and settle the real IndexedDB boundary instead.
* style(ui): format session mutation imports
* fix(ui): keep incognito drafts memory-only
* perf(ui): lazy-load durable draft storage
* fix(ui): sequence durable draft transitions
* fix(ui): restore text-only new session drafts
Programmatic draft restoration must not advance the user-mutation generation; real user input continues through setMessage.
Move navigation transition ownership into the existing handoff module as a behavior-neutral extraction that keeps the submission owner within max-lines.
* fix(ui): restore text-only drafts without import cycles
Complete the text-only restore fix by moving started-session route transitions into a leaf module. This keeps max-lines ownership clean without reintroducing the Madge cycle.
* fix(ui): reconcile attachment handoffs with durable drafts
Restore programmatic attachment handoffs without recording a user mutation. Cover stale navigation state losing to newer durable drafts across two pages and a fresh page.
* fix(ui): start durable attachment writes before teardown
Start each CAS IndexedDB write and retirement immediately so text and attachment transactions register before page teardown. IndexedDB readwrite ordering and draft revisions keep snapshots serialized.
* fix(ui): persist New Session drafts before teardown
Accept the committed predecessor or a known in-flight local write ID atomically so New Session writes can start before teardown without spurious local-lineage conflicts.
Reset cached lineage when authoritative storage is missing and re-snapshot the still-current edit.
* feat(dashboard): add session:progress board tile rendering the live progress card
Advertise the core-owned widget kind via hello controlUiWidgetKinds at operator.read.
Render it inline without an iframe from the session-progress-cards store.
Pin it with dashboard tool widget_put using pluginKind session:progress and optional props.sessionKey.
Follow up the progress-card unification from #125125.
* fix(dashboard): surface session progress load failures
Record protected progress-card read failures in the shared per-session store.
Render an actionable board-tile error with retry instead of indefinite loading.
Cover the rejected-read and successful-retry flow at the widget boundary.
* fix(dashboard): honor progress tile access and activity
Avoid progress-card reads while a retained board is inactive.
Distinguish sharing denial from transient load failures and show the correct remedy.
Qualify cross-session pinning docs and cover activation plus denial behavior.