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

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.
Files changed (30) hide show
  1. package/README.md +29 -0
  2. package/package.json +10 -10
  3. package/src/index.ts +16 -0
  4. package/src/jdw/builtins/renderBuiltinWidgetLeaf.tsx +1 -3
  5. package/src/jdw/createBuiltinJdwRegistry.ts +1 -4
  6. package/src/jdw/cssRenderBackend.tsx +1 -4
  7. package/src/modal/modalPosition.ts +3 -6
  8. package/src/overlay/measureAnchoredOverlayPanel.ts +4 -6
  9. package/src/primitives/select/Select.tsx +1 -13
  10. package/src/test-utils/workbenchMonacoMock.tsx +20 -0
  11. package/src/utils/clamp.ts +4 -0
  12. package/src/utils/normalizeKeyToken.ts +13 -0
  13. package/src/utils/readNumber.ts +4 -0
  14. package/src/widget-tree/WidgetTreeCanvasPreview.tsx +1 -4
  15. package/src/workbench/commands/ShortcutCommandBridge.tsx +1 -13
  16. package/src/workbench/commands/WorkbenchQuickOpen.tsx +410 -0
  17. package/src/workbench/commands/createWorkspaceFilesQuickOpenProvider.ts +86 -0
  18. package/src/workbench/commands/quick-open-model.ts +57 -0
  19. package/src/workbench/index.ts +16 -0
  20. package/src/workbench/management/ExtensionManagementPanel.tsx +1 -34
  21. package/src/workbench/management/ExtensionManagementSidebar.tsx +1 -34
  22. package/src/workbench/management/IntegrationSettingsSurface.tsx +2 -5
  23. package/src/workbench/management/KeybindingCaptureField.tsx +1 -9
  24. package/src/workbench/management/WorkbenchNotice.tsx +2 -5
  25. package/src/workbench/management/extension-management-filters.ts +38 -0
  26. package/src/workbench/settings/StructuredDataForm.tsx +1 -4
  27. package/src/workbench/settings/structuredDataFormModel.ts +2 -1
  28. package/src/workbench/shell/WorkbenchShell.tsx +40 -1
  29. package/src/workbench/shell/WorkbenchShellTitleBarLayoutControls.tsx +4 -7
  30. package/src/workbench/shell/activity-bar.css +47 -0
