@workbench-kit/shell-react 0.0.2-prototype.0.2.13 → 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
@@ -92,6 +92,21 @@ const flowActionsRef = useRef<FieldRemapFlowActions | null>(null);
92
92
  />;
93
93
  ```
94
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
+
95
110
  ## Related docs
96
111
 
97
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.13",
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/field-remap": "0.0.2-prototype.0.2.13",
25
- "@workbench-kit/react": "0.0.2-prototype.0.2.13",
26
- "@workbench-kit/platform": "0.0.2-prototype.0.2.13",
27
- "@workbench-kit/tokens": "0.0.2-prototype.0.2.13",
28
- "@workbench-kit/workbench-core": "0.0.2-prototype.0.2.13",
29
- "@workbench-kit/workbench-config": "0.0.2-prototype.0.2.13",
30
- "@workbench-kit/workspace": "0.0.2-prototype.0.2.13"
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
  }
@@ -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
 
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,