Commit Graph

3500 Commits

Author SHA1 Message Date
Peter Steinberger
b07985ef45 feat(ui): show waiting-for-approval run status in chat (#111445)
* feat(ui): show waiting approval run status

* fix(ui): hydrate waiting approval status

* fix(ui): correlate approval status by run

* fix(ui): harden approval status reconciliation
2026-07-19 19:44:25 -07:00
Peter Steinberger
c5f9addb52 fix(ui): keep custodian transcript aligned after inference errors (#111562)
* fix(ui): reconcile invalidated custodian sessions

* test(ui): preserve custodian page harness type
2026-07-19 16:00:08 -07:00
Peter Steinberger
3247a56d15 fix(ui): prevent cross-file mock leakage in Control UI tests (#111554)
* test(ui): stop shared mock registry leakage

* ci: add three-worker UI leakage canary
2026-07-19 15:38:50 -07:00
Peter Steinberger
6c25f5ae33 fix(ui): keep steered composer messages visible until the transcript owns them (#111540)
* fix(ui): keep steered composer messages visible until the transcript owns them

Typing while the agent was busy showed a brief steering state and then the
message vanished until the next full history reload. The steer path removed
the queued row at ack time and only restored the steered chip when the tab
had adopted the active run id, which never happens for runs started by
automations, other clients, or tabs opened mid-run.

- steer sends reuse the durable row's sendRunId as the wire idempotencyKey so
  delivered-turn and history-proof reconciliation can correlate steered rows
- the steered chip is restored after every accepted ack, keyed to the active
  run when the tab still tracks it, else to the steer's own gateway lifecycle
- terminal events materialize acknowledged steered chips into the transcript
  before clearing them, and stale history reloads keep idempotency-marked
  local turns until authoritative history catches up
- in-flight (unacknowledged) steers are never materialized, so a rejected
  send cannot leave a phantom user turn behind

Fix authored with Codex; reviewed and hardened via structured review.

* fix(ui): use bracket access for __openclaw test marker (no-underscore-dangle)

* fix(ui): resolve steered attachment payloads through the store before materializing

Queue rows carry attachment metadata only; composer upload bytes live in the
payload store. Resolving through getChatAttachmentDataUrl keeps attachment-only
steers visible when their chip retires (Codex connector review finding).

* fix(ui): only user-role entries satisfy queued-turn presence checks

An assistant entry can carry the same run's idempotency key; matching it let
the chip re-add and terminal materialization skip the user's copy
(ClawSweeper rank-up move).
2026-07-19 15:17:08 -07:00
Peter Steinberger
c6f7ddff1a fix(ui): show attributed sender identity properly in chat (#111537)
* fix(ui): show attributed sender identity properly in chat

Three defects around multi-user sender attribution:

- Sender labels rendered as 'name (<profile uuid>)': resolveSenderLabel
  appended the id part even when it is an opaque UUID. Suppress UUID
  suffixes server-side and strip already-baked ones in the UI normalizer
  so historical transcripts read clean too.
- Attributed messages showed initials instead of the author's avatar:
  chat sender metadata carries only the profile id, so resolveAvatar now
  derives the canonical /api/users/<id>/avatar route (upload -> gateway
  Gravatar proxy -> initials) from UUID-shaped sender ids.
- Author identity was hover-only: identity-resolving gateways keep the
  always-visible avatar gutter even in 1:1-shaped sessions, and the
  gutter renders the message author's avatar (with identity-colored
  initials fallback) instead of the local viewer's.

* fix(ui): preserve legacy author identity and 404-safe sender avatars

Review findings: stripping the baked UUID suffix discarded the only
author key on legacy rows (the gutter then showed the local viewer's
face under another person's label) — the UUID now survives as
structured non-display sender identity. And the derived avatar route
can 404 (no upload, no Gravatar), so the gutter image swaps to
identity-colored initials on error instead of a broken image.

* fix(ui): attribute bare-UUID legacy sender labels to their profile

A nameless legacy sender labels as the bare profile UUID; recognize that
whole-label shape as the sender id (keeping it as last-resort display)
so the row resolves the author's avatar instead of misattributing to the
local viewer.
2026-07-19 14:59:33 -07:00
Peter Steinberger
6a408983d0 fix(onboard): make first-run hatch reliable (#111465)
* fix(onboard): harden first-run handoff

* style(ui): avoid onboarding comparator shadowing

* fix(agents): preserve mixed refresh failures
2026-07-19 14:38:58 -07:00
Peter Steinberger
77378cb0f3 fix(ui): keep stray file drops from leaving Control UI (#111531)
* fix(ui): guard unhandled file drops

* test(ui): exercise accepted file drop target

* test(ui): prove file drop guard in browser
2026-07-19 14:38:30 -07:00
Peter Steinberger
06480d767c feat(dashboard): stitch session dashboards end-to-end — live provider, show_widget pin, board commands (#111218)
* feat(dashboard): pin canvas widgets through board domain

* feat(control-ui): wire session dashboards end to end

* fix(control-ui): preserve direct reset confirmation

* fix(control-ui): localize dashboard default tab

* test(dashboard): satisfy changed typecheck gate

* fix(dashboard): satisfy changed lint gate

* fix(dashboard): harden pinned widget lifecycle

* fix(dashboard): use extracted mock normalizer

* test(dashboard): reconcile hardened board admission tests

* fix(dashboard): align grant and canvas protocol requests

* fix(dashboard): preserve refresh and pin invariants

* fix(boards): treat missing board tables as empty on read-only paths

* fix(boards): accept read-only handles in board table probe

* fix(control-ui): preserve virtual board widget types

* fix(dashboard): align view types with protocol

* fix(dashboard): keep resolved board view typed

* chore(canvas): drop test-only html read wrapper

* chore(release): defer dashboard note generation

* fix(protocol): align board widget put params type

* fix(control-ui): preserve queued reset approval

* fix(control-ui): gate dashboard pinning capability

* chore(test): isolate dashboard capability fixture

* fix(control-ui): refresh dashboard pin affordance
2026-07-19 12:36:54 -07:00
Peter Steinberger
c69d6e148e feat(ui): who's-online roster menu on the footer facepile (#111501)
* feat(ui): who's-online roster menu on the footer facepile

Clicking the sidebar footer facepile now opens a scrollable roster of
everyone online — avatar, name, and email subtitle per person, with your
own entry pinned first and marked (you). Uses the stock menu-surface +
wa-dropdown idiom; long rosters scroll inside the popup via the dropdown's
menu part. Session-row facepiles stay non-interactive so row clicks keep
navigating.

* fix(ui): roster menu interaction hardening from review

- close explicitly on wa-select (preventDefault also cancels the
  dropdown's own select-and-hide)
- clear the open state when a presence update unmounts the footer
  facepile, so the menu cannot remount stale when presence returns
- restore focus to the visible facepile button on keyboard dismissal
  (the dropdown's own trigger is a hidden throwaway anchor)

* fix(ui): return focus to the facepile trigger on roster row selection
2026-07-19 11:34:17 -07:00
Peter Steinberger
40d31f3481 feat(control-ui): show relative commit age and hover-only copy button on About (#111495)
The About page commit row now embeds the commit's committer timestamp
(commitAt, advisory like branch/dirty) and renders it as an always-relative
age with the exact localized date-time on hover. The copy button becomes a
compact borderless icon revealed on hover/focus only where hover exists;
touch keeps it visible.
2026-07-19 11:13:25 -07:00
Peter Steinberger
6d4082ef97 fix(ui): prevent unsafe custodian nudge answers (#111430)
* fix(ui): harden custodian nudge actions

* refactor(ui): split custodian page helpers

* fix(ui): classify custodian nudge delivery

* fix(ui): ignore stale custodian question replies

* fix(ui): restore rejected custodian questions

* fix(ui): preserve custodian wizard uncertainty

* refactor(ui): keep nudge text helper private

* test(ui): model uncertain custodian delivery

* fix(ui): scope delayed custodian dismissals

* fix(ui): retain in-flight custodian nudges

* fix(ui): block nudges during hosted wizard input

* chore(protocol): regenerate gateway models
2026-07-19 11:10:41 -07:00
Peter Steinberger
e6d7aee41d fix(ui): stop chat normalizers throwing on null/undefined message entries (#111494)
normalizeMessage / isToolResultMessage / isStandaloneToolMessageForDisplay
cast their unknown input to a record and then read `typeof m.role`. That
still evaluates `m.role`, so an undefined entry (e.g. groupMessages calling
normalizeMessage(item.message) with an absent message on a pending or
malformed transcript row) threw "Cannot read properties of undefined
(reading 'role')" inside the gateway event handler — a caught but recurring
error storm on the live team instance. Coerce non-object input to {} at the
source so every downstream `typeof m.<field>` check works and role degrades
to "unknown". The existing safeNormalizeMessage try/catch wrapper existed
precisely for this hazard; the guard now lives in normalizeMessage itself.
2026-07-19 10:54:55 -07:00
Shakker
8e0c50380c fix: preserve message-only terminal output (#111493) 2026-07-19 18:51:06 +01:00
Shakker
0db0142ec8 fix: preserve streamed text before terminal errors (#108509) 2026-07-19 18:07:12 +01:00
Shakker
1e80a0ee88 chore: format chat run error changes 2026-07-19 18:07:12 +01:00
Shakker
9dd021a5cc fix: align run error on mobile 2026-07-19 18:07:12 +01:00
Shakker
0d511d1a67 chore: remove unrelated run failure changes 2026-07-19 18:07:12 +01:00
Shakker
d2a4025d1f test: cover chat run error recovery 2026-07-19 18:07:12 +01:00
Shakker
25b1c6c400 fix: show run failures above the composer 2026-07-19 18:07:12 +01:00
vyctorbrzezowski
86b1961bcc fix: show run failures above composer 2026-07-19 18:07:12 +01:00
Peter Steinberger
197c4f5a04 feat: generic session discussion seam with Control UI panel (#111337)
* feat: add session discussion panel seam

* fix: keep discussion iframe cookie-capable and changelog release-owned

* test: cover cookie-capable discussion iframe sandbox

* fix: stretch discussion panel host so the embed fills the rail

* fix: keep provider failures retryable and probe discussion availability before showing the action

* fix: block same-origin discussion embeds, show action on catalog sessions, close stale panel on reconnect

* fix: scope discussion probes and panel callbacks to the issuing connection

* fix: dedupe in-flight discussion probes per session

* fix: retry superseded discussion probes and key-scope panel results

* fix: regenerate Swift protocol models and extend advertised-method expectations

* style: format chat-pane-header

* fix: regenerate Kotlin protocol models and date discussion methods in the 2026.7 train

* chore: restore release-owned changelog to main state

* chore: keep changelog untouched relative to merge-base
2026-07-19 09:47:47 -07:00
Peter Steinberger
f8e35afecc feat: avatar upload UI, self-service profiles, server-side avatar route (#111421)
* feat: avatar upload UI, self-service profiles, server-side avatar proxy

* refactor(ui): make resolveAvatar synchronous so avatar updates render immediately

resolveAvatar no longer does async work (gravatar moved server-side), so
the until()-deferred render left the identity chip img stale after
updateSelfUser. Rendering synchronously reuses the img element and
reflects avatarUrl changes on the next render.

* fix(gateway): bound avatar Gravatar lookup to one timeout budget

Linked-email Gravatar lookups ran sequentially, each with its own 5s
timeout, so an upstream outage stalled the held connection for
5s x linked-email-count. Resolve them concurrently and pick the first
hit in email order, keeping primary-email precedence while capping the
request to a single timeout budget.

* fix(gateway): cap per-profile Gravatar fan-out to bound sockets and memory

A profile with many linked emails would fan out one concurrent fetch per
email with no bound, each able to buffer up to MAX_GRAVATAR_BYTES before
the cache budget applies. Cap the lookups to the first 8 primary-ordered
emails so a single avatar request holds a bounded number of sockets and
transient bytes.

* test: reconcile avatar tests with server-side gravatar model after merge

main landed a client-side Gravatar path (browser computes the gravatar.com
URL); this branch resolves avatars through the same-origin gateway route,
which is the only approach that works under the Control UI CSP (img-src
'self'). Update the identity-section, app-sidebar footer chip, and
profile-page e2e expectations to assert the canonical /api/users/<id>/avatar
route (gateway serves the Gravatar fallback behind it) instead of a
CSP-blocked direct gravatar.com image.

* test(gateway): fix fetchImpl mock param type for check-test-types

The Gravatar mock typed its param as (url: string), narrower than the
fetchImpl signature (URL | RequestInfo); function-param contravariance
made it unassignable under tsgo:test. Widen to the fetch input type and
extract the URL via a small helper that avoids no-base-to-string on a
Request.

* fix(gateway): short-circuit Gravatar lookups and version presence avatar URL

Two autoreview findings on the avatar path:

- Privacy: the concurrent Promise.all fan-out queried every linked email's
  hash against Gravatar even when the primary already had one, exposing
  secondary work/personal addresses. Resolve sequentially with short-circuit
  so a later hash is disclosed only after the earlier email is a definite
  miss, under one shared request deadline that still bounds total latency.

- Staleness: presence published an unversioned /api/users/<id>/avatar, so a
  reconnecting viewer's <img> kept the stale cached image after an upload.
  Carry the profile revision as ?v=<updatedAt> so a changed avatar refetches.
2026-07-19 08:52:01 -07:00
Peter Steinberger
1ef99e6dfb feat(agents): authoritative write diffs end to end (#111456)
* feat(agents): authoritative write diffs end to end

The write tool now reports what actually happened: no-op (changed:false),
created files (created:true with a bounded numbered diff), and overwrites
(created:false with a real old-to-new diff when the old content is
readable text within byte, line, and edit-distance budgets — unknown
removals are never fabricated). The gateway history projection forwards
the changed/created flags, the web and iOS write rows prefer the
authoritative diff and suppress the +N -0 stat when creation is not
proven, and the iOS transcript cache persists one bounded apply_patch
envelope so patch diffs survive cold opens.

* chore(i18n): sync native inventory
2026-07-19 08:48:26 -07:00
Peter Steinberger
8c7f09a3c4 feat(system-agent): durable rolling transcript with openclaw.chat.history (#111440)
* feat(system-agent): durable rolling transcript with openclaw.chat.history

* chore(ui): scanner-safe auth root in custodian session ownership key

* fix(system-agent): durable reset boundary for transcript seeding

* fix(ui): ownership-aware custodian session rotation with capability gating

* fix(ui): bounded history load and reconnect-safe transcript refresh

* fix(ui): keep unknown-outcome warning and event nudges across transport transitions

* fix(ui): flag-backed unknown-outcome warning independent of retry state

* fix(ui): armed ownership boundaries and abandon-before-rotate ordering

* chore(ui): localize the server-side sensitive mask in durable history

* fix(ui): repair rebase-dropped import and mock arity

* chore: gate fixes — void history handler, module-local transcript internals

* refactor(gateway): leaf history-turn contract to keep the module graph acyclic

* chore(gateway): stamp openclaw.chat.history with its introduction train

* test(gateway): record openclaw.chat.history in the 2026.7 train pin
2026-07-19 08:37:00 -07:00
Peter Steinberger
783a5d21cf refactor(config): purge numeric tuning knobs behind built-in defaults (#111382) 2026-07-19 07:35:45 -07:00
Peter Steinberger
ec01949d86 feat(ui): gravatar fallback in shared avatar primitive (#111418)
* feat(ui): gravatar fallback in shared avatar primitive

* fix(ui): expire gravatar negative cache instead of page-lifetime poisoning

* docs(ui): name gravatar retry tradeoff at the negative cache

* fix(ui): scope avatar failures to current viewer
2026-07-19 07:12:11 -07:00
Peter Steinberger
0cc182844a fix(ui): refresh agent roster on config changes (#111395) 2026-07-19 06:23:46 -07:00
Peter Steinberger
c95a8e3df1 feat(ui): identity settings and own-identity chip (#111371)
* feat(ui): identity settings and own-identity chip

* feat(ui): resolve own identity via users.self

* fix(ui): rebase identity settings onto landed identity stack

* fix(ui): footer identity chip prefers locally updated self profile

* fix(ui): settings CI conformance — split profile page, lean exports
2026-07-19 04:14:13 -07:00
Peter Steinberger
3b84a55d99 refactor(protocol): pre-publish cheat-window cleanup and vintage tracking (#111041)
* fix(codex): drain dynamic-tool handlers before side-thread cleanup

* refactor(protocol): rename question ids to questionId and flatten answer maps

* refactor(protocol): slim worker stack and unify session-catalog shapes

* refactor(protocol): delete dead public surface and polish packaging

* feat(protocol): track release-train vintage on gateway methods and schemas

* fix(apps): align question surfaces merged from main with reshaped protocol

* test(health): shape secret fixtures to scanner-safe token names

* test(health): use scanner-safe token fixtures

* fix(apps): align question surfaces merged from main with reshaped protocol

* fix(ci): prove reshaped protocol in shallow checks

* fix(ui): align sidebar question fixtures with protocol

* fix(apps): read flat Swift question answers

* fix(apps): align question surfaces merged from main with reshaped protocol

* fix(apps): refresh native question inventory

* fix(apps): align macOS snapshot fixtures with protocol

* fix(ui): align narration question fixture with protocol
2026-07-19 04:07:15 -07:00
Peter Steinberger
6add21bf40 fix(ui): remove duplicate instanceId getter left by concurrent merges 2026-07-19 03:20:46 -07:00
Peter Steinberger
5c9916950e feat(gateway): watched sessions in presence with viewer facepiles (#111225)
* feat(gateway): expose watched sessions in presence

* fix(gateway): resolve concurrent watched-session rollbacks via in-flight state

* fix(ui): viewing CI conformance — lazy facepile, bindings, dead exports

* fix(ui): move viewer presence projection out of the startup chunk

* fix(ui): capture sessionKey before facepile filter closure
2026-07-19 02:53:19 -07:00
Peter Steinberger
b9fa1ffa27 feat(gateway): system change history RPC and Ask OpenClaw recent-changes panel (#111286)
* feat(gateway): system change history RPC and Ask OpenClaw recent-changes panel

* test(gateway): fix journal fixtures, split custodian page tests, refresh docs map

* chore: quiet lint on journal reader tests and custodian harness
2026-07-19 02:38:05 -07:00
Peter Steinberger
ba06fe1541 feat(ui): author avatar chips on user messages (incl. queued) (#111309)
* feat(ui): author avatar chips on user messages with gravatar and initials fallback

Renders a small author avatar on user-authored chat messages, including
queued/pending sends, so multi-user gateways show at a glance who wrote
which prompt. Resolution: profile avatar URL (when available) -> Gravatar
by SHA-256 email hash (d=404) -> deterministic initials with stable color.
Assistant/tool output stays unmarked, and messages without sender identity
render unchanged, keeping single-user gateways visually identical.

* fix(ui): clear avatar fallback state on image load; document gravatar tradeoff

* fix(ui): gate email-hash avatars behind a gateway proxy base URL

Without a same-origin proxy base the email tier is disabled outright, so
rendering a transcript can never disclose hashed sender emails or viewer
IPs to a third-party avatar host. The gateway-side proxy route (follow-up)
supplies the base and becomes the only avatar fetch path.

* fix(ui): restrict profile avatar URLs to same-origin relative paths

* fix(ui): validate profile avatar URLs with the URL parser, not string prefixes

Backslashes normalize to slashes and tabs/newlines are stripped by browser
URL parsing, so startsWith checks can be bypassed into cross-origin fetches.
Resolving against a probe origin and requiring the origin to survive is
parser-equivalent and closes the class.
2026-07-19 02:08:05 -07:00
Peter Steinberger
918f6615d0 feat(ui): Swarm progress widget — dot grid per swarm group (#110325) (#111297)
* feat(ui): Swarm progress widget — dot grid per swarm group (#110325)

* fix(ui): keep swarm renderer internals private
2026-07-19 01:19:56 -07:00
Peter Steinberger
430147fc89 feat(ui): approval UX overhaul — inline cards, fair queue, shortcuts, badges (#110989)
* feat(ui): overhaul approval experience

* fix(ui): guard approval shortcuts against key auto-repeat and keep scoped errors on enqueue

* fix(ui): arm approval shortcuts after focus settles and scope error clearing to the settled request

* fix(ui): approval hardening — modifier-chord shortcuts, pinned active card, badge fallback, quiet countdown

* fix(ui): scope approval errors per request

* style(ui): format approval queue test

* fix(ui): tolerate missing approval overlays

* perf(ui): lazy-load approval modal controls

* fix(ui): satisfy approval lazy-load guards

* style(ui): satisfy approval card lint
2026-07-19 01:18:09 -07:00
Peter Steinberger
3a5da4faa8 feat(ui): live agent activity subtitles on running sidebar sessions (#111221)
* feat(ui): live agent activity subtitles on running sidebar sessions

* refactor(ui): lazy-load narration controller off the startup chunk, unexport test-only symbols

* chore(ui): raise startup JS budget to 312 KiB for the lazy narration feature

* test(ui): restore real timers after narration controller cases
2026-07-19 01:08:06 -07:00
Peter Steinberger
0f95e66b7f feat(talk): add durable client voice sessions (#111216)
Live-append voice transcripts into the agent session and persist a per-agent SQLite call record across relay and client transcript paths.

Add run-scoped spoken confirmation for high-impact actions, mutation digests, bootstrap-context injection, talk.client.transcript and talk.client.close protocol methods, and Control UI adoption. This adds zero new configuration.

Co-authored-by: Clifton King <clifton@users.noreply.github.com>
2026-07-19 01:06:49 -07:00
Peter Steinberger
9d2f5d4dbb feat(ui): enable Swarm toggle in Labs (#110325) (#111267) 2026-07-19 00:27:06 -07:00
Peter Steinberger
9aa6e672a7 fix(custodian): show mode-appropriate guidance and defaults (#111263)
* fix(custodian): align option-card defaults

* test(custodian): preserve default confirm choice
2026-07-19 00:17:09 -07:00
Peter Steinberger
aae5b0f041 feat(ui): hold-to-record dictation on the composer mic button (#111114)
* feat(ui): add hold-to-record dictation

* chore: drop changelog edit (release generation owns CHANGELOG.md)

* fix(ui): preserve composer controls after rebase

* fix(ui): preserve dictation pointer capture

* fix(ui): finish dictation on cancellation

* docs(ui): note transcription-relay event envelope contract at the dictation parser

* test(ui): complete realtime-talk-input mock factory for shared-registry workers

* test(ui): isolate composer mocks across workers

* test(ui): isolate shared chat module mocks

* test(ui): type shared chat spies
2026-07-19 00:03:09 -07:00
Alix-007
aaacee8166 fix(ui): prevent stale sidebar attention refreshes (#111061) 2026-07-18 23:37:20 -07:00
Peter Steinberger
5e4a324f22 fix(ui): report WhatsApp logout no-ops (#105929)
* fix(ui): report WhatsApp logout no-ops

* test(ui): align WhatsApp logout E2E with channels hub
2026-07-18 23:22:22 -07:00
Alix-007
2320f338bf fix(ui): recover stalled Nostr profile requests (#111093)
* fix(ui): bound Nostr profile requests

* fix(ui): clarify Nostr timeout outcome

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-07-18 23:16:25 -07:00
Peter Steinberger
6eb569ed6e fix(ui): chat message copy and canvas buttons render as boxed buttons in light mode (#111230)
The copy-as-markdown and open-in-canvas actions under chat messages carried
the global .btn chrome; the light-theme .btn background override outranks the
chat footer's ghost-button reset, so both buttons rendered as large boxed
squares (44px touch targets made them worse on narrow viewports) while the
hide button next to them stayed a small ghost icon. Render them bare in chat
so the footer ghost rules own their look; the boxed variant remains for the
file preview and connect-command surfaces.
2026-07-18 22:58:48 -07:00
Peter Steinberger
932e8be06c feat(agents): canonical agent creation with Custodian hatch flow (#111052)
* refactor(agents): centralize agent creation

* feat(ui): add new agent hatch flow

* refactor(agents): keep creation paths lean

* fix(agents): hide internal creation types
2026-07-18 22:50:02 -07:00
Jason (Json)
2f7da3057c feat(mcp-apps): support bounded model context updates (#111212)
* feat(mcp-apps): support model context updates

* fix(mcp-apps): satisfy protocol and runtime guards
2026-07-18 23:08:35 -06:00
Peter Steinberger
721aee1f19 feat(gateway): attribute prompts to authenticated users (#111207) 2026-07-18 21:37:57 -07:00
Jason (Json)
060b30f23a fix(ui): ignore malformed tool stream entries (#111145)
Prevent malformed transient tool-stream entries from aborting Control UI chat reconciliation while preserving valid tool message order and rendering.
2026-07-18 22:21:11 -06:00
Peter Steinberger
c2a9579c59 fix(ui): composer small-width polish — single-line placeholder, matched talk split-button (#111150) 2026-07-18 20:06:52 -07:00
Jason (Json)
7d62a277ff fix(ui): teardown MCP Apps before unmount (#111124)
* fix(ui): teardown MCP Apps before unmount

* fix(ui): keep MCP teardown helpers internal
2026-07-18 20:25:46 -06:00