Commit Graph

229 Commits

Author SHA1 Message Date
Vyctor H. Brzezowski d91b3970b4 fix(ui): show mobile toasts above the drawer (#121858)
* fix(ui): keep toasts above modal overlays

* fix(ui): route toasts through open modal state

* fix(ui): simplify modal toast routing

* fix(ui): preserve modal toasts with atomic moves

* fix(ui): route toasts across modal roots

* test(ui): cover nested shadow modal toasts

* fix(ui): keep modal toast lifecycle internal
2026-08-11 05:53:57 -03:00
Peter Steinberger ea06d72e85 feat(secrets): manage team secrets in Control UI (#121724)
* feat(secrets): add gateway store settings

* perf(control-ui): trim secrets startup copy

* perf(control-ui): reduce secrets startup payload

* fix(secrets): harden store mutation refresh

* perf(control-ui): meet secrets startup budget

* test(control-ui): update secrets navigation copy

* fix(ui): pluralize secret-detection count and drop duplicated dialog hint

* chore(protocol): regenerate gateway clients and SDK baseline after rebase

* fix(gateway): merge secrets store methods after project RPCs in advertised order

* chore: leave changelog to release generation

* test(gateway): retain desktop launch train coverage
2026-08-11 07:20:38 +00:00
Peter Steinberger fa03d9b913 refactor: consolidate coercion helpers (#121366)
* refactor: consolidate coercion helpers

* fix: remove duplicate coercion imports

* fix: preserve serialized coercion guard

* chore: ratchet coercion helper carve-outs

* fix(test): keep gauntlet subprocess startup lean

* fix: preserve imported session timestamp semantics

* fix: preserve catalog timestamp string semantics

* chore: align plugin SDK surface ratchet

* fix: preserve trajectory and SDK string contracts

* fix(test): preserve QA record assertion semantics

* fix: complete standalone record guard rename

* refactor(cron): use canonical string coercion

* fix(acpx): preserve Pi timestamp parsing

* test(channels): adapt custody test harnesses

* test(telegram): classify media harness as test support

* test(acpx): split timestamp contract coverage

* test(channels): support generated custody contracts

* chore: ban the full coercion helper name set

Extends the declaration guard to all eleven consolidated helper names and
renames the cron schedule-identity readNumber wrapper to readScheduleInteger
so the banned generic name cannot regrow.

* fix(scripts): repair release-validation guard drift and lint cause

Restores the renamed isJsonRecord guard in assertTrustedWorkflowHarness after
main added isRecord call sites in parallel, and attaches the caught YAML error
as the thrown error cause (preserve-caught-error was red on main).

* fix: preserve Claude timestamp string semantics

* fix: preserve persisted timestamp string semantics

* fix: preserve date-first timestamp contracts

* fix(openai): harden delegation failure formatting

* chore: close coercion helper guard gaps

* test(openai): model non-error delegation rejection

* chore: refresh plugin SDK API contract

* fix(tasks): use canonical string field reader

* fix(ai): use canonical provider error field coercion

* fix(browser): migrate native bootstrap coercion

* docs(plugin-sdk): clarify text record export compatibility

* fix(gateway): normalize approval execution identity

* test(outbound): isolate message action poll harness
2026-08-11 00:02:18 -07:00
Josh Lehman 2ae316e2f3 fix(ui): session titles flicker during archive transitions (#121868)
* fix(ui): preserve session titles through archive transitions

* oc-ba3: fix archive title CI regressions
2026-08-10 23:48:02 -07:00
Vyctor H. Brzezowski 9178c476f6 improve(ui): stop marking pinned sidebar rows with a message bubble (#121719)
A pinned session led with a generic message-bubble glyph that no other
session row gets. renderSessionLeadingState fills each row's single
leading slot from a priority ladder, and the pinned branch outranked the
creator avatar chip, so a pinned row could never show who created it and
carried a glyph whose only claim was "this is a session".

Drop the pinned branch and its styling. Pinned rows already render
through the same renderSessionTree anatomy as the session list, so they
now fall through the same ladder as any other row: attention icon, owner
avatar, or nothing. The leading slot stays width-reserved, so titles do
not shift.
2026-08-11 02:44:39 -03:00
Vyctor H. Brzezowski 3dc03f9265 fix(ui): confirm destructive sidebar session actions in-app (#121286)
* fix(ui): confirm destructive sidebar session actions in-app

The sessions sidebar still gated batch delete, single delete, cloud-worker
stop, and preserved-worktree removal on native window.confirm. In an
embedded WebView without a dialog bridge that call resolves false, so each
action returned early with no request, no error, and no visible outcome.

PR #118250 introduced showConfirmDialog and converted the Sessions,
Worktrees, and Nodes pages but not the sidebar operations layer, leaving
one product action with two confirmation mechanisms. Route all four sites
through the same helper and let the already-present post-decision scope
guards do their job, extracting the cloud-worker reclaim guard above the
await so the modal never opens for a stop this surface never performs.

Closes #121275

* feat(ui): let operators opt out of the session delete confirm

Session deletes are the repeatable, per-row destructive action in the
sidebar, so their confirm now offers "Don't ask me again". Stopping a cloud
worker and removing a preserved worktree deliberately get no opt-out: the
first is a rare shared-resource action, the second destroys the only copy
of uncommitted work. The checkbox exists only for callers that pass a skip
preference, so the serious confirms stay unskippable by construction.

The preference is device-local in UiSettings rather than a synced ui.prefs
key, so opting out on one browser cannot lower the bar on the operator's
other devices. Appearance -> Sidebar carries the matching toggle with the
standard reset affordance, which is how asking gets turned back on.

* fix(ui): scope the delete opt-out copy and refresh its settings view

Autoreview surfaced two preference-consistency defects.

The setting copy promised a general "session or a selection" policy, but
only the sidebar honours it; the Sessions page delete paths still prompt.
Extending the preference there would cross into another owner's in-flight
surface, so narrow the copy to what actually ships and keep the extension
as the recorded follow-up.

Persisting the opt-out also wrote local storage without notifying the
appearance subscription, so a mounted Settings -> Appearance kept showing
the toggle enabled while deletes already skipped the prompt. Publish the
refresh through the scope's theme capability after persisting.

* fix(ui): space the confirm opt-out away from the message copy

The checkbox rendered flush under the message with no separation, so it
read as a third line of the paragraph rather than a distinct control, and
the group crowded the action row.

Give it the 12px separation the details block already uses in this card,
keeping the 16px action gap below, plus the 10px control gap and 16px box
the config-form checkboxes use so the box and label align. Verified in
light and dark at desktop and mobile widths.

* fix(ui): use the cursor-action token on the confirm opt-out

The spacing fix hardcoded cursor: pointer, which the Control UI cursor
policy forbids outside link rules; controls consume var(--cursor-action)
so the hand stays configurable from one place.

Caught by ui/src/styles/cursor-policy.node.test.ts. Token resolves to the
same hand, so the published dialog captures are unaffected.

* fix(ui): keep the delete opt-out to the surface its setting names

deleteSession is shared: the chat-pane header menu calls it too. The
skip preference was applied inside the operation, so opting out from the
sidebar also silenced future header confirmations while the setting text
promised it applied to sidebar deletes only.

Make the opt-out opt-in per caller with a default that keeps asking, and
let the sidebar be the one caller that offers it. The header and any
future caller now match the copy without having to remember anything.

* refactor(ui): split the session group catalog out of the operations runtime

Adding the in-app group confirm on main pushed
session-organizer-operations.runtime.ts past the 700-line ceiling once
this branch's session confirms landed beside it.

Move the catalog and section-order operations into their own module. They
write the group catalog directly and never touch session rows, so the
dependency runs one way and no import cycle appears. The shared access
gate moves to the batch-mutations sibling both now import, and the
runtime keeps re-exporting the catalog names the controller loads as one
namespace.
2026-08-11 02:15:05 -03:00
Vyctor H. Brzezowski d7ea18e600 feat(ui): give pinned sessions their own sidebar group (#121712)
Pinned sessions shared the Pages navigation group's container and head,
so a pinned chat rendered as one more nav row directly under Automations
or Plugins with nothing separating elevated content from navigation.

Split the sidebar zone at render time: navigation entries stay in the
Pages list, session entries move into a second group with the sidebar's
inter-group spacing and a muted "Pinned" section label matching the
PAGES/SESSIONS treatment. Both groups keep the zone drop handlers, so
dragging a session into either one still pins it and records its slot,
and the group disappears entirely when nothing is pinned.
2026-08-11 01:49:03 -03:00
Peter Steinberger 7dbc2916a3 fix: cloud workers work with trusted-proxy browser auth (#121671)
* fix(ui): make cloud recovery scope gateway-owned

Allow trusted-proxy and other authorized browser sessions to use cloud workers without deriving recovery ownership from client-visible credentials or Web Crypto. The Gateway now emits an opaque principal-bound recovery scope.

* test(gateway): type recovery scope fixture

* fix(ui): preserve cloud recovery across upgrades

Keep the shipped credential-derived scope for older v4 Gateways and migrate credential-proven pending recovery rows to the Gateway-owned namespace before publishing readiness.

* perf(ui): lazy-load recovery migration

* fix(ui): preserve canonical recovery promotion

* test(ui): use deterministic provider navigation

* fix(ui): authorize recovery scope migration

* build(plugin-sdk): refresh recovery compatibility baseline

* test(gateway): expect recovery migration authorization
2026-08-10 20:52:55 -07:00
Vyctor H. Brzezowski b953cb2597 fix(ui): rename and delete session groups without browser prompts (#121738)
* fix(ui): rename and delete session groups in owned dialogs

The sidebar group menu was the last session surface still asking browser
chrome: Rename group opened window.prompt and Delete group opened
window.confirm. Both are unthemed, unvalidated and unavailable in the
webviews the Control UI also ships in, where they answer silently.

Rename now uses the owned input dialog, prefilled with the name it is
changing and titled with it. showInputDialog gains requireChange, which
holds submission closed while the entry still equals defaultValue, so the
no-op rename the caller used to discard after the fact is not submittable
in the first place. That replaces the caller-side "next === group" bail and
puts the empty check on one predicate shared by the button state and the
submit path.

Delete now uses the owned danger confirm, beside the other destructive
session mutations in the lazily loaded operations module, with Cancel
focused and no opt-out. Its copy said the sessions move to "Ungrouped", a
section id no operator ever sees; the list is labelled Sessions, so it now
says they move back to the session list. Deleting a group really does keep
them: session-groups clears the category on every member rather than
removing anything.

The confirm follows the access check so nobody is asked about a delete the
Gateway would refuse, and the mutation scope is reproven once they answer.

* test(ui): freeze animations in Control UI proof captures

A dialog capture taken during its fade-in shows a half-transparent card over
the page behind it, which proves nothing about the state it was taken for.
Playwright can settle running transitions before the shot.

* fix(ui): stop repeating the group name in the delete confirm

The title said Delete group and the message repeated it with the name, so
the operator read the same three words twice before reaching what actually
happens. The title now carries the group it is about and the message is
left to state the outcome.

* fix(ui): keep stale group deletes retryable

* fix(ui): match the stale delete notice to its sibling

The retry notice led with the negation and used curly quotes the rest of
this dialog family does not, so it read as a different product's copy. It
now states the cause and the next step the same way newGroupStale does for
the same replaced connection.
2026-08-10 23:27:39 -03:00
Peter Steinberger 750d0dcd9e improve(ui): make warm session switching instant (#121625)
* perf(ui): make warm session switching instant

Amp-Thread-ID: https://ampcode.com/threads/T-019fe957-0e49-707f-859f-9600ef536125

* fix(ui): harden retained session ownership

Amp-Thread-ID: https://ampcode.com/threads/T-019fe957-0e49-707f-859f-9600ef536125

* test(ui): scope retained session assertions

Amp-Thread-ID: https://ampcode.com/threads/T-019fe957-0e49-707f-859f-9600ef536125

* fix(ui): align generated image preview checks

* fix(ui): preserve early transcript reading position

Amp-Thread-ID: https://ampcode.com/threads/T-019fe957-0e49-707f-859f-9600ef536125

* fix(ui): adopt latest-navigation-wins router

Amp-Thread-ID: https://ampcode.com/threads/T-019fe957-0e49-707f-859f-9600ef536125

* fix(ui): restore retained pane reactivity

Amp-Thread-ID: https://ampcode.com/threads/T-019fe957-0e49-707f-859f-9600ef536125

* test(auto-reply): align item lifecycle expectations

* fix(ci): repair current-main validation gates

Amp-Thread-ID: https://ampcode.com/threads/T-019fe957-0e49-707f-859f-9600ef536125

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-08-10 16:17:25 -07:00
Vyctor H. Brzezowski 20d928e3c3 fix(ui): name new session groups in an owned dialog instead of a browser prompt (#121249)
* fix(ui): name new session groups in an owned dialog

Sidebar Move to group -> New group and the Sessions page New group action collected the group name with window.prompt, so the only text-entry step in that flow was browser chrome: unthemed, unvalidated, and unable to keep the typed name when the create was rejected.

Adds showPromptDialog next to the existing showConfirmDialog helper and routes both new-group surfaces through it. createSessionGroup now reports its mutation result so a rejected create keeps the dialog and its value for a retry.

* fix(ui): keep the prompt dialog usable when its operation throws

A rejected submit left the field disabled and the module-level guard latched, so every later prompt in the session was dropped. Report the thrown error as the visible failure instead.

* refactor(ui): keep the prompt dialog options type module-local

Nothing outside prompt-dialog.ts consumes the options type, and the deadcode export gate rejects unused public surface.

* fix(ui): keep the new-group catalog write and assignment on one scope

rememberSessionCustomGroup discarded whether its connection was still current, so a groups.put that outlived its connection was followed by a sessions.patch issued on the replacement one. It now reports completed/failed/stale like the sidebar catalog write, and the Sessions page threads one captured scope through both writes.

* refactor(ui): name new session groups through the shared input dialog

input-dialog.ts already owns Control UI text entry, so the new-group flow no
longer ships a second near-identical dialog next to it. showInputDialog gains
two additive options instead:

- requireValue trims the entry and holds submission closed while it is blank,
  replacing the ?.trim() bail every prompt call site repeated. Rename keeps it
  off, because an empty rename still has to clear a custom label.
- submit runs the operation behind the dialog and keeps it open on failure with
  the typed value intact, so a rejected create is correctable rather than
  retyped. The Gateway rejects a group name over 512 characters, and that
  message now reaches the operator without discarding what they wrote.

The input stays uncontrolled: its value binding is constant, so repaints for the
submit and failure states leave the caret and IME composition alone. The
AbortSignal contract and the reentrancy guard are unchanged.

Both catalog writes also honor the result groupsPut returns. The capability
retires a write on its own connection epoch, which the caller's scope predicate
cannot observe, so discarding it could file a session into a group no live
connection ever confirmed.

* fix(ui): survive a submit callback that throws before it returns

The catch was attached to the returned promise, so a non-async callback that
threw during synchronous validation escaped it: the rejection left submitting
latched, every control disabled, escape blocked, and the module-level guard held
for the rest of the session. The call now happens inside the try.

Also aligns the shared sidebar harness with the catalog contract. groupsPut
resolved undefined while its groupsRename and groupsDelete siblings already
resolved "completed", so the harness disagreed with the capability it stands in
for as soon as the caller started reading that result.

* fix(ui): do not recreate a session that vanished during the catalog write

Awaiting the group catalog write before the assignment opens a window in which
the target row can be deleted. sessions.patch creates a store entry for an
unknown key, so the assignment would resurrect the session the operator just
removed. assignCategory already guards its own patch this way; the new-group
path now does the same.

* test(ui): widen the empty session-list cast for the vanished-row case

* fix(ui): re-resolve sidebar group targets before assigning them

The new-group dialog no longer blocks, so the rows captured when the menu opened
can be deleted while the catalog write is in flight. sessions.patch creates a
store entry for an unknown key, so assigning them would resurrect the sessions
the operator just removed. The Sessions-page path already guards this; the
sidebar now re-resolves every target against the current list before patching.

* refactor(ui): load the input dialog behind one lazy boundary

input-dialog.ts was imported statically by the Sessions page and dynamically by
the sidebar controller. Mixing both for one module makes the dynamic import
ineffective and pulls the dialog into a startup chunk that never needs it until
an operator opens a menu. All four call sites now share the lazy boundary.

* fix(ui): keep a stale group submission open for retry

A Gateway connection replaced mid-write confirmed neither the group nor the
move, but both surfaces mapped that outcome to a silent close: the dialog
vanished and the typed name went with it, leaving nothing on screen to explain
why no group appeared. Both now report a retryable message so the entry stays
put and resubmitting runs against the replacement connection.

The row-vanished path still closes: there the group did land, and only the
assignment was skipped.

* fix(ui): close the input dialog when its owner goes away

The dialog mounts on document.body, so navigating away left it over the
destination with a detached owner, and a later submit ran against a page that
had already torn down its subscriptions. Both the Sessions page and the sidebar
controller now hand it a lifecycle AbortSignal and abort on disconnect, using
the option the component already accepted.

* fix(ui): prove the target session when a delayed patch lands

The new-group assignment guarded itself by asking whether the row was still in
the current list. That list is a bounded, filtered projection, so an ordinary
refresh that pages a row out of view read as a deletion and silently dropped a
legitimate move, while a row that was genuinely replaced still looked present.

Both surfaces now carry the identity captured when the operator acted, and the
Gateway decides: sessions-patch-engine compares expectedSessionId against the
stored entry and refuses a changed target, so a patch can neither land on a
successor session nor recreate one that is gone. The projection guards are
removed rather than kept alongside it.

SessionPatch and the sidebar patchMany targets carry the field, and
SidebarRecentSession keeps the sessionId its rows already had from the Gateway,
so every sidebar mutation the operator starts before a replacement is covered,
not just group creation.

* fix(ui): make the dialog's lazy boundary safe to await

Three races opened up when the dialog moved behind a dynamic import.

The Sessions page read the target's identity after awaiting the chunk, so a
refresh during a cold load handed back whichever row had replaced it and the
identity guard then approved the wrong session. The lookup now happens before
any await.

The lifecycle was armed only after the chunk resolved, so a sidebar that
disconnected mid-import left nothing for hostDisconnected to abort and the
dialog opened behind a dead host. The import now runs inside the lifecycle.

A rejected chunk load produced no dialog, no error and an unhandled rejection at
the void callers. Both surfaces now report it where they report their other
failures.

* test(ui): make the sidebar projection case prove the assignment

The case waited on a condition that was already true before the catalog write
landed, so it returned before the continuation reached patchSessions and its
negative assertions passed without exercising anything. It also still claimed
the old behaviour: rows leaving the projection now do not suppress the
assignment, because a bounded, filtered list is not evidence of deletion.

It now waits for the batch itself and asserts each target carries the identity
captured with its row, which is what lets the Gateway refuse a replaced target.

* test(ui): split the sidebar new-group cases out of interactions

Adding the projection case pushed interactions.ts past the 700-line ceiling.
The multi-select helpers move to multi-select-support.ts so both files share one
definition, and the two new-group dialog cases get their own case module beside
the other per-topic sidebar suites.

No behaviour change: 246 sidebar cases still pass, and interactions.ts drops to
well under the limit without a suppression.

* fix(ui): stop cancelling the sidebar dialog on a re-layout

The compact-viewport E2E caught this: at 420px the sidebar is dropped from the
DOM, which fired hostDisconnected and aborted the open dialog, so resizing the
window mid-edit closed it and discarded the typed name — the same silent loss
this PR set out to remove.

A sidebar detach is not the operator leaving. The dialog is a body-level modal
and outlives the sidebar's DOM position by design, so the controller no longer
tears it down. The Sessions page keeps its binding, where a page unmount really
is a navigation.

* style(ui): format the extracted sidebar multi-select helpers

* fix(ui): keep the live dialog abortable when a second open overlaps

Two fire-and-forget new-group actions during the lazy import both installed a
lifecycle controller. showInputDialog drops the reentrant request, but the
second call still cleared the field on its way out, so the dialog actually on
screen was left with nothing for disconnect to abort and survived navigation.

A second open now reuses the active controller instead of taking ownership, and
a regression case overlaps two opens then detaches the page.

* refactor(ui): defer session-identity plumbing to its own change

The new-group dialog work had grown a second, separable concern: threading the
identity of the row the operator acted on through SessionPatch, the sidebar
patchMany targets and SidebarRecentSession, so the Gateway could refuse a patch
whose session had been replaced. That contract is real and already enforced by
server-methods/sessions-patch-engine.ts, but it reaches every session mutation
the sidebar makes rather than group creation alone, and a rejected identity
still needs its own terminal outcome before it helps an operator. It belongs in
a change that can be judged on those terms.

Both surfaces return to the projection-presence guarantee this change shipped
first: captured rows are re-resolved against the current list and an assignment
whose row is gone is skipped, which is what keeps sessions.patch from recreating
a session the operator just deleted.

The sidebar case covering that keeps the waiting fix it gained meanwhile. It now
waits for the dialog to be removed, which happens only once the submit chain has
run, instead of for a condition that was already true before the catalog write
landed and let the negative assertions pass without exercising anything.

* fix(ui): say when a new group landed without its move

Both new-group paths skip the assignment when the captured row is no longer in
the current list, because sessions.patch would otherwise recreate a store entry
for a session the operator had removed. That guard was silent: the dialog closed
on the same "completed" a full success returns, so an operator whose list had
simply refreshed or paged got a new group, an unmoved session, and nothing that
accounted for the difference.

The list is a bounded, filtered projection, so a row leaving it is not proof the
session is gone. The skip stays — it is the safe choice without the target's
identity — but it now ends in a visible outcome. The Sessions page records the
partial result in its own error surface and closes; the sidebar raises a toast,
singular or plural with the rows the operator selected. Both are terminal rather
than retryable: the group already exists, so resubmitting the same name could
only fail.

A header-created group still starts empty with no notice, since nothing was
requested to move.

* fix(ui): tighten the skipped-move notice

The two-string singular/plural pair pushed the Control UI startup bundle past
its gzip ceiling: the catalog is loaded at startup, so long copy is paid for on
every page load, and the check failed by 41 bytes.

One string covers both surfaces and both counts. It still states the outcome and
the next step, which is what the notice is for, and it drops the count branch in
createSessionGroup along with the second key.

* test(ui): split the new-group case out of the groups e2e file

The groups e2e file crossed the 1000-line ceiling for test files once this
branch's new case landed on top of the growth main had already added, and a
max-lines suppression is not an option here.

The owned-dialog case moves to its own file beside it, matching the split the
sidebar cases already got. It keeps the same shared helpers, so the move is
mechanical, and it leaves the groups file with room for the cases that stay.

* fix(ui): report sidebar group moves that were skipped

Re-resolving the selection against the live list stopped a removed row from
being recreated, but only the all-removed case reached the operator. When part
of a multi-row selection left the list while the catalog write was in flight,
the survivors were patched and the call returned a plain success, so the dialog
closed with the group created, some sessions moved, and nothing saying the rest
were not.

The count comparison now covers the partial case: the surviving subset is still
patched, and whenever fewer rows resolve than were requested the skipped outcome
is named. It stays terminal, since the group already exists and retrying would
only recreate it.

The Sessions-page path takes a single optional key, so all-or-nothing is the
only shape it has and it already reports the skip; the sidebar is the surface
with a multi-row selection to partially satisfy.
2026-08-10 17:16:16 -03:00
Peter Steinberger 8a5978154f fix(ui): handle keyboard context menu shortcuts (#121628)
* fix(ui): handle keyboard context menu shortcuts

* fix(ci): repair shared validation regressions

* fix(ci): repair setup admission lint

* fix(ci): settle setup admission tests
2026-08-10 12:39:22 -07:00
Peter Steinberger ebd03db156 test(ui): target canonical reconnect session refresh (#121609) 2026-08-10 07:23:02 -07:00
Peter Steinberger 6b565c047f feat(ui): show unsent-draft pencil on sidebar session rows (#121476)
* feat(ui): show unsent-draft pencil on sidebar session rows

Typed-but-unsent composer text now surfaces as a pencil badge on the
owning session's sidebar row (and Home row) once you switch away.
Draft persistence now notifies stored-outbox subscribers so the
indicator appears and clears live. The active session suppresses the
badge since its composer is already visible.

* chore: refresh merge ref for CI against current main

* chore: refresh merge ref against healed main

* fix(ui): notify draft indicator only on presence transitions

Unconditional notify on every draft persist let outbox-projection
subscribers re-persist a stale pane over a newer draft (chat-state
route-fallback invariant). The sidebar pencil only consumes presence,
so notify on empty/non-empty transitions only.
2026-08-10 04:53:15 -07:00
Peter Steinberger e0262e6aa4 feat(ui): add people session sorting (#121470)
* feat(ui): add people session sorting

Offer People sorting in the Control UI session sidebar when the Gateway reports multiple session-sharing identities.

* fix(ui): restore sidebar translation dependency

* fix(ui): preserve session sort ordering contracts
2026-08-10 00:18:02 -07:00
Vyctor H. Brzezowski 35e8798828 fix(ui): offer undo and a named recovery path when hiding a sidebar section (#121385)
* fix(ui): offer undo and a named recovery path when hiding a sidebar section

Hiding a session catalog section wrote the preference and returned nothing:
the section vanished, no undo was offered, and the only standing recovery
(Settings > Appearance > Sidebar) was never named anywhere near the action.

Announce the outcome at the producer. hideSessionCatalog now names the section
that disappeared, offers Undo, and carries the Settings path in the toast text
for after it times out. The hidden-catalog set gets one owner,
setStoredSessionCatalogHidden, so the sidebar, the undo, and the Settings
toggle all mutate the same authoritative read instead of each rebuilding the
set from its own copy.

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

* fix(ui): align the automation-sessions checkmark with its popover siblings

Every selection state in the sidebar sessions filter popover reads on the
trailing edge, but "Show automation sessions" used Web Awesome's
type="checkbox" checkmark, which its template renders before the label. The one
row in the menu that is a checkbox rather than a radio put its check on the
opposite side of the row from every neighbour.

Hide the leading part and render the shared session-menu__check in the details
slot, so the row matches GROUP BY / SORT BY / STATUS while Web Awesome keeps
owning the menuitemcheckbox role and aria-checked.

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

* fix(ui): stop toasts from ellipsing away their recovery instruction

app-toast__message was nowrap + ellipsis, so any message longer than one line
lost its tail. That is fine for a bare acknowledgement and wrong for a toast
that tells the operator how to undo or where to re-enable something: the part
that gets cut is exactly the part they need.

Wrap instead, clamped to three lines so a pathological string still cannot grow
the toast without bound.

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

* refactor(ui): make the hidden-catalog writer module-private

With setStoredSessionCatalogHidden owning every hide/show, the raw
storeHiddenSessionCatalogIds writer no longer has a caller outside its own
module and the deadcode:exports gate flagged it. Drop the export rather than
keep a second public way to write the same preference, and move the two tests
that used it onto the owner they are actually meant to exercise.

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

* fix(ui): anchor toasts to the bottom-right corner

Toasts were centered over the content column, which on chat routes puts a
passive notification directly on top of the composer. Move the shared
.app-toast container to the trailing bottom corner, the standard desktop
placement for passive notifications.

Anchoring it there alone would trade one overlap for a worse one: at a 20px
inset the toast lands exactly on the composer's mic and send controls. Chat
routes therefore lift it above the composer via --app-toast-inset-bottom on
.shell--chat, which the toast host inherits as a child of .shell. Routes
without a composer keep the tight 20px corner.

Phone widths keep the full-width bottom idiom, and now get it properly: the
centered toast used to collapse to its content width (200px measured at 390px
viewport), so the explicit left/right insets also widen it to 366px.

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

* fix(ui): drop the chat-route toast lift

The corner toast no longer clears the composer on chat routes. Overlapping
the composer region is the accepted trade for one placement rule on every
route, so the route-specific 140px lift and its custom property are gone and
every surface uses the same 20px safe-area corner inset. Wrapped toast text
balances its lines now that the message can run to two.

* fix(ui): open the sidebar recovery path from its toast

Naming "Settings > Appearance > Sidebar" in the hide toast still left the
operator to find it. The recovery sentence is now a link built from the
settings-search destination that already describes that block, so the toast
opens the re-enable list instead of describing where it lives. Toast messages
accept a template for that.

* revert: leave the automation-checkmark repair to its own PR

#121429 fixes the same Web Awesome checkmark alignment on its own, with
browser regression coverage this branch does not carry. Two PRs owning one
CSS part is worse than losing the drive-by, so this keeps to the hide and
recovery invariant.

This reverts commit 6b1438a5be4bb1c9dd4c2ce9cd7d0dd54e77cb00.

* fix(ui): let the hide outcome reach a phone operator

On a phone the sidebar only exists inside the navigation drawer, and that
drawer is a modal dialog: a toast raised behind it is both occluded and inert,
so hiding a section there stayed exactly as silent as before this branch.
Promoting the toast into the top layer only fixes the paint half — a modal
dialog inerts everything outside it, so Undo and the recovery link still take
no clicks. So the hide hands the drawer back to the shell first, through a
seam the sidebar can use for any outcome that belongs on the main surface.
It is a no-op wherever the drawer is not open.

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 03:41:48 -03:00
Peter Steinberger b81946d6af fix(ui): make Cloud Worker startup responsive (#121200)
* fix(openai): filter platform-only ChatGPT catalog rows

* fix(ui): show Cloud Worker startup progress

* fix(ui): keep cloud startup out of app bootstrap

* fix(ui): reduce cloud startup bootstrap cost

* fix(ui): satisfy cloud startup lint

* fix(ui): keep cloud startup within UI limits

* fix(ui): satisfy cloud startup CI limits

* fix(ui): isolate concurrent cloud startup

* fix(ui): address cloud startup review findings

* fix(ui): persist cloud startup recovery per session

* style(ui): format cloud startup chat props
2026-08-09 22:42:45 -07:00
Peter Steinberger 3c5f89449e fix(ui): restore sidebar and Appearance accessibility (#121360)
* fix(ui): restore sidebar accessibility

Amp-Thread-ID: https://ampcode.com/threads/T-019fe94c-c697-71e9-982f-c164bb98bb6c

* refactor(ui): own sidebar list semantics at hierarchy boundaries

Amp-Thread-ID: https://ampcode.com/threads/T-019fe94c-c697-71e9-982f-c164bb98bb6c

* test(ui): keep sidebar cases within lint limits

Amp-Thread-ID: https://ampcode.com/threads/T-019fe94c-c697-71e9-982f-c164bb98bb6c

* test(ui): assert session tree list ownership

Amp-Thread-ID: https://ampcode.com/threads/T-019fe94c-c697-71e9-982f-c164bb98bb6c

* chore: refresh control UI CI

Amp-Thread-ID: https://ampcode.com/threads/T-019fe94c-c697-71e9-982f-c164bb98bb6c

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-08-09 22:25:26 -07:00
Patrick Erichsen d4c3b9e783 refactor(sessions): remove custom icon feature (#121263)
* refactor(sessions): remove custom icon feature

* test(sessions): expect legacy icons to be stripped

* fix(gateway): preserve retired icon request compatibility

* chore(plugin-sdk): refresh API baseline

* fix(plugins): reserve retired session icon slot
2026-08-10 04:51:56 +00:00
Vyctor H. Brzezowski 95fdce7049 fix(ui): confirm disruptive Control UI updates before they start (#121234)
* fix(ui): confirm disruptive Control UI updates before dispatch

Every Control UI update affordance dispatched `update.run` (or the macOS
bridge `start-update` message) on the first click, so a misclick could
install new code, restart the Gateway, and interrupt active work.

Add one canonical confirmation gate in the Control UI owner boundary and
route the sidebar/settings-sidebar/floating update card and the
Settings -> Updates row through it. The gate owns the copy, the versions,
the restart consequence, the action-specific confirm label, and the
choice between the macOS bridge and `update.run`, so the bridge fork no
longer lives in the card and no surface can drift.

CLI, API, automation, agent-driven paths, automatic update campaigns, and
the `update.run` contract are unchanged.

Addresses the Control UI portion of #117178.

* style(ui): apply oxfmt to update confirmation sources

* perf(ui): load the update confirmation lazily and share its impact copy

Keeps the Control UI startup bundle under its 317 KiB gzip ceiling: the
confirmation dialog only loads when an operator opens it.

* test(ui): drive the update confirmation in sidebar and shell wiring tests
2026-08-09 19:40:06 -03:00
Patrick Erichsen ce133d4a35 fix(ui): use in-app dialog for session rename (#121255) 2026-08-09 15:24:02 -07:00
Peter Steinberger 44a2a87134 fix(ui): prevent cloud draft recovery dead ends (#120970)
* fix(ui): harden cloud draft recovery

Refs #120953.

* fix(gateway): keep minimal metadata startup lazy

* fix(ui): preserve interrupted cloud setup state

* fix(ui): separate cloud recovery lifecycle fences
2026-08-09 04:06:05 -07:00
Peter Steinberger f67da420f6 feat(ui): labs-gated CLI agent targets in new-session model picker (#120949)
* feat(config): gate CLI agent discovery

* feat(ui): add CLI agents to model picker
2026-08-09 00:00:00 -07:00
Peter Steinberger 5ea44f5916 refactor(ui): remove dead Control UI weight (#120870)
* refactor(ui): remove dead Control UI weight

* test(ui): trim redundant preview fixture

* fix(ui): preserve agent file preview behavior
2026-08-08 22:22:32 -07:00
Peter Steinberger bd6d35443f fix(ui): keep dashboards alive across view switches (#120640)
* fix(ui): retain dashboards across view switches

Keep visited dashboard runtimes alive across Chat, Split, and Dashboard transitions, suspend hidden work, and avoid same-face persistence requests.

Refs #120639

* fix(ui): preserve session owner during route loads

Carry the last successful route match through pending loads so face-only navigation retains dashboard runtimes without stranding session or catalog switches.

Refs #120639

* fix(ui): preserve sandbox bridge while hidden

Suspend hidden board frame work without disposing the one-shot sandbox handshake, loaded document, or MessagePort.

Refs #120639

* fix(ui): forward sandbox readiness while hidden

Keep the constrained sandbox message channel alive during dashboard suspension so one-shot proxy readiness reaches the retained host without resuming widget work.

Refs #120639

* fix(ui): pause hidden plugin widgets

Propagate board activity through trusted plugin renderers so Workboard widgets suppress hidden refresh and mutation work, then refresh once without remounting.

Refs #120639
2026-08-08 15:53:59 -07:00
Peter Steinberger 51944498eb refactor(gateway): generalize bulk session patches (#120629)
* refactor(gateway): generalize bulk session patches

* fix(gateway): normalize bulk session fences

* refactor(gateway): simplify normalized target fence

* test(ui): align disconnected session wording
2026-08-08 15:00:09 -07:00
Peter Steinberger 1889764369 revert(ui): session is the product noun — undo thread copy rename (#120667)
Implements docs/plan/runners.md milestone 1a.
2026-08-08 12:43:05 -07:00
Peter Steinberger e3de98a451 feat(update): scheduled update campaigns with countdown, dev-channel auto-update, and Updates settings page (#120506)
* feat(update): scheduled update campaigns with countdown, dev-channel auto-update, and Updates settings page

* fix(ui): satisfy update campaign deadcode checks

* feat(update): extend scheduled update campaigns

* test(ui): include Updates page in settings sidebar search expectations

* fix(update): end campaigns when apply fails

* fix(update): pin dev campaign applies to the announced commit

* test(ui): align update e2e fixtures

* fix(update): pin adopted dev updates to the announced commit

* docs(update): document automatic dev update campaigns

* fix(ui): gate automatic dev updates on git installs

* fix(gateway): scope detailed update metadata to read-capable clients

* fix(gateway): break post-attach madge cycle with a leaf client-lookup type

* chore(protocol): regenerate clients after rebase

* test(gateway): update method tail after rebase

* fix(update): preserve campaign target and hold state

* test(update): align adoption mocks and split handoff suite

* test(update): route split handoff suites on Windows
2026-08-08 11:19:53 -07:00
Peter Steinberger 74b1627f88 feat(ui): show agent runtime distinction in model picker rows (#120617)
* feat(ui): show agent runtime distinction in model picker rows

* fix(ui): label known CLI runtime ids in model picker rows
2026-08-08 09:36:19 -07:00
Peter Steinberger 876a3f0d8f fix(ui): bulk session archive no longer stalls per thread (#120493)
* perf(ui): batch session archive requests

* fix(ci): sync bulk archive protocol surfaces

* fix(plugins): validate bulk archive ownership

* fix(ci): restore current main quality gates

* fix(ui): bound bulk archive dispatch

* fix(ui): bound bulk archive dispatch

* fix(ui): preserve bulk archive compatibility

* fix(ui): recover metadata-less archive fallback
2026-08-08 07:53:43 -07:00
Peter Steinberger 0575610f4c refactor(ui): align sidebar session list on a single lead-slot rail (#120473)
* refactor(ui): align sidebar session list on a single lead-slot rail

* fix(ui): keep sidebar group drag handle interactive

* fix(ui): keep catalog chevron visible on touch devices

Touch contexts have no hover, so branded catalog headers keep the chevron as their resting affordance (ClawSweeper P2 on #120473).
2026-08-08 03:47:26 -07:00
Peter Steinberger 2cfacdb671 fix(ui): move stale-client refresh notice into the sidebar update card (#120483)
* fix(ui): move stale-client refresh notice into the sidebar update card

The 'Server updated — refresh for full capabilities' notice rendered as a
full-width callout at the top of <main>, where the fixed shell-chrome
controls (nav toggle / search, layout.css z-index 45) overlap its text.

Move the notice into the existing sidebar-update-card slot above the
identity/connectivity footer: refreshRequired takes precedence over
updateAvailable (a stale client cannot trust its own update metadata),
renders non-dismissible with a refresh icon and two-line text, and is
wired through the sidebar, settings sidebar, and collapsed-nav floating
card. The old top-of-main banner branch, its resolver helper, and the
orphaned i18n key are deleted.

* fix(ui): keep stale-client refresh visible during onboarding and clear collapsed chrome cluster

Address both accepted ClawSweeper P2 findings:

- restore the stale-client refresh action during onboarding
- keep the floating update card clear of the three-button collapsed chrome cluster
2026-08-08 03:06:12 -07:00
Peter Steinberger c933145088 feat(ui): show custom commit age in sidebar footer (#120388)
* feat(ui): show custom commit age in sidebar footer

* test: repair sidebar build status CI gates

* test(ui): assert gateway state during reconnects

* fix(ui): expose build provenance in sidebar label

* fix(ci): align SDK baseline and test formatting

* fix(sessions): preserve context usage provenance

* test(agents): split session loop lifecycle coverage

* test(agents): reuse session loop resource helpers

* test(sessions): preserve stale totals in JSON output
2026-08-08 03:03:50 -07:00
Peter Steinberger 8bf62e7007 refactor(ui): derive gateway types from protocol and consolidate navigation/error helpers (#120348)
* refactor(protocol): derive shared gateway types

* refactor(ui): consolidate navigation click handling

* refactor(ui): centralize UI state and error helpers
2026-08-07 15:25:07 -07:00
Shakker 54e2ab545f test: cover read-only preference sync 2026-08-06 21:44:44 +01:00
Peter Steinberger d3558460d5 fix(ui): show Pi logo in session catalog (#119998)
Show the official Pi provider mark beside Pi external-session catalog headings in the Control UI sidebar. Keep OpenCode sibling branding covered and preserve transcript navigation and paired-node view-only behavior.

Proof: focused sidebar tests, browser-driven external-catalog E2E, production Control UI build/performance checks, UI typecheck, formatting/style checks, source-blind behavior validation, and clean Codex autoreview. GitHub Actions was in a confirmed service outage; maintainer explicitly directed landing without Actions. The repository full local gate reached unrelated failures across Codex, launchd, CLI, plugin naming, QA, and tooling suites after build and check passed.
2026-08-06 11:22:17 -07:00
Jason (Json) 3645e369a8 fix(ui): preserve chat history and active run timing (#119729)
* fix(ui): preserve chat history and active run timing

* test(ui): prove reconnect preserves active timer

* fix(gateway): keep recovery fallback within budget

* test(gateway): keep chat abort coverage within lint budget
2026-08-05 17:48:02 -06:00
Jason (Json) 462f8c4f29 improve(ui): trail transient session status beside row actions (#119560)
* improve(ui): trail transient session status

* fix(ui): swap running state for row actions on hover

* fix(ui): reserve only visible sidebar state width

* fix(ui): ensure actions override state spacing

* test(ui): await activated service-worker controller
2026-08-05 12:06:07 -06:00
Shakker 036f97d25b test: cover read-only admin method gates 2026-08-04 17:48:31 +01:00
Shakker c80810e700 test: advertise config writes in ui e2e 2026-08-04 17:48:31 +01:00
Vincent Koc a2ccd6ea61 test(ui): add service-worker update primary QA proof (#118805)
* test(ui): prove service-worker production updates

* test(qa): register control ui service-worker proof

* fix(ui): type production build environment

* fix(ui): avoid service-worker test shadowing

* test(ui): gate service-worker proof capture

* test(ui): isolate production e2e build output

* test(ui): assert claimed service worker version
2026-08-04 06:56:47 +08:00
Peter Steinberger 73535e4ede improve(ui): cut E2E shard runtime by reusing the bundle (#118697)
* test(ui): reuse bundled runtime across e2e files

* test(ui): isolate bundled E2E build output

* test(ui): align bundled E2E output paths

* test(ui): register E2E setup entrypoints

* test(ui): preserve bundled E2E gateway semantics

---------

Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>
2026-08-03 08:01:44 -07:00
Patrick Erichsen 5bb27ecd46 fix(chat): restore active turns after navigation and reconnect (#118472)
* fix(gateway): retain active turn progress for reconnect

* fix(ui): restore active turns after navigation
2026-08-02 22:00:49 -07:00
Peter Steinberger faf2602d22 fix(ui): keep native context menus out of the sidebar and make session catalogs hideable (#118217)
* fix(ui): keep native context menus out of the sidebar and make session catalogs hideable

Suppress the WKWebView default context menu across the sidebar while preserving editable inputs, and route agent cards and session-catalog headers to the existing menus. Persist hidden session sections in localStorage and expose live-synced Show controls under Settings → Appearance → Sidebar.

Co-authored-by: Codex <codex@openai.com>

* test(ui): serve mock session catalogs so the sidebar catalog sections are exercisable in dev:ui:mock

Advertises sessions.catalog.list and returns synthetic Codex/Claude Code
catalogs, enabling reproducible live proof of the catalog header context
menu and hide/restore preference flow.

Co-authored-by: Codex <codex@openai.com>

---------

Co-authored-by: Codex <codex@openai.com>
2026-08-02 16:02:53 -07:00
Peter Steinberger 920a265d81 fix(ui): expose selected automation history filters to assistive tech (#118133)
Co-authored-by: Peter Steinberger <steipete@macos.shared>
2026-08-02 12:11:08 -07:00
Shakker 603321a315 test: advertise session methods in UI scenarios (#117920) 2026-08-02 18:32:26 +01:00
Shakker a737e95234 refactor: split session access controls and tests 2026-08-02 18:32:20 +01:00
Shakker 040e0f5a6e fix: disable unauthorized session controls 2026-08-02 18:32:19 +01:00
Shakker c0104d5675 fix: guard remaining session mutation paths 2026-08-02 18:32:19 +01:00
Shakker 4e484da052 fix: honor operator scopes in session controls 2026-08-02 18:32:19 +01:00