From 25c819457962ff54b852c505402305f9aea51fc7 Mon Sep 17 00:00:00 2001
From: Danny White <3104761+dnywh@users.noreply.github.com>
Date: Fri, 22 May 2026 01:32:22 +1000
Subject: [PATCH] feat(studio): add Connect sheet shortcut guidance (#46185)
## What kind of change does this PR introduce?
Feature, docs update.
- Resolves FE-3419
- First pass for DEPR-578
## What is the current behaviour?
The Connect sheet can be opened from visible UI and command-menu
actions, but it does not have a direct keyboard shortcut. Studio also
has shortcut conventions in code, but limited agent-facing review
guidance for contributors adding or touching Studio UI.
## What is the new behaviour?
FE-3419:
- Adds `O then C` to open the Connect sheet for active healthy projects.
- Mounts the shortcut from the always-rendered Connect sheet, so it
works without first opening the lazy command menu.
- Surfaces the shortcut on the Connect button tooltip, in the shortcuts
reference sheet, and on the Connect command-menu action.
- Forces the tooltip closed while the sheet is open so Escape closes the
sheet without also driving tooltip state.
- Tracks keyboard shortcut opens with the existing Connect sheet
telemetry event.
- Moves single-item AI Assistant and Inline Editor shortcuts to the
_Global Actions_ section in the cheatsheet.
DEPR-578:
- Adds a short Studio shortcut convention to `.claude/CLAUDE.md`.
- Adds scoped Copilot review guidance for Studio shortcut coverage,
discovery, and collision checks.
- Points the guidance back to the existing shortcut registry,
`useShortcut`, `Shortcut`, and `ShortcutTooltip` implementation context.
| After |
| --- |
| |
| |
Validation:
- `pnpm --dir apps/studio exec vitest --run
components/ui/GlobalShortcuts/ShortcutsReferenceSheet.test.tsx
components/interfaces/ConnectButton/Connect.Commands.test.tsx
components/interfaces/ConnectSheet/useConnectSheetShortcut.test.ts`
- `git diff --check`
## Summary by CodeRabbit
* **New Features**
* Keyboard shortcut to open the Connect sheet from anywhere; Connect
button displays the shortcut and is enabled only for eligible projects.
* New "Global Actions" group in the shortcuts reference including AI
Assistant, Inline Editor, and Connect.
* **Documentation**
* Added Studio keyboard-shortcuts guidance and linked it in project
instructions.
* **Tests**
* Added tests covering connect shortcut behavior and command
registration.
* **Telemetry**
* Connect-sheet open events now record keyboard shortcut as a source.
[](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/46185?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)
---------
Co-authored-by: Ali Waseem
---
.claude/CLAUDE.md | 2 +
.github/copilot-instructions.md | 1 +
.../studio-shortcuts.instructions.md | 37 ++++++++
.../ConnectButton/Connect.Commands.test.tsx | 70 +++++++++++++++
.../ConnectButton/Connect.Commands.tsx | 19 ++++-
.../ConnectButton/ConnectButton.tsx | 37 ++++++--
.../interfaces/ConnectSheet/ConnectSheet.tsx | 3 +
.../useConnectSheetShortcut.test.ts | 85 +++++++++++++++++++
.../ConnectSheet/useConnectSheetShortcut.ts | 26 ++++++
.../ShortcutsReferenceSheet.test.tsx | 21 +++++
apps/studio/state/app-state.ts | 4 +-
.../studio/state/shortcuts/referenceGroups.ts | 3 +
apps/studio/state/shortcuts/registry.ts | 11 +++
packages/common/telemetry-constants.ts | 2 +-
14 files changed, 312 insertions(+), 9 deletions(-)
create mode 100644 .github/instructions/studio-shortcuts.instructions.md
create mode 100644 apps/studio/components/interfaces/ConnectButton/Connect.Commands.test.tsx
create mode 100644 apps/studio/components/interfaces/ConnectSheet/useConnectSheetShortcut.test.ts
create mode 100644 apps/studio/components/interfaces/ConnectSheet/useConnectSheetShortcut.ts
diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md
index cb06d2c3907..6fe68cc8e93 100644
--- a/.claude/CLAUDE.md
+++ b/.claude/CLAUDE.md
@@ -33,6 +33,8 @@ pnpm typecheck # typecheck all packages
**Language** — Use U.S. English everywhere.
+**Studio shortcuts** — when adding or changing repeated Studio UI actions, use the shared shortcut registry and primitives in `apps/studio/state/shortcuts/` and `apps/studio/components/ui/Shortcut*.tsx`. Prefer registered, discoverable shortcuts over one-off keyboard listeners; keep `G then ...` chords for navigation.
+
## Studio
Pages router. Co-locate sub-components with parent. Avoid barrel re-export files.
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index 75e6f46f14c..d9819a0edf8 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -55,5 +55,6 @@ Path-specific rules in `.github/instructions/`:
- **E2E Tests**: `studio-e2e-tests.instructions.md` — selector priority, anti-patterns (`waitForTimeout`, `force: true`)
- **Composition Patterns**: `studio-composition-patterns.instructions.md` — avoid boolean props, use compound components
- **shadcn/Radix Components**: `studio-shadcn-components.instructions.md` — accessibility handled by primitives, do not flag
+- **Keyboard Shortcuts**: `studio-shortcuts.instructions.md` — shared registry usage, discovery, collision checks
These files are scoped to `apps/studio/` and applied automatically during reviews.
diff --git a/.github/instructions/studio-shortcuts.instructions.md b/.github/instructions/studio-shortcuts.instructions.md
new file mode 100644
index 00000000000..c11944993c5
--- /dev/null
+++ b/.github/instructions/studio-shortcuts.instructions.md
@@ -0,0 +1,37 @@
+---
+applyTo: 'apps/studio/**'
+---
+
+# Studio Shortcut Review Rules
+
+All comments are **advisory**.
+
+## Core Principle
+
+When Studio UI changes introduce or materially alter repeated user actions, consider whether keyboard shortcut coverage should be added or updated. Shortcuts should use the shared Studio shortcut system and be discoverable from the visible UI.
+
+## When to Flag
+
+- PR adds a primary repeated action, toolbar action, list/table operation, or sub-page navigation without considering shortcut coverage.
+- PR adds a one-off `keydown` listener for a normal Studio action instead of using the shortcut registry and `useShortcut`.
+- PR registers a shortcut but does not expose it via `ShortcutTooltip`, `ShortcutBadge`, or command-menu badge where the action is visible.
+- PR wires `useShortcut` and `ShortcutTooltip` separately for a single visible element instead of using the `` wrapper.
+- PR uses `G then ...` for a non-navigation action.
+- PR adds a broad `Mod+letter` shortcut that overlaps common browser, editor, system, copy/save/search, or devtools behavior.
+- PR adds a shortcut without checking existing registry and non-registry listeners for collisions.
+- PR adds a search or filter input with custom Escape handling instead of `onSearchInputEscape` from `@/lib/keyboard`.
+
+## Preferred Pattern
+
+- Add definitions in `apps/studio/state/shortcuts/registry.ts` or `apps/studio/state/shortcuts/registry/*`.
+- Add or reuse a cheatsheet group in `apps/studio/state/shortcuts/referenceGroups.ts` when a shortcut belongs to a new surface; prefer existing groups for global actions, navigation, and established feature surfaces.
+- Register with `useShortcut`.
+- For a single visible element that owns the action (button, icon button, menu trigger), prefer the `` wrapper in `apps/studio/components/ui/Shortcut.tsx` — it binds `useShortcut` and `ShortcutTooltip` from one `id` so the hotkey and tooltip can't drift. Drop down to `useShortcut` + `ShortcutTooltip`/`ShortcutBadge` separately only when the trigger and the visible affordance live on different elements.
+- Gate availability with `enabled`.
+- Use `showInSettings: false` for contextual shortcuts that only work inside a page state, panel, sheet, or selected-row mode.
+- Surface visible actions with `ShortcutTooltip` or `ShortcutBadge`.
+- For sheet-owned actions, mount the shortcut from the sheet or a sheet-owned hook; gate with `enabled` when the action only applies while the sheet is open. See `apps/studio/components/interfaces/ConnectSheet/useConnectSheetShortcut.ts`.
+- For search/filter inputs, wire `onKeyDown` to `onSearchInputEscape(value, setValue)` from `@/lib/keyboard` so Escape clears the value, then blurs on a second press, and doesn't bubble to a parent dialog/popover. Don't re-implement this with a local `keydown` listener.
+- Prefer scoped, mnemonic sequential chords over global modifier chords.
+
+Canonical implementation context: `apps/studio/state/shortcuts/registry.ts`, `apps/studio/state/shortcuts/useShortcut.tsx`, and `apps/studio/components/ui/Shortcut*.tsx`
diff --git a/apps/studio/components/interfaces/ConnectButton/Connect.Commands.test.tsx b/apps/studio/components/interfaces/ConnectButton/Connect.Commands.test.tsx
new file mode 100644
index 00000000000..057f5aa29dc
--- /dev/null
+++ b/apps/studio/components/interfaces/ConnectButton/Connect.Commands.test.tsx
@@ -0,0 +1,70 @@
+import { renderHook } from '@testing-library/react'
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+
+import { useConnectCommands } from './Connect.Commands'
+import { PROJECT_STATUS } from '@/lib/constants'
+
+const {
+ mockSetCommandMenuOpen,
+ mockSetConnectTab,
+ mockSetShowConnect,
+ mockUseRegisterCommands,
+ mockUseSelectedProjectQuery,
+} = vi.hoisted(() => ({
+ mockSetCommandMenuOpen: vi.fn(),
+ mockSetConnectTab: vi.fn(),
+ mockSetShowConnect: vi.fn(),
+ mockUseRegisterCommands: vi.fn(),
+ mockUseSelectedProjectQuery: vi.fn(),
+}))
+
+vi.mock('ui-patterns/CommandMenu', () => ({
+ useRegisterCommands: mockUseRegisterCommands,
+ useSetCommandMenuOpen: () => mockSetCommandMenuOpen,
+}))
+
+vi.mock('nuqs', () => ({
+ parseAsBoolean: { withDefault: vi.fn(() => 'showConnectParser') },
+ parseAsString: 'stringParser',
+ useQueryState: (key: string) => {
+ if (key === 'showConnect') return [false, mockSetShowConnect]
+ if (key === 'connectTab') return [null, mockSetConnectTab]
+ return [null, vi.fn()]
+ },
+}))
+
+vi.mock('@/hooks/misc/useSelectedProject', () => ({
+ useSelectedProjectQuery: mockUseSelectedProjectQuery,
+}))
+
+describe('useConnectCommands', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ mockUseSelectedProjectQuery.mockReturnValue({
+ data: { status: PROJECT_STATUS.ACTIVE_HEALTHY },
+ })
+ })
+
+ it('shows the shortcut badge on the Connect command', () => {
+ renderHook(() => useConnectCommands())
+
+ const [, commands, options] = mockUseRegisterCommands.mock.calls[0]
+
+ expect(options.enabled).toBe(true)
+ expect(commands[0]).toMatchObject({
+ id: 'connect-to-project',
+ name: 'Connect to your project',
+ })
+ expect(commands[0].badge).toBeTypeOf('function')
+ })
+
+ it('disables the commands when the selected project cannot connect', () => {
+ mockUseSelectedProjectQuery.mockReturnValue({
+ data: { status: PROJECT_STATUS.INACTIVE },
+ })
+
+ renderHook(() => useConnectCommands())
+
+ expect(mockUseRegisterCommands.mock.calls[0][2].enabled).toBe(false)
+ })
+})
diff --git a/apps/studio/components/interfaces/ConnectButton/Connect.Commands.tsx b/apps/studio/components/interfaces/ConnectButton/Connect.Commands.tsx
index 2364a4a2de4..96949e35c35 100644
--- a/apps/studio/components/interfaces/ConnectButton/Connect.Commands.tsx
+++ b/apps/studio/components/interfaces/ConnectButton/Connect.Commands.tsx
@@ -1,5 +1,7 @@
import { Plug } from 'lucide-react'
import { parseAsBoolean, parseAsString, useQueryState } from 'nuqs'
+import { Fragment } from 'react'
+import { KeyboardShortcut } from 'ui'
import type { ICommand } from 'ui-patterns/CommandMenu'
import { useRegisterCommands, useSetCommandMenuOpen } from 'ui-patterns/CommandMenu'
@@ -7,11 +9,25 @@ import { COMMAND_MENU_SECTIONS } from '@/components/interfaces/App/CommandMenu/C
import { orderCommandSectionsByPriority } from '@/components/interfaces/App/CommandMenu/ordering'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { PROJECT_STATUS } from '@/lib/constants'
+import { hotkeyToKeys } from '@/state/shortcuts/formatShortcut'
+import { SHORTCUT_DEFINITIONS, SHORTCUT_IDS } from '@/state/shortcuts/registry'
+
+const ConnectShortcutBadge = () => (
+