Repository navigation
feat(workspace): ⌘/ focuses the sidebar search, and the rail shows shortcut tips (ent#621 follow-up) - #3249
Conversation
…rinity-enterprise#784) Replaces `landingThread` with one pure rule, `agentLanding`, that every door which has to RESOLVE a landing calls. ent#523 landed you in the chat you were most recently active in; most visits to an agent start new work, so resuming cost two actions every time. The default is now a new, empty chat. Nothing is minted by the landing: the helper is pure and returns a null session, and the row is born on the first send (`newThread`, ent#451), so repeated visits accumulate no empty chats. An unused Main is already kept out of the chat list by `sidebarThreadsOf` — the AC 4 guard, still green. `agentLanding` takes `lastOpenSessionId` as the seam ent#621's agent-switch keys will pass, honoured only when it still names a live, unarchived chat of this agent in the principal's own thread list — so a stale or forged id falls back to the default rather than landing somewhere it should not (#3140 class). No Map is built here; ent#621 adds the state and the handler. `resolveAgentLanding` keeps its signature and delegates, so the `?agent=` deep link and the sidebar row cannot drift. `forceNew` stays accepted for existing `?new=1` links and is now redundant rather than wrong. Shell wiring (`landOnAgent`, the focus gate) follows in the next commit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…lityai/trinity-enterprise#784) Wires the shell to the ent#784 rule and gates the composer focus. `landOnAgent` is now synchronous. The awaited `ensureMainListed` is gone from the landing path — the rule needs nothing from the network — and with it the overtake race that awaited round trip required. The pinned Main is still minted on the first visit; `watch(activeAgentName)` owns that promise (ent#523) and no longer blocks the landing. The landed chat KEEPS `/workspace/a/:name` rather than escaping to bare `/workspace`, so a reload, a bookmark or a copied link still names the agent; the first send replaces it with the thread's own URL as today. An idempotency guard makes the second watcher fire (the thread list arriving) a no-op, so it cannot remount an unsent chat and throw away what was being typed. `landOnAgent` now asks `guardLeaveCall` BEFORE touching `activeAgentName`, which feeds `convKey`. Back/forward and a typed `/workspace/a/:name` reach the landing without passing a click door, so they could end a live voice call without a word (the ent#551 class; the click doors were already guarded). Focus is gated on WHY the fresh composer mounted, not on the pointer alone. A gesture (New chat, ⌘J, the agent picker, switch-agent) passes `always` and focuses on any pointer, keeping #2579 AC 2 on Android. A landing passes `fine-pointer` and focuses only where that cannot summon an on-screen keyboard — the "unprompted" the issue names. `shouldFocusOnRestore` is renamed `shouldAutoFocusComposer`: two reasons now share the one rule. Tests: the focus gate is proven by MOUNTING PortalConversation under a stubbed `matchMedia` (#2918), asserting `document.activeElement`, with focus shown to be elsewhere first. Reverting either the rule or the gate turns 10 cases red on behaviour — session ids and activeElement, not source text. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…y-enterprise#784) Tiered docs for a feature change: the owning area file, the requirement, and the feature flow. - `architecture/workspace.md` — `agentLanding` is the one landing rule and what it now answers; the synchronous `landOnAgent`, the kept URL, the guard order, and the `focusOnMount` mode. - `requirements/core-agent.md` — the ent#523 "One page" rule amended rather than rewritten: what reversed, and that landing mints no row. - `feature-flows/workspace-agents-at-the-centre.md` — the rule table, the "a tab is not a landing" note (the strip is now the only way back), the two doors that RESOLVE vs the gesture doors that ASSERT, and a `## Changed by ent#784` section rather than edited history. Names the follow-up: backend adoption of an empty Main on `new_thread=True` is not done here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ted chat (Abilityai/trinity-enterprise#784) ent#784's landing stays on `/workspace/a/:name`, so the watcher that resolves that route can now re-fire with the route unchanged — once per thread-list refresh — for as long as the person stays there. `landOnAgent`'s idempotency guard keys on `startingNewChat`, which the first send clears, so a refresh settling in the window between `onSessionAdopted` and the asynchronous `router.replace` nulled `pendingSession` and bumped `convGen`, remounting an empty composer over the thread whose first reply was streaming (and handing the `/c/:id` watcher a #3140 "chat isn't available" for the chat just created). Guarded in the watcher instead: a list-only re-fire is a no-op, keyed on "the route did not change on this fire" AND on having already landed this name. Both clauses are load-bearing — the name alone swallows the cold deep link, whose only fire IS the list arriving with the param already in place. Deliberately NOT keyed on `pendingSession`: back/forward from `/workspace/c/:id` arrives with a session set and must still land fresh (T4). New behavioural pin `portalAgentLandingRemount.mount.spec.js` (shallow-mounts Portal.vue, asserts the conversation's props and instance identity, not source text): the adopted-session case went red before this change with `sessionId: null`; the three regression cases — composing, same-agent back/forward, cold deep link — were green before and stay green. Also fixes `portalUnavailableTargets.mount.spec.js`, which still asserted ent#523's landing (`/workspace/a/scout` → `/workspace/c/t1`) and was failing on this branch; it now proves the page opened by the conversation on screen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…source-text pins (Abilityai/trinity-enterprise#784) `landOnAgent` was pinned only by regexes over its own function body (`workspaceNewChat.spec.js:181-211`), so nothing executed it: an inverted guard, or the right lines in the wrong order, reads byte-identically to a regex. The shell is mount-testable (`portalUnavailableTargets.mount.spec.js`), so these assert STATE instead. Decision #13 (ent#551 class) — back/forward reaches the landing without passing a click door, and `activeAgentName` feeds `convKey`, so a live call must be asked about BEFORE anything is written: mid-call, the dialog opens and the chat, its props and its instance identity are untouched; confirming then performs the landing it deferred; cancelling leaves call and chat exactly as they were. T4 — one navigation to an agent page mounts exactly ONE conversation, however often the thread list moves underneath it. Mutation-verified, both directions: with `guardLeaveCall` removed from `landOnAgent`, the two mid-call cases go red; with the T4 guards removed, the composing and one-mint cases go red. The source-text pins are kept as supplements (they still pin "no await / no ensureMainListed / no router.push" inside the function, which state cannot see). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…inity-enterprise#784) F1 of the operator's 2026-10-05 reopen. `agentLanding` always answered a new chat, so a draft typed into an existing chat was not where opening the agent landed — and the draft mark on the agent's row pointed at words that clicking it could not get back to. `e2e/workspace-drafts.spec.js:92` and `:116` describe the wanted behaviour and were red at c8af3d7. The rule is now the ruling's precedence: a link naming a chat (never reaches here), then the ent#621 `lastOpenSessionId` seam unchanged as the first arm, then the agent's chat holding an unsent draft — newest `updatedAt`, with the unsaved `new:<agent>` chat weighed on the same clock — then a new chat. A `new:` winner is `sessionId: null`, which is where those words already live; a thread winner opens that thread and its remount restores the draft. The candidate set is the set that lights the sidebar's mark: `isDraftedThread` is shared by `agentsWithDrafts` and the new `draftedLandingFor` rather than copied, so "the mark means click here to continue" cannot drift — a room and an archived thread are excluded on both sides. The drafts map is passed IN by both doors (`landOnAgent`, `resolveAgentLanding`), never read inside the rule, so one rule serves every door and stays pure. `composerFocusMode` moves above the branch in `landOnAgent`: a landing on a drafted chat is the same kind of arrival, and `openThread` does not touch that ref, so a previous gesture's `always` would otherwise have made the next landing focus on a touch device (T3(b)). The restore's own caret already shares `shouldAutoFocusComposer`. The T4 remount guard is untouched: the watcher still returns on a list-only re-fire, and the drafted-thread arm leaves the agent URL through `openThread`, so the guard is never reached with a stale answer. Tests: `portalDraftLanding.spec.js` (each arm, newest-wins both ways, the tie, room/archived exclusion, and the property that every MARKED row is a landing candidate) and `portalDraftLanding.mount.spec.js`, which replays both e2e flows at the shell's seam — red at c8af3d7 on the e2e's own symptom, 2 of 3 failing with `sessionId: null` where the draft was. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…y-enterprise#784) F2 of the operator's 2026-10-05 reopen: "opening an agent must not create a new chat every time; if an empty chat with that agent already exists it is reused, and at most one empty chat per agent exists at any time." Arm 4 of the precedence, under the drafts arm and above a new chat. `agentEmptyChat` is the read side: an unarchived, non-room thread of this agent with no message sent — ent#523's own "unused Main" test, widened to any row that fits it. Both fields are required because the two reads disagree: the cross-agent batch omits `message_count` (so an absent count must not read as used) while the per-agent read carries it (so a count of 2 must not read as empty just because `last_message_at` is missing). Several empty rows — legacy data — resolve Main first, then newest `created_at`, then id, so list order never decides and two doors reading one list cannot reuse two different rows. `ensureMainListed` is the write side, and the only place the "at most one" half can be held: it is a GET that INSERTS (`list_sessions` → `ensure_main_session`), so for an agent with an empty chat but no Main — legacy, since nothing mints a non-Main empty row today — visiting it would add a SECOND empty chat and then land on one of the two. It now returns early for that case. #2579's "the pinned tab has to be there" still holds for every agent whose chats are all used, which is the case it was about; that is pinned as its own mount case rather than left to the comment. No backend change: nothing here asks the server for anything it does not already do. Three existing specs carried fixtures with neither message field, which under the old rule meant "a thread exists" and under the ruling means "an empty chat to reuse" — the assertion they were written for. Each is updated to a USED row and the empty case is asserted separately, so none of them went green by accident: `portalAgentsAtCentre` (the landing arms), `workspaceAgentLanding` (the `?agent=` door, both id shapes) and `workspaceNewChat`, where `?new=1` is load-bearing again — it is the one way past the whole precedence, so the two answers now have to differ. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…and the caret (Abilityai/trinity-enterprise#784 review) ent#784 made /workspace/a/:name the URL a landed new chat RESTS on. Three things had only been true because nobody stayed there; each was reproduced in a browser against the rebased branch. - Clicking the row of the agent whose new chat is already on stage cleared `startingNewChat` in `openAgentPage` and then pushed the URL it was already on, so the landing never re-ran. The composer still read "New chat" and sent its first message without `new_thread`, which the server resolves to the agent's Main chat. The click is now a no-op that hands the caret back. - The rail rules still read the agent URL as "a page, not a conversation", so a landed new chat had no rail, and the first send (which moves the URL to /workspace/c/:id) slid one in beside a reply mid-stream. The URL is rail-free only until the landing has put the named agent on stage, and its column is reserved mid-load like any 1:1 route. - A landing that reuses a chat (the agent's empty one) goes through `openThread`, and a thread mount focuses nothing, so the caret stayed on the sidebar row. The shell hands it over under the landing's own fine-pointer rule. Tests: portalAgentLandingStage.mount.spec.js (10 cases, mounted shell). Four were red before the fix, for these reasons. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…rinity-enterprise#621) Checkpoint A of ent#621: the declaration every Workspace key will be dispatched from, with nothing wired to it yet (the shell dispatcher is B). `portalKeymap.js` declares all nine shell chords — ⌘J, ⌘., ⌘/, ⌥↑/↓, ⌥⇧↑/↓, ⌥. — plus the reserved ⌘K (ent#577) and the six component-owned protocol keys (Esc, the call's M, the preview's arrows, tab roving, a dialog's Tab cycle, the typeahead's bare arrows). The protocol entries are declared precisely because `keymapCollisions` is only worth anything if it can see the whole surface: a shell chord that shadows Esc is the 2026-09-07 class, and a map listing only its own keys cannot tell you. Matching is platform-free — `primary` is meta XOR ctrl everywhere (the shipped ⌘J semantics) and a chord matches the printed `key` OR the physical `code`. The two arms disagree about Shift on purpose: without the `key` arm DE's `Shift+7` → `/` is dead, without the `code` arm `⌥.` is dead on a US Mac (it types `≥`), and with Shift accepted on the `code` arm macOS `⌘?` Help search is swallowed. - `isNewChatHotkey` / `newChatHotkeyLabel` delegate to the map, keeping their signatures and their truth table, so "what ⌘J means" has one answer. The physical `KeyJ` arm only widens the set. - `resolveComposerKey` claims BARE arrows only. It used to claim an arrow whatever the modifiers were and the caller `preventDefault`s a `move-*`, so ⌥↓ / ⌥⇧↓ were silently dead whenever an @-popup happened to be open — a handler claiming a chord it never declared. - The reserved ⌘K resolves to nothing: the shell must not `preventDefault` a key it cannot answer, so the browser's Ctrl+K keeps working until Spotlight ships. It is hidden from the key list too — a dead row in a help dialog is worse than an undocumented key. - `keymapSuppressed` is a matrix: anything modal stops every shell key (⌘J included — today it remounts under an open file preview), while a call stops only the keys that would move you. ⌘J keeps its own "leave the call?" ask; ⌘/ is allowed, because opening a dialog leaves nothing. - `hasModalOpen` is a DOM probe, not a registry: it sees overlays that do not exist yet, and its exemption is per action (the rail keys do not see the rail's own sheet; ⌘/ can close its own list). - The stale `agentLanding` header comment is corrected — the switch-agent keys ARE a caller, passing arm 2's `lastOpenSessionId`; the gesture doors (New chat, the picker, ⌘J) still mean "fresh". `portalKeymap.js` reuses `isMacLike` / `nextActiveIndex` and `portalUtils.js` delegates back, which closes a module cycle. It is benign — neither module touches an import of the other at evaluation time — and both init orders are exercised: `workspaceKeymap.spec.js` imports the map first, `portalChatTabsAndTitles.spec.js` the utils. Tests: 190 pass across the four specs. Mutation battery, restored from a scratch copy: dropping the bare-arrow guard reddens the composer row; a chat chord without its Shift reddens four keymap cases including the collision check; Shift on the physical arm reddens the Decision 29 case; a reserved key that resolves reddens the ⌘K case. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ityai/trinity-enterprise#621) Checkpoint B of the Workspace key map. The map and its pure rules landed in 58350ed; this wires them to the shell. * `Portal.vue`'s `onGlobalKeydown` becomes the ONE dispatcher over `resolveWorkspaceKey` + `keymapSuppressed`, with the full ladder: an unclaimed chord returns without `preventDefault` (⌘K keeps reaching the browser until ent#577), ⌘J keeps its place at the top and its own "leave the call?" ask, a nearer owner's `defaultPrevented` wins, a focused `<select>` keeps its native Alt+↓, and anything modal / the mobile drawer / a live call suppresses the rest. * `lastOpen`: a plain session-scoped Map with ONE writer. `pendingSession` is a load instruction, not the chat on stage — a null under an unchanged agent is ignored, a null under a new agent is a real unsent new chat and is recorded as such, so `agentLanding`'s own arms return to the draft. (T1 / Decision 35) * `landOnAgent(name, { lastOpenSessionId, focus })`, with the idempotency guard HOISTED above the focus-mode write and above `agentLanding` (Decision 34): the key path's own push re-fires the route watcher, and a second landing over a thread list that has meanwhile grown a minted Main would `openThread` it, replacing the chat the key just landed on. * `stepAgent(±1)` walks `orderedRoster`, lifted to the shell (T6) and handed to both sidebar instances, which stop sorting — the keys must walk the order the eye reads, and there were two of those computeds before. The fresh arm pushes `/workspace/a/:name` AFTER the state write, so the watcher re-fire is absorbed. * `PortalConversation.cycleChat(delta)` + `defineExpose` (an identifier, not an inline body): the strip's inputs live there, and it emits `open-thread` / `new-chat` exactly as a tab click does. * `PortalSidebar` gains `activeAgentName` → `aria-current`, an active-row tint on the semantic `action-primary` token, auto-expand when a walk lands beyond the fold, `visibleAgentRows(..., { keep })` and the same keep in search (Decisions 27/37). The sidebar had no notion of a current AGENT at all, by key or by click. * An `aria-live` announcer for every move; `data-ws-rail-sheet` on the same element that carries the sheet's `aria-modal`, so the probe can exclude it. Red list re-pinned in the same commit, never loosened (Decision 42): `portalVoiceMode.spec.js` exact `defineExpose` string, `workspaceNewChat.spec.js` `landOnAgent` signature + guard-first shape, `portalAgentsAtCentre.spec.js` roster-order pin moved to the shell plus a new `keep` case. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…walk (Abilityai/trinity-enterprise#621) Checkpoint B's behavioural half. Both files MOUNT (#2918): a regex over `Portal.vue` passes an inverted suppression condition and a reversed `delta` byte-identically. * `workspaceKeymap.mount.spec.js` — the real `Portal.vue` (shallow) with ONE custom stub: the conversation, because the auto-stub exposes nothing and `conversationRef.value?.cycleChat?.()` is optional-chained twice, so deleted wiring would hide behind its own `?.`. Real `KeyboardEvent`s on the real `window`. 18 cases: the walk and both wraps, the caret returning to the field, a roster of one, the fresh arm (route + `focusOnMount`), the announcer, the last-open memory (return, unsent-new-chat, a stale id falling through), the chat keys reaching `cycleChat(±1)`, and the whole suppression ladder — modal, the rail sheet's per-action exemption, the drawer, a live call, `defaultPrevented`, a focused `<select>`, reserved ⌘K, and `e.repeat`. Wrappers are unmounted between cases: one left standing answers the next test's key press first and `preventDefault`s it, after which the shell correctly yields and the case reads as broken wiring. * `portalChatCycle.mount.spec.js` — 8 cases on the real conversation: the walk agrees with `agentChatTabs`' own order, both directions wrap, other agents' chats are not in it, the provisional tab is walked from and onto (`new-chat`, as a click answers), under two tabs is silent, and `cycleChat` is exposed. * `portalAgentsAtCentre.spec.js` gains the `keep` behaviour (function) and the sidebar's call site (structure) — pinned inside the `visibleAgentRows(` call, because the file's other `keep:` is `searchAgents`' and a loose pin stayed green with the collapse's argument deleted. Mutation battery, each reverted from a scratch copy and restored byte-identical: `defaultPrevented: false` → "yields to a nearer owner" red; `-delta` in `cycleChat` → 5 cases in portalChatCycle red; `keep` dropped from the collapse → "orders the roster BEFORE the collapse" red; `focus: 'always'` dropped → "a key press IS a gesture" red; `lastOpenSessionId: null` → "returns to the chat that was open there" red. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…bilityai/trinity-enterprise#621) Checkpoint C. The three remaining shell keys land on the ONE dispatcher B built — no second listener — and every control the keys drive now says which key drives it, derived from the same map the dispatcher resolves against. - `⌘.` toggles the rail in whichever form the viewport shows (column at and above `sm`, bottom sheet below it), reusing `railState.tab` and the persisted width: the key stores nothing of its own, so it cannot disagree with the expand/collapse button's memory (T5). - `⌥.` opens a closed rail on its remembered tab first, then walks `railTabs` — the tabs this session may SEE — never the registry, so a door-failed tab stays as unreachable by key as it is by click (Decision 25). - Both rail keys are suppressed, NOT dispatched to a no-op, where there is no rail: the difference is `preventDefault`, and a `⌘.` that silently ate Safari's Stop on every rail-less page would be a worse bug than a missing feature. `keymapSuppressed` reads the map's own `needsRail` flag for this. - `⌘/` opens `PortalKeyList` on `BaseModal` (#1923's Esc, focus trap and focus-return; always mounted and `v-model`-driven, because a `v-if` toggle unmounts past the close branch and focus never comes back). Every row is rendered from `keyListRows(WORKSPACE_KEYMAP, …)`; ⌘K is reserved for ent#577 and has no row, because a help dialog promising a key nothing binds is worse than an undocumented key. The list is the one action allowed to see through its own dialog, so the chord that opened it closes it. - Discoverability: agent rows, the rail's expand/collapse and tab buttons, and the chat tabs each carry the chord in BOTH channels — the tooltip people read and `aria-keyshortcuts` in ARIA's spelling — plus a sidebar-footer button so a mouse reaches what `⌘/` reaches. All of it comes through `keyHint` / `keyShortcutsFor` over the map: a typed glyph would be a second declaration of the same chord, and the copy that goes stale is always the one nobody tests. `BaseModal` now passes its attributes to the DIALOG element rather than dropping them on `Teleport`: `data-ws-key-list` has to sit on the same element as `aria-modal`, because that is the element the dispatcher's `:not(...)` exclusion matches. Without it the marker is a comment, not a selector. The sidebar footer's two icon buttons share one class string (`PortalRail`'s `ICON_BTN` shape) — two identical inline copies is two places for a hover state to drift, and the raw-colour ratchet counts every copy. Tests (all mounts, no source-text pins): the rail and key-list cases in `workspaceKeymap.mount.spec.js` prove a real `keydown` on the real `window` reaches the real dialog in the document — `teleport`, `BaseModal` and `PortalKeyList` are deliberately unstubbed, since the `[aria-modal]` probe can only see an overlay that actually reached `<body>`. `portalKeyList.mount.spec.js` asserts rows against the map (never against repeated strings) and drives Esc, the close button and focus-return with focus placed by hand first. `portalKeyHints.mount.spec.js` asserts each control's two attributes equal `keyHint`/`keyShortcutsFor` output, so a hand-typed hint fails. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ai/trinity-enterprise#621) T6 lifted `orderedRoster` out of `PortalSidebar` and into `Portal.vue` — one order, computed once, handed to both sidebar instances so the switch-agent keys walk exactly the order the eye reads. Three specs pinned the identifiers at their old address and went red on the lift rather than on a broken rule: - `portalRosterRow.spec.js` asserted `visibleAgentRows(orderedRoster.value` and the `orderedRoster` computed in the sidebar's source. Both are gone from that file, and neither proved more than "the code was written". Converted to a MOUNT of `PortalSidebar`: the bound is now a count of rendered rows, and "the sidebar does not re-sort what it was handed" is a deliberately unsorted roster coming back out in the same order. A re-sort that slipped back in fails this even though it would satisfy any spelling of the old regex. - `portalSidebarRecency.spec.js` pinned the `null` primary (ent#500 does not exist) on the sidebar's call site. Re-pinned onto the shell's — the one call site there now is. It stays a call-site pin: "the primary argument is null" is an argument-position fact no rendered order can show, because with ent#500 absent a named primary and null agree on screen. - `portalSidebarSearch.spec.js` pinned the same `orderedRoster.value` spelling; re-pinned onto `props.roster`, plus the negative that keeps the lift honest — no second `orderRosterAgents(` inside the sidebar. No assertion was dropped or weakened; each was shown red by a one-line mutation (a re-sort in the sidebar, a reintroduced second sort, a guessed primary in the shell), with both source files restored byte-identical after each. Sweep green: 130 files / 2368 tests, including the raw-colour, loading-gate and source-text ratchets (the source-text baseline is unchanged — the mount conversion left `portalRosterRow` at its one existing read). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rinity-enterprise#621) Checkpoint D. The docs described a Workspace with one inline hotkey; it now has a declared map, a single dispatcher, and one landing rule the keys call. - `requirements/core-agent.md` §5.21a — the new sub-section beside §5.21: the nine working chords, the reserved one, one declaration / one dispatcher, the inverted editable-target rule (every key works from the message field), the suppression rule, the non-US `key`-or-`code` statement, the roster-order and last-open rules, and discoverability. OSS-core recorded explicitly rather than inferred from the merge. - `feature-flows/workspace-chat-tabs-and-titles.md` — "New chat in the header" keeps the ent#451 facts and gains "The key map": the table, the matcher's platform-free rule, the dispatcher's ladder in order (the order IS the design), the two rules it leans on, and the key list that is built FROM the map so a key cannot ship undocumented. Title and front matter follow; the Tests section names the ent#621 specs. - `feature-flows/workspace-agents-at-the-centre.md` — the stale half of "two doors RESOLVE, the rest ASSERT": the switch-agent keys resolve now, and they pass `lastOpenSessionId`. Walking the roster is navigation, not a request for a blank page. The pure-helper table says `orderRosterAgents` is called once, in the shell. - `architecture/frontend.md` — the Workspace shell paragraph: one map, one listener, the ladder, and the two consequences for anyone adding a key. - `Portal.vue` — one stale dispatcher comment ("the rail keys land in the next checkpoint" — they landed in C). The `agentLanding` header comment the plan also named was already corrected in Checkpoint A. Public wording only: the shipped behaviour, no enterprise design detail and no link to any private design document. The enterprise-docs guard's own pattern runs clean over `docs/` and `CLAUDE.md`. Sweep still green: 130 files / 2368 tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…21 review H1) The last-open writer tested only "did the agent change", so a null `pendingSession` under an unchanged agent was always read as a load instruction. But ⌘J and the strip's New chat tab (both `newChatWithAgent`) null it on the agent already on stage — the commonest way to be sitting on an unsent chat — so ⌥↓ then ⌥↑ came back to the chat ⌘J had just left instead of the new one, against AC-8. The watcher now also observes `startingNewChat` and passes `keyChanged || (fresh && !sid)`: a deliberate fresh chat records `null` for the current agent, while every load-instruction clear (`openAgentPage`, `openRoom`, the unreachable arm, `openThread`) leaves the flag false and is still ignored, so the chat the person was reading survives those. Two mount cases: ⌘J on the agent on stage then ⌥↓/⌥↑ returns the new chat, and a load-instruction clear under an unchanged agent still returns the old one. Reverting only the watcher turns the first red and leaves the second green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ding (ent#621 review I1-I3) I1 — the ladder sentence read as though nothing claims the event before the suppression rung, but ⌘J during an active voice call calls preventDefault first and routes to the leave-call guard. That is the pre-existing ent#534/551 ask, kept deliberately, so the exception is now stated in architecture/frontend.md and in AC-3 rather than the code being moved. I2 — the docs named a `platform` argument `keymapCollisions(map)` does not take. Matching is platform-free, so one call is the whole proof; the three mentions say that instead of implying a per-OS check. I3 — "one window listener" becomes "one chord dispatcher" in frontend.md (and AC-3), naming PortalRail.vue's pre-existing sheet-only Esc listener as the `close-top` protocol entry it is. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…w listener (ent#621 review I3) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…browser (ent#621 review) A key repeat resolved to nothing and the dispatcher returned before `preventDefault`, so the auto-repeat of a chord the shell had just claimed went to the browser. Reproduced against the rebased branch: a held ⌥. types `≥` into the message field on a Mac, and a held Ctrl+J opens Downloads on Chrome/Firefox for Windows and Linux (⌘J's repeats were swallowed before the map existed). The dispatcher now remembers the action of the press it claimed (`heldKey`) and swallows repeats of that chord, matched through the new pure `workspaceChord`. A repeat still never dispatches, the repeat of a press the shell did not claim stays the browser's, and any new press ends the hold. Also re-pins the "no rail" case after the ent#784 review fix: a landed agent URL is a chat with a rail, so ⌘. works there; the rail-less page in the test is now the unreachable-agent refusal. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…cut tips (ent#621 follow-up) - `⌘/` (Ctrl+/) puts the cursor in the sidebar's "Search agents and chats" field and selects its text; pressed again from the field it hands the caret back to the message field. Below `sm` it opens the drawer and focuses that instance. The field shows the chord (key cap, title, aria-keyshortcuts). Chosen to echo the Dashboard's `/` filter key. - The shortcuts list moves to `⌥/` to free the chord, keeping the `⌘.` / `⌥.` pairing. - `PortalKeyTips`: a small panel pinned to the bottom of the open rail with four headline chords and an "All shortcuts" button; one icon at the foot of the collapsed strip; neither on the mobile sheet. Rows are derived from the key map (`KEY_TIPS` -> `keyTipRows`). Dismissible, remembered per browser in localStorage. - `PortalKeyCap`: the one `<kbd>` recipe for the key list, the tips and the search field. Requirements: core-agent.md 5.21a AC-1/9 amended, AC-11 and AC-12 added. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ollow-up) The panel was a heading, four rows and a footer button (213px). It is now two rows of two with the close and open-the-list controls stacked beside them (80px), and nothing above or below. - `keyHintCompact` packs a chord pair that differs only in its last key into one cap (`⌥↑↓`, `Alt+Shift+↑↓`); anything it cannot pack without losing something falls back to `keyHint`. - Columns size to their content; when the rail is dragged narrow a label shortens with an ellipsis (full text on hover) while a key cap never wraps and a row never becomes two. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Conflicts in the landing rule's doc, portalUtils.js, Portal.vue and workspaceNewChat.spec.js, all between ent#784 (already carried here) and ent#621's options bag on landOnAgent; resolved to the ent#621 side, which is a superset of dev's hunk in every case. Portal.vue keeps the key map as the only "⌘J" definition, so dev's isNewChatHotkey import stays dropped. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…son confirms (ent#621 merge-train) The call branch `preventDefault`ed the event and then re-entered the ladder with that same event, so on the resumed pass the suppression rung read the shell's own mark as "a nearer owner claimed it" and bailed — ⌘J was dead exactly when the person had said "end the call and leave". The continuation now re-enters as the `resumed` pass after a `nextTick` (the confirm dialog is aria-modal and leaves the DOM on that flush), and `keymapSuppressed` waives only the `defaultPrevented` rung for it; modal, drawer and call still apply. Also from the finding: the physical-`code` arm answers only when the printed key is not a single printable ASCII character, so a Latin non-QWERTY layout keeps paste and undo (Dvorak's ⌘V arrives on `Period`, ⌘Z on `Slash`). The non-ASCII cases the arm exists for (`≥`, Cyrillic) are unchanged. Tests: pure cases for the resumed rung and the Dvorak shape in workspaceKeymap.spec.js; the shell mounted for real in workspaceKeymap.mount.spec.js — ⌘J during a call asks, confirm fires the new chat, cancel fires nothing; the two voice-mode source pins re-pinned to the new continuation. `isNewChatHotkey` stays: workspaceKeymap.spec.js and portalChatTabsAndTitles.spec.js (here and on the stacked ent#621 follow-up branch) import it as the ⌘J equivalence pin, and the chat-tabs flow documents it as the delegate kept on purpose. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Resolve by running |
…ollow-up) Parent #3241 was squash-merged to dev (42543a4). Every conflicted file resolves to dev's version (identical to #3241's final head 54cd121, review fixes included) plus only this PR's own delta: ⌘/ focuses the sidebar search, the shortcuts list on ⌥/, the PortalKeyTips rail panel, keyHintCompact and PortalKeyCap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Brought up to date with Merge commits
How the conflicts were resolved.
Tests (
Note: at the first merge, the full suite had 1 failure: The PR is still a draft and has not been merged. 🤖 Generated with Claude Code |
…nsumed tip-row aria field - workspace.md / FAQ said ⌘/ opens the shortcuts list; it is ⌥/ now. Add the ⌘/ search row and the rail's shortcut-tips panel. - keyTipRows' ariaKeyshortcuts had no consumer (aria-keyshortcuts belongs on the control the chord activates, not the tip row) — removed with its assertion. - Stale ⌘/ → ⌥/ in test names and comments about the key list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
merge-train: pushed to this branch — merged |
vybe
left a comment
There was a problem hiding this comment.
merge-train: validated in the 2026-10-08 batch
Part of abilityai/trinity-enterprise#621 — a follow-up requested by the operator while trying the keys. Builds on #3219 and #3241 (both merged). Frontend and docs only.
What
⌘/(Ctrl+/) puts the cursor in the sidebar's "Search agents and chats" field and selects what is in it. Pressed again from the field, it hands the caret back to the message field. Belowsmthe sidebar is the drawer, so the key opens it and focuses that instance. The chord echoes the Dashboard's/filter key. The field shows it three ways: a key cap while empty and unfocused,title, andaria-keyshortcuts.⌥/to free the chord, keeping the⌘./⌥.pairing.⌘Kstays reserved and unbound.PortalKeyTips.vue): a small panel pinned to the bottom of the open rail. Two rows of two (switch agent, switch chat, search, new chat) with the close and open-the-list controls stacked beside them, 80px tall. The collapsed strip carries one icon at its foot instead; the mobile sheet carries neither. Dismissible, remembered per browser (localStorage).keyHintCompactpacks a chord pair that differs only in its last key into one cap (⌥↑↓,Alt+Shift+↑↓), and falls back tokeyHintfor anything it cannot pack without losing something.PortalKeyCap.vue: one<kbd>recipe for the key list, the tips and the search field.WORKSPACE_KEYMAP(KEY_TIPS->keyTipRows); nothing is hand-typed.docs/memory/requirements/core-agent.md§5.21a (AC-1 and AC-9 amended, AC-11 and AC-12 added, written before the code) and the chat-tabs feature flow.Operator rulings carried
⌘/for search (operator, 2026-10-05).Decisions made here, for the operator to overrule:
⌥/rather than losing its chord.Tests
workspaceKeymap.spec.js(the new chords,keyHintCompact,keyTipRows, the dismissal storage),workspaceKeymap.mount.spec.js(the search key in the mounted shell: column, drawer, return to the composer, suppression under a dialog, allowed during a call; the dismissal surviving a remount),portalKeyTips.mount.spec.js(the panel in all three rail forms),portalKeyHints.mount.spec.js(the search field's key cap andfocusSearch).⌥/; the panel button; dismissal across a reload; light and dark. The two-row layout was measured at the default rail width and at the narrowest (280px), with Mac labels and with Windows labels (platform faked, not a Windows machine): two rows, no wrapped key cap, no horizontal overflow. Windows labels shorten with an ellipsis at the narrowest width.Not covered
⌥/on a layout where/is a shifted key), screen readers, a tablet with a hardware keyboard belowsm.workspace.md, the Workspace FAQ) now say⌥/opens the key list and⌘/jumps to search; updated on this branch by the merge train.🤖 Generated with Claude Code