diff --git a/docs/memory/feature-flows/workspace-chat-tabs-and-titles.md b/docs/memory/feature-flows/workspace-chat-tabs-and-titles.md
index acd8de7fb..b5d55be43 100644
--- a/docs/memory/feature-flows/workspace-chat-tabs-and-titles.md
+++ b/docs/memory/feature-flows/workspace-chat-tabs-and-titles.md
@@ -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 |
@@ -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.
@@ -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
diff --git a/docs/memory/requirements/core-agent.md b/docs/memory/requirements/core-agent.md
index ff2f1f67e..3656a5e57 100644
--- a/docs/memory/requirements/core-agent.md
+++ b/docs/memory/requirements/core-agent.md
@@ -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)`
@@ -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
@@ -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
@@ -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).
@@ -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
diff --git a/docs/user-docs/faq/sharing-access-and-monetization.md b/docs/user-docs/faq/sharing-access-and-monetization.md
index dbcc64e72..603c84439 100644
--- a/docs/user-docs/faq/sharing-access-and-monetization.md
+++ b/docs/user-docs/faq/sharing-access-and-monetization.md
@@ -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?
diff --git a/docs/user-docs/sharing-and-access/workspace.md b/docs/user-docs/sharing-and-access/workspace.md
index 3a8d10b89..7fc486a06 100644
--- a/docs/user-docs/sharing-and-access/workspace.md
+++ b/docs/user-docs/sharing-and-access/workspace.md
@@ -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
@@ -236,6 +236,8 @@ 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) |
@@ -243,7 +245,8 @@ Inside a room, **+ Add agent** recruits another, and only a person can do that.
| **⌥⇧↓** / **⌥⇧↑** (**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 |
diff --git a/src/frontend/src/components/base/BaseModal.vue b/src/frontend/src/components/base/BaseModal.vue
index cd52d9077..1d428d72b 100644
--- a/src/frontend/src/components/base/BaseModal.vue
+++ b/src/frontend/src/components/base/BaseModal.vue
@@ -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.
diff --git a/src/frontend/src/components/portal/PortalKeyCap.vue b/src/frontend/src/components/portal/PortalKeyCap.vue
new file mode 100644
index 000000000..d3c46a881
--- /dev/null
+++ b/src/frontend/src/components/portal/PortalKeyCap.vue
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/src/frontend/src/components/portal/PortalKeyList.vue b/src/frontend/src/components/portal/PortalKeyList.vue
index 95b9486e2..a8af25d4c 100644
--- a/src/frontend/src/components/portal/PortalKeyList.vue
+++ b/src/frontend/src/components/portal/PortalKeyList.vue
@@ -1,5 +1,5 @@
-
- {{ row.keys }}
+ {{ row.keys }}
@@ -60,7 +58,7 @@
diff --git a/src/frontend/src/components/portal/PortalRail.vue b/src/frontend/src/components/portal/PortalRail.vue
index 0c585c12a..4ed50ce41 100644
--- a/src/frontend/src/components/portal/PortalRail.vue
+++ b/src/frontend/src/components/portal/PortalRail.vue
@@ -84,6 +84,10 @@
+
+
@@ -177,6 +181,18 @@
+
+
+
@@ -188,6 +204,7 @@ import OverflowTabs from '../OverflowTabs.vue'
import BaseButton from '@/components/base/BaseButton.vue'
import BaseBadge from '@/components/base/BaseBadge.vue'
import PortalAvatar from './PortalAvatar.vue'
+import PortalKeyTips from './PortalKeyTips.vue'
import {
RAIL_SIGNAL_LIVE,
RAIL_SIGNAL_UPDATED,
@@ -222,9 +239,13 @@ const props = defineProps({
participants: { type: Array, default: () => [] },
// The mobile bottom-sheet form. A sheet is always "open".
sheet: { type: Boolean, default: false },
+ // ent#621 (follow-up): show the shortcut tips — the panel at the foot of the
+ // open column, the icon at the foot of the collapsed strip. The shell turns
+ // it off once the person has closed it; the rail holds no memory of its own.
+ keyTips: { type: Boolean, default: false },
})
-const emit = defineEmits(['update:open', 'update:activeTab', 'close', 'see-hints'])
+const emit = defineEmits(['update:open', 'update:activeTab', 'close', 'see-hints', 'open-keys', 'dismiss-key-tips'])
const mode = computed(() => (props.sheet ? 'sheet' : props.open ? 'open' : 'collapsed'))
diff --git a/src/frontend/src/components/portal/PortalSidebar.vue b/src/frontend/src/components/portal/PortalSidebar.vue
index 0588c998b..daebc8420 100644
--- a/src/frontend/src/components/portal/PortalSidebar.vue
+++ b/src/frontend/src/components/portal/PortalSidebar.vue
@@ -42,13 +42,31 @@
+
+
+ {{ SEARCH_KEY_HINT }}
@@ -331,7 +349,7 @@
+ the `⌥/` chord opens (the ent#261 `` precedent). -->