Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
d479873
feat(workspace): opening an agent resolves to a new chat (abilityai/t…
Oct 4, 2026
c2d2a2a
feat(workspace): land on the new chat, and focus only when asked (abi…
Oct 4, 2026
c79b3e1
feat(workspace): document the new-chat landing rule (abilityai/trinit…
Oct 4, 2026
b2863a6
fix(portal): [I1] a thread-list re-fire must not re-mint over an adop…
Oct 4, 2026
f7101fe
fix(portal): [I3] mount cases for decision #13 and the T4 guard, not …
Oct 4, 2026
7212223
fix(portal): opening an agent lands on its newest draft (abilityai/tr…
Oct 5, 2026
dace0c2
fix(portal): opening an agent reuses its empty chat (abilityai/trinit…
Oct 5, 2026
1db1dd5
fix(portal): a landed new chat keeps its rail, its new-thread intent …
trinity-ability Oct 5, 2026
27cfcc4
feat(portal): the Workspace key map and its pure helpers (abilityai/t…
Oct 5, 2026
88be733
feat(portal): the agent and chat keys, and the last-open memory (abil…
Oct 5, 2026
418397c
test(portal): mount the Workspace keys — the dispatcher and the chat …
Oct 5, 2026
340e5f2
feat(portal): the rail keys, the key list, and the keys made visible …
Oct 5, 2026
e8ec33e
test(portal): re-pin the sidebar roster rules after the lift (ability…
Oct 5, 2026
9e30e18
docs(portal): the Workspace key map in the feature flows (abilityai/t…
Oct 5, 2026
10bfd65
fix(portal): remember a same-agent new chat for the agent keys (ent#6…
Oct 5, 2026
61cdcf6
docs(memory): key map ladder exception + platform-free collisions wor…
Oct 5, 2026
75c430a
docs(memory): chat-tabs flow says one chord dispatcher, not one windo…
Oct 5, 2026
7136e8c
fix(portal): a held Workspace key no longer leaks its repeats to the …
trinity-ability Oct 5, 2026
1334f5b
feat(portal): ⌘/ focuses the sidebar search, and the rail shows short…
trinity-ability Oct 5, 2026
dc6f569
feat(portal): the rail's shortcut tips are two rows of two (ent#621 f…
trinity-ability Oct 5, 2026
b879f4e
merge: origin/dev into feature/ent621-workspace-keymap
trinity-ability Oct 6, 2026
54cd121
fix(portal): a ⌘J parked behind the leave-call ask fires once the per…
trinity-ability Oct 6, 2026
212aaec
merge: feature/ent621-workspace-keymap into feature/ent621-search-key…
trinity-ability Oct 6, 2026
6d64a36
merge: origin/dev into feature/ent621-search-key-and-hints (ent#621 f…
Oct 7, 2026
9def95b
Merge remote-tracking branch 'origin/dev' into feature/ent621-search-…
Oct 7, 2026
04e57d2
Merge remote-tracking branch 'origin/dev' into HEAD
trinity-ability Oct 8, 2026
b268da5
merge-train: ⌥/ in the user docs, ⌘/ search documented; drop the unco…
trinity-ability Oct 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions docs/memory/feature-flows/workspace-chat-tabs-and-titles.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ surface rather than only within one owner's share:
| ⌥⇧↓ / ⌥⇧↑ (Alt+Shift+↓ / ↑) | Next / previous chat with this agent | shell |
| ⌘. / Ctrl+. | Show or hide the rail | shell |
| ⌥. (Alt+.) | Next rail tab | shell |
| ⌘/ / Ctrl+/ | The keyboard-shortcuts list | shell |
| ⌘/ / Ctrl+/ | Cursor into the sidebar's search field; again from the field, back to the message field | shell |
| ⌥/ (Alt+/) | The keyboard-shortcuts list | shell |
| Esc, the call's `M`, the preview's ←/→, tab roving, a dialog's Tab cycle, the composer typeahead's bare arrows | — | the component that owns the overlay |
| ⌘K / Ctrl+K | nothing — **reserved** for search (ent#577) | nobody |

Expand Down Expand Up @@ -139,7 +140,7 @@ protocol entry the map declares, not a second dispatcher), armed above `bootstra
6. `keymapSuppressed`: a nearer owner already claimed the event
(`defaultPrevented`, which is how the Esc protocol and the typeahead's bare
arrows win), anything modal (`[aria-modal="true"]`, with a per-action
exemption so ⌘/ can close its own dialog and the rail sheet does not count
exemption so ⌥/ can close its own dialog and the rail sheet does not count
as something modal over the rail), the mobile drawer, or a voice call —
which suppresses the moving and rail keys silently.

Expand All @@ -164,7 +165,23 @@ inverted here, since the hands that want the next chat are already typing.
rule's own arms rather than a second rule. Memory is in-memory and
session-scoped, as the acceptance criteria asked: a reload starts fresh.

**Discoverability.** ⌘/ opens `PortalKeyList` — a `BaseModal` dialog built
**Search key.** ⌘/ runs `Portal.vue::focusSearch`: at and above `sm` it calls
the desktop `PortalSidebar`'s exposed `focusSearch()` (focus + select), or —
when that field already has focus (`searchHasFocus()`) — hands the caret back
through `focusConversationComposer`; below `sm` it opens the drawer and focuses
that instance's field on the next tick. The field shows the chord in a
`PortalKeyCap` while it is empty and unfocused.

**Tips panel.** `PortalKeyTips.vue` is pinned to the bottom of the OPEN rail
(`PortalRail`'s `keyTips` prop; an icon button at the foot of the collapsed
strip is the same door; the sheet has neither). Its rows are
`keyTipRows(platform)` over `KEY_TIPS`, laid out as two rows of two with the
two controls stacked beside them (`keyHintCompact` packs a chord pair into one
cap; labels truncate, caps never wrap). Its keyboard button emits `open-keys`, and its
close emits `dismiss-key-tips`, which the shell persists with
`saveKeyTipsDismissed` (`localStorage['trinity-workspace-key-tips']`).

**Discoverability.** ⌥/ opens `PortalKeyList` — a `BaseModal` dialog built
from `keyListRows(WORKSPACE_KEYMAP, platform)`, so a key cannot exist without
appearing in the list (minus the reserved row). The sidebar footer has a
"Keyboard shortcuts" button carrying the same chord, and the surfaces a key
Expand Down
35 changes: 31 additions & 4 deletions docs/memory/requirements/core-agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -1845,7 +1845,8 @@ bounding the table. OSS-core (Workspace rule above). Flow:
- **AC-1 — the keys**: `⌘J`/`Ctrl+J` new chat with this agent (§5.21,
unchanged); `⌥↓`/`⌥↑` next / previous **agent**; `⌥⇧↓`/`⌥⇧↑` next /
previous **chat with this agent**; `⌘.`/`Ctrl+.` show or hide the rail;
`⌥.` next rail tab; `⌘/`/`Ctrl+/` the keyboard-shortcuts list.
`⌥.` next rail tab; `⌘/`/`Ctrl+/` the cursor into the sidebar's search
field (AC-11); `⌥/` the keyboard-shortcuts list.
- **AC-2 — one declaration**: every chord lives in
`src/frontend/src/components/portal/portalKeymap.js` (pure — no DOM, no
store), with its scope, its owner, and its label. `keymapCollisions(map)`
Expand Down Expand Up @@ -1873,10 +1874,10 @@ bounding the table. OSS-core (Workspace rule above). Flow:
through `defaultPrevented` (the Esc protocol, the composer typeahead's bare
arrows, which claim bare arrows only).
- **AC-5 — suppression**: anything modal (`[aria-modal="true"]`, with a
per-action exemption so `⌘/` can close its own dialog and the rail sheet
per-action exemption so `⌥/` can close its own dialog and the rail sheet
does not count as modal over the rail), the mobile drawer, and a live voice
call suppress the moving and rail keys **silently**; `⌘J` keeps its own ask
during a call, and `⌘/` stays available. A held key does not walk the list
during a call, and `⌘/` and `⌥/` stay available. A held key does not walk the list
(`e.repeat` resolves to nothing) and an IME composition is never a chord.
The auto-repeat of a press the shell **claimed** is still swallowed
(`heldKey` + `workspaceChord`), so a held `⌥.` cannot type `≥` into the
Expand All @@ -1903,7 +1904,7 @@ bounding the table. OSS-core (Workspace rule above). Flow:
that agent. An unsent new chat records nothing, so its draft is reached
through the landing rule's own arms rather than a second rule. Every moving
key focuses the composer afterwards.
- **AC-9 — discoverable**: `⌘/` opens a shortcuts dialog built **from the
- **AC-9 — discoverable**: `⌥/` opens a shortcuts dialog built **from the
map**, so a key cannot ship without a row (the reserved chord is excluded —
nothing binds it, and a dead row in a help dialog is worse than an
undocumented key). The sidebar footer offers the same dialog, and every
Expand All @@ -1912,6 +1913,31 @@ bounding the table. OSS-core (Workspace rule above). Flow:
- **AC-10 — reserved, not bound**: `⌘K`/`Ctrl+K` is declared and
**unhandled** — no `preventDefault`, no row in the list — so the browser's
own behaviour stands until the search surface ships.
- **AC-11 — the search key**: `⌘/`/`Ctrl+/` puts the cursor in the sidebar's
"Search agents and chats" field and selects what is in it — the Dashboard's
`/` filter key (§ fleet filter), with the modifier a key needs to work from
inside a text field. Pressed again while the field has focus it hands the
caret back to the message field. Below `sm` the sidebar is the drawer, so the
key opens it and focuses that instance's field. The field shows the chord
(a key cap while it is empty and unfocused, plus `title` and
`aria-keyshortcuts`). It is NOT the reserved `⌘K` surface: it focuses the
search that exists and adds none. The shortcuts list moved to `⌥/` to free
the chord, keeping the `⌘.` / `⌥.` pairing (the primary key acts, the Option
key steps to its neighbour).
- **AC-12 — the tips panel**: the open rail carries a small panel pinned to its
bottom edge listing four headline chords (switch agent, switch chat, search,
new chat) and a button that opens the shortcuts dialog. It is **two rows of
two** and nothing else — no heading line, no footer: the close and the
open-the-list controls stack beside the rows. A pair of chords prints its
modifiers once (`⌥↑↓`, `Alt+Shift+↑↓` — `keyHintCompact`), and when the rail
is dragged narrow a label shortens (full text on hover) while a key cap never
wraps and a row never becomes two. Rows are derived from the map (`KEY_TIPS`
→ `keyTipRows`), never typed. The collapsed strip carries
the same door as one icon button at its foot; the mobile sheet carries
neither. The panel is dismissible, and the dismissal is remembered per
browser (`localStorage['trinity-workspace-key-tips']`, a per-viewer
convenience — nothing is stored server-side); the sidebar footer button stays
as the way back to the full list.
- **Not this slice**: a search/spotlight surface behind `⌘K`; remapping or
disabling the chords in settings (they are modifier chords; nothing conflicts
with typing); the mobile drawer's own Esc/focus-trap contract (#1923).
Expand All @@ -1924,6 +1950,7 @@ bounding the table. OSS-core (Workspace rule above). Flow:
`workspaceKeymap.mount.spec.js` (the shell: dispatch, the walk, the memory,
the bails and suppression), `portalChatCycle.mount.spec.js`,
`portalKeyList.mount.spec.js`, `portalKeyHints.mount.spec.js`,
`portalKeyTips.mount.spec.js` (the tips panel, both rail forms),
`portalRosterRow.spec.js` (the bound and the handed-down order, mounted).
- **Flow**: `docs/memory/feature-flows/workspace-chat-tabs-and-titles.md`
(→ The key map); the landing rule it calls is
Expand Down
2 changes: 1 addition & 1 deletion docs/user-docs/faq/sharing-access-and-monetization.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ Most visits to an agent start new work, so opening an agent now starts a new cha

## Does the Workspace have keyboard shortcuts?

Yes, and they work while you are typing in the message field. **⌘J** / **Ctrl+J** starts a new chat, **⌥↓** / **⌥↑** moves between agents, **⌥⇧↓** / **⌥⇧↑** between chats with the current agent, **⌘.** / **Ctrl+.** shows or hides the rail, **⌥.** steps through the rail's tabs, and **⌘/** / **Ctrl+/** opens the full list (also the **Keyboard shortcuts** button at the bottom of the sidebar). Esc closes the innermost thing first, then stops a running turn. The keys pause while a dialog is open, and during a voice call only ⌘J answers. See [Workspace](../sharing-and-access/workspace.md#keyboard-shortcuts).
Yes, and they work while you are typing in the message field. **⌘J** / **Ctrl+J** starts a new chat, **⌥↓** / **⌥↑** moves between agents, **⌥⇧↓** / **⌥⇧↑** between chats with the current agent, **⌘.** / **Ctrl+.** shows or hides the rail, **⌥.** steps through the rail's tabs, **⌘/** / **Ctrl+/** jumps to the sidebar search, and **⌥/** / **Alt+/** opens the full list (also the **Keyboard shortcuts** button at the bottom of the sidebar). Esc closes the innermost thing first, then stops a running turn. The keys pause while a dialog is open, and during a voice call only ⌘J answers. See [Workspace](../sharing-and-access/workspace.md#keyboard-shortcuts).

## How do I reply to one specific message from the agent?

Expand Down
9 changes: 6 additions & 3 deletions docs/user-docs/sharing-and-access/workspace.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,11 @@ Top to bottom:
- **Trinity Workspace** — the wordmark. It links to the Inbox.
- **Inbox** — a pinned row with two badges: the number of asks agents are waiting on you to answer, and the number of chats with something you haven't read. See [The Inbox](#the-inbox).
- **New chat** — opens the agent picker.
- **Search agents and chats…** — two characters or more. Agents filter in place; chat results replace the chat list below, and each half says separately when it matched nothing.
- **Search agents and chats…** — two characters or more. **⌘/** / **Ctrl+/** puts the cursor there from anywhere; pressed again from the field, it returns you to the message field. Agents filter in place; chat results replace the chat list below, and each half says separately when it matched nothing.
- **Agents** — your roster, ordered by the agents you worked with most recently, then by name. Five rows show, with **Show all** to expand. A row carries the agent's display name, the title of your newest chat with it (or its slug when the name is a label), when you last heard from it, an availability chip when the agent is stopped or unreachable, and its own ask and unread badges. **Clicking an agent opens a new, empty chat with it**, with the cursor in the message field — or returns you to a chat with that agent where you left unsent text. Your earlier chats are its tabs, one click away.
- **Starred** — chats you've starred, lifted out of the date groups so each chat appears exactly once.
- **Today / Yesterday / Previous 7 days / Older** — everything else. A Main you have never used is not listed here; the agent's row is the way into it.
- **Keyboard shortcuts** — at the bottom, beside sign-out. It opens the list of keys (also **⌘/** / **Ctrl+/**).
- **Keyboard shortcuts** — at the bottom, beside sign-out. It opens the list of keys (also **⌥/** / **Alt+/**).

### The Inbox

Expand Down Expand Up @@ -236,14 +236,17 @@ Inside a room, **+ Add agent** recruits another, and only a person can do that.

### Keyboard shortcuts

The open rail carries a small **shortcut tips** panel at its foot with the most-used keys; close it with its **×** and this browser remembers. The collapsed rail shows a single keyboard icon instead, which opens the full list.

| Keys | Where | Does |
|------|-------|------|
| **⌘J** / **Ctrl+J** | Anywhere in the Workspace | New chat with the agent in front of you (the picker when there is none) |
| **⌥↓** / **⌥↑** (**Alt+↓** / **Alt+↑**) | Anywhere | Next / previous agent in the sidebar |
| **⌥⇧↓** / **⌥⇧↑** (**Alt+Shift+↓** / **↑**) | Anywhere | Next / previous chat with this agent |
| **⌘.** / **Ctrl+.** | Anywhere | Show or hide the rail |
| **⌥.** (**Alt+.**) | Anywhere | Next rail tab |
| **⌘/** / **Ctrl+/** | Anywhere | The keyboard-shortcuts list |
| **⌘/** / **Ctrl+/** | Anywhere | Cursor to the sidebar search; again, back to the message field. On a narrow screen it opens the sidebar first |
| **⌥/** (**Alt+/**) | Anywhere | The keyboard-shortcuts list |
| **Enter** / **Shift+Enter** | Composer | Send / new line |
| **↓ ↑**, **Enter**, **Tab**, **Esc** | Composer, with the `/` or `@` list open | Move, insert the selected row, insert the top row, dismiss |
| **Esc** | Composer, with a *Replying to* chip | Drop the reply |
Expand Down
2 changes: 1 addition & 1 deletion src/frontend/src/components/base/BaseModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ import {

// Attributes land on the DIALOG, not on `Teleport`. A consumer that needs the
// overlay element itself marked — `PortalKeyList`'s `data-ws-key-list`, which
// the Workspace key dispatcher excludes so `⌘/` can close the list it opened
// the Workspace key dispatcher excludes so `⌥/` can close the list it opened
// — must put that attribute on the SAME element as `aria-modal`, because that
// is the element the `:not(...)` exclusion matches. Without this the attribute
// is dropped silently and the marker is a comment rather than a selector.
Expand Down
7 changes: 7 additions & 0 deletions src/frontend/src/components/portal/PortalKeyCap.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<template>
<!-- `<kbd>` on gray + `font-mono`, the Dashboard's shape: a key cap reads as
a key cap without a hue claiming meaning it has not got. ONE recipe for
the three places a chord is printed (the key list, the rail's tips, the
sidebar search field), so they cannot drift apart. -->
<kbd class="shrink-0 whitespace-nowrap rounded border border-gray-200 bg-gray-50 px-1.5 py-0.5 text-[11px] font-mono text-gray-600 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300"><slot /></kbd>
</template>
9 changes: 4 additions & 5 deletions src/frontend/src/components/portal/PortalKeyList.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<!-- `data-ws-key-list` is how `⌘/` can CLOSE the list it opened: the probe
<!-- `data-ws-key-list` is how `⌥/` can CLOSE the list it opened: the probe
suppresses every shell key under anything `aria-modal`, and the key list
is one. The marker rides the dialog element itself (the same element that
carries `aria-modal`), because that is the element the `:not(...)`
Expand Down Expand Up @@ -41,9 +41,7 @@
<span class="block text-sm text-gray-900 dark:text-gray-100">{{ row.label }}</span>
<span v-if="row.note" class="block text-xs text-gray-500 dark:text-gray-400">{{ row.note }}</span>
</span>
<!-- `<kbd>` on gray + `font-mono`, the Dashboard's shape: a key cap
reads as a key cap without a hue claiming meaning it has not got. -->
<kbd class="shrink-0 rounded border border-gray-200 bg-gray-50 px-1.5 py-0.5 text-[11px] font-mono text-gray-600 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300">{{ row.keys }}</kbd>
<PortalKeyCap>{{ row.keys }}</PortalKeyCap>
</li>
</ul>
</section>
Expand All @@ -60,7 +58,7 @@

<script setup>
/**
* The `⌘/` key list (ent#621).
* The `⌥/` key list (ent#621).
*
* Every row is rendered from `keyListRows(WORKSPACE_KEYMAP, …)` — the same
* declaration the dispatcher resolves against. A hand-typed list is a second
Expand All @@ -75,6 +73,7 @@
*/
import { computed } from 'vue'
import BaseModal from '@/components/base/BaseModal.vue'
import PortalKeyCap from './PortalKeyCap.vue'
import { SCOPE_MOVING, SCOPE_DOING, OWNER_PROTOCOL } from './portalKeymap'
import { isMacLike } from './portalUtils'

Expand Down
Loading
Loading