* feat(ui): composer capability menu with per-session overrides
* refactor(ui): keep chat render within lint ceiling
* fix(ui): preserve loaded capability overrides
* fix(ui): remove stale capability menu field
* fix(ui): gate capability menu on runtime-config readiness
Chat kept optimistic thinking-level and fast-mode row patches only in the
host copy of the session list. Any mid-flight capability publish (for
example the loading flip at the start of the post-patch list refresh)
replaced that copy wholesale and reverted the patched row until the
refreshed canonical list landed. The reasoning slider visibly snapped
back to the inherited default during that window and keyboard commits
landing inside it were swallowed, which is the race behind the flaky
chat-composer-redesign thinking-slider e2e assertion on CI.
patchSessionRow now mirrors the optimistic fields into the published
capability snapshot via a new SessionCapability.patchRowLocal, matching
how model overrides already live capability-side, so intermediate
publishes carry the patch and the revert window is gone.
* feat(ui): resolve session URLs through the gateway with best-effort slug matching
Session URLs now resolve a stored board face and a display-name slug through
the gateway instead of guessing from the browser's cached session page.
- An uncached open marks only the in-app navigation, never the shareable href,
so the chat loader can re-derive the face and replace the URL.
- A cached row settles the lookup locally, keeping navigation off the wire.
- A one-segment reference resolves short id, then literal key, then display-name
slug; ties reuse the existing disambiguation view.
* refactor(ui): keep slug ties on short disambiguation prefixes
Slug candidates reused the full uuid, which produced 32-character links in the
disambiguation view. uniqueShortIdPrefix already falls back to the full id when
the bounded search was truncated, so reuse it and drop the flag.
* fix(ui): navigate worktree owner links in-app so the face marker survives
The Worktrees owner link was a bare anchor consuming only the shareable href,
so preferenceDerivedFace was a no-op there and an uncached owner always opened
on chat. Match the other session surfaces: keep the clean href and navigate
with the options on plain clicks.
* fix(ui): search the first uuid block so long session references resolve
The gateway matches sessions.list search as a plain substring and stored keys
hold a hyphenated uuid, so any hyphen-stripped reference past the first block
matched nothing and 404'd. This hit disambiguation links, which offer prefixes
longer than eight characters. Send the first block and keep applying the full
prefix per row.
A resolved slug now canonicalizes to that same short reference instead of a
full uuid, so /chat/main/deploy-monitor settles on -6db92d48.
* docs(ui): record why the face marker stays out of shareable hrefs
* feat(ui): durable session board face and dashboards index
Board face lived only in client-side boardSessionViews, capped at 50 entries,
so the preference never followed the user to another device, evicted as
sessions accumulated, and could not be seen as a set.
Persist it as SessionEntry.boardFace, which rides the existing entry_json blob
and so needs no SQLite schema change or version bump. Expose it on the session
list row and add it to the sessions.patch write-scope allowlist alongside label,
pinned, and archived: setting your own view preference is user-level chat
organization, not policy. Unknown patch fields still fail closed to
operator.admin.
Generic navigation now reads the stored face, so the sidebar and session list
open a thread on the face you left it on. boardSessionViews keeps only
activeTabId and reopenDockByTab, which are genuinely per-device.
Add /dashboards listing threads whose preferred face is dashboard. Filtering
runs server-side in filterSessionEntries before pagination, because the client
holds only a capped page and a client-side filter would silently omit
dashboards.
* test(protocol): assert the pre-rename face param is rejected
The gateway-protocol validator test still passed the pre-rename 'face' key,
which the closed schema rejects. Use boardFace, and pin the old name as a
negative case so it cannot silently return.
* chore(protocol): regenerate Swift bindings and docs map for boardFace
Adding boardFace to the sessions schema changes two committed generated
artifacts: the Swift gateway models (pnpm protocol:gen:swift) and the docs map
(pnpm docs:map:gen), which now lists the dashboards index section.
* feat(ui): path-based session and dashboard URLs
* docs(ui): document control UI URLs
* fix(ui): finalize session path routing
* feat(ui): anchor session URLs on stable keys
* docs(ui): clarify stable session URL identity
* fix(ui): resolve session prefixes with full prefix and pagination
Query sessions.list with the full supplied prefix instead of an eight-char
truncation, and paginate up to a bounded page count so longer disambiguation
links resolve instead of being reported ambiguous. Zero strict-prefix matches
now fall through to literal-key resolution rather than rendering an empty
ambiguity view.
Also document the ~dot/~dotdot segment escape: peer ids reach session keys
trimmed and lowercased only (src/routing/session-key.ts), so a literal '.' or
'..' segment is reachable and browsers would normalize it away.
* fix(ui): synchronize committed session routes
* test(ui): split native shell host coverage
* fix(ui): thread configured mainKey through session URL builders
Reserved-set disambiguation needs the operator-configured mainKey at runtime, so
thread it from agentsList through every session path builder and the ClickClack
control URL. Unambiguous non-hex single-segment rests now resolve literally while
short-id-shaped rests still fail closed, which restores ClickClack channel
compatibility detection and control-link reconciliation.
Also bound prefix-resolution retries, preserve catalog thread identity, and keep
draft state on ambiguous candidate links.
* fix(ui): repair session URL CI integration
* perf(ui): lazy-load session route resolution
* perf(ui): isolate session prefix resolution
* perf(ui): defer session path parsing
* perf(ui): defer session navigation startup
* fix(ui): preserve first-run and literal session navigation
* fix(ui): satisfy session routing type and export gates
* fix(clickclack): preserve unscoped control link agent
* style(ui): satisfy chat page line limit
* refactor(ui): move chat page helpers to owning modules
* fix(ui): preserve destination session route identity
* fix(ui): preserve agent identity in session routes
* fix(ui): escape dots in literal session path segments
encodeURIComponent leaves periods intact, so a literal key segment like
channel:release.js produced /chat/main/channel/release.js. In-app navigation is
intercepted by the SPA, but a refresh, an external link, or a ClickClack link
would be served as a static asset request and never reach the app.
pathForWorkboardBoard already escapes dots for this reason; mirror it in both the
session URL contract and the ClickClack encoder, route the agent id through the
same segment encoder, and pin the case in both shared vector tables.
* fix(ui): stop bootstrap after teardown race
* refactor(ui): centralize session navigation targets
* fix(ui): consume bootstrap teardown abort
* fix(ui): canonicalize configured main session routes
* fix(ui): preserve distinct session references
* fix(ui): redirect released session query links
* fix(ui): make bootstrap teardown abort-safe
* 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.
* 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
* feat(ui): add persisted sidebar layout model
* feat(ui): render flexible chat sidebar columns
* test(ui): align board sidebar fixture type
* fix(ui): preserve sidebar panels across responsive layout
* fix(ui): satisfy sidebar CI ownership and performance
* test(ui): derive discussion helpers from panel config
* test(ui): mount session rail through its registry
* fix(ui): preserve sidebar state across projections
* style(ui): format sidebar state fixes
* fix(ui): satisfy sidebar lint constraints
* refactor(ui): break sidebar layout import cycle
* fix(ui): stabilize sidebar panel rendering
* fix(ui): keep the narrow sidebar grid off for an empty layout
The two-row narrow grid reserved a panel row even with no sidebar panel open, halving the primary surface height on every default mobile chat pane.
* fix(ui): lazy-load chat sidebar region
* style(ui): format rebased chat state page
* chore(ui): raise sidebar startup baseline
* fix(ui): preserve sidebar move and resize state
* chore(ui): align sidebar startup baseline
* chore(ui): refresh sidebar startup baseline
* test(ui): register discussion element in isolated test
* fix(ui): persist the dragged panel as the collapsed active panel
Drag moves activated the panel in its destination column but left the separate persisted collapsed-mode selection stale, so the narrow layout foregrounded the wrong panel after a move and the stale choice survived reload.
* fix(ui): preserve resolved canvas URL in detail panel
* perf(ui): refresh the session list once per batch action
Batch sidebar actions patched each row and then forced a full sessions.list
replacement per row, so archiving a nine-row multi-select paid nine list
rebuilds on top of nine patches. Measured against the gateway handlers on a
60-session store, sessions.list costs ~2.9s versus ~176ms for sessions.patch.
Batch rows now pass deferListRefresh and each batch helper issues one refresh
per owning agent after its last row; pushed sessions.changed events keep rows
reconciled while the batch runs. Single-row actions are unchanged.
* test(ui): prove batch archive costs one list refresh in the browser
Adds a Control UI e2e case that cmd-clicks three sidebar rows, archives the
selection from the batch menu, and asserts the Gateway saw three sessions.patch
calls in row order plus exactly one sessions.list. It holds past the batch so a
late per-row refresh would still be caught.
Before the batch-refresh change this flow issued one full sessions.list per
archived row.
* feat(gateway): add web-only incognito sessions held in process memory
* feat(ui): add incognito toggle and badges to the web new-session flow
* fix(sessions): classify incognito by key shape, fail closed on stale keys, and gate memory writes
* fix(codex): start harness threads ephemeral for incognito sessions
* fix(sessions): reshape internal-effects incognito keys and add doctor repair for reserved key collisions
* refactor(plugin-sdk): export canonical incognito key classifier and guard the sentinel path
* fix(state): classify incognito DB handles from the recorded open-time set
* fix(gateway): isolate incognito sessions from durable lineage and allocation on read-only misses
* docs(sessions): pin the reserved incognito namespace ownership decision
* feat(gateway): admin-scope incognito visibility and incognito-blind cross-session surfaces
* fix(ci): repair kysely guardrails, dead export, docs map, protocol bindings, and ACP reset rotation
* fix(gateway): remove non-admin observability side channels for incognito sessions
* fix(gateway): enforce admin-scope incognito access and cover all parent-reference creation paths
* refactor(ui): model gateway connection state as one closed phase union
ApplicationGatewaySnapshot carried connected + reconnecting as parallel
booleans whose illegal combination (both true) was representable, and
every consumer re-derived the same flag logic. The snapshot now carries
phase: "stopped" | "connecting" | "connected" | "reconnecting" |
"offline"; offlineStable stays as the store-owned 2s debounced
presentation boolean, and lastError/lastErrorCode are unchanged.
Mapping preserves shipped behavior exactly: never-connected terminal
closes return to "stopped" (login gate), established-connection drops
with retry are "reconnecting" (shell stays mounted), without retry
"offline". Component props stay boolean, derived once at the app-host
boundary; no compat aliases remain on the snapshot.
Closes#112741.
* fix(ui): migrate rebased-in test fixtures to the gateway phase union
* fix(ui): migrate post-rebase gateway snapshot readers to the phase union
* fix(ui): migrate document-title test harness snapshot to the phase union
* feat(sessions): first-class archived-session handling
Archived sessions are now exempt from every automatic maintenance path
(age prune, entry cap, model-run prune, disk budget, cleanup repairs);
only explicit sessions.delete removes them. sessions.list gains an
additive tri-state archived filter (true | false | "all"). The sidebar
gets a persisted Active/Archived/All status filter with inline dimmed
archived rows, replacing the nav-away View-archived button; the Sessions
page toggle becomes the same tri-state with dimmed+badged rows in All
mode; the chat composer's archived notice becomes a full-width banner
with an Unarchive action.
* chore(protocol): regenerate Swift gateway models for tri-state archived filter
* feat(anthropic): import Claude Desktop custom groups
* fix(anthropic): resolve Claude Desktop groups from LevelDB entries
Group discovery regexed whole decompressed blocks, so matches were not
attributable to a Local Storage value and byte order decided the winner.
On a real 5.4MB store that mislabelled 59 of 159 sessions, surfacing a
mojibake label spliced out of Snappy copy-record bytes.
Parse SSTable entries properly instead: prefix-delta keys bounded by the
restart array, newest internal sequence per user key (honoring deletions),
and record scanning confined to a single value. Values are normalized so
Chromium's UTF-16 form scans like Latin-1, and unflushed WAL writes keep
precedence over SSTables.
Prod LOC grows ~77; it buys structural correctness in place of ordering
luck, and folds the old index-only walk into one shared entry decoder.
Verified against the live store: 159 assignments, 0 mislabelled versus an
independent entry-level ground truth, 0 control-character labels.
* fix(ui): sort custom session groups ahead of project groups
Custom groups were pushed into the section list as encountered, so their
position depended on roster order rather than the documented behavior;
the existing test only passed because its fixture happened to be ordered
that way. Collect custom and project groups separately and concatenate,
and assert the guarantee with a reversed-input case.
* fix(anthropic): widen LevelDB fixture key type for test typecheck
The prefix-delta helper assigned a Buffer into a Buffer-typed accumulator
whose generic argument differed, which tsgo rejects in the test lane.
* chore(anthropic): drop release-owned changelog edit from the PR
CHANGELOG.md is generated at release time, so a normal PR must not carry
an entry. The release-note context lives in the feature commit message and
the PR body instead.
* feat(ui): finish the chat session → thread rename across the Control UI
Completes the #110933 creation-surface rename mechanically: Threads page
(title, filters, archive/delete/rename/pin menus, transcript search),
sidebar labels and menus, welcome suggestion, palette category, tools
availability copy, workboard lifecycle/card strings, dreaming history,
board/diff/workspace-rail chrome, and the untitled main/channel fallback
names. Technical identifiers stay: Session ID, session keys, terminal
sessions, protocol wording. Living docs updated (control-ui.md);
historical release notes left as shipped.
* style(ui): format session-management e2e
* fix(ui): finish thread rename in navigation labels and stale test literals
Codex review caught the palette Threads entry and the Worktrees owner
column still saying Session; CI caught test literals asserting the old
copy (Threads page overview tile, filter tooltips, group counts, catalog
terminal notice, create-rejection error, workboard lifecycle hint). Also
completes the usage-stat cluster (Threads + hints) and the tool-catalog
group label. Native app strings (iOS/Android/macOS) keep session wording
and reconcile through their own i18n pipelines as a follow-up.