mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
pash/codex-native-future-models
16 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9b43f1c82e |
improve(ui): fix sidebar session row hierarchy and add a message-preview toggle
Pinned sessions inherited the nav zone's muted colour on the session *title*, so a pinned row's preview line outshone its own name and the same session read dimmer pinned than unpinned. A title is content in every zone; only glyphs, meta and the hover fill follow the zone. The subtitle drops to plain --muted, already proven AA against every surface by theme-contrast.test.ts. Rows with no preview text reserved a fixed 18px second line anyway, leaving a dead band or a lone spinner hanging below-right of the title. They now collapse to one line via a --single-line class plus a CSS variant, so the endcap rides beside the title. Rows also gained a little vertical air and reclaimed the right-hand gutter inside the row, leaving the deliberate scrollbar clearance from #124879 intact. Adds a localStorage-backed "Show message preview" toggle to the session sort menu (default on). Operator-actionable state is exempt: attention, the queued concurrency-slot explanation, and critical observer headlines (stuck / waiting-on-user) always show, because hiding them behind a display preference is the silent-failure class. Also registers extensions/codex/src/app-server/run-attempt-tools.test.ts, which arrived in #126189 with no shard claim and left main red. |
||
|
|
a928da457f |
Show worktree option only for Git group folders (#125280)
* feat(ui): show worktrees only for Git group folders * feat(ui): enrich group environment picker * fix(ui): preserve worktree defaults on probe errors * fix(ui): reject stale group repository probes * test(ui): type repository inspection helper |
||
|
|
b5b17c654b |
fix: keep Workboard visible in the sidebar (#125473)
* fix(ui): keep workboard in sidebar navigation * test(ui): align workboard navigation ownership * fix(ui): preserve saved workboard sidebar slots * test(ui): shrink assertion safety baseline * fix(plugins): bind native routes to bundled owners * refactor(plugins): isolate native route policy |
||
|
|
55ce95fac8 |
feat(sessions): three-layer session ownership — agent attribution, assignable owner, participants (#125057)
* feat(sessions): stamp agent identity on spawned sessions and return spawn receipts Agent-spawned sessions recorded the requesting session key as createdActor.id, so the Control UI creator chip rendered an opaque key. Spawn producers now stamp the canonical requester agent id; parent-authority validation moves to a new trusted requesterSessionKey field. projectSessionActor enriches agent actors with configured identity name/avatar at read time, and visible sessions_spawn returns a sessionUrl + owner receipt with URL-first acknowledgement guidance. * feat(sessions): assignable session ownership with owner facet and menus GitHub-assignee-style ownership: sessions get a mutable owner (defaulting to the immutable createdActor) stored in additive bare-nullable SQLite columns with first-use lazy ensure. New operator.write sessions.assignOwner validates targets, requires an identified caller, authorizes by session visibility, and records assignedBy/assignedAt inside the write transaction. The sessions agent tool gains assign_owner; the Control UI adds Assign-to-me/Assign-to menus in sidebar rows and chat headers, renders the effective owner chip, and the creator facet/filter now keys on effective owner. Sharing authority stays anchored on createdActor. * feat(sessions): record session participants and stack them in the owner chip Records every distinct external prompter (human profile/channel sender, or a requesting agent) per session in an additive session_participants table at the turn-admission boundary — best-effort, deferred, never blocking the turn; the session's own agent and viewers are never recorded, capped at 32 per session. The session row projects a bounded participants list (owner excluded) plus a total count with the same actor enrichment as owner/createdActor. The sidebar chip becomes a pair-stack when others have prompted (owner front, one peeking participant or +N behind), the chat header shows the full facepile, and an authenticated involvingMe list filter adds an Involving-me sidebar predicate. Participant projection is excluded from logical-session CAS equality so display history never invalidates session writes. * fix(sessions): identify built-in agent tool callers for owner assignment The sessions tool's assign_owner dispatched through the in-process synthetic client, which carries neither a signed agent-runtime identity nor a human profile, so agent-initiated reassignment always failed with FORBIDDEN. The tool now captures its trusted requester agent identity and carries it across in-process dispatch as internal client state (never wire params); the handler derives assignedBy as signed runtime identity, then trusted agent-tool caller, then authenticated human. Live-verified end-to-end on a dev gateway. * fix(ci): split oversized session modules and refresh prompt snapshots Split the max-lines offenders at concept boundaries for session equality, tool overrides, and protocol owner schemas. Remove the redundant Number conversion from the node:sqlite participant count. Refresh prompt snapshots after drift from the sessions and sessions_spawn tool description updates. * fix(ci): restore solo-mode chip suppression and conform new method descriptors Solo-mode root cause: owner-assignment submenu options reused the permanent owner-chip custom element, so hidden menu avatars were counted as attribution chrome. Menus now use viewer avatars while gateway-gated owner chips remain exclusive to collaborative sessions. Conform sessions.assignOwner to the 2026.8 descriptor and append-only advertised-method inventories, and regenerate the Swift and Kotlin protocol surfaces. Keep historical v15/v14 fixtures frozen by stripping the new owner columns; the existing range already excludes the participant table. Replace the new raw SQLite schema probes with synchronous Kysely queries. Clear max-lines by splitting the organizer host contract, pure agent-navigation projections, and ownership/filtering sidebar cases at their concept boundaries. * fix(ci): integrate ownership series with latest main surfaces Wire the sessions-page assign-owner action, merge capability imports, narrow the navigation export scope, and apply sessions-create formatting. The owner-presence regression came from hidden assign-owner menu avatars emitting data-viewer-id, so owner and menu chrome now opt out of presence markers while real facepiles retain them. * fix(sessions): scope the involving-me filter to profile-backed participants Session participant history mixed channel-native sender ids with authenticated Gateway profile ids, so involving-me missed real sessions and could accept numeric collisions. Record the actor_source namespace at each producer, carry it through the internal SQLite projection, and match authenticated viewers only against profile-backed human participants. Legacy NULL sources fail closed for filtering, while channel ids remain available for display. * build(ui): raise startup budget baseline for session ownership surfaces Ownership chips, assignment menus, and the participant stack add ~0.7 KiB gzip to the startup path; CI compression landed just over the previous baseline+tolerance. Hard cap (350 KiB) unchanged. * refactor(sessions): drop raw NULL projection for the lazy actor_source column The Kysely guardrail rejects typed raw sql snippets outside allowlisted boundaries; select the lazily-ensured column only when present and let the row projection treat its absence as unknown/legacy. * build(ui): refresh combined startup baseline |
||
|
|
bf4ec4b58a |
fix(ui): disambiguate multi-account session labels (#124228)
* fix(ui): disambiguate multi-account session labels Parse the optional account segment on per-account-channel-peer DM and group keys so Control UI no longer falls through to the raw routing key. Append a non-default account as " · accountId" on label, displayName, and fallback so two Alice rows from different Telegram accounts stay distinguishable. Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca> * fix(ui): take the session account from the gateway row The display resolver was reading the account out of the session key and inferring it from label text. That missed the shipped `dm` spelling of direct keys, invented an account-qualified group shape no key builder emits, and treated any name ending in "(<account>)" as already disambiguated. The Gateway already projects the canonical `accountId` onto every session row from the delivery route, so display consumes that and parses the key only for panes rendered before their row arrives. Key parsing now covers both `direct` and the pre-#11881 `dm` spelling, group keys go back to their canonical account-less shape, and the account suffix is applied at a single point instead of five. `resolveChannelSessionInfo` classifies `dm` keys as channel sessions and no longer reads a peer kind sitting in the channel slot as a channel. * fix(ui): stop inferring session identity from rendered text Three related text-inference defects in the same display neighborhood: The account suffix was skipped when the resolved name already ended in " · <account>". That reads rendered output as if it were a fact, and it cannot tell an account apart from a user label that happens to look like one. The suffix is now appended from the recorded account alone. The string it was defending against came from the sidebar rename dialog, which pre-filled with the resolved display name. Submitting persisted a derived string as a real user label, which then outranked every later derivation. Sidebar rows now carry the stored label alongside the resolved one, and rename edits that, matching the Sessions page. Channel classification accepted an account segment before any peer kind, so `agent:<x>:<channel>:<account>:group:<id>` was filed under a channel even though accounts qualify direct chats only and the canonical route parser rejects the shape. The grammar now allows an account only before `direct`/`dm`. Resolver tests now prove the row projection is authoritative rather than duplicating an account the key already carries: one case where only the projection supplies it, and one where the two disagree. * ci: retrigger after check-lint shard cancel Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca> * test(ui): prove rename opens on the stored label The existing sidebar rename coverage uses a row that already has a stored label, so it passes whether rename pre-fills the stored label or the resolved display name. It could not catch the round-trip this branch repaired. Adds a browser case on a displayName-only row with a projected account: the sidebar shows "Alice · cards" and the rename field opens blank. On the parent commit the field opens holding "Alice · cards", which is the string that used to become a real user label on submit. * fix(ui): do not double-append a stored account suffix A persisted label that already ends in " · accountId" must stay as-is so "Alice · cards" does not become "Alice · cards · cards". Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca> * docs(ui): name the producer the account suffix check defends The comment above withAccountDisambiguator claimed the rendered name is never inspected, which the suffix check contradicts. State the real contract instead: the account itself still comes only from the recorded value, and the endsWith check is idempotence against the chat pane's inline rename, which seeds its input with the rendered title and persists a partial edit verbatim. --------- Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca> Co-authored-by: vyctorbrzezowski <krzyszchweski@gmail.com> |
||
|
|
edb5adfbf5 |
Start new sessions with folder group defaults (#123276)
* feat(ui): add folder group session defaults * fix(ci): align folder group contracts * fix(protocol): refresh Android gateway methods * fix(ui): reuse folder picker for group defaults * fix(ui): harden session group defaults * test(ui): align group defaults with current main * test(ui): keep group catalog defaults path-free * fix(ui): close folder group CI gaps * fix(ui): satisfy folder group CI contracts * fix(session-groups): enforce defaults safety * test(gateway): keep group defaults in workspace * fix(session-groups): enforce defaults participation * fix(session-groups): close review authorization races * fix(session-groups): canonicalize defaults authorization --------- Co-authored-by: Jesse Merhi <jesse-merhi@users.noreply.github.com> Co-authored-by: Jesse Merhi <openclaw@users.noreply.github.com> |
||
|
|
89df45c0f5 |
fix(ui): make DONE moves visibly reversible (#122303)
* fix(ui): make group category moves reversible * fix(ui): address native Groups drag review |
||
|
|
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. |
||
|
|
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> |
||
|
|
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. |
||
|
|
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. |
||
|
|
ce133d4a35 | fix(ui): use in-app dialog for session rename (#121255) | ||
|
|
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 |
||
|
|
2de58d408a |
Control UI: gateway-owned sidebar section order (#113930)
* feat(ui): gateway-owned sidebar section order with hover-only drag grips * fix(ui): repair sidebar section order CI gates * fix(protocol): keep sectionOrder optional for older gateways Native Swift clients ship separately from gateways, so new clients must decode older gateway responses that omit sectionOrder. * fix(ui): reconcile gateway section ordering with main * fix(state): allow lazy tables in v5 maintenance * refactor(ui): retire prefs session section order in favor of gateway-owned order Supersedes #113948 by deleting the unreleased ui.prefs.sessionSectionOrder key and its browser plumbing. Gateway SQLite sidebar_sections remains the single canonical store. |
||
|
|
f90cef67c8 |
feat(ui): drag custom sidebar groups between built-in session zones (#113948)
* feat(ui): drag custom sidebar groups between built-in session zones * test(ui): remove stale split ratio fixture * perf(ui): raise startup budget baseline for session-section ordering pref |
||
|
|
b74f04ada8 |
refactor(ui): move sidebar mutations and groups into one organizer controller (#112923)
* refactor(ui): move sidebar mutations and groups into controllers * refactor(ui): lazy-load session organizer operations * fix(ui): order organizer mutation scope before options |