Skip to content

Commit aa76566

Browse files
committed
docs(planning): add unified Register client modal plan
Records the scoping decisions and phase breakdown behind the Cursor/Generic tab consolidation shipped in the prior 3 commits. Signed-off-by: crimsonsunset <jsangio1@gmail.com>
1 parent 03aaae7 commit aa76566

1 file changed

Lines changed: 190 additions & 0 deletions

File tree

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
# Unified Register Client Modal — Cursor Preset + Generic
2+
3+
**Last Updated:** Jul 23, 2026
4+
**Status:** Planning — not started
5+
**Branch:** `dev-rebased`
6+
**Depends on:** `upstream-client-mapping-reconciliation.md` Phase 1 (`mcpk_` API-key auth, `register_api_key_client`/`create_client_api_key`), `cursor-workspace-routing-bridge.md` Phase 2 (`buildCursorBridgeMcpJson`, the Cursor bridge concept this consolidates)
7+
**Unblocks:** A single, less confusing Connections entry point for minting API-key clients — no backend changes required
8+
9+
---
10+
11+
## Problem
12+
13+
The Connections page currently exposes two API-key registration surfaces that both call the same backend (`register_api_key_client` in [`oauth.rs`](../../apps/desktop/src-tauri/src/commands/oauth.rs)) but look unrelated to a first-time user:
14+
15+
1. A big always-visible **"Global Cursor setup (no per-repo files)"** card ([`CursorBridgeSection.tsx`](../../apps/desktop/src/features/clients/CursorBridgeSection.tsx)) with its own "Generate global config" button
16+
2. A **"Register client (API key)"** button in the page header that opens a separate generic modal ([`RegisterApiKeyClientModal.tsx`](../../apps/desktop/src/features/clients/RegisterApiKeyClientModal.tsx))
17+
18+
Dig research (`dig-and-ask` session, Jul 23) confirmed these were built in separate planning tracks weeks apart — `upstream-client-mapping-reconciliation.md` shipped the generic modal as the general inbound-auth primitive; `cursor-workspace-routing-bridge.md` added the Cursor card as a follow-on that reuses the same primitive to assemble a paste-ready `~/.cursor/mcp.json` snippet instead of showing a bare key. The bridge planning doc even flagged this ambiguity itself: its file table lists `CursorBridgeSection.tsx` "(or fold into `ClientsPage.tsx`)" — the fold-in was never done.
19+
20+
There's no in-app guidance distinguishing the three ways to connect Cursor (OAuth deep link on the empty state, the global bridge card, per-repo Workspaces install), and the generic modal's Space/Machine pickers are plain `<select>` elements while other parts of the app (e.g. `SpaceSwitcher.tsx`) use a nicer custom dropdown pattern.
21+
22+
---
23+
24+
## Decisions
25+
26+
| # | Decision | Choice | Rationale |
27+
| - | -------- | ------ | --------- |
28+
| 1 | Surface count | Consolidate into **one** modal with client-type presets (Cursor / Generic), reachable only from the header "Register client" button | User explicitly asked for this over keeping two surfaces; removes the redundant always-visible card. |
29+
| 2 | Default preset | **Cursor tab is the default** view when the modal opens | Matches current de-facto behavior (the big card is the first thing users see today) — don't regress the primary path's prominence just because it moved into a modal. |
30+
| 3 | Path education | Add a collapsible **"Which should I use?"** comparison table directly above the submit button row, not a separate doc link | User wants the explainer inline, not requiring a context switch to docs. |
31+
| 4 | Regenerate semantics | User picks **rotate key on existing client** vs **create new client** at regenerate time | Today's Cursor card always mints a new `cursor-global-bridge` client, silently accumulating duplicates in the Connections list. Existing `create_client_api_key` (rotation) already exists and is unused by this flow — wire it in as a choice, don't force one behavior. |
32+
| 5 | Machine assignment for Cursor preset | **Add it** — Cursor preset gets the same machine picker the Generic preset already has | Today only the Generic modal exposes machine tagging; no technical reason the Cursor client can't be tagged too, and per-device machine header routing already supports it. |
33+
| 6 | Dropdown styling | Build a shared **`SearchableSelect`** combobox (typeahead) from existing `DropdownMenu` + `SearchField` primitives in `@mcpmux/ui`, replacing the raw `<select>` Space/Machine pickers | No searchable combobox exists anywhere in the codebase today — this is new shared component work, not a reuse. Built from primitives already in `@mcpmux/ui` rather than a new dependency. |
34+
| 7 | Backend changes | **None** | `register_api_key_client`, `create_client_api_key`, `setClientMachineId` already support everything the unified modal needs. This is a frontend-only consolidation. |
35+
36+
---
37+
38+
## Scope
39+
40+
**In:**
41+
42+
- New `SearchableSelect` component in `packages/ui/src/components/common/` (typeahead-filterable dropdown, built from `DropdownMenu` + `SearchField`)
43+
- Reworked `RegisterApiKeyClientModal.tsx` with a Cursor/Generic tab switch, Cursor defaulting to selected
44+
- Cursor tab: name (defaults to `cursor-global-bridge`, editable), Space lock, machine picker (new), snippet output via existing `buildCursorBridgeMcpJson()`
45+
- Generic tab: unchanged behavior (name, Space lock, machine, raw key output), restyled with `SearchableSelect`
46+
- Inline "Which should I use?" expander with a comparison table (Cursor bridge / Generic key / per-repo Workspaces install / OAuth deep link)
47+
- Regenerate flow: explicit rotate-vs-new-client choice when a client for the current name/type already exists
48+
- Removal of the always-visible `CursorBridgeSection` card from `ClientsPage.tsx`; deletion of `CursorBridgeSection.tsx`
49+
- i18n updates to `clients.json` for the merged copy
50+
51+
**Out:**
52+
53+
| Item | Reason / Deferral |
54+
| ---- | ------------------ |
55+
| Backend changes to `register_api_key_client` or key rotation | Decision 7 — existing commands already cover every code path this modal needs. |
56+
| Changes to OAuth deep-link onboarding (`ConnectIDEs`, empty-state) | Out of scope — that's a different auth mechanism (dynamic OAuth client, not `mcpk_`), only referenced in the comparison table for context. |
57+
| Changes to per-repo Workspaces install (`workspace_install.rs`, `WorkspaceInstallPanel`) | Out of scope — stays as documented fallback per `cursor-workspace-routing-bridge.md` Decision 4; only referenced in the comparison table. |
58+
| Deduplication/cleanup of already-existing duplicate `cursor-global-bridge` clients in users' live databases | One-time data cleanup, not a UI concern — the new rotate option only prevents *future* duplicates. |
59+
| `SearchableSelect` adoption elsewhere in the app (e.g. `workspace-binding-form.component.tsx`'s `Picker`, `RegistryPage`'s `FilterDropdown`) | New component ships scoped to this modal only; broader adoption is a separate follow-up once it's proven here. |
60+
61+
---
62+
63+
## Architecture
64+
65+
### Before → after
66+
67+
```text
68+
Before:
69+
ClientsPage
70+
├── Header "Register client" button → RegisterApiKeyClientModal (generic only)
71+
└── Always-visible CursorBridgeSection card → its own "Generate global config" flow
72+
73+
After:
74+
ClientsPage
75+
└── Header "Register client" button → RegisterApiKeyClientModal
76+
├── Tab: Cursor (default) → mints mcpk_ key → buildCursorBridgeMcpJson() snippet
77+
├── Tab: Generic → mints mcpk_ key → raw key + Bearer instructions
78+
└── "Which should I use?" expander (comparison table, above submit row)
79+
```
80+
81+
Both tabs call the same `registerApiKeyClient()` / `createClientApiKey()` / `setClientMachineId()` frontend wrappers — no new Tauri commands.
82+
83+
### Comparison table content (for the in-modal expander)
84+
85+
| Path | Best for | Setup |
86+
| ---- | -------- | ----- |
87+
| Cursor bridge (this modal, Cursor tab) | Cursor, all repos, zero per-repo files | Paste one snippet into `~/.cursor/mcp.json` |
88+
| Generic API key (this modal, Generic tab) | Headless/CI/remote clients that can't complete browser OAuth | Copy key into client config manually |
89+
| Per-repo Workspaces install | Cursor users who don't want an `npx`/Node dependency | One-click install per folder in Workspaces |
90+
| OAuth deep link | VS Code, Claude Code, other IDEs with reliable `roots` reporting | One click from the empty-state onboarding |
91+
92+
### `SearchableSelect` shape
93+
94+
```ts
95+
interface SearchableSelectProps<T extends string> {
96+
value: T;
97+
onChange: (value: T) => void;
98+
options: Array<{ value: T; label: string; icon?: string }>;
99+
placeholder: string;
100+
onCreateNew?: () => void; // e.g. "+ New machine…" affordance
101+
disabled?: boolean;
102+
testId?: string;
103+
}
104+
```
105+
106+
Built on top of the existing `DropdownMenu`/`DropdownMenuTrigger`/`DropdownMenuContent` primitives ([`packages/ui/src/components/common/DropdownMenu.tsx`](../../packages/ui/src/components/common/DropdownMenu.tsx)) for the trigger/panel/click-outside/escape behavior, and `SearchField` ([`packages/ui/src/components/common/SearchField.tsx`](../../packages/ui/src/components/common/SearchField.tsx)) for the filter input rendered at the top of the panel.
107+
108+
### Regenerate decision flow
109+
110+
```
111+
User clicks "Regenerate" on an existing Cursor-tab client
112+
→ if a client named `cursor-global-bridge` (or the current custom name) already exists:
113+
show two options:
114+
- "Rotate key" → createClientApiKey(existingClientId) → new snippet with new key, same client
115+
- "New client" → registerApiKeyClient(name, ...) → new client, old one left as-is (user can revoke separately)
116+
→ if no existing client: skip the choice, just registerApiKeyClient(name, ...) as today
117+
```
118+
119+
---
120+
121+
## Files to create / modify
122+
123+
| Area | File | Action |
124+
| ---- | ---- | ------ |
125+
| Shared UI | `packages/ui/src/components/common/SearchableSelect.tsx` | Create — typeahead combobox built from `DropdownMenu` + `SearchField` |
126+
| Shared UI | `packages/ui/src/index.ts` (or equivalent barrel) | Modify — export `SearchableSelect` |
127+
| Desktop UI | [`apps/desktop/src/features/clients/RegisterApiKeyClientModal.tsx`](../../apps/desktop/src/features/clients/RegisterApiKeyClientModal.tsx) | Modify — add Cursor/Generic tabs, comparison expander, `SearchableSelect` pickers, rotate-vs-new-client regenerate choice |
128+
| Desktop UI | [`apps/desktop/src/features/clients/CursorBridgeSection.tsx`](../../apps/desktop/src/features/clients/CursorBridgeSection.tsx) | Delete — logic absorbed into the unified modal |
129+
| Desktop UI | [`apps/desktop/src/features/clients/cursor-bridge-config.helpers.ts`](../../apps/desktop/src/features/clients/cursor-bridge-config.helpers.ts) | Keep as-is — `buildCursorBridgeMcpJson()`/`CURSOR_BRIDGE_CLIENT_NAME` now imported into the modal instead of the deleted card |
130+
| Desktop UI | [`apps/desktop/src/features/clients/ClientsPage.tsx`](../../apps/desktop/src/features/clients/ClientsPage.tsx) | Modify — remove the `CursorBridgeSection` render and its import; header button still opens the (now unified) modal |
131+
| i18n | [`apps/desktop/src/locales/en/clients.json`](../../apps/desktop/src/locales/en/clients.json) | Modify — merge `cursorBridge.*` keys into modal-scoped keys, add comparison-table copy, drop unused card-specific keys |
132+
| Tests | `tests/e2e/specs/**` (any spec referencing `cursor-bridge-*` or `register-api-key-*` testids) | Modify — update selectors for the unified modal |
133+
134+
---
135+
136+
## Phases
137+
138+
### Phase 1 — `SearchableSelect` component (~half day)
139+
140+
- Build `SearchableSelect.tsx` in `packages/ui` using `DropdownMenu` (trigger/content/click-outside/escape) + `SearchField` (filter input)
141+
- Support `onCreateNew` for the "+ New machine…" affordance the modal already relies on
142+
- No consumers wired yet in this phase — component ships standalone with its own unit test
143+
144+
**Outcome:** `SearchableSelect` renders, filters options by typed text, and calls `onChange`/`onCreateNew` correctly in isolation. `pnpm typecheck && pnpm lint` pass.
145+
146+
---
147+
148+
### Phase 2 — Unified modal with Cursor/Generic tabs (~1 day)
149+
150+
- Add tab state to `RegisterApiKeyClientModal.tsx`, Cursor selected by default
151+
- Cursor tab: name field defaulting to `CURSOR_BRIDGE_CLIENT_NAME`, Space lock via `SearchableSelect`, machine picker via `SearchableSelect` (new for this tab), submit calls `registerApiKeyClient()` then `buildCursorBridgeMcpJson()` for snippet output (moved import from the deleted `CursorBridgeSection`)
152+
- Generic tab: existing fields, restyled with `SearchableSelect` in place of the raw `<select>`s, output unchanged (raw key)
153+
- Add the "Which should I use?" comparison-table expander directly above the submit button row
154+
155+
**Outcome:** Opening "Register client" defaults to a Cursor-preset view producing the same `~/.cursor/mcp.json` snippet the old card produced; switching to Generic reproduces the old modal's behavior exactly. `pnpm typecheck && pnpm lint` pass.
156+
157+
---
158+
159+
### Phase 3 — Regenerate choice + `ClientsPage` cleanup (~half day)
160+
161+
- Wire the rotate-vs-new-client choice into the Cursor tab's regenerate action (`createClientApiKey` vs `registerApiKeyClient`)
162+
- Remove `CursorBridgeSection` import/render from `ClientsPage.tsx`; delete `CursorBridgeSection.tsx`
163+
- Update `clients.json`: merge `cursorBridge.*` into new modal keys, drop unused ones
164+
- Sweep `tests/e2e/specs/**` for `cursor-bridge-*`/`register-api-key-*` testids and update selectors
165+
166+
**Outcome:** Connections page shows a single header "Register client" entry point; regenerating a Cursor config either rotates the existing client's key or creates a new one per user choice, with no silent duplicate accumulation. `pnpm test:ts && pnpm typecheck && pnpm lint` pass; any affected e2e specs updated.
167+
168+
---
169+
170+
## Key files referenced
171+
172+
| File | Notes |
173+
| ---- | ----- |
174+
| [`apps/desktop/src/features/clients/RegisterApiKeyClientModal.tsx`](../../apps/desktop/src/features/clients/RegisterApiKeyClientModal.tsx) | Generic modal being extended with tabs — current Space/Machine `<select>` fields are what `SearchableSelect` replaces |
175+
| [`apps/desktop/src/features/clients/CursorBridgeSection.tsx`](../../apps/desktop/src/features/clients/CursorBridgeSection.tsx) | Card being deleted — its generate/regenerate logic moves into the modal's Cursor tab |
176+
| [`apps/desktop/src/features/clients/cursor-bridge-config.helpers.ts`](../../apps/desktop/src/features/clients/cursor-bridge-config.helpers.ts) | `buildCursorBridgeMcpJson()`, `CURSOR_BRIDGE_CLIENT_NAME` — unchanged, re-imported into the modal |
177+
| [`apps/desktop/src/features/clients/ClientsPage.tsx`](../../apps/desktop/src/features/clients/ClientsPage.tsx) | Header button + modal mount point; card removal happens here |
178+
| [`apps/desktop/src/lib/api/gateway.ts`](../../apps/desktop/src/lib/api/gateway.ts) | `registerApiKeyClient()`, `createClientApiKey()` — existing wrappers, no changes needed |
179+
| [`packages/ui/src/components/common/DropdownMenu.tsx`](../../packages/ui/src/components/common/DropdownMenu.tsx) | Primitive `SearchableSelect` is built on top of |
180+
| [`packages/ui/src/components/common/SearchField.tsx`](../../packages/ui/src/components/common/SearchField.tsx) | Primitive providing the typeahead input |
181+
| [`apps/desktop/src/components/SpaceSwitcher.tsx`](../../apps/desktop/src/components/SpaceSwitcher.tsx) | Reference pattern for the "nicer" custom dropdown styling the user pointed at |
182+
| [`apps/desktop/src/features/workspaces/workspace-binding-form.component.tsx`](../../apps/desktop/src/features/workspaces/workspace-binding-form.component.tsx) | Has its own `Picker` (still a styled native `<select>`, not a true combobox) — left untouched per Scope/Out |
183+
184+
---
185+
186+
## Related documentation
187+
188+
- [`docs/planning/upstream-client-mapping-reconciliation.md`](./upstream-client-mapping-reconciliation.md) — origin of the generic `RegisterApiKeyClientModal` and `mcpk_` API-key auth this unification builds on
189+
- [`docs/planning/cursor-workspace-routing-bridge.md`](./cursor-workspace-routing-bridge.md) — origin of the Cursor bridge concept and `buildCursorBridgeMcpJson()`, including the never-resolved "or fold into `ClientsPage.tsx`" note this doc finally addresses
190+
- [`docs/manual/workspace-header-routing.md`](../manual/workspace-header-routing.md) — background on why Cursor needs a dedicated routing path at all

0 commit comments

Comments
 (0)