Command palette
Cmd/Ctrl+K from anywhere. The single search-and-jump surface in the app.apps/ui/src/components/shared/command-palette.tsx — built on cmdk.
Sections when no query
| Section | Contents |
|---|---|
| Recent | Last 10 items the user opened (any type). Persisted in localStorage via addRecentItem() / getRecentItems(). Rendered with a Clock icon to distinguish from navigation entries. |
| Navigation | All app pages, gated by the workspace’s enabled modules. New modules register here. |
| Collections | The user’s collections — dynamic from the workspace. |
| Quick Actions | Create Task, Add Contact, Create Organization, Create Knowledge, Register Agent. |
Search results
Grouped by entity type when a query is present: Knowledge, Knowledge Chunks, Tasks, Contacts, Collections, Agents, Routines. Each result has a type-colored icon (blue for knowledge, amber for tasks, emerald for contacts, etc.), a title, and a subtitle showing matched snippet or metadata. Async search runs in a debounced effect; the palette shows “Searching…” while results are fetching.Adding entries
A new module registers in two places:- The Navigation array inside
command-palette.tsx. - The dashboard sidebar (see layout).
search_knowledge_items, search_knowledge_chunks).
Keyboard shortcuts
apps/ui/src/components/shared/keyboard-shortcuts.tsx
A single global registry, mounted once via KeyboardShortcutsProvider at the dashboard layout. Input-aware: skips when focus is in INPUT, TEXTAREA, or any element with contentEditable=true. Press ? to open the help sheet — every registered shortcut shows up there.
Global shortcuts
| Keys | Action |
|---|---|
| Cmd/Ctrl+K | Open command palette |
| Cmd/Ctrl+B | Toggle sidebar (or open mobile drawer) |
| ? | Open keyboard-shortcut help |
| Escape | Close current overlay (Sheet, Dialog, Popover, inline editor) |
G-then-letter navigation
PressG, then the next key within 800ms, to jump to a module:
| Sequence | Destination | Module gate |
|---|---|---|
G D | Dashboard | Always |
G C | Contacts | CRM enabled |
G O | Organizations | CRM enabled |
G T | Tasks | Always |
G A | Agents | Always |
G K | Knowledge | Always |
G E | Collections | Always |
G R | Routines | Always |
G L | Activity | Always |
G N | Notifications | Always |
G S | Settings | Always |
keyboard-shortcuts.tsx, gate it on the relevant ModulesContext flag, and the help sheet will pick it up.
Module-level shortcuts
A feature can register additional shortcuts via theuseShortcuts hook. Examples worth using sparingly: C to open the create panel from a list view, / to focus the filter-bar search input. Every module-level shortcut shows in the help sheet — there is no hidden registry.
Don’t register shortcuts that conflict with browser defaults (Cmd+W, Cmd+T, Cmd+R, Cmd+L) or with text editing (any single letter that types into an input — except in input-aware paths the registry already filters).
Drag and drop
The canonical library is@dnd-kit. Reach for it whenever a feature needs drag-and-drop.
Reference implementation: apps/ui/src/components/shared/folder-tree.tsx — recursive tree with drag-to-reorder and drag-to-nest.
The canonical setup:
DndContextat the boundary — sensorsPointerSensorandKeyboardSensorboth wired so drag works with mouse, touch, and keyboard.SortableContextfor ordered lists (kanban columns, file lists, sidebar groups).useSortableon each draggable item; the hook returnssetNodeRef, listeners, attributes, transform, and transition for the drag state.DragOverlayfor the floating drag preview — keeps the original item in place visually.- Modifiers (
restrictToVerticalAxis,restrictToParentElement) when the gesture should be constrained.
@dnd-kit.
Visual conventions during drag
- The dragged item’s source position renders at 50% opacity and removes its border-on-hover behavior.
- The overlay clone gets a 1px
border-strongand a slightshadow-lg. - Drop zones get a
bg-surface-selectedhighlight ondragOver. - The drag is canceled on Escape, committed on drop. Persistence happens in the
onDragEndhandler — the parent owns the data, the dragged item never persists itself.
Realtime
Lists subscribe to Supabase Realtime so other sessions and agent activity surface live, without a refresh.apps/ui/src/hooks/use-realtime-sync.ts — the multi-table version, used by orchestrator hooks (use-contacts, use-tasks, use-collection-records).
apps/ui/src/hooks/use-realtime.ts — the single-table version, simpler API for one-off subscriptions.
The pattern, in shape:
- The hook subscribes on mount, unsubscribes on unmount.
- Insert / update / delete events from any session (including agents) flow through.
- Tenant-scoped RLS filters out events for other tenants — no client-side filtering needed.
- Optimistic local mutations are reconciled with realtime echoes by ID — the hook does not double-apply.
When to subscribe
- Always — list views and detail views of records that other sessions or agents can change.
- Sometimes — settings pages where stale data is just inconvenient (rather than wrong).
- Never — pure UI state, transient form inputs, anything client-only.
Hover, focus, and selection feedback
Three subtle but consistent affordances:- Hover —
bg-surface-hover(oklch(0 0 0 / 4%)light,oklch(1 0 0 / 5%)dark). 120ms transition. - Focus — visible ring,
ring-ringtoken, 2px. Always visible via keyboard, never suppressed. - Selected —
bg-surface-selected(slightly darker than hover). For multi-select, also a checkbox in the row’s leading slot.
bg-foreground), not a heavy fill. Depth via accent, not background — see principles.
Pulling it together
Most modules touch all four systems. A correctly-built list view will:- Render inside the shell with a
PageHeader. - Subscribe to its data via a realtime hook.
- Register its routes in the command palette and a G-shortcut.
- Use
@dnd-kitif rows are reorderable. - Compose its filters via the
FilterBarand its rows via theDataTable.

