* 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.
waitForRequest(method) is satisfied by ANY prior request of that method
and returns the latest match, so a second same-method wait in one page
context can return a stale earlier request on slow runners. Add the
opt-in { after: priorCount } cursor (same shape as PR #125107's helper
change) and pin the 11 genuinely stale-prone waits: chat-composer-
capability-menu, claude-sessions (3), desktop-panel, model-providers (3,
replacing expect.poll count workarounds), session-management.group-
defaults (3). Skipped: cloud-workers-settings (owned by PR #125107),
chat-attachment-read-lifecycle (ring asserted empty), login-gate (waits
follow app-initiated stale-build reloads, which restart the ring).
* feat(control-ui): persistent Ask OpenClaw companion with global toggle
The custodian surface now behaves like the persistent machine-wide agent it
already is on the Gateway: the session id persists in localStorage so a
reopened surface rebinds to the live engine (wizard and approval state
survive close/reopen), the durable transcript is refetched when a surface
opens or the gateway reconnects (idle-gated so active question/wizard cards
are never clobbered), and the panel toggles from anywhere via the shared
panel-toggle contract, a command-palette action, and an admin-gated lobster
chrome button.
One server-side line: the openclaw.chat owner-mismatch rejection now carries
the existing structured session-invalidated details so persisted clients
re-mint their id from a closed code instead of matching error prose.
No gateway events, no protocol schema changes, no polling. Splits
(session-identity/variant modules, session-lifecycle and panel-toggle test
files) keep the touched files under the max-lines ratchet.
* fix(control-ui): coerce custodian toggle detail without a type assertion
The assertion-safety ratchet holds custodian-panel.ts at zero uncommented
assertions; parse the toggle CustomEvent detail through the canonical
record-coerce guard and literal narrowing instead of casting.
* fix(control-ui): delete unused CustodianPanelToggleDetail export
The record-coerce toggle parsing left the exported type without a
production consumer; the deadcode gate rightly flags it. The palette test
keeps a local shape.
* test(control-ui): select the palette custodian item via keyboard
Async session-search results can reflow the palette list mid-click on slow
CI runners, silently dropping the positional click; keyboard selection of
the asserted-active item is atomic against reflow. Also stage the reopen
wait (panel section, then text) for sharper failure localization.
* fix(control-ui): project live wizard state on rejoin and scope-gate the toggles
Address both ClawSweeper P1 findings. The welcome-only rejoin of an
existing session now routes through engine.decorateRejoinReply (the
existing ChatWizardHost projection), so a reconnecting client re-renders
the live wizard/question controls the session still awaits; the stale
welcome question only fills in when no interaction is live. The chrome
button, palette action, and deferred panel loading now use the
scope-aware canCallGatewayMethod gate (operator.admin) that the session
store already used, so advertised-but-read-scoped clients see nothing.
* test(control-ui): fix the cloud-workers e2e flake at both roots
The mocked config.get stayed frozen at the empty initial config while
patch responses advanced, so a config-store reconciliation refetch could
flap the snapshot to empty and saveProfile silently dropped the next
save; the mock now stays consistent before each patch resolution. Also
give waitForRequest an opt-in after-cursor: it is satisfied by any prior
same-method request and returns the latest match, so a second wait could
assert against the stale earlier request; the cloud-workers waits pin it
(15x green locally, previously failing 1-in-3).
* fix(ci): cover rejoin projection in sibling engine mocks; bump startup baseline
The greeting-welcome and session-ownership suites' engine mocks now
export decorateRejoinReply like the handler requires. The Control UI
startup-JS baseline moves 337511 -> 338920 B via the documented update
command: the shell chrome toggle, palette action, and scope-aware gating
are genuine startup surface (~1.4 KiB gzip, within the committed
ceiling).
* fix(control-ui): settle interrupted structured replies and racing turns on rejoin
Address both ClawSweeper reconnect P1s. A submitted question/wizard reply
with an unknown outcome now triggers a full session rejoin on reconnect
instead of being blocked by its own uncertainty flag: the Gateway projects
whether the answer was consumed and which control is live. A restored
persisted id also arms a one-shot rejoin barrier: the welcome-only request
queues behind any in-flight turn on the Gateway's per-session queue, so a
post-response history refresh deterministically surfaces rows a racing
turn persisted after the initial fetch. The open-agent handoff moved to
custodian-navigation (its owner) to keep the store under the size cap.
Live-Gateway proof (isolated state dir, real gpt-5.6-luna turns): video
and screenshots on the PR.
* test(control-ui): reopen via the chrome toggle in the custodian e2e
The palette click-through composition proved timing-flaky on loaded CI
runners in three different ways while adding no coverage: the palette
action's dispatch is pinned by the palette unit test and the event-opens-
panel path by the chrome-toggle step. Keep the gated palette entry
assertion + screenshot; reopen through the chrome path.
* fix(control-ui): keep the agent-handoff path helper module-local
The store now routes through performCustodianAgentHandoff, leaving the
path builder without external callers; the deadcode gate rightly flags
the export.
* fix(control-ui): run the rejoin barrier even when a live control projects
The racing-history refresh happens before the reply/control message is
appended, so skipping it for projected wizard/question rejoins had no
purpose and lost rows a turn persisted while the page was closed mid-
wizard. Regression covers the live-step rejoin reconciling racing rows.
* feat(gateway): carry machine class through session moves
Profile move targets accept an optional machineClass with new-session
parity: validated in the protocol, persisted on the durable move intent
(bare nullable column, lazy same-version ALTER), replayed exactly by
restart recovery, and selectable from the Control UI move dialog.
Moving to the current profile with a different class resizes the
session's worker.
* fix(gateway): project placement facts on chat startup
chat.startup and chat.history built sessionInfo without the placement
projection sessions.list applies, so clients merging that row erased a
live worker placement and its move intent — the Runs on Cloud chip
disappeared after any turn until a full reload.
Reuse the canonical ensureColumn helper for the move table's additive
column instead of a hand-rolled PRAGMA read.
Keep embedded mobile Side chat inside its tabbed panel so the main composer and shared controls stay tappable.
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* feat(agents): unify agent status into a durable progress_card
Replace the write-only update_plan to-do tool and the fragmented plan
rendering with one durable status artifact per session: progress_card
({plan?, markdown?}, replace-on-write, 8 KiB markdown / 50-step caps).
Cards persist in a lazy-additive session_progress_cards table in the
per-agent DB (no schema-version bump), broadcast progressCard.changed,
and render from the store with exactly one live placement per view
(session rail when visible, else the composer-adjacent bar); transcripts
collapse to one-line receipts, and the sidebar hovercard shows other
sessions' cards inline (markdown + <progress>, DOMPurify allowlist, no
iframes). The three stream-derived plan renderers and their dedup
heuristics are deleted.
Codex runs disable the native plan tool per thread
(tools.update_plan.enabled=false) and receive progress_card via the
dynamic-tool bridge; compaction restore now reinjects the card (steps +
bounded markdown). Card writes still emit the legacy plan stream event so
native apps and channels keep working until their per-platform
migrations. Policy names map update_plan -> progress_card; the shipped
tools.updatePlan=false kill switch is honored.
Net -277 production LOC; -480 test LOC.
* test(agents): regenerate Codex prompt snapshots for update_plan thread-config disable
* chore(protocol): allowlist progressCard.changed for native apps pending card migration
* fix(ci): repair progress card integration checks
* fix(codex): canonicalize native progress cards
* test(gateway): reconcile progress card method order
* test(codex): stabilize native approval fixture
* fix(ui): preserve steering stream order
Record exact steering provenance and keep cumulative assistant output on its causal side of persisted user turns across live streaming, terminal events, tool boundaries, and history reloads.
* chore(ci): refresh post-rebase gates
Tighten the inherited environment-variable budget and remove a stale test import exposed by the rebased lint gate.
* fix(ui): preserve terminal steer stream segment
Assert the causal stream rollover when a steer lands and move persisted split-layout normalization off the startup path to keep the Control UI bundle within budget.
* refactor(ui): extract split layout types
Keep the persisted split-layout normalizer off the interactive module cycle while preserving the Control UI startup bundle reduction.
* fix(gateway): resolve steering provenance at injection
* fix(gateway): confirm steering provenance after persistence
* refactor(ui): move live tool filtering to identity owner
* fix(ui): preserve queued user stream ceiling
* feat(sessions): teach session tools the Control UI link rule
Gate guidance on publicOrigin plus enabled Control UI, with exact literal-URL fallback after short-link misses.
* feat(ui): linkify session keys in chat markdown
Match agent session keys structurally in plain text and inline code, then delegate canonical chat navigation.
Allowlist data-session-key through markdown sanitization.
Pathfinder: keep internal Control UI route anchors in-app by removing target="_blank" and external-link rel attributes while preserving external link behavior.
* feat(ui): session hovercard + titled session chips backed by controlUi.sessionPreview
* fix(gateway): scope controlUi.sessionPreview to caller-visible sessions
Hover previews now apply the same createSessionListEntryFilter predicate as
sessions.list, so identity-bearing non-admin callers cannot preview-by-key
incognito rows or non-owner drafts the sidebar hides. Regression test proves
the viewer/admin split; pre-fix run leaked ok-status metadata.
* feat(sessions): carry the session-link rule in tool result envelopes
Deferred-description mode hides prose tool descriptions at decision time. Carry the shared Control UI session-link sentence in successful session lookup result envelopes so every tool mode sees the rule.
* fix(ui): upgrade session chips on appearance, not first pointer event
* fix(android): regenerate gateway protocol methods
* test(gateway): track session preview release train
* perf(ui): lazy-load session hovercard registration
* fix(ui): keep session hovercards off sidebar navigation
* fix(ui): cancel routed session-link navigation
* fix(sessions): advertise forced-literal ~key URLs so short-ID collisions cannot misroute
* test(sessions): update forced-literal guidance expectation
* fix(ui): collision-proof raw-key navigation and SPA-route internal session URLs
* perf(ui): preserve session route lazy boundary
* fix(ui): defer unseeded session-preview fetches to hover intent
* fix(sessions): hard-cap the model-visible session-link base
* feat(sessions): stamp agent identity on spawned sessions and return spawn receipts
Agent-spawned sessions recorded the requesting session key as createdActor.id,
so the Control UI creator chip rendered an opaque key. Spawn producers now stamp
the canonical requester agent id; parent-authority validation moves to a new
trusted requesterSessionKey field. projectSessionActor enriches agent actors
with configured identity name/avatar at read time, and visible sessions_spawn
returns a sessionUrl + owner receipt with URL-first acknowledgement guidance.
* feat(sessions): assignable session ownership with owner facet and menus
GitHub-assignee-style ownership: sessions get a mutable owner (defaulting to
the immutable createdActor) stored in additive bare-nullable SQLite columns
with first-use lazy ensure. New operator.write sessions.assignOwner validates
targets, requires an identified caller, authorizes by session visibility, and
records assignedBy/assignedAt inside the write transaction. The sessions agent
tool gains assign_owner; the Control UI adds Assign-to-me/Assign-to menus in
sidebar rows and chat headers, renders the effective owner chip, and the
creator facet/filter now keys on effective owner. Sharing authority stays
anchored on createdActor.
* feat(sessions): record session participants and stack them in the owner chip
Records every distinct external prompter (human profile/channel sender, or a
requesting agent) per session in an additive session_participants table at the
turn-admission boundary — best-effort, deferred, never blocking the turn; the
session's own agent and viewers are never recorded, capped at 32 per session.
The session row projects a bounded participants list (owner excluded) plus a
total count with the same actor enrichment as owner/createdActor. The sidebar
chip becomes a pair-stack when others have prompted (owner front, one peeking
participant or +N behind), the chat header shows the full facepile, and an
authenticated involvingMe list filter adds an Involving-me sidebar predicate.
Participant projection is excluded from logical-session CAS equality so display
history never invalidates session writes.
* fix(sessions): identify built-in agent tool callers for owner assignment
The sessions tool's assign_owner dispatched through the in-process synthetic
client, which carries neither a signed agent-runtime identity nor a human
profile, so agent-initiated reassignment always failed with FORBIDDEN. The
tool now captures its trusted requester agent identity and carries it across
in-process dispatch as internal client state (never wire params); the handler
derives assignedBy as signed runtime identity, then trusted agent-tool caller,
then authenticated human. Live-verified end-to-end on a dev gateway.
* fix(ci): split oversized session modules and refresh prompt snapshots
Split the max-lines offenders at concept boundaries for session equality, tool overrides, and protocol owner schemas. Remove the redundant Number conversion from the node:sqlite participant count. Refresh prompt snapshots after drift from the sessions and sessions_spawn tool description updates.
* fix(ci): restore solo-mode chip suppression and conform new method descriptors
Solo-mode root cause: owner-assignment submenu options reused the permanent owner-chip custom element, so hidden menu avatars were counted as attribution chrome. Menus now use viewer avatars while gateway-gated owner chips remain exclusive to collaborative sessions.
Conform sessions.assignOwner to the 2026.8 descriptor and append-only advertised-method inventories, and regenerate the Swift and Kotlin protocol surfaces.
Keep historical v15/v14 fixtures frozen by stripping the new owner columns; the existing range already excludes the participant table. Replace the new raw SQLite schema probes with synchronous Kysely queries.
Clear max-lines by splitting the organizer host contract, pure agent-navigation projections, and ownership/filtering sidebar cases at their concept boundaries.
* fix(ci): integrate ownership series with latest main surfaces
Wire the sessions-page assign-owner action, merge capability imports, narrow the navigation export scope, and apply sessions-create formatting.
The owner-presence regression came from hidden assign-owner menu avatars emitting data-viewer-id, so owner and menu chrome now opt out of presence markers while real facepiles retain them.
* fix(sessions): scope the involving-me filter to profile-backed participants
Session participant history mixed channel-native sender ids with authenticated Gateway profile ids, so involving-me missed real sessions and could accept numeric collisions.
Record the actor_source namespace at each producer, carry it through the internal SQLite projection, and match authenticated viewers only against profile-backed human participants. Legacy NULL sources fail closed for filtering, while channel ids remain available for display.
* build(ui): raise startup budget baseline for session ownership surfaces
Ownership chips, assignment menus, and the participant stack add ~0.7 KiB
gzip to the startup path; CI compression landed just over the previous
baseline+tolerance. Hard cap (350 KiB) unchanged.
* refactor(sessions): drop raw NULL projection for the lazy actor_source column
The Kysely guardrail rejects typed raw sql snippets outside allowlisted
boundaries; select the lazily-ensured column only when present and let the
row projection treat its absence as unknown/legacy.
* build(ui): refresh combined startup baseline
* fix(sessions): flatten Markdown in session list previews
Session-list previews were extracted verbatim from the last transcript
message, so raw Markdown leaked into every surface that renders the
subtitle as plain text — Control UI sidebar, TUI picker, native session
lists, and the sessions_list tool. A finished session read as
"Landed [PR #124879](https://github.com/...)".
Flatten lastMessagePreview at the Gateway producer, inside the
watermark-validated title-field cache, so the cost is amortized and no
consumer re-implements stripping. The flattener is the regex chain that
already existed privately in the Control UI narration line; it moves to
@openclaw/normalization-core/markdown-plain-text and both surfaces now
share one implementation. Session titles keep their own normalization
and are unchanged.
Also let an unread final observer digest outrank the raw last reply in
the sidebar subtitle, so the utility model's headline wins the slot it
was written for. The finalDigestUnread gate is untouched, so an
already-read digest still falls back to the flattened preview.
* fix(ci): register Markdown preview module
* fix(sessions): preserve literal preview punctuation
* fix(sessions): flatten previews before truncation
* fix(test): adapt session projection callback
Every surface that survives only until the messages.visibleReplies default
flips to message_tool now carries a greppable TRANSITIONAL(marker-retirement)
comment naming that trigger: the reply/audio parser family, streaming
tail-buffering, the write-boundary applier, automatic-mode prompt teaching,
the [[tts]] DSL + streaming cleaner, and the live narration strip. Comment-only.
* refactor(gateway,ui): one bounded display projection; delete marker strip sites
Persisted transcripts are marker-free since the write-boundary projection
(#124793), the historical migration (#124888), and TTS facts (#124913), so
display surfaces stop compensating. sessions.list.lastMessagePreview and its
siblings (sessions.preview/describe, TUI picker, sessions_list tool, MCP) now
share one bounded role-aware projection (240 chars, tool/system/thinking and
suppressed control replies excluded, directive-only rows fall through). The
web reply chip reads the typed openclawDelivery fact instead of parsing text;
chat.history preserves the field to the UI. Post-hoc display strips are
deleted across web/TUI/MCP/sessions-list; live streaming cleaners stay.
Stale gateway-protocol preview comments corrected; no schema change.
Assertion-safety baseline pruned for shrunk files (sanctioned direction).
Production net -173, tests net -137. Fixes the sidebar [[reply_to_current]]
preview leak and the empty-code-pill overstrip of quoted markers.
* fix(agents): preserve restart recovery transcript reads
* refactor(gateway): remove obsolete transcript exports
* fix(gateway): normalize injected delivery directives
* fix(ci): scope projection and recovery checks
* chore(ci): shrink plugin SDK surface budgets
* test: deflake loaded side question and worker checks
* test: align display projection CI fixtures
* style: format display projection fixture
* feat(workboard): full-board dashboard widget
Adds a third trusted plugin widget kind, workboard:board, rendering the
complete Kanban board (all columns, live plugin.workboard.changed
refresh) inside a session dashboard. Props {boardId} scope it to one
board; without it the widget shows every board, matching workboard:mini
semantics. Card rendering reuses the workboard page renderColumn with an
explicit surface option so page and widget cannot drift; the shared
board filter moved to ui/src/lib/workboard/board-filter.ts. Mutations
follow the connection's canMutate; read-only connections get a
view-only board.
Proof: pnpm test ui/src/lib/board ui/src/pages/workboard
ui/src/lib/workboard extensions/workboard (9,494 passed), i18n baseline
green, autoreview clean (codex/gpt-5.6-sol). tsgo core/ui/extensions
green locally; remote check:changed blocked by Daytona capacity and the
tsgo:core Daytona exit reproduced locally as a heavy-check lock race,
passing on retry.
* test(ui): seed workboard fixtures in the mocked dev server
* fix(ci): register workboard proof capture script
* refactor(webui): drop gateway-version fallbacks per version-lock decision
* refactor(webui): drop chat.startup history fallbacks
* test(webui): advertise full method catalog in terminal e2e scenarios
* refactor(webui): enforce the build-identity handshake at connect
* docs(webui): state exempted skew paths fail visibly by design
* feat(workboard): reference the automation job that owns a board's prompt
Boards can now record automationJobId, linking a board to the cron
automation whose prompt/model/schedule categorize its cards. The cron
job remains the single owner of that configuration; workboard stores
only the reference. The board toolbar shows an Automation chip linking
to the cron page when set. Deleting a board never deletes the job.
Proof: pnpm test extensions/workboard packages/workboard-contract
ui/src/pages/workboard ui/src/lib/workboard (9,492 passed), autoreview
clean (codex/gpt-5.6-sol). check:changed blocked by remote capacity
(Blacksmith down, Daytona lease cap); local fallback lanes green except
a parent-branch lint finding fixed on that branch.
* fix(ui): describe workboard automation destination