@workbench-kit/react 0.0.2-prototype.0.2.14 → 0.0.2-prototype.0.2.16

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/README.md CHANGED
@@ -87,7 +87,16 @@ const providers: QuickOpenProvider[] = [
87
87
 
88
88
  Contract:
89
89
 
90
- - `provider.search(query)` returns items (sync or `Promise`)
90
+ - `provider.search(query, { signal })` returns items (sync or `Promise`); the context is
91
+ optional so simple and existing single-provider implementations can keep a one-argument function
92
+ - Without `providerId`, providers search concurrently and contribute results as they settle;
93
+ final grouping is deterministic by declared provider order, then provider item order
94
+ - A rejected or slow provider does not block results from other providers; query/provider changes
95
+ abort the prior signal and late results are ignored
96
+ - `onProviderError` can observe an isolated provider failure without failing the whole search
97
+ - Every result shows its provider label, and selection reports the matching `providerId`
98
+ - Provider IDs are unique; item IDs only need to be stable within their provider
99
+ - Set `providerId` to restrict the overlay to one provider
91
100
  - Empty query → all files, with optional `recentPaths` elevated first
92
101
  - Enter selects the active item; Escape closes (modal focus trap)
93
102
  - Shell default: Ctrl/Cmd+P opens Quick Open; Ctrl/Cmd+Shift+P opens the command palette
package/package.json CHANGED
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "name": "@workbench-kit/react",
3
- "version": "0.0.2-prototype.0.2.14",
3
+ "version": "0.0.2-prototype.0.2.16",
4
4
  "private": false,
5
+ "sideEffects": [
6
+ "**/*.css"
7
+ ],
5
8
  "type": "module",
6
9
  "exports": {
7
10
  ".": "./src/index.ts",
@@ -21,8 +24,11 @@
21
24
  "./workbench/settings": "./src/workbench/settings/index.ts",
22
25
  "./workbench": "./src/workbench/index.ts",
23
26
  "./workbench/activityBarOrder": "./src/workbench/shell/activityBarOrder.ts",
27
+ "./workbench/command-ui": "./src/workbench/commands/command-ui.ts",
24
28
  "./workbench/markdown-preview": "./src/workbench/markdown/MarkdownPreview.tsx",
25
29
  "./workbench/shell": "./src/workbench/shell/shell.ts",
30
+ "./workbench/standalone": "./src/workbench/shell/standalone.ts",
31
+ "./workbench/theme": "./src/workbench/theme/theme.ts",
26
32
  "./workbench/theme-provider": "./src/workbench/theme/WorkbenchThemeProvider.tsx",
27
33
  "./workbench/themePresets": "./src/workbench/theme/themePresets.ts",
28
34
  "./workbench/split-view": "./src/workbench/shell/SplitView.tsx",
@@ -54,19 +60,18 @@
54
60
  "!src/**/*.stories.tsx"
55
61
  ],
56
62
  "dependencies": {
57
- "@dnd-kit/core": "^6.3.1",
58
63
  "@vscode/codicons": "^0.0.45",
59
64
  "remark-gfm": "^4.0.1",
60
65
  "rehype-sanitize": "^6.0.0",
61
- "@workbench-kit/contracts": "0.0.2-prototype.0.2.14",
62
- "@workbench-kit/jdw": "0.0.2-prototype.0.2.14",
63
- "@workbench-kit/adapters": "0.0.2-prototype.0.2.14",
64
- "@workbench-kit/monaco": "0.0.2-prototype.0.2.14",
65
- "@workbench-kit/runtime": "0.0.2-prototype.0.2.14",
66
- "@workbench-kit/services": "0.0.2-prototype.0.2.14",
67
- "@workbench-kit/workspace": "0.0.2-prototype.0.2.14",
68
- "@workbench-kit/platform": "0.0.2-prototype.0.2.14",
69
- "@workbench-kit/tokens": "0.0.2-prototype.0.2.14"
66
+ "@workbench-kit/adapters": "0.0.2-prototype.0.2.16",
67
+ "@workbench-kit/contracts": "0.0.2-prototype.0.2.16",
68
+ "@workbench-kit/monaco": "0.0.2-prototype.0.2.16",
69
+ "@workbench-kit/platform": "0.0.2-prototype.0.2.16",
70
+ "@workbench-kit/jdw": "0.0.2-prototype.0.2.16",
71
+ "@workbench-kit/runtime": "0.0.2-prototype.0.2.16",
72
+ "@workbench-kit/tokens": "0.0.2-prototype.0.2.16",
73
+ "@workbench-kit/workspace": "0.0.2-prototype.0.2.16",
74
+ "@workbench-kit/services": "0.0.2-prototype.0.2.16"
70
75
  },
71
76
  "peerDependencies": {
72
77
  "react": "^19.0.0",
@@ -89,7 +94,7 @@
89
94
  "directory": "packages/react"
90
95
  },
91
96
  "scripts": {
92
- "test": "pnpm exec vitest run --config ../../vitest.config.ts src/widget-tree",
97
+ "test": "pnpm exec vitest run --config ../../vitest.config.ts src",
93
98
  "typecheck": "tsc -p tsconfig.json --noEmit",
94
99
  "typecheck:exact-optional": "tsc -p typecheck-exact-optional/tsconfig.emit.json && tsc -p typecheck-exact-optional/tsconfig.json --noEmit"
95
100
  }
package/src/index.ts CHANGED
@@ -100,6 +100,7 @@ export type {
100
100
  CreateWorkspaceFilesQuickOpenProviderOptions,
101
101
  QuickOpenItem,
102
102
  QuickOpenProvider,
103
+ QuickOpenSearchContext,
103
104
  QuickOpenSelectContext,
104
105
  WorkbenchQuickOpenProps,
105
106
  } from './workbench/commands/WorkbenchQuickOpen';
@@ -151,6 +151,8 @@ export type {
151
151
  } from './propertyFieldFilter';
152
152
  export { WorkbenchMediaPreviewViewport } from './WorkbenchMediaPreviewViewport';
153
153
  export type { WorkbenchMediaPreviewViewportProps } from './WorkbenchMediaPreviewViewport';
154
+ export { WorkbenchPreviewCanvas } from './WorkbenchCanvas';
155
+ export type { WorkbenchPreviewCanvasProps } from './WorkbenchCanvas';
154
156
  export {
155
157
  clampPreviewViewportZoom,
156
158
  computePreviewViewportFitScale,
@@ -30,6 +30,12 @@ interface WorkbenchQuickOpenKeyEvent {
30
30
  preventDefault: () => void;
31
31
  }
32
32
 
33
+ interface QuickOpenResult {
34
+ item: QuickOpenItem;
35
+ key: string;
36
+ provider: QuickOpenProvider;
37
+ }
38
+
33
39
  function useControllableQuery({
34
40
  defaultQuery = '',
35
41
  query,
@@ -63,11 +69,12 @@ export interface WorkbenchQuickOpenProps extends Omit<
63
69
  emptyLabel?: ReactNode | undefined;
64
70
  onActiveItemChange?: ((itemId: string) => void) | undefined;
65
71
  onClose: () => void;
72
+ onProviderError?: ((error: unknown, provider: QuickOpenProvider) => void) | undefined;
66
73
  onQueryChange?: ((query: string) => void) | undefined;
67
74
  onSelectItem?: ((item: QuickOpenItem, context: QuickOpenSelectContext) => void) | undefined;
68
75
  open?: boolean | undefined;
69
76
  placeholder?: string | undefined;
70
- /** When omitted, the first provider is used. */
77
+ /** Restrict search to one provider. When omitted, all providers contribute. */
71
78
  providerId?: string | undefined;
72
79
  providers: readonly QuickOpenProvider[];
73
80
  query?: string | undefined;
@@ -84,6 +91,7 @@ export function WorkbenchQuickOpen({
84
91
  emptyLabel = 'No matching files',
85
92
  onActiveItemChange,
86
93
  onClose,
94
+ onProviderError,
87
95
  onQueryChange,
88
96
  onSelectItem,
89
97
  open = true,
@@ -99,33 +107,49 @@ export function WorkbenchQuickOpen({
99
107
  const listId = useId();
100
108
  const dialogRef = useRef<HTMLDivElement>(null);
101
109
  const inputRef = useRef<HTMLInputElement>(null);
102
- const [uncontrolledActiveItemId, setUncontrolledActiveItemId] = useState<string>();
103
- const [items, setItems] = useState<QuickOpenItem[]>([]);
110
+ const [uncontrolledActiveResultKey, setUncontrolledActiveResultKey] = useState<string>();
111
+ const [results, setResults] = useState<QuickOpenResult[]>([]);
104
112
  const [searching, setSearching] = useState(false);
113
+ const onProviderErrorRef = useRef(onProviderError);
105
114
  const [resolvedQuery, setResolvedQuery] = useControllableQuery({
106
115
  defaultQuery,
107
116
  onQueryChange,
108
117
  query,
109
118
  });
110
119
 
111
- const activeProvider = useMemo(() => {
120
+ useEffect(() => {
121
+ onProviderErrorRef.current = onProviderError;
122
+ }, [onProviderError]);
123
+
124
+ const activeProviders = useMemo(() => {
112
125
  if (providerId) {
113
- return providers.find((provider) => provider.id === providerId) ?? providers[0];
126
+ const provider = providers.find((candidate) => candidate.id === providerId);
127
+ return provider ? [provider] : [];
114
128
  }
115
- return providers[0];
129
+ return providers;
116
130
  }, [providerId, providers]);
117
131
 
118
- const resolvedActiveItemId = activeItemId ?? uncontrolledActiveItemId ?? items[0]?.id;
119
- const activeIndex = items.findIndex((item) => item.id === resolvedActiveItemId);
120
- const activeItem = activeIndex >= 0 ? items[activeIndex] : items.find(isQuickOpenItemSelectable);
132
+ const items = useMemo(() => results.map(({ item }) => item), [results]);
133
+ const controlledActiveResult = activeItemId
134
+ ? results.find(({ item }) => item.id === activeItemId && isQuickOpenItemSelectable(item))
135
+ : undefined;
136
+ const uncontrolledActiveResult = results.find(
137
+ ({ item, key }) => key === uncontrolledActiveResultKey && isQuickOpenItemSelectable(item),
138
+ );
139
+ const activeResult =
140
+ controlledActiveResult ??
141
+ uncontrolledActiveResult ??
142
+ results.find(({ item }) => isQuickOpenItemSelectable(item));
143
+ const resolvedActiveResultKey = activeResult?.key;
144
+ const activeIndex = activeResult ? results.indexOf(activeResult) : -1;
121
145
 
122
146
  const updateActiveItem = useCallback(
123
- (itemId: string | undefined) => {
124
- if (!itemId) return;
147
+ (result: QuickOpenResult | undefined) => {
148
+ if (!result) return;
125
149
  if (activeItemId === undefined) {
126
- setUncontrolledActiveItemId(itemId);
150
+ setUncontrolledActiveResultKey(result.key);
127
151
  }
128
- onActiveItemChange?.(itemId);
152
+ onActiveItemChange?.(result.item.id);
129
153
  },
130
154
  [activeItemId, onActiveItemChange],
131
155
  );
@@ -139,25 +163,44 @@ export function WorkbenchQuickOpen({
139
163
  });
140
164
 
141
165
  useEffect(() => {
142
- if (!open || !activeProvider) {
143
- setItems([]);
166
+ if (!open || activeProviders.length === 0) {
167
+ setResults([]);
144
168
  setSearching(false);
145
169
  return undefined;
146
170
  }
147
171
 
172
+ const controller = new AbortController();
148
173
  let cancelled = false;
149
174
  const runSearch = async () => {
175
+ setResults([]);
150
176
  setSearching(true);
151
- try {
152
- const nextItems = await Promise.resolve(activeProvider.search(resolvedQuery));
153
- if (!cancelled) {
154
- setItems(nextItems);
155
- }
156
- } finally {
157
- if (!cancelled) {
158
- setSearching(false);
177
+ const providerResults = activeProviders.map<QuickOpenResult[] | undefined>(() => undefined);
178
+ const publishAvailableResults = () => {
179
+ setResults(providerResults.flatMap((providerItems) => providerItems ?? []));
180
+ };
181
+ const searches = activeProviders.map(async (provider, providerIndex) => {
182
+ try {
183
+ const providerItems = await Promise.resolve(
184
+ provider.search(resolvedQuery, { signal: controller.signal }),
185
+ );
186
+ if (cancelled) return;
187
+
188
+ providerResults[providerIndex] = providerItems.map((item, itemIndex) => ({
189
+ item,
190
+ key: `${providerIndex}:${itemIndex}:${provider.id}:${item.id}`,
191
+ provider,
192
+ }));
193
+ publishAvailableResults();
194
+ } catch (error) {
195
+ if (cancelled) return;
196
+ providerResults[providerIndex] = [];
197
+ publishAvailableResults();
198
+ onProviderErrorRef.current?.(error, provider);
159
199
  }
160
- }
200
+ });
201
+
202
+ await Promise.allSettled(searches);
203
+ if (!cancelled) setSearching(false);
161
204
  };
162
205
 
163
206
  // Empty query (recent / top-level) should paint immediately; debounce typed queries.
@@ -166,6 +209,7 @@ export function WorkbenchQuickOpen({
166
209
  void runSearch();
167
210
  return () => {
168
211
  cancelled = true;
212
+ controller.abort();
169
213
  };
170
214
  }
171
215
 
@@ -175,30 +219,35 @@ export function WorkbenchQuickOpen({
175
219
 
176
220
  return () => {
177
221
  cancelled = true;
222
+ controller.abort();
178
223
  window.clearTimeout(timer);
179
224
  };
180
- }, [activeProvider, debounceMs, open, resolvedQuery]);
225
+ }, [activeProviders, debounceMs, open, resolvedQuery]);
181
226
 
182
227
  useEffect(() => {
183
228
  if (!open) return;
184
229
  if (activeItemId !== undefined) return;
185
230
 
186
- setUncontrolledActiveItemId((currentItemId) => {
187
- const nextItemId = items.find(isQuickOpenItemSelectable)?.id;
188
- return currentItemId === nextItemId ? currentItemId : nextItemId;
231
+ setUncontrolledActiveResultKey((currentResultKey) => {
232
+ const currentResult = results.find(({ key }) => key === currentResultKey);
233
+ if (currentResult && isQuickOpenItemSelectable(currentResult.item)) {
234
+ return currentResultKey;
235
+ }
236
+
237
+ return results.find(({ item }) => isQuickOpenItemSelectable(item))?.key;
189
238
  });
190
- }, [activeItemId, items, open]);
239
+ }, [activeItemId, open, results]);
191
240
 
192
241
  const selectItem = useCallback(
193
- (item: QuickOpenItem, index: number) => {
194
- if (!isQuickOpenItemSelectable(item) || !activeProvider) return;
195
- onSelectItem?.(item, {
242
+ (result: QuickOpenResult, index: number) => {
243
+ if (!isQuickOpenItemSelectable(result.item)) return;
244
+ onSelectItem?.(result.item, {
196
245
  index,
197
- providerId: activeProvider.id,
246
+ providerId: result.provider.id,
198
247
  query: resolvedQuery,
199
248
  });
200
249
  },
201
- [activeProvider, onSelectItem, resolvedQuery],
250
+ [onSelectItem, resolvedQuery],
202
251
  );
203
252
 
204
253
  const handleQuickOpenKeyDown = useCallback(
@@ -216,9 +265,9 @@ export function WorkbenchQuickOpen({
216
265
  }
217
266
 
218
267
  if (event.key === 'Enter') {
219
- if (!activeItem) return;
268
+ if (!activeResult) return;
220
269
  event.preventDefault();
221
- selectItem(activeItem, Math.max(activeIndex, 0));
270
+ selectItem(activeResult, Math.max(activeIndex, 0));
222
271
  return;
223
272
  }
224
273
 
@@ -258,10 +307,10 @@ export function WorkbenchQuickOpen({
258
307
  }
259
308
 
260
309
  if (nextIndex >= 0) {
261
- updateActiveItem(items[nextIndex]?.id);
310
+ updateActiveItem(results[nextIndex]);
262
311
  }
263
312
  },
264
- [activeIndex, activeItem, items, selectItem, updateActiveItem],
313
+ [activeIndex, activeResult, items, results, selectItem, updateActiveItem],
265
314
  );
266
315
 
267
316
  useEffect(() => {
@@ -283,14 +332,14 @@ export function WorkbenchQuickOpen({
283
332
  };
284
333
 
285
334
  useEffect(() => {
286
- if (!resolvedActiveItemId) return;
335
+ if (!resolvedActiveResultKey) return;
287
336
 
288
337
  const activeElement = Array.from(
289
338
  dialogRef.current?.querySelectorAll<HTMLElement>('[data-quick-open-id]') ?? [],
290
- ).find((element) => element.dataset.quickOpenId === resolvedActiveItemId);
339
+ ).find((element) => element.dataset.quickOpenId === resolvedActiveResultKey);
291
340
 
292
341
  activeElement?.scrollIntoView?.({ block: 'nearest' });
293
- }, [items, resolvedActiveItemId]);
342
+ }, [resolvedActiveResultKey, results]);
294
343
 
295
344
  if (!open) return null;
296
345
 
@@ -322,7 +371,7 @@ export function WorkbenchQuickOpen({
322
371
  <TextInput
323
372
  ref={inputRef}
324
373
  aria-activedescendant={
325
- activeItem ? `${listId}-${activeItem.id.replace(/[^\w-]/g, '_')}` : undefined
374
+ activeResult ? `${listId}-${activeResult.key.replace(/[^\w-]/g, '_')}` : undefined
326
375
  }
327
376
  aria-controls={listId}
328
377
  aria-label={placeholder}
@@ -342,29 +391,30 @@ export function WorkbenchQuickOpen({
342
391
  className={cx('ui-workbench-command-list', 'ui-workbench-scrollbar')}
343
392
  role="listbox"
344
393
  >
345
- {items.length === 0 ? (
394
+ {results.length === 0 ? (
346
395
  <EmptyState compact icon="codicon-search">
347
396
  {emptyLabel}
348
397
  </EmptyState>
349
398
  ) : (
350
- items.map((item, index) => {
351
- const active = item.id === resolvedActiveItemId;
352
- const itemDomId = `${listId}-${item.id.replace(/[^\w-]/g, '_')}`;
399
+ results.map((result, index) => {
400
+ const { item, key, provider } = result;
401
+ const active = key === resolvedActiveResultKey;
402
+ const itemDomId = `${listId}-${key.replace(/[^\w-]/g, '_')}`;
353
403
  const descriptionId = item.description ? `${itemDomId}-description` : undefined;
354
404
 
355
405
  return (
356
406
  <Button
357
- key={item.id}
407
+ key={key}
358
408
  id={itemDomId}
359
409
  aria-describedby={descriptionId}
360
410
  aria-selected={active}
361
411
  className="ui-workbench-command-item"
362
412
  data-active={active ? 'true' : undefined}
363
- data-quick-open-id={item.id}
413
+ data-quick-open-id={key}
364
414
  disabled={item.disabled}
365
415
  role="option"
366
- onClick={() => selectItem(item, index)}
367
- onMouseEnter={() => updateActiveItem(item.id)}
416
+ onClick={() => selectItem(result, index)}
417
+ onMouseEnter={() => updateActiveItem(result)}
368
418
  >
369
419
  <span className="ui-workbench-command-item__icon">
370
420
  {item.icon ? <i aria-hidden="true" className={cxCodicon(item.icon)} /> : null}
@@ -380,11 +430,8 @@ export function WorkbenchQuickOpen({
380
430
  <span className="ui-workbench-command-item__meta">
381
431
  {item.detail ? (
382
432
  <span className="ui-workbench-command-item__category">{item.detail}</span>
383
- ) : activeProvider ? (
384
- <span className="ui-workbench-command-item__category">
385
- {activeProvider.label}
386
- </span>
387
433
  ) : null}
434
+ <span className="ui-workbench-command-item__category">{provider.label}</span>
388
435
  </span>
389
436
  </Button>
390
437
  );
@@ -396,7 +443,12 @@ export function WorkbenchQuickOpen({
396
443
  );
397
444
  }
398
445
 
399
- export type { QuickOpenItem, QuickOpenProvider, QuickOpenSelectContext } from './quick-open-model';
446
+ export type {
447
+ QuickOpenItem,
448
+ QuickOpenProvider,
449
+ QuickOpenSearchContext,
450
+ QuickOpenSelectContext,
451
+ } from './quick-open-model';
400
452
  export {
401
453
  DEFAULT_QUICK_OPEN_SEARCH_DEBOUNCE_MS,
402
454
  getNextQuickOpenItemIndex,
@@ -0,0 +1,3 @@
1
+ export * from './CommandPalette';
2
+ export * from './ShortcutCommandBridge';
3
+ export * from './WorkbenchQuickOpen';
@@ -1,5 +1,5 @@
1
1
  export interface QuickOpenItem {
2
- /** Stable item id (often a workspace path). */
2
+ /** Stable within its provider (often a workspace path). */
3
3
  id: string;
4
4
  label: string;
5
5
  description?: string | undefined;
@@ -10,10 +10,19 @@ export interface QuickOpenItem {
10
10
  data?: unknown;
11
11
  }
12
12
 
13
+ export interface QuickOpenSearchContext {
14
+ /** Aborted when the query, provider set, or open state changes. */
15
+ signal: AbortSignal;
16
+ }
17
+
13
18
  export interface QuickOpenProvider {
19
+ /** Unique within the provider set passed to Quick Open. */
14
20
  id: string;
15
21
  label: string;
16
- search: (query: string) => Promise<QuickOpenItem[]> | QuickOpenItem[];
22
+ search: (
23
+ query: string,
24
+ context?: QuickOpenSearchContext,
25
+ ) => Promise<QuickOpenItem[]> | QuickOpenItem[];
17
26
  }
18
27
 
19
28
  export interface QuickOpenSelectContext {
@@ -228,6 +228,7 @@ export type {
228
228
  CreateWorkspaceFilesQuickOpenProviderOptions,
229
229
  QuickOpenItem,
230
230
  QuickOpenProvider,
231
+ QuickOpenSearchContext,
231
232
  QuickOpenSelectContext,
232
233
  WorkbenchQuickOpenProps,
233
234
  } from './commands/WorkbenchQuickOpen';
@@ -99,3 +99,15 @@ export type WorkbenchStandaloneBootstrapEvent<TActivityId extends string = strin
99
99
  | { type: 'activity-change'; payload: WorkbenchActivityChangeEvent<TActivityId> }
100
100
  | { type: 'status-message'; message: string }
101
101
  | { type: 'error'; error: Error };
102
+
103
+ export * from './WorkbenchStandaloneShell';
104
+ export {
105
+ createWorkbenchStandaloneShellStateSnapshot,
106
+ useWorkbenchStandaloneShellContext,
107
+ } from './workbenchStandaloneShellReactContext';
108
+ export type {
109
+ WorkbenchStandaloneShellStateChange,
110
+ WorkbenchStandaloneShellStateChangeKind,
111
+ WorkbenchStandaloneShellStateSnapshot,
112
+ } from './workbenchStandaloneShellReactContext';
113
+ export { useWorkbenchStandaloneShellStateSync } from './useWorkbenchStandaloneShellStateSync';
@@ -3,19 +3,19 @@
3
3
  .workbench-search-view,
4
4
  .workbench-chat-view,
5
5
  .workbench-primary-sidebar .ui-sidebar-view,
6
- .shell-react-primary-sidebar {
6
+ .shell-react-sidebar-host {
7
7
  display: flex;
8
8
  flex: 1 1 auto;
9
9
  flex-direction: column;
10
10
  min-height: 0;
11
11
  }
12
12
 
13
- .shell-react-primary-sidebar > section {
13
+ .shell-react-sidebar-host > section {
14
14
  /* section is semantic only; view host frame carries sidebar flex sizing */
15
15
  display: contents;
16
16
  }
17
17
 
18
- .shell-react-primary-sidebar [data-view-host-id] {
18
+ .shell-react-sidebar-host [data-view-host-id] {
19
19
  display: flex;
20
20
  flex: 1 1 auto;
21
21
  flex-direction: column;