@workbench-kit/shell-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.
package/README.md CHANGED
@@ -63,13 +63,15 @@ for Flow-only embeds and custom bundler setups. The full barrel
63
63
  `FieldRemapFlowMapper` (and `FieldRemapPanel` pass-through) accept optional chrome
64
64
  hooks so hosts avoid CSS/DOM workarounds:
65
65
 
66
- | Prop | Behavior |
67
- | --------------------------------------------------------------- | --------------------------------------------------------------------- |
68
- | `showMinimap` | Default `true`. When `false`, MiniMap is not mounted. |
69
- | `onPaneContextMenu` / `onNodeContextMenu` / `onEdgeContextMenu` | Native event + selection payload; host owns menu UI. |
70
- | `flowActionsRef` | `{ fitView(options?) }` using the same defaults as Controls fit-view. |
71
- | `labels` / `t` | Override edge-list / Convert palette chrome (e.g. “Field maps”). |
72
- | `ioChrome` (Panel) | `'browse' \| 'edit' \| 'none'` prefer browse for inspect-only I/O. |
66
+ | Prop | Behavior |
67
+ | --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
68
+ | `showMinimap` | Default `true`. When `false`, MiniMap is not mounted. |
69
+ | `onShowMinimapChange` | When set, adds a MiniMap toggle inside Flow Controls (+/−/fit). |
70
+ | `includeHidden` / `onIncludeHiddenChange` | When callback set, adds a hidden-fields toggle in the same Controls. Host/Panel still project shapes. |
71
+ | `onPaneContextMenu` / `onNodeContextMenu` / `onEdgeContextMenu` | Native event + selection payload; host owns menu UI. |
72
+ | `flowActionsRef` | `{ fitView(options?) }` using the same defaults as Controls fit-view. |
73
+ | `labels` / `t` | Override edge-list / Convert palette chrome (e.g. “Field maps”). |
74
+ | `ioChrome` (Panel) | `'browse' \| 'edit' \| 'none'` — prefer browse for inspect-only I/O. |
73
75
 
74
76
  ```tsx
75
77
  const flowActionsRef = useRef<FieldRemapFlowActions | null>(null);
@@ -90,6 +92,21 @@ const flowActionsRef = useRef<FieldRemapFlowActions | null>(null);
90
92
  />;
91
93
  ```
92
94
 