@@ -0,0 +1,410 @@
1
+ import {
2
+ useCallback,
3
+ useEffect,
4
+ useId,
5
+ useMemo,
6
+ useRef,
7
+ useState,
8
+ type ComponentPropsWithRef,
9
+ type KeyboardEvent,
10
+ type ReactNode,
11
+ } from 'react';
12
+ import { useModalFocusTrap } from '../../modal/useModalFocusTrap';
13
+ import { Button } from '../../primitives/button';
14
+ import { EmptyState } from '../../primitives/empty-state';
15
+ import { IconButton } from '../../primitives/icon-button';
16
+ import { TextInput } from '../../primitives/text-input';
17
+ import { cxCodicon } from '../../utils/codicon';
18
+ import { cx } from '../../utils/cx';
19
+ import {
20
+ DEFAULT_QUICK_OPEN_SEARCH_DEBOUNCE_MS,
21
+ getNextQuickOpenItemIndex,
22
+ isQuickOpenItemSelectable,
23
+ type QuickOpenItem,
24
+ type QuickOpenProvider,
25
+ type QuickOpenSelectContext,
26
+ } from './quick-open-model';
27
+
28
+ interface WorkbenchQuickOpenKeyEvent {
29
+ key: string;
30
+ preventDefault: () => void;
31
+ }
32
+
33
+ function useControllableQuery({
34
+ defaultQuery = '',
35
+ query,
36
+ onQueryChange,
37
+ }: {
38
+ defaultQuery?: string | undefined;
39
+ query?: string | undefined;
40
+ onQueryChange?: ((query: string) => void) | undefined;
41
+ }) {
42
+ const [uncontrolledQuery, setUncontrolledQuery] = useState(defaultQuery);
43
+ const resolvedQuery = query ?? uncontrolledQuery;
44
+
45
+ const setQuery = (nextQuery: string) => {
46
+ if (query === undefined) {
47
+ setUncontrolledQuery(nextQuery);
48
+ }
49
+ onQueryChange?.(nextQuery);
50
+ };
51
+
52
+ return [resolvedQuery, setQuery] as const;
53
+ }
54
+
55
+ export interface WorkbenchQuickOpenProps extends Omit<
56
+ ComponentPropsWithRef<'div'>,
57
+ 'children' | 'onSelect' | 'title'
58
+ > {
59
+ activeItemId?: string | undefined;
60
+ closeLabel?: string | undefined;
61
+ debounceMs?: number | undefined;
62
+ defaultQuery?: string | undefined;
63
+ emptyLabel?: ReactNode | undefined;
64
+ onActiveItemChange?: ((itemId: string) => void) | undefined;
65
+ onClose: () => void;
66
+ onQueryChange?: ((query: string) => void) | undefined;
67
+ onSelectItem?: ((item: QuickOpenItem, context: QuickOpenSelectContext) => void) | undefined;
68
+ open?: boolean | undefined;
69
+ placeholder?: string | undefined;
70
+ /** When omitted, the first provider is used. */
71
+ providerId?: string | undefined;
72
+ providers: readonly QuickOpenProvider[];
73
+ query?: string | undefined;
74
+ restoreFocusOnClose?: boolean | undefined;
75
+ title?: ReactNode | undefined;
76
+ }
77
+
78
+ export function WorkbenchQuickOpen({
79
+ activeItemId,
80
+ className,
81
+ closeLabel = 'Close Quick Open',
82
+ debounceMs = DEFAULT_QUICK_OPEN_SEARCH_DEBOUNCE_MS,
83
+ defaultQuery,
84
+ emptyLabel = 'No matching files',
85
+ onActiveItemChange,
86
+ onClose,
87
+ onQueryChange,
88
+ onSelectItem,
89
+ open = true,
90
+ placeholder = 'Search files by name',
91
+ providerId,
92
+ providers,
93
+ query,
94
+ restoreFocusOnClose = true,
95
+ title = 'Quick Open',
96
+ ...props
97
+ }: WorkbenchQuickOpenProps) {
98
+ const titleId = useId();
99
+ const listId = useId();
100
+ const dialogRef = useRef<HTMLDivElement>(null);
101
+ const inputRef = useRef<HTMLInputElement>(null);
102
+ const [uncontrolledActiveItemId, setUncontrolledActiveItemId] = useState<string>();
103
+ const [items, setItems] = useState<QuickOpenItem[]>([]);
104
+ const [searching, setSearching] = useState(false);
105
+ const [resolvedQuery, setResolvedQuery] = useControllableQuery({
106
+ defaultQuery,
107
+ onQueryChange,
108
+ query,
109
+ });
110
+
111
+ const activeProvider = useMemo(() => {
112
+ if (providerId) {
113
+ return providers.find((provider) => provider.id === providerId) ?? providers[0];
114
+ }
115
+ return providers[0];
116
+ }, [providerId, providers]);
117
+
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);
121
+
122
+ const updateActiveItem = useCallback(
123
+ (itemId: string | undefined) => {
124
+ if (!itemId) return;
125
+ if (activeItemId === undefined) {
126
+ setUncontrolledActiveItemId(itemId);
127
+ }
128
+ onActiveItemChange?.(itemId);
129
+ },
130
+ [activeItemId, onActiveItemChange],
131
+ );
132
+
133
+ useModalFocusTrap({
134
+ enabled: open,
135
+ containerRef: dialogRef,
136
+ initialFocusRef: inputRef,
137
+ onClose,
138
+ restoreFocusOnClose,
139
+ });
140
+
141
+ useEffect(() => {
142
+ if (!open || !activeProvider) {
143
+ setItems([]);
144
+ setSearching(false);
145
+ return undefined;
146
+ }
147
+
148
+ let cancelled = false;
149
+ const runSearch = async () => {
150
+ 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);
159
+ }
160
+ }
161
+ };
162
+
163
+ // Empty query (recent / top-level) should paint immediately; debounce typed queries.
164
+ const delay = resolvedQuery.trim() ? Math.max(0, debounceMs) : 0;
165
+ if (delay === 0) {
166
+ void runSearch();
167
+ return () => {
168
+ cancelled = true;
169
+ };
170
+ }
171
+
172
+ const timer = window.setTimeout(() => {
173
+ void runSearch();
174
+ }, delay);
175
+
176
+ return () => {
177
+ cancelled = true;
178
+ window.clearTimeout(timer);
179
+ };
180
+ }, [activeProvider, debounceMs, open, resolvedQuery]);
181
+
182
+ useEffect(() => {
183
+ if (!open) return;
184
+ if (activeItemId !== undefined) return;
185
+
186
+ setUncontrolledActiveItemId((currentItemId) => {
187
+ const nextItemId = items.find(isQuickOpenItemSelectable)?.id;
188
+ return currentItemId === nextItemId ? currentItemId : nextItemId;
189
+ });
190
+ }, [activeItemId, items, open]);
191
+
192
+ const selectItem = useCallback(
193
+ (item: QuickOpenItem, index: number) => {
194
+ if (!isQuickOpenItemSelectable(item) || !activeProvider) return;
195
+ onSelectItem?.(item, {
196
+ index,
197
+ providerId: activeProvider.id,
198
+ query: resolvedQuery,
199
+ });
200
+ },
201
+ [activeProvider, onSelectItem, resolvedQuery],
202
+ );
203
+
204
+ const handleQuickOpenKeyDown = useCallback(
205
+ (event: WorkbenchQuickOpenKeyEvent) => {
206
+ if (
207
+ event.key !== 'ArrowDown' &&
208
+ event.key !== 'ArrowUp' &&
209
+ event.key !== 'Home' &&
210
+ event.key !== 'End' &&
211
+ event.key !== 'PageDown' &&
212
+ event.key !== 'PageUp' &&
213
+ event.key !== 'Enter'
214
+ ) {
215
+ return;
216
+ }
217
+
218
+ if (event.key === 'Enter') {
219
+ if (!activeItem) return;
220
+ event.preventDefault();
221
+ selectItem(activeItem, Math.max(activeIndex, 0));
222
+ return;
223
+ }
224
+
225
+ event.preventDefault();
226
+
227
+ const direction =
228
+ event.key === 'ArrowUp' || event.key === 'End' || event.key === 'PageUp'
229
+ ? 'previous'
230
+ : 'next';
231
+ const stepCount = event.key === 'PageDown' || event.key === 'PageUp' ? 5 : 1;
232
+ let nextIndex =
233
+ event.key === 'Home'
234
+ ? getNextQuickOpenItemIndex({
235
+ currentIndex: -1,
236
+ direction: 'next',
237
+ items,
238
+ })
239
+ : event.key === 'End'
240
+ ? getNextQuickOpenItemIndex({
241
+ currentIndex: 0,
242
+ direction: 'previous',
243
+ items,
244
+ })
245
+ : activeIndex;
246
+
247
+ if (event.key !== 'Home' && event.key !== 'End') {
248
+ for (let step = 0; step < stepCount; step += 1) {
249
+ const steppedIndex = getNextQuickOpenItemIndex({
250
+ currentIndex: nextIndex,
251
+ direction,
252
+ items,
253
+ });
254
+
255
+ if (steppedIndex < 0 || steppedIndex === nextIndex) break;
256
+ nextIndex = steppedIndex;
257
+ }
258
+ }
259
+
260
+ if (nextIndex >= 0) {
261
+ updateActiveItem(items[nextIndex]?.id);
262
+ }
263
+ },
264
+ [activeIndex, activeItem, items, selectItem, updateActiveItem],
265
+ );
266
+
267
+ useEffect(() => {
268
+ if (!open) return undefined;
269
+
270
+ const onKeyDown = (event: globalThis.KeyboardEvent) => {
271
+ if (event.defaultPrevented) return;
272
+ handleQuickOpenKeyDown(event);
273
+ };
274
+
275
+ window.addEventListener('keydown', onKeyDown);
276
+ return () => {
277
+ window.removeEventListener('keydown', onKeyDown);
278
+ };
279
+ }, [handleQuickOpenKeyDown, open]);
280
+
281
+ const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
282
+ handleQuickOpenKeyDown(event);
283
+ };
284
+
285
+ useEffect(() => {
286
+ if (!resolvedActiveItemId) return;
287
+
288
+ const activeElement = Array.from(
289
+ dialogRef.current?.querySelectorAll<HTMLElement>('[data-quick-open-id]') ?? [],
290
+ ).find((element) => element.dataset.quickOpenId === resolvedActiveItemId);
291
+
292
+ activeElement?.scrollIntoView?.({ block: 'nearest' });
293
+ }, [items, resolvedActiveItemId]);
294
+
295
+ if (!open) return null;
296
+
297
+ return (
298
+ <div className="ui-workbench-command-palette-overlay" onClick={onClose}>
299
+ <div
300
+ aria-labelledby={titleId}
301
+ aria-modal="true"
302
+ className={cx('ui-workbench-command-palette', className)}
303
+ data-testid="workbench-quick-open"
304
+ role="dialog"
305
+ {...props}
306
+ ref={dialogRef}
307
+ onClick={(event) => event.stopPropagation()}
308
+ >
309
+ <div className="ui-workbench-command-palette__header">
310
+ <span id={titleId} className="ui-workbench-command-palette__title">
311
+ {title}
312
+ </span>
313
+ <IconButton
314
+ className="ui-workbench-command-palette__close"
315
+ icon="codicon-close"
316
+ label={closeLabel}
317
+ onClick={onClose}
318
+ />
319
+ </div>
320
+ <div className="ui-workbench-command-palette__search">
321
+ <i aria-hidden="true" className="codicon codicon-search" />
322
+ <TextInput
323
+ ref={inputRef}
324
+ aria-activedescendant={
325
+ activeItem ? `${listId}-${activeItem.id.replace(/[^\w-]/g, '_')}` : undefined
326
+ }
327
+ aria-controls={listId}
328
+ aria-label={placeholder}
329
+ className="ui-workbench-command-palette__input"
330
+ controlWidth="full"
331
+ placeholder={placeholder}
332
+ type="search"
333
+ value={resolvedQuery}
334
+ onValueChange={setResolvedQuery}
335
+ onKeyDown={handleKeyDown}
336
+ />
337
+ </div>
338
+ <div
339
+ id={listId}
340
+ aria-busy={searching || undefined}
341
+ aria-label="Quick Open results"
342
+ className={cx('ui-workbench-command-list', 'ui-workbench-scrollbar')}
343
+ role="listbox"
344
+ >
345
+ {items.length === 0 ? (
346
+ <EmptyState compact icon="codicon-search">
347
+ {emptyLabel}
348
+ </EmptyState>
349
+ ) : (
350
+ items.map((item, index) => {
351
+ const active = item.id === resolvedActiveItemId;
352
+ const itemDomId = `${listId}-${item.id.replace(/[^\w-]/g, '_')}`;
353
+ const descriptionId = item.description ? `${itemDomId}-description` : undefined;
354
+
355
+ return (
356
+ <Button
357
+ key={item.id}
358
+ id={itemDomId}
359
+ aria-describedby={descriptionId}
360
+ aria-selected={active}
361
+ className="ui-workbench-command-item"
362
+ data-active={active ? 'true' : undefined}
363
+ data-quick-open-id={item.id}
364
+ disabled={item.disabled}
365
+ role="option"
366
+ onClick={() => selectItem(item, index)}
367
+ onMouseEnter={() => updateActiveItem(item.id)}
368
+ >
369
+ <span className="ui-workbench-command-item__icon">
370
+ {item.icon ? <i aria-hidden="true" className={cxCodicon(item.icon)} /> : null}
371
+ </span>
372
+ <span className="ui-workbench-command-item__content">
373
+ <span className="ui-workbench-command-item__label">{item.label}</span>
374
+ {item.description ? (
375
+ <span id={descriptionId} className="ui-workbench-command-item__description">
376
+ {item.description}
377
+ </span>
378
+ ) : null}
379
+ </span>
380
+ <span className="ui-workbench-command-item__meta">
381
+ {item.detail ? (
382
+ <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
+ ) : null}
388
+ </span>
389
+ </Button>
390
+ );
391
+ })
392
+ )}
393
+ </div>
394
+ </div>
395
+ </div>
396
+ );
397
+ }
398
+
399
+ export type { QuickOpenItem, QuickOpenProvider, QuickOpenSelectContext } from './quick-open-model';
400
+ export {
401
+ DEFAULT_QUICK_OPEN_SEARCH_DEBOUNCE_MS,
402
+ getNextQuickOpenItemIndex,
403
+ isQuickOpenItemSelectable,
404
+ } from './quick-open-model';
405
+ export {
406
+ WORKSPACE_FILES_QUICK_OPEN_PROVIDER_ID,
407
+ createWorkspaceFilesQuickOpenProvider,
408
+ resolveQuickOpenItemPath,
409
+ } from './createWorkspaceFilesQuickOpenProvider';
410
+ export type { CreateWorkspaceFilesQuickOpenProviderOptions } from './createWorkspaceFilesQuickOpenProvider';
@@ -0,0 +1,86 @@
1
+ import { searchWorkspaceFiles, type WorkspaceFile } from '@workbench-kit/workspace';
2
+
3
+ import type { QuickOpenItem, QuickOpenProvider } from './quick-open-model';
4
+
5
+ export const WORKSPACE_FILES_QUICK_OPEN_PROVIDER_ID = 'workspace.files' as const;
6
+
7
+ export interface CreateWorkspaceFilesQuickOpenProviderOptions {
8
+ /** Current workspace files, or a getter read on each search. */
9
+ files: readonly WorkspaceFile[] | (() => readonly WorkspaceFile[]);
10
+ id?: string | undefined;
11
+ label?: string | undefined;
12
+ /**
13
+ * When the query is empty, these paths appear first (still present in `files`).
14
+ * Remaining files follow in `searchWorkspaceFiles` order.
15
+ */
16
+ recentPaths?: readonly string[] | undefined;
17
+ }
18
+
19
+ function resolveFiles(
20
+ files: CreateWorkspaceFilesQuickOpenProviderOptions['files'],
21
+ ): readonly WorkspaceFile[] {
22
+ return typeof files === 'function' ? files() : files;
23
+ }
24
+
25
+ function toQuickOpenItem(path: string, matchedBy?: string, preview?: string): QuickOpenItem {
26
+ const segments = path.split('/');
27
+ const label = segments[segments.length - 1] || path;
28
+ const parent = segments.length > 1 ? segments.slice(0, -1).join('/') : undefined;
29
+
30
+ return {
31
+ data: { path },
32
+ description: parent,
33
+ detail: matchedBy ?? preview,
34
+ icon: 'codicon-file',
35
+ id: path,
36
+ label,
37
+ };
38
+ }
39
+
40
+ export function createWorkspaceFilesQuickOpenProvider(
41
+ options: CreateWorkspaceFilesQuickOpenProviderOptions,
42
+ ): QuickOpenProvider {
43
+ const providerId = options.id ?? WORKSPACE_FILES_QUICK_OPEN_PROVIDER_ID;
44
+ const providerLabel = options.label ?? 'Files';
45
+
46
+ return {
47
+ id: providerId,
48
+ label: providerLabel,
49
+ search(query: string) {
50
+ const files = [...resolveFiles(options.files)];
51
+ const results = searchWorkspaceFiles(files, query);
52
+ const items = results.map((result) =>
53
+ toQuickOpenItem(result.path, result.matchedBy, result.preview),
54
+ );
55
+
56
+ if (query.trim() || !options.recentPaths?.length) {
57
+ return items;
58
+ }
59
+
60
+ const recentSet = new Set(options.recentPaths);
61
+ const byPath = new Map(items.map((item) => [item.id, item]));
62
+ const recentItems: QuickOpenItem[] = [];
63
+
64
+ for (const path of options.recentPaths) {
65
+ const item = byPath.get(path);
66
+ if (item) {
67
+ recentItems.push({ ...item, detail: 'Recent' });
68
+ }
69
+ }
70
+
71
+ const rest = items.filter((item) => !recentSet.has(item.id));
72
+ return [...recentItems, ...rest];
73
+ },
74
+ };
75
+ }
76
+
77
+ export function resolveQuickOpenItemPath(item: QuickOpenItem): string | undefined {
78
+ if (typeof item.data === 'object' && item.data !== null && 'path' in item.data) {
79
+ const path = (item.data as { path?: unknown }).path;
80
+ if (typeof path === 'string' && path.length > 0) {
81
+ return path;
82
+ }
83
+ }
84
+
85
+ return item.id || undefined;
86
+ }
@@ -0,0 +1,57 @@
1
+ export interface QuickOpenItem {
2
+ /** Stable item id (often a workspace path). */
3
+ id: string;
4
+ label: string;
5
+ description?: string | undefined;
6
+ detail?: string | undefined;
7
+ icon?: string | undefined;
8
+ disabled?: boolean | undefined;
9
+ /** Provider-specific payload (e.g. `{ path }`). */
10
+ data?: unknown;
11
+ }
12
+
13
+ export interface QuickOpenProvider {
14
+ id: string;
15
+ label: string;
16
+ search: (query: string) => Promise<QuickOpenItem[]> | QuickOpenItem[];
17
+ }
18
+
19
+ export interface QuickOpenSelectContext {
20
+ index: number;
21
+ providerId: string;
22
+ query: string;
23
+ }
24
+
25
+ export function isQuickOpenItemSelectable(item: QuickOpenItem) {
26
+ return !item.disabled;
27
+ }
28
+
29
+ export function getNextQuickOpenItemIndex({
30
+ currentIndex,
31
+ direction,
32
+ items,
33
+ }: {
34
+ currentIndex: number;
35
+ direction: 'next' | 'previous';
36
+ items: readonly QuickOpenItem[];
37
+ }) {
38
+ if (items.length === 0) {
39
+ return -1;
40
+ }
41
+
42
+ const step = direction === 'next' ? 1 : -1;
43
+ let nextIndex = currentIndex;
44
+
45
+ for (let attempt = 0; attempt < items.length; attempt += 1) {
46
+ nextIndex = (nextIndex + step + items.length) % items.length;
47
+ const item = items[nextIndex];
48
+ if (item && isQuickOpenItemSelectable(item)) {
49
+ return nextIndex;
50
+ }
51
+ }
52
+
53
+ return -1;
54
+ }
55
+
56
+ /** Default debounce for provider search while typing. */
57
+ export const DEFAULT_QUICK_OPEN_SEARCH_DEBOUNCE_MS = 200;
@@ -59,6 +59,15 @@ export {
59
59
  isWorkbenchCommandRunnable,
60
60
  resolveWorkbenchCommandExecutionPolicy,
61
61
  } from './commands/CommandPalette';
