Commit Graph

19 Commits

Author SHA1 Message Date
Peter Steinberger 053b89d80f improve(ui): open short session links without extra lookup (#128778)
* perf(ui): remove short session route waterfall

* docs: clarify short link gateway requirement
2026-08-24 09:33:20 -07:00
Peter Steinberger 8fe2c1b83c perf(ui): consolidate Control UI boot chunk graph for HTTP/1.1 gateways (#128514)
* perf(ui): consolidate Control UI boot chunk graph for HTTP/1.1 gateways

The Control UI boot flow (app shell + sidebar + chat route) lazily loaded
~124 automatic chunks in one burst after the gateway handshake, which the
gateway's HTTP/1.1 transport serializes into ~24 six-connection round-trips
on high-latency links (Tailscale, remote gateways).

Add a measured boot-module manifest (ui/config/control-ui-boot-modules.json,
regenerated via pnpm ui:boot-manifest:gen) and a control-ui-boot codeSplitting
group that merges exactly that module set into a handful of chunks with
recursive dependency inclusion. Lazy islands (locales, ghostty-web, novnc,
non-default routes) keep their own chunks; stale manifest entries degrade
gracefully back to automatic chunking.

Measured on the built dist with the mocked gateway (chat route, 3 runs):
unique boot JS requests 140 -> 45, raw boot JS 3751 -> 3717 KiB, chat
composer interactive at simulated 50 ms RTT ~1600 ms -> ~575 ms.

Largest-CSS budget rises 45 -> 47 KiB for the merged boot CSS; startup JS
gzip baseline ratchets down (345049 -> 339214 B) as consolidation shrinks
the startup graph.

* chore(ui): refresh boot module manifest after rebase onto current main

* fix(ui): stop the pending lazy shell action replay loop starving boot

When a pending lazy shell action (command palette open, panel toggle)
replayed while the shell was still splash-gated, the dispatched event had
no rendered element to consume it and re-entered requestLazyElement in a
microtask cycle: request -> load -> replay -> dispatch -> request. The
cycle starved tasks (Gateway WebSocket messages included), so the boot
never finished and the recovery e2e froze on the splash screen.

Gate replay on the element actually being rendered: the controller skips
the action after load until the host's render root contains the tag, and
restorePendingLazyAction skips dispatch while a defined element is still
render-gated. The host retries after every completed update, so the replay
fires on the update that first renders the element. Regression test fails
on the pre-fix controller.

* fix(ui): re-anchor the scope-upgrade details popover before opening

wa-popover resolves its `for` target once per property change and never
re-resolves a missing or replaced anchor. The trigger with the shared id
can render after the popover's first update (the header trigger ships with
the lazy chat chunk), leaving the opened popover permanently invisible:
active popup with a native [popover] part stuck at UA display:none because
showPopover() never ran without an anchor. Re-arm the watcher when opening
while the anchor is missing or disconnected.

* test(ui): compare settled layouts in device-scope stability assertions

The 0.5px no-move assertions sampled geometry that later reflowed when the
details surface's first render fetched glyph subsets, reporting sub-pixel
drift the open never caused. Burn in the one-time open per context and
sample the baseline adjacent to the click.

* fix(ui): map the keyboard shortcuts dialog in lazy replay gating

Current main added the keyboard-shortcuts lazy shell event; the replay
gate's exhaustive event-to-element record needs its entry.

* chore(ui): refresh startup budget baseline after rebase onto current main
2026-08-24 03:55:37 -07:00
Peter Steinberger e879376f18 feat(control-ui): Cmd+/ keyboard shortcuts overview from a declarative catalog (#128563)
* feat(control-ui): add Cmd+/ keyboard shortcuts overview dialog

A declarative shortcut catalog (keyboard-shortcut-contract/catalog) now owns
every Control UI chord: matching, platform-aware formatting, and the new
lazy-loaded Cmd+/ (Ctrl+/ elsewhere) overview dialog all derive from it, so
the overlay cannot drift from real handler behavior. Migrates the scattered
hardcoded shortcut labels (palette, debug overlay, appearance, sidebar,
terminal, workspace) and the duplicated Apple-platform check onto the catalog,
which also makes the previously meta-only sidebar and workspace chords
reachable on non-Apple platforms. The Slash matcher keeps Latin layouts that
put another printable on the physical Slash key (German Cmd+'-' zoom) intact.

* feat(control-ui): list non-obvious shortcuts in the Cmd+/ overview

Adds the invisible-but-real shortcuts to the catalog: composer history
recall (up/down), Esc dictation cancel, new-session-page submit (follows
the same chatSendShortcut preference as chat send), image-viewer zoom
(+/-/0), and the sidebar multi-select mouse chords (mod+Click,
Shift+Click) via a display-only Click key token. Multi-combo rows now
group their chips on the right instead of spreading across the row.

* fix(control-ui): keep Ctrl+Shift+Backquote toggling the terminal

The catalog matcher's exact-Shift comparison rejected the shipped
Ctrl+Shift+` terminal chord that main's predicate accepted (Shift was
deliberately unconstrained for layouts where the Backquote key is
shifted). Backquote now joins '/' as a Shift-insensitive key, with a
regression test. Found by ClawSweeper review on #128563.
2026-08-24 00:26:56 -07:00
Peter Steinberger bbbd70542b feat(sessions): recover offline device placements (#126284)
* feat(sessions): recover offline device placements

* chore(protocol): refresh session placement models

* perf(ui): lazy-load session placement recovery

* test(ui): remove dropdown timing assertion

* test(ui): await committed cloud recovery route

* fix(sessions): complete explicit abandonment locally

* fix(sessions): fence lists by runner availability

* fix(ui): preserve canonical session freshness

* fix(sessions): preserve recovery contracts after rebase

* test(ui): await durable cloud recovery entry

* test(gateway): complete current runner fixtures

* fix(gateway): publish runner availability edges

* fix(ui): preserve shared session freshness

* fix(ui): preserve canonical sidebar session state

* fix(sessions): preserve abandoned partials and run-owned replies

* fix(sessions): resume durable abandonment retries

* test(gateway): compose provisioning replay with runner availability

* fix(sessions): publish recovered move transitions
2026-08-20 09:59:59 -07:00
Peter Steinberger 5eda48ed99 fix(ui): recover failed lazy surfaces (#126409) 2026-08-19 12:27:35 -07:00
Peter Steinberger 75bcc5cebe fix(ui): keep Control UI device identity working on plain-HTTP origins (#124724)
* fix(ui): keep Control UI device identity working on plain-HTTP origins

@noble/ed25519 defaults its SHA-512 provider to crypto.subtle, which
browsers gate to secure contexts, so device identity silently vanished
on http:// LAN dashboards and connects fell back to shared-credential
auth with no pairing. Wire a lazy pure-JS @noble/hashes fallback for
SHA-512 and the fingerprint SHA-256, and drop the isSecureContext gate
in the connect path. Secure contexts keep the platform digests and pay
no startup bytes: the fallback loads as its own lazy chunk, kept out of
the gateway-runtime startup chunk on purpose.

* test(ui): cover device identity minting and signing without crypto.subtle

New jsdom regression suite fails on pre-fix code (subtle-less crypto stub
with getRandomValues, which real insecure contexts keep). Rewrites the
gateway connect tests that previously asserted the device-less insecure
fallback: an insecure context now attaches a device identity.

* docs(web): plain-HTTP dashboards now pair with a device identity

The signing key never crosses the wire, so HTTP+pairing is strictly
stronger than the old HTTP token-only fallback; HTTPS (Tailscale Serve)
stays the recommendation for transport privacy.

* fix(ui): drop unnecessary boolean literal compare in secure-context timing meta

* test(ui): declare device.id on the connect-frame test shape

* test(ui): split the subtle-less scope-upgrade e2e into the two real invariants

Without crypto.subtle the browser can now sign, so the banner offers the
explicit admin upgrade; manual-only guidance is reserved for browsers that
cannot mint an identity at all (no WebCrypto RNG). Also corrects the
connect-path comment: blocked storage yields an ephemeral identity, only a
failed mint degrades device-less.

* fix(ui): address review findings on the HTTP device-identity path

- Storage-blocked pages keep one stable in-memory identity per page
  lifetime instead of minting a fresh unpaired key on every reconnect,
  and a write-rejecting store no longer fails the mint (regression tests
  bite pre-fix).
- Connect timing now reports the real browser secure-context fact via a
  shared browserSecureContext() helper instead of inferring it from
  device-identity presence.
- Docs state the accepted trusted-proxy contract: browsers attach a
  device identity on every origin, so first connects follow the standard
  pairing flow (deviceAutoApprove or a one-time approval); device-less
  admission remains only for browsers that cannot mint an identity.

* refactor(ui): trim the connect-path additions under the max-lines cap
2026-08-16 12:20:00 -07:00
Peter Steinberger bfe1f33ea0 improve(ui): make Control UI feel native on mobile (#122492)
* improve(ui): make Control UI feel native on mobile

* fix(ui): keep coarse-pointer input floor text-scale aware

* fix(ui): let self-sized controls opt out of the touch input floor

* fix(ui): fold per-control coarse-pointer font floors into the shared touch floor
2026-08-12 00:46:35 -07:00
joshavant 5f7ea6b548 fix(ui): lazy-load run inspector copy 2026-08-11 18:44:38 -05:00
Peter Steinberger cad77fb39c refactor: consolidate remaining coercion helpers (#122020) 2026-08-11 10:22:01 -07:00
Peter Steinberger 7e78de747b refactor(ui): replace duplicated locale bundles with canonical translation memory (#117157)
* refactor(ui): generate locale bundles from canonical translation memory

* fix(ui): keep server-only locale config out of browser test helpers
2026-07-31 19:40:27 -07:00
Peter Steinberger 85fda04df7 feat(ui): surface queued-outbox counts in composer hint, session rows, and offline footer (#112649)
* feat(ui): surface queued-outbox counts in composer hint, session rows, and offline footer

Follow-up to the offline-state unification: queued sends were mechanically
safe (durable outbox with reconnect replay) but only visible inside the
open session's thread.

- Composer offline hint now includes the visible session's queued count.
- Session rows show a clock badge with the per-scope outbox count,
  independent of connection state (covers waiting-idle and failed too),
  with alias-safe scope resolution so agent-main never double-counts.
- The offline footer button appends the aggregate ("· N queued"); the
  connected state stays completely silent.
- One narrow subscription seam (subscribeStoredChatOutboxChanges) added
  in composer-persistence; no send/drain logic touched.

* perf(ui): keep the startup bundle under budget with a lean outbox read module

The queued-count feature statically imported composer-persistence from
startup modules, hoisting the chat page's persistence machinery into the
startup chunk and breaking the Control UI startup JS gzip budget
(319.6 KiB > 314.0 KiB limit).

Split ownership instead of gaming the budget: a lean read/subscribe
module (ui/src/lib/chat/outbox-store.ts + codec/draft-state) serves
startup consumers (app-host, sidebar), while writes, migrations, and
drain stay in the lazy chat chunk (composer-outbox-store/composer-storage);
composer-persistence keeps its export surface for chat callers. Startup
is back to 313.8 KiB gzip at 12 requests with no chunking-config changes.

Also fixes an autoreview finding in the new summary: legacy bare-main
outbox rows now resolve through session defaults (online) or the
persisted mainAlias (offline reload) instead of trusting the row's stale
embedded agent id, so badge counts key to the same scope the sidebar
resolves. The shared footer status renderer is deduplicated into
session-row-badges.

* fix(ui): correct type-only import and const tuple in outbox split

* perf(ui): idle-load the outbox summary so startup carries no outbox code

The lean outbox read module still cost ~2.8 KiB of startup gzip against
1.5 KiB of budget headroom. Follow the sidebar chrome pattern
(lobster-pet/facepile): app-host idle-loads outbox-store, subscribes on
arrival, and passes the sidebar a resolver callback instead of letting
startup modules import scope resolution. Badges and counts hydrate
moments after load; before that the summary is empty by design.

Failed chunk loads recover on browser online events and, because chunks
are usually served by the gateway itself, on gateway reconnect — the
exact moment the offline badges become relevant again.

Raise the initial-graph packing ceiling 448->512 KiB: the grown core
graph split at the old boundary into an extra chunk, costing ~1.9 KiB of
startup gzip to compression-context resets (same documented tradeoff as
the earlier 400->448 bump). Startup lands at 313.5 KiB gzip / 9 requests,
matching the origin/main baseline, limit 315.0.
2026-07-22 10:53:22 -07:00
Peter Steinberger f0c43dcf72 feat(ui): format international phone numbers for display (#112400)
* feat(ui): format phone numbers for display

* ci: track normalization package exports in Knip
2026-07-21 17:10:25 -07:00
Peter Steinberger cad4e395d2 perf(ui): cut forced reflows in Control UI chat render path (#110472)
* perf(ui): cut forced reflows in chat render path

Profiled the Control UI with Chrome DevTools tracing against a real-data
gateway: session-switch INP was 367ms with ForcedReflow insights on every
load and interaction trace.

- chat-thread: per-row stable Lit ref callbacks (keyed by row key) so the
  virtualizer stops cache-sweeping and re-measuring every visible row on
  every render; prune callbacks when rows leave the list
- chat-composer: stable textarea ref on per-pane state instead of an
  inline arrow, so the textarea is re-measured only on attach or when the
  draft changes programmatically, not on every chat render
- app-sidebar: coalesce scrollHeight/scrollTop reads from updated() into
  one rAF per frame instead of a forced layout flush per render

After: INP 133-143ms on the same interaction sequence, no ForcedReflow
insight in load or interaction traces.

* perf(ui): keep Control UI startup under budget after reflow fixes

The reflow fixes shifted rolldown's chunk partition: the 400 KiB core
maxSize boundary split one core chunk in two (~1.4 KiB gzip compression
loss) and re-balancing minted a tiny build-info startup chunk, pushing
startup JS to 370.8 KiB over the 370 KiB budget.

- pin build-info.ts + build-info-normalizers.ts into control-ui-shared so
  partition noise stops minting extra startup preload requests
- raise core maxSize 400 -> 448 KiB so the core graph packs into fewer,
  better-compressing chunks

Startup JS: 22 requests, 369.1 KiB gzip (main: 23 requests, 369.3 KiB).
2026-07-18 08:04:03 +01:00
Peter Steinberger cf8b57e7d0 feat(skills): scan session history for workshop ideas (#106766)
* feat: scan past sessions for skill proposals

* feat(ui): add progressive skill history scans

* fix(ui): keep skill history scans synchronized

* refactor: split skill history scan ownership

* style: fix mock helper formatting

* style: format skill history scan

* build: refresh skill history schema baselines

* build: refresh plugin SDK baseline after rebase

* perf(ui): keep startup request budget bounded

* fix(skills): satisfy history scan integration gates

* fix(ui): bound control ui startup chunks

* build: refresh plugin SDK API baseline

* build(ui): refresh self-learning translation memory

* refactor(ui): split skill workshop state

* fix(ci): refresh skill workshop gates

* fix(ci): satisfy skill history lint

* build: refresh plugin SDK baseline after main rebase
2026-07-13 16:15:50 -07:00
Peter Steinberger 995c75baa0 fix(ci): restore current-main quality gates (#106751)
* fix(ci): restore Control UI quality gates

* fix(ci): repair dead export cleanup fallout

* fix(ci): repair latest main quality gates

* fix(ci): satisfy current-main gate contracts

* docs: format Cloud Workers guide

* test: isolate context lookup module
2026-07-13 13:54:03 -07:00
Vincent Koc 7e995cbf2b refactor(ui): remove stale helper exports (#101549) 2026-07-07 02:49:42 -07:00
Vincent Koc 62e5d4406d refactor(ui): remove unused module exports (#101262) 2026-07-06 20:20:06 -07:00
Peter Steinberger 12a56d4d46 docs: document control ui sources 2026-06-04 20:57:09 -04:00
Vincent Koc 968c87d798 fix(ui): move control ui chunking helper out of runtime source 2026-05-25 14:31:38 +02:00