Commit Graph

4642 Commits

Author SHA1 Message Date
Peter Steinberger 2183d637a2 fix(ui): keep discussions closed until toggled (#122072) 2026-08-11 07:51:18 -07:00
Peter Steinberger e390781534 refactor: burn cross-directory export name collisions (#121893)
* refactor: name subsystem logger exports

* refactor(test): distinguish exported test doubles

* refactor: consolidate canonical owner helpers

* refactor: give cross-domain helpers distinct names

* chore(lint): ratchet collision debt baselines

* fix(test): complete collision rename consumers

* fix(test): update remaining collision mock consumers

* fix(test): update transcript reader mock export

* refactor: keep embedded logger name at its owner

* fix(test): align embedded logger mock with owner

* refactor: name shared assistant phase extraction

* fix(ui): update assistant phase extractor import

* chore(generated): refresh collision and SDK baselines

* style(test): format merged plugin mocks

* chore(sdk): refresh API content hashes
2026-08-11 06:50:22 -07:00
Vyctor H. Brzezowski 16d0132c1b feat(ui): lead the chat header with the project and its own icon (#121734)
* feat(ui): show a workspace's own project icon in the chat workspace chip

Resolve the conventional project icon inside a session's workspace root once
per Gateway process and serve it over a strictly scoped authenticated route so
the Control UI can render real project identity instead of a folder glyph.

Refs #121723

* test(gateway): keep the workspace icon server teardown out of a promise executor return

* fix(ui): keep flat project icons legible on the workspace chip in every theme

* feat(ui): lead the chat header with the project, then the session title

Reorder the header into an identity trail — project chip, muted slash, session
title — rendered from one segment list so a later parent-session segment
extends it. Drop the project icon halo and record the flat-dark-icon contrast
tradeoff at the style site.

Refs #121723

* fix(ui): type the header identity trail segments without a lit result predicate

* fix(gateway): scope the workspace icon route to visible local sessions

Owner-gate the route so it cannot answer for sessions the caller's own
sessions.list would hide, withhold the workspace root of exec-node sessions so
a remote project never resolves to this host's icon, bound SVG icons before
they reach a renderer, and retry ordered Control UI credentials so a stale
saved token no longer silently drops the icon.

Refs #121723

* fix(ui): render the header project as a flat breadcrumb segment, not a badge

Drop the pill border, fill, and radius from the project name so it reads as a
quiet trail segment inline with the session title: one type size across the
trail, hierarchy carried by colour, hover and focus reusing the sibling title
button's tint so removing the border keeps a visible focus indicator.

Refs #121723

* fix(ui): balance the header trail separator spacing after the title pull-back

* test(ui): migrate the shared avatar loader tests to ordered credential arrays

* fix(ui): center the chat header identity trail

* refactor(ui): reuse workspace icon credentials

* test(ui): scope deleted-session recovery to the visible pane

* fix(ui): cache missing workspace icons across renders

* chore: refresh plugin SDK API baseline
2026-08-11 09:33:57 -03:00
Peter Steinberger bcaec0cf14 fix: resume main sessions after gateway restarts (#121969)
* fix(agents): always resume main sessions after restart

* fix(ui): render internal messages as system rows

* docs: update gateway restart recovery semantics

* refactor(sessions): simplify restart hook checkpoints

* refactor(agents): consolidate restart recovery claims

* test(agents): update delivery recovery revision guard

* fix(plugin-sdk): preserve restart hook state contract

* fix(ui): localize system row label

* test(agents): remove obsolete recovery import

* test(agents): align abort-code propagation with always-resume recovery

* fix(ui): share notice-aware turn boundaries

* test(agents): reconcile restart recovery ownership

* fix(ci): reconcile rebased recovery and UI checks
2026-08-11 04:01:34 -07:00
NianJiu f5ecd2c884 fix(ui): pair earlier tool results with calls (#110868)
Punchcard-Session: silver-orchard-workshop-q1

Co-authored-by: NianJiuZst <180004567+NianJiuZst@users.noreply.github.com>
2026-08-11 17:55:05 +08:00
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 f0d6cc4ade fix(ui): keep composer pickers usable on mobile (#121881)
* fix(ui): keep composer pickers usable on mobile

* fix(ui): right-align mobile composer controls

* fix(ui): isolate mobile composer sizing

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-08-11 01:46:47 -07:00
Peter Steinberger 4841e4e6b9 improve(ui): declutter narrow session headers (#121935)
* improve(ui): declutter narrow session headers

* test: drop stale unit-fast helper expectations

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-08-11 01:19:25 -07:00
Josh Lehman a8839a8923 fix(ui): Ctrl/Cmd+F search renders at normal size and restores focus (#121910)
* fix(ui): stabilize transcript search layout and focus

* fix(ui): satisfy transcript search lint

* test(ui): assert search focus outline properties
2026-08-11 00:59:59 -07:00
Peter Steinberger a12e0decd6 refactor(ui): simplify terminal replay lifecycle (#121925)
Behavior-preserving follow-up cleanup related to #121637.

Make replay a required structured contract, move stream currentness into AbortSignal ownership, keep controller construction private to the session owner, and remove the optional replay fallback, exported factory, tab factory state, and nested currentness callbacks.
2026-08-11 00:54:43 -07: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 8e54d696eb fix(ui): prevent duplicated worktree base branches (#121649)
* fix(ui): keep loaded worktree branch stable

* test(ui): stabilize model setup picker transitions
2026-08-11 00:14:30 -07: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 588427357b fix(ui): add spaces between compact duration units (#121857)
* fix(ui): separate compact duration units

* fix(ui): make compact durations consistently spaced
2026-08-11 03:17:07 -03: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
Peter Steinberger 10d3466c98 test(ui): remove duplicate dark contrast case (#121862) 2026-08-10 22:08:06 -07:00
Vyctor H. Brzezowski 9327c10d2b feat(ui): edit a queued chat message in place (#121692)
* feat(ui): edit a queued chat message in place

The pencil on a queued row, or a double-click, lifts the message and its
attachments into the composer. The row stays where it is, marked Editing,
so the position the corrected message will take is visible rather than
promised. The composer shows an edit banner with an X to cancel.

Cancelling touches storage not at all, because the row never left the
queue. Sending retires the original and hands its position to the
replacement, which lands in the same slot. While an edit is open the
drain refuses that row, so a reconnect cannot deliver the text the
operator is visibly rewriting; the queue behind it waits, which is the
same contract the held position promises.

* fix(ui): retire an edited queued row only with its replacement

The send retired the source row first and admitted the replacement second,
so a rejected store write left the operator with neither: the original gone,
the replacement never persisted, and the attachments it dropped already
released. Admission now carries the source id and swaps both rows in one
store write, so a rejected write changes nothing and the edit stays open on
the row that is still there, which is what cancelling already promises.
Filtering the source before the cap check also keeps a replacement
admissible on a full queue.

The edit token recorded only the session key, but an outbox is scoped by
session and agent, and a raw global session keeps its key while the selected
agent changes underneath it. The token now carries the agent that owned the
row and every reader validates the whole scope, so a send after a switch
cannot retire a row in the outbox it left behind.

* fix(ui): hold an edited queued row across every pane

Panes share one outbox and one drain lane while composer state stays per
pane, so the hold that keeps a row from being delivered while it is rewritten
was invisible to the pane that drained it: a session event in a second split
pane resumed every stored outbox, saw no edit, and sent the original text.
The hold is now read across every pane subscribed to the outbox, which keeps
one source of truth — each pane's own scope-validated edit — and needs no
lifecycle of its own, so closing a pane or switching agent releases it by
simply no longer being an active edit.

The payloads an edit owns now travel with its token. The write that admits
the replacement retires the source row and tells every pane synchronously, so
by the time the send released the images the replacement had dropped, the row
holding them was already gone and nothing was released. That leaked the
payload and its object URL for the rest of the browser session.

* fix(ui): release attachments from cancelled queue edits
2026-08-11 01:59:56 -03:00
Ayaan Zaidi 45ec5f4662 feat(ui): hide system-created probe sessions from the sessions sidebar (#121855)
Machine-created probe/run sessions (health checks, internal effect sessions) previously surfaced in the Control UI sessions sidebar as raw first-message rows and buried real conversations. Classify them from recorded creation provenance only (system actor, or unnamed run/internal creation without a human actor) and hide them by default behind a persisted "Show system sessions" toggle, mirroring the automation-sessions toggle. Cron rows stay owned by the automation toggle; the selected session, the Sessions page, and the toggle keep hidden rows reachable. Legacy rows without provenance stay visible.

Closes #121851

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 04:53:15 +00:00
Vyctor H. Brzezowski 09c7cd98d9 fix(ui): align agent switcher rows on one leading column (#121705)
* fix(ui): align sidebar menu rows on one leading column

The sidebar dropdown family never received the Web Awesome shadow-row
corrections that .session-menu__item already documents, so the agent
switcher stacked our 8px flex gap on top of Web Awesome's forced
`margin-inline-end: 0.75em` slotted-icon margin, kept a layout-only
native checkmark it never shows, and mixed a 24px avatar column with a
16px icon column. Agent rows started their labels 19px right of the
command rows and their avatars 11px right of the command glyphs.

Give the family one leading column: drop the stacked gap, hide the
agent menu's unused native checkmark (selection lives in the trailing
details rail), widen command glyph boxes to the avatar's 24px, and pull
the section label, provenance note, and empty state onto the rows' 8px
inline edge. Command-row label positions are unchanged; the avatar rows
and the More menu's route rows move onto the shared gutter.

* test(ui): assert the sidebar menu's shared leading column

The alignment fix had no committed regression: the existing agent-menu
E2E opens and navigates the menu but never reads its geometry, so the
19px label drift could return silently.

Assert in the browser that an avatar row and a command row start their
leading slot and their label on the same x. Fails on pre-fix code, where
the avatar column sits 11px right of the command glyphs.
2026-08-11 01:50:55 -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 c896b9b2cd test(ui): remove duplicate update confirmation cases (#121836) 2026-08-10 21:40:03 -07:00
Peter Steinberger ee7fae8a90 fix(ui): native command settings no longer require Raw mode (#121832)
* fix(ui): render native command settings controls

Render boolean-or-auto command settings as safe On/Off/Auto controls instead of forcing Raw mode, while preserving typed config values and fail-closed handling for unsafe unions.

* fix(ui): preserve oneOf union exclusivity
2026-08-10 21:28:06 -07:00
Peter Steinberger 45e4c838d0 feat(ui): show live subagent activity rows (#121840)
* feat(ui): show live subagent activity rows

* test(ui): split subagent activity coverage
2026-08-10 21:08:27 -07:00
Peter Steinberger 29bd7e462f fix(ui): hide agent selectors for single-agent setups (#121826)
* fix(ui): hide redundant agent selectors

* test(ui): split agent selector regression

* test(ui): keep selector e2e fixtures multi-agent
2026-08-10 20:55:54 -07: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 619f3c9ab1 feat(ui): identify project files in Control UI chat (#121775)
Recognized workspace paths in chat now read as their basename behind a
file-type glyph, so a file reference is identifiable before it is read.
Markdown, package manifests, TypeScript and other code, TSX/JSX components,
config/data, shell scripts, and images each get their own mark; anything else
falls back to a plain document. Paths sharing a basename keep the smallest
trailing suffix that tells them apart, and Windows paths keep their own
separator.

Classification wins over authoring syntax: a path written in backticks is a
file link first, so it drops the inline-code chip and renders exactly like a
bare path beside it. Code spans that are not file links keep the chip.

The full path stays addressable: it drives the file panel, the new tooltip,
and the message Copy action (which returns the original Markdown).
Author-written labels in [label](path) links are never rewritten, and text
that is not a recognizable path stays plain prose.

The glyph is painted as a masked ::before like the sibling GitHub mark, so it
stays out of the accessibility tree and out of copied text and follows the
link color in every theme. Extension classification moves to one shared
resolver that the file preview modal now uses too, replacing its own
code/text extension set.
2026-08-11 00:48:43 -03:00
Peter Steinberger b5d5ec340f feat(cloud-workers): add desktop apps and browser autonomy (#121475)
* feat(cloud-workers): add desktop apps and browser autonomy

provider-attested Browser/Terminal launchers, shared visible loopback CDP Browser tool, no MCP/cookies/generic command.

* feat(ui): add cloud desktop app launcher

* docs(gateway): document cloud desktop apps and browser autonomy

* perf(ui): trim desktop launcher startup copy

* refactor(ui): simplify desktop launch feedback

* perf(ui): reuse desktop app labels

* fix(ui): keep desktop launch failures actionable

* fix(crabbox): allow browser bootstrap to finish

* fix(cloud-workers): honor provider provision budgets

* fix(cloud-workers): persist browser screenshot receipts

* fix(cloud-workers): bound browser screenshot lifecycle

* fix(cloud-workers): avoid replaying desktop launches

* test(cloud-workers): isolate browser runtime integration

* refactor(cloud-workers): tighten desktop runtime boundaries

* test(cloud-workers): keep browser runtime mock synchronous

* fix(cloud-workers): break gateway type import cycle

* fix(ci): settle admitted setup sessions in tests

* build(plugin-sdk): refresh desktop app contract

* ci: refresh merge-tree validation

* build(plugin-sdk): regenerate desktop app baseline

* style(gateway): format merged method order test
2026-08-10 20:31:07 -07:00
ffyuuu efe5d8d76e fix(longcat): correct cache pricing and add provider icon (#121369)
* longcat: align baseUrl with docs, fix cacheWrite pricing, add brand icon

- Point baseUrl at the documented https://api.longcat.chat/openai/v1 path
  instead of the undocumented unversioned alias route.
- Fix cacheWrite pricing to 0: the LongCat pricing page has no separate
  cache-write charge (matches deepseek/moonshot/zai catalog conventions).
- Update the pricing page link to the current docs path.
- Add the official LongCat brand icon (provenance recorded in
  ATTRIBUTION.md) and display-name mapping so the Control UI shows proper
  branding instead of the letter-badge fallback.
- Migrate the persisted legacy default baseUrl via a plugin-owned doctor
  configRepair contract: onboarding persists
  models.providers.longcat.baseUrl and the runtime reads the stored value,
  so the contract rewrites exactly the former default to /openai/v1,
  preserves custom endpoints, and warns via a legacy-config rule.

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

* fix(longcat): correct provider metadata

* fix(longcat): migrate persisted stock pricing

* style(longcat): format doctor repair

* fix(longcat): preserve doctor migration after compat cleanup

* fix(longcat): avoid map spread in doctor repair

---------

Co-authored-by: yuzehui02 <yuzehui02@meituan.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Patrick Erichsen <patrick.a.erichsen@gmail.com>
2026-08-10 20:15:18 -07:00
Peter Steinberger 4e22cee056 perf(ui): format sidebar timestamps on demand (#121819)
Amp-Thread-ID: https://ampcode.com/threads/T-019fe957-0e49-707f-859f-9600ef536125

Co-authored-by: Amp <amp@ampcode.com>
2026-08-10 20:09:52 -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
Vyctor H. Brzezowski 4ea11b93c1 fix(ui): open the session companion from one header toggle (#121652)
* fix(ui): open the session companion from one header toggle

The collapsed rail was a button floating over the message area whose click
set the display preference to "pill", so opening the companion took three
interactions and the first two landed on surfaces nobody asked for. On an
idle session the pill had no digest to show and degraded to a bare title
with an X and a chevron.

The control moves into the pane header action row beside background tasks,
workspace files, and split view, where it carries aria-expanded and closes
the panel when pressed again. Opening now expands transiently instead of
persisting "card", matching the automatic path from /btw and the transcript
selection actions; the pill stays as the ambient density for a running
digest. Closing drops the manual-open claim so an idle session falls back to
nothing rather than to an empty pill.

The panel's empty state offers three starter questions instead of a sentence
about being read-only, the digest band no longer renders when there is no
digest, the destructive thread clear moves into an overflow menu, and the
composer input is anchored to its footer.

Closes #121440

* test(ui): cover the session rail header action slot

* fix(ui): label the companion composer without the undefined sr-only class

The composer's screen-reader label used class sr-only, which no stylesheet
defines, so "Ask the session companion" rendered as visible text above the
input. An aria-label on the input carries the same information to assistive
technology and does not depend on a missing utility class.

The class is applied by roughly fifteen other Control UI modules and is
tracked separately; this change only removes the rail's dependency on it.

* refactor(ui): read the mirrored rail mode through one accessor

* docs(ui): state why collapsing the rail keeps observer visibility

* fix(ui): dock the session companion as a side column instead of a card

The companion opened as a floating card over the conversation on any pane
narrower than 1080px, which is most of them once a sidebar or a split pane
takes its share. It covered the transcript the operator was reading and read
as a pop-up chat rather than a surface belonging to the pane.

Docking is now the default whenever the column and a readable thread both
fit, measured against the width left after the workspace and task rails take
theirs — the same way the workspace rail decides. The docked rail claims
layout space, so the thread reflows narrower instead of being painted over,
and it drops the shadow, blur, and animation that made it read as an overlay.

Below that width no pane can hold two columns, so the companion becomes a
full-height sheet on the trailing edge, and a full-screen sheet under 768px.
It still covers the thread there, but as a surface that took the pane over
rather than a card hovering above it.

The starters now sit where the first exchange will land; centring stranded
them mid-column once the rail became full height.

* fix(ui): give the companion the app's send button and a bottom-left opener stack

The rail composer had its own ghost submit icon, which made a second send
pattern in a product that already has one. It now renders chat-send-btn with
icons.arrowUp — the same component and classes the main composer uses, so the
brand circle and every state come from one place instead of a lookalike.

The openers move out of the thread and stack against the composer they feed,
left-aligned, so the eye travels chips to input without crossing the thread.
The explainer keeps the thread's empty space to itself, centred on both axes.

Also drops the now-unused submit colour rule and three svg declarations that
strokeIcon already inlines.

* test(ui): bound the rail against its container, not the old card cap

Three browser assertions encoded the floating card's max-height (680px
desktop, 460px mobile). The docked rail and the narrow sheet are deliberately
full height, so those numbers no longer describe anything.

The invariant they protected is still real and still asserted: a long thread
scrolls inside itself instead of the rail outgrowing what contains it. The
bound is now the pane's own height on desktop and the viewport on mobile,
which is what "do not expand to swallow the layout" actually means.

* fix(ui): stop rail metadata inheriting the thread's scroll layout

Removing the duplicate __empty declaration took its closing brace with it, so
the pr-checks, timestamp and hint selectors merged into the thread's rule.
Each would have rendered as a 96px scrolling bordered flex column instead of
a muted line of metadata.

The narrow sheet also kept the base rule's shadow and blur, which are card
cues on a surface that is meant to read as part of the pane; it drops them,
and the docked rule stops repeating resets it now inherits.

The fixture grows a timestamp and a hint so a browser assertion can hold the
metadata and the thread apart — the empty-companion captures never rendered
either node, which is why this reached review.

* fix(ui): reset the undocked sheet's fade-in and cover the metadata layout

The expanded sheet reset the shared shadow and blur but still inherited the
rail's fade-in, so a narrow-width open animated in like the floating card the
redesign replaces. The reset now lives on the expanded rule and the docked
rule stops repeating what it inherits.

Coverage follows the defect rather than the fix: the browser fixture renders
PR checks alongside the timestamp and hint, and the undocked-sheet assertion
now reads computed shadow, blur, and animation instead of geometry alone.

* test(ui): compare rail metadata against the thread, not a literal

min-height computes to auto on these grid items, so asserting 0px failed on
the metadata the guard was meant to protect. The invariant is that the nodes
do not share the thread's rule, so compare against the thread's own value and
let it change without breaking the guard.
2026-08-10 23:23:52 -03:00
Peter Steinberger 36c3f38a3a improve(ui): make warm session switches settle faster (#121792)
* perf(ui): reuse sidebar session projection per update

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

* perf(ui): keep reply callback out of transcript renders

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

* fix(ui): invalidate chat rows on media updates

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

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-08-10 19:19:14 -07:00
Peter Steinberger 58a538767c fix(security): centralize provider diagnostic redaction (#121599)
* fix(security): centralize provider diagnostic redaction

* fix(workers): redact finishing diagnostics

* fix(security): fail closed on malformed diagnostics

* fix(security): redact media wrapper diagnostics

* fix(security): normalize diagnostic field redaction

* fix(security): redact diagnostic media URIs

* fix(ai): preserve diagnostic compatibility in error projection

* refactor(ai): simplify diagnostic error handling

* fix(security): redact credential query diagnostics

* fix(security): redact diagnostic media arrays

* fix(security): redact diagnostic credential headers

* test: align shared expectations with current runtime contracts

* test(ui): align managed media and picker expectations

* fix(security): redact plural media diagnostics

* fix(ai): preserve bracketed provider errors

* fix(security): cover generic diagnostic credentials

* fix(security): redact prefixed diagnostic JSON

* fix(security): redact embedded diagnostic JSON

* test(agents): align cache trace media redaction

* fix(security): redact cookie diagnostics

* refactor(ai): isolate diagnostic host policy

* refactor(ai): narrow diagnostic helper exports

* fix(security): bound diagnostic descriptor reads

* fix(security): bound diagnostic graph traversal
2026-08-10 18:53:45 -07:00
Peter Steinberger b6b937d6ac chore(types): give test helpers nameable exported types (#121783) 2026-08-10 18:17:50 -07:00
Peter Steinberger dd2aedf08f refactor: burn export name collision debt (#121767)
* refactor(tasks): disambiguate lifecycle helpers

* refactor(channels): clarify account and id helpers

* refactor(media): separate SDK facades from cores

* refactor(auto-reply): consolidate command helpers

* refactor(cli): disambiguate registration helpers

* refactor(gateway): clarify runtime and test helpers

* chore(scripts): burn resolved collision debt

* fix(refactor): update remaining core callers

* fix(refactor): update remaining UI callers

* chore(plugin-sdk): refresh API baseline
2026-08-10 18:17:13 -07:00
Peter Steinberger 32894a3ba5 feat: start sessions from registered projects (#121465)
* feat(gateway): add project registry sessions

* fix(gateway): authorize project branch discovery

* test(gateway): reuse worktree test imports

* fix(protocol): register projects schema owner

* fix(gateway): gate project paths by write scope

* feat(control-ui): register projects from folder browser

* refactor(gateway): extract session diff baseline preparation

* fix(gateway): honor admin scope in project listings
2026-08-10 17:56:12 -07:00
Peter Steinberger e5de0a8434 refactor(ui): simplify retained chat sessions (#121777)
Amp-Thread-ID: https://ampcode.com/threads/T-019fe957-0e49-707f-859f-9600ef536125

Co-authored-by: Amp <amp@ampcode.com>
2026-08-10 17:50:03 -07:00
Vyctor H. Brzezowski 1d989027cf feat(ui): let operators reorder queued chat messages (#121682)
Queue position becomes an explicit, operator-owned fact on ChatQueueItem
instead of an accident of arrival time and storage array order. One
comparator now serves the visible projection, the drain head selection,
the steer rebuild, and the alias merge, so what the composer shows is
what the Gateway receives.

Reordering lives entirely on one handle at the left of a queued row:
drag it, or focus it and press the arrow keys. Keyed rows keep focus on
the handle as it moves. Rows already handed to a run keep their place.

Also routes .chat-image-action through var(--cursor-action); its
hardcoded pointer was failing the cursor-policy test on main.
2026-08-10 21:44:40 -03:00
Vyctor H. Brzezowski 41d023ab8a fix(ui): give the chat header a diff glyph, a task glyph, and one centerline (#121739)
* fix(ui): give the chat header a diff glyph, a task glyph, and one centerline

The session-changes action shipped icons.gitBranch, which reads as branching,
not as changes; background tasks shipped icons.activity, a heartbeat line that
says nothing to a developer about work running behind the thread. Both actions
now carry the glyph their tooltip promises: a Lucide file-diff (file plus +/-
marks) and a Lucide list-with-checks, applied at every entry point to each
action so one command never shows two icons.

Measuring the rendered header also found the row's controls disagreeing about
their box: openclaw-chat-header-session-menu is a custom element, so it
blockifies as a flex child while keeping a text line box, and the descender
space under its button rendered it 29.8px tall and dragged its glyph 0.9px
below the 18px icons beside it. The pill chips stood 26px against the 28px
icon buttons, the branches trigger drew a 14px glyph among 18px siblings, and
the rename affordance's padding shifted the title 5px sideways the moment
renaming was allowed. The actions row now declares one box model for every
child, the chips adopt the shared 28px control height, and the title's hover
padding is pulled back out of the layout.

* fix(ui): optically center chat session title

* test(ui): mirror the shipped title markup in the split-header fixture

The optical-center commit moved the title's ellipsis onto an inner
.chat-pane__session-title-text span, so the split-header fixture's bare
.chat-pane__session-title no longer had white-space: nowrap or
text-overflow: ellipsis. Its deliberately long title wrapped instead of
truncating, which still satisfied the close-button assertion but stopped
exercising the ellipsis path the fixture exists to guard.
2026-08-10 21:07:15 -03:00
Vyctor H. Brzezowski 94490bd69b feat(ui): show GitHub link identity in Control UI chat (#121728)
* feat(ui): mark GitHub links in Control UI chat

Chat markdown anchors pointing at github.com now carry a decorative GitHub mark before their text, so a bare URL and a [#3434] shorthand read as the same destination. The mark is painted from the bundled brand path through a CSS mask on currentColor: it stays out of the accessibility tree and out of copied text, tracks the chat text size, and follows the link color in every theme. Image-only links such as badges and code spans stay undecorated.

* feat(ui): break long autolinked URLs at any character in chat

An autolinked URL is one unbreakable word, so the inherited overflow-wrap only splits it after every other option is exhausted: a long URL was pushed whole onto the next line, and with the new GitHub mark ahead of it the mark was left stranded alone at the end of the previous line. Autolinked anchors now carry markdown-bare-url and get unconditional character break opportunities, so the URL fills the line it starts on. Authored link labels keep word-boundary wrapping.

* fix(ui): drop resting underline on chat GitHub links

* test(ui): split markdown link tests into a sibling file

markdown.test.ts crossed the 1000-line oxlint budget once the GitHub-link
cases landed. Move the link-rendering describes (autolinks, explicit
protocol links, file links, bare-url marks, GitHub marks) into
markdown-links.test.ts. Pure move; no assertion changes.

* test(ui): assert GitHub links rest without an underline

Covers the resting/hover decoration contract through the real rendered
page, and pins the rule's scope with a non-GitHub link that keeps the
base underline.
2026-08-10 20:57:35 -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
Alix-007 a287c2d96b fix(ui): prevent failed plugin icons from retaining connections (#121129)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-08-10 18:12:41 -05:00
Peter Steinberger 1263450938 improve: start fresh gateways without migration overhead (#121639)
* Optimize fresh gateway startup migrations

* Fix startup optimization CI gates

* Fix compact startup CI coverage

Amp-Thread-ID: https://ampcode.com/threads/T-019fec00-3abd-710b-b9de-8925c23ad779

* Fix stale managed image CI coverage

* Fix stale model setup route expectation

* Fix setup admission lint

* Fix system agent setup test isolation

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-08-10 15:01:37 -07:00
Peter Steinberger 0b0d9f5811 fix(ui): prevent terminal garble after Gateway reconnect (#121637)
* fix(ui): recreate terminal runtime after replay gaps

Replace Ghostty's in-place reset with a fresh controller/runtime for authoritative recovery replay so Gateway reconnects cannot expose stale WASM terminal state. Preserve replay ordering, focus and tab visibility, and dependency cleanup.\n\nFixes #121636

* test(ui): fix terminal replay type coverage

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

* test(discord): align progress fixtures with callbacks

* fix(ui): restore focus after cancelled terminal replay

* chore(ci): drop superseded baseline fixups

* fix(ui): stage terminal replay replacements safely

* fix(ui): serialize asynchronous terminal replay

* fix(ui): guard superseded terminal replay recovery

* fix(ui): invalidate superseded terminal replay callbacks

* fix(ui): keep staged terminal replay inert
2026-08-10 13:27:28 -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 97bd54ded7 fix(sessions): prevent failures and stale state under rapid use (#121680)
* fix(sessions): harden lifecycle reliability and listing speed

Amp-Thread-ID: https://ampcode.com/threads/T-019febfb-f949-77cd-b15b-d31d0b42d2fb

* test: repair strict type fixtures

Amp-Thread-ID: https://ampcode.com/threads/T-019febfb-f949-77cd-b15b-d31d0b42d2fb

* chore(plugin-sdk): refresh current main API baseline

Amp-Thread-ID: https://ampcode.com/threads/T-019febfb-f949-77cd-b15b-d31d0b42d2fb

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

Amp-Thread-ID: https://ampcode.com/threads/T-019febfb-f949-77cd-b15b-d31d0b42d2fb

* test(cron): preserve upstream assertion order

Amp-Thread-ID: https://ampcode.com/threads/T-019febfb-f949-77cd-b15b-d31d0b42d2fb

* fix(gateway): settle setup admission test sessions

Amp-Thread-ID: https://ampcode.com/threads/T-019febfb-f949-77cd-b15b-d31d0b42d2fb

* test(ui): sample wizard action geometry atomically

Amp-Thread-ID: https://ampcode.com/threads/T-019febfb-f949-77cd-b15b-d31d0b42d2fb

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-08-10 12:58:59 -07:00
Peter Steinberger 084ed7f04b fix(ui): stabilize mobile drawer focus E2E (#121663)
* test(ui): await native drawer focus settlement

* test(ui): distinguish browser chrome focus
2026-08-10 12:56:19 -07: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