@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.
- package/package.json +49 -0
- package/src/chat-ai-mock-proposals.ts +39 -0
- package/src/chat-command-input.ts +52 -0
- package/src/chat-command-policy.ts +54 -0
- package/src/chat-command-surface.tsx +155 -0
- package/src/chat-view-data.ts +20 -0
- package/src/chat-view.tsx +220 -0
- package/src/command-inspector-surface.tsx +46 -0
- package/src/commands-view-data.ts +17 -0
- package/src/commands-view.tsx +43 -0
- package/src/createCommandWorkspaceExplorerPort.ts +86 -0
- package/src/devtools/WorkbenchDevtoolsPanel.tsx +124 -0
- package/src/devtools/WorkbenchDevtoolsShell.tsx +17 -0
- package/src/devtools/index.ts +11 -0
- package/src/devtools/use-workbench-devtools-snapshot.ts +84 -0
- package/src/devtools/workbench-devtools-snapshot.ts +140 -0
- package/src/devtools/workbench-devtools.css +82 -0
- package/src/editor-area-dnd.ts +69 -0
- package/src/editor-area-model.ts +71 -0
- package/src/editor-area.css +454 -0
- package/src/editor-area.tsx +676 -0
- package/src/editor-host-surface.tsx +634 -0
- package/src/editor-pane-visibility.ts +130 -0
- package/src/editor-resource.ts +36 -0
- package/src/editor-state-storage.ts +193 -0
- package/src/editor-tab-context-menu.ts +118 -0
- package/src/editor-view-providers.tsx +552 -0
- package/src/editor-workspace-file-availability.ts +15 -0
- package/src/editor-workspace-reconcile.tsx +28 -0
- package/src/explorer-context-menu.ts +114 -0
- package/src/explorer-reveal.ts +112 -0
- package/src/explorer-view-data.ts +58 -0
- package/src/explorer-view.tsx +224 -0
- package/src/extension-context-menu.ts +52 -0
- package/src/extension-management-model.ts +313 -0
- package/src/extensions-view-data.ts +19 -0
- package/src/extensions-view.tsx +38 -0
- package/src/field-remap-demo.tsx +18 -0
- package/src/field-remap-editor-surface.tsx +36 -0
- package/src/field-remap-flow-adapter.ts +240 -0
- package/src/field-remap-flow.tsx +369 -0
- package/src/field-remap-graph.tsx +559 -0
- package/src/field-remap-panel.tsx +175 -0
- package/src/field-remap-samples.ts +352 -0
- package/src/field-remap-tree.tsx +404 -0
- package/src/field-remap-view-data.ts +16 -0
- package/src/field-remap-view.css +676 -0
- package/src/field-remap-view.tsx +86 -0
- package/src/index.ts +199 -0
- package/src/jdw-lab-view-data.ts +25 -0
- package/src/jdw-lab-view.css +21 -0
- package/src/jdw-lab-view.tsx +79 -0
- package/src/jdw-widget-form-view.tsx +65 -0
- package/src/jdw-widget-preview-view.tsx +51 -0
- package/src/json-form-source-patch.ts +323 -0
- package/src/jsonata-transform.ts +36 -0
- package/src/keybinding-management-settings.tsx +16 -0
- package/src/keybinding-overrides-storage.ts +57 -0
- package/src/management-palette-commands.ts +38 -0
- package/src/management-settings-ids.ts +12 -0
- package/src/management-settings.tsx +75 -0
- package/src/preference-settings-storage.ts +58 -0
- package/src/provider.tsx +573 -0
- package/src/search-view-data.ts +15 -0
- package/src/search-view.tsx +62 -0
- package/src/shell-model.tsx +149 -0
- package/src/shell-secondary-actions.tsx +59 -0
- package/src/shell-settings-constants.ts +9 -0
- package/src/shell-settings.tsx +599 -0
- package/src/shell-titlebar-layout-controls.tsx +4 -0
- package/src/shell-view-host.tsx +199 -0
- package/src/shell.tsx +649 -0
- package/src/table-mapping-adapter.ts +59 -0
- package/src/use-active-workspace-path.ts +15 -0
- package/src/use-command-management.ts +191 -0
- package/src/use-context-key-revision.ts +18 -0
- package/src/use-editor.ts +107 -0
- package/src/use-extension-management.ts +193 -0
- package/src/use-keybinding-management.ts +130 -0
- package/src/use-persisted-workbench-appearance.ts +33 -0
- package/src/use-workbench-chat-command-proposals.ts +194 -0
- package/src/use-workbench-command-descriptors.ts +41 -0
- package/src/workbench-appearance-storage.ts +115 -0
- package/src/workbench-command-host.tsx +243 -0
- package/src/workbench-command-palette.ts +208 -0
- package/src/workbench-keybinding-bridge.ts +54 -0
- package/src/workbench-layout-storage.ts +145 -0
- package/src/workbench-profile-modal.tsx +118 -0
- package/src/workbench-shell-command-registration.ts +58 -0
- package/src/workbench-startup-gate.tsx +136 -0
- package/src/workbench-status-sections.ts +43 -0
- package/src/workbench-user-commands.ts +46 -0
- package/src/workspace-view-state.ts +35 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { useCallback, useRef, type Dispatch, type SetStateAction } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
isWorkbenchCommandRunnable,
|
|
4
|
+
resolveWorkbenchCommandExecutionPolicy,
|
|
5
|
+
type ResolveWorkbenchCommandExecutionPolicyInput,
|
|
6
|
+
type WorkbenchCommandDescriptor,
|
|
7
|
+
} from '@workbench-kit/react/workbench';
|
|
8
|
+
import type { ChatCommandProposal, ChatMessage } from '@workbench-kit/react/workbench/chat';
|
|
9
|
+
|
|
10
|
+
import { type WorkbenchChatCommandRunResult } from './chat-command-surface.js';
|
|
11
|
+
import { useWorkbench } from './provider.js';
|
|
12
|
+
|
|
13
|
+
export interface WorkbenchChatCommandProposalInput {
|
|
14
|
+
args?: readonly unknown[] | undefined;
|
|
15
|
+
commandId: string;
|
|
16
|
+
description?: string | undefined;
|
|
17
|
+
id?: string | undefined;
|
|
18
|
+
label?: string | undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface UseWorkbenchChatCommandProposalsOptions {
|
|
22
|
+
commands: readonly WorkbenchCommandDescriptor[];
|
|
23
|
+
onCommandResult?: ((result: WorkbenchChatCommandRunResult) => void) | undefined;
|
|
24
|
+
policyInput?: ResolveWorkbenchCommandExecutionPolicyInput | undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getCommandErrorMessage(error: unknown) {
|
|
28
|
+
if (error instanceof Error) {
|
|
29
|
+
return error.message;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return String(error);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function createProposalId(commandId: string) {
|
|
36
|
+
return `proposal-${commandId}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function useWorkbenchChatCommandProposals({
|
|
40
|
+
commands,
|
|
41
|
+
onCommandResult,
|
|
42
|
+
policyInput,
|
|
43
|
+
}: UseWorkbenchChatCommandProposalsOptions) {
|
|
44
|
+
const { executeCommand } = useWorkbench();
|
|
45
|
+
const runningProposalIds = useRef(new Set<string>());
|
|
46
|
+
|
|
47
|
+
const resolveDescriptor = useCallback(
|
|
48
|
+
(commandId: string, label?: string) =>
|
|
49
|
+
commands.find((command) => command.id === commandId) ?? {
|
|
50
|
+
id: commandId,
|
|
51
|
+
label: label ?? commandId,
|
|
52
|
+
},
|
|
53
|
+
[commands],
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const enrichProposal = useCallback(
|
|
57
|
+
(proposal: WorkbenchChatCommandProposalInput): ChatCommandProposal => {
|
|
58
|
+
const descriptor = resolveDescriptor(proposal.commandId, proposal.label);
|
|
59
|
+
const policy = resolveWorkbenchCommandExecutionPolicy(descriptor, policyInput);
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
args: proposal.args,
|
|
63
|
+
commandId: proposal.commandId,
|
|
64
|
+
description: proposal.description ?? descriptor.description,
|
|
65
|
+
id: proposal.id ?? createProposalId(proposal.commandId),
|
|
66
|
+
label: proposal.label ?? descriptor.label,
|
|
67
|
+
policy,
|
|
68
|
+
status: policy === 'auto-deny' ? 'blocked' : 'pending',
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
[policyInput, resolveDescriptor],
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const enrichMessageProposals = useCallback(
|
|
75
|
+
(proposals: readonly WorkbenchChatCommandProposalInput[]) =>
|
|
76
|
+
proposals.map((proposal) => enrichProposal(proposal)),
|
|
77
|
+
[enrichProposal],
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const updateProposalStatus = useCallback(
|
|
81
|
+
(
|
|
82
|
+
setMessages: Dispatch<SetStateAction<ChatMessage[]>>,
|
|
83
|
+
messageId: string,
|
|
84
|
+
proposalId: string,
|
|
85
|
+
status: ChatCommandProposal['status'],
|
|
86
|
+
) => {
|
|
87
|
+
setMessages((currentMessages) =>
|
|
88
|
+
currentMessages.map((message) => {
|
|
89
|
+
if (message.id !== messageId || !message.commandProposals) {
|
|
90
|
+
return message;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
...message,
|
|
95
|
+
commandProposals: message.commandProposals.map((proposal) =>
|
|
96
|
+
proposal.id === proposalId ? { ...proposal, status } : proposal,
|
|
97
|
+
),
|
|
98
|
+
};
|
|
99
|
+
}),
|
|
100
|
+
);
|
|
101
|
+
},
|
|
102
|
+
[],
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const runProposal = useCallback(
|
|
106
|
+
async (
|
|
107
|
+
setMessages: Dispatch<SetStateAction<ChatMessage[]>>,
|
|
108
|
+
messageId: string,
|
|
109
|
+
proposal: ChatCommandProposal,
|
|
110
|
+
) => {
|
|
111
|
+
if (runningProposalIds.current.has(proposal.id)) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const descriptor = resolveDescriptor(proposal.commandId, proposal.label);
|
|
116
|
+
if (!isWorkbenchCommandRunnable(descriptor)) {
|
|
117
|
+
updateProposalStatus(setMessages, messageId, proposal.id, 'failed');
|
|
118
|
+
onCommandResult?.({
|
|
119
|
+
commandId: proposal.commandId,
|
|
120
|
+
label: proposal.label,
|
|
121
|
+
message: descriptor.disabledReason ?? 'Command is currently unavailable.',
|
|
122
|
+
status: 'error',
|
|
123
|
+
});
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
runningProposalIds.current.add(proposal.id);
|
|
128
|
+
updateProposalStatus(setMessages, messageId, proposal.id, 'running');
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
await executeCommand(proposal.commandId, ...(proposal.args ?? []));
|
|
132
|
+
updateProposalStatus(setMessages, messageId, proposal.id, 'executed');
|
|
133
|
+
onCommandResult?.({
|
|
134
|
+
commandId: proposal.commandId,
|
|
135
|
+
label: proposal.label,
|
|
136
|
+
status: 'success',
|
|
137
|
+
});
|
|
138
|
+
} catch (error) {
|
|
139
|
+
updateProposalStatus(setMessages, messageId, proposal.id, 'failed');
|
|
140
|
+
onCommandResult?.({
|
|
141
|
+
commandId: proposal.commandId,
|
|
142
|
+
label: proposal.label,
|
|
143
|
+
message: getCommandErrorMessage(error),
|
|
144
|
+
status: 'error',
|
|
145
|
+
});
|
|
146
|
+
} finally {
|
|
147
|
+
runningProposalIds.current.delete(proposal.id);
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
[executeCommand, onCommandResult, resolveDescriptor, updateProposalStatus],
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const processAutoPolicies = useCallback(
|
|
154
|
+
(
|
|
155
|
+
setMessages: Dispatch<SetStateAction<ChatMessage[]>>,
|
|
156
|
+
messageId: string,
|
|
157
|
+
proposals: readonly ChatCommandProposal[],
|
|
158
|
+
) => {
|
|
159
|
+
proposals.forEach((proposal) => {
|
|
160
|
+
if (proposal.policy === 'auto-allow' && proposal.status === 'pending') {
|
|
161
|
+
void runProposal(setMessages, messageId, proposal);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
},
|
|
165
|
+
[runProposal],
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const createProposalHandlers = useCallback(
|
|
169
|
+
(setMessages: Dispatch<SetStateAction<ChatMessage[]>>) => ({
|
|
170
|
+
onProposalAllow: (messageId: string, proposal: ChatCommandProposal) => {
|
|
171
|
+
if (proposal.status !== 'pending' || proposal.policy !== 'approval-required') {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
updateProposalStatus(setMessages, messageId, proposal.id, 'allowed');
|
|
176
|
+
void runProposal(setMessages, messageId, proposal);
|
|
177
|
+
},
|
|
178
|
+
onProposalDeny: (messageId: string, proposal: ChatCommandProposal) => {
|
|
179
|
+
if (proposal.status !== 'pending') {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
updateProposalStatus(setMessages, messageId, proposal.id, 'denied');
|
|
184
|
+
},
|
|
185
|
+
}),
|
|
186
|
+
[runProposal, updateProposalStatus],
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
createProposalHandlers,
|
|
191
|
+
enrichMessageProposals,
|
|
192
|
+
processAutoPolicies,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { useEffect, useMemo, useReducer } from 'react';
|
|
2
|
+
import type { WorkbenchCommandDescriptor } from '@workbench-kit/react/workbench';
|
|
3
|
+
|
|
4
|
+
import { useWorkbench } from './provider.js';
|
|
5
|
+
import {
|
|
6
|
+
collectExtensionCommandFeaturesById,
|
|
7
|
+
extensionCommandToDescriptor,
|
|
8
|
+
mergeWorkbenchCommandDescriptors,
|
|
9
|
+
} from './workbench-command-palette.js';
|
|
10
|
+
|
|
11
|
+
const EMPTY_COMMAND_DESCRIPTORS: readonly WorkbenchCommandDescriptor[] = [];
|
|
12
|
+
|
|
13
|
+
export function useWorkbenchCommandDescriptors(
|
|
14
|
+
additionalCommands: readonly WorkbenchCommandDescriptor[] = EMPTY_COMMAND_DESCRIPTORS,
|
|
15
|
+
) {
|
|
16
|
+
const { extensionRegistry } = useWorkbench();
|
|
17
|
+
const [refreshToken, refreshCommands] = useReducer((count: number) => count + 1, 0);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const disposable = extensionRegistry.commands.onDidRegisterCommand(() => {
|
|
21
|
+
refreshCommands();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return () => {
|
|
25
|
+
disposable.dispose();
|
|
26
|
+
};
|
|
27
|
+
}, [extensionRegistry.commands]);
|
|
28
|
+
|
|
29
|
+
return useMemo(() => {
|
|
30
|
+
const commandFeaturesById = collectExtensionCommandFeaturesById(extensionRegistry);
|
|
31
|
+
|
|
32
|
+
return mergeWorkbenchCommandDescriptors(
|
|
33
|
+
extensionRegistry.commands
|
|
34
|
+
.getCommands()
|
|
35
|
+
.map((command) =>
|
|
36
|
+
extensionCommandToDescriptor(command, commandFeaturesById.get(command.id)),
|
|
37
|
+
),
|
|
38
|
+
[...additionalCommands],
|
|
39
|
+
);
|
|
40
|
+
}, [additionalCommands, extensionRegistry, refreshToken]);
|
|
41
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_DARK_THEME_PRESET,
|
|
3
|
+
DEFAULT_LIGHT_THEME_PRESET,
|
|
4
|
+
DEFAULT_SHELL_PRESET,
|
|
5
|
+
isShellPresetId,
|
|
6
|
+
type WorkbenchAppearanceSettings,
|
|
7
|
+
type WorkbenchColorSchemePreference,
|
|
8
|
+
} from '@workbench-kit/react/workbench';
|
|
9
|
+
import type { WorkbenchStorageReader, WorkbenchStorageWriter } from '@workbench-kit/workbench-core';
|
|
10
|
+
|
|
11
|
+
export type { WorkbenchAppearanceSettings } from '@workbench-kit/react/workbench/themePresets';
|
|
12
|
+
|
|
13
|
+
export const DEFAULT_WORKBENCH_APPEARANCE_STORAGE_KEY = 'workbench-kit/.workbench/appearance';
|
|
14
|
+
|
|
15
|
+
export const DEFAULT_WORKBENCH_APPEARANCE: WorkbenchAppearanceSettings = {
|
|
16
|
+
darkPreset: DEFAULT_DARK_THEME_PRESET,
|
|
17
|
+
lightPreset: DEFAULT_LIGHT_THEME_PRESET,
|
|
18
|
+
shellPreset: DEFAULT_SHELL_PRESET,
|
|
19
|
+
themePreference: 'system',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function isWorkbenchAppearancePersistenceAvailable(): boolean {
|
|
23
|
+
try {
|
|
24
|
+
return typeof globalThis.localStorage !== 'undefined';
|
|
25
|
+
} catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function readPersistedWorkbenchAppearance(
|
|
31
|
+
storageKey = DEFAULT_WORKBENCH_APPEARANCE_STORAGE_KEY,
|
|
32
|
+
storage?: WorkbenchStorageReader,
|
|
33
|
+
): WorkbenchAppearanceSettings {
|
|
34
|
+
const resolvedStorage = storage ?? getBrowserLocalStorage();
|
|
35
|
+
if (!resolvedStorage) {
|
|
36
|
+
return DEFAULT_WORKBENCH_APPEARANCE;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const raw = resolvedStorage.getItem(storageKey);
|
|
41
|
+
if (!raw) {
|
|
42
|
+
return DEFAULT_WORKBENCH_APPEARANCE;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return normalizeWorkbenchAppearance(JSON.parse(raw) as unknown);
|
|
46
|
+
} catch {
|
|
47
|
+
return DEFAULT_WORKBENCH_APPEARANCE;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function writePersistedWorkbenchAppearance(
|
|
52
|
+
settings: WorkbenchAppearanceSettings,
|
|
53
|
+
storageKey = DEFAULT_WORKBENCH_APPEARANCE_STORAGE_KEY,
|
|
54
|
+
storage?: WorkbenchStorageWriter,
|
|
55
|
+
): void {
|
|
56
|
+
const resolvedStorage = storage ?? getBrowserLocalStorage();
|
|
57
|
+
if (!resolvedStorage) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
resolvedStorage.setItem(storageKey, JSON.stringify(settings, null, 2));
|
|
63
|
+
} catch {
|
|
64
|
+
// Ignore quota and security errors so the shell keeps working offline.
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizeWorkbenchAppearance(value: unknown): WorkbenchAppearanceSettings {
|
|
69
|
+
if (typeof value !== 'object' || value === null) {
|
|
70
|
+
return DEFAULT_WORKBENCH_APPEARANCE;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const record = value as Record<string, unknown>;
|
|
74
|
+
const themePreference = normalizeThemePreference(record.themePreference);
|
|
75
|
+
// Preset ids may belong to a contributed theme, which isn't known until extensions
|
|
76
|
+
// register, so we can only validate shape here; the appearance UI falls back to a
|
|
77
|
+
// default option if the persisted id no longer resolves to anything.
|
|
78
|
+
const lightPreset = normalizePresetId(
|
|
79
|
+
record.lightPreset,
|
|
80
|
+
DEFAULT_WORKBENCH_APPEARANCE.lightPreset,
|
|
81
|
+
);
|
|
82
|
+
const darkPreset = normalizePresetId(record.darkPreset, DEFAULT_WORKBENCH_APPEARANCE.darkPreset);
|
|
83
|
+
const shellPreset = normalizeShellPresetId(record.shellPreset);
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
darkPreset,
|
|
87
|
+
lightPreset,
|
|
88
|
+
shellPreset,
|
|
89
|
+
themePreference,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function normalizeShellPresetId(value: unknown): string {
|
|
94
|
+
return typeof value === 'string' && isShellPresetId(value) ? value : DEFAULT_SHELL_PRESET;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function normalizePresetId(value: unknown, fallback: string): string {
|
|
98
|
+
return typeof value === 'string' && value.length > 0 ? value : fallback;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function normalizeThemePreference(value: unknown): WorkbenchColorSchemePreference {
|
|
102
|
+
if (value === 'system' || value === 'light' || value === 'dark') {
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return DEFAULT_WORKBENCH_APPEARANCE.themePreference;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function getBrowserLocalStorage(): (WorkbenchStorageReader & WorkbenchStorageWriter) | undefined {
|
|
110
|
+
if (!isWorkbenchAppearancePersistenceAvailable()) {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return globalThis.localStorage;
|
|
115
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createCommandRegistryFromContributions,
|
|
3
|
+
type CommandRegistry,
|
|
4
|
+
} from '@workbench-kit/platform';
|
|
5
|
+
import {
|
|
6
|
+
WorkbenchCommandPalette,
|
|
7
|
+
WorkbenchShortcutCommandBridge,
|
|
8
|
+
createWorkbenchShellCommands,
|
|
9
|
+
type WorkbenchCommandDescriptor,
|
|
10
|
+
type WorkbenchCommandRunContext,
|
|
11
|
+
type WorkbenchShellCommandContext,
|
|
12
|
+
} from '@workbench-kit/react/workbench';
|
|
13
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
14
|
+
|
|
15
|
+
import { useWorkbench } from './provider.js';
|
|
16
|
+
import { registerWorkbenchShellCommandHandlers } from './workbench-shell-command-registration.js';
|
|
17
|
+
import {
|
|
18
|
+
buildWorkbenchPaletteCommands,
|
|
19
|
+
collectExtensionCommandFeaturesById,
|
|
20
|
+
matchesWorkbenchCommandPaletteShortcut,
|
|
21
|
+
matchesWorkbenchQuickAccessShortcut,
|
|
22
|
+
resolveShellCommandActivities,
|
|
23
|
+
} from './workbench-command-palette.js';
|
|
24
|
+
import { resolveExtensionKeybindingCommand } from './workbench-keybinding-bridge.js';
|
|
25
|
+
|
|
26
|
+
export interface WorkbenchCommandHostProps {
|
|
27
|
+
additionalCommands?: readonly WorkbenchCommandDescriptor[];
|
|
28
|
+
enableCommandPalette?: boolean;
|
|
29
|
+
enableExtensionKeybindings?: boolean;
|
|
30
|
+
enableShortcutBridge?: boolean;
|
|
31
|
+
onOpenSettings: () => void;
|
|
32
|
+
onRunCommand?: (
|
|
33
|
+
command: WorkbenchCommandDescriptor,
|
|
34
|
+
context: WorkbenchCommandRunContext,
|
|
35
|
+
) => boolean | void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function WorkbenchCommandHost({
|
|
39
|
+
additionalCommands = [],
|
|
40
|
+
enableCommandPalette = true,
|
|
41
|
+
enableExtensionKeybindings = true,
|
|
42
|
+
enableShortcutBridge = true,
|
|
43
|
+
onOpenSettings,
|
|
44
|
+
onRunCommand,
|
|
45
|
+
}: WorkbenchCommandHostProps) {
|
|
46
|
+
const { executeCommand, extensionRegistry, keybindingOverrides, layoutService } = useWorkbench();
|
|
47
|
+
const [paletteOpen, setPaletteOpen] = useState(false);
|
|
48
|
+
const [paletteQuery, setPaletteQuery] = useState('');
|
|
49
|
+
const [layout, setLayout] = useState(() => layoutService.getState());
|
|
50
|
+
const shellContextRef = useRef<WorkbenchShellCommandContext | undefined>(undefined);
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
const disposable = layoutService.onDidChangeLayout(({ state }) => {
|
|
54
|
+
setLayout(state);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return () => {
|
|
58
|
+
disposable.dispose();
|
|
59
|
+
};
|
|
60
|
+
}, [layoutService]);
|
|
61
|
+
|
|
62
|
+
const shellActivities = useMemo(
|
|
63
|
+
() => resolveShellCommandActivities(extensionRegistry),
|
|
64
|
+
[extensionRegistry],
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const shellCommandDefinitions = useMemo(
|
|
68
|
+
() =>
|
|
69
|
+
createWorkbenchShellCommands({
|
|
70
|
+
activities: shellActivities,
|
|
71
|
+
includeSettings: true,
|
|
72
|
+
includeSidebarToggle: true,
|
|
73
|
+
}),
|
|
74
|
+
[shellActivities],
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const shellCommandRegistry = useMemo(
|
|
78
|
+
() =>
|
|
79
|
+
createCommandRegistryFromContributions<WorkbenchShellCommandContext>([
|
|
80
|
+
{ commands: shellCommandDefinitions },
|
|
81
|
+
]),
|
|
82
|
+
[shellCommandDefinitions],
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const shellContext = useMemo<WorkbenchShellCommandContext>(
|
|
86
|
+
() => ({
|
|
87
|
+
isPrimarySidebarVisible: layout.sideBar.visible,
|
|
88
|
+
openSettings: onOpenSettings,
|
|
89
|
+
showActivity: (activityId) => {
|
|
90
|
+
layoutService.setActiveViewContainer(activityId);
|
|
91
|
+
layoutService.setSideBarVisible(true);
|
|
92
|
+
},
|
|
93
|
+
togglePrimarySidebar: () => {
|
|
94
|
+
layoutService.setSideBarVisible(!layout.sideBar.visible);
|
|
95
|
+
},
|
|
96
|
+
}),
|
|
97
|
+
[layout.sideBar.visible, layoutService, onOpenSettings],
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
shellContextRef.current = shellContext;
|
|
101
|
+
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
const registration = registerWorkbenchShellCommandHandlers(
|
|
104
|
+
extensionRegistry.commands,
|
|
105
|
+
shellCommandDefinitions,
|
|
106
|
+
() => {
|
|
107
|
+
const context = shellContextRef.current;
|
|
108
|
+
if (!context) {
|
|
109
|
+
throw new Error('Workbench shell command context is not available.');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return context;
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
return () => {
|
|
117
|
+
registration.dispose();
|
|
118
|
+
};
|
|
119
|
+
}, [extensionRegistry.commands, shellCommandDefinitions]);
|
|
120
|
+
|
|
121
|
+
const paletteCommands = useMemo(
|
|
122
|
+
() =>
|
|
123
|
+
buildWorkbenchPaletteCommands({
|
|
124
|
+
additionalCommands,
|
|
125
|
+
extensionCommandFeaturesById: collectExtensionCommandFeaturesById(extensionRegistry),
|
|
126
|
+
extensionCommands: extensionRegistry.commands.getCommands(),
|
|
127
|
+
shellCommands: shellCommandDefinitions,
|
|
128
|
+
shellContext,
|
|
129
|
+
}),
|
|
130
|
+
[additionalCommands, extensionRegistry, shellContext],
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const closePalette = useCallback(() => {
|
|
134
|
+
setPaletteOpen(false);
|
|
135
|
+
}, []);
|
|
136
|
+
|
|
137
|
+
const openPalette = useCallback((query = '') => {
|
|
138
|
+
setPaletteQuery(query);
|
|
139
|
+
setPaletteOpen(true);
|
|
140
|
+
}, []);
|
|
141
|
+
|
|
142
|
+
const runPaletteCommand = useCallback(
|
|
143
|
+
(command: WorkbenchCommandDescriptor, context: WorkbenchCommandRunContext) => {
|
|
144
|
+
const finish = () => {
|
|
145
|
+
closePalette();
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
if (onRunCommand?.(command, context)) {
|
|
149
|
+
finish();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
void executeCommand(command.id).finally(finish);
|
|
154
|
+
},
|
|
155
|
+
[closePalette, executeCommand, onRunCommand],
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
if (!enableCommandPalette) {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
164
|
+
if (matchesWorkbenchCommandPaletteShortcut(event)) {
|
|
165
|
+
event.preventDefault();
|
|
166
|
+
openPalette('>');
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (matchesWorkbenchQuickAccessShortcut(event)) {
|
|
171
|
+
event.preventDefault();
|
|
172
|
+
openPalette();
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
window.addEventListener('keydown', onKeyDown);
|
|
177
|
+
return () => {
|
|
178
|
+
window.removeEventListener('keydown', onKeyDown);
|
|
179
|
+
};
|
|
180
|
+
}, [enableCommandPalette, openPalette]);
|
|
181
|
+
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
if (!enableExtensionKeybindings) {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
188
|
+
if (
|
|
189
|
+
matchesWorkbenchCommandPaletteShortcut(event) ||
|
|
190
|
+
matchesWorkbenchQuickAccessShortcut(event)
|
|
191
|
+
) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const match = resolveExtensionKeybindingCommand(
|
|
196
|
+
extensionRegistry.keybindings,
|
|
197
|
+
event,
|
|
198
|
+
{},
|
|
199
|
+
keybindingOverrides,
|
|
200
|
+
);
|
|
201
|
+
if (!match) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
event.preventDefault();
|
|
206
|
+
void executeCommand(match.command, ...(match.args ?? []));
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
window.addEventListener('keydown', onKeyDown);
|
|
210
|
+
return () => {
|
|
211
|
+
window.removeEventListener('keydown', onKeyDown);
|
|
212
|
+
};
|
|
213
|
+
}, [
|
|
214
|
+
enableExtensionKeybindings,
|
|
215
|
+
executeCommand,
|
|
216
|
+
extensionRegistry.keybindings,
|
|
217
|
+
keybindingOverrides,
|
|
218
|
+
]);
|
|
219
|
+
|
|
220
|
+
return (
|
|
221
|
+
<>
|
|
222
|
+
{enableShortcutBridge ? (
|
|
223
|
+
<WorkbenchShortcutCommandBridge
|
|
224
|
+
context={shellContext}
|
|
225
|
+
preventDefault
|
|
226
|
+
registry={shellCommandRegistry as CommandRegistry<WorkbenchShellCommandContext>}
|
|
227
|
+
/>
|
|
228
|
+
) : null}
|
|
229
|
+
{enableCommandPalette ? (
|
|
230
|
+
<WorkbenchCommandPalette
|
|
231
|
+
commands={paletteCommands}
|
|
232
|
+
open={paletteOpen}
|
|
233
|
+
placeholder="Search commands"
|
|
234
|
+
query={paletteQuery}
|
|
235
|
+
title="Command Palette"
|
|
236
|
+
onClose={closePalette}
|
|
237
|
+
onQueryChange={setPaletteQuery}
|
|
238
|
+
onRunCommand={runPaletteCommand}
|
|
239
|
+
/>
|
|
240
|
+
) : null}
|
|
241
|
+
</>
|
|
242
|
+
);
|
|
243
|
+
}
|