Commit Graph

563 Commits

Author SHA1 Message Date
Peter Steinberger 6e82723125 fix(ui): keep reconnect-uncertain sends inline and prove delivery by run id (#131171)
After a reconnect lost the chat.send ack, the outbox reconcile only accepted
the transcript idempotency marker as delivery proof, so a send whose run was
actively streaming parked as a red 'Delivery uncertain' composer chip for the
whole run. The reconcile now also retires a queued send when the session row
reports its run id in activeRunIds or as lastRunId — the same fact live
terminal-event retirement already trusts.

Attempted unconfirmed messages now stay in the conversation as ordinary
bubbles with an amber 'Delivery unconfirmed · Retry' footer (failed keeps the
red 'Not sent'); the duplicate pane banner is suppressed for them since the
bubble owns the outcome. Command chips, never-attempted failures, and storage
errors keep their existing surfaces. Calmer copy for the remaining honest
uncertainty case.
2026-08-27 13:57:27 -07:00
Peter Steinberger 24d93f26a4 fix(ui): preserve session identity through rename and group actions (#130936)
* fix(ui): reject stale session renames after replacement

* fix(ui): preserve selected session identity through group actions
2026-08-27 11:17:44 -07:00
Vyctor H. Brzezowski 049f4940b9 fix(ui): keep effort in the composer row (#130788)
* refactor(ui): keep effort out of model picker

* fix(ui): restore mobile composer effort trigger

* fix(ui): keep effort reachable with animated mobile gauge

Preserve separate model and effort controls across chat, New session, and narrow desktop split panes. Let long model names ellipsize and remove the obsolete model-menu bridge and focus fallback.

Co-authored-by: vyctorbrzezowski <krzyszchweski@gmail.com>

* test(ui): expect model-only loading labels

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-08-27 02:09:15 -07:00
Peter Steinberger 84484c1ae3 fix(ui): keep chat messages separated after scroll interruptions (#130758)
* fix(ui): keep chat messages separated after scroll interruptions

* refactor(ui): place transcript types with their owners

* fix(ui): publish settled transcript scroll position
2026-08-27 01:44:42 -07:00
Peter Steinberger fa19150155 feat(ui): give every theme family its own typeface and refine palettes (#130767)
* chore(ui): ship self-hosted webfont assets for theme families

Latin and latin-ext woff2 subsets of Instrument Sans, Geist, DM Sans,
Fraunces, and IBM Plex Sans, fetched unmodified from Google Fonts, with
their SIL OFL 1.1 license texts beside the files.

* feat(ui): give every theme family its own typeface and refine palettes

Claw ships Instrument Sans (the declared-but-never-shipped Inter token is
gone), Knot ships Geist, Dash pairs DM Sans with Fraunces chat prose, and
Tide ships IBM Plex Sans; Absolutely, Beacon, and Phosphor keep their
existing faces. Every built-in family now derives fonts/<theme>.css, so
the per-theme map is gone; imported custom themes stay on the system stack.

Palette repairs: Knot's invisible maroon second accent becomes steel
silver (2.0 -> 10.6:1) with slightly structural borders; Dash's muddy
brown accent becomes toasted caramel (5.0 -> 6.55:1) with a gold-cream
second voice; Knot and Tide dark body text move from 15.2/14.2:1 into
the ~10-12:1 dark comfort band (11.8/12.0:1). Light variants mirror the
changes; theme-picker chips and docs follow.
2026-08-27 00:44:07 -07:00
RoboClaw fa16a4f80e fix(ui): remove context compact button (#130775)
* fix(ui): remove context compact button

* fix(ui): remove context compact button

OpenClaw-Publication: 37fcd8f7-5da2-4fe3-afcf-c77609e0ab33

* fix(ui): remove context compact button

OpenClaw-Publication: 8779879a-59ce-40a4-a6ea-26a4f468f1ca

---------

Co-authored-by: roboclaw-bot <309084314+roboclaw-bot@users.noreply.github.com>
2026-08-27 00:30:17 -07:00
Peter Steinberger 958111c03f fix(ui): expose complete cloud failure diagnostics (#130672) 2026-08-26 22:22:41 -07:00
Peter Steinberger 8f237b6a97 fix(ui): show cloud session startup progress in chat (#130669) 2026-08-26 21:13:04 -07:00
Peter Steinberger a872abc978 fix(ui): preserve startup errors across session navigation (#130593) 2026-08-26 20:03:43 -07:00
Peter Steinberger 31b26d09f0 feat(ui): group advanced settings by messaging channel (#130591)
* feat(ui): group advanced settings by messaging channel

* test(ui): connect channel settings fixture for label checks
2026-08-26 19:40:34 -07:00
Peter Steinberger c35b183093 fix(ui): highlight source code in chat and session diffs (#130578)
* fix(ui): highlight source code in chat and session diffs

* test(ui): isolate roster bootstrap from slug lookup

* fix(ui): preserve both languages in renamed diffs
2026-08-26 19:28:03 -07:00
Peter Steinberger a4c01c1a77 feat(ui): save appearance preferences per user profile (#130340)
* feat(ui): save appearance preferences per user profile

When a Control UI connection is bound to an authenticated user profile
(trusted-proxy, Tailscale, GitHub identity), theme, theme mode, and accent
become per-profile: stored in the existing user_preferences KV store via
users.prefs.set, overriding gateway-wide ui.prefs, with live cross-device
updates over a new additive users.prefs.changed event scoped to the same
merged profile. Restore default deletes only the profile key and falls back
to the gateway-wide value. talk.config projects the caller's profile accent
for native clients. Profile-less connections (token/password/none auth)
keep the existing gateway-config behavior byte-identically.

Release-note context: on multi-person gateways appearance choices are now
personal and follow you across devices; an admin restyling their own UI no
longer reskins the whole team.

* chore(protocol): allowlist users.prefs.changed for mobile clients

iOS and Android resolve the profile accent through talk.config on connect
and config refresh; live per-profile appearance push on natives is a named
follow-up.

* chore(protocol): regenerate Swift/Kotlin models for users.prefs.changed

* fix(ui): honor explicit defaults and identity switches for profile appearance

Addresses the three ClawSweeper review findings:
- resetValue for profile-bound appearance keys is the deletion fallback
  (gateway value), so an explicit product-default selection persists as a
  profile write instead of being misclassified as a reset.
- An identity switch between two known scopes forces a full appearance
  reconcile and clears appearance keys the returning identity never set,
  so a shared browser cannot keep the previous profile's look (boot keeps
  the last-seen shortcut).
- talk.config joins the profile-dependent dispatch gate so a pending
  GitHub identity sync completes before the profile accent is projected;
  token clients pass through untouched.

* chore(ui): satisfy assertion-safety ratchet and docs formatting

* refactor(ui): extract server-prefs storage primitives

server-prefs.ts crossed the 700-line cap after the review fixes; the
stateless localStorage persistence primitives move to
server-prefs-storage.ts (no behavior change). Shrink-prunes the
assertion-safety baseline for the moved casts.

* fix(ui): keep imported custom themes out of profile storage

Custom palettes are browser-local by contract, so a profile must never
carry theme=custom to a browser that cannot render it. The wire contract
drops custom from the storable theme values (readers self-heal any stored
value), a profile-bound custom selection stays retained browser-local
(including the offline-queued path), and the exhaustive theme test now
encodes the exception.
2026-08-26 16:48:26 -07:00
Marvinthebored ce4a680544 feat(models): add configurable model selection scopes (#127813)
* feat: make model persistence scope explicit

* fix: keep unauthorized model directives as plain text

An unauthorized sender's inline directives are cleared to plain text before
they are acted on, but the persistent write target was derived from the
directives as they arrived. An unauthorized `/model <provider/model> -a` or
`-g` therefore reached the owner-authority error return instead of the
plain-text path every other directive takes.

Derive the target, the authority flag, and the persist flag after the
clearing. Authorized senders skip the clearing, so their behavior is
unchanged; the only closure that captures the target is invoked well after
the new declaration site.

Covers both persistent flags with a regression test that fails on the prior
head with "Agent and global model defaults require owner authority or
operator.admin scope." and passes here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(models): make model selection scope configurable

Preserve current model-selection defaults when the optional scope preference is unset. Add explicit session, agent, and global overrides without broadening config-write authority.

Co-authored-by: Marvinthebored <peter@lindsey.jp>

* test(models): simplify scope expectation fixtures

---------

Co-authored-by: Marvinthebored <peter@lindsey.jp>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-08-26 14:31:56 -07:00
Peter Steinberger f0291d7e5d fix(ui): show dashboard refresh failures with a retry action (#130376)
Keep cached dashboards visible when their session-list refresh fails, show the existing stale-data notice, and retry the current agent query. Cover connected refresh failures, first-load recovery, and retired scope callbacks.
2026-08-26 14:01:58 -07:00
Peter Steinberger 1fc29beba2 refactor(state): fold singleton tables into config_machine_state at schema v12 (#129876)
* refactor(state): fold singleton tables into config_machine_state at schema v11

Eight singleton tables (skill_curator_state, update_check_state,
clawhub_promotions_feed_state, model_catalog_remote, voicewake_triggers,
voicewake_routing_config, voicewake_routing_routes,
onboarding_recommendations) were each one logical JSON value behind a
fixed key; their bespoke schemas, lazy ensures, and per-table accessors
collapse onto the shared config_machine_state KV under namespaced keys.
cron_store_epochs retires outright: it was born write-only in #114388
and no reader ever existed in any language. Durable values (update
check state, voicewake triggers and routing, per-workspace onboarding
answers) migrate insert-if-absent during the v10->v11 migration; cache
class contents rebuild on next use. Deferred with named reasons:
exec_approvals_config (macOS direct-SQL contract), installed_plugin_index
(same-tx lease fence), node_host_config and web_push_vapid_keys
(secret-table git-backup redaction).

# Conflicts:
#	src/skills/workshop/collection-review-state.ts
#	src/skills/workshop/collection-review.gateway-admission.test.ts

* test: register v11 guard carve-outs and suppression pin

The v11 migration module joins the raw-SQLite allowlist (migrations are
the named guardrail exception), the lint-suppression allowlist records
the second type-parameter suppression in config-machine-state, and the
identity module keeps only externally consumed exports.

* test: surface CLI stderr when migration-diagnostic assertion fails

* test: expect migration diagnostics on stderr for models plain commands

The #129037 pending-migration cases asserted that aliases/fallbacks
lists never open the state database, but config-health observation
(observeConfigSnapshot -> readConfigHealthStateFromStore) full-opens it
on any config read whose file exists — reproduced identically on clean
main with a main-built dist. The protected contract is exact stdout;
the diagnostic legitimately lands on stderr for every case.

* test: drop unused defaults import from CLI stdout e2e

* test: split session path derivation out of oversized session-files suite

#130016 pushed session-files.test.ts to 1008 lines, over the 1000-line
lint cap and red for every PR's check-lint. The sessionPathForFile
describe moves to a self-contained sibling following the existing
session-files.*.test.ts split pattern; no assertions change.

* refactor(state): fold four more singleton tables into schema v12

tui_last_sessions (cache-class, regenerates on next session switch),
sidebar_sections (persistent section order, migrated as one JSON array),
node_host_config, and web_push_vapid_keys join the v12 fold-in, taking
the retirement to thirteen tables at the same version. The two secret
singletons were blocked on table-granular git-backup redaction; backups
now exclude config_machine_state rows by secret key prefix (nodeHost.*,
webPush.vapidKeys) with a fail-closed row filter and regression proof,
so STATE_SECRET_TABLE_NAMES sheds both tables. The sidebar fold also
retires its lazy-ensure WeakSet and inline DDL; sidebar edits stay
inside the existing session-group write transaction via direct Kysely.

* fix(node-host): omit absent Cloudflare Access config like the column reader

The KV rewrite returned gateway.cloudflareAccess as an own undefined
property where the retired column reader omitted the key; toStrictEqual
consumers (state-migrations doctor-repair test) caught the shape drift.
Mirror the column reader's conditional spread at both construction
sites.

* fix(backup): disclose redacted machine-state prefixes after restore

The prefix-granular secret redaction recorded omitted key prefixes in
the backup manifest but the restore result exposed only excludedTables,
so a redacted restore looked complete while nodeHost.* and
webPush.vapidKeys configuration were intentionally absent. The restore
result and CLI output now disclose the omitted prefixes (JSON mode
carries them via the result shape), with restore-side regression
coverage.

* fix(tui): compare-and-delete retired session pointers

Doctor cleanup read matching pointer keys then deleted them
unconditionally, so a replacement pointer written between the scan and
the delete was erased. The delete now re-checks the stored value inside
the write transaction and only removes pointers that still name a
retired session; a live replacement survives (regression covered).
Also corrects the stale schema-version line in database-first.md.
2026-08-26 13:32:19 -07:00
Peter Steinberger d9b41332e4 fix(ui): restore person grouping for multiple session owners (#130326)
* fix(ui): restore person grouping for multiple session owners

Use the live session-owner roster for Person grouping and People sorting instead of the connection-time identity policy. Preserve preferences while the roster is unknown and retain single-owner dormancy across sidebar and Sessions page.

* docs(ui): match the owner sorting label

* refactor(ui): reuse session owner and menu selection paths
2026-08-26 13:28:36 -07:00
Peter Steinberger 08a42a561c feat(control-ui): add the Tide, Beacon, and Phosphor themes (#130232)
* chore(control-ui): vendor JetBrains Mono and Atkinson Hyperlegible Next

Adds the woff2 faces for the Beacon and Phosphor themes plus the stylesheets
that declare them. Both families are SIL Open Font License 1.1 and ship
unmodified with their upstream license text, as the license requires.

Atkinson Hyperlegible Next was drawn by the Braille Institute for low vision:
its letterforms disambiguate the pairs that blur first at low acuity (I/l/1,
O/0, rn/m), which is why it belongs to the accessibility theme specifically.

base.css has always named JetBrains Mono first in --mono, but nothing shipped
it, so code blocks rendered in it only for people who happened to have it
installed locally. Phosphor makes that deterministic.

Latin and latin-ext subsets only, 195 KB. Served from the gateway rather than a
font CDN so font-src 'self' holds, no third-party request happens on load, and
an offline or LAN-only gateway still renders correctly. The url() references are
relative to their stylesheet so they follow a configured Control UI mount.

* feat(control-ui): add the Tide, Beacon, and Phosphor themes

Three built-in themes, each filling a gap the existing four share rather than
adding a fifth variation on them. Every shipped accent today is warm — coral,
crimson, chocolate, clay — and the only cool token anywhere is Claw's teal
secondary.

Tide is the cool one: steel cyan on deep slate, deep teal-blue on cool paper.
It deliberately ships no webfonts, so it costs nothing beyond its tokens.

Beacon is the accessibility one. It targets WCAG AAA (7:1) rather than the AA
4.5:1 floor the other themes hold, for low vision, direct sunlight, projectors,
and poor panels, and sets everything in Atkinson Hyperlegible Next. Reaching AAA
forced one deliberate break from the status-token convention: elsewhere each
--x-subtle is --x at 8%, but a label on a tint of its own hue caps achievable
contrast because the tint pulls the background toward the text. Beacon tints
neutrally and lets the label carry the contrast. Its focus ring is opaque and
3px for the same reason — a translucent ring is the first thing to vanish on a
bad panel. The contrast guardrail now enforces 7:1 for Beacon specifically, so a
later palette edit cannot quietly demote it to an ordinary dark theme.

Phosphor is the terminal one: phosphor green on green-cast black with the whole
surface, chat prose included, in JetBrains Mono. Monospace prose is a character
choice for an operator console, not a readability claim, which is why it is
opt-in.

Every palette was solved against the real guardrail before any CSS was written.
Worst text pairing per theme: Tide 6.12:1 dark / 4.63:1 light, Beacon 10.29:1 /
9.72:1, Phosphor 6.69:1 / 4.76:1. Worst status label on its own tint: Beacon
7.05:1 / 7.02:1, the others above 4.6:1.

Budgets: the startup CSS ceiling moves 45 -> 47 KiB and the JS ratchet baseline
moves 523 B. Every built-in theme's tokens ship in the startup sheet, so each
new theme costs ~0.5 KiB gzip whether or not anyone selects it. Moving per-theme
palettes to lazily linked stylesheets (as theme webfonts already are) would take
this back under the old ceiling, but it needs a first-paint story first: a late
palette flashes default colors where a late font only swaps.

* fix(control-ui): use Beacon's destructive foreground in chat confirmations

The chat confirmation button paints from --danger under --media-foreground, a
theme-invariant white, and each dark palette opts out of that pairing through a
selector list in chat/grouped.css. Beacon defines a deliberately light --danger
(#ffabab, chosen so status labels clear AAA) but was missing from that list, so
its confirm button rendered white on pale pink at 1.80:1 — on the theme whose
entire promise is contrast.

Adds Beacon to the list, which routes it to --destructive over
--destructive-foreground at 11.67:1.

Also adds a guardrail for the bug class, since the selector list has to be
extended by hand for every new dark theme and I have now missed it once. The
test reads membership back out of grouped.css rather than restating it, resolves
each theme's effective (background, color) pair, and holds it to that theme's
floor. It reproduces this defect at 1.80:1 against the pre-fix stylesheet.

Reported by ClawSweeper review on #130232.
2026-08-26 11:47:01 -07:00
RoboClaw 44d60a29e6 fix: preserve unread reminder for open sessions (#129386)
* fix: preserve manual unread markers in open sessions

* fix: distinguish explicit session reads

* fix(ui): gate unread contract on gateway capability

* perf(ui): keep server capabilities out of startup bundle

* test(gateway): keep agent fixtures roster-consistent

* fix(sessions): preserve legacy read compatibility

* test(gateway): type agent fixture configs

* fix(ui): remove unread gateway fallback

Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>

* test(infra): avoid fixed SSH tunnel port

* fix(ui): acknowledge unread after history commit

* docs: clarify unread upgrade boundary

* test(ui): drive mobile session menu by tap

* fix(ios): remove stale read reconciliation call

---------

Co-authored-by: roboclaw-bot <309084314+roboclaw-bot@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-08-26 05:04:39 -07:00
Peter Steinberger 9f9ad63246 fix: honor session policy for widget approvals (#129970) 2026-08-26 01:34:24 -07:00
Peter Steinberger 80ecc7c8b5 feat(control-ui): add the Absolutely theme with self-hosted typography (#129885)
* chore(control-ui): vendor Space Grotesk and Lora webfont subsets

Adds the woff2 faces the Absolutely theme paints in, plus the stylesheet that
declares them. Both families are SIL Open Font License 1.1 and ship unmodified,
with their upstream license text beside the files as the license requires.

Latin and latin-ext subsets only, 184 KB total. Serving these from the gateway
rather than a font CDN keeps font-src 'self' intact, avoids a third-party
request on every load, and works on an offline or LAN-only gateway.

The stylesheet is a static asset rather than a bundled import: it is linked at
runtime only while a theme that declares faces is active, so it stays out of
the startup CSS budget and out of every other theme's load.

* feat(control-ui): add the Absolutely theme with self-hosted typography

Adds a fourth built-in Control UI theme: terracotta clay on warm graphite in
dark mode, burnt clay on ivory in light mode, with its own typography — Space
Grotesk for chrome and Lora for chat prose.

Typography is theme-owned rather than global. A new --font-chat token defaults
to --font-body, so every other theme is byte-for-byte unchanged, and the face
declarations live in a static stylesheet that the theme applier links only
while a theme that declares fonts is active. Themes without declared faces
issue no font request at all.

The faces themselves are vendored in the preceding commit.

Also fixes two defects found while building it:
- Theme cards overflowed their border on long labels: the grid's 1fr label
  track floors at min-content, so a long name pushed the trailing column past
  the card edge. The track is now minmax(0, 1fr) with an ellipsis, which also
  covers imported tweakcn labels. The selected-state checkmark that made the
  overflow visible is gone; the accent border, tint, and ring already carry
  selection, and aria-pressed owns the semantics.
- index.html's first-paint bootstrap only knew claw/knot/dash, so any new theme
  flashed the default palette before the app corrected it. It now resolves the
  new theme and has a matching pre-paint background, with mount-fallback
  coverage for both modes.

Startup JS grows 198 B for the theme wiring, appearance option, locale
strings, and font loader, which fits the existing ratchet tolerance, so no
budget baseline moves. Startup CSS stays within its 45 KiB ceiling, which is
why the faces are a linked static asset rather than a bundled import.

* style(control-ui): format the vendored font stylesheet

The generated @font-face block kept its upstream single-line unicode-range
declarations, which oxfmt wraps. Repo-wide `pnpm format:check` is clean.

* test(control-ui): type the chat runId in the typography e2e

Reuses the chat-flow requireRecord/requireString helpers instead of asserting
the params shape, so runId is a real string rather than an unknown coerced by
String() (oxlint no-base-to-string).

* fix(control-ui): resolve theme fonts against the configured UI mount

The theme font stylesheet and its faces used root-absolute URLs, so a gateway
served under a configured Control UI base path (gateway.controlUi.basePath,
for example /openclaw) fetched them from the wrong origin path. Both requests
404ed there and Absolutely silently fell back to system fonts while its palette
still applied, which reads as "the theme looks a bit off" rather than as a
broken asset.

The link href now goes through inferControlUiPublicAssetPath, the same helper
plugin art and provider icons already use, and the stylesheet's own url()
references are relative to it, so both levels follow the mount without the
loader knowing the base path. Adds an e2e case under /openclaw asserting the
resolved href and that the browser fetches below the mount; it fails on the
previous code with the root-absolute path.

Reported by ClawSweeper review on #129885.

* test(slack): stop racing the aborted read request in the Web API bound test

`bounds dedicated reads without timing out shared clients` asserted the mock
server received exactly two requests, but the read client aborts at 20ms and
the server records on arrival, so whether the aborted request lands first is a
race. It failed on a loaded CI runner with 1 received.

The bound is still asserted directly (read rejects, shared resolves); the count
now only requires the shared client's arrival, and checks its token so the
dedicated client is still proven not to have carried the call.

Unrelated to this PR's theme work; fixed here rather than left red.
2026-08-26 01:06:41 -07:00
RoboClaw 46fab66580 feat(ui): align session menus (#128995)
Co-authored-by: roboclaw-bot <309084314+roboclaw-bot@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-08-25 21:45:54 -07:00
Peter Steinberger 3626a8e18e fix(agents): enforce session permissions for shared terminal input (#129604)
* fix(agents): enforce session permissions for shared terminal input

* chore: keep terminal release notes in pull request

* fix(agents): fence full terminal input to active run authority
2026-08-25 15:59:28 -07:00
Josh Avant f9dbda38dc fix(mcp): revoke resource access with app authority (#129550) 2026-08-25 14:04:14 -07:00
RoboClaw 0415c403a7 improve: use agent identity as the sole assistant identity (#129067)
* refactor(config): remove duplicate assistant identity

* test(config): isolate assistant migration coverage

---------

Co-authored-by: roboclaw-bot <309084314+roboclaw-bot@users.noreply.github.com>
2026-08-25 00:21:13 -07:00
RoboClaw 49c7fc5bae fix(ui): stop claiming queued sessions need a slot (#129081)
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-08-25 00:19:58 -07:00
Peter Steinberger 8a1ff3966b feat(workers): run OpenClaw and Codex on the same cloud profile (#127752)
* feat(workers): run both harnesses on one cloud node profile

* docs(workers): explain shared OpenClaw and Codex cloud profiles

* fix(workers): preserve trusted Codex cloud enrollment

* docs(codex): clarify bundled cloud plugin eligibility

* fix(workers): honor Crabbox bootstrap retries

* fix(workers): allow cloud profile setup to finish

* fix(workers): reject unsupported cloud execution modes

* refactor(workers): split cloud placement modules

* fix(workers): preserve completed container results
2026-08-25 00:13:29 -07:00
Peter Steinberger 3c2667ca40 fix(gateway): fill older chat pages without duplicate transcript storage (#128810)
* fix(gateway): page visible chat history without duplicate storage

* chore: keep release changelog owned by release automation
2026-08-24 10:43:59 -07:00
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 9ee238dd45 fix(tui): restore session setting reset directives (#128731) 2026-08-24 06:12:46 -07:00
Peter Steinberger e4b36f2e97 feat(ui): open sessions in real browser tabs and keep sidebar visibility per-tab (#128474)
* feat(ui): make sidebar visibility per-tab and free Cmd-click for new tabs

Sidebar visibility was persisted in localStorage, which is shared origin-wide,
so collapsing it in one tab leaked into every other tab and window on reload.
Visibility is view state, not a preference: stop persisting it and keep it in
the tab-local navigation snapshot. Width stays persisted.

A tab whose first load targets one specific conversation now starts collapsed,
seeded once at bootstrap from the canonical session-URL contract so bare /chat
and dashboards are unaffected and SPA navigation never re-collapses. Cmd-B,
the toggle, hover-peek, and the remote UiCommand path all still work.

Cmd/Ctrl-click on a sidebar session row now falls through to the browser so
sessions open in a real new tab or window; multi-select moves to Alt/Option-
click, which previously fell through to the browser's link-download behavior.
Shift-click range selection is unchanged.

* test(ui): prove per-tab sidebar visibility at the browser boundary

Covers the two states that matter: a bare /chat first load keeps the sidebar,
and a tab opened directly on one conversation renders that conversation
full-width with the sidebar collapsed and Cmd-B restoring it. Waits on the
composer, not just the missing sidebar, so the capture cannot pass against a
blank pane.

* fix(ui): narrow new-tab sidebar collapse intent

* fix(ui): collapse sidebar for catalog sessions opened in new tabs

* test(ui): split bootstrap navigation visibility coverage
2026-08-24 00:49:39 -07:00
Peter Steinberger 6178409d9a fix(dashboard): preserve widget content ownership (#128489) 2026-08-23 21:53:39 -07:00
Peter Steinberger 0978d55b05 feat(nodes): automatic device placement for sessions.dispatch (#128421)
* feat(nodes): automatic device placement for sessions.dispatch

sessions.dispatch gains autoDevice: true — the gateway selects the eligible
session-host node with the most available worker slots (deterministic
tie-break), retries up to three candidates when a node churns at the
pre-provisioning eligibility fence, and reports the chosen device in the
placement runner projection. Control UI offers Any available node with
actionable disabled reasons. No-eligible-host failures state why.

* fix(ui): break draft-place-state/draft-session-placement import cycle

resolveDraftSessionPlacement only needs four scalar fields; a structural
param type replaces the Pick<DraftPlaceState,...> import that created the
madge cycle.

* fix(ui): keep the devices section hidden when no devices are paired

The Any available node row lives inside the Your devices section; rendering
it with zero paired devices resurrected the section on gateway-only setups.
Gate it on device presence — Connect a machine remains the discoverability
path — and cover both the empty and non-hostable cases.

* fix(gateway): project dispatch runner state through the canonical reader

The dispatch reply no longer synthesizes an available device runner; it uses
the fenced workerPlacementRunnerAvailabilityReader (and disk-space reader)
exactly like session reads, so a node lost after durable provisioning
projects offline consistently. Documents placement.runner.deviceId in the
protocol reference.
2026-08-23 21:08:33 -07:00
Peter Steinberger 8e6c34fba3 feat(ui): user-selectable accent color for the Control UI (#128432)
* feat(ui): user-selectable accent color for the Control UI

Adds an Accent color section to Settings -> Appearance: curated preset
swatches plus a native custom color input. The choice persists as the
gateway-synced ui.prefs.accent (#rrggbb) and takes precedence over the
operator-level ui.seamColor, falling back to the active theme's accent
when cleared. The shared apply path now also derives readable
--accent-foreground/--primary-foreground ink from accent luminance, so
light accents keep dark text on primary controls.

Bundled accent-invariant cleanup: the embedded terminal resolves its
background/cursor/foreground from live computed tokens instead of a
hand-copied claw palette (fixes desync on knot/dash/custom themes),
a baked rgba(255,92,92) gradient now uses color-mix over --accent, and
duplicated diff-tint literals collapse into --danger-subtle/--ok-subtle.
Environment branding moved behind a lazy runtime module (-2.5KB gzip
startup JS).

* fix(ui): derive status glows and tints from semantic tokens

Status-dot glows, callout gradients, compaction-indicator borders, chip
borders, and dreams pulse animations baked literal green/blue/amber/red
rgba values -- several stale copies of old --danger/--info hexes -- so
they ignored theme families and light mode's deeper status hues. All 22
sites now color-mix over --ok/--warn/--warn-strong/--danger/--info.

* fix(ui): scope default accent swatch chip vars to a swatch class

Reusing .settings-theme-card--<theme> on the accent swatch made theme-card
Playwright locators resolve to two elements (strict-mode violation in the
prefs-reconnect e2e suite). The default swatch now uses
.settings-accent-theme--<theme>, added alongside the theme-card selectors
in the theme-invariant chip var blocks.

* fix(ui): carry --primary-hover through the accent override

The accent apply path set --primary but not --primary-hover, so dark
Claw/Knot primary buttons reverted to the theme hover color while an
accent was active (ClawSweeper P2). Derive it from --primary like the
accent hover, clear it on reset, and prove the resolved hover color via
a painted probe in the appearance e2e.
2026-08-23 20:32:05 -07:00
Peter Steinberger c8514ed20a fix(gateway): preserve mixed media failure receipts (#128423)
* fix(gateway): preserve mixed media failure receipts

Adopted from Microsoft's Lobster patch 0044 via giodl73-repo/lobster-plugins-and-patches (source commit e9f84234ebdf).

* chore: drop changelog edit; release generation owns that file
2026-08-23 17:29:57 -07:00
Mitra Kermanian d710cbd8be docs: fix four broken cross-page anchor links (#128278)
* docs: fix five broken cross-page anchor links

Five internal doc links pointed at anchors that do not exist on the target
page, so readers following them landed at the top of the page instead of the
referenced section.

- /gateway/cloud-workers#desktop -> #desktop-interactive
  (heading is "## Desktop (interactive)"), from experimental-features and
  the gateway protocol reference.
- /web/urls#special-documents-and-startup-modes ->
  #other-special-documents-and-startup-modes
  (heading is "## Other special documents and startup modes"), twice from
  the Control UI page.
- /plugins/sdk-runtime#api-runtime-agent -> #runtime-namespaces
  "api.runtime.agent" is an <Accordion> title, not a heading, so it emits no
  anchor; the enclosing "## Runtime namespaces" heading is the real target.

Sibling links on all three target pages already use heading-derived anchors
(#coordinator-backed-crabbox, #route-table, #reusable-runtime-utilities),
and this was the only link in docs/** aiming at an Accordion title out of
1342 such titles.

Docs-only change; no runtime or config surface is touched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: restore the sdk-entrypoints deep link

Running the real Mintlify anchor audit (pnpm docs:check-links:anchors) showed
that /plugins/sdk-runtime#api-runtime-agent already resolves: Mintlify does
generate anchors from <Accordion title="..."> elements, so the original link
was correct.

The audit flags fragments it cannot resolve -- it reported both
cloud-workers#desktop and urls#special-documents-and-startup-modes on main --
and it never reported this one. Retargeting it to #runtime-namespaces would
have replaced a precise deep link with a coarser section link, so this restores
the original.

The remaining four link instances in this PR are confirmed broken by the same
audit: main reports 313 broken links across 45 files, this branch reports 309
across 42, with no newly broken links.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 16:31:01 -07:00
Peter Steinberger fd4404d0b6 fix(dashboard): open widget links through the host instead of sandbox popups (#128125)
* fix(dashboard): open widget links through the host instead of sandbox popups

Widget frames granted allow-popups/allow-popups-to-escape-sandbox to every
rendered widget, including grantState "none" widgets that never reach an
operator approval card. CSP has no navigate-to, so a popup bypassed the
connect-src 'none' containment those widgets otherwise run under. Scripted
window.open was blocked only by the document guard's open lock, which is
emitted solely when blockDescendantFrames is set, so two unrelated knobs had
to agree for the boundary to hold.

Restore both sandbox strings and route user-clicked links through the existing
ticket-bound bridge: a trusted click on a ticketed board widget's http(s)
target="_blank" anchor forwards to the Control UI, which opens the tab with
noopener,noreferrer. Links keep working for every rendered widget, the host
owns the scheme filter instead of trusting widget-authored rel attributes, and
scripted popups are impossible by construction rather than by coincidence.

Also collapses the unreachable controlUiBaseUrl branch and corrects both docs
pages, including the claim that links opened for every rendered widget, which
was never true for MCP-App board widgets.

Follow-up to #127315; refs #127314.

* fix(dashboard): open widget links through the Control UI external-url owner

The widget bridge opened links with a local window.open helper, which the
lint:ui:no-raw-window-open boundary guard rejects. Delegate to
openExternalUrlSafe(), the Control UI owner for external navigation: it
applies noopener/noreferrer, detaches the opener handle, and keeps one
canonical open path. The bridge keeps a narrower absolute-http(s) gate,
since widget-supplied links must not reach blob: URLs that the shared
external-link policy permits.

* fix(dashboard): route middle-button widget links and honor canceled clicks

Address ClawSweeper review on #128125.

The wrapper only handled primary click, so with popup permission removed from
both sandbox layers a middle-button activation silently did nothing. It also
listened on capture, so a widget handler's preventDefault could not cancel the
link the wrapper then opened itself.

Match the Control UI activation contract in ui/src/app/native-link-routing.ts:
listen for click and auxclick, gate on !defaultPrevented && !shiftKey &&
!altKey && (click+button 0 || auxclick+button 1), and listen on bubble so
widget cancellation stays effective. A middle click now routes for any widget
link, not just target=_blank, since middle-click always means new tab and
leaving plain links unrouted would keep the same silent-no-op class.

Move the wrapped-document test into src/canvas/wrap.test.ts: it exercises
buildWidgetDocument, so it belongs with its owner, and the added assertions
would otherwise push widget-tool.test.ts past the max-lines cap that AGENTS.md
forbids suppressing.
2026-08-23 08:47:04 -07:00
Peter Steinberger 224d11f222 feat(ui): environment differentiation for the Control UI (gateway.controlUi.environment) (#127711)
* feat(ui): differentiate gateway environments in Control UI

* fix(ui): guard sidebar environment read for partial session-data hosts

* fix(ui): clear environment presentation when refreshed config is unset
2026-08-22 14:06:28 -07:00
ClawSweeper 48a2b45fed fix: stop limited access status from shifting page layouts (#127704)
* fix(ui): move limited access status into shell chrome

* fix(ui): cover tablet limited access layout

---------

Co-authored-by: RoboClaw <309084314+roboclaw-bot@users.noreply.github.com>
2026-08-21 21:42:05 -07:00
Peter Steinberger 9903e404ef feat(ui): add live CPU/memory/delay sparkline graphs to the system busyness overlay (#127650)
* feat(ui): add cpu/memory graphs to system busyness overlay

* refactor(ui): redesign busyness overlay vitals as sparkline stat tiles

Merge the duplicated sparkline row and numeric tile grid into three vital
tiles (CPU, memory, event-loop delay) with gradient area charts, a live
pulse dot, pointer scrubbing with value+age readout, and a danger tint
driven by the gateway's own event-loop degradation reasons. Memory
auto-ranges its baseline so RSS trends stay visible.
2026-08-21 15:06:17 -07:00
Hannes Rudolph 5b67b61964 feat(dashboard): let widgets run granted actions and open links (#127315) 2026-08-21 14:57:40 -06:00
Josh Avant ccbfa6c3a3 feat(ui): explain decision receipts in Activity (#126007)
* fix(audit): project safe decision receipt displays

* docs(agents): preserve audit display privacy

* fix(ci): satisfy audit receipt guardrails
2026-08-21 11:33:32 -07:00
Peter Steinberger 584e750c16 fix(tui): route usage cost correctly (#126668) 2026-08-20 05:55:11 -07:00
Peter Steinberger 0a86702241 feat(github): authorize agent identities from Settings (#126474)
* feat(github): add device authorization lifecycle

* fix(github): harden device authorization lifecycle

* fix(github): refresh native tool display snapshot

* fix(github): refresh config and UI baselines

* refactor(github): break OAuth identity import cycle

* test: make gateway retry deadline assertion scheduler-safe
2026-08-19 17:41:58 -07:00
Peter Steinberger 0606e31d0e feat(gateway): broker GitHub publication (#126306)
* feat(gateway): broker GitHub publication

* refactor(gateway): split publication owners

* fix(gateway): enforce publication branch authority

* fix(gateway): bind publication to remote identity

* fix(gateway): bind publication recovery to remote state

* fix(gateway): preserve publication git state

* fix(gateway): retain publication recovery authority

* fix(gateway): commit publication index atomically

* fix(gateway): narrow publication index errors

* refactor(gateway): keep publication CAS errors private

* fix(gateway): recover publication index transactions

* fix(agents): describe GitHub publication tool

* fix(gateway): harden publication base fetch

* fix(gateway): reject publication filter semantics

* fix(gateway): verify publication creation base

* refactor(agents): align publication tool options

* fix(gateway): isolate publication object lineage

* refactor(gateway): use shared table probe

* test(gateway): keep publication helpers in routed suite

* perf(ui): lazy-load GitHub publication request

* fix(gateway): preserve publication support contracts

* fix(gateway): recover publication before authority checks

* fix(gateway): fence local publication snapshots

* fix(android): format generated protocol models

* fix(gateway): harden publication recovery

* fix(gateway): fence publication recovery

* fix(ui): reset completed publication cycles
2026-08-19 11:05:12 -07:00
Peter Steinberger ae55a4090c refactor(canvas): make the panel a widget presenter (#126030)
* refactor(canvas): retire legacy host and commands

* refactor(apple): narrow shared Canvas contracts

* refactor(macos): keep Canvas as widget presenter

* refactor(ios): remove Canvas client

* refactor(android): remove Canvas client

* refactor(linux): remove Canvas client

* fix(ci): isolate native locale artifacts

* fix(linux): regenerate companion lockfile

* fix(canvas): refresh native tool display metadata

* test(canvas): align coverage with presenter surface

* test(canvas): remove obsolete asset root seam

* test(canvas): stabilize retirement CI coverage

* refactor(swift): remove orphaned resource wrapper

* test(ios): remove retired canvas layout assertion

* fix(macos): reserve retired canvas command namespace

* refactor(macos): isolate canvas command policy

* fix(canvas): select only eligible macOS panels

* fix(canvas): keep panel selection plugin-owned
2026-08-19 08:21:07 -07:00
Peter Steinberger 97557ec3f5 fix(widgets): route show_widget through Discord Activities (#126294)
* refactor(widgets): unify Discord presentation

* fix(discord): keep incomplete Activity routes private

* fix(discord): require usable Activity accounts

* docs(discord): clarify hidden Activity routes
2026-08-19 05:41:37 -07:00
Peter Steinberger 4af09d4961 feat(ui): unify focused presentation routes (#126143)
* feat(ui): unify focused presentation routes

/focus/<target> replaces unshipped standalone query links across dashboard, terminal, desktop, and native apps.

Gateway-served index assets are anchored so nested documents resolve their bundles from the Control UI base path.

* test(gateway): narrow emitted asset URLs

Fixes check:test-types TS18048/TS2322 by dropping unmatched optional captures before comparing emitted asset URLs.

* test(docs): follow centralized cloud secret guidance

Fixes the stale current-main docs test after #126132 centralized GCP and Hetzner setup in docker-vm-runtime.

* test(ui): retry missing locator reads

The 500ms locator text read can time out while the menu label is still rendering, causing expect.poll to reject instead of using its owning 10s retry window. Treat only Playwright TimeoutError as a missing value so the outer poll retries while page-closure and arbitrary failures still surface.

* test(android): capture TLS probe coroutine

The TLS probe test inferred its coroutine from mutable scope children, racing unrelated child startup and teardown in CI. Capture the exact Job from inside the probe coroutine and join that owner before asserting the stale-attempt guard.

* fix(gateway): preserve plugin focus routes

Keep approval handling ahead of plugin dispatch, but treat focus documents as an unclaimed Control UI fallback after plugin authentication and routing. Exact and prefix plugin routes therefore retain ownership, while unclaimed reads serve the focus document and other methods return 404.

* fix(ui): migrate released terminal links

Preserve stable v2026.7.1 terminal query compatibility by rewriting the root/base ?view=terminal URL once to the canonical /focus/terminal path with history.replace. Keep URL parsing path-only, and leave the removed desktop and dashboard query forms as a hard cut.

* test(codex): assign run-attempt tools shard

Cached filtered configs caused duplicate ownership, and the test lacked a canonical full-suite owner.

* test(ui): keep cloud recovery proof state-owned

The recovery test should assert owner state and reload identity, while dedicated tests own transient alert visibility.

* test(qa): wait for outbound bus state

* fix(qa): reserve gateway ports through staging

* refactor(qa): keep socket creation in gateway owner
2026-08-19 03:41:29 -07:00
Peter Steinberger 67750753a2 fix: capture GitHub identity from authenticated sign-in (#126114)
* fix: capture GitHub identity from authenticated sign-in

Automatically persist verified GitHub identities from Cloudflare Access and Tailscale Serve while keeping public Git co-author credit as a separate opt-in.

* test: stabilize cleanup and activity capture

* fix(security): bind GitHub profiles by account id

* test: scope activity capture to route

* fix(security): gate profile requests on identity sync

* fix(security): close pending profile authorization gaps

* test(ui): stabilize terminal continuation menu

* test: stabilize startup recovery timing

* test: keep one Codex attempt tools owner

* fix(plugins): allow profile-independent gateway reads
2026-08-19 01:35:52 -07:00
Peter Steinberger 80934e5639 feat(ui): dispatch sessions to paired devices (#126187) 2026-08-19 01:11:49 -07:00
Josh Avant 784676537c feat(ui): enable link favicons by default (#126109) 2026-08-18 18:41:54 -07:00