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 @@ @@ -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 @@
+ + +
@@ -331,7 +349,7 @@ + the `⌥/` chord opens (the ent#261 `` precedent). -->