62
+ export {
63
+ DEFAULT_QUICK_OPEN_SEARCH_DEBOUNCE_MS,
64
+ WORKSPACE_FILES_QUICK_OPEN_PROVIDER_ID,
65
+ WorkbenchQuickOpen,
66
+ createWorkspaceFilesQuickOpenProvider,
67
+ getNextQuickOpenItemIndex,
68
+ isQuickOpenItemSelectable,
69
+ resolveQuickOpenItemPath,
70
+ } from './commands/WorkbenchQuickOpen';
62
71
  export { WorkbenchMarkdownPreview } from './markdown/MarkdownPreview';
63
72
  export type { WorkbenchMarkdownPreviewProps } from './markdown/MarkdownPreview';
64
73
  export { sanitizeMarkdownHref } from './markdown/sanitizeMarkdownHref';
@@ -215,6 +224,13 @@ export type {
215
224
  WorkbenchCommandSuggestProps,
216
225
  } from './commands/CommandPalette';
217
226
  export type { ResolveWorkbenchCommandExecutionPolicyInput } from './commands/CommandPalette';
227
+ export type {
228
+ CreateWorkspaceFilesQuickOpenProviderOptions,
229
+ QuickOpenItem,
230
+ QuickOpenProvider,
231
+ QuickOpenSelectContext,
232
+ WorkbenchQuickOpenProps,
233
+ } from './commands/WorkbenchQuickOpen';
218
234
  export type {
219
235
  UseWorkbenchShortcutCommandsOptions,
220
236
  WorkbenchShortcutCommandBinding,
@@ -8,6 +8,7 @@ import {
8
8
  formatExtensionCategoryLabel,
9
9
  } from './extension-category-display.js';
10
10
  import { resolveExtensionInstallOptions } from './extension-install-approval.js';
11
+ import { filterBrowseEntries, filterInstalledEntries } from './extension-management-filters.js';
11
12
  import { ManagementFilterChips } from './ManagementFilterChips.js';
12
13
  import { ManagementCard, ManagementCardList } from './ManagementCard.js';
13
14
  import { ManagementGroup, ManagementGroups } from './ManagementGroup.js';
@@ -491,37 +492,3 @@ function groupBrowseEntries(entries: readonly ExtensionCatalogBrowseEntry[]) {
491
492
  })
