@workbench-kit/shell-react 0.0.2-prototype.0.2.10
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 +98 -0
- package/package.json +51 -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/use-chat-command-proposals.ts +194 -0
- package/src/chat/view-data.ts +20 -0
- package/src/chat/view.tsx +220 -0
- package/src/commands/inspector-surface.tsx +46 -0
- package/src/commands/use-command-descriptors.ts +41 -0
- package/src/commands/use-context-key-revision.ts +18 -0
- package/src/commands/view-data.ts +17 -0
- package/src/commands/view.tsx +43 -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 +691 -0
- package/src/editor/markdown-document-view-data.ts +16 -0
- package/src/editor/pane-visibility.ts +130 -0
- package/src/editor/resource.ts +36 -0
- package/src/editor/state-storage.ts +182 -0
- package/src/editor/tab-context-menu.ts +118 -0
- package/src/editor/use-editor.ts +138 -0
- package/src/editor/view-providers.tsx +504 -0
- package/src/editor/workspace-file-availability.ts +15 -0
- package/src/editor/workspace-reconcile.tsx +31 -0
- package/src/explorer/context-menu.ts +114 -0
- package/src/explorer/create-command-workspace-explorer-port.ts +86 -0
- package/src/explorer/reveal.ts +112 -0
- package/src/explorer/search-view-data.ts +15 -0
- package/src/explorer/search-view.tsx +65 -0
- package/src/explorer/use-active-workspace-path.ts +15 -0
- package/src/explorer/view-data.ts +58 -0
- package/src/explorer/view.tsx +227 -0
- package/src/extensions/context-menu.ts +52 -0
- package/src/extensions/management-model.ts +313 -0
- package/src/extensions/use-extension-management.ts +241 -0
- package/src/extensions/view-data.ts +19 -0
- package/src/extensions/view.tsx +63 -0
- package/src/field-remap/chrome-labels.ts +74 -0
- package/src/field-remap/convert-note-editor.tsx +206 -0
- package/src/field-remap/convert-palette.tsx +126 -0
- package/src/field-remap/demo.tsx +105 -0
- package/src/field-remap/detail-panel.tsx +659 -0
- package/src/field-remap/editor-surface.tsx +36 -0
- package/src/field-remap/flow-adapter.ts +838 -0
- package/src/field-remap/flow-ops.ts +442 -0
- package/src/field-remap/flow.tsx +948 -0
- package/src/field-remap/graph.tsx +559 -0
- package/src/field-remap/index.ts +88 -0
- package/src/field-remap/io-class-browse.tsx +145 -0
- package/src/field-remap/jsonata-transform.ts +166 -0
- package/src/field-remap/panel.tsx +466 -0
- package/src/field-remap/samples.ts +507 -0
- package/src/field-remap/shape-io-editor.tsx +170 -0
- package/src/field-remap/table-mapping-adapter.ts +59 -0
- package/src/field-remap/transform-options-editor.tsx +312 -0
- package/src/field-remap/tree.tsx +404 -0
- package/src/field-remap/view-data.ts +16 -0
- package/src/field-remap/view.css +1367 -0
- package/src/field-remap/view.tsx +86 -0
- package/src/index.ts +294 -0
- package/src/jdw/document-view-data.ts +31 -0
- package/src/jdw/json-form-source-patch.ts +323 -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 +68 -0
- package/src/jdw/widget-preview-view.tsx +54 -0
- package/src/management/keybinding-overrides-storage.ts +49 -0
- package/src/management/keybinding-settings.tsx +16 -0
- package/src/management/palette-commands.ts +38 -0
- package/src/management/preference-settings-storage.ts +50 -0
- package/src/management/settings-ids.ts +12 -0
- package/src/management/settings.tsx +96 -0
- package/src/management/use-command-management.ts +191 -0
- package/src/management/use-keybinding-management.ts +130 -0
- package/src/shell/model.tsx +149 -0
- package/src/shell/provider.tsx +606 -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/shell.tsx +680 -0
- package/src/shell/titlebar-layout-controls.tsx +4 -0
- package/src/shell/view-host.tsx +212 -0
- package/src/workbench/appearance-storage.ts +107 -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 +134 -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/use-persisted-appearance.ts +33 -0
- package/src/workbench/user-commands.ts +46 -0
- package/src/workbench/workspace-view-state.ts +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# `@workbench-kit/shell-react`
|
|
2
|
+
|
|
3
|
+
React shell and host assembly for Workbench Kit: `WorkbenchProvider`,
|
|
4
|
+
`WorkbenchShell`, editor area wiring, layout/appearance persistence helpers, and
|
|
5
|
+
settings/profile/help composition.
|
|
6
|
+
|
|
7
|
+
Published on npm with the **`prototype`** dist tag.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```powershell
|
|
12
|
+
pnpm add @workbench-kit/shell-react@prototype @workbench-kit/react@prototype @workbench-kit/tokens@prototype
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Peer: React 19.
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
import { WorkbenchProvider, WorkbenchShell } from '@workbench-kit/shell-react';
|
|
21
|
+
|
|
22
|
+
export function App() {
|
|
23
|
+
return (
|
|
24
|
+
<WorkbenchProvider>
|
|
25
|
+
<WorkbenchShell title="Workbench" primarySidebar={<aside />} />
|
|
26
|
+
</WorkbenchProvider>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Prefer this package when the host needs provider + shell orchestration. For
|
|
32
|
+
layout-only chrome without host services, start from
|
|
33
|
+
`@workbench-kit/react/workbench/shell` — see
|
|
34
|
+
[Getting Started](../../docs/guides/getting-started.md).
|
|
35
|
+
|
|
36
|
+
## Field Remap embed (slim subpath)
|
|
37
|
+
|
|
38
|
+
Hosts that only need the Field Remap mapper UI can import the slim subpath instead
|
|
39
|
+
of the full shell barrel (avoids pulling workbench shell / extension-host surfaces
|
|
40
|
+
into the consumer import graph):
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import {
|
|
44
|
+
FieldRemapPanel,
|
|
45
|
+
FieldRemapFlowMapper,
|
|
46
|
+
createJsonataValueTransform,
|
|
47
|
+
} from '@workbench-kit/shell-react/field-remap';
|
|
48
|
+
import '@workbench-kit/shell-react/field-remap/view.css';
|
|
49
|
+
|
|
50
|
+
// Uncontrolled demo:
|
|
51
|
+
// <FieldRemapPanel sample="nested-ab" />
|
|
52
|
+
|
|
53
|
+
// Controlled (host-persisted edges):
|
|
54
|
+
// <FieldRemapPanel edges={edges} onEdgesChange={setEdges} sources={…} targets={…} sourceSample={…} />
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`FieldRemapFlowMapper` side-imports the same CSS; the explicit CSS export remains
|
|
58
|
+
for Flow-only embeds and custom bundler setups. The full barrel
|
|
59
|
+
`import { FieldRemapPanel } from '@workbench-kit/shell-react'` stays supported.
|
|
60
|
+
|
|
61
|
+
### Flow host chrome hooks
|
|
62
|
+
|
|
63
|
+
`FieldRemapFlowMapper` (and `FieldRemapPanel` pass-through) accept optional chrome
|
|
64
|
+
hooks so hosts avoid CSS/DOM workarounds:
|
|
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. |
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
const flowActionsRef = useRef<FieldRemapFlowActions | null>(null);
|
|
76
|
+
|
|
77
|
+
<FieldRemapFlowMapper
|
|
78
|
+
sources={sources}
|
|
79
|
+
targets={targets}
|
|
80
|
+
edges={edges}
|
|
81
|
+
transforms={registry}
|
|
82
|
+
onEdgesChange={setEdges}
|
|
83
|
+
showMinimap={false}
|
|
84
|
+
labels={{ bindingsTitle: 'Field maps' }}
|
|
85
|
+
flowActionsRef={flowActionsRef}
|
|
86
|
+
onPaneContextMenu={(event, { selection }) => {
|
|
87
|
+
event.preventDefault();
|
|
88
|
+
// host ContextMenu…
|
|
89
|
+
}}
|
|
90
|
+
/>;
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Related docs
|
|
94
|
+
|
|
95
|
+
- [Component Map](../../docs/guides/component-map.md)
|
|
96
|
+
- [Sample Screens](../../docs/guides/sample-screens.md)
|
|
97
|
+
- [Consumer Capabilities](../../docs/workbench/consumer-capabilities.md)
|
|
98
|
+
- Sample host: [`examples/workbench-sample`](../../examples/workbench-sample/README.md)
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@workbench-kit/shell-react",
|
|
3
|
+
"version": "0.0.2-prototype.0.2.10",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.ts",
|
|
8
|
+
"./field-remap": "./src/field-remap/index.ts",
|
|
9
|
+
"./field-remap/view.css": "./src/field-remap/view.css"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"src",
|
|
13
|
+
"!src/**/*.test.ts",
|
|
14
|
+
"!src/**/*.test.tsx",
|
|
15
|
+
"!src/**/*.stories.ts",
|
|
16
|
+
"!src/**/*.stories.tsx"
|
|
17
|
+
],
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@radix-ui/react-select": "^2.3.4",
|
|
20
|
+
"@radix-ui/react-slot": "^1.3.0",
|
|
21
|
+
"@xyflow/react": "^12.11.2",
|
|
22
|
+
"jsonata": "^2.2.0",
|
|
23
|
+
"react-table-mapping": "^1.0.3",
|
|
24
|
+
"@workbench-kit/platform": "0.0.2-prototype.0.2.10",
|
|
25
|
+
"@workbench-kit/tokens": "0.0.2-prototype.0.2.10",
|
|
26
|
+
"@workbench-kit/workbench-config": "0.0.2-prototype.0.2.10",
|
|
27
|
+
"@workbench-kit/react": "0.0.2-prototype.0.2.10",
|
|
28
|
+
"@workbench-kit/field-remap": "0.0.2-prototype.0.2.10",
|
|
29
|
+
"@workbench-kit/workbench-core": "0.0.2-prototype.0.2.10",
|
|
30
|
+
"@workbench-kit/workspace": "0.0.2-prototype.0.2.10"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"react": "^19.0.0",
|
|
34
|
+
"react-dom": "^19.0.0"
|
|
35
|
+
},
|
|
36
|
+
"description": "React shell and host assembly for Workbench Kit.",
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public",
|
|
39
|
+
"tag": "prototype",
|
|
40
|
+
"provenance": true
|
|
41
|
+
},
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "git+https://github.com/NewChoBo/workbench-kit.git",
|
|
45
|
+
"directory": "packages/shell-react"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"test": "vitest run --config vitest.config.ts src",
|
|
49
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { WorkbenchChatCommandProposalInput } from './use-chat-command-proposals.js';
|
|
2
|
+
|
|
3
|
+
const MOCK_AI_COMMAND_PROPOSALS: readonly WorkbenchChatCommandProposalInput[] = [
|
|
4
|
+
{
|
|
5
|
+
commandId: 'workbench.showActivity.explorer',
|
|
6
|
+
description: 'Focus the Explorer activity in the primary sidebar.',
|
|
7
|
+
id: 'mock-proposal-show-explorer',
|
|
8
|
+
label: 'Show Explorer',
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
commandId: 'workbench.togglePrimarySidebar',
|
|
12
|
+
description: 'Toggle the primary sidebar visibility.',
|
|
13
|
+
id: 'mock-proposal-toggle-sidebar',
|
|
14
|
+
label: 'Toggle Primary Sidebar',
|
|
15
|
+
},
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export function createMockAiChatCommandProposals(
|
|
19
|
+
userMessage: string,
|
|
20
|
+
): readonly WorkbenchChatCommandProposalInput[] {
|
|
21
|
+
const normalized = userMessage.trim().toLocaleLowerCase();
|
|
22
|
+
if (!normalized || normalized === 'help') {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return MOCK_AI_COMMAND_PROPOSALS;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createMockAiChatResponseContent(userMessage: string) {
|
|
30
|
+
const proposals = createMockAiChatCommandProposals(userMessage);
|
|
31
|
+
if (!proposals.length) {
|
|
32
|
+
return `Received: ${userMessage}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return [
|
|
36
|
+
`I can help with "${userMessage.trim()}".`,
|
|
37
|
+
'Review the suggested commands below. Safe commands run automatically; workspace changes require approval.',
|
|
38
|
+
].join('\n\n');
|
|
39
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export type WorkbenchChatCommandInputParseResult =
|
|
2
|
+
| { type: 'none' }
|
|
3
|
+
| { type: 'unknown'; token: string }
|
|
4
|
+
| { commandId: string; message: string; type: 'invalid-arguments' }
|
|
5
|
+
| { args: readonly unknown[]; commandId: string; type: 'run' };
|
|
6
|
+
|
|
7
|
+
export function parseWorkbenchChatCommandInput(
|
|
8
|
+
input: string,
|
|
9
|
+
isValidCommandId: (id: string) => boolean,
|
|
10
|
+
): WorkbenchChatCommandInputParseResult {
|
|
11
|
+
const trimmed = input.trim();
|
|
12
|
+
if (!trimmed.startsWith('/')) {
|
|
13
|
+
return { type: 'none' };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const [rawToken] = trimmed.slice(1).split(/\s+/, 1);
|
|
17
|
+
if (!rawToken) {
|
|
18
|
+
return { token: '', type: 'unknown' };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!isValidCommandId(rawToken)) {
|
|
22
|
+
return { token: rawToken, type: 'unknown' };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const argumentText = getSlashCommandArgumentText(trimmed, rawToken);
|
|
26
|
+
if (!argumentText) {
|
|
27
|
+
return {
|
|
28
|
+
args: [],
|
|
29
|
+
commandId: rawToken,
|
|
30
|
+
type: 'run',
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
return {
|
|
36
|
+
args: [JSON.parse(argumentText) as unknown],
|
|
37
|
+
commandId: rawToken,
|
|
38
|
+
type: 'run',
|
|
39
|
+
};
|
|
40
|
+
} catch {
|
|
41
|
+
return {
|
|
42
|
+
commandId: rawToken,
|
|
43
|
+
message:
|
|
44
|
+
'Command arguments must be valid JSON. Use /command.id {"key":"value"} or /command.id "value".',
|
|
45
|
+
type: 'invalid-arguments',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getSlashCommandArgumentText(trimmedInput: string, commandId: string) {
|
|
51
|
+
return trimmedInput.slice(1 + commandId.length).trim();
|
|
52
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ResolveWorkbenchCommandExecutionPolicyInput,
|
|
3
|
+
WorkbenchCommandExecutionPolicy,
|
|
4
|
+
} from '@workbench-kit/react/workbench';
|
|
5
|
+
import type { WorkbenchStorageReader } from '@workbench-kit/workbench-core';
|
|
6
|
+
|
|
7
|
+
import { readPersistedLocalPreferences } from '../management/preference-settings-storage.js';
|
|
8
|
+
|
|
9
|
+
export const WORKBENCH_AI_CHAT_COMMAND_DEFAULT_POLICY_KEY = 'workbench.chat.aiCommandDefaultPolicy';
|
|
10
|
+
|
|
11
|
+
function isWorkbenchCommandExecutionPolicy(
|
|
12
|
+
value: unknown,
|
|
13
|
+
): value is WorkbenchCommandExecutionPolicy {
|
|
14
|
+
return value === 'auto-allow' || value === 'approval-required' || value === 'auto-deny';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function readWorkbenchAiChatCommandPolicyInput(
|
|
18
|
+
storageKey?: string,
|
|
19
|
+
storage?: WorkbenchStorageReader,
|
|
20
|
+
): ResolveWorkbenchCommandExecutionPolicyInput {
|
|
21
|
+
const preferences = readPersistedLocalPreferences(storageKey, storage);
|
|
22
|
+
const configuredDefault = preferences[WORKBENCH_AI_CHAT_COMMAND_DEFAULT_POLICY_KEY];
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
defaultPolicy: isWorkbenchCommandExecutionPolicy(configuredDefault)
|
|
26
|
+
? configuredDefault
|
|
27
|
+
: undefined,
|
|
28
|
+
mutatingDefaultPolicy: 'approval-required',
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createWorkbenchAiChatCommandPolicyById(
|
|
33
|
+
entries: Readonly<Record<string, WorkbenchCommandExecutionPolicy>>,
|
|
34
|
+
): ResolveWorkbenchCommandExecutionPolicyInput {
|
|
35
|
+
return {
|
|
36
|
+
policyByCommandId: entries,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function mergeWorkbenchAiChatCommandPolicyInput(
|
|
41
|
+
...inputs: readonly ResolveWorkbenchCommandExecutionPolicyInput[]
|
|
42
|
+
): ResolveWorkbenchCommandExecutionPolicyInput {
|
|
43
|
+
return inputs.reduce<ResolveWorkbenchCommandExecutionPolicyInput>(
|
|
44
|
+
(merged, input) => ({
|
|
45
|
+
defaultPolicy: input.defaultPolicy ?? merged.defaultPolicy,
|
|
46
|
+
mutatingDefaultPolicy: input.mutatingDefaultPolicy ?? merged.mutatingDefaultPolicy,
|
|
47
|
+
policyByCommandId: {
|
|
48
|
+
...merged.policyByCommandId,
|
|
49
|
+
...input.policyByCommandId,
|
|
50
|
+
},
|
|
51
|
+
}),
|
|
52
|
+
{},
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { useCallback, useMemo, useRef, type KeyboardEvent } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
WorkbenchCommandSuggest,
|
|
4
|
+
isWorkbenchCommandRunnable,
|
|
5
|
+
useSlashCommandSuggest,
|
|
6
|
+
type WorkbenchCommandDescriptor,
|
|
7
|
+
} from '@workbench-kit/react/workbench';
|
|
8
|
+
|
|
9
|
+
import { parseWorkbenchChatCommandInput } from './command-input.js';
|
|
10
|
+
import { useWorkbench } from '../shell/provider.js';
|
|
11
|
+
import { useWorkbenchCommandDescriptors } from '../commands/use-command-descriptors.js';
|
|
12
|
+
|
|
13
|
+
export interface WorkbenchChatCommandRunResult {
|
|
14
|
+
commandId: string;
|
|
15
|
+
label?: string | undefined;
|
|
16
|
+
message?: string | undefined;
|
|
17
|
+
status: 'error' | 'success';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface WorkbenchChatCommandSurfaceOptions {
|
|
21
|
+
additionalCommands?: readonly WorkbenchCommandDescriptor[] | undefined;
|
|
22
|
+
onCommandResult?: ((result: WorkbenchChatCommandRunResult) => void) | undefined;
|
|
23
|
+
onValueChange: (value: string) => void;
|
|
24
|
+
value: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function useWorkbenchChatCommandSurface({
|
|
28
|
+
additionalCommands,
|
|
29
|
+
onCommandResult,
|
|
30
|
+
onValueChange,
|
|
31
|
+
value,
|
|
32
|
+
}: WorkbenchChatCommandSurfaceOptions) {
|
|
33
|
+
const { executeCommand } = useWorkbench();
|
|
34
|
+
const composerRef = useRef<HTMLTextAreaElement>(null);
|
|
35
|
+
const commands = useWorkbenchCommandDescriptors(additionalCommands);
|
|
36
|
+
const commandIds = useMemo(() => new Set(commands.map((command) => command.id)), [commands]);
|
|
37
|
+
const suggest = useSlashCommandSuggest({ commands, value });
|
|
38
|
+
|
|
39
|
+
const runCommand = useCallback(
|
|
40
|
+
async (commandId: string, args: readonly unknown[] = []) => {
|
|
41
|
+
const command = commands.find((candidate) => candidate.id === commandId);
|
|
42
|
+
if (command && !isWorkbenchCommandRunnable(command)) {
|
|
43
|
+
onCommandResult?.({
|
|
44
|
+
commandId,
|
|
45
|
+
label: command.label,
|
|
46
|
+
message: command.disabledReason ?? 'Command is currently unavailable.',
|
|
47
|
+
status: 'error',
|
|
48
|
+
});
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
onValueChange('');
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
await executeCommand(commandId, ...args);
|
|
56
|
+
onCommandResult?.({
|
|
57
|
+
commandId,
|
|
58
|
+
label: command?.label,
|
|
59
|
+
status: 'success',
|
|
60
|
+
});
|
|
61
|
+
} catch (error) {
|
|
62
|
+
onCommandResult?.({
|
|
63
|
+
commandId,
|
|
64
|
+
label: command?.label,
|
|
65
|
+
message: getCommandErrorMessage(error),
|
|
66
|
+
status: 'error',
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
[commands, executeCommand, onCommandResult, onValueChange],
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const runInputAsCommand = useCallback(
|
|
74
|
+
(message: string) => {
|
|
75
|
+
const parsed = parseWorkbenchChatCommandInput(message, (commandId) =>
|
|
76
|
+
commandIds.has(commandId),
|
|
77
|
+
);
|
|
78
|
+
if (parsed.type === 'none') {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (parsed.type === 'unknown') {
|
|
83
|
+
onValueChange('');
|
|
84
|
+
onCommandResult?.({
|
|
85
|
+
commandId: parsed.token || '/',
|
|
86
|
+
message: parsed.token ? `Unknown command: ${parsed.token}` : 'Type a command after /.',
|
|
87
|
+
status: 'error',
|
|
88
|
+
});
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (parsed.type === 'invalid-arguments') {
|
|
93
|
+
onValueChange('');
|
|
94
|
+
onCommandResult?.({
|
|
95
|
+
commandId: parsed.commandId,
|
|
96
|
+
message: parsed.message,
|
|
97
|
+
status: 'error',
|
|
98
|
+
});
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
void runCommand(parsed.commandId, parsed.args);
|
|
103
|
+
return true;
|
|
104
|
+
},
|
|
105
|
+
[commandIds, onCommandResult, onValueChange, runCommand],
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const handleKeyDown = useCallback(
|
|
109
|
+
(event: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
110
|
+
suggest.handleKeyDown(event, (commandId) => {
|
|
111
|
+
void runCommand(commandId);
|
|
112
|
+
});
|
|
113
|
+
},
|
|
114
|
+
[runCommand, suggest],
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const openCommandSuggest = useCallback(() => {
|
|
118
|
+
if (!value.trimStart().startsWith('/')) {
|
|
119
|
+
onValueChange('/');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
window.requestAnimationFrame(() => {
|
|
123
|
+
composerRef.current?.focus();
|
|
124
|
+
});
|
|
125
|
+
}, [onValueChange, value]);
|
|
126
|
+
|
|
127
|
+
const commandSuggestPopover = (
|
|
128
|
+
<WorkbenchCommandSuggest
|
|
129
|
+
activeCommandId={suggest.activeCommandId}
|
|
130
|
+
commands={suggest.suggestedCommands}
|
|
131
|
+
query={suggest.commandQuery}
|
|
132
|
+
visible={suggest.isOpen}
|
|
133
|
+
onActiveCommandChange={suggest.setActiveCommandByKey}
|
|
134
|
+
onRunCommand={(command) => {
|
|
135
|
+
void runCommand(command.id);
|
|
136
|
+
}}
|
|
137
|
+
/>
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
commandSuggestPopover,
|
|
142
|
+
composerRef,
|
|
143
|
+
onCommandClick: openCommandSuggest,
|
|
144
|
+
onKeyDown: handleKeyDown,
|
|
145
|
+
runInputAsCommand,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function getCommandErrorMessage(error: unknown) {
|
|
150
|
+
if (error instanceof Error) {
|
|
151
|
+
return error.message;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return String(error);
|
|
155
|
+
}
|
|
@@ -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 './command-surface.js';
|
|
11
|
+
import { useWorkbench } from '../shell/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,20 @@
|
|
|
1
|
+
export const BUILTIN_CHAT_VIEW_RENDER_KIND = 'workbench-kit.builtin.chat.view' as const;
|
|
2
|
+
|
|
3
|
+
export type BuiltinChatViewMode = 'aiChat' | 'chatting';
|
|
4
|
+
|
|
5
|
+
export interface BuiltinChatViewRenderData {
|
|
6
|
+
readonly kind: typeof BUILTIN_CHAT_VIEW_RENDER_KIND;
|
|
7
|
+
readonly mode: BuiltinChatViewMode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function isBuiltinChatViewRenderData(value: unknown): value is BuiltinChatViewRenderData {
|
|
11
|
+
if (typeof value !== 'object' || value === null) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const candidate = value as Partial<BuiltinChatViewRenderData>;
|
|
16
|
+
return (
|
|
17
|
+
candidate.kind === BUILTIN_CHAT_VIEW_RENDER_KIND &&
|
|
18
|
+
(candidate.mode === 'chatting' || candidate.mode === 'aiChat')
|
|
19
|
+
);
|
|
20
|
+
}
|