@hachej/boring-workspace 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +94 -0
- package/dist/CodeEditor-DQqOn4xz.js +266 -0
- package/dist/CommandPalette-aM61U-b0.js +5229 -0
- package/dist/FileTree-DRq_bfue.js +245 -0
- package/dist/MarkdownEditor-DjiHxnRv.js +349 -0
- package/dist/WorkspaceLoadingState-By0dZoPD.js +568 -0
- package/dist/agent-tool-NvxKfist.d.ts +28 -0
- package/dist/app-front.d.ts +485 -0
- package/dist/app-front.js +452 -0
- package/dist/app-server.d.ts +53 -0
- package/dist/app-server.js +769 -0
- package/dist/bootstrapServer-BRUqUpVW.d.ts +66 -0
- package/dist/boring-workspace.css +1 -0
- package/dist/charts.d.ts +114 -0
- package/dist/charts.js +143 -0
- package/dist/events.d.ts +178 -0
- package/dist/events.js +88 -0
- package/dist/explorer-DtLUnuah.d.ts +129 -0
- package/dist/panel-DnvDNQac.js +6 -0
- package/dist/server.d.ts +84 -0
- package/dist/server.js +811 -0
- package/dist/shared.d.ts +113 -0
- package/dist/shared.js +11 -0
- package/dist/testing-e2e.d.ts +68 -0
- package/dist/testing-e2e.js +45 -0
- package/dist/testing.d.ts +464 -0
- package/dist/testing.js +10984 -0
- package/dist/utils-B6yFEsav.js +8 -0
- package/dist/workspace.css +5780 -0
- package/dist/workspace.d.ts +2119 -0
- package/dist/workspace.js +1884 -0
- package/docs/INTERFACES.md +58 -0
- package/docs/PLUGIN_STRUCTURE.md +162 -0
- package/docs/README.md +19 -0
- package/docs/bridge.md +135 -0
- package/docs/panels.md +102 -0
- package/docs/plans/GENERIC_EXPLORER_PLUGIN_PLAN.md +455 -0
- package/docs/plans/MACRO_PLUGIN_GENERIC_HELPERS_AUDIT.md +962 -0
- package/docs/plans/PLUGIN_OUTPUTS_ISOLATION_PLAN.md +301 -0
- package/docs/plans/README.md +9 -0
- package/docs/plans/UI_BRIDGE_OWNERSHIP_REFACTOR.md +303 -0
- package/docs/plans/archive/CODE_OWNERSHIP_CLEANUP_PLAN.md +387 -0
- package/docs/plans/archive/COMMAND_PALETTE_REGISTRY.md +814 -0
- package/docs/plans/archive/DECLARATIVE_LAYOUT_MIGRATION.md +277 -0
- package/docs/plans/archive/PLUGIN_MODEL.md +3674 -0
- package/docs/plans/archive/SRC_FOLDER_REORG_PLAN.md +307 -0
- package/docs/plans/archive/UNIFIED_EVENT_BUS.md +647 -0
- package/docs/plans/archive/WORKSPACE_V2_PLAN.md +2489 -0
- package/docs/plugins.md +158 -0
- package/package.json +164 -0
|
@@ -0,0 +1,814 @@
|
|
|
1
|
+
# Command palette: generic command + search registry
|
|
2
|
+
|
|
3
|
+
> **⚠️ SUPERSEDED by [`PLUGIN_MODEL.md`](./PLUGIN_MODEL.md) (2026-04-28).**
|
|
4
|
+
> The palette becomes ONE consumer of the wider plugin model, not a
|
|
5
|
+
> top-level concern. The catalog/command/panel/agent-tool/etc.
|
|
6
|
+
> abstractions all live as contributions inside a `Plugin`. Read
|
|
7
|
+
> `PLUGIN_MODEL.md` for the canonical design; this doc is kept for
|
|
8
|
+
> historical reference of the palette-first iterations.
|
|
9
|
+
>
|
|
10
|
+
> **2026-04-30 update:** references below to `src/data`, `front/data`, or
|
|
11
|
+
> `createFilesCatalog` as a core export are historical. Filesystem catalogs
|
|
12
|
+
> now live under `src/plugins/filesystemPlugin`; the palette searches generic
|
|
13
|
+
> catalog outputs, and "files" are only the filesystem plugin's catalog.
|
|
14
|
+
|
|
15
|
+
**Status:** SUPERSEDED — review v3 (factory pattern + shell auto-registration locked in)
|
|
16
|
+
**Owners:** workspace
|
|
17
|
+
**Last updated:** 2026-04-28
|
|
18
|
+
|
|
19
|
+
## v3 lock-ins (supersede earlier sections where they conflict)
|
|
20
|
+
|
|
21
|
+
After codex + gemini reviews and three rounds of grilling, the final
|
|
22
|
+
shape is:
|
|
23
|
+
|
|
24
|
+
1. **Catalogs are composed via factories, not auto-magic defaults.**
|
|
25
|
+
`@boring/workspace` exports `createFilesCatalog`,
|
|
26
|
+
`createSessionsCatalog`, etc. Hosts pass them through
|
|
27
|
+
`<WorkspaceProvider catalogs={[…]}>`. There is no `onOpenFile`
|
|
28
|
+
prop on WorkspaceProvider, no slot config, no auto-mount of
|
|
29
|
+
built-ins from a host-passed shape. Each factory takes
|
|
30
|
+
`onSelect` (the host-specific intent) plus optional overrides
|
|
31
|
+
(`paletteLimit`, `paletteIcon`, `label`, `order`).
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
<WorkspaceProvider catalogs={[
|
|
35
|
+
createFilesCatalog({
|
|
36
|
+
onSelect: (row) => surface.openPanel({
|
|
37
|
+
id: row.id, component: "code-editor", params: { path: row.id },
|
|
38
|
+
}),
|
|
39
|
+
}),
|
|
40
|
+
// host's own domain catalogs:
|
|
41
|
+
createReportsCatalog({ … }),
|
|
42
|
+
]}>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
2. **Components like `<ChatCenteredShell />` AUTO-REGISTER their own
|
|
46
|
+
catalogs and commands internally** (no new shell props). When the
|
|
47
|
+
shell receives `sessions` + `onSwitchSession`, it calls
|
|
48
|
+
`useCatalogRegistry().register(createSessionsCatalog({sessions,
|
|
49
|
+
onSelect: onSwitchSession}))` internally. When mounted, it
|
|
50
|
+
registers `toggleDrawer` / `toggleWorkbench` / `newChat` via
|
|
51
|
+
`useCommandRegistry()`.
|
|
52
|
+
|
|
53
|
+
Sessions "just work" from the shell; the host wires nothing
|
|
54
|
+
extra at the catalog level.
|
|
55
|
+
|
|
56
|
+
3. **Late-wins-on-id is the universal override.** A host that wants
|
|
57
|
+
different palette limit / icon / behavior for ANY catalog
|
|
58
|
+
(including a shell-auto-registered one) registers a catalog with
|
|
59
|
+
the same `id` on `WorkspaceProvider`. The registry's
|
|
60
|
+
late-wins-on-id rule replaces the inner registration. Same
|
|
61
|
+
mechanism works for built-in catalogs, shell-internal catalogs,
|
|
62
|
+
and any future contribution.
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
<WorkspaceProvider catalogs={[
|
|
66
|
+
createSessionsCatalog({
|
|
67
|
+
sessions, onSelect: customSwitcher,
|
|
68
|
+
paletteLimit: 20, // override default 5
|
|
69
|
+
paletteIcon: <CustomIcon />,
|
|
70
|
+
}),
|
|
71
|
+
]}>
|
|
72
|
+
<ChatCenteredShell sessions={sessions} onSwitchSession={customSwitcher} />
|
|
73
|
+
</WorkspaceProvider>
|
|
74
|
+
// Shell tries to register id:"sessions"; host's wins.
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
4. **Factories ARE the public API for advanced composition.** Both
|
|
78
|
+
user decisions from grilling rounds — "auto-register" and
|
|
79
|
+
"exported factories" — are the same thing under this shape:
|
|
80
|
+
factories are exported, hosts use them, late-wins-on-id is the
|
|
81
|
+
override.
|
|
82
|
+
|
|
83
|
+
5. **Recent dropped from v2 stays dropped.** Typed Recent entries
|
|
84
|
+
({kind, id, title, lastOpenedAt}); legacy `string[]` and
|
|
85
|
+
`"cmd:foo"` entries dropped on migration; ⌘Enter / async
|
|
86
|
+
onSelect deferred; commands stay `>`-only.
|
|
87
|
+
|
|
88
|
+
The detailed sections below describe the full design; where they
|
|
89
|
+
mention `onOpenFile`, "auto-mount built-ins", "default Files
|
|
90
|
+
catalog", or any other shape that conflicts with the lock-ins above,
|
|
91
|
+
the lock-ins win.
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
## What changed in v2
|
|
96
|
+
|
|
97
|
+
Codex caught several P0 baseline errors in v1; gemini added concrete UX
|
|
98
|
+
gaps; user picked: 5-row default per catalog, single Enter action, typed
|
|
99
|
+
Recent entries.
|
|
100
|
+
|
|
101
|
+
Substantive corrections from v1:
|
|
102
|
+
|
|
103
|
+
1. **Baseline was wrong about Files.** v1 implied the palette runs
|
|
104
|
+
`useFileSearch` today. It doesn't —
|
|
105
|
+
`packages/workspace/src/components/CommandPalette.tsx:36-37` exposes
|
|
106
|
+
a sync `fileSearchFn?: (q: string) => string[]` prop, and
|
|
107
|
+
`packages/workspace/src/WorkspaceProvider.tsx:377` mounts
|
|
108
|
+
`<CommandPalette />` with no props. **Files search is not wired in
|
|
109
|
+
the default runtime today.** That's a freebie this plan delivers,
|
|
110
|
+
not a regression to manage.
|
|
111
|
+
2. **`ExplorerAdapter` reuse is NOT verbatim.** v1 implied identity;
|
|
112
|
+
`ExplorerRow.leading` is `Badge` (mono code chip), not a ReactNode
|
|
113
|
+
icon. The cmd-palette wants icons. v2 adds an explicit
|
|
114
|
+
`CatalogConfig.paletteIcon?: ReactNode` separate from the row's
|
|
115
|
+
badge so the same adapter feeds both surfaces with their own visual
|
|
116
|
+
language.
|
|
117
|
+
3. **`withCommandPalette={false}` override doesn't exist on
|
|
118
|
+
`WorkspaceProvider` today.** v1 referenced it as if it did. v2
|
|
119
|
+
adds it as part of Phase 1 (BEFORE dropping the shell prop).
|
|
120
|
+
4. **"No breaking changes" was false.** Dropping `CommandPaletteProps`
|
|
121
|
+
and the shell's `withCommandPalette` prop ARE public API breaks.
|
|
122
|
+
v2 calls them out + lists what consumers need to do.
|
|
123
|
+
5. **Phase 1 omitted shell work.** v1 listed `ChatCenteredShell.tsx`
|
|
124
|
+
line removals in the dead-code section but Phase 1 steps didn't
|
|
125
|
+
include the shell. v2 adds shell migration to Phase 1.
|
|
126
|
+
6. **Registry reactivity unspecified.** Current `CommandRegistry` is
|
|
127
|
+
a mutable Map with no subscribe. Late
|
|
128
|
+
`registry.register/unregister` won't trigger React re-renders. v2
|
|
129
|
+
moves to a useSyncExternalStore-backed registry with explicit
|
|
130
|
+
subscribe semantics.
|
|
131
|
+
7. **File-open ownership.** The provider can't call into the chat
|
|
132
|
+
shell's `openArtifact` ref. v2: hosts register the FilesCatalog
|
|
133
|
+
with their OWN `onSelect`. WorkspaceProvider supplies a
|
|
134
|
+
sane-default Files adapter; the host overrides `onSelect` for
|
|
135
|
+
non-default file-open behavior (chat shell's surface, IDE's
|
|
136
|
+
dockview, …).
|
|
137
|
+
8. **Recent stored command IDs but opened as paths.** Real bug,
|
|
138
|
+
currently exists at
|
|
139
|
+
`packages/workspace/src/components/CommandPalette.tsx:128-134`
|
|
140
|
+
(`addToRecent("cmd:" + id)`) +
|
|
141
|
+
`packages/workspace/src/components/CommandPalette.tsx:230-236`
|
|
142
|
+
(Recent group renders all recents through `handleFileSelect`).
|
|
143
|
+
v2 fixes this in Phase 1 alongside the typed-recent migration.
|
|
144
|
+
9. **`pg_trgm` mentions deleted.** Workspace package plan should not
|
|
145
|
+
leak DB index choices for hypothetical future SessionsCatalog
|
|
146
|
+
backends.
|
|
147
|
+
|
|
148
|
+
User decisions (collected via grilling):
|
|
149
|
+
|
|
150
|
+
- **paletteLimit default = 5 per catalog.** Files goes from 50 → 5
|
|
151
|
+
(regression accepted in favor of spotlight-style multi-catalog
|
|
152
|
+
layout).
|
|
153
|
+
- **Single action per row.** `CatalogConfig.onSelect(row)` is the only
|
|
154
|
+
intent. ⌘Enter / right-arrow deferred until real demand.
|
|
155
|
+
- **Typed Recent entries now.** Recent localStorage migrates from
|
|
156
|
+
`string[]` of paths to `Array<{ kind: "file" | "session" | …, id:
|
|
157
|
+
string, lastOpenedAt: number }>` in Phase 1.
|
|
158
|
+
|
|
159
|
+
## Problem
|
|
160
|
+
|
|
161
|
+
The workspace package ships a `<CommandPalette />` (⌘K) that today is
|
|
162
|
+
three result paths bolted together — and the Files path doesn't even
|
|
163
|
+
run by default:
|
|
164
|
+
|
|
165
|
+
1. **Recent (Files only)** — `localStorage`-backed list rendered when
|
|
166
|
+
the search box is empty
|
|
167
|
+
(`packages/workspace/src/components/CommandPalette.tsx:34-43`).
|
|
168
|
+
2. **Files (synchronous, prop-driven, currently dead in default
|
|
169
|
+
runtime)** —
|
|
170
|
+
`packages/workspace/src/components/CommandPalette.tsx:36`
|
|
171
|
+
exposes `fileSearchFn?: (q: string) => string[]`. Provider mounts
|
|
172
|
+
the palette with no props
|
|
173
|
+
(`packages/workspace/src/WorkspaceProvider.tsx:377`), so file results
|
|
174
|
+
never appear unless a host wraps another `<CommandPalette />` with
|
|
175
|
+
the prop wired (no host does today).
|
|
176
|
+
3. **Commands (active when user types `>`)** — three default commands
|
|
177
|
+
from `WorkspaceProvider`
|
|
178
|
+
(`packages/workspace/src/WorkspaceProvider.tsx:292-323`) plus
|
|
179
|
+
3-N more registered ad-hoc by `ChatCenteredShell` in a
|
|
180
|
+
`useEffect` (`packages/workspace/src/components/chat/ChatCenteredShell.tsx:400-431`).
|
|
181
|
+
The session-row loop is the giveaway — those aren't really
|
|
182
|
+
commands, they're search results.
|
|
183
|
+
|
|
184
|
+
Plus an actual bug: Recent stores command IDs as
|
|
185
|
+
`addToRecent("cmd:" + id)`
|
|
186
|
+
(`packages/workspace/src/components/CommandPalette.tsx:128-134`) but
|
|
187
|
+
the Recent group renders every entry as if it were a file path
|
|
188
|
+
(`packages/workspace/src/components/CommandPalette.tsx:230-236`).
|
|
189
|
+
Selecting a recent command currently fires `onOpenFile?.("cmd:foo")`,
|
|
190
|
+
which means recently-run commands silently break.
|
|
191
|
+
|
|
192
|
+
The shape stops scaling the moment a child app wants to:
|
|
193
|
+
|
|
194
|
+
- Surface its **sessions** alongside files (e.g. `Sessions: "Workspace
|
|
195
|
+
demo"`, `Sessions: "Plan review"`)
|
|
196
|
+
- Surface its **workspaces / members** for jump-to navigation
|
|
197
|
+
- Plug an arbitrary catalog into the palette (the same catalog that
|
|
198
|
+
already powers `<DataExplorer />`'s data surface, see
|
|
199
|
+
`packages/workspace/src/components/DataExplorer/types.ts`)
|
|
200
|
+
|
|
201
|
+
Doing any of those today means **forking `CommandPalette.tsx`** or
|
|
202
|
+
threading bespoke props down. Both are non-starters once we have more
|
|
203
|
+
than two child apps.
|
|
204
|
+
|
|
205
|
+
Adjacent observation: `@boring/workspace` already defines
|
|
206
|
+
`ExplorerAdapter` — an async, AbortSignal-aware, filterable, paginated
|
|
207
|
+
search interface used by the data catalog UI. That IS the search
|
|
208
|
+
engine the palette wants. We should not be inventing a second one.
|
|
209
|
+
|
|
210
|
+
## Goal
|
|
211
|
+
|
|
212
|
+
A single command palette where any consumer (workspace, child app, or
|
|
213
|
+
even another `@boring/*` package) can register either:
|
|
214
|
+
|
|
215
|
+
- **Commands** — discrete actions with `id`, `title`, optional
|
|
216
|
+
`shortcut`, optional `when` predicate, `run()` callback. (Already
|
|
217
|
+
exists; scope here is to formalize the registration shape so the
|
|
218
|
+
shell stops registering imperatively.)
|
|
219
|
+
- **Search catalogs** — async, query-driven result lists with the
|
|
220
|
+
`ExplorerAdapter` shape that `<DataExplorer />` already consumes.
|
|
221
|
+
Each catalog renders its own group in the palette
|
|
222
|
+
(`<CommandGroup heading="Sessions">`, `<CommandGroup heading="Files">`,
|
|
223
|
+
…).
|
|
224
|
+
|
|
225
|
+
Two presentations of the same engine:
|
|
226
|
+
|
|
227
|
+
| Surface | Scope | Use case |
|
|
228
|
+
|---|---|---|
|
|
229
|
+
| `<DataExplorer />` pane | One catalog at a time, full-screen, with facets | Browse sessions / data / members in detail |
|
|
230
|
+
| `<CommandPalette />` (⌘K) | Top-N from EVERY registered catalog, plus commands | Spotlight-style jump-to-anything |
|
|
231
|
+
|
|
232
|
+
One adapter per entity, two presentations, zero duplicated search
|
|
233
|
+
logic.
|
|
234
|
+
|
|
235
|
+
## Non-goals
|
|
236
|
+
|
|
237
|
+
- Replacing the file-tree's own search. The file tree still goes
|
|
238
|
+
through `useFileSearch` directly and renders its own UX; we just
|
|
239
|
+
register the same backend as a catalog so the palette can also
|
|
240
|
+
surface file results. (No double network calls — react-query caches
|
|
241
|
+
by `[base, "search", q, limit]`.)
|
|
242
|
+
- Adding a SessionsCatalog, WorkspacesCatalog, etc. in this PR. The
|
|
243
|
+
scope is the registry mechanism + migrating the existing Files /
|
|
244
|
+
Recent / Commands paths onto it. Future PRs add new catalogs as
|
|
245
|
+
~30-line additions.
|
|
246
|
+
- Server-side coordination. Each catalog backend is its own HTTP
|
|
247
|
+
route with its own indexing strategy; the palette doesn't care.
|
|
248
|
+
Backend-specific concerns (postgres indexing, full-text search
|
|
249
|
+
configuration) are explicitly out of this frontend-package plan.
|
|
250
|
+
- Persistent favorites / pinned items. Out of scope; revisit in a
|
|
251
|
+
follow-up.
|
|
252
|
+
- Cross-catalog ranking. Each catalog's results are sorted within its
|
|
253
|
+
group; no inter-group score is attempted.
|
|
254
|
+
|
|
255
|
+
## Design
|
|
256
|
+
|
|
257
|
+
### The catalog interface
|
|
258
|
+
|
|
259
|
+
Reuse `ExplorerAdapter` verbatim from
|
|
260
|
+
`packages/workspace/src/components/DataExplorer/types.ts:68-72`:
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
export type ExplorerAdapter = {
|
|
264
|
+
search(args: SearchArgs): Promise<SearchResult>
|
|
265
|
+
fetchFacets?(args: FacetsArgs): Promise<Facets>
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
The cmd-palette doesn't use `fetchFacets` (no facet popover at this
|
|
270
|
+
size) and ignores `group` / `filters` in `SearchArgs`. `SearchResult`
|
|
271
|
+
provides `items: ExplorerRow[]` which renders 1:1 to a
|
|
272
|
+
`<CommandItem>` per row.
|
|
273
|
+
|
|
274
|
+
### `ExplorerRow.leading` semantics
|
|
275
|
+
|
|
276
|
+
`ExplorerRow.leading` is a `Badge` (
|
|
277
|
+
`packages/workspace/src/components/DataExplorer/types.ts:10-14`,
|
|
278
|
+
`{ code: string, tooltip?: string }`) — a mono text chip, NOT a
|
|
279
|
+
ReactNode icon. `<DataExplorer />` renders it via the `<Chip>`
|
|
280
|
+
component
|
|
281
|
+
(`packages/workspace/src/components/DataExplorer/DataExplorer.tsx:498-507`).
|
|
282
|
+
|
|
283
|
+
For the palette we want lucide icons, not text chips. The
|
|
284
|
+
`CatalogConfig` exposes a SEPARATE `paletteIcon?: ReactNode` and the
|
|
285
|
+
palette renderer ignores `row.leading` entirely. That keeps the
|
|
286
|
+
adapter signature unchanged (no new field on `ExplorerRow`) while
|
|
287
|
+
giving each surface its own visual language.
|
|
288
|
+
|
|
289
|
+
### `CatalogConfig`
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
export type CatalogConfig = {
|
|
293
|
+
/** Stable id, used for keys + debugging. */
|
|
294
|
+
id: string
|
|
295
|
+
/** Group heading in the palette ("Files", "Sessions", "Members"). */
|
|
296
|
+
label: string
|
|
297
|
+
/**
|
|
298
|
+
* Top-N rows shown in the cmd-palette inline. Defaults to 5
|
|
299
|
+
* (per-spotlight-style: keep palette tight, room for multiple groups).
|
|
300
|
+
* Hosts can override per-catalog.
|
|
301
|
+
*/
|
|
302
|
+
paletteLimit?: number
|
|
303
|
+
/**
|
|
304
|
+
* Optional priority for ordering palette groups (lower = earlier).
|
|
305
|
+
* Defaults to 100. Built-in: Commands group (when in `>` mode) is
|
|
306
|
+
* always rendered first, followed by Recent (when query is empty),
|
|
307
|
+
* followed by catalogs sorted by `order`.
|
|
308
|
+
*/
|
|
309
|
+
order?: number
|
|
310
|
+
/**
|
|
311
|
+
* Lucide-style icon rendered ahead of every row. Replaces the
|
|
312
|
+
* adapter's `row.leading` Badge for palette presentation; the
|
|
313
|
+
* data-explorer surface continues to render `row.leading` as a
|
|
314
|
+
* mono text chip.
|
|
315
|
+
*/
|
|
316
|
+
paletteIcon?: ReactNode
|
|
317
|
+
/**
|
|
318
|
+
* Single canonical action when the user picks a row. Fires on Enter
|
|
319
|
+
* / click. The palette closes itself + records a typed Recent entry
|
|
320
|
+
* automatically — catalogs only handle the side-effect.
|
|
321
|
+
*/
|
|
322
|
+
onSelect: (row: ExplorerRow) => void
|
|
323
|
+
/**
|
|
324
|
+
* The "kind" written into the typed Recent entry on select. Must
|
|
325
|
+
* match the catalog id's domain (e.g. "file" for FilesCatalog,
|
|
326
|
+
* "session" for SessionsCatalog) so the Recent group can route
|
|
327
|
+
* each entry to the right catalog's onSelect on re-pick.
|
|
328
|
+
*/
|
|
329
|
+
recentKind: string
|
|
330
|
+
/** The actual search engine. */
|
|
331
|
+
adapter: ExplorerAdapter
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
Note what's absent vs v1: no `searchEmpty` flag (premature — Recent
|
|
336
|
+
is its own thing), no `defaultIcon` (replaced by `paletteIcon` to
|
|
337
|
+
disambiguate from the badge concept), no second action.
|
|
338
|
+
|
|
339
|
+
### Reactivity-safe registry
|
|
340
|
+
|
|
341
|
+
Current `CommandRegistry` is a mutable Map
|
|
342
|
+
(`packages/workspace/src/registry/CommandRegistry.ts:4`) with no
|
|
343
|
+
subscribe API. If a host registers a catalog late (e.g. chat shell
|
|
344
|
+
adds a SessionsCatalog after `props.sessions` arrives), an
|
|
345
|
+
already-rendered palette won't update.
|
|
346
|
+
|
|
347
|
+
Phase 1 introduces `CatalogRegistry` as a useSyncExternalStore-friendly
|
|
348
|
+
store:
|
|
349
|
+
|
|
350
|
+
```ts
|
|
351
|
+
export class CatalogRegistry {
|
|
352
|
+
private catalogs = new Map<string, CatalogConfig>()
|
|
353
|
+
private listeners = new Set<() => void>()
|
|
354
|
+
|
|
355
|
+
register(cfg: CatalogConfig) {
|
|
356
|
+
this.catalogs.set(cfg.id, cfg)
|
|
357
|
+
this.emit()
|
|
358
|
+
}
|
|
359
|
+
unregister(id: string) {
|
|
360
|
+
this.catalogs.delete(id)
|
|
361
|
+
this.emit()
|
|
362
|
+
}
|
|
363
|
+
list(): CatalogConfig[] {
|
|
364
|
+
return Array.from(this.catalogs.values()).sort(
|
|
365
|
+
(a, b) => (a.order ?? 100) - (b.order ?? 100),
|
|
366
|
+
)
|
|
367
|
+
}
|
|
368
|
+
subscribe(fn: () => void): () => void {
|
|
369
|
+
this.listeners.add(fn)
|
|
370
|
+
return () => this.listeners.delete(fn)
|
|
371
|
+
}
|
|
372
|
+
private emit() {
|
|
373
|
+
for (const fn of this.listeners) fn()
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
`useCatalogs()` is implemented via `useSyncExternalStore(reg.subscribe,
|
|
379
|
+
reg.list, reg.list)`, so any palette already mounted re-renders when
|
|
380
|
+
register/unregister fires. We retrofit the same pattern onto
|
|
381
|
+
`CommandRegistry` (also currently mutation-without-subscribe) as part
|
|
382
|
+
of this PR — small change, fixes a latent bug where late
|
|
383
|
+
`registerCommand` calls in the chat shell race with palette open.
|
|
384
|
+
|
|
385
|
+
### Catalog merge semantics
|
|
386
|
+
|
|
387
|
+
`WorkspaceProvider` accepts:
|
|
388
|
+
|
|
389
|
+
```ts
|
|
390
|
+
interface WorkspaceProviderProps {
|
|
391
|
+
// … existing props …
|
|
392
|
+
catalogs?: CatalogConfig[]
|
|
393
|
+
/** Drop the built-in defaults (currently just FilesCatalog). */
|
|
394
|
+
withDefaultCatalogs?: boolean // default true
|
|
395
|
+
/** Hosts that want to suppress the palette entirely. */
|
|
396
|
+
withCommandPalette?: boolean // default true (currently always true)
|
|
397
|
+
}
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
The provider's effective catalog list is:
|
|
401
|
+
|
|
402
|
+
1. Built-in defaults (FilesCatalog) — only when `withDefaultCatalogs`
|
|
403
|
+
is `true` AND the host has supplied an `onOpenFile` (or whatever
|
|
404
|
+
shape we settle on for default file-open; see "File-open
|
|
405
|
+
ownership" below).
|
|
406
|
+
2. Host-passed `catalogs` prop.
|
|
407
|
+
3. Catalogs registered imperatively via
|
|
408
|
+
`useCatalogRegistry().register(cfg)`.
|
|
409
|
+
|
|
410
|
+
By id: later wins on collision. Hosts can override the default
|
|
411
|
+
FilesCatalog by registering their own `id: "files"` catalog.
|
|
412
|
+
|
|
413
|
+
### File-open ownership
|
|
414
|
+
|
|
415
|
+
Codex flagged this. The provider can't dispatch into the chat shell's
|
|
416
|
+
`openArtifact` ref or the IDE's dockview API. So the provider doesn't
|
|
417
|
+
own file-open behavior — it just supplies the search adapter. Two
|
|
418
|
+
shapes:
|
|
419
|
+
|
|
420
|
+
- **Hosts that use `<DataProvider>` and want a sane default**: pass
|
|
421
|
+
`onOpenFile?: (path: string) => void` to `WorkspaceProvider`. The
|
|
422
|
+
provider builds the FilesCatalog with that callback wired into
|
|
423
|
+
`onSelect`. Apps that don't pass `onOpenFile` get no FilesCatalog
|
|
424
|
+
by default.
|
|
425
|
+
- **Hosts with custom file-open semantics** (chat shell): register
|
|
426
|
+
their own FilesCatalog via `catalogs={[customFilesCatalog]}` (or
|
|
427
|
+
via the registry), with `onSelect` that calls
|
|
428
|
+
`surface.openPanel(...)` or whatever they need. They get the
|
|
429
|
+
built-in adapter via a small helper:
|
|
430
|
+
|
|
431
|
+
```ts
|
|
432
|
+
import { createFileSearchAdapter } from "@boring/workspace"
|
|
433
|
+
|
|
434
|
+
const filesCatalog: CatalogConfig = {
|
|
435
|
+
id: "files",
|
|
436
|
+
label: "Files",
|
|
437
|
+
order: 10,
|
|
438
|
+
paletteIcon: <FileIcon className="size-4" />,
|
|
439
|
+
recentKind: "file",
|
|
440
|
+
adapter: createFileSearchAdapter(client),
|
|
441
|
+
onSelect: (row) => surface.openPanel({ id: row.id, component: "code-editor", params: { path: row.id } }),
|
|
442
|
+
}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
`createFileSearchAdapter(client: DataClient)` is a thin pure factory
|
|
446
|
+
(no React) that takes a `DataClient` instance and returns the
|
|
447
|
+
adapter:
|
|
448
|
+
|
|
449
|
+
```ts
|
|
450
|
+
export function createFileSearchAdapter(client: DataClient): ExplorerAdapter {
|
|
451
|
+
return {
|
|
452
|
+
async search({ query, limit, signal }) {
|
|
453
|
+
if (!query) return { items: [], total: 0, hasMore: false }
|
|
454
|
+
const paths = await client.search(query, limit, { signal })
|
|
455
|
+
return {
|
|
456
|
+
items: paths.map((p) => ({
|
|
457
|
+
id: p,
|
|
458
|
+
title: basename(p),
|
|
459
|
+
subtitle: dirname(p),
|
|
460
|
+
})),
|
|
461
|
+
total: paths.length,
|
|
462
|
+
hasMore: false,
|
|
463
|
+
}
|
|
464
|
+
},
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
Note: this requires teaching `FetchClient.search` to accept an
|
|
470
|
+
AbortSignal
|
|
471
|
+
(`packages/workspace/src/data/fetchClient.ts:125-128` — currently
|
|
472
|
+
no signal arg). That's part of Phase 1's scope; ~5 lines.
|
|
473
|
+
|
|
474
|
+
### Recent migration
|
|
475
|
+
|
|
476
|
+
Current shape:
|
|
477
|
+
|
|
478
|
+
```ts
|
|
479
|
+
// localStorage["boring-ui-v2:command-palette:recent"] = ["foo.ts", "cmd:workspace.toggleSidebar", ...]
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
New shape:
|
|
483
|
+
|
|
484
|
+
```ts
|
|
485
|
+
type RecentEntry = {
|
|
486
|
+
kind: string // matches CatalogConfig.recentKind
|
|
487
|
+
id: string // catalog row id (file path, session id, …)
|
|
488
|
+
title: string // for display when the entry's catalog is unloaded
|
|
489
|
+
subtitle?: string
|
|
490
|
+
lastOpenedAt: number
|
|
491
|
+
}
|
|
492
|
+
// localStorage["boring-ui-v2:command-palette:recent:v2"] = RecentEntry[]
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
Migration runs once on palette mount: read the old key, transform each
|
|
496
|
+
entry. `"cmd:foo"` → `{ kind: "command", id: "foo", title: ..., ...}`
|
|
497
|
+
(or simply DROPPED — see open question below). Plain strings →
|
|
498
|
+
`{ kind: "file", id: <path>, title: basename(path), subtitle:
|
|
499
|
+
dirname(path), lastOpenedAt: Date.now() }`. Old key deleted after
|
|
500
|
+
migration.
|
|
501
|
+
|
|
502
|
+
The Recent group renders each entry through the catalog matching
|
|
503
|
+
`entry.kind`. If the catalog isn't registered (e.g. user has a recent
|
|
504
|
+
session but the SessionsCatalog isn't loaded in this app), the entry
|
|
505
|
+
renders read-only (with the saved `title` / `subtitle`) and clicking
|
|
506
|
+
does nothing visible — better than firing the wrong handler.
|
|
507
|
+
|
|
508
|
+
### Palette body shape
|
|
509
|
+
|
|
510
|
+
```tsx
|
|
511
|
+
<CommandList>
|
|
512
|
+
<CommandEmpty>
|
|
513
|
+
{isCommandMode ? "No matching commands" : "No results"}
|
|
514
|
+
</CommandEmpty>
|
|
515
|
+
{!isCommandMode && !searchQuery && recentEntries.length > 0 && (
|
|
516
|
+
<RecentGroup entries={recentEntries} catalogs={catalogs} />
|
|
517
|
+
)}
|
|
518
|
+
{!isCommandMode && catalogs.map((c) => (
|
|
519
|
+
<CatalogGroup key={c.id} catalog={c} query={searchQuery} />
|
|
520
|
+
))}
|
|
521
|
+
{isCommandMode && commandResults.length > 0 && (
|
|
522
|
+
<CommandsGroup commands={commandResults} … />
|
|
523
|
+
)}
|
|
524
|
+
</CommandList>
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
`<CatalogGroup>`:
|
|
528
|
+
|
|
529
|
+
1. Calls `catalog.adapter.search(...)` debounced 300ms via a SHARED
|
|
530
|
+
palette-level debounce (one debounced query → all catalogs
|
|
531
|
+
receive the same stable string). Avoids 10 catalogs × independent
|
|
532
|
+
timers.
|
|
533
|
+
2. Skips the call when `searchQuery === ""` (catalogs have no
|
|
534
|
+
"default" results; Recent fills that role).
|
|
535
|
+
3. Each catalog gets its own AbortController, cancelled on every
|
|
536
|
+
query change.
|
|
537
|
+
4. Wraps each call in a try/catch so a 500 from one catalog renders
|
|
538
|
+
a small inline error chip in that group — the rest of the palette
|
|
539
|
+
keeps working.
|
|
540
|
+
5. Renders `<CommandGroup heading={catalog.label}>` with up to
|
|
541
|
+
`catalog.paletteLimit ?? 5` `<CommandItem>`s.
|
|
542
|
+
6. Wraps `catalog.onSelect(row)` so the palette closes + writes a
|
|
543
|
+
typed Recent entry; catalogs never have to remember either step.
|
|
544
|
+
7. Returns `null` when results are empty so empty groups don't
|
|
545
|
+
render.
|
|
546
|
+
|
|
547
|
+
Async-arrival ordering: groups render in their declared `order`
|
|
548
|
+
priority regardless of which adapter resolves first. Slow Files +
|
|
549
|
+
fast Sessions = Sessions slot reserves space (renders an empty
|
|
550
|
+
group + a "loading" line for the first 250ms of a query) so the
|
|
551
|
+
Files results don't push Sessions down when they arrive. Layout is
|
|
552
|
+
stable from the first render.
|
|
553
|
+
|
|
554
|
+
### `CommandEmpty`
|
|
555
|
+
|
|
556
|
+
Becomes `"No results"` (catalog-agnostic) when in search mode, and
|
|
557
|
+
`"No matching commands"` when in `>` mode. Distinct strings keep the
|
|
558
|
+
empty-state honest about which mode the user is in.
|
|
559
|
+
|
|
560
|
+
### Test plan
|
|
561
|
+
|
|
562
|
+
Phase 1 ships with:
|
|
563
|
+
|
|
564
|
+
- **Unit: `CatalogRegistry`** — register/unregister/list ordering,
|
|
565
|
+
subscribe fires on mutation, useSyncExternalStore re-renders the
|
|
566
|
+
consumer.
|
|
567
|
+
- **Unit: `createFileSearchAdapter`** — empty query short-circuits,
|
|
568
|
+
populated query maps `paths` to `ExplorerRow`s, AbortSignal
|
|
569
|
+
threading.
|
|
570
|
+
- **Unit: typed Recent migration** — old `string[]` shape →
|
|
571
|
+
`RecentEntry[]`; `"cmd:..."` entries handled per the user
|
|
572
|
+
decision below.
|
|
573
|
+
- **Integration: `<CommandPalette />` end-to-end against
|
|
574
|
+
`<WorkspaceProvider catalogs={[stubCatalog]}>` (jsdom)** — open
|
|
575
|
+
palette, type query, assert one stub-catalog group renders top-5
|
|
576
|
+
rows, Enter on a row fires `onSelect` AND the palette closes.
|
|
577
|
+
Open palette again → first row of Recent is the just-selected
|
|
578
|
+
entry, with the right `kind`.
|
|
579
|
+
- **Integration: register-while-open** — open palette, register a
|
|
580
|
+
new catalog, palette re-renders with the new group.
|
|
581
|
+
- **Integration: error per group** — stub catalog rejects;
|
|
582
|
+
palette still renders other groups + an error chip in the failing
|
|
583
|
+
group.
|
|
584
|
+
- **Integration: keyboard nav across async-loaded groups** — ↑/↓
|
|
585
|
+
through groups while one's still loading; aria-selected stays
|
|
586
|
+
correct, scroll-into-view doesn't jump.
|
|
587
|
+
- **Regression: recents type-mix bug** — populate localStorage with
|
|
588
|
+
the legacy `["cmd:foo", "src/a.ts"]` shape, mount palette,
|
|
589
|
+
assert `"cmd:foo"` does NOT trigger the file open path on click.
|
|
590
|
+
- **E2E in workspace-playground** — register a stub second catalog
|
|
591
|
+
in the playground, ⌘K, type, assert two `<CommandGroup>` headings
|
|
592
|
+
appear with disjoint rows. Existing cmd-palette tests
|
|
593
|
+
(Escape/click-outside/effects) keep passing.
|
|
594
|
+
|
|
595
|
+
## Code simplifications enabled by this pattern
|
|
596
|
+
|
|
597
|
+
After the registry lands several adjacent things either get smaller,
|
|
598
|
+
get more declarative, or just stop being broken. The accounting below
|
|
599
|
+
is grounded in actual current code (codex caught v1 inventing
|
|
600
|
+
`useFileSearch` in the palette — there's no such thing today).
|
|
601
|
+
|
|
602
|
+
1. **`<CommandPalette />` body collapses to one loop + one commands
|
|
603
|
+
group.** Today
|
|
604
|
+
(`packages/workspace/src/components/CommandPalette.tsx:200-232`)
|
|
605
|
+
has three hardcoded result blocks (`recentFiles`, `fileResults`,
|
|
606
|
+
`commandResults`), each with its own group rendering. After: one
|
|
607
|
+
`catalogs.map(c => <CatalogGroup catalog={c} query={searchQuery}
|
|
608
|
+
/>)` plus a single commands group when in `>` mode.
|
|
609
|
+
|
|
610
|
+
2. **Palette props go from 2 to 0.** Today's
|
|
611
|
+
`CommandPaletteProps = { fileSearchFn, onOpenFile }`
|
|
612
|
+
(`packages/workspace/src/components/CommandPalette.tsx:36-39`)
|
|
613
|
+
exists only because the host had to thread the file-search
|
|
614
|
+
callback AND the file-open callback in, but
|
|
615
|
+
`WorkspaceProvider` mounts the palette with no props
|
|
616
|
+
(`packages/workspace/src/WorkspaceProvider.tsx:377`) — the props
|
|
617
|
+
are EFFECTIVELY DEAD in the default runtime. With catalogs they
|
|
618
|
+
move into the FilesCatalog config. Palette becomes prop-less.
|
|
619
|
+
|
|
620
|
+
3. **`ChatCenteredShell`'s imperative `useEffect` block becomes
|
|
621
|
+
declarative.** Today
|
|
622
|
+
(`packages/workspace/src/components/chat/ChatCenteredShell.tsx:400-431`)
|
|
623
|
+
re-registers commands on every render. The 3 toggle/new-chat
|
|
624
|
+
actions stay as commands but become declarative — the shell
|
|
625
|
+
accepts a `commands?: CommandConfig[]` shape mirroring the
|
|
626
|
+
`catalogs?:` prop on the provider. The "Switch to: <session
|
|
627
|
+
title>" loop becomes a `SessionsCatalog` (in-memory adapter
|
|
628
|
+
filtering `props.sessions`) — Phase 1 includes this migration.
|
|
629
|
+
|
|
630
|
+
4. **`withCommandPalette` no-op shell prop drops.** Today
|
|
631
|
+
(`packages/workspace/src/components/chat/ChatCenteredShell.tsx:77,
|
|
632
|
+
197-205, 591-603`) the prop exists but is a runtime no-op (one
|
|
633
|
+
palette is mounted by the provider, not the shell). Phase 1 adds
|
|
634
|
+
`withCommandPalette` to `WorkspaceProvider` (where it actually
|
|
635
|
+
has somewhere to control), then drops the shell prop.
|
|
636
|
+
|
|
637
|
+
5. **Recent type-mix bug gets fixed.** Today
|
|
638
|
+
(`packages/workspace/src/components/CommandPalette.tsx:128-134`)
|
|
639
|
+
stores `"cmd:" + id` for command recents but
|
|
640
|
+
(`packages/workspace/src/components/CommandPalette.tsx:230-236`)
|
|
641
|
+
renders every recent through `handleFileSelect`, calling
|
|
642
|
+
`onOpenFile?.("cmd:foo")`. The typed-Recent migration in this
|
|
643
|
+
PR removes the bug as a side-effect: typed entries route per
|
|
644
|
+
`kind`.
|
|
645
|
+
|
|
646
|
+
6. **Test infra converges.** Catalog adapters become the unit of
|
|
647
|
+
test for both surfaces. A SessionsCatalog tested in isolation
|
|
648
|
+
against a mock adapter automatically tests the data path for
|
|
649
|
+
both the data-pane explorer view and the cmd-palette inline view.
|
|
650
|
+
|
|
651
|
+
## Dead code that comes out
|
|
652
|
+
|
|
653
|
+
Not aspirational cleanup; each is gated on Phase 1 landing.
|
|
654
|
+
|
|
655
|
+
| File | Net | What |
|
|
656
|
+
|---|---|---|
|
|
657
|
+
| `packages/workspace/src/components/CommandPalette.tsx` | ~–80 | The dual `fileSearchFn` / `onOpenFile` props (lines 36-39, 64-65), the `fileResults` `useMemo` (140-147), `handleFileSelect` (158-168), the standalone Files `<CommandGroup>` (211-220). Consolidated into the catalog loop. |
|
|
658
|
+
| `packages/workspace/src/components/CommandPalette.tsx` (`FilePathLabel`) | –12 | `FilePathLabel` (239-251) is a perfect candidate for deletion. Its filename / dir split becomes `title` / `subtitle` on FilesCatalog rows; the row renderer uses `<DataExplorer />`-style "title bold + subtitle muted" baked in. |
|
|
659
|
+
| `packages/workspace/src/components/CommandPalette.tsx` (`CommandPaletteProps`) | –4 + breaking | Props type goes to `{}`; export removed. **Breaking change for any external consumer that imports `CommandPaletteProps`.** |
|
|
660
|
+
| `packages/workspace/src/components/chat/ChatCenteredShell.tsx` (imperative useEffect) | ~–30 | Lines 400-431. Replaced with one declarative `commands={[…]}` + `catalogs={[…]}` prop on the shell (or threaded via the `WorkspaceProvider` it sits under). |
|
|
661
|
+
| `packages/workspace/src/components/chat/ChatCenteredShell.tsx` (`withCommandPalette`) | –6 + breaking | Lines 77, 197-205, 591-603 (the no-op JSX comment block). **Breaking change for any external consumer that passes `withCommandPalette={false}` on the shell.** |
|
|
662
|
+
| `packages/workspace/src/components/CommandPalette.tsx` (loadRecent/saveRecent + RECENT_STORAGE_KEY constants 27-50) | rewritten | Stays in the file as the typed-Recent v2 implementation, but the v1 `string[]` shape is retired with a one-shot migration. |
|
|
663
|
+
|
|
664
|
+
**Files only changed (not deleted):**
|
|
665
|
+
|
|
666
|
+
- `packages/workspace/src/WorkspaceProvider.tsx` — gains
|
|
667
|
+
`catalogs?: CatalogConfig[]`, `withCommandPalette?: boolean`,
|
|
668
|
+
`withDefaultCatalogs?: boolean`, `onOpenFile?: (path: string) =>
|
|
669
|
+
void`. ~+15 lines.
|
|
670
|
+
- `packages/workspace/src/registry/RegistryProvider.tsx` — adds
|
|
671
|
+
`catalogRegistry` to the context. ~+8 lines.
|
|
672
|
+
- `packages/workspace/src/registry/CatalogRegistry.ts` (new, ~40
|
|
673
|
+
lines) — sibling of `CommandRegistry`, with subscribe semantics.
|
|
674
|
+
`CommandRegistry` retrofitted with the same subscribe pattern
|
|
675
|
+
(~+20 lines).
|
|
676
|
+
- `packages/workspace/src/registry/index.ts` — re-exports
|
|
677
|
+
`useCatalogs`, `useCatalogRegistry`, `CatalogRegistry`,
|
|
678
|
+
`CatalogConfig`.
|
|
679
|
+
- `packages/workspace/src/data/fetchClient.ts` — `search` accepts
|
|
680
|
+
`{ signal?: AbortSignal }`. ~+5 lines.
|
|
681
|
+
- `packages/workspace/src/components/CommandPalette.tsx` — net
|
|
682
|
+
~–80 lines after removals + ~+50 for `<CatalogGroup>` (parallel
|
|
683
|
+
fetch + AbortSignal + render) + `<RecentGroup>` (typed routing).
|
|
684
|
+
|
|
685
|
+
**Net package size:** ~–60 lines of net source (well, minus
|
|
686
|
+
roughly +30 for new types and tests), simpler public surface, no
|
|
687
|
+
silently-broken Files path, no recent-type-mix bug. Two breaking
|
|
688
|
+
changes, both flagged.
|
|
689
|
+
|
|
690
|
+
## Migration / rollout
|
|
691
|
+
|
|
692
|
+
**Breaking changes shipped in Phase 1** (call them out in the
|
|
693
|
+
release notes; bump the workspace package's minor):
|
|
694
|
+
|
|
695
|
+
1. `CommandPaletteProps` export removed
|
|
696
|
+
(`packages/workspace/src/index.ts:88`). External code that imports
|
|
697
|
+
the type must remove the import. The component is now prop-less.
|
|
698
|
+
2. `ChatCenteredShellProps.withCommandPalette` removed
|
|
699
|
+
(`packages/workspace/src/components/chat/ChatCenteredShell.tsx:77`).
|
|
700
|
+
External code that disables the shell-level palette must move the
|
|
701
|
+
flag onto `WorkspaceProvider` instead:
|
|
702
|
+
`<WorkspaceProvider withCommandPalette={false}>`.
|
|
703
|
+
3. `localStorage["boring-ui-v2:command-palette:recent"]` keyspace
|
|
704
|
+
migrates to `:recent:v2` with typed entries. Migration runs once
|
|
705
|
+
on palette mount; old key deleted. Pre-migration recents from
|
|
706
|
+
external builds will be lost if they don't follow the legacy
|
|
707
|
+
shape — acceptable.
|
|
708
|
+
|
|
709
|
+
**Phase 1 (this PR) — the work, in order:**
|
|
710
|
+
|
|
711
|
+
1. Add `CatalogRegistry` + `useCatalogs` + `useCatalogRegistry`
|
|
712
|
+
(subscribe-aware).
|
|
713
|
+
2. Retrofit `CommandRegistry` with `subscribe` so late
|
|
714
|
+
`registerCommand` calls trigger palette re-render.
|
|
715
|
+
3. Add `withCommandPalette?` + `catalogs?` + `withDefaultCatalogs?` +
|
|
716
|
+
`onOpenFile?` to `WorkspaceProvider`.
|
|
717
|
+
4. Teach `FetchClient.search` to accept an AbortSignal.
|
|
718
|
+
5. Add `createFileSearchAdapter(client)`.
|
|
719
|
+
6. Refactor `<CommandPalette />` to consume catalogs + render
|
|
720
|
+
`<CatalogGroup>` / `<RecentGroup>`. Drop `fileSearchFn` /
|
|
721
|
+
`onOpenFile` props + `FilePathLabel` + the Files `<CommandGroup>`
|
|
722
|
+
block.
|
|
723
|
+
7. Run typed-Recent migration on palette mount.
|
|
724
|
+
8. Migrate `ChatCenteredShell`'s imperative `useEffect` to
|
|
725
|
+
declarative `commands={[…]}` + `catalogs={[sessionsCatalog]}` on
|
|
726
|
+
the shell (or threaded through the provider). Drop
|
|
727
|
+
`withCommandPalette` prop.
|
|
728
|
+
9. Tests above.
|
|
729
|
+
|
|
730
|
+
**Phase 2 (separate PR per catalog):**
|
|
731
|
+
|
|
732
|
+
- `SessionsCatalog` for hosts with persistent sessions (full-app /
|
|
733
|
+
similar). Backend: an `/api/v1/sessions/search` route — frontend
|
|
734
|
+
package plan stays silent on the indexing strategy; backend's
|
|
735
|
+
problem.
|
|
736
|
+
- `WorkspacesCatalog` (small N, in-memory filter is fine).
|
|
737
|
+
- Whichever catalog the consuming app needs next.
|
|
738
|
+
|
|
739
|
+
## Open questions
|
|
740
|
+
|
|
741
|
+
1. **What should the typed-Recent migration do with legacy
|
|
742
|
+
`"cmd:foo"` entries?**
|
|
743
|
+
Options: (a) DROP them — fewer surprises, no broken handlers; (b)
|
|
744
|
+
convert to `{ kind: "command", id: "foo", title: ?, lastOpenedAt:
|
|
745
|
+
<now> }` — preserves history but the title is missing in
|
|
746
|
+
localStorage so we'd need a lookup at mount time. **Recommend
|
|
747
|
+
(a):** drop them. Recents are ephemeral; the bug they expose
|
|
748
|
+
(commands re-fired through file-open path) is more important than
|
|
749
|
+
preserving stale entries.
|
|
750
|
+
|
|
751
|
+
2. **Should hosts that don't pass `onOpenFile` get NO FilesCatalog,
|
|
752
|
+
or get a FilesCatalog whose `onSelect` is a no-op (search but
|
|
753
|
+
can't open)?**
|
|
754
|
+
Search-without-open feels like a footgun (user clicks → nothing
|
|
755
|
+
happens). **Recommend: no FilesCatalog by default unless
|
|
756
|
+
`onOpenFile` is supplied.** Hosts with custom file-open logic
|
|
757
|
+
register their own. Documented + defaulted.
|
|
758
|
+
|
|
759
|
+
3. **Should `CatalogConfig.onSelect` be allowed to be async?**
|
|
760
|
+
Codex flagged. The palette wants to close + record Recent
|
|
761
|
+
immediately, before the host's side-effect resolves. **Recommend:
|
|
762
|
+
`onSelect` returns `void` synchronously; if a host needs async
|
|
763
|
+
work, fire-and-forget inside the callback.** Palette doesn't
|
|
764
|
+
block on it.
|
|
765
|
+
|
|
766
|
+
4. **Should the catalog set be scoped per provider or per shell?**
|
|
767
|
+
Per provider. Consistent with `commandRegistry` today. Multiple
|
|
768
|
+
shells under one provider share. If we ever need per-shell
|
|
769
|
+
scoping it can be a sibling provider — out of scope here.
|
|
770
|
+
|
|
771
|
+
5. **Should we expose the imperative registry mutation pattern for
|
|
772
|
+
non-React-tree consumers (e.g. server-side code)?**
|
|
773
|
+
Defer. The chat shell scenario v1 worried about turns out to be
|
|
774
|
+
pure-React (registering catalogs in response to props). Premature
|
|
775
|
+
to design a non-React API. Revisit if a real consumer shows up.
|
|
776
|
+
|
|
777
|
+
## Acceptance
|
|
778
|
+
|
|
779
|
+
- `WorkspaceProvider` accepts `catalogs?: CatalogConfig[]` and
|
|
780
|
+
exposes them via `useCatalogs()` (subscribe-aware).
|
|
781
|
+
- `<CommandPalette />` renders one group per registered catalog
|
|
782
|
+
with matching results (top 5 default, debounced palette-wide,
|
|
783
|
+
AbortSignal-aware, error-isolated per group).
|
|
784
|
+
- Files keep working when a host supplies `onOpenFile` — and works
|
|
785
|
+
for the FIRST TIME in the default runtime (today the Files path is
|
|
786
|
+
dead).
|
|
787
|
+
- Adding a second catalog is a 30-line addition: write the
|
|
788
|
+
`ExplorerAdapter`, write the `CatalogConfig`, pass it through
|
|
789
|
+
`<WorkspaceProvider catalogs={[…]}>`. No `<CommandPalette />`
|
|
790
|
+
changes needed.
|
|
791
|
+
- Recent entries are typed; selecting a "recent command" no longer
|
|
792
|
+
fires the file-open path.
|
|
793
|
+
- `ChatCenteredShell` no longer registers commands imperatively in
|
|
794
|
+
`useEffect`; the registration is declarative.
|
|
795
|
+
- Two flagged breaking changes (CommandPaletteProps export,
|
|
796
|
+
withCommandPalette shell prop) are documented in the release
|
|
797
|
+
notes.
|
|
798
|
+
- Tests in §Test plan all pass.
|
|
799
|
+
|
|
800
|
+
## Reference
|
|
801
|
+
|
|
802
|
+
- Existing palette:
|
|
803
|
+
`packages/workspace/src/components/CommandPalette.tsx`
|
|
804
|
+
- Existing adapter shape:
|
|
805
|
+
`packages/workspace/src/components/DataExplorer/types.ts`
|
|
806
|
+
- Existing command registry:
|
|
807
|
+
`packages/workspace/src/registry/CommandRegistry.ts`
|
|
808
|
+
- File search HTTP route (one shared backend with the LLM tool, just
|
|
809
|
+
landed at commit `12098fd`):
|
|
810
|
+
`packages/agent/src/server/http/routes/search.ts`
|
|
811
|
+
- Workspace provider's current palette mount point:
|
|
812
|
+
`packages/workspace/src/WorkspaceProvider.tsx:377`
|
|
813
|
+
- Chat shell's imperative command useEffect to remove:
|
|
814
|
+
`packages/workspace/src/components/chat/ChatCenteredShell.tsx:400-431`
|