* refactor(ui): move sidebar mutations and groups into controllers
* refactor(ui): lazy-load session organizer operations
* fix(ui): order organizer mutation scope before options
* fix(ui): disable the thread-changes button when the workspace is not a git checkout
* chore(protocol): regenerate Swift gateway models for sessions.files.list gitCheckout
* refactor(ui): sidebar cleanups — shared tooltips, one idle-import helper, cross-tab outbox bridge
Three bounded cleanups following the offline-state work:
- Tooltip unification: every sidebar-family tooltip (session-row badges,
offline status, agent card, attention actions, brand icons) now uses
the shared <openclaw-tooltip> component instead of raw title attrs,
completing the direction #112639 started. Aria labels unchanged.
- One idle-import helper (ui/src/lib/idle-import.ts): the duplicated
idle-load/retry logic from app-sidebar chrome and app-host's outbox
loader collapses into createIdleImport (cached promise clears on
failure, one idle retry while online, online re-arm, dispose). The
helper migration is net negative at its call sites.
- Cross-tab outbox bridge: subscribeStoredChatOutboxChanges now also
notifies on storage events for the composer outbox keys, installed on
first subscribe and removed with the last subscriber, so a message
queued in another tab refreshes badges here.
* fix(ui): keep idle-import scheduling statement-form for narrow-safe types and consistent-return
* fix(ui): give idle-import a strictly void schedule and promise-only load
* feat(cron): system-owned heartbeat monitor jobs replace the interval scheduler
- new internal cron payload kind {kind:"heartbeat"}: execution pokes
requestHeartbeat({source:"interval"}); reported in the protocol job
schema, not accepted from client create/patch
- gateway converges one declaration-keyed monitor job per heartbeat-enabled
agent (schedule every+deterministic phase anchor) at startup and on
config reload; removes monitors for unconfigured agents
- heartbeat runner loses its interval setTimeout machinery; nextDueMs
stays as the cooldown gate, event wakes unchanged
* test(cron): heartbeat monitor regressions; docs for cron-owned cadence
- converge/prune/failure-containment tests for heartbeat monitor jobs
- heartbeat payload run fires an interval wake, no system event
- scheduler tests converted from timer self-fire to wake-queue pokes;
timer-mechanics-only tests deleted with the timer
- persisted-shape accepts the heartbeat payload kind
- docs: heartbeat cadence ownership + system payload kind
* fix(cron): heartbeat monitor review round 1
- targeted cron-monitor interval ticks use the full per-agent path so
due-commitment sessions still deliver
- cron-disabled gateways keep a local fallback interval timer (shipped
cron.enabled=false contract; removed when heartbeat config folds into
cron in #110950)
- heartbeat job reconciliations serialize with latest-wins epochs and a
bounded 30s retry after a failed convergence pass
* fix(cron): chain clamped fallback heartbeat timers past the setTimeout cap
* fix(cron): heartbeat monitor review round 3
- targeted monitor redirect skips wakes carrying heartbeat overrides and
surfaces the per-agent terminal skip reason instead of not-due
- cron-disabled fallback timer re-arms with a 1s floor after each firing
so a dropped wake cannot end the chain
- heartbeat payloads are system-owned at the service boundary: add requires
the gateway opt-in, patches to the kind are rejected
* fix(cron): heartbeat monitor review round 4 — full ownership enforcement
- prune only jobs proven to be monitors (prefix AND heartbeat payload)
- existing monitors reject every update patch; declarative upserts on the
monitor key require the gateway opt-in even with a different payload
* fix(cron): complete heartbeat monitor ownership boundary
- converge scopes declarative matching to real monitors so a colliding
user job with the same key is never adopted or overwritten
- monitor removal requires the gateway systemOwned opt-in; ad-hoc
API/CLI deletion is rejected, reconciliation cleanup still prunes
* docs(cron): record intentional enrollment-snapshot semantics for monitor ticks
* fix(cron): repair heartbeat monitor CI gates
* fix(ui): keep composer sends on one stable bubble through the history handoff and add a subtle entry animation
* fix(ui): keep userTurnSendIdentity module-local
* feat(ui): surface queued-outbox counts in composer hint, session rows, and offline footer
Follow-up to the offline-state unification: queued sends were mechanically
safe (durable outbox with reconnect replay) but only visible inside the
open session's thread.
- Composer offline hint now includes the visible session's queued count.
- Session rows show a clock badge with the per-scope outbox count,
independent of connection state (covers waiting-idle and failed too),
with alias-safe scope resolution so agent-main never double-counts.
- The offline footer button appends the aggregate ("· N queued"); the
connected state stays completely silent.
- One narrow subscription seam (subscribeStoredChatOutboxChanges) added
in composer-persistence; no send/drain logic touched.
* perf(ui): keep the startup bundle under budget with a lean outbox read module
The queued-count feature statically imported composer-persistence from
startup modules, hoisting the chat page's persistence machinery into the
startup chunk and breaking the Control UI startup JS gzip budget
(319.6 KiB > 314.0 KiB limit).
Split ownership instead of gaming the budget: a lean read/subscribe
module (ui/src/lib/chat/outbox-store.ts + codec/draft-state) serves
startup consumers (app-host, sidebar), while writes, migrations, and
drain stay in the lazy chat chunk (composer-outbox-store/composer-storage);
composer-persistence keeps its export surface for chat callers. Startup
is back to 313.8 KiB gzip at 12 requests with no chunking-config changes.
Also fixes an autoreview finding in the new summary: legacy bare-main
outbox rows now resolve through session defaults (online) or the
persisted mainAlias (offline reload) instead of trusting the row's stale
embedded agent id, so badge counts key to the same scope the sidebar
resolves. The shared footer status renderer is deduplicated into
session-row-badges.
* fix(ui): correct type-only import and const tuple in outbox split
* perf(ui): idle-load the outbox summary so startup carries no outbox code
The lean outbox read module still cost ~2.8 KiB of startup gzip against
1.5 KiB of budget headroom. Follow the sidebar chrome pattern
(lobster-pet/facepile): app-host idle-loads outbox-store, subscribes on
arrival, and passes the sidebar a resolver callback instead of letting
startup modules import scope resolution. Badges and counts hydrate
moments after load; before that the summary is empty by design.
Failed chunk loads recover on browser online events and, because chunks
are usually served by the gateway itself, on gateway reconnect — the
exact moment the offline badges become relevant again.
Raise the initial-graph packing ceiling 448->512 KiB: the grown core
graph split at the old boundary into an extra chunk, costing ~1.9 KiB of
startup gzip to compression-context resets (same documented tradeoff as
the earlier 400->448 bump). Startup lands at 313.5 KiB gzip / 9 requests,
matching the origin/main baseline, limit 315.0.
* fix(ui): prevent repeated settings select chevrons
Signed-off-by: sallyom <somalley@redhat.com>
* refactor(ui): fix settings select chevron wipe at the root cause
Replace the restored chevron longhands with background-color in the
select.settings-select rule so the .field select chevron (image, repeat,
position) is never wiped, in dark mode too, and scope the 36px gutter to
.field select.settings-select so native-appearance settings selects keep
their 10px padding.
---------
Signed-off-by: sallyom <somalley@redhat.com>
Co-authored-by: sallyom <somalley@redhat.com>
* feat(ui): unify Control UI offline state into one indicator and a queueing composer hint
The Control UI showed three competing offline surfaces (top connection
banner, agent-card presence dot, sidebar footer label), each deriving its
own view of the same gateway snapshot with inconsistent debounce.
- Centralize the 2s offline debounce as `offlineStable` in the gateway
store; delete the sidebar-local timer.
- Delete the connection banner and the agent-card presence dot; the
gateway link is client state, not agent presence.
- The sidebar footer indicator is now canonical: a single offline button
with reconnect detail, redacted last-error tooltip, and click-to-retry
(absorbs the banner's "Retry now"). Connected state renders nothing.
- Settings sidebar footer follows the same offline-only rule (no green
connected dot).
- The composer shows an offline hint strip ("messages will be queued and
sent when the connection returns") while staying fully usable; the
existing durable outbox already replays queued sends on reconnect.
* fix(ui): keep offline debounce constant module-local for knip production scan
* feat(ui): redesign session observer HUD with labeled status and visibility-gated digests
* chore(protocol): regenerate app protocol bindings for sessions.observer.visibility
* fix(ui): unify tooltip arrows and rework sidebar footer hover cards
* fix(ui): keep rich tooltips open while trigger or card retains hover or focus
* fix(ui): type trigger slot query and drop unused build-card export
* test(ui): type usage hint button query for test-types lane
* feat(sessions): first-class archived-session handling
Archived sessions are now exempt from every automatic maintenance path
(age prune, entry cap, model-run prune, disk budget, cleanup repairs);
only explicit sessions.delete removes them. sessions.list gains an
additive tri-state archived filter (true | false | "all"). The sidebar
gets a persisted Active/Archived/All status filter with inline dimmed
archived rows, replacing the nav-away View-archived button; the Sessions
page toggle becomes the same tri-state with dimmed+badged rows in All
mode; the chat composer's archived notice becomes a full-width banner
with an Unarchive action.
* chore(protocol): regenerate Swift gateway models for tri-state archived filter
Zen (2%) breathes through a 6s cycle with one deliberate late snip,
drummer (1%) rocks a two-beat rhythm with jaw hits on the tilts, and
peekaboo (1%) ducks down small and pops back wide open. All three stay
in place (scale/rotate/vertical only); weights rebalance from the
default and southpaw stances.
The initials fallback span inside openclaw-viewer-avatar was hidden with a
single-class selector (.viewer-avatar__fallback, 0-1-0) that the later
.viewer-avatar > span display rule (0-1-1) outranks, so image avatars rendered
the photo and the colored initials side by side in the who's-online roster,
footer facepile, and session facepiles. Bump the hiding selector to
.viewer-avatar > .viewer-avatar__fallback so it wins again; the
.is-fallback swap (0-3-0) still shows initials when the image fails to load.
Regressed in #111421.
* feat(ui): show live run output tokens
* fix(ui): map live usage to active runs
* test(ui): split live usage coverage
* fix(agents): scope live usage cleanup
* fix(agents): reject stale live usage events
* fix(agents): keep live usage type local
* refactor(ui): keep chat view within line budget
* fix(agents): require lifecycle for live usage
* refactor(ui): dedupe chat reply target type