Commit Graph

1058 Commits

Author SHA1 Message Date
Peter Steinberger b9c6789560 feat(secrets): authenticated egress substitution proxy with destination binding (#123216)
* feat(secrets): add authenticated egress substitution proxy

* feat(secrets): bind egress substitution to hosts

* ci(codeql): classify egress proxy bypass tunnel in network boundary query

* refactor(proxy-capture): use the canonical IP parser instead of node:net

* fix(secrets): compare proxy tokens with a process-keyed MAC
2026-08-13 20:49:31 -07:00
Vyctor H. Brzezowski 7025da83e8 fix(ui): rotating another device's token ends with nothing on screen (#121361)
* fix(ui): report the outcome of a cross-device token rotation

Rotating another device's token ended with nothing on screen. The Gateway
returns the replacement only to a device rotating its own token
(shouldReturnRotatedDeviceToken; docs/cli/devices.md), so every rotation of
another device answered without a token and the Control UI, which only rendered
when one came back, showed no dialog, no message, and no error while the target
device was invalidated and disconnected.

device.token.rotate now records how it delivered the replacement in a
tokenDelivery discriminator ("in-band" | "withheld-cross-device"), additive and
optional so gateways that predate it still decode. The Devices page reports the
recorded fact rather than inferring one from a missing token: the show-once
reveal for an in-band rotation, and an outcome dialog for a withheld one that
states the token is not shown here, that the device was disconnected, that it
picks up the replacement on its next connect with a Gateway token or password,
and that a device which can only sign in with its device token has to be paired
again.

The reveal component takes an optional secret: with one, dismissal stays refused
because the value is unrecoverable; without one there is nothing to lose, so
Escape and backdrop close it like any dialog.

* fix(ui): lead the rotation outcome with what happened, not why

The withheld-rotation dialog opened on a negation, stacked three clauses of
protocol reasoning, and left "do I have to do anything?" to the last sentence.

It now reads outcome, then next step, then the one exception, with the security
rationale demoted to a muted trailing line: the device is named, told to be
signed out, and the common case says nothing else is needed. The rotate callback
carries the row's resolved label so the dialog names the same device the operator
clicked instead of rederiving the precedence.

The reveal component takes paragraphs plus an optional muted note, so an outcome
and its exception stay separate instead of collapsing into one block.

* fix(ui): give the rotation outcome a visual hierarchy

Four near-equal paragraphs in a plain box read as one undifferentiated block:
the reassurance, the conditional branch, and the footnote all carried the same
weight, and the calm text was rendered in muted grey because it reused
.exec-approval-sub.

The dialog now has zones. A success mark in the shared icon-tile geometry sits
beside the title, the two reassurance sentences carry full --text weight, the
one conditional branch becomes a house .callout.info so it cannot be skimmed
past, and the security rationale is a 12px muted footnote. Spacing follows the
card's 4px rhythm.

The acknowledge button follows the confirm-dialog convention: the accent button
is the action that commits something, so a show-once reveal keeps it and a
report of work already done uses the neutral button.

* fix(ui): collapse the rotation outcome to a single reassurance

The body zigzagged: it announced a new token, then raised a disconnect, then
spent the next line walking that alarm back. The middle beat existed only to be
contradicted, so it is gone -- the disconnect is transient and self-healing, and
naming it bought the reader nothing.

The title now carries the announcement and the device name, the body is the one
reassurance, and the callout drops its "this device" echo. The neutral dismiss
button gets --border-strong: .btn's resting border is within ~4/255 of --card in
dark, so it read as unpainted on this surface. The callout wraps balanced.

Both dialogs now pass a single message, so the paragraph-array support added for
the previous structure is removed rather than left speculative.

* fix(ui): key the rotation callout to an observable symptom

"If it can only sign in with its device token" asked the operator to know which
credential the device holds, which is internal state they cannot see. The
condition is now the symptom that state produces: the device does not come back
on its own. Same behavior, same technical truth, decidable from the Devices page.

* fix(ui): reject contradictory device-token rotation results

The Devices page inferred the outcome from an unchecked RPC payload, so an
explicit `tokenDelivery: "in-band"` with no token, a withheld result that
carried one, or a delivery mode this client predates all rendered as a
successful withheld rotation - telling the operator the device re-credentials
itself while their previous token was already invalid.

rotateDeviceToken now parses the response into the closed outcome: the two
current pairs, the two legacy omission states from gateways released before
tokenDelivery, and every other explicit pair through the existing error path,
which surfaces as the page's danger callout carrying the recovery step.

* chore(plugin-sdk): refresh the API contract for the rotation result schema

Registering DeviceTokenRotateResultSchema in PluginLifecycleProtocolSchemas is
what makes the generators emit the Swift model, and that registry sits in the
type closure of 18 plugin-sdk exports, so their closureHash entries move. This
is generator output rather than a hand edit: the export set and every declaration
are unchanged, only the hashes.

* fix(ui): reject malformed device-token rotation envelopes

The parser trusted the envelope around the fields it read. A null, scalar, or
empty payload carries neither tokenDelivery nor token, so it matched the legacy
omission state and produced the reassuring completion dialog after the previous
credential had already been invalidated. A blank token did the same, though the
result schema bounds token to a non-empty string.

Only DeviceTokenRotateResultSchema's shapes are accepted now: the payload must be
a record that identifies the grant it rotated - every Gateway answering this
method returns deviceId and role, before and after tokenDelivery existed - and
token must be either absent or a non-empty string. This has to happen here
because the browser Gateway client resolves frame.payload directly, so the
registered result schema never runs on the client.

The mid-flight-reconnect reveal test asserted on a two-field stub no Gateway
sends; it now uses the real response shape.

* fix(ui): reject rotation results that do not answer the request

The rotation parser accepted any envelope naming some device and role. It
never checked scopes or rotatedAtMs, both required by
DeviceTokenRotateResultSchema, and never compared the returned grant with the
one that was requested -- so an incomplete or unrelated reply opened a success
dialog claiming a credential the operator may still hold had been replaced.

Require the full result shape and bind it to the requested grant, comparing on
the same trim normalization the device-auth store applies. The page fixture
now echoes the requested grant instead of hardcoding device-1, which is what
let a cross-device test accept a success dialog for the wrong device.

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

* test(ui): complete the rotation fixture the epoch test resolves

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

* test(ui): update token rotation E2E envelope

* fix(protocol): correlate rotated token delivery

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Patrick Erichsen <patrick.a.erichsen@gmail.com>
2026-08-13 20:41:04 -07:00
Peter Steinberger 31d54a61fb fix(ui): one failed full-message load pins the truncated preview for the whole session (#123324)
* fix(ui): retry failed full-assistant-message loads instead of pinning the truncated preview

Since ce466fccb0 removed the manual Show-more toggle, a failed
loadFullAssistantMessage left {status:error} that no renderer reads and the
render-time kick-off never retried (it only fired when no expansion state
existed). One transient RPC failure pinned the '...(truncated)...' preview
for the rest of the session with no visible outcome. Retry on later render
passes while attempts remain, bounded by revision so a dead loader cannot
hot-loop.

* fix(ui): render a retry affordance once automatic full-message loads exhaust

Addresses ClawSweeper P2 on the bounded-retry fix: after
FULL_MESSAGE_RETRY_REVISION_LIMIT the truncated preview previously stayed
silent. The exhausted error state now renders an inline error line with a
Retry button that re-enters the canonical loader via the same toggle path;
the retry bound is a named shared constant.
2026-08-13 19:51:15 -07:00
Jason (Json) 95240c0d57 fix(ui): pin notifications to viewport top (#123423) 2026-08-13 19:43:22 -07:00
Vyctor H. Brzezowski b0583e2643 fix(ui): contain long sharing member labels (#123017) 2026-08-13 23:19:26 -03:00
Peter Steinberger 6d716d9318 fix(ui): keep composer pickers visible near viewport edges (#123386)
* fix(ui): keep composer pickers visible near viewport edges

Model and effort pickers now share viewport-aware, preferred-above placement through the anchored overlay. This replaces the fixed CSS positioning policy so either picker can flip and shift within the viewport.

* test(ui): move picker regression to model flow suite

Keep the viewport-placement regression in the existing model and reasoning E2E owner so the composer redesign suite remains below the max-lines gate without losing coverage.

* test(ui): wait for adaptive pickers before interaction

Wait for the Web Awesome popup content to become visible before measuring menus or sending keyboard input. This aligns existing E2E interactions with the asynchronous details toggle lifecycle.
2026-08-13 18:53:34 -07:00
Peter Steinberger f0076b0ca6 feat(ui): thicken chat effort slider into capsule rail (#123380)
Restyle the effort popover slider from a 3px line to a full-height capsule rail with a pill thumb.
Retune the fill and dots for the tall track.
Verify the result in both dark and light themes.
2026-08-13 17:27:47 -07:00
Peter Steinberger 09940f718d fix(ui): stop app-card badges from squeezing titles into mid-word wraps (#123375)
On narrow cards the nowrap bundled-app badge left the title as the only
shrinkable item in the title row, so "Apple Watch" / "Wear OS" broke
across two lines. Let the row wrap: the badge drops to its own line
(left-aligned under the title) while wide cards keep it inline right.
2026-08-13 17:09:25 -07:00
Vyctor H. Brzezowski f73b436f05 refactor(ui): make chat rails full-height resizable columns (#122475)
* refactor(ui): make chat rails resizable columns

* fix(ui): preserve board runtime across rail modes

* fix(ui): unify chat rail chrome

* fix(ui): unify rail header controls

* refactor(ui): normalize rail header chrome

* fix(ui): render background tasks rail

* test(ui): preserve workspace collapse semantics

* fix(ui): keep companion separator single

* test(ui): distinguish companion sheet and column borders

* test(ui): compare shared rail focus chrome

* test(ui): drive rail focus with keyboard

* fix(ui): align single-panel rail headers

* fix(ui): add custodian rail toggle label

* test(ui): narrow mock chat request params

* test(ui): follow shared rail controls

* test(ui): keep rich rail state visible

* fix(ui): center terminal rail actions

* test(ui): keep chat notices in conversation column

* test(ui): detect focus inside rail shadow roots

* test(ui): respect desktop overlay interactions

* test(ui): restore rails after route navigation

* test(ui): rebuild route-scoped rail matrix

* test(ui): preserve custodian rail during matrix

* chore(ui): keep chat pane within lint budget

* test(ui): tolerate subpixel icon geometry
2026-08-13 19:49:11 -03:00
Peter Steinberger f8bca11d77 chore(ui): add dead-CSS audit script and delete unreferenced stylesheet rules (#123178)
* chore(ui): add dead-CSS audit script and delete unreferenced stylesheet rules

* fix(scripts): type-safe AST walking in the dead-CSS audit

* fix(scripts): detect ternary-headed class stems and restore live wizard rules
2026-08-13 14:53:17 -07:00
Peter Steinberger 3cdbe3a511 fix(ollama): verify guided setup with runtime model (#123190) 2026-08-13 14:27:34 -07:00
Josh Lehman f04fdd5594 fix(ui): align transcript search corner radius (#123299) 2026-08-13 14:12:30 -07:00
Josh Lehman 0b847dc92f fix(ui): keep question prompts from fading transcript (#123301) 2026-08-13 14:11:48 -07:00
Peter Steinberger 12ada43464 refactor(ui): resolve duplicate stylesheet selectors and restore no-duplicate-selectors (#123166)
* refactor(ui): resolve duplicate stylesheet selectors and restore no-duplicate-selectors

* refactor(ui): merge duplicate .kbd block in file-preview modal instead of config carve-out
2026-08-13 12:50:18 -07:00
Peter Steinberger 21d59e16e2 feat(gateway): monitor cloud session disk space (#123177)
* feat(gateway): monitor cloud session disk space

* fix(gateway): keep disk monitor contract acyclic
2026-08-13 12:47:53 -07:00
Peter Steinberger 9415181b75 fix(ui): make active visibility pill distinct from its hover state (#123173)
* fix(ui): make active visibility pill distinct from its hover state

* test(ui): advertise sessions.create in the control-ui mock harness

* ci: refresh pull_request run after zombie-queued Blacksmith jobs
2026-08-13 12:42:40 -07:00
Peter Steinberger f2f3ba2eaf refactor(ui): consolidate responsive breakpoints onto a canonical ladder (#123160)
Use the 400, 560, 640, 768, 900, 1100, and 1320px max-width ladder. Round noncanonical thresholds up to the next rung so compact layouts engage before desktop layouts become cramped.

Move the 1180px agent-tool summary breakpoint to 1320px because the persistent desktop rail leaves about a 1020px content column at that viewport; 1100px would coincide with the rail disappearing and the content expanding.

Stylelint now enforces the ladder and the existing compound-query exceptions.
2026-08-13 12:34:50 -07:00
Peter Steinberger 843017073c refactor(ui): route stylesheet colors through the token layer and gate hex literals (#123156)
Add semantic, presentation, brand, and shared highlight tokens in base.css.
Remove dead fallbacks and consolidate dark/light syntax colors behind tokens.
Enforce color-no-hex for CSS while exempting token definitions and sprite artwork.
2026-08-13 05:36:52 -07:00
Peter Steinberger 449ca204a6 fix(ui): keep limited-access banner clear of shell controls (#123128)
* fix(ui): keep limited-access banner clear of shell controls

* fix(ui): avoid native notice gutters
2026-08-13 04:40:26 -07:00
Peter Steinberger 2a8b322ebf feat: view your machine's screen from the iOS and Android apps (#123097)
* feat(ui): add mobile desktop document mode

Add a shell-free mobile desktop route that reuses the dock panel controller and lazy noVNC client, with source preselection, touch controls, keyboard input, and retryable inventory failures.

* feat(ios): add desktop viewer entry points

* feat(android): add desktop viewer

* fix(android): keep System Back inside the desktop viewer

The per-session viewer replaces SessionDashboardScreen in place instead of
pushing a shell tab, so System Back fell through to the shell-level handler
and popped the whole Dashboard tab, stranding the operator on Chat. Claim
Back while the viewer is showing.

Also carry over TerminalSettingsScreen's imePadding: the viewer's own touch
toolbar hosts the keyboard affordance, so the soft keyboard would cover it.

Proof (emulator, Medium_Phone_API_36.0, stub control UI on 18789):
pre-fix Back from the viewer lands on Chat; post-fix it returns to Dashboard.
No Robolectric regression test — no existing screen test constructs
MainViewModel, and BackHandler under Robolectric would need new scaffolding
for weaker evidence than the live repro.

* test(ui): stop the pairing views leaking dialogs into the shared document

`ui/vitest.config.ts` runs the unit project with `isolate: false`, so test files
share one jsdom document inside a worker. `view.pairing.test.ts` appends a
container to `document.body` for every case and never tears down, unlike its
sibling `channels-page.test.ts`, so whichever suite the worker scheduled next
inherited a mounted pairing dialog.

That surfaced on this PR's first CI run as ten failures in the untouched
`input-dialog.test.ts`, which found "Approve DM access" where it expected
"Rename session". A rerun went green, so the ordering is scheduler-dependent
rather than deterministic; this removes the contamination source rather than
leaving the next suite to lose the race.

Not a proven fix for that specific run — the leak reproduces only under CI's
file scheduling, and the full suite passes locally either way — but the missing
teardown is a real violation of the shared-environment contract.

* test(ui): stop the background-tasks rail asserting on a ticking clock

The rail e2e captured the main transcript's text before opening a task detail
and required it to be byte-identical afterwards. A running task renders a live
elapsed label, so the assertion failed whenever a second ticked over between
the two reads — twice while landing this PR, both times "12s" against "13s"
with no other difference.

Normalize elapsed labels on both sides instead of weakening the assertion. The
invariant it protects, that opening a detail leaves the main transcript alone,
still holds: a real content change is still caught, and only complete duration
tokens collapse, so diffstat figures like +14/-3 and phrases like "5 messages"
are untouched.
2026-08-13 04:10:48 -07:00
Peter Steinberger 983af028cf fix(ui): restore the composer model picker dialog (#123090)
* fix(ui): restore the composer model picker dialog

Partial revert of #122964: the chat/new-session composer returns to the
rich searchable model picker dialog (provider groups, CLI-agent targets
folded inside). Settings pages keep the shared unified picker.

* test(ui): restore chat-model-picker in the Web Awesome ownership inventory

The restored composer picker legitimately owns its details-menu and ARIA
combobox usage; #122964 had removed it from both expected lists.

* test(ui): stop leaking pairing-prompt modals into document.body

view.pairing.test.ts mounted containers (including an open
openclaw-modal-dialog from the approve-prompt tests) into document.body
without cleanup, poisoning later dialog-owning test files in the same
worker. Surfaced by this branch's changed test scheduling in checks-ui;
input-dialog.test.ts then saw the leaked 'Approve DM access' dialog.

* fix(ui): keep a persisted model override selectable when the catalog drops it

Port #122964's synthesized-override row into the restored composer
picker: a session pinned to a retired/unavailable model gets its row in
the dialog so the selection stays visible and escapable. An entirely
empty catalog keeps its no-models state.

* fix(ui): reconcile #123085 with the restored composer picker

The rebase merged #123085's wa-select label-width mechanics into the
restored dialog picker: its chat/layout.css hunks dropped the old
trigger reservation while its tests asserted the new geometry, and its
new-session e2e block targeted a wa-select that no longer exists there.
Restore the pre-#122964 chat layout CSS and responsive test, and drop
the unified-picker label block from the new-session e2e. #123085's
shared select-picker fix stays for the settings surfaces that use it.
2026-08-13 03:45:55 -07:00
Peter Steinberger 600126acaa fix: picker option labels collapse to ellipsis at phone widths (#123085)
* fix(ui): keep picker labels readable on mobile

Preserve readable unified model and channel option labels while allowing new-session mode controls to wrap at narrow widths.

* fix(ui): propagate shared picker minimum
2026-08-13 02:53:22 -07:00
Peter Steinberger c68039e7fb refactor(ui): one task-detail surface for rail and subagent clicks (#123003)
* refactor(ui): unify task details in sidebar panel

* docs(ui): describe task detail sidebar

* refactor(ui): collapse onSelectTask into onOpenTaskDetail

Two names for the same open-the-panel action; rows now call the
callback directly.

* fix(ui): clear rail open-task highlight when detail slot closes

openTaskId derived from sidebarContent alone, but region close only
collapses the layout and retains content, leaving a stale
aria-current row; gate the highlight on detail-slot visibility.

* fix(ui): keep requester sessions out of subagent transcripts and let reopen retry failed lookups

A queued subagent has no child session yet; falling back to its
sessionKey rendered the requester's conversation as the task
transcript. And a failed tasks.get permanently suppressed the panel's
render-driven load; selection now clears the recorded error so
reopening retries, matching the old rail behavior.
2026-08-12 21:51:07 -07:00
Peter Steinberger edcf197483 improve: one channel picker and one model picker across the Control UI (#122964)
* fix(ui): unify channel pickers with icons

Preserve channel values and mutation ownership across Automations, pairing, setup, and Workboard while sharing accessible picker rendering and artwork.

* refactor(ui): unify model pickers

Share provider-aware model selection across chat, new sessions, Automations, Agents, model defaults, Session Observer, Talk, and Memory while preserving owner-specific values and locks.

* fix(ui): keep picker change callbacks value-only

Keep the Web Awesome element private to model-picker custom-mode handling while preserving one-argument callbacks for every picker consumer.

* fix(ui): satisfy model picker types

Keep custom sentinel options within the shared model option shape and guard optional Memory placeholder translation keys.

* refactor(ui): remove obsolete model option template import

Drop the Lit import after Agents model options became pure shared-picker data.

* fix(ui): satisfy picker lint gates

Use explicit model option mapping, remove stale E2E locals, and keep the Cron behavior suite within its enforced line budget.

* test(ui): target unified session model picker
2026-08-12 21:32:51 -07:00
Vyctor H. Brzezowski aba94bbe0b fix: install or review the publisher you picked when ClawHub skills share a slug (#121697)
* fix(skills): keep ClawHub publisher identity from search through install

ClawHub search returns one entry per publisher, so several results can share a
slug. Every client collapsed the selection to that bare slug before calling
skills.detail and skills.install, and ClawHub answered 409 AMBIGUOUS_SKILL_SLUG
with no in-product way forward.

searchClawHubSkills now records the publisher-qualified reference once, on the
result that carries it, and the Gateway protocol documents it. skills.detail
parses the same reference grammar skills.install already accepted, so review and
install cannot resolve to different publishers. Control UI carries that one
reference through row actions, detail, busy state, and acknowledgement retries,
and shows it so otherwise identical rows are distinguishable.

Fixes #117633

* fix(apps): send the ClawHub publisher reference from native skill browsers

macOS, iOS, and Android read the qualified reference from search results and use
it for skills.detail, install, busy state, installed matching, and list identity,
so two publishers sharing a slug stay distinct instead of collapsing into one
ambiguous request.

* fix(skills): refuse external-source skill detail instead of reading a same-slug skill

ClawHub has no source-qualified read endpoint, so a skills-sh reference parsed
down to its bare slug would have returned a registry skill's card while install
resolved the external artifact. Review and install could name different skills.

skills.detail now fails closed on any reference that carries a source, and the
macOS and AgentPro rows show the publisher reference next to the summary instead
of only when a summary is missing, so same-slug rows stay distinguishable.

* chore(apps): refresh native i18n source baseline for the skill row references

* refactor(skills): drop the unread search-result ownerHandle field

installRef is the one reference clients send back, and no client reads the
publisher handle separately, so the protocol and Control UI carry one field
instead of two.

* fix(skills): name the next step when external skill detail is refused

Clients that gate install behind a successful review would otherwise see only a
refusal, so the error names the direct install path and the CLI equivalent.

* fix(macos): use a doc comment on the ClawHub row subtitle

swift-format's docComments rule requires doc comments on declarations; the
subtitle property carried a regular comment and failed macos-swift.

* fix(skills): carry ClawHub trust state to clients that can install

Forwarding installRef let clients install the exact publisher the operator
picked, including external skills-sh sources. It did not forward the trust
state that says ClawHub never scanned that source, so iOS AgentPro — the one
surface that installs in a single tap with no review step — could install an
unscanned artifact with nothing on screen saying so. The CLI already labels
these (docs/clawhub/cli.md, docs/cli/skills.md); native clients could not,
because trustState was never on the wire.

trustState becomes an optional field on SkillsSearchResultSchema. It is purely
additive: older clients ignore an unknown key and the field is absent for
registry results, so downgraded readers are unaffected and no protocol version
moves.

Every client that renders a search row now shows "Not scanned by ClawHub",
matching the CLI wording exactly: iOS AgentPro in the row above the install
button, macOS and Android beside the review action, and Control UI on the row
that explains why review is refused for these sources.

Covered by a wire assertion that the state reaches clients for an external
source and stays absent for registry rows, plus decode-and-label tests on the
shared Swift kit and the Android parser, and a Control UI render assertion.

* fix(ui): size the ClawHub detail dialog to a refusal message

Refusing detail for an external source made an error-only dialog reachable.
The shared preview panel reserves a tall reader height for skill documents, so
a two-line refusal rendered in a mostly empty dialog and read as broken rather
than deliberate. Found by inspecting the review captures.

* revert(ui,apps): drop the ClawHub trust label layer

Maintainer product decision: skills.sh runs its own scanners, so OpenClaw does
not add a second alert layer in the apps. Removes the label from Control UI,
iOS, macOS and Android, and drops the trustState wire field that nothing would
render. The CLI keeps its existing label; changing that is a separate call.

Publisher identity, the fail-closed detail refusal, and the message-only dialog
are unchanged. Splits the oversized skills view test file to satisfy max-lines
without a suppression.

* test(ui): fix ClawHub skill fixture checks

* chore(plugin-sdk): refresh API baseline

---------

Co-authored-by: Patrick Erichsen <patrick.a.erichsen@gmail.com>
2026-08-12 20:13:15 -07:00
Peter Steinberger f5ad8735d1 feat(ui): open subagent details in chat sidebar (#122941)
* feat(ui): open subagent details in chat sidebar

* chore: drop changelog edit (release generation owns it)

* refactor(ui): drop duplicate close in subagent detail panel

The sidebar region header already owns a Close Details control in both
wide and narrow layouts; the panel-local X duplicated it 40px away.

* fix(ui): stop subagent transcript loader when pane presentation retires

Pane retention wipes sidebarContent directly, so the detail slot's
render-time reset can never run again; a pending refresh timer plus
incoming task events kept refetching chat.history for a hidden panel.

* docs(ui): note close-control ownership in subagent detail header

* fix(ui): break transcript renderer import cycle

* fix(ui): use shared action cursor for subagent rows
2026-08-12 20:11:13 -07:00
Peter Steinberger 495f295f25 feat(ui): split new session place picker (#122938)
* feat(ui): split new session place picker

* test(ui): cover three-chip session picker
2026-08-12 20:09:27 -07:00
Colin Johnson ae20e2c163 fix(ui): model picker shifts when another user starts typing (#122809)
* fix(ui): keep composer model picker stable while typing

* test(ui): prove typing keeps model picker stable

Co-authored-by: Colin Johnson <colin@solvely.net>

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-08-12 20:08:45 -07:00
Peter Steinberger cba8aff63f feat(ui): show useful environment facts in the picker (#122923)
* feat(ui): show environment facts in Where picker

Preserve environment platform, trust, session-host, and capability metadata through new-session discovery while keeping placement availability tied to live executable nodes. Render bounded quiet device/cloud facts and add mocked browser coverage for task #33.

* docs(plan): track picker environment facts

* refactor(ui): split place browser rendering
2026-08-12 19:32:34 -07:00
Colin Johnson 9d9c36c459 fix(ui): contain OAuth wizard modal content (#122892)
* fix(ui): contain OAuth wizard modal content

* test(ui): poll OAuth modal containment
2026-08-12 22:30:18 -04:00
Peter Steinberger cef071582e feat: let limited browsers request admin access (#121459)
* feat(gateway): add live device scope upgrades

* feat(ui): add limited-access upgrade flow

* fix(protocol): refresh Swift scope upgrade models

* perf(ui): lazy-load device scope upgrades

* fix(ci): complete scope upgrade generated surfaces

* perf(ui): lazy-load GitHub link hovercards

* fix(ui): keep admin repair guidance focusable

* fix(ui): gate and refresh scope upgrade banner

* refactor(ui): keep gateway client within line budget

* fix(ci): align rebased scope upgrade checks

* fix(ui): resolve scope upgrade in browser tests

* fix(gateway): honor refreshed scope upgrade deadline

* fix(gateway): honor refreshed scope upgrade deadline

* fix(gateway): coalesce scope upgrade waiters

* fix(ui): gate scope upgrade actions

* chore(plugin-sdk): refresh rebased API baseline

* fix(scope-upgrade): return canonical request ids

* fix(ui): preserve gateway event type binding

* fix(protocol): generate scope upgrade result models

* fix(ui): preserve scope upgrade recovery guidance

* chore(plugin-sdk): refresh rebased API baseline

* test(ui): avoid scope upgrade navigation race

* docs(control-ui): clarify scope upgrade approver

* test(gateway): align appended method counts

* chore(plugin-sdk): refresh rebased API baseline

* refactor(ui): keep place picker within line budget

* chore(plugin-sdk): refresh rebased API baseline

* chore(plugin-sdk): refresh rebased API baseline

* chore(plugin-sdk): refresh rebased API baseline

* chore(plugin-sdk): refresh rebased API baseline

* chore(plugin-sdk): refresh rebased API baseline

* chore(plugin-sdk): refresh rebased API baseline

* chore(plugin-sdk): refresh rebased API baseline

* chore(plugin-sdk): refresh rebased API baseline

* chore(plugin-sdk): refresh rebased API baseline

* chore(plugin-sdk): refresh rebased API baseline

* chore(plugin-sdk): refresh rebased API baseline

* fix(gateway): preserve scope-upgrade browser origin
2026-08-12 18:32:00 -07:00
Josh Lehman a3207b574c fix(ui): show progress during new session startup (#122713)
* fix(ui): show progress during new session startup

Refs #122703

* oc-1fa: keep new-session progress through handoff

* fix(ui): keep new-session handoff live

* fix(ui): preserve navigation callback contract
2026-08-13 00:17:12 +00:00
Vyctor H. Brzezowski a5ff8f5c33 fix(ui): keep composer capability menus within the viewport (#122296)
* fix(ui): constrain composer capability menus

* fix(ui): cap composer capability menu height

* test(ui): cover long capability menu states

* test(ui): capture capability menu interaction states

* fix(ui): keep dark capability states legible
2026-08-12 17:10:57 -07:00
Peter Steinberger 08b134324f feat: continue web sessions in the terminal (#122870)
* feat: continue sessions in terminal

Add a credential-free Control UI continuation command and allow openclaw resume to reuse current-profile authentication only for byte-exact configured Gateway targets.

* fix(gateway): separate public origin TLS ownership

Allow exact public-origin resume targets to reuse local authentication without inheriting the direct local listener certificate fingerprint.

* fix(gateway): scope exact targets to gateway mode

Prevent remote profiles from reusing dormant local Gateway authentication for explicit loopback or public-origin targets.

* fix(cli): encode terminal resume handoffs

Replace shell-specific quoting with a strict credential-free base64url handoff, gate configured auth reuse to validated handoffs, and skip unused session discovery.

* fix(gateway): isolate handoff auth identity

Suppress ambient Gateway auth fallback for validated handoffs while preserving explicit credentials, configured SecretRefs, stored device auth, and exact-target TLS ownership.

* fix(cli): harden terminal resume handoffs

* fix(cli): parse terminal handoff outcomes

* fix(cli): bind handoffs to resolved agent

* test(ui): align terminal continuation proof

* docs(plan): track terminal continuation

* refactor(ui): keep terminal handoff result local
2026-08-12 17:07:48 -07:00
Josh Lehman bb5ef9872a fix: resume sessions interrupted by gateway restarts (#122644)
* fix: resume sessions interrupted by gateway restarts

* fix: make session recovery retry-safe

* refactor: isolate restart session recovery

* test: track recovery temp directories

* fix: inherit recovery state under lifecycle lock

* fix: surface rejected recovery continuation

* fix: fence recovery continuation authority

* chore: regenerate plugin sdk api baselines

* test(gateway): track sessions.recover release train
2026-08-12 16:18:58 -07:00
Josh Lehman 88d6b571b0 fix(ui): keep tool failure state on individual calls (#122681) 2026-08-12 13:20:58 -07:00
Peter Steinberger 901dd11a13 improve(ui): header scope dropdown and unchanged-line expansion in session diff (#122711)
* feat(ui): expand session diff navigation

Release note: Make the session diff scope selectable from its header and let working-tree diffs reveal unchanged file context in bounded chunks.

* fix(ui): keep unchanged-line expander labels inside their row

Chromium's flex layout on button elements pins the label line box to the
button's vertical center, painting the gap count into the diff row above.
Grid centering avoids the quirk; skip rows also center-align their controls.

* fix(ui): revalidate diff snapshot before expanding unchanged lines

Addresses the ClawSweeper P2 stale collapsed-gap content finding by revalidating the target file patch before reading current file text.
2026-08-12 10:37:42 -07:00
Peter Steinberger ad8f803311 feat(ui): connect machines from the Where picker (#122635)
* feat(ui): add connect machine dialog

* docs(plan): track connect machine picker

* fix(ui): harden connect-machine dialog lifecycle
2026-08-12 07:51:37 -07:00
Peter Steinberger d44f70eb4b feat(pairing): one-paste device pairing via oc-pair setup links (#120768)
* feat(pairing): one-paste device pairing via oc-pair setup links

Implements milestone 3 from docs/plan/runners.md.

* fix(pairing): sign bootstrap handshake, keep URL candidates, wire pairing countdown

* test(gateway): update client callsite guard

* fix(pairing): preserve setup URL context paths

* fix(ui): keep pairing help aligned with setup mode

* fix(pairing): isolate bootstrap credentials

* perf(ui): keep one-paste pairing within bundle budget

* refactor(pairing): isolate native pair URL prefix parsing

* fix(pairing): preserve candidate lifecycle state

* fix(pairing): retire shared credentials after bootstrap

* fix(pairing): apply rotated manifest through client owner

* test(pairing): prove bootstrap retirement across reconnect

* fix(pairing): preserve native gateway context paths

* fix(pairing): carry native context paths through reconnect

* fix(ios): preserve encoded gateway context path

* chore(plugin-sdk): refresh pairing API baselines
2026-08-12 02:01:33 -07:00
Peter Steinberger 85a176b98a feat(ui): commit-scoped session diff viewer with file, sync, and view menus (#122470)
* feat(ui): commit-scoped session diff viewer with file, sync, and view menus

The Control UI session diff panel becomes a dense Amp-style viewer: per-file
menus (copy path, open file, reveal in file tree, open in editor), a Sync
Locally popover with a copyable git fetch command, view options (collapse all,
wrapping, split/unified layout), and a sticky footer that reports how far the
branch is ahead of its merge base and switches between all changes,
uncommitted work, and individual commits.

sessions.diff gains an additive scope param (all | uncommitted | commit) plus
commits, aheadCount, and mergeBase metadata. Commit-scope diffs read only the
object database, skip untracked collection, and bypass session-start baseline
filtering; unknown commits surface a typed unavailableReason. Offscreen file
bodies use content-visibility so large diffs stay responsive.

* fix(ui): satisfy session diff CI gates

* fix(gateway): fence commit-scoped session diffs to the advertised branch history

ClawSweeper found that commit-scoped sessions.diff accepted any commit resolvable in the checkout. Fence operator.read commit reads to the advertised merge-base..HEAD history and cover sibling-branch and base-history commits.
2026-08-12 01:08:33 -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
Peter Steinberger 0de17482ae feat(gateway,ui): quiet Where picker, placement chip, and projects read model (#120804)
* feat(gateway): projects.list groups known checkouts by repo identity

Implements docs/plan/runners.md milestone 4 derived projects read model.

* feat(ui): regroup the Where picker by gateway, devices, and cloud

* feat(ui): placement chip shows where a session runs with reclaim

Implements docs/plan/runners.md milestone 4 placement display and reclaim.
2026-08-12 00:21:23 -07:00
Peter Steinberger 6f39f953ec fix(ui): show each chat panel button once, in the pane header (#122507)
* test(ui): advertise terminal and browser panels in the mock harness

The mocked Control UI never advertised browser.request or terminal.open and
left terminalEnabled false, so the chat header's panel toggles were invisible
in the harness and could not be visually verified.

* fix(ui): give the chat pane header the only panel toggle row

The session workspace rail header rendered Terminal, Browser, Ask OpenClaw and
Changes alongside its own dock/refresh/collapse controls. Terminal and Changes
already lived in the chat pane header, so both rendered twice at once, while
Browser was reachable only from inside a files rail and Ask OpenClaw sat in a
per-session rail despite being a global surface that already owns a sidebar
entry (settings route 'custodian').

The rail header now owns workspace-file actions only. Browser moves up to the
pane header beside Terminal and into the narrow-header overflow menu; the
duplicated Terminal and Changes buttons and the Ask OpenClaw toggle are gone.

Production LOC: +21 -84.

* test(ui): stop the vite stub from shadowing the mock bootstrap config

ui/vite.config.ts registers a placeholder /control-ui-config.json middleware
and config-file plugins load before inline ones, so the mock gateway plugin's
bootstrap body never reached the app and every scenario bootstrap field was
silently dropped. Marking the mock plugin 'pre' lets it answer first.
2026-08-12 00:13:13 -07:00
Vyctor H. Brzezowski 0295b7ab54 fix(ui): gate model shortcuts by search focus (#122316)
* improve(ui): unify model picker shortcut hints

* fix(ui): gate model shortcuts by search focus

* fix(ui): focus model trigger on open
2026-08-12 02:14:47 -03:00
Vyctor H. Brzezowski 2a1eba7713 fix(ui): show local project icons in the chat header (#122406)
* fix(ui): resolve project icons from deterministic paths

Prepare project icon bytes during chat startup and serve only the process-stable snapshot. Keep all filesystem work asynchronous and lift the breadcrumb trail onto the topbar's optical axis.

* fix(ui): keep project icon paths web-focused

Drop the IDE-specific icon convention so every candidate remains an explicit web project path.

* fix(ui): preserve project icon compatibility

Restore existing web icon paths and refresh bounded session snapshots when they are served. Keep the IDE-specific path excluded per the final product decision.
2026-08-12 00:52:44 -03:00
Vyctor H. Brzezowski fe1dafbada fix(ui): show nested session parent in chat header (#122335)
* fix(ui): show nested session parent in chat header

* fix(ui): cap nested chat breadcrumb width

* test(ui): avoid breadcrumb geometry shadowing

* chore(ui): keep breadcrumb comment issue-agnostic
2026-08-12 00:18:11 -03:00
Vyctor H. Brzezowski 56e6221a81 fix(ui): align chat disclosure gutters (#122286) 2026-08-11 23:40:36 -03:00
Vyctor H. Brzezowski 7630d060f8 improve(ui): quiet slash command menu hierarchy (#122237)
* improve(ui): quiet slash command menu hierarchy

* improve(ui): compact slash command menu

* improve(ui): polish slash command rows

* improve(ui): streamline slash command sheet

* chore(ui): refresh command copy baseline

* fix(ui): keep skill selection in view

* test(ui): type skill menu viewport stub
2026-08-11 23:00:58 -03:00
Peter Steinberger 8876528f7c feat: clone GitHub projects from session picker (#121818)
* feat(projects): add managed GitHub clones

* feat(ui): clone GitHub projects from session picker

* fix(projects): integrate current gateway owners

* fix(protocol): derive Swift error detail accessors

* fix(ui): gate project cloning by scope

* test(gateway): isolate session prewarm probes

* revert: drop duplicate session prewarm repair
2026-08-11 18:49:16 -07:00
Vyctor H. Brzezowski 7c83fc1729 fix(ui): show a placeholder for remote markdown images (#122312)
* fix(ui): show remote image placeholders in chat

Keep remote Markdown images fetch-blocked while giving users a visible outcome and an explicit HTTPS open action.

* fix(ui): preserve links around image placeholders
2026-08-11 22:48:20 -03:00