Commit Graph

3869 Commits

Author SHA1 Message Date
Peter Steinberger
da4ae86232 fix(ui): claim new-session handoffs on the direct chat route (#114377)
* test(ui): align new-session e2e with path routes and startup transcript

* fix(ui): claim new-session handoffs on the direct chat route

Deferred session navigation can route a pane straight onto the created
session key, which never runs the switch path that admits the one-shot
handoffs. The rejected turn then expired unclaimed instead of offering a
retry, and an accepted prompt stayed hidden until the transcript bootstrap
resolved. Mirror chat-state-route's admission on the direct branch.

Fixes #114374

* fix(ui): keep the projected first prompt above the replies it started

A pane that admits the accepted prompt before its transcript bootstrap
resolves leaves that row in chatMessages while the history request is in
flight. The merge then re-placed it as a late optimistic tail, and since
#113266 stable rows keep insertion order, the first turn rendered below the
tool activity it kicked off. The handoff reconciler owns that head row, so
exclude it from the late tail.
2026-07-27 04:34:17 -04:00
Peter Steinberger
5a8db91459 fix(macos): keep polling gateway-executed wizard steps (#114383)
* fix(macos): keep polling gateway-executed wizard steps

* docs(ui): note the prebuilt control-UI bundle in live verification

* test(macos): fail fast when gateway progress polling stalls

* chore(i18n): refresh native source inventory anchors
2026-07-27 04:33:09 -04:00
Peter Steinberger
af552d4bc6 feat(ui): resolve session URLs through the gateway with best-effort slug matching (#114422)
* feat(ui): resolve session URLs through the gateway with best-effort slug matching

Session URLs now resolve a stored board face and a display-name slug through
the gateway instead of guessing from the browser's cached session page.

- An uncached open marks only the in-app navigation, never the shareable href,
  so the chat loader can re-derive the face and replace the URL.
- A cached row settles the lookup locally, keeping navigation off the wire.
- A one-segment reference resolves short id, then literal key, then display-name
  slug; ties reuse the existing disambiguation view.

* refactor(ui): keep slug ties on short disambiguation prefixes

Slug candidates reused the full uuid, which produced 32-character links in the
disambiguation view. uniqueShortIdPrefix already falls back to the full id when
the bounded search was truncated, so reuse it and drop the flag.

* fix(ui): navigate worktree owner links in-app so the face marker survives

The Worktrees owner link was a bare anchor consuming only the shareable href,
so preferenceDerivedFace was a no-op there and an uncached owner always opened
on chat. Match the other session surfaces: keep the clean href and navigate
with the options on plain clicks.

* fix(ui): search the first uuid block so long session references resolve

The gateway matches sessions.list search as a plain substring and stored keys
hold a hyphenated uuid, so any hyphen-stripped reference past the first block
matched nothing and 404'd. This hit disambiguation links, which offer prefixes
longer than eight characters. Send the first block and keep applying the full
prefix per row.

A resolved slug now canonicalizes to that same short reference instead of a
full uuid, so /chat/main/deploy-monitor settles on -6db92d48.

* docs(ui): record why the face marker stays out of shareable hrefs
2026-07-27 04:31:33 -04:00
Thiago Pontes
bb687fa2de fix(ui): keep long Markdown previews scrollable (#113465)
* [AI] fix(ui): add min-height:0 to flex scroll containers for Firefox

Firefox enforces the CSS Flexbox automatic minimum size more strictly
than Chromium, preventing .sidebar-content and .md-preview-dialog__body
from shrinking below their content height. This blocked overflow:auto
from creating a scrollable area when Markdown content exceeded the
available panel height.

Add min-height:0 to both flex children so Firefox allows them to shrink
and become the inner scroll container.

Related to #107571

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [AI] fix(ui): bound Markdown sidebar previews

* [AI] fix(ui): bound file sidebar editors

* test(ui): cover scroll repair in sidebar column layout

---------

Co-authored-by: 杨爱文 <yang.aiwen@xydigit.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Thiago Pontes AI <thiagopontesai@Hefestos-Mac-Mini.local>
2026-07-27 03:54:03 -04:00
Peter Steinberger
24e6d5edd7 fix(ui): recover offline-selected locales once the gateway reconnects (#114378)
* fix(ui): retry offline-failed locale loads when the gateway reconnects

* fix(ui): recover offline-failed locale loads via guarded stale-chunk reload

* test(ui): isolate locale retry tests from the i18n singleton cache

* test(ui): move locale manager test factory to test-support
2026-07-27 03:18:02 -04:00
Peter Steinberger
1fe66e0fe8 fix(ui): guard empty catalog refresh responses (#114394) 2026-07-27 03:09:25 -04:00
Peter Steinberger
9a0fa8f375 fix(ui): back off unavailable session PR polling (#114266)
* fix(ui): back off unavailable session PR polling

The sidebar PR indicators iterated worktree rows sequentially; each ~68ms
UNAVAILABLE reply immediately advanced to the next row, hammering
controlUi.sessionPullRequests at ~14 calls/second forever (the method is
advertised while its runtime dependency is down, so the advertised-method
gate never stopped it).

Route all callers through GatewayBrowserClient.requestSessionPullRequests:
a non-retryable UNAVAILABLE latches until the next socket hello; retryable
failures back off 30s doubling to 5min, honoring longer server retry hints.
A generation counter keeps concurrent rows from multiplying one outage wave.

* fix(ui): satisfy test-types and knip lanes

Type the PR-indicator request mocks with real signatures instead of bare
vi.fn shells, and keep GatewaySessionPullRequestsParams module-local until a
second consumer exists.

* test(ui): route hydration stubs through the PR latch wrapper

* chore(ui): drop unused ControlUiSessionPullRequests re-export

* test(ui): stub the PR latch wrapper in sidebar and clobber fixtures
2026-07-27 02:49:45 -04:00
ClawSweeper
5b58d8540d fix(ui): new sessions remember valid workspace and model choices (#113884)
* fix(ui): remember new session selections

* fix(ui): repair stale new session folder preference

* fix(ui): clear node cwd when switching agents

* fix(ui): revalidate remembered model selections

* fix(ui): repair moved workspace preference

* fix(ui): anchor stale folder errno detection

* fix(ui): wait for remembered session choices

* fix(ui): satisfy new session preference contracts

* fix(ui): persist early folder for resolved agent

* fix(ui): anchor stale folder errno detection

* fix(ui): repair rejected workspace worktree preference

* test(ui): narrow new session request params

* fix(ui): keep an unusable remembered worktree from blocking submit

* test(ui): wait for the committed chat route before leaving it

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-07-27 02:47:09 -04:00
Peter Lee
0406e66369 fix(ui): keep stable chat rows in insertion order and only sort live tool/stream items by timestamp (#113266)
* fix(ui): keep stable chat rows in insertion order and only sort live tool/stream items by timestamp

* fix(ui): keep live rows within current turn

* fix(ui): keep current work above queued turns

* fix(ui): keep streamed replies above queued turns

* fix(ui): preserve reconnecting run order

* fix(ui): preserve causal terminal ordering

* fix(ui): bound replay rows to owning turns

* fix(ui): keep question summaries in owning turns

* fix(ui): scope question run ownership to session

* fix(ui): restore reconnecting chat run identity

* fix(ui): remove unused chatItemTimestamp import in chat-thread-build.ts

* fix(ui): correlate question summaries with agent runs

Co-authored-by: Peter Lee <li.xialong@xydigit.com>

* chore(i18n): refresh native source baseline

Co-authored-by: Peter Lee <li.xialong@xydigit.com>

* fix(ui): remove unused chatItemTimestamp export and split tool-stream test file

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-07-27 02:08:22 -04:00
Peter Steinberger
564ba951a7 refactor(ui): consolidate chat lifecycle regression tests (#114335) 2026-07-27 01:59:43 -04:00
Peter Steinberger
256988e068 refactor(ui): consolidate workboard regression fixtures (#114348)
* refactor(ui): consolidate workboard regression fixtures

* test(ui): preserve nullable workboard refresh fixture
2026-07-27 01:48:05 -04:00
Peter Steinberger
e154330a42 perf(ui): parallelize session-switch hydration (#114338)
* perf(ui): parallelize session switch hydration

* test(ui): cover reconnect session hydration

* test(ui): give hydration request mocks real signatures
2026-07-27 01:43:47 -04:00
Peter Steinberger
e78310fc23 fix(ui): validate session catalog agent (#114303) 2026-07-27 01:33:08 -04:00
Peter Steinberger
e0eed257c8 fix(ui): persist Control UI settings across reconnects via gateway-owned LWW prefs writes (#114286)
* fix(ui): sync Control UI prefs via hash-free LWW config.patch

* test(ui): prove prefs reconnect replay end to end

* fix(gateway): surface hash-free prefs commit races instead of replaying stale intent

* fix(ui): never re-reconcile a retained config snapshot over acked prefs

* fix(ui): merge persisted pending prefs across tabs instead of clobbering

* test(ui): pass current runtime config through sidebar prefs reconcile fixture
2026-07-27 01:17:03 -04:00
Peter Steinberger
8b66fc103d feat(ui): durable session board face and dashboards index (#114262)
* feat(ui): durable session board face and dashboards index

Board face lived only in client-side boardSessionViews, capped at 50 entries,
so the preference never followed the user to another device, evicted as
sessions accumulated, and could not be seen as a set.

Persist it as SessionEntry.boardFace, which rides the existing entry_json blob
and so needs no SQLite schema change or version bump. Expose it on the session
list row and add it to the sessions.patch write-scope allowlist alongside label,
pinned, and archived: setting your own view preference is user-level chat
organization, not policy. Unknown patch fields still fail closed to
operator.admin.

Generic navigation now reads the stored face, so the sidebar and session list
open a thread on the face you left it on. boardSessionViews keeps only
activeTabId and reopenDockByTab, which are genuinely per-device.

Add /dashboards listing threads whose preferred face is dashboard. Filtering
runs server-side in filterSessionEntries before pagination, because the client
holds only a capped page and a client-side filter would silently omit
dashboards.

* test(protocol): assert the pre-rename face param is rejected

The gateway-protocol validator test still passed the pre-rename 'face' key,
which the closed schema rejects. Use boardFace, and pin the old name as a
negative case so it cannot silently return.

* chore(protocol): regenerate Swift bindings and docs map for boardFace

Adding boardFace to the sessions schema changes two committed generated
artifacts: the Swift gateway models (pnpm protocol:gen:swift) and the docs map
(pnpm docs:map:gen), which now lists the dashboards index section.
2026-07-27 00:35:34 -04:00
Peter Steinberger
bb55d44840 fix(gateway): stop fabricating assistant agent id "main" before roster resolution (#114257)
Three fabrication points told clients the assistant agent was "main" before
any roster/config resolution: DEFAULT_ASSISTANT_IDENTITY in
src/gateway/assistant-identity.ts, the Control UI store's initial snapshot
(ui/src/app/gateway-store.ts:65), and its hello fallback (:336). On installs
whose implicit main agent is retired, every page reload fired
sessions.catalog.list for the nonexistent agent and flashed
'unknown agent id "main"' in the sidebar.

The no-roster state now carries no agent id: DEFAULT_ASSISTANT_IDENTITY drops
agentId, resolveAssistantIdentity returns ResolvedAssistantIdentity (agentId
required) for config-backed paths, the bootstrap omits the optional
assistantAgentId field without config, the UI store starts null, and agent
selection adopts the roster default once agents.list arrives.
2026-07-26 23:48:13 -04:00
Shakker
24786f7219 test: cover agent scope first-switch sync (#114259)
Adds focused Control UI regression coverage for first-switch agent scope label synchronization.

Closes #114142.
Prepared head SHA: 415d6e9336
Reviewed-by: @shakkernerd
2026-07-27 04:33:51 +01:00
ClawSweeper
f4d454c0c6 fix(ui): new-session composer grows before scrolling (#113842)
* fix(ui): grow new session prompt before scrolling

* fix(ui): preserve composer fallback for non-pixel caps

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-07-26 22:23:54 -04:00
ClawSweeper
ca11ee6db1 fix(ui): new worktree sessions stay in Coding during creation (#113834)
* fix(ui): preserve new coding session selection

* test(ui): cover prepared sidebar sessions

* fix(ui): retire prepared work sessions on delete

* test(ui): refresh prepared sidebar mock for the configured main key

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-07-26 22:18:07 -04:00
Peter Steinberger
a3283af7c7 feat(ui): complete the Labs roster with the remaining experimental gates (#114233)
* feat(ui): complete the Labs roster with the remaining experimental gates

Labs shipped with two entries while three more experimental surfaces stayed
reachable only by hand-editing config. Adds Tool Search, lean local-model tools,
and message audit metadata, each with the subtext and documentation link the
page already promises.

Two of them do not fit the boolean gate the registry assumed, so the registry
now states each row's on/off values instead of hardcoding true/false:

- Tool Search is a mode, and resolveToolSearchConfig defaults an unset mode to
  "code" even in object form. A bare enable would therefore select the surface
  with the weakest recall rather than the bounded directory this row advertises,
  so enabling writes `mode: "directory"` alongside the gate in one patch.
- Message audit is `off | direct | all`. Labs offers the conservative `direct`,
  so turning it on cannot start recording group or unknown conversations the
  operator never opted into, and `all` deliberately does not read as on — that
  is a broader choice made elsewhere and this row must not quietly narrow it.

The audit row carries a restart hint because startGatewayEventSubscriptions
resolves the mode once and bakes it into the recorder, which outlives the reload
plan's `logging: none` rule. The other four are read per agent run.

* fix(ui): read a broader audit mode as enabled instead of narrowing it

`all` records more than the `direct` this row offers, but it is still on. Strict
equality against onValue rendered it as off, which made the switch look available
and would have silently rewritten a deliberately broader operator setting down to
`direct` on the next click.

Separate "what enabling writes" from "what counts as enabled": onValue stays the
value Labs sets, activeValues lists every value that reads as on. Turning the row
off from `all` now writes `off`, which is the only narrowing the operator asked
for.

The test that covered this asserted the opposite of what its own comment
described; corrected, plus coverage for the off-from-all write.

* fix(ui): mirror the runtime rule for Tool Search enablement

ToolSearchConfig is a union, and resolveToolSearchConfig treats an object that
configures anything besides `enabled` as already on:
`readBoolean(raw.enabled, configured)`. Reading only the `enabled` leaf showed
`{ mode: "tools" }` as off, so the row offered a switch that would have replaced
that operator's mode with `directory` — the same narrowing the audit row was
just fixed for.

Give the registry a `readEnabled` override for gates whose enablement is not a
single leaf, and point Tool Search at a copy of the runtime rule with the
resolver named so the two stay comparable. The other four rows keep the leaf
read and declare `readEnabled: null` explicitly.

* chore(ui): keep LabFeatureValue local to the registry

Only the registry names the type, and the hard-zero Knip production scan rejects
an export with no production consumer outside its own module.
2026-07-26 22:11:14 -04:00
Peter Steinberger
32719ea59a fix(ui): show real cron failure detail in the sidebar attention tooltip (#114224)
* fix(ui): show cron failure detail in sidebar attention tooltip

* refactor(ui): reuse clampText for cron tooltip error cap
2026-07-26 21:44:18 -04:00
Jesse Merhi
d7627d6f4c refactor(prompt): use plain inbound context labels and drop system-tag sanitizer (#112000)
* refactor(prompt): plain inbound context labels with a provenance marker

Replaces trust-worded inbound context labels ("(untrusted metadata)",
"(untrusted, for context)") with plain labels plus a fixed provenance
marker suffix appended to every OpenClaw-injected context header.

Detection keys on the marker, not label text, so strippers stay correct
across UI, TUI, replay, /trace segmentation, memory recall, and the Swift
chat preprocessor. Drops sanitizeInboundSystemTags in favor of the marker
boundary plus trusted system-prompt narration.

Renames the untrusted-named plugin SDK context identifiers to
channel-provenance names, keeping deprecated aliases registered for
removal after 2026-09-08.

Adds `openclaw doctor --fix` migrations that rewrite legacy inbound
labels in stored SQLite transcripts and purge legacy envelope-
contaminated LanceDB recall rows.

* fix(ci): resolve gate failures for plain inbound context labels

- doctor sqlite readers: open read-only connections via openNodeSqliteDatabase
  so the Kysely connection-boundary guardrail holds; unexport the now-internal
  transcript snapshot type (Knip unused-export gate).
- compat registry: split the record table into registry-records.ts and
  plugin-sdk-subpath-records.ts. The new compat record pushed registry.ts past
  the 700-line oxlint cap; suppressions are disallowed, so follow the existing
  sibling record-module pattern. Public exports and PluginCompatCode literals
  unchanged.
- acp-runtime test: assert current finalization behavior (newline normalization
  only). The bracket de-fang and System: rewrite it expected were removed with
  sanitizeInboundSystemTags; forged system lines are neutralized at the
  system-event queue, the single chokepoint feeding the System:-per-line render.
- regenerate docs_map and the plugin SDK API baseline manifest.

* fix(prompt): harden inbound context label migration and drop in-band sanitizer

Review follow-ups on the plain-label + provenance-marker change:

- Remove src/security/system-tags.ts. Rewriting inbound text to neutralize
  look-alike `System:`/`[System]` markers corrupted legitimate user text and is
  not a real injection boundary; role separation plus external-content wrapping
  is. Explicit product decision, recorded at the system-event queue.
- Narrow the LanceDB legacy-row purge so it cannot delete benign memories. It
  now requires a complete known legacy sentinel line, a legacy label followed by
  a fenced JSON body, or the complete legacy external-content header. The prior
  predicates matched ordinary prose such as `Notes (untrusted metadata):`, and
  deletion is irreversible.
- Make explicit-empty canonical ChannelStructuredContext win over the deprecated
  alias via a present/absent result instead of collapsing `[]` to undefined.
- Keep `\r?` in the active-memory doctor rule. It is the only rule spanning the
  header's line break, migrated assistant rows skip newline normalization, and
  without it the marked-header replace wins and the body strips to empty. Added
  a CRLF regression test.
- Fix stale comments that described removed behavior, and cover the Swift
  prose-block strip path.

Claude-Session: https://claude.ai/code/session_01WNzsPddQmxy9Y7jKD4wAxH
2026-07-27 11:27:53 +10:00
Peter Steinberger
472f8e7c72 fix(ui): keep the live token counter current in memoized chat rows (#114213) 2026-07-26 21:09:18 -04:00
Peter Steinberger
c91a37aeed feat(ui): add a Memory settings page (#114037)
* feat(ui): add a Memory settings page with Dreaming as a tab

Memory config was scattered across five surfaces: the memory.* schema section
lived on AI & Agents with 43 of 51 keys behind the Advanced tier, the memory
slot owner was only visible on Plugins, dreaming's knobs were JSON-only, its
status UI sat under Agents, and Memory Import was a separate route.

/settings/memory now owns that surface, following the MCP page shape (curated
rows above an embedded schema editor):

- Overview: the exclusive memory slot rendered as a segmented control over
  installed memory-kind plugins, memory.backend promoted out of Advanced with
  the qmd sub-config revealed only when qmd is selected, additive add-on rows,
  and a Memory Import link.
- Search: the memory.search surface via the embedded editor.
- Dreaming: the global frequency/model/timezone/storage/phase knobs, which
  previously required hand-editing openclaw.json, plus an agent picker feeding
  the existing dream scene/diary/advanced panel for the agent-scoped reads.

Engine selection calls plugins.setEnabled so the gateway's exclusive slot
policy stays the single owner instead of being duplicated in the UI.

* fix(ui): redirect stale ai-agents memory deep links to the memory page

* fix(ui): report memory runtime defaults on the Memory page

The Dreaming tab rendered its own defaults instead of the ones
resolveMemoryDreamingConfig applies, so a config carrying only
dreaming.enabled showed all three phases off while they were running, and
an unset storage mode read as inline instead of separate. Toggle specs now
carry the runtime fallback and the storage default is stated once, both
pointing at src/memory-host-sdk/dreaming.ts.

Three more surfaces asserted things the runtime does not do:

- plugins.slots.memory "none" is the explicit-off sentinel, not an engine
  id, so the segmented control selected nothing. The slot now resolves to a
  closed auto/off/pinned selection with its own hint.
- memory.backend is resolved by the memory runtime the slot owner
  registers, which only memory-core ships, so the row is hidden for any
  other engine instead of saving a value nothing reads.
- The Dreaming tab wrote config.dreaming for whichever plugin owns the
  slot even when that plugin's schema cannot hold it. It now reuses the
  enablement flow's schema check (resolveDreamingConfigPathSupport, shared
  with updateDreamingEnabled) and renders an unsupported state instead.

Also key the plugin-catalog sync on the connected phase: the connecting ->
connected transition keeps the same client object, so a page mounted
during the handshake never loaded the catalog and never showed the engine
picker.

The tab keeps the autosave status line and restart banner the embedded
editor renders on the other tabs; these knobs autosave, but nothing
reported it. The pure view moved to memory-dreaming.ts with the element in
memory-dreaming-page.ts, matching memory.ts/memory-page.ts.

* fix(ui): resolve the memory slot through the canonical policy

The Memory page re-derived plugins.slots.memory instead of using the rule the
runtime applies, which broke both directions of the engine control:

- An unset slot was reported as "the first enabled memory-kind plugin in the
  catalog". The runtime resolves it to the slot's default owner
  (DEFAULT_SLOT_BY_KEY.memory), so the page could show one engine as active
  while another was loaded, reveal or hide the backend row for the wrong
  plugin, and target the wrong plugin when switching memory off.
- Off called plugins.setEnabled(false), which writes enablement only. The slot
  stayed pinned, so the choice did not survive a refresh and re-enabling that
  plugin from the Plugins page silently switched memory back on.

resolveSlotSelection now lives next to defaultSlotIdForKey in
src/plugins/slots.ts and owns the rule once; config normalization consumes it
and the page imports it instead of restating it. Off writes the explicit "none"
sentinel through the config form, so it round-trips; picking an engine still
goes through plugins.setEnabled, which is where the exclusive slot policy
lives. The dreaming controller's own copy of the rule is gone too.

Four smaller fixes on the same surface:

- A failed engine change is reported next to the control instead of being
  swallowed, so the selector no longer just snaps back.
- Dreaming's numeric inputs carry the memory-core manifest's integer/min/max
  bounds and refuse out-of-range edits at the field, rather than patching a
  value autosave then fails to write.
- Settings search destinations carry the Memory tab that renders the matched
  child, so a memory.search hit no longer lands on Overview, whose narrowed
  editor omits it.
- The Dreaming tab caches only a definitive schema-capability answer. An
  offline or failed lookup now reports "unknown" and is retried on reconnect
  instead of permanently suppressing the recheck.

* fix(ui): model unknown memory state instead of collapsing it

The Memory page reported unknowns as decided values. An empty catalog meant
loading, disconnected, or a failed plugins.list, yet add-on rows rendered
"Disabled"; catalog completions were keyed on client identity, which survives a
phase flip, so a stale load could repopulate a disconnected page or overwrite a
newer read; and `?tab=` was adopted once per distinct value, so a repeat
navigation to a tab the user had left was ignored.

Replace the ad-hoc nullable fields with closed shapes. MemoryCatalog is a
loading/unavailable/ready union, so absence of an entry only decides anything
inside `ready`, and MemoryAddonRow carries a four-state enablement the view
renders without ever inventing an "off". CatalogConnection is one object per
(client, connected) transition and doubles as the request generation an
in-flight load carries, so obsolete completions are dropped by identity. The tab
is no longer page state at all: the URL owns it, tab clicks navigate, and every
arrival is honored.

Settings search now resolves the engine/backend through the same
resolveMemoryBackend the page uses and matches only the `memory.*` children the
page can surface, so a `memory.qmd` hit under the built-in backend no longer
routes to an Overview whose editor omits it.

* fix(ui): surface a disabled memory owner and anchor curated backend search

The slot and plugin enablement are independent config surfaces, so
`plugins.slots.memory` can name a plugin the catalog reports as disabled.
The engine control showed that plugin as selected, and because re-picking an
already-selected radio fires no change event, there was no way back on. Add an
explicit enable row for that state and let the same-id write through when the
owner is not running; picking Off stays a no-op.

`memory.backend` is curated out of the schema editor, so the generic
`#config-section-memory` anchor scrolled past it. Fold the memory tab and hash
choice into one `memoryDestination` owner that routes a curated-only match to
the new anchor above the editor.

* fix(ui): scope the dreaming capability probe to its connection

The probe was deduplicated by plugin id alone, which cannot tell a current
answer from a stale one. A disconnect and reconnect on the same slot owner left
the token armed, so the reconnect read as "already in flight" and swallowed the
retry that an `unknown` answer requires — leaving an unsupported engine's knobs
editable until some unrelated config notification arrived. An A -> B -> A switch
had the mirror problem: the old A response was accepted for the new A probe.

Make the in-flight probe an object whose identity is the generation, drop it
whenever the owner or the connection changes, and accept only the completion
that still owns the slot. Same shape as the catalog guard on the Memory page.

* fix(ui): satisfy the lint and dead-export gates on the memory page

Exhaustive switches need a terminal `default:` to satisfy
typescript/consistent-return, matching the existing view-status.ts shape.

Seven symbols were exported with no production consumer outside their own
module, which the hard-zero Knip production scan rejects. Tests alone do not
make internals contracts, so drop the exports and reach the behavior through
each module's public surface instead: the view props type comes from
`Parameters<typeof renderMemory>`, the tab panel is found by its ARIA role, and
the dreaming number/storage helpers are proven through `renderDreamingSettings`.

Folding those helper unit tests into the render path also corrected one of them:
a `type="number"` input coerces unparseable text to empty, so the "reject
garbage" case was unreachable through the real control. Replaced with the
inclusive-bound and clear-the-field cases, which are reachable.

* refactor(ui): keep the memory schema facts out of the startup bundle

Settings pages are already lazy — the config route is `import("./config-page.ts")`
— but settings search runs from app-host at startup, and it needed the same
answers about which `memory.*` children are reachable and where a match lives.
Importing those from the view module dragged lit, hub-tabs, and settings-ui into
the startup chunk with it, blowing the Control UI startup budget.

Move the rendering-free facts (slot/backend resolution, tab and curated key
lists, schema narrowing, the anchor id) into memory-schema.ts, which imports
only record-coerce and the shared slot policy. The view keeps the templates and
now consumes the same module, so there is still one owner per fact.

* chore(ui): record the memory settings surface in the startup budget baseline

Routing settings search through memory-schema.ts instead of the view module
recovered 10,872 B of the startup chunk (334,992 -> 324,120 B), which is back
under the 324,608 B ceiling. The remaining 2,795 B over the old baseline is the
honest cost of the new surface: its i18n strings, plus the slot/backend facts
the startup search index has to read.

Measured by hosted CI (run 30189972795); this worktree cannot build locally
because pnpm wants to purge a node_modules shared with other running agents.
2026-07-26 21:05:03 -04:00
Peter Steinberger
abd9eededf test(ui): assert the Home run ring in chat run lifecycle e2e (#114212) 2026-07-26 21:03:21 -04:00
Patrick Erichsen
c66ca2fbb2 feat: show ClawHub skill icons in Control UI (#114152) 2026-07-26 14:44:36 -05:00
Peter Steinberger
cca5b14785 feat(ui): path-based session and dashboard URLs (#113883)
* feat(ui): path-based session and dashboard URLs

* docs(ui): document control UI URLs

* fix(ui): finalize session path routing

* feat(ui): anchor session URLs on stable keys

* docs(ui): clarify stable session URL identity

* fix(ui): resolve session prefixes with full prefix and pagination

Query sessions.list with the full supplied prefix instead of an eight-char
truncation, and paginate up to a bounded page count so longer disambiguation
links resolve instead of being reported ambiguous. Zero strict-prefix matches
now fall through to literal-key resolution rather than rendering an empty
ambiguity view.

Also document the ~dot/~dotdot segment escape: peer ids reach session keys
trimmed and lowercased only (src/routing/session-key.ts), so a literal '.' or
'..' segment is reachable and browsers would normalize it away.

* fix(ui): synchronize committed session routes

* test(ui): split native shell host coverage

* fix(ui): thread configured mainKey through session URL builders

Reserved-set disambiguation needs the operator-configured mainKey at runtime, so
thread it from agentsList through every session path builder and the ClickClack
control URL. Unambiguous non-hex single-segment rests now resolve literally while
short-id-shaped rests still fail closed, which restores ClickClack channel
compatibility detection and control-link reconciliation.

Also bound prefix-resolution retries, preserve catalog thread identity, and keep
draft state on ambiguous candidate links.

* fix(ui): repair session URL CI integration

* perf(ui): lazy-load session route resolution

* perf(ui): isolate session prefix resolution

* perf(ui): defer session path parsing

* perf(ui): defer session navigation startup

* fix(ui): preserve first-run and literal session navigation

* fix(ui): satisfy session routing type and export gates

* fix(clickclack): preserve unscoped control link agent

* style(ui): satisfy chat page line limit

* refactor(ui): move chat page helpers to owning modules

* fix(ui): preserve destination session route identity

* fix(ui): preserve agent identity in session routes

* fix(ui): escape dots in literal session path segments

encodeURIComponent leaves periods intact, so a literal key segment like
channel:release.js produced /chat/main/channel/release.js. In-app navigation is
intercepted by the SPA, but a refresh, an external link, or a ClickClack link
would be served as a static asset request and never reach the app.

pathForWorkboardBoard already escapes dots for this reason; mirror it in both the
session URL contract and the ClickClack encoder, route the agent id through the
same segment encoder, and pin the case in both shared vector tables.

* fix(ui): stop bootstrap after teardown race

* refactor(ui): centralize session navigation targets

* fix(ui): consume bootstrap teardown abort

* fix(ui): canonicalize configured main session routes

* fix(ui): preserve distinct session references

* fix(ui): redirect released session query links

* fix(ui): make bootstrap teardown abort-safe
2026-07-26 12:46:04 -04:00
Peter Steinberger
08a2f51e4e fix(ui): show the correct main-session title for every agent (#114120) 2026-07-26 10:59:14 -04:00
Peter Steinberger
201e02f226 fix(ui): preserve concurrent background task progress (#114085)
* fix(ui): preserve concurrent background task progress

* fix(ui): retain concurrent task events across stale snapshots

* fix(ui): preserve chronological task snapshot progress

* fix(ui): type buffered task event replay
2026-07-26 08:03:18 -04:00
Peter Steinberger
cd5a5ecb4c fix(ui,gateway): make catalog sidebar sections drag-reorderable (#114074)
* fix(ui,gateway): make catalog sidebar sections drag-reorderable

* test(ui): split sidebar section-reordering cases under max-lines

* perf(ui): keep catalog section rendering out of startup bundle
2026-07-26 05:31:33 -04:00
Peter Steinberger
27293aa273 feat(ui): gateway subtitle in the sidebar footer identity card (#114059)
* feat(ui): show gateway subtitle in sidebar footer

* chore: keep CHANGELOG release-only

* test(ui): fix indexed-access type in sidebar gateway case

* perf(ui): trim footer subtitle startup bytes

* perf(ui): trim gateway footer strings

* perf(ui): re-record startup baseline for gateway footer

* perf(ui): re-record startup baseline from Linux build
2026-07-26 05:23:42 -04:00
ClawSweeper
bfd6f2efdc feat: make working claw tricks rare surprises (#114025)
One deterministic special move on ~3% of runs, played once after a delay,
then back to the default loop; approval waits and reduced motion stay on
the default claw. Includes render-path test coverage for the wiring.

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-07-26 04:43:57 -04:00
ClawSweeper
203ef71ca3 fix(ui): center running-task preview over link (#114069)
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-07-26 01:39:30 -07:00
ClawSweeper
af26c7ed72 fix(ui): keep chat run status in one assistant turn (#114039)
* fix(ui): unify active chat status

* fix(ui): preserve chat recap ordering

* fix(ui): re-render turn rows when embedded run status changes owner

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-07-26 04:09:08 -04:00
Peter Steinberger
8eafae4af7 fix(ui): recover from unknown selected agents and surface session-catalog failures (#114048)
* fix(ui): recover from unknown selected agents and surface session-catalog failures

* test(ui): split catalog request-error cases under max-lines
2026-07-26 02:42:59 -04:00
Peter Steinberger
1677058cf5 fix(ui): keep first prompt across gateway reconnects (#114042) 2026-07-26 01:46:56 -04:00
Peter Steinberger
2de58d408a Control UI: gateway-owned sidebar section order (#113930)
* feat(ui): gateway-owned sidebar section order with hover-only drag grips

* fix(ui): repair sidebar section order CI gates

* fix(protocol): keep sectionOrder optional for older gateways

Native Swift clients ship separately from gateways, so new clients must decode older gateway responses that omit sectionOrder.

* fix(ui): reconcile gateway section ordering with main

* fix(state): allow lazy tables in v5 maintenance

* refactor(ui): retire prefs session section order in favor of gateway-owned order

Supersedes #113948 by deleting the unreleased ui.prefs.sessionSectionOrder key and its browser plumbing. Gateway SQLite sidebar_sections remains the single canonical store.
2026-07-26 01:15:19 -04:00
Peter Steinberger
36404eaa3b fix(ui): dashboard widgets leave a gap below content and hover chrome blocks widget buttons (#114012)
* fix(ui): hug auto widget cards to content and compact board chrome into a top-left pill

* test(gateway): retry temp-dir cleanup in chat abort persistence suite to absorb ENOTEMPTY race

* fix(ui): strip board pill to move and menu on narrow widgets so the action corner stays widget-owned
2026-07-26 01:14:40 -04:00
Jesse Merhi
c0d4cafbcc refactor(ui): extract wizard step controls into a shared component (#113733) 2026-07-26 14:20:35 +10:00
Peter Steinberger
5aef17205a fix(ui): add empty sidebar drop targets (#114005) 2026-07-25 21:02:58 -07:00
Peter Steinberger
adfb59c19b feat(mac): dashboard gateway picker with in-place switching (#113965)
* feat(mac): add dashboard gateway switching

* feat(ui): add dashboard gateway picker

* docs(mac): document dashboard gateway picker

* fix(mac): harden gateway switching after review

* fix(ui): refresh gateway picker snapshots

* fix(mac): carry TLS pins through promotion and serialize gateway switches

* style(ui): satisfy lint rules in gateway picker files

* fix(ui): tolerate absent context in gateway picker pane props

* chore(ci): refresh generated inventories

* style(mac): satisfy Swift CI checks

* refactor(mac): drop dead GatewayEndpointStore.requireConfig

* perf(ui): own gateway capability in the chat chunk

* chore(ui): keep gateway capability factory module-private
2026-07-25 20:57:28 -07:00
Peter Steinberger
852a686a1b feat(chat): restore prompt image attachments on rewind/fork (#113945)
* feat(chat): restore prompt image attachments on rewind/fork

* chore(i18n): refresh native source inventory anchors

* fix(gateway): restore media-store images on rewind/fork

* fix(gateway): bound and dedupe rewind media reads

* fix(sessions): bound inline editor attachments in message cut

* fix(ui): align restored attachment cap with gateway 5 MiB limit

* fix(gateway): dedupe rewind media refs by resolved id

* fix(sessions): lint-safe __openclaw access in message cut
2026-07-25 20:32:16 -07:00
Peter Steinberger
8f3f746b2f test(ui): stabilize drag-managed group e2e (#113989) 2026-07-25 20:08:48 -07:00
Peter Steinberger
71cb266c1f fix(ui): keep Coding sessions visible after reconnect (#113981)
* fix(ui): preserve scoped session hydration

* chore: leave changelog to release automation

* build(ui): refresh startup budget baseline
2026-07-25 19:56:50 -07:00
Peter Steinberger
77b44473c7 fix(ui): match plugins by package name (#113933) 2026-07-25 19:48:24 -07:00
Peter Steinberger
3807591ff4 build(lint): adopt stylelint for Control UI css templates and stylesheets (#113971) 2026-07-25 19:13:46 -07:00
Peter Steinberger
f90cef67c8 feat(ui): drag custom sidebar groups between built-in session zones (#113948)
* feat(ui): drag custom sidebar groups between built-in session zones

* test(ui): remove stale split ratio fixture

* perf(ui): raise startup budget baseline for session-section ordering pref
2026-07-25 19:11:25 -07:00
Peter Steinberger
98542bc55b refactor(ui): inline stroke attributes via shared strokeIcon shell (#113952) 2026-07-25 19:08:17 -07:00
Peter Steinberger
a328e900fd fix(ui): keep sidebar selection on archived sessions and replace the composer with an archived notice (#113882)
* fix(ui): keep sidebar selection on archived sessions and replace composer with archived notice

* fix(ui): narrow lineage-row lookup and restructure fallback projection scan

* fix(ui): publish routed lineage row so cold-loaded archived sessions replace the composer

* chore(ui): drop unused mergeChildSessionRows export
2026-07-25 19:07:25 -07:00
Peter Steinberger
c7fcfd3482 fix(ui): gate chat sidebar mutations (#113947) 2026-07-25 19:01:32 -07:00