95
+ ## Shell chrome labels / `t()`
96
+
97
+ `WorkbenchShell` accepts optional `labels` and `t(key, fallback)` for high-visibility chrome
98
+ (ActivityBar / StatusBar aria names, Profile/Settings secondary items, command palette).
99
+ English defaults apply when neither is set. See
100
+ `resolveWorkbenchShellChromeLabels` / `workbenchShellChromeLabelKeys` and
101
+ [Consumer Capabilities — Shell chrome label injection](../../docs/workbench/consumer-capabilities.md#shell-chrome-label--t-injection-126).
102
+
103
+ ```tsx
104
+ <WorkbenchShell
105
+ t={(key, fallback) => hostTranslate(key, fallback)}
106
+ labels={{ settingsLabel: 'Settings' }}
107
+ />
108
+ ```
109
+
93
110
  ## Related docs
94
111
 
95
112
  - [Component Map](../../docs/guides/component-map.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workbench-kit/shell-react",
3
- "version": "0.0.2-prototype.0.2.11",
3
+ "version": "0.0.2-prototype.0.2.14",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {
@@ -21,13 +21,13 @@
21
21
  "@xyflow/react": "^12.11.2",
22
22
  "jsonata": "^2.2.0",
23
23
  "react-table-mapping": "^1.0.3",
24
- "@workbench-kit/platform": "0.0.2-prototype.0.2.11",
25
- "@workbench-kit/react": "0.0.2-prototype.0.2.11",
26
- "@workbench-kit/tokens": "0.0.2-prototype.0.2.11",
27
- "@workbench-kit/workbench-config": "0.0.2-prototype.0.2.11",
28
- "@workbench-kit/workspace": "0.0.2-prototype.0.2.11",
29
- "@workbench-kit/field-remap": "0.0.2-prototype.0.2.11",
30
- "@workbench-kit/workbench-core": "0.0.2-prototype.0.2.11"
24
+ "@workbench-kit/field-remap": "0.0.2-prototype.0.2.14",
25
+ "@workbench-kit/tokens": "0.0.2-prototype.0.2.14",
26
+ "@workbench-kit/workbench-config": "0.0.2-prototype.0.2.14",
27
+ "@workbench-kit/workbench-core": "0.0.2-prototype.0.2.14",
28
+ "@workbench-kit/workspace": "0.0.2-prototype.0.2.14",
29
+ "@workbench-kit/react": "0.0.2-prototype.0.2.14",
30
+ "@workbench-kit/platform": "0.0.2-prototype.0.2.14"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "react": "^19.0.0",
@@ -0,0 +1,7 @@
1
+ export function getCommandErrorMessage(error: unknown) {
2
+ if (error instanceof Error) {
3
+ return error.message;
4
+ }
5
+
6
+ return String(error);
7
+ }
@@ -6,6 +6,7 @@ import {
6
6
  type WorkbenchCommandDescriptor,
7
7
  } from '@workbench-kit/react/workbench';
8
8
 
9
+ import { getCommandErrorMessage } from './command-error-message.js';
9
10
  import { parseWorkbenchChatCommandInput } from './command-input.js';
10
11
  import { useWorkbench } from '../shell/provider.js';
11
12
  import { useWorkbenchCommandDescriptors } from '../commands/use-command-descriptors.js';
@@ -145,11 +146,3 @@ export function useWorkbenchChatCommandSurface({
145
146
  runInputAsCommand,
146
147
  };
147
148
  }
148
-
149
- function getCommandErrorMessage(error: unknown) {
150
- if (error instanceof Error) {
151
- return error.message;
152
- }
153
-
154
- return String(error);
155
- }
@@ -7,6 +7,7 @@ import {
7
7
  } from '@workbench-kit/react/workbench';
8
8
  import type { ChatCommandProposal, ChatMessage } from '@workbench-kit/react/workbench/chat';
9
9
 
10
+ import { getCommandErrorMessage } from './command-error-message.js';
10
11
  import { type WorkbenchChatCommandRunResult } from './command-surface.js';
11
12
  import { useWorkbench } from '../shell/provider.js';
12
13
 
@@ -24,14 +25,6 @@ export interface UseWorkbenchChatCommandProposalsOptions {
24
25
  policyInput?: ResolveWorkbenchCommandExecutionPolicyInput | undefined;
25
26
  }
26
27
 
27
- function getCommandErrorMessage(error: unknown) {
28
- if (error instanceof Error) {
29
- return error.message;
30
- }
31
-
32
- return String(error);
33
- }
34
-
35
28
  function createProposalId(commandId: string) {
36
29
  return `proposal-${commandId}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
37
30
  }
@@ -21,6 +21,10 @@ export interface FieldRemapChromeLabels {
21
21
  readonly placeConvert: string;
22
22
  readonly operatorsTitle: string;
23
23
  readonly operatorsDescription: string;
24
+ readonly showMinimap: string;
25
+ readonly hideMinimap: string;
26
+ readonly showHiddenFields: string;
27
+ readonly hideHiddenFields: string;
24
28
  }
25
29
 
26
30
  export const defaultFieldRemapChromeLabels: FieldRemapChromeLabels = {
@@ -34,6 +38,10 @@ export const defaultFieldRemapChromeLabels: FieldRemapChromeLabels = {
34
38
  operatorsTitle: 'n→m operators',
35
39
  operatorsDescription:
36
40
  'Create combine (n→1) or split (1→n), then wire ports or edit in the side rail.',
41
+ showMinimap: 'Show minimap',
42
+ hideMinimap: 'Hide minimap',
43
+ showHiddenFields: 'Show hidden fields',
44
+ hideHiddenFields: 'Hide hidden fields',
37
45
  };
38
46
 
39
47
  /** Stable capability ids for optional `t()` injection (not free prose). */
@@ -46,6 +54,10 @@ export const fieldRemapChromeLabelKeys = {
46
54
  placeConvert: 'fieldRemap.placeConvert',
47
55
  operatorsTitle: 'fieldRemap.operatorsTitle',
48
56
  operatorsDescription: 'fieldRemap.operatorsDescription',
57
+ showMinimap: 'fieldRemap.showMinimap',
58
+ hideMinimap: 'fieldRemap.hideMinimap',
59
+ showHiddenFields: 'fieldRemap.showHiddenFields',
60
+ hideHiddenFields: 'fieldRemap.hideHiddenFields',
49
61
  } as const satisfies Record<keyof FieldRemapChromeLabels, string>;
50
62
 
51
63
  export function resolveFieldRemapChromeLabels(
@@ -70,5 +82,9 @@ export function resolveFieldRemapChromeLabels(
70
82
  placeConvert: resolve('placeConvert'),
71
83
  operatorsTitle: resolve('operatorsTitle'),
72
84
  operatorsDescription: resolve('operatorsDescription'),
85
+ showMinimap: resolve('showMinimap'),
86
+ hideMinimap: resolve('hideMinimap'),
87
+ showHiddenFields: resolve('showHiddenFields'),
88
+ hideHiddenFields: resolve('hideHiddenFields'),
73
89
  };
74
90
  }
@@ -57,14 +57,6 @@ export function SampleFieldRemapDemo({
57
57
  data-testid="field-remap-host-chrome"
58
58
  style={{ display: 'flex', gap: '0.5rem', marginBottom: '0.75rem', flexWrap: 'wrap' }}
59
59
  >
60
- <Button
61
- compact
62
- type="button"
63
- data-testid="field-remap-toggle-minimap"
64
- onClick={() => setShowMinimap((current) => !current)}
65
- >
66
- {showMinimap ? 'Hide MiniMap' : 'Show MiniMap'}
67
- </Button>
68
60
  <Button
69
61
  compact
70
62
  type="button"
@@ -84,6 +76,7 @@ export function SampleFieldRemapDemo({
84
76
  key={`${sample.id}:${ioChrome ?? 'default'}:${browseSeedShapes ? 'seed' : 'plain'}`}
85
77
  sample={sample}
86
78
  showMinimap={showMinimap}
79
+ onShowMinimapChange={setShowMinimap}
87
80
  ioChrome={ioChrome}
88
81
  editableShapes={ioChrome === 'browse' ? false : undefined}
89
82
  sources={browseShapes?.sources}
@@ -12,6 +12,7 @@ import {
12
12
  } from 'react';
13
13
  import {
14
14
  Background,
15
+ ControlButton,
15
16
  Controls,
16
17
  Handle,
17
18
  MiniMap,
@@ -333,6 +334,22 @@ export interface FieldRemapFlowMapperProps {
333
334
  * backward compatibility with existing samples.
334
335
  */
335
336
  readonly showMinimap?: boolean | undefined;
337
+ /**
338
+ * When set, Flow Controls (+/−/fit) include a MiniMap toggle button in the same
339
+ * panel. Hosts should prefer this over a separate toolbar control.
340
+ */
341
+ readonly onShowMinimapChange?: ((show: boolean) => void) | undefined;
342
+ /**
343
+ * When false (default), hosts should project shapes with hidden fields omitted
344
+ * before passing `sources` / `targets`. Flow itself does not filter — this flag
345
+ * drives the Controls toggle pressed state only.
346
+ */
347
+ readonly includeHidden?: boolean | undefined;
348
+ /**
349
+ * When set, Flow Controls include a hidden-fields toggle next to zoom / MiniMap.
350
+ * Pair with host (or Panel) shape projection on `includeHidden`.
351
+ */
352
+ readonly onIncludeHiddenChange?: ((includeHidden: boolean) => void) | undefined;
336
353
  /** Pane context menu; host owns menu UI. Receives current Field Remap selection. */
337
354
  readonly onPaneContextMenu?:
338
355
  | ((event: MouseEvent | globalThis.MouseEvent, ctx: { selection: FieldRemapSelection }) => void)
@@ -375,6 +392,9 @@ function FieldRemapFlowCanvas({
375
392
  selection: selectionProp,
376
393
  onSelectionChange: onSelectionChangeProp,
377
394
  showMinimap = true,
395
+ onShowMinimapChange,
396
+ includeHidden = false,
397
+ onIncludeHiddenChange,
378
398
  onPaneContextMenu,
379
399
  onNodeContextMenu,
380
400
  onEdgeContextMenu,
@@ -713,6 +733,7 @@ function FieldRemapFlowCanvas({
713
733
  className="workbench-field-remap-flow"
714
734
  data-testid="field-remap-mapper"
715
735
  data-minimap={showMinimap ? 'on' : 'off'}
736
+ data-hidden-fields={includeHidden ? 'on' : 'off'}
716
737
  onKeyDown={onKeyDown}
717
738
  >
718
739
  <p className="workbench-field-remap-mapper__hint" data-testid="field-remap-hint">
@@ -784,7 +805,93 @@ function FieldRemapFlowCanvas({
784
805
  >
785
806
  <FieldRemapFlowActionsBridge flowActionsRef={flowActionsRef} />
786
807
  <Background gap={16} color="var(--xy-background-pattern-color)" />
787
- <Controls showInteractive={false} fitViewOptions={DEFAULT_FIT_VIEW_OPTIONS} />
808
+ <Controls showInteractive={false} fitViewOptions={DEFAULT_FIT_VIEW_OPTIONS}>
809
+ {onShowMinimapChange ? (
810
+ <ControlButton
811
+ aria-label={showMinimap ? chromeLabels.hideMinimap : chromeLabels.showMinimap}
812
+ className={
813
+ showMinimap
814
+ ? 'workbench-field-remap-flow__minimap-toggle is-active'
815
+ : 'workbench-field-remap-flow__minimap-toggle'
816
+ }
817
+ data-testid="field-remap-toggle-minimap"
818
+ title={showMinimap ? chromeLabels.hideMinimap : chromeLabels.showMinimap}
819
+ onClick={() => {
820
+ onShowMinimapChange(!showMinimap);
821
+ }}
822
+ >
823
+ <svg
824
+ aria-hidden="true"
825
+ fill="none"
826
+ height="16"
827
+ stroke="currentColor"
828
+ strokeLinecap="round"
829
+ strokeLinejoin="round"
830
+ strokeWidth="1.75"
831
+ viewBox="0 0 24 24"
832
+ width="16"
833
+ >
834
+ <path d="M3 6.5 9 4l6 2.5L21 4v13.5L15 20l-6-2.5L3 20z" />
835
+ <path d="M9 4v13.5" />
836
+ <path d="M15 6.5V20" />
837
+ </svg>
838
+ </ControlButton>
839
+ ) : null}
840
+ {onIncludeHiddenChange ? (
841
+ <ControlButton
842
+ aria-label={
843
+ includeHidden ? chromeLabels.hideHiddenFields : chromeLabels.showHiddenFields
844
+ }
845
+ aria-pressed={includeHidden}
846
+ className={
847
+ includeHidden
848
+ ? 'workbench-field-remap-flow__hidden-toggle is-active'
849
+ : 'workbench-field-remap-flow__hidden-toggle'
850
+ }
851
+ data-testid="field-remap-toggle-hidden-fields"
852
+ title={
853
+ includeHidden ? chromeLabels.hideHiddenFields : chromeLabels.showHiddenFields
854
+ }
855
+ onClick={() => {
856
+ onIncludeHiddenChange(!includeHidden);
857
+ }}
858
+ >
859
+ {includeHidden ? (
860
+ <svg
861
+ aria-hidden="true"
862
+ fill="none"
863
+ height="16"
864
+ stroke="currentColor"
865
+ strokeLinecap="round"
866
+ strokeLinejoin="round"
867
+ strokeWidth="1.75"
868
+ viewBox="0 0 24 24"
869
+ width="16"
870
+ >
871
+ <path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z" />
872
+ <circle cx="12" cy="12" r="3" />
873
+ </svg>
874
+ ) : (
875
+ <svg
876
+ aria-hidden="true"
877
+ fill="none"
878
+ height="16"
879
+ stroke="currentColor"
880
+ strokeLinecap="round"
881
+ strokeLinejoin="round"
882
+ strokeWidth="1.75"
883
+ viewBox="0 0 24 24"
884
+ width="16"
885
+ >
886
+ <path d="M3 3l18 18" />
887
+ <path d="M10.6 10.6a3 3 0 0 0 4.2 4.2" />
888
+ <path d="M9.9 5.1A10.6 10.6 0 0 1 12 5c6.5 0 10 7 10 7a17.4 17.4 0 0 1-3.2 4.4" />
889
+ <path d="M6.1 6.1C3.9 7.7 2 12 2 12s3.5 7 10 7a10.4 10.4 0 0 0 4.2-.9" />
890
+ </svg>
891
+ )}
892
+ </ControlButton>
893
+ ) : null}
894
+ </Controls>
788
895
  {showMinimap ? (
789
896
  <MiniMap
790
897
  pannable
@@ -83,6 +83,8 @@ export interface FieldRemapPanelProps {
83
83
  readonly targetShape?: unknown;
84
84
  /** Forwarded to {@link FieldRemapFlowMapper} (default true). */
85
85
  readonly showMinimap?: boolean | undefined;
86
+ /** Forwarded to {@link FieldRemapFlowMapper} Controls MiniMap toggle. */
87
+ readonly onShowMinimapChange?: FieldRemapFlowMapperProps['onShowMinimapChange'];
86
88
  readonly onPaneContextMenu?: FieldRemapFlowMapperProps['onPaneContextMenu'];
87
89
  readonly onNodeContextMenu?: FieldRemapFlowMapperProps['onNodeContextMenu'];
88
90
  readonly onEdgeContextMenu?: FieldRemapFlowMapperProps['onEdgeContextMenu'];
@@ -136,6 +138,7 @@ export function FieldRemapPanel({
136
138
  sourceSample: sourceSampleProp,
137
139
  targetShape: targetShapeProp,
138
140
  showMinimap,
141
+ onShowMinimapChange,
139
142
  onPaneContextMenu,
140
143
  onNodeContextMenu,
141
144
  onEdgeContextMenu,
@@ -396,14 +399,6 @@ export function FieldRemapPanel({
396
399
  className="workbench-field-remap-demo__shapes"
397
400
  data-testid="field-remap-io-browse-wrap"
398
401
  >
399
- <label className="workbench-field-remap-io-browse__toggle">
400
- <input
401
- checked={includeHidden}
402
- onChange={(event) => setIncludeHidden(event.target.checked)}
403
- type="checkbox"
404
- />
405
- Show hidden fields
406
- </label>
407
402
  <FieldRemapIoClassBrowse
408
403
  includeHidden={includeHidden}
409
404
  sources={sourceFields}
@@ -425,6 +420,9 @@ export function FieldRemapPanel({
425
420
  sourceTitle={sample.sourceLabel}
426
421
  targetTitle={sample.targetLabel}
427
422
  showMinimap={showMinimap}
423
+ onShowMinimapChange={onShowMinimapChange}
424
+ includeHidden={includeHidden}
425
+ onIncludeHiddenChange={setIncludeHidden}
428
426
  onPaneContextMenu={onPaneContextMenu}
429
427
  onNodeContextMenu={onNodeContextMenu}
430
428
  onEdgeContextMenu={onEdgeContextMenu}
@@ -331,7 +331,11 @@
331
331
  display: flex;
332
332
  flex-direction: column;
333
333
  gap: 0.65rem;
334
+ box-sizing: border-box;
334
335
  min-width: 0;
336
+ min-height: 0;
337
+ height: 100%;
338
+ overflow: hidden;
335
339
  padding: 0.75rem;
336
340
  border: 1px solid var(--vscode-panel-border, var(--color-border));
337
341
  border-radius: 0.375rem;
@@ -354,14 +358,21 @@
354
358
  color: var(--vscode-descriptionForeground, var(--color-text-muted));
355
359
  }
356
360
 
361
+ .workbench-field-remap-convert-palette__header,
362
+ .workbench-field-remap-convert-palette__place,
363
+ .workbench-field-remap-convert-palette__operators {
364
+ flex: 0 0 auto;
365
+ }
366
+
357
367
  .workbench-field-remap-convert-palette__list {
358
368
  list-style: none;
359
369
  margin: 0;
360
370
  padding: 0;
361
371
  display: flex;
372
+ flex: 1 1 auto;
362
373
  flex-direction: column;
363
374
  gap: 0.35rem;
364
- max-height: min(22rem, 45vh);
375
+ min-height: 0;
365
376
  overflow: auto;
366
377
  }
367
378
 
@@ -483,6 +494,23 @@
483
494
  max-height: 12px;
484
495
  }
485
496
 
497
+ .workbench-field-remap-flow__canvas
498
+ .react-flow__controls-button.workbench-field-remap-flow__minimap-toggle
499
+ svg,
500
+ .workbench-field-remap-flow__canvas
501
+ .react-flow__controls-button.workbench-field-remap-flow__hidden-toggle
502
+ svg {
503
+ fill: none;
504
+ stroke: currentColor;
505
+ }
506
+
507
+ .workbench-field-remap-flow__canvas
508
+ .react-flow__controls-button.workbench-field-remap-flow__minimap-toggle.is-active,
509
+ .workbench-field-remap-flow__canvas
510
+ .react-flow__controls-button.workbench-field-remap-flow__hidden-toggle.is-active {
511
+ color: var(--vscode-focusBorder, var(--color-accent, #3794ff));
512
+ }
513
+
486
514
  .workbench-field-remap-flow__canvas .react-flow__minimap {
487
515
  margin: 0.5rem;
488
516
  border: 1px solid var(--vscode-panel-border, var(--color-border));
package/src/index.ts CHANGED
@@ -286,6 +286,14 @@ export {
286
286
  type WorkbenchSecondaryActivityItemsInput,
287
287
  type WorkbenchSecondaryActivityRoute,
288
288
  } from './shell/secondary-actions.js';
289
+ export {
290
+ defaultWorkbenchShellChromeLabels,
291
+ resolveWorkbenchShellChromeLabels,
292
+ workbenchShellChromeLabelKeys,
293
+ type WorkbenchI18n,
294
+ type WorkbenchShellChromeLabels,
295
+ type WorkbenchTranslate,
296
+ } from './shell/chrome-labels.js';
289
297
  export {
290
298
  WorkbenchProfileModal,
291
299
  type WorkbenchProfileDetail,
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Host-overridable chrome strings for WorkbenchShell surfaces
3
+ * (ActivityBar / StatusBar aria, secondary Profile/Settings, command palette,
4
+ * Quick Open).
5
+ *
6
+ * Resolution order per string: `labels[key]` → `t(capabilityId, default)` → English default.
7
+ * Kit does not ship locale packs — hosts inject `t` or partial `labels`.
8
+ */
9
+
10
+ export type WorkbenchTranslate = (
11
+ key: string,
12
+ fallback: string,
13
+ params?: Readonly<Record<string, string | number>>,
14
+ ) => string;
15
+
16
+ /** Optional host i18n bag (same `t` signature as Field Remap chrome). */
17
+ export interface WorkbenchI18n {
18
+ readonly t: WorkbenchTranslate;
19
+ }
20
+
21
+ export interface WorkbenchShellChromeLabels {
22
+ readonly activityBarAriaLabel: string;
23
+ readonly statusBarAriaLabel: string;
24
+ readonly profileLabel: string;
25
+ readonly profileTitle: string;
26
+ readonly settingsLabel: string;
27
+ readonly commandPaletteTitle: string;
28
+ readonly commandPalettePlaceholder: string;
29
+ readonly commandPaletteCloseLabel: string;
30
+ readonly commandPaletteEmptyLabel: string;
31
+ readonly quickOpenTitle: string;
32
+ readonly quickOpenPlaceholder: string;
33
+ readonly quickOpenCloseLabel: string;
34
+ readonly quickOpenEmptyLabel: string;
35
+ }
36
+
37
+ export const defaultWorkbenchShellChromeLabels: WorkbenchShellChromeLabels = {
38
+ activityBarAriaLabel: 'Activity bar',
39
+ statusBarAriaLabel: 'Status bar',
40
+ profileLabel: 'Profile',
41
+ profileTitle: 'Open profile',
42
+ settingsLabel: 'Settings',
43
+ commandPaletteTitle: 'Command Palette',
44
+ commandPalettePlaceholder: 'Search commands',
45
+ commandPaletteCloseLabel: 'Close command palette',
46
+ commandPaletteEmptyLabel: 'No commands match your search',
47
+ quickOpenTitle: 'Quick Open',
48
+ quickOpenPlaceholder: 'Search files by name',
49
+ quickOpenCloseLabel: 'Close Quick Open',
50
+ quickOpenEmptyLabel: 'No matching files',
51
+ };
52
+
53
+ /** Stable capability ids for optional `t()` injection (not free prose). */
54
+ export const workbenchShellChromeLabelKeys = {
55
+ activityBarAriaLabel: 'shell.activityBar',
56
+ statusBarAriaLabel: 'shell.statusBar',
57
+ profileLabel: 'shell.profile',
58
+ profileTitle: 'shell.profileTitle',
59
+ settingsLabel: 'shell.settings',
60
+ commandPaletteTitle: 'commandPalette.title',
61
+ commandPalettePlaceholder: 'commandPalette.placeholder',
62
+ commandPaletteCloseLabel: 'commandPalette.close',
63
+ commandPaletteEmptyLabel: 'commandPalette.empty',
64
+ quickOpenTitle: 'quickOpen.title',
65
+ quickOpenPlaceholder: 'quickOpen.placeholder',
66
+ quickOpenCloseLabel: 'quickOpen.close',
67
+ quickOpenEmptyLabel: 'quickOpen.empty',
68
+ } as const satisfies Record<keyof WorkbenchShellChromeLabels, string>;
69
+
70
+ export function resolveWorkbenchShellChromeLabels(
71
+ labels?: Partial<WorkbenchShellChromeLabels> | undefined,
72
+ t?: WorkbenchTranslate | undefined,
73
+ ): WorkbenchShellChromeLabels {
74
+ const resolve = <K extends keyof WorkbenchShellChromeLabels>(key: K): string => {
75
+ const override = labels?.[key];
76
+ if (override !== undefined) {
77
+ return override;
78
+ }
79
+ const fallback = defaultWorkbenchShellChromeLabels[key];
80
+ return t?.(workbenchShellChromeLabelKeys[key], fallback) ?? fallback;
81
+ };
82
+
83
+ return {
84
+ activityBarAriaLabel: resolve('activityBarAriaLabel'),
85
+ statusBarAriaLabel: resolve('statusBarAriaLabel'),
86
+ profileLabel: resolve('profileLabel'),
87
+ profileTitle: resolve('profileTitle'),
88
+ settingsLabel: resolve('settingsLabel'),
89
+ commandPaletteTitle: resolve('commandPaletteTitle'),
90
+ commandPalettePlaceholder: resolve('commandPalettePlaceholder'),
91
+ commandPaletteCloseLabel: resolve('commandPaletteCloseLabel'),
92
+ commandPaletteEmptyLabel: resolve('commandPaletteEmptyLabel'),
93
+ quickOpenTitle: resolve('quickOpenTitle'),
94
+ quickOpenPlaceholder: resolve('quickOpenPlaceholder'),
95
+ quickOpenCloseLabel: resolve('quickOpenCloseLabel'),
96
+ quickOpenEmptyLabel: resolve('quickOpenEmptyLabel'),
97
+ };
98
+ }
@@ -1,8 +1,13 @@
1
1
  import type { ReactNode } from 'react';
2
- import type { ActivityBarItem, StatusBarSectionModel } from '@workbench-kit/react/workbench/shell';
2
+ import type {
3
+ ActivityBarItem,
4
+ StatusBarItemModel,
5
+ StatusBarSectionModel,
6
+ } from '@workbench-kit/react/workbench/shell';
3
7
  import type {
4
8
  ExtensionDependencyDiagnosticSeverity,
5
9
  WorkbenchActivityContribution,
10
+ WorkbenchStatusBarContribution,
6
11
  WorkbenchViewContainerContribution,
7
12
  WorkbenchViewContribution,
8
13
  } from '@workbench-kit/workbench-core';
@@ -65,6 +70,63 @@ export function createWorkbenchShellActivityItems({
65
70
  });
66
71
  }
67
72
 
73
+ function compareStatusBarPriority(
74
+ left: WorkbenchStatusBarContribution,
75
+ right: WorkbenchStatusBarContribution,
76
+ ): number {
77
+ const leftPriority = left.priority ?? 0;
78
+ const rightPriority = right.priority ?? 0;
79
+ if (leftPriority !== rightPriority) {
80
+ return rightPriority - leftPriority;
81
+ }
82
+
83
+ return left.id.localeCompare(right.id);
84
+ }
85
+
86
+ function toStatusBarItemModel(item: WorkbenchStatusBarContribution): StatusBarItemModel {
87
+ return {
88
+ id: item.id,
89
+ label: item.text,
90
+ order: item.priority,
91
+ title: item.text,
92
+ };
93
+ }
94
+
95
+ /** Map extension `contributes.statusBar` items into shell status sections. */
96
+ export function createContributedWorkbenchStatusSections(
97
+ items: readonly WorkbenchStatusBarContribution[],
98
+ ): StatusBarSectionModel[] {
99
+ const leftItems = items
100
+ .filter((item) => item.alignment === 'left')
101
+ .sort(compareStatusBarPriority)
102
+ .map(toStatusBarItemModel);
103
+ const rightItems = items
104
+ .filter((item) => item.alignment === 'right')
105
+ .sort(compareStatusBarPriority)
106
+ .map(toStatusBarItemModel);
107
+
108
+ return [
109
+ ...(leftItems.length > 0
110
+ ? [
111
+ {
112
+ align: 'start' as const,
113
+ id: 'extension-status-left',
114
+ items: leftItems,
115
+ },
116
+ ]
117
+ : []),
118
+ ...(rightItems.length > 0
119
+ ? [
120
+ {
121
+ align: 'end' as const,
122
+ id: 'extension-status-right',
123
+ items: rightItems,
124
+ },
125
+ ]
126
+ : []),
127
+ ];
128
+ }
129
+
68
130
  export function createDefaultWorkbenchStatusSections({
69
131
  dependencyDiagnostics,
70
132
  extensionCount,
@@ -11,6 +11,10 @@ export interface WorkbenchSecondaryActivityItemsInput {
11
11
  isProfileOpen: boolean;
12
12
  isSettingsOpen: boolean;
13
13
  showSettings?: boolean | undefined;
14
+ /** Resolved chrome labels (defaults match English kit copy). */
15
+ profileLabel?: string | undefined;
16
+ profileTitle?: string | undefined;
17
+ settingsLabel?: string | undefined;
14
18
  }
15
19
 
16
20
  export function createWorkbenchSecondaryActivityItems({
@@ -18,6 +22,9 @@ export function createWorkbenchSecondaryActivityItems({
18
22
  isProfileOpen,
19
23
  isSettingsOpen,
20
24
  showSettings = true,
25
+ profileLabel = 'Profile',
26
+ profileTitle = 'Open profile',
27
+ settingsLabel = 'Settings',
21
28
  }: WorkbenchSecondaryActivityItemsInput): ActivityBarItem[] {
22
29
  return [
23
30
  ...(hasProfile
@@ -26,8 +33,8 @@ export function createWorkbenchSecondaryActivityItems({
26
33
  active: isProfileOpen,
27
34
  icon: <i aria-hidden="true" className="codicon codicon-account" />,
28
35
  id: WORKBENCH_PROFILE_ACTIVITY_ITEM_ID,
29
- label: 'Profile',
30
- title: 'Open profile',
36
+ label: profileLabel,
37
+ title: profileTitle,
31
38
  },
32
39
  ]
33
40
  : []),
@@ -37,7 +44,7 @@ export function createWorkbenchSecondaryActivityItems({
37
44
  active: isSettingsOpen,
38
45
  icon: <i aria-hidden="true" className="codicon codicon-settings-gear" />,
39
46
  id: WORKBENCH_SETTINGS_ACTIVITY_ITEM_ID,
40
- label: 'Settings',
47
+ label: settingsLabel,
41
48
  },
42
49
  ]
43
50
  : []),
@@ -35,6 +35,11 @@ import {
35
35
  sortActivityBarItems,
36
36
  } from '@workbench-kit/react/workbench/activityBarOrder';
37
37
  import { useWorkbench } from './provider.js';
38
+ import {
39
+ resolveWorkbenchShellChromeLabels,
40
+ type WorkbenchShellChromeLabels,
41
+ type WorkbenchTranslate,
42
+ } from './chrome-labels.js';
38
43
  import { WorkbenchCommandHost, type WorkbenchCommandHostProps } from '../workbench/command-host.js';
39
44
  import {
40
45
  MANAGE_ACCOUNTS_COMMAND_ID,
@@ -60,10 +65,12 @@ import {
60
65
  import { SETTINGS_EXTENSION_ID, WORKBENCH_PREFERENCE_SCOPES } from './settings-constants.js';
61
66
  import { createSettingsCategories, type WorkbenchThemeOption } from './settings.js';
62
67
  import {
68
+ createContributedWorkbenchStatusSections,
63
69
  createDefaultWorkbenchStatusSections,
64
70
  createWorkbenchShellActivityItems,
65
71
  } from './model.js';
66
- import { renderDefaultPrimarySidebar } from './view-host.js';
72
+ import { mergeWorkbenchStatusSections } from '../workbench/status-sections.js';
73
+ import { renderDefaultBottomPanel, renderDefaultPrimarySidebar } from './view-host.js';
67
74
  import { WorkbenchShellTitleBarLayoutControls } from './titlebar-layout-controls.js';
68
75
  import { WorkbenchProfileModal, type WorkbenchProfileInput } from '../workbench/profile-modal.js';
69
76
  import { useContextKeyRevision } from '../commands/use-context-key-revision.js';
@@ -102,6 +109,11 @@ export interface WorkbenchShellProps {
102
109
  editorArea?: ReactNode;
103
110
  helpContent?: ReactNode;
104
111
  helpTitle?: ReactNode;
112
+ /**
113
+ * Partial chrome label overrides for ActivityBar / StatusBar / secondary items /
114
+ * command palette. Wins over `t` when both are set for the same key.
115
+ */
116
+ labels?: Partial<WorkbenchShellChromeLabels> | undefined;
105
117
  lightPreset?: string | undefined;
106
118
  onDarkPresetChange?: ((preset: string) => void) | undefined;
107
119
  onLightPresetChange?: ((preset: string) => void) | undefined;
@@ -116,6 +128,11 @@ export interface WorkbenchShellProps {
116
128
  rootClassName?: string;
117
129
  shellPreset?: string | undefined;
118
130
  statusSections?: StatusBarSectionModel[];
131
+ /**
132
+ * Optional `t(key, fallback)` injection for shell chrome strings.
133
+ * Missing `t` keeps English defaults from `resolveWorkbenchShellChromeLabels`.
134
+ */
135
+ t?: WorkbenchTranslate | undefined;
119
136
  theme?: string;
120
137
  themeOptions?: readonly WorkbenchThemeOption[] | undefined;
121
138
  title?: ReactNode;
@@ -148,6 +165,7 @@ export function WorkbenchShell({
148
165
  editorArea,
149
166
  helpContent,
150
167
  helpTitle = 'Workbench Help',
168
+ labels: labelOverrides,
151
169
  lightPreset,
152
170
  locale = 'en',
153
171
  onDarkPresetChange,
@@ -162,6 +180,7 @@ export function WorkbenchShell({
162
180
  rootClassName,
163
181
  shellPreset = DEFAULT_SHELL_PRESET,
164
182
  statusSections,
183
+ t,
165
184
  theme,
166
185
  themeOptions,
167
186
  title = 'Workbench',
@@ -210,15 +229,20 @@ export function WorkbenchShell({
210
229
  const resolvedStatusSections = useMemo(
211
230
  () =>
212
231
  statusSections ??
213
- createDefaultWorkbenchStatusSections({
214
- dependencyDiagnostics: extensionRegistry.getDependencyDiagnostics(),
215
- extensionCount: extensionRegistry.getExtensions().length,
216
- missingExtensionIds,
217
- profile,
218
- }),
232
+ mergeWorkbenchStatusSections(
233
+ createDefaultWorkbenchStatusSections({
234
+ dependencyDiagnostics: extensionRegistry.getDependencyDiagnostics(),
235
+ extensionCount: extensionRegistry.getExtensions().length,
236
+ missingExtensionIds,
237
+ profile,
238
+ }),
239
+ createContributedWorkbenchStatusSections(extensionRegistry.statusBar.getStatusBarItems()),
240
+ ),
219
241
  [extensionRegistry, missingExtensionIds, profile, statusSections],
220
242
  );
221
243
  const activeViewContainerId = layout.sideBar.activeViewContainer;
244
+ const activePanelViewContainerId = layout.panel.activeViewContainer;
245
+ const panelViewContainers = extensionRegistry.views.getViewContainers('panel');
222
246
  const visibleActivities = useMemo(
223
247
  () =>
224
248
  filterActivitiesByWhenClause(
@@ -231,7 +255,7 @@ export function WorkbenchShell({
231
255
  createWorkbenchShellActivityItems({
232
256
  activeViewContainerId,
233
257
  activities: visibleActivities,
234
- viewContainers: extensionRegistry.views.getViewContainers(),
258
+ viewContainers: extensionRegistry.views.getViewContainers('activitybar'),
235
259
  views: extensionRegistry.views.getViews(),
236
260
  }),
237
261
  layout.activityBar.itemOrder,
@@ -243,11 +267,18 @@ export function WorkbenchShell({
243
267
  const canOpenSettingsValue = contextKeyService.get(
244
268
  WORKBENCH_PERMISSION_CONTEXT_KEY_CAN_OPEN_SETTINGS,
245
269
  );
270
+ const chromeLabels = useMemo(
271
+ () => resolveWorkbenchShellChromeLabels(labelOverrides, t),
272
+ [labelOverrides, t],
273
+ );
246
274
  const secondaryActivityItems = createWorkbenchSecondaryActivityItems({
247
275
  hasProfile: profile !== undefined,
248
276
  isProfileOpen,
249
277
  isSettingsOpen,
250
278
  showSettings: canOpenSettingsValue !== false,
279
+ profileLabel: chromeLabels.profileLabel,
280
+ profileTitle: chromeLabels.profileTitle,
281
+ settingsLabel: chromeLabels.settingsLabel,
251
282
  });
252
283
 
253
284
  useEffect(() => {
@@ -260,6 +291,20 @@ export function WorkbenchShell({
260
291
  layoutService.setActiveViewContainer(visibleActivityItems[0]?.id);
261
292
  }
262
293
  }, [activeViewContainerId, layoutService, visibleActivityItems]);
294
+
295
+ useEffect(() => {
296
+ if (panelViewContainers.length === 0) {
297
+ return;
298
+ }
299
+
300
+ const panelContainerIds = new Set(panelViewContainers.map((container) => container.id));
301
+ if (
302
+ activePanelViewContainerId === undefined ||
303
+ !panelContainerIds.has(activePanelViewContainerId)
304
+ ) {
305
+ layoutService.setActivePanelViewContainer(panelViewContainers[0]?.id);
306
+ }
307
+ }, [activePanelViewContainerId, layoutService, panelViewContainers]);
263
308
  const settingsCategories = useMemo(() => {
264
309
  const managementCategories: WorkbenchSettingsCategory[] = [];
265
310
 
@@ -389,6 +434,16 @@ export function WorkbenchShell({
389
434
  }
390
435
  }, [activeViewContainerId, extensionRegistry, forceRender]);
391
436
 
437
+ useEffect(() => {
438
+ if (!activePanelViewContainerId) {
439
+ return;
440
+ }
441
+
442
+ for (const view of extensionRegistry.views.getViews(activePanelViewContainerId)) {
443
+ void extensionRegistry.activateView(view.id).then(forceRender);
444
+ }
445
+ }, [activePanelViewContainerId, extensionRegistry, forceRender]);
446
+
392
447
  useEffect(() => {
393
448
  if (extensionRegistry.capabilityRegistry.has(WORKBENCH_SETTINGS_CAPABILITY_ID)) {
394
449
  return undefined;
@@ -429,6 +484,17 @@ export function WorkbenchShell({
429
484
  return {
430
485
  ...hostProps,
431
486
  additionalCommands,
487
+ commandPaletteCloseLabel:
488
+ hostProps.commandPaletteCloseLabel ?? chromeLabels.commandPaletteCloseLabel,
489
+ commandPaletteEmptyLabel:
490
+ hostProps.commandPaletteEmptyLabel ?? chromeLabels.commandPaletteEmptyLabel,
491
+ commandPalettePlaceholder:
492
+ hostProps.commandPalettePlaceholder ?? chromeLabels.commandPalettePlaceholder,
493
+ commandPaletteTitle: hostProps.commandPaletteTitle ?? chromeLabels.commandPaletteTitle,
494
+ quickOpenCloseLabel: hostProps.quickOpenCloseLabel ?? chromeLabels.quickOpenCloseLabel,
495
+ quickOpenEmptyLabel: hostProps.quickOpenEmptyLabel ?? chromeLabels.quickOpenEmptyLabel,
496
+ quickOpenPlaceholder: hostProps.quickOpenPlaceholder ?? chromeLabels.quickOpenPlaceholder,
497
+ quickOpenTitle: hostProps.quickOpenTitle ?? chromeLabels.quickOpenTitle,
432
498
  onRunCommand: (command, context) => {
433
499
  if (command.id === MANAGE_COMMANDS_COMMAND_ID) {
434
500
  layoutService.setActiveViewContainer(BUILTIN_COMMANDS_VIEW_CONTAINER_ID);
@@ -455,7 +521,7 @@ export function WorkbenchShell({
455
521
  return hostProps.onRunCommand?.(command, context) ?? false;
456
522
  },
457
523
  };
458
- }, [commandHost, layoutService]);
524
+ }, [chromeLabels, commandHost, layoutService]);
459
525
 
460
526
  const handleStatusItemActivate = useCallback(
461
527
  (item: StatusBarItemModel) => {
@@ -473,14 +539,28 @@ export function WorkbenchShell({
473
539
  return;
474
540
  }
475
541
 
542
+ const contributed = extensionRegistry.statusBar.getStatusBarItem(item.id);
543
+ if (contributed?.command) {
544
+ void executeCommand(contributed.command).catch(() => undefined);
545
+ return;
546
+ }
547
+
476
548
  onStatusItemActivate?.(item);
477
549
  },
478
- [accountManagement, onStatusItemActivate, profile, showProfileModal],
550
+ [
551
+ accountManagement,
552
+ executeCommand,
553
+ extensionRegistry,
554
+ onStatusItemActivate,
555
+ profile,
556
+ showProfileModal,
557
+ ],
479
558
  );
480
559
 
481
560
  return (
482
561
  <ReactWorkbenchShell
483
562
  activityBar={{
563
+ 'aria-label': chromeLabels.activityBarAriaLabel,
484
564
  visible: layout.activityBar.visible,
485
565
  items: visibleActivityItems,
486
566
  reorderable: true,
@@ -506,6 +586,7 @@ export function WorkbenchShell({
506
586
  },
507
587
  }}
508
588
  compactStatus={compactStatus}
589
+ statusBarAriaLabel={chromeLabels.statusBarAriaLabel}
509
590
  onStatusItemActivate={handleStatusItemActivate}
510
591
  primarySidebar={{
511
592
  isVisible: layout.sideBar.visible,
@@ -536,7 +617,20 @@ export function WorkbenchShell({
536
617
  }}
537
618
  bottomPanel={{
538
619
  isVisible: layout.panel.visible,
539
- node: <section aria-label="Panel" className="workbench-bottom-panel" />,
620
+ node: renderDefaultBottomPanel(extensionRegistry, activePanelViewContainerId, {
621
+ catalogTrustPolicy,
622
+ catalogUrl,
623
+ onActiveViewContainerChange: (viewContainerId) => {
624
+ layoutService.setActivePanelViewContainer(viewContainerId);
625
+ if (!layout.panel.visible) {
626
+ layoutService.setPanelVisible(true);
627
+ }
628
+ },
629
+ }),
630
+ onSizePercentChange: (sizePercent) => {
631
+ layoutService.setPanelSizePercent(sizePercent);
632
+ },
633
+ sizePercent: layout.panel.sizePercent,
540
634
  }}
541
635
  rootClassName={rootClassName}
542
636
  secondaryArea={resolvedEditorArea}
@@ -563,7 +657,7 @@ export function WorkbenchShell({
563
657
  footer={<Button onClick={() => setSettingsOpen(false)}>Close</Button>}
564
658
  scopes={[...WORKBENCH_PREFERENCE_SCOPES]}
565
659
  searchValue={settingsSearchValue}
566
- title="Settings"
660
+ title={chromeLabels.settingsLabel}
567
661
  titleSuffix={
568
662
  <Badge variant="muted">
569
663
  {settingsContributionCount === 1
@@ -56,6 +56,79 @@ export function renderDefaultPrimarySidebar(
56
56
  );
57
57
  }
58
58
 
59
+ export function renderDefaultBottomPanel(
60
+ extensionRegistry: Pick<ExtensionRegistry, 'viewHostFactories' | 'views'>,
61
+ activeViewContainerId: string | undefined,
62
+ options: {
63
+ catalogTrustPolicy?: ExtensionCatalogTrustPolicy | undefined;
64
+ catalogUrl?: string | undefined;
65
+ onActiveViewContainerChange?: ((viewContainerId: string) => void) | undefined;
66
+ } = {},
67
+ ) {
68
+ const containers = extensionRegistry.views.getViewContainers('panel');
69
+ if (containers.length === 0) {
70
+ return (
71
+ <section aria-label="Panel" className="workbench-bottom-panel">
72
+ <div className="workbench-bottom-panel__empty">No panel views contributed.</div>
73
+ </section>
74
+ );
75
+ }
76
+
77
+ const resolvedActiveViewContainerId =
78
+ activeViewContainerId !== undefined &&
79
+ containers.some((container) => container.id === activeViewContainerId)
80
+ ? activeViewContainerId
81
+ : containers[0]?.id;
82
+ const views = resolvedActiveViewContainerId
83
+ ? extensionRegistry.views.getViews(resolvedActiveViewContainerId)
84
+ : [];
85
+
86
+ return (
87
+ <section aria-label="Panel" className="workbench-bottom-panel">
88
+ <div className="workbench-bottom-panel__header" role="tablist" aria-label="Panel views">
89
+ {containers.map((container) => {
90
+ const isActive = container.id === resolvedActiveViewContainerId;
91
+ return (
92
+ <button
93
+ key={container.id}
94
+ type="button"
95
+ role="tab"
96
+ aria-selected={isActive}
97
+ className={
98
+ isActive
99
+ ? 'workbench-bottom-panel__tab workbench-bottom-panel__tab--active'
100
+ : 'workbench-bottom-panel__tab'
101
+ }
102
+ data-panel-view-container-id={container.id}
103
+ onClick={() => options.onActiveViewContainerChange?.(container.id)}
104
+ >
105
+ {container.title}
106
+ </button>
107
+ );
108
+ })}
109
+ </div>
110
+ <div className="workbench-bottom-panel__body">
111
+ {views.length === 0 ? (
112
+ <div className="workbench-bottom-panel__empty">No views in this panel container.</div>
113
+ ) : (
114
+ views.map((view) => (
115
+ <section key={view.id} data-view-id={view.id} className="workbench-bottom-panel__view">
116
+ <WorkbenchViewHost
117
+ catalogTrustPolicy={options.catalogTrustPolicy}
118
+ catalogUrl={options.catalogUrl}
119
+ fallback={view.name}
120
+ provider={extensionRegistry.views.getViewProvider(view.id)}
121
+ viewHostFactories={extensionRegistry.viewHostFactories}
122
+ viewId={view.id}
123
+ />
124
+ </section>
125
+ ))
126
+ )}
127
+ </div>
128
+ </section>
129
+ );
130
+ }
131
+
59
132
  export function WorkbenchViewHost({
60
133
  catalogTrustPolicy,
61
134
  catalogUrl,
@@ -4,8 +4,14 @@ import {
4
4
  } from '@workbench-kit/platform';
5
5
  import {
6
6
  WorkbenchCommandPalette,
7
+ WorkbenchQuickOpen,
7
8
  WorkbenchShortcutCommandBridge,
8
9
  createWorkbenchShellCommands,
10
+ createWorkspaceFilesQuickOpenProvider,
11
+ resolveQuickOpenItemPath,
12
+ type QuickOpenItem,
13
+ type QuickOpenProvider,
14
+ type QuickOpenSelectContext,
9
15
  type WorkbenchCommandDescriptor,
10
16
  type WorkbenchCommandRunContext,
11
17
  type WorkbenchShellCommandContext,
@@ -22,33 +28,94 @@ import {
22
28
  resolveShellCommandActivities,
23
29
  } from './command-palette.js';
24
30
  import { resolveExtensionKeybindingCommand } from './keybinding-bridge.js';
31
+ import { isWorkspaceResourceService, useWorkspaceResourceState } from './workspace-view-state.js';
32
+
33
+ const WORKSPACE_OPEN_COMMAND_ID = 'workspace.open' as const;
25
34
 
26
35
  export interface WorkbenchCommandHostProps {
27
36
  additionalCommands?: readonly WorkbenchCommandDescriptor[];
37
+ /** Override command palette close control label (default English). */
38
+ commandPaletteCloseLabel?: string | undefined;
39
+ /** Override command palette empty-state copy (default English). */
40
+ commandPaletteEmptyLabel?: string | undefined;
41
+ /** Override command palette search placeholder (default English). */
42
+ commandPalettePlaceholder?: string | undefined;
43
+ /** Override command palette dialog title (default English). */
44
+ commandPaletteTitle?: string | undefined;
28
45
  enableCommandPalette?: boolean;
29
46
  enableExtensionKeybindings?: boolean;
47
+ /**
48
+ * When true (default), Ctrl/Cmd+P opens Quick Open instead of the command palette.
49
+ * Ctrl/Cmd+Shift+P still opens the command palette.
50
+ */
51
+ enableQuickOpen?: boolean;
30
52
  enableShortcutBridge?: boolean;
31
53
  onOpenSettings: () => void;
54
+ /**
55
+ * Called when a Quick Open item is selected. Return `true` to skip the default
56
+ * `workspace.open` path for file items.
57
+ */
58
+ onOpenQuickOpenItem?: (item: QuickOpenItem, context: QuickOpenSelectContext) => boolean | void;
32
59
  onRunCommand?: (
33
60
  command: WorkbenchCommandDescriptor,
34
61
  context: WorkbenchCommandRunContext,
35
62
  ) => boolean | void;
63
+ /** Override Quick Open close control label (default English). */
64
+ quickOpenCloseLabel?: string | undefined;
65
+ /** Override Quick Open empty-state copy (default English). */
66
+ quickOpenEmptyLabel?: string | undefined;
67
+ /** Override Quick Open search placeholder (default English). */
68
+ quickOpenPlaceholder?: string | undefined;
69
+ /**
70
+ * Extra / replacement Quick Open providers. When omitted, the host wires a
71
+ * workspace-files provider from the registered workspace host port.
72
+ */
73
+ quickOpenProviders?: readonly QuickOpenProvider[];
74
+ /** Override Quick Open dialog title (default English). */
75
+ quickOpenTitle?: string | undefined;
76
+ /** Optional recent paths elevated when the Quick Open query is empty. */
77
+ quickOpenRecentPaths?: readonly string[] | undefined;
36
78
  }
37
79
 
38
80
  export function WorkbenchCommandHost({
39
81
  additionalCommands = [],
82
+ commandPaletteCloseLabel = 'Close command palette',
83
+ commandPaletteEmptyLabel = 'No commands match your search',
84
+ commandPalettePlaceholder = 'Search commands',
85
+ commandPaletteTitle = 'Command Palette',
40
86
  enableCommandPalette = true,
41
87
  enableExtensionKeybindings = true,
88
+ enableQuickOpen = true,
42
89
  enableShortcutBridge = true,
43
90
  onOpenSettings,
91
+ onOpenQuickOpenItem,
44
92
  onRunCommand,
93
+ quickOpenCloseLabel = 'Close Quick Open',
94
+ quickOpenEmptyLabel = 'No matching files',
95
+ quickOpenPlaceholder = 'Search files by name',
96
+ quickOpenProviders,
97
+ quickOpenRecentPaths,
98
+ quickOpenTitle = 'Quick Open',
45
99
  }: WorkbenchCommandHostProps) {
46
- const { executeCommand, extensionRegistry, keybindingOverrides, layoutService } = useWorkbench();
100
+ const {
101
+ executeCommand,
102
+ extensionRegistry,
103
+ keybindingOverrides,
104
+ layoutService,
105
+ workspaceHostPort,
106
+ } = useWorkbench();
47
107
  const [paletteOpen, setPaletteOpen] = useState(false);
48
108
  const [paletteQuery, setPaletteQuery] = useState('');
109
+ const [quickOpenOpen, setQuickOpenOpen] = useState(false);
110
+ const [quickOpenQuery, setQuickOpenQuery] = useState('');
49
111
  const [layout, setLayout] = useState(() => layoutService.getState());
50
112
  const shellContextRef = useRef<WorkbenchShellCommandContext | undefined>(undefined);
51
113
 
114
+ const workspaceService = isWorkspaceResourceService(workspaceHostPort?.service)
115
+ ? workspaceHostPort.service
116
+ : undefined;
117
+ const workspaceState = useWorkspaceResourceState(workspaceService);
118
+
52
119
  useEffect(() => {
53
120
  const disposable = layoutService.onDidChangeLayout(({ state }) => {
54
121
  setLayout(state);
@@ -127,18 +194,43 @@ export function WorkbenchCommandHost({
127
194
  shellCommands: shellCommandDefinitions,
128
195
  shellContext,
129
196
  }),
130
- [additionalCommands, extensionRegistry, shellContext],
197
+ [additionalCommands, extensionRegistry, shellContext, shellCommandDefinitions],
131
198
  );
132
199
 
200
+ const resolvedQuickOpenProviders = useMemo(() => {
201
+ if (quickOpenProviders) {
202
+ return quickOpenProviders;
203
+ }
204
+
205
+ const files = workspaceState?.files ?? workspaceService?.getState().files ?? [];
206
+ return [
207
+ createWorkspaceFilesQuickOpenProvider({
208
+ files: () => workspaceService?.getState().files ?? files,
209
+ recentPaths: quickOpenRecentPaths,
210
+ }),
211
+ ];
212
+ }, [quickOpenProviders, quickOpenRecentPaths, workspaceService, workspaceState?.files]);
213
+
133
214
  const closePalette = useCallback(() => {
134
215
  setPaletteOpen(false);
135
216
  }, []);
136
217
 
137
218
  const openPalette = useCallback((query = '') => {
219
+ setQuickOpenOpen(false);
138
220
  setPaletteQuery(query);
139
221
  setPaletteOpen(true);
140
222
  }, []);
141
223
 
224
+ const closeQuickOpen = useCallback(() => {
225
+ setQuickOpenOpen(false);
226
+ }, []);
227
+
228
+ const openQuickOpen = useCallback((query = '') => {
229
+ setPaletteOpen(false);
230
+ setQuickOpenQuery(query);
231
+ setQuickOpenOpen(true);
232
+ }, []);
233
+
142
234
  const runPaletteCommand = useCallback(
143
235
  (command: WorkbenchCommandDescriptor, context: WorkbenchCommandRunContext) => {
144
236
  const finish = () => {
@@ -155,13 +247,35 @@ export function WorkbenchCommandHost({
155
247
  [closePalette, executeCommand, onRunCommand],
156
248
  );
157
249
 
250
+ const runQuickOpenItem = useCallback(
251
+ (item: QuickOpenItem, context: QuickOpenSelectContext) => {
252
+ const finish = () => {
253
+ closeQuickOpen();
254
+ };
255
+
256
+ if (onOpenQuickOpenItem?.(item, context)) {
257
+ finish();
258
+ return;
259
+ }
260
+
261
+ const path = resolveQuickOpenItemPath(item);
262
+ if (!path) {
263
+ finish();
264
+ return;
265
+ }
266
+
267
+ void executeCommand(WORKSPACE_OPEN_COMMAND_ID, { path }).finally(finish);
268
+ },
269
+ [closeQuickOpen, executeCommand, onOpenQuickOpenItem],
270
+ );
271
+
158
272
  useEffect(() => {
159
- if (!enableCommandPalette) {
273
+ if (!enableCommandPalette && !enableQuickOpen) {
160
274
  return undefined;
161
275
  }
162
276
 
163
277
  const onKeyDown = (event: KeyboardEvent) => {
164
- if (matchesWorkbenchCommandPaletteShortcut(event)) {
278
+ if (enableCommandPalette && matchesWorkbenchCommandPaletteShortcut(event)) {
165
279
  event.preventDefault();
166
280
  openPalette('>');
167
281
  return;
@@ -169,7 +283,14 @@ export function WorkbenchCommandHost({
169
283
 
170
284
  if (matchesWorkbenchQuickAccessShortcut(event)) {
171
285
  event.preventDefault();
172
- openPalette();
286
+ if (enableQuickOpen) {
287
+ openQuickOpen();
288
+ return;
289
+ }
290
+
291
+ if (enableCommandPalette) {
292
+ openPalette();
293
+ }
173
294
  }
174
295
  };
175
296
 
@@ -177,7 +298,7 @@ export function WorkbenchCommandHost({
177
298
  return () => {
178
299
  window.removeEventListener('keydown', onKeyDown);
179
300
  };
180
- }, [enableCommandPalette, openPalette]);
301
+ }, [enableCommandPalette, enableQuickOpen, openPalette, openQuickOpen]);
181
302
 
182
303
  useEffect(() => {
183
304
  if (!enableExtensionKeybindings) {
@@ -228,16 +349,32 @@ export function WorkbenchCommandHost({
228
349
  ) : null}
229
350
  {enableCommandPalette ? (
230
351
  <WorkbenchCommandPalette
352
+ closeLabel={commandPaletteCloseLabel}
231
353
  commands={paletteCommands}
354
+ emptyLabel={commandPaletteEmptyLabel}
232
355
  open={paletteOpen}
233
- placeholder="Search commands"
356
+ placeholder={commandPalettePlaceholder}
234
357
  query={paletteQuery}
235
- title="Command Palette"
358
+ title={commandPaletteTitle}
236
359
  onClose={closePalette}
237
360
  onQueryChange={setPaletteQuery}
238
361
  onRunCommand={runPaletteCommand}
239
362
  />
240
363
  ) : null}
364
+ {enableQuickOpen ? (
365
+ <WorkbenchQuickOpen
366
+ closeLabel={quickOpenCloseLabel}
367
+ emptyLabel={quickOpenEmptyLabel}
368
+ open={quickOpenOpen}
369
+ placeholder={quickOpenPlaceholder}
370
+ providers={resolvedQuickOpenProviders}
371
+ query={quickOpenQuery}
372
+ title={quickOpenTitle}
373
+ onClose={closeQuickOpen}
374
+ onQueryChange={setQuickOpenQuery}
375
+ onSelectItem={runQuickOpenItem}
376
+ />
377
+ ) : null}
241
378
  </>
242
379
  );
243
380
  }
@@ -30,6 +30,8 @@ export function workbenchLayoutConfigToInput(
30
30
  visible: config.auxiliaryBar.visible,
31
31
  },
32
32
  panel: {
33
+ activeViewContainer: config.panel.activeViewContainer,
34
+ sizePercent: config.panel.sizePercent,
33
35
  visible: config.panel.visible,
34
36
  },
35
37
  sideBar: {
@@ -58,6 +60,10 @@ export function workbenchLayoutStateToStorageValue(
58
60
  },
59
61
  panel: {
60
62
  visible: state.panel.visible,
63
+ ...(state.panel.activeViewContainer
64
+ ? { activeViewContainer: state.panel.activeViewContainer }
65
+ : {}),
66
+ ...(state.panel.sizePercent !== undefined ? { sizePercent: state.panel.sizePercent } : {}),
61
67
  },
62
68
  sideBar: {
63
69
  visible: state.sideBar.visible,