@workbench-kit/shell-react 0.0.2-prototype.0.2.6

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 (93) hide show
  1. package/package.json +49 -0
  2. package/src/chat-ai-mock-proposals.ts +39 -0
  3. package/src/chat-command-input.ts +52 -0
  4. package/src/chat-command-policy.ts +54 -0
  5. package/src/chat-command-surface.tsx +155 -0
  6. package/src/chat-view-data.ts +20 -0
  7. package/src/chat-view.tsx +220 -0
  8. package/src/command-inspector-surface.tsx +46 -0
  9. package/src/commands-view-data.ts +17 -0
  10. package/src/commands-view.tsx +43 -0
  11. package/src/createCommandWorkspaceExplorerPort.ts +86 -0
  12. package/src/devtools/WorkbenchDevtoolsPanel.tsx +124 -0
  13. package/src/devtools/WorkbenchDevtoolsShell.tsx +17 -0
  14. package/src/devtools/index.ts +11 -0
  15. package/src/devtools/use-workbench-devtools-snapshot.ts +84 -0
  16. package/src/devtools/workbench-devtools-snapshot.ts +140 -0
  17. package/src/devtools/workbench-devtools.css +82 -0
  18. package/src/editor-area-dnd.ts +69 -0
  19. package/src/editor-area-model.ts +71 -0
  20. package/src/editor-area.css +454 -0
  21. package/src/editor-area.tsx +676 -0
  22. package/src/editor-host-surface.tsx +634 -0
  23. package/src/editor-pane-visibility.ts +130 -0
  24. package/src/editor-resource.ts +36 -0
  25. package/src/editor-state-storage.ts +193 -0
  26. package/src/editor-tab-context-menu.ts +118 -0
  27. package/src/editor-view-providers.tsx +552 -0
  28. package/src/editor-workspace-file-availability.ts +15 -0
  29. package/src/editor-workspace-reconcile.tsx +28 -0
  30. package/src/explorer-context-menu.ts +114 -0
  31. package/src/explorer-reveal.ts +112 -0
  32. package/src/explorer-view-data.ts +58 -0
  33. package/src/explorer-view.tsx +224 -0
  34. package/src/extension-context-menu.ts +52 -0
  35. package/src/extension-management-model.ts +313 -0
  36. package/src/extensions-view-data.ts +19 -0
  37. package/src/extensions-view.tsx +38 -0
  38. package/src/field-remap-demo.tsx +18 -0
  39. package/src/field-remap-editor-surface.tsx +36 -0
  40. package/src/field-remap-flow-adapter.ts +240 -0
  41. package/src/field-remap-flow.tsx +369 -0
  42. package/src/field-remap-graph.tsx +559 -0
  43. package/src/field-remap-panel.tsx +175 -0
  44. package/src/field-remap-samples.ts +352 -0
  45. package/src/field-remap-tree.tsx +404 -0
  46. package/src/field-remap-view-data.ts +16 -0
  47. package/src/field-remap-view.css +676 -0
  48. package/src/field-remap-view.tsx +86 -0
  49. package/src/index.ts +199 -0
  50. package/src/jdw-lab-view-data.ts +25 -0
  51. package/src/jdw-lab-view.css +21 -0
  52. package/src/jdw-lab-view.tsx +79 -0
  53. package/src/jdw-widget-form-view.tsx +65 -0
  54. package/src/jdw-widget-preview-view.tsx +51 -0
  55. package/src/json-form-source-patch.ts +323 -0
  56. package/src/jsonata-transform.ts +36 -0
  57. package/src/keybinding-management-settings.tsx +16 -0
  58. package/src/keybinding-overrides-storage.ts +57 -0
  59. package/src/management-palette-commands.ts +38 -0
  60. package/src/management-settings-ids.ts +12 -0
  61. package/src/management-settings.tsx +75 -0
  62. package/src/preference-settings-storage.ts +58 -0
  63. package/src/provider.tsx +573 -0
  64. package/src/search-view-data.ts +15 -0
  65. package/src/search-view.tsx +62 -0
  66. package/src/shell-model.tsx +149 -0
  67. package/src/shell-secondary-actions.tsx +59 -0
  68. package/src/shell-settings-constants.ts +9 -0
  69. package/src/shell-settings.tsx +599 -0
  70. package/src/shell-titlebar-layout-controls.tsx +4 -0
  71. package/src/shell-view-host.tsx +199 -0
  72. package/src/shell.tsx +649 -0
  73. package/src/table-mapping-adapter.ts +59 -0
  74. package/src/use-active-workspace-path.ts +15 -0
  75. package/src/use-command-management.ts +191 -0
  76. package/src/use-context-key-revision.ts +18 -0
  77. package/src/use-editor.ts +107 -0
  78. package/src/use-extension-management.ts +193 -0
  79. package/src/use-keybinding-management.ts +130 -0
  80. package/src/use-persisted-workbench-appearance.ts +33 -0
  81. package/src/use-workbench-chat-command-proposals.ts +194 -0
  82. package/src/use-workbench-command-descriptors.ts +41 -0
  83. package/src/workbench-appearance-storage.ts +115 -0
  84. package/src/workbench-command-host.tsx +243 -0
  85. package/src/workbench-command-palette.ts +208 -0
  86. package/src/workbench-keybinding-bridge.ts +54 -0
  87. package/src/workbench-layout-storage.ts +145 -0
  88. package/src/workbench-profile-modal.tsx +118 -0
  89. package/src/workbench-shell-command-registration.ts +58 -0
  90. package/src/workbench-startup-gate.tsx +136 -0
  91. package/src/workbench-status-sections.ts +43 -0
  92. package/src/workbench-user-commands.ts +46 -0
  93. package/src/workspace-view-state.ts +35 -0