492
493
  .map(([category, categoryEntries]) => ({ category, entries: categoryEntries }));
493
494
  }
494
-
495
- function filterInstalledEntries(entries: readonly ExtensionManagementEntry[], query: string) {
496
- const normalized = query.trim().toLowerCase();
497
- if (!normalized) {
498
- return entries;
499
- }
500
-
501
- return entries.filter((entry) =>
502
- [entry.displayName, entry.id, entry.category, entry.description ?? '']
503
- .join(' ')
504
- .toLowerCase()
505
- .includes(normalized),
506
- );
507
- }
508
-
509
- function filterBrowseEntries(
510
- entries: readonly ExtensionCatalogBrowseEntry[],
511
- query: string,
512
- category?: string,
513
- ) {
514
- const normalized = query.trim().toLowerCase();
515
- return entries.filter((entry) => {
516
- if (category && entry.category !== category) {
517
- return false;
518
- }
519
- if (!normalized) {
520
- return true;
521
- }
522
- return [entry.displayName, entry.id, entry.category, entry.description]
523
- .join(' ')
524
- .toLowerCase()
525
- .includes(normalized);
526
- });
527
- }
@@ -18,6 +18,7 @@ import {
18
18
  formatExtensionCategoryLabel,
19
19
  } from './extension-category-display.js';
20
20
  import { resolveExtensionInstallOptions } from './extension-install-approval.js';
21
+ import { filterBrowseEntries, filterInstalledEntries } from './extension-management-filters.js';
21
22
  import { ManagementFilterChips } from './ManagementFilterChips.js';
22
23
  import type {
23
24
  ExtensionCatalogBrowseEntry,
@@ -473,37 +474,3 @@ function formatFeatureBadge(label: string, count: number) {
473
474
 
474
475
  return `${label} ${count}`;
475
476
  }
476
-
477
- function filterInstalledEntries(entries: readonly ExtensionManagementEntry[], query: string) {
478
- const normalized = query.trim().toLowerCase();
479
- if (!normalized) {
480
- return entries;
481
- }
482
-
483
- return entries.filter((entry) =>
484
- [entry.displayName, entry.id, entry.category, entry.description ?? '']
485
- .join(' ')
486
- .toLowerCase()
487
- .includes(normalized),
488
- );
489
- }
490
-
491
- function filterBrowseEntries(
492
- entries: readonly ExtensionCatalogBrowseEntry[],
493
- query: string,
494
- category?: string,
495
- ) {
496
- const normalized = query.trim().toLowerCase();
497
- return entries.filter((entry) => {
498
- if (category && entry.category !== category) {
499
- return false;
500
- }
501
- if (!normalized) {
502
- return true;
503
- }
504
- return [entry.displayName, entry.id, entry.category, entry.description]
505
- .join(' ')
506
- .toLowerCase()
507
- .includes(normalized);
508
- });
509
- }
@@ -7,6 +7,7 @@ import {
7
7
  WorkbenchPropertySection,
8
8
  WorkbenchPropertyStack,
9
9
  } from '../../primitives/index';
10
+ import { cx } from '../../utils/cx';
10
11
 
11
12
  type IntegrationSettingsDensity = 'compact' | 'default';
12
13
  type IntegrationSettingsInset = 'default' | 'flush';
@@ -206,7 +207,7 @@ export function IntegrationAccountRowEditor({
206
207
  export function IntegrationBodyText({ children, size = 'default' }: IntegrationBodyTextProps) {
207
208
  return (
208
209
  <WorkbenchPropertyHint
209
- className={joinClasses('block', size === 'compact' ? 'text-xs' : undefined)}
210
+ className={cx('block', size === 'compact' ? 'text-xs' : undefined)}
210
211
  data-size={size}
211
212
  >
212
213
  {children}
@@ -284,7 +285,3 @@ function IntegrationEditorDescription({ children }: { children?: ReactNode }) {
284
285
  function hasRenderableNode(node: ReactNode | undefined): boolean {
285
286
  return node !== undefined && node !== null && node !== false;
286
287
  }
287
-
288
- function joinClasses(...classes: Array<string | undefined>): string {
289
- return classes.filter(Boolean).join(' ');
290
- }