@@ -0,0 +1,208 @@
1
+ import type { CommandDefinition } from '@workbench-kit/platform';
2
+ import type { WorkbenchCommandDescriptor } from '@workbench-kit/react/workbench';
3
+ import type {
4
+ WorkbenchShellCommandActivity,
5
+ WorkbenchShellCommandContext,
6
+ } from '@workbench-kit/react/workbench';
7
+ import type { ExtensionCommandFeatureSpec, ExtensionRegistry } from '@workbench-kit/workbench-core';
8
+
9
+ export const WORKBENCH_COMMAND_PALETTE_SHORTCUT = 'Ctrl+Shift+P';
10
+ export const WORKBENCH_QUICK_ACCESS_SHORTCUT = 'Ctrl+P';
11
+
12
+ type ExtensionCommand = ReturnType<ExtensionRegistry['commands']['getCommands']>[number];
13
+
14
+ function resolveCommandValue<TContext, TValue>(
15
+ value: CommandDefinition<TContext>[keyof CommandDefinition<TContext>],
16
+ context: TContext,
17
+ ): TValue | undefined {
18
+ if (value === undefined) {
19
+ return undefined;
20
+ }
21
+
22
+ if (typeof value === 'function') {
23
+ return (value as (ctx: TContext) => TValue)(context);
24
+ }
25
+
26
+ return value as TValue;
27
+ }
28
+
29
+ export function formatWorkbenchCommandIcon(icon: string | undefined): string | undefined {
30
+ if (!icon) {
31
+ return undefined;
32
+ }
33
+
34
+ return icon.startsWith('codicon-') ? icon : `codicon-${icon}`;
35
+ }
36
+
37
+ export function resolveExtensionCommandIcon(icon: ExtensionCommand['icon']): string | undefined {
38
+ if (typeof icon === 'function') {
39
+ return formatWorkbenchCommandIcon(icon(undefined as void));
40
+ }
41
+
42
+ return formatWorkbenchCommandIcon(icon);
43
+ }
44
+
45
+ export function resolveShellCommandActivities(
46
+ extensionRegistry: ExtensionRegistry,
47
+ ): WorkbenchShellCommandActivity[] {
48
+ const activities = extensionRegistry.activities.getActivities();
49
+ if (activities.length > 0) {
50
+ return activities
51
+ .map((activity) => ({
52
+ icon: formatWorkbenchCommandIcon(activity.icon),
53
+ id: activity.viewContainerId,
54
+ label: `Show ${activity.title}`,
55
+ }))
56
+ .sort((left, right) => left.label.localeCompare(right.label));
57
+ }
58
+
59
+ const viewContainerIds = new Set(
60
+ extensionRegistry.views.getViews().map((view) => view.containerId),
61
+ );
62
+
63
+ return [...viewContainerIds].map((containerId) => {
64
+ const container = extensionRegistry.views.getViewContainer(containerId);
65
+ const firstView = extensionRegistry.views.getViews(containerId)[0];
66
+
67
+ return {
68
+ icon: formatWorkbenchCommandIcon(container?.icon ?? 'files'),
69
+ id: containerId,
70
+ label: `Show ${container?.title ?? firstView?.name ?? containerId}`,
71
+ };
72
+ });
73
+ }
74
+
75
+ export function shellCommandDefinitionToDescriptor<TContext>(
76
+ command: CommandDefinition<TContext>,
77
+ context: TContext,
78
+ ): WorkbenchCommandDescriptor {
79
+ const label =
80
+ resolveCommandValue<TContext, string>(command.label, context) ?? command.title ?? command.id;
81
+ const disabled = command.isEnabled ? !command.isEnabled(context) : false;
82
+
83
+ return {
84
+ category: command.category ?? 'View',
85
+ icon: formatWorkbenchCommandIcon(resolveCommandValue(command.icon, context)),
86
+ id: command.id,
87
+ label,
88
+ shortcut: resolveCommandValue(command.shortcut, context),
89
+ disabled,
90
+ };
91
+ }
92
+
93
+ export function extensionCommandToDescriptor(
94
+ command: ExtensionCommand,
95
+ feature?: ExtensionCommandFeatureSpec | undefined,
96
+ ): WorkbenchCommandDescriptor {
97
+ const metadata = {
98
+ argsSchema: feature?.argsSchema,
99
+ chat: feature?.chat,
100
+ requiresApproval: feature?.requiresApproval,
101
+ };
102
+
103
+ return {
104
+ category: command.category ?? feature?.category,
105
+ danger: feature?.danger,
106
+ description: feature?.description,
107
+ icon: resolveExtensionCommandIcon(command.icon),
108
+ id: command.id,
109
+ keywords: feature
110
+ ? [feature.command, feature.title].filter((value) => value !== command.id)
111
+ : undefined,
112
+ label: command.title ?? feature?.title ?? command.id,
113
+ metadata:
114
+ metadata.argsSchema || metadata.chat || metadata.requiresApproval !== undefined
115
+ ? metadata
116
+ : undefined,
117
+ };
118
+ }
119
+
120
+ export function collectExtensionCommandFeaturesById(
121
+ extensionRegistry: ExtensionRegistry,
122
+ ): ReadonlyMap<string, ExtensionCommandFeatureSpec> {
123
+ return new Map(
124
+ extensionRegistry
125
+ .getFeatureSpecs()
126
+ .flatMap((feature) => feature.commands.map((command) => [command.id, command] as const)),
127
+ );
128
+ }
129
+
130
+ export function mergeWorkbenchCommandDescriptors(
131
+ ...groups: readonly WorkbenchCommandDescriptor[][]
132
+ ): WorkbenchCommandDescriptor[] {
133
+ const seen = new Set<string>();
134
+
135
+ return groups.flat().filter((command) => {
136
+ if (seen.has(command.id)) {
137
+ return false;
138
+ }
139
+
140
+ seen.add(command.id);
141
+ return true;
142
+ });
143
+ }
144
+
145
+ export function buildWorkbenchPaletteCommands({
146
+ additionalCommands = [],
147
+ extensionCommandFeaturesById,
148
+ extensionCommands,
149
+ shellCommands,
150
+ shellContext,
151
+ }: {
152
+ additionalCommands?: readonly WorkbenchCommandDescriptor[];
153
+ extensionCommandFeaturesById?: ReadonlyMap<string, ExtensionCommandFeatureSpec> | undefined;
154
+ extensionCommands: readonly ExtensionCommand[];
155
+ shellCommands: readonly CommandDefinition<WorkbenchShellCommandContext>[];
156
+ shellContext: WorkbenchShellCommandContext;
157
+ }): WorkbenchCommandDescriptor[] {
158
+ const shellDescriptors = shellCommands.map((command) =>
159
+ shellCommandDefinitionToDescriptor(command, shellContext),
160
+ );
161
+ const contributedDescriptors = extensionCommands.map((command) =>
162
+ extensionCommandToDescriptor(command, extensionCommandFeaturesById?.get(command.id)),
163
+ );
164
+
165
+ return mergeWorkbenchCommandDescriptors(shellDescriptors, contributedDescriptors, [
166
+ ...additionalCommands,
167
+ ]);
168
+ }
169
+
170
+ export function matchesWorkbenchCommandPaletteShortcut(
171
+ event: Pick<KeyboardEvent, 'altKey' | 'ctrlKey' | 'key' | 'metaKey' | 'shiftKey'>,
172
+ ) {
173
+ const key = event.key.toLowerCase();
174
+
175
+ if (key !== 'p') {
176
+ return false;
177
+ }
178
+
179
+ if (!(event.ctrlKey || event.metaKey) || !event.shiftKey || event.altKey) {
180
+ return false;
181
+ }
182
+
183
+ return true;
184
+ }
185
+
186
+ export function matchesWorkbenchQuickAccessShortcut(
187
+ event: Pick<KeyboardEvent, 'altKey' | 'ctrlKey' | 'key' | 'metaKey' | 'shiftKey'>,
188
+ ) {
189
+ const key = event.key.toLowerCase();
190
+
191
+ if (key !== 'p') {
192
+ return false;
193
+ }
194
+
195
+ if (!(event.ctrlKey || event.metaKey) || event.shiftKey || event.altKey) {
196
+ return false;
197
+ }
198
+
199
+ return true;
200
+ }
201
+
202
+ export function getWorkbenchCommandPaletteShortcutLabel() {
203
+ return WORKBENCH_COMMAND_PALETTE_SHORTCUT;
204
+ }
205
+
206
+ export function getWorkbenchQuickAccessShortcutLabel() {
207
+ return WORKBENCH_QUICK_ACCESS_SHORTCUT;
208
+ }
@@ -0,0 +1,54 @@
1
+ import type { ContextKeyValue, KeybindingRegistry } from '@workbench-kit/platform';
2
+ import { resolveKeybindingWithOverrides, type KeybindingDefinition } from '@workbench-kit/platform';
3
+
4
+ function normalizeKeyToken(token: string): string {
5
+ const key = token.trim().toLowerCase();
6
+ if (key === 'del') return 'delete';
7
+ if (key === 'esc') return 'escape';
8
+ if (key === 'return') return 'enter';
9
+ if (key === 'spacebar' || key === 'space') return 'space';
10
+ return key.length === 1 ? key : key;
11
+ }
12
+
13
+ export function normalizeKeybindingKeyFromEvent(
14
+ event: Pick<KeyboardEvent, 'altKey' | 'ctrlKey' | 'key' | 'metaKey' | 'shiftKey'>,
15
+ ): string {
16
+ const parts: string[] = [];
17
+
18
+ if (event.ctrlKey || event.metaKey) {
19
+ parts.push('ctrl');
20
+ }
21
+ if (event.altKey) {
22
+ parts.push('alt');
23
+ }
24
+ if (event.shiftKey) {
25
+ parts.push('shift');
26
+ }
27
+
28
+ parts.push(normalizeKeyToken(event.key));
29
+ return parts.join('+');
30
+ }
31
+
32
+ export function resolveExtensionKeybindingCommand(
33
+ registry: KeybindingRegistry,
34
+ event: Pick<KeyboardEvent, 'altKey' | 'ctrlKey' | 'key' | 'metaKey' | 'shiftKey'>,
35
+ contextKeys: Readonly<Record<string, ContextKeyValue>> = {},
36
+ overrides: readonly KeybindingDefinition[] = [],
37
+ ) {
38
+ const key = normalizeKeybindingKeyFromEvent(event);
39
+
40
+ if (overrides.length > 0) {
41
+ const match = resolveKeybindingWithOverrides(
42
+ registry.getKeybindings(),
43
+ overrides,
44
+ key,
45
+ contextKeys,
46
+ );
47
+ if (match) {
48
+ return match;
49
+ }
50
+ }
51
+
52
+ const matches = registry.resolveKeybindings(key, contextKeys);
53
+ return matches[0];
54
+ }
@@ -0,0 +1,145 @@
1
+ import {
2
+ parseWorkbenchLayoutConfig,
3
+ type WorkbenchLayoutConfig,
4
+ } from '@workbench-kit/workbench-config';
5
+ import {
6
+ createWorkbenchLayoutState,
7
+ type WorkbenchStorageReader,
8
+ type WorkbenchStorageWriter,
9
+ type WorkbenchLayoutState,
10
+ type WorkbenchLayoutStateInput,
11
+ } from '@workbench-kit/workbench-core';
12
+
13
+ export const DEFAULT_WORKBENCH_LAYOUT_STORAGE_KEY = 'workbench-kit/.workbench/layout';
14
+
15
+ export function isWorkbenchLayoutPersistenceAvailable(): boolean {
16
+ try {
17
+ return typeof globalThis.localStorage !== 'undefined';
18
+ } catch {
19
+ return false;
20
+ }
21
+ }
22
+
23
+ export function workbenchLayoutConfigToInput(
24
+ config: WorkbenchLayoutConfig,
25
+ ): WorkbenchLayoutStateInput {
26
+ return {
27
+ activityBar: {
28
+ hiddenItemIds: config.activityBar.hiddenItemIds,
29
+ itemOrder: config.activityBar.itemOrder,
30
+ visible: config.activityBar.visible,
31
+ },
32
+ auxiliaryBar: {
33
+ visible: config.auxiliaryBar.visible,
34
+ },
35
+ panel: {
36
+ visible: config.panel.visible,
37
+ },
38
+ sideBar: {
39
+ activeViewContainer: config.sideBar.activeViewContainer,
40
+ sizePercent: config.sideBar.sizePercent,
41
+ visible: config.sideBar.visible,
42
+ },
43
+ };
44
+ }
45
+
46
+ export function workbenchLayoutStateToStorageValue(
47
+ state: WorkbenchLayoutState,
48
+ ): WorkbenchLayoutConfig {
49
+ return {
50
+ activityBar: {
51
+ visible: state.activityBar.visible,
52
+ ...(state.activityBar.hiddenItemIds?.length
53
+ ? { hiddenItemIds: [...state.activityBar.hiddenItemIds] }
54
+ : {}),
55
+ ...(state.activityBar.itemOrder?.length
56
+ ? { itemOrder: [...state.activityBar.itemOrder] }
57
+ : {}),
58
+ },
59
+ auxiliaryBar: {
60
+ visible: state.auxiliaryBar.visible,
61
+ },
62
+ panel: {
63
+ visible: state.panel.visible,
64
+ },
65
+ sideBar: {
66
+ visible: state.sideBar.visible,
67
+ ...(state.sideBar.activeViewContainer
68
+ ? { activeViewContainer: state.sideBar.activeViewContainer }
69
+ : {}),
70
+ ...(state.sideBar.sizePercent !== undefined
71
+ ? { sizePercent: state.sideBar.sizePercent }
72
+ : {}),
73
+ },
74
+ };
75
+ }
76
+
77
+ export function readPersistedWorkbenchLayout(
78
+ storageKey = DEFAULT_WORKBENCH_LAYOUT_STORAGE_KEY,
79
+ storage?: WorkbenchStorageReader,
80
+ ): WorkbenchLayoutStateInput | undefined {
81
+ const resolvedStorage = storage ?? getBrowserLocalStorage();
82
+ if (!resolvedStorage) return undefined;
83
+
84
+ try {
85
+ const raw = resolvedStorage.getItem(storageKey);
86
+ if (!raw) return undefined;
87
+
88
+ return workbenchLayoutConfigToInput(parseWorkbenchLayoutConfig(JSON.parse(raw) as unknown));
89
+ } catch {
90
+ return undefined;
91
+ }
92
+ }
93
+
94
+ export function writePersistedWorkbenchLayout(
95
+ state: WorkbenchLayoutState,
96
+ storageKey = DEFAULT_WORKBENCH_LAYOUT_STORAGE_KEY,
97
+ storage?: WorkbenchStorageWriter,
98
+ ): void {
99
+ const resolvedStorage = storage ?? getBrowserLocalStorage();
100
+ if (!resolvedStorage) return;
101
+
102
+ try {
103
+ resolvedStorage.setItem(
104
+ storageKey,
105
+ JSON.stringify(workbenchLayoutStateToStorageValue(state), null, 2),
106
+ );
107
+ } catch {
108
+ // Ignore quota and security errors so the shell keeps working offline.
109
+ }
110
+ }
111
+
112
+ export function resolvePersistedWorkbenchLayout(
113
+ initialLayout: WorkbenchLayoutStateInput | undefined,
114
+ options: {
115
+ persistLayout?: boolean | undefined;
116
+ storage?: WorkbenchStorageReader | undefined;
117
+ storageKey?: string | undefined;
118
+ } = {},
119
+ ): WorkbenchLayoutStateInput | undefined {
120
+ const {
121
+ initialLayout: baseLayout,
122
+ persistLayout = options.storage !== undefined || isWorkbenchLayoutPersistenceAvailable(),
123
+ storage,
124
+ storageKey = DEFAULT_WORKBENCH_LAYOUT_STORAGE_KEY,
125
+ } = { initialLayout, ...options };
126
+
127
+ if (!persistLayout) {
128
+ return baseLayout;
129
+ }
130
+
131
+ const persisted = readPersistedWorkbenchLayout(storageKey, storage);
132
+ if (!persisted) {
133
+ return baseLayout;
134
+ }
135
+
136
+ return createWorkbenchLayoutState(persisted, createWorkbenchLayoutState(baseLayout ?? {}));
137
+ }
138
+
139
+ function getBrowserLocalStorage(): (WorkbenchStorageReader & WorkbenchStorageWriter) | undefined {
140
+ if (!isWorkbenchLayoutPersistenceAvailable()) {
141
+ return undefined;
142
+ }
143
+
144
+ return globalThis.localStorage;
145
+ }
@@ -0,0 +1,118 @@
1
+ import { Modal } from '@workbench-kit/react/modal';
2
+ import { Badge, Button } from '@workbench-kit/react/primitives';
3
+ import type { ReactNode } from 'react';
4
+
5
+ export interface WorkbenchProfileDetail {
6
+ readonly label: string;
7
+ readonly value: ReactNode;
8
+ }
9
+
10
+ export interface WorkbenchProfileInput {
11
+ readonly accountId?: string | undefined;
12
+ readonly displayName: string;
13
+ readonly email?: string | undefined;
14
+ readonly providerLabel?: string | undefined;
15
+ readonly roleLabel?: string | undefined;
16
+ readonly sessionLabel?: string | undefined;
17
+ readonly statusLabel?: string | undefined;
18
+ readonly workspaceLabel?: string | undefined;
19
+ readonly details?: readonly WorkbenchProfileDetail[] | undefined;
20
+ readonly onSignOut?: (() => void) | undefined;
21
+ }
22
+
23
+ export interface WorkbenchProfileModalProps {
24
+ extraContent?: ReactNode | undefined;
25
+ profile: WorkbenchProfileInput;
26
+ onClose: () => void;
27
+ }
28
+
29
+ export function WorkbenchProfileModal({
30
+ extraContent,
31
+ profile,
32
+ onClose,
33
+ }: WorkbenchProfileModalProps) {
34
+ const initials = getProfileInitials(profile.displayName);
35
+ const details = createProfileDetails(profile);
36
+
37
+ return (
38
+ <Modal
39
+ bodyLayout="stack"
40
+ bodyPadding="lg"
41
+ bodyScroll="auto"
42
+ className="workbench-profile-modal"
43
+ closeLabel="Close profile"
44
+ footer={
45
+ <>
46
+ {profile.onSignOut ? (
47
+ <Button type="button" variant="danger" onClick={profile.onSignOut}>
48
+ Sign out
49
+ </Button>
50
+ ) : null}
51
+ <span className="workbench-profile-modal__footer-spacer" />
52
+ <Button type="button" onClick={onClose}>
53
+ Close
54
+ </Button>
55
+ </>
56
+ }
57
+ title="Profile"
58
+ titleSuffix={
59
+ profile.statusLabel ? <Badge variant="accent">{profile.statusLabel}</Badge> : null
60
+ }
61
+ onClose={onClose}
62
+ >
63
+ <section className="workbench-profile-card" aria-label="Workbench profile">
64
+ <div className="workbench-profile-card__avatar" aria-hidden="true">
65
+ {initials}
66
+ </div>
67
+ <div className="workbench-profile-card__identity">
68
+ <h2>{profile.displayName}</h2>
69
+ {profile.email ? <p>{profile.email}</p> : null}
70
+ </div>
71
+ </section>
72
+
73
+ {profile.sessionLabel ? (
74
+ <p className="workbench-profile-session" role="status">
75
+ {profile.sessionLabel}
76
+ </p>
77
+ ) : null}
78
+
79
+ <section className="workbench-profile-section" aria-label="Account details">
80
+ <h3>Account</h3>
81
+ <dl className="workbench-profile-detail-grid">
82
+ {details.map((detail) => (
83
+ <div key={detail.label}>
84
+ <dt>{detail.label}</dt>
85
+ <dd>{detail.value}</dd>
86
+ </div>
87
+ ))}
88
+ </dl>
89
+ </section>
90
+
91
+ {extraContent}
92
+ </Modal>
93
+ );
94
+ }
95
+
96
+ function createProfileDetails(profile: WorkbenchProfileInput): WorkbenchProfileDetail[] {
97
+ return [
98
+ ...(profile.providerLabel ? [{ label: 'Provider', value: profile.providerLabel }] : []),
99
+ ...(profile.roleLabel ? [{ label: 'Role', value: profile.roleLabel }] : []),
100
+ ...(profile.workspaceLabel ? [{ label: 'Workspace', value: profile.workspaceLabel }] : []),
101
+ ...(profile.accountId
102
+ ? [{ label: 'Account ID', value: <code>{profile.accountId}</code> }]
103
+ : []),
104
+ ...(profile.details ?? []),
105
+ ];
106
+ }
107
+
108
+ function getProfileInitials(displayName: string): string {
109
+ const initials = displayName
110
+ .split(/\s+/)
111
+ .map((part) => part[0])
112
+ .filter(Boolean)
113
+ .slice(0, 2)
114
+ .join('')
115
+ .toUpperCase();
116
+
117
+ return initials || 'U';
118
+ }
@@ -0,0 +1,58 @@
1
+ import type { CommandDefinition, CommandRegistry } from '@workbench-kit/platform';
2
+ import type { WorkbenchShellCommandContext } from '@workbench-kit/react/workbench';
3
+
4
+ export interface WorkbenchShellCommandRegistration {
5
+ dispose(): void;
6
+ }
7
+
8
+ export function registerWorkbenchShellCommandHandlers(
9
+ registry: CommandRegistry,
10
+ commands: readonly CommandDefinition<WorkbenchShellCommandContext>[],
11
+ getContext: () => WorkbenchShellCommandContext,
12
+ ): WorkbenchShellCommandRegistration {
13
+ const disposables = commands.map((command) => {
14
+ const handler = () => {
15
+ command.run?.(getContext());
16
+ };
17
+ const existing = registry.getCommand(command.id);
18
+
19
+ if (existing) {
20
+ const previousHandler = existing.handler;
21
+ existing.handler = handler;
22
+
23
+ return {
24
+ dispose() {
25
+ if (existing.handler === handler) {
26
+ existing.handler = previousHandler;
27
+ }
28
+ },
29
+ };
30
+ }
31
+
32
+ return registry.registerCommand({
33
+ category: command.category ?? 'Workbench',
34
+ handler,
35
+ icon: resolveStaticCommandIcon(command),
36
+ id: command.id,
37
+ title: resolveStaticCommandTitle(command),
38
+ });
39
+ });
40
+
41
+ return {
42
+ dispose() {
43
+ [...disposables].reverse().forEach((disposable) => disposable.dispose());
44
+ },
45
+ };
46
+ }
47
+
48
+ function resolveStaticCommandTitle(command: CommandDefinition<WorkbenchShellCommandContext>) {
49
+ if (command.title) {
50
+ return command.title;
51
+ }
52
+
53
+ return typeof command.label === 'string' ? command.label : command.id;
54
+ }
55
+
56
+ function resolveStaticCommandIcon(command: CommandDefinition<WorkbenchShellCommandContext>) {
57
+ return typeof command.icon === 'string' ? command.icon : undefined;
58
+ }