@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.
Files changed (105) hide show
  1. package/README.md +98 -0
  2. package/package.json +51 -0
  3. package/src/chat/ai-mock-proposals.ts +39 -0
  4. package/src/chat/command-input.ts +52 -0
  5. package/src/chat/command-policy.ts +54 -0
  6. package/src/chat/command-surface.tsx +155 -0
  7. package/src/chat/use-chat-command-proposals.ts +194 -0
  8. package/src/chat/view-data.ts +20 -0
  9. package/src/chat/view.tsx +220 -0
  10. package/src/commands/inspector-surface.tsx +46 -0
  11. package/src/commands/use-command-descriptors.ts +41 -0
  12. package/src/commands/use-context-key-revision.ts +18 -0
  13. package/src/commands/view-data.ts +17 -0
  14. package/src/commands/view.tsx +43 -0
  15. package/src/devtools/WorkbenchDevtoolsPanel.tsx +124 -0
  16. package/src/devtools/WorkbenchDevtoolsShell.tsx +17 -0
  17. package/src/devtools/index.ts +11 -0
  18. package/src/devtools/use-workbench-devtools-snapshot.ts +84 -0
  19. package/src/devtools/workbench-devtools-snapshot.ts +140 -0
  20. package/src/devtools/workbench-devtools.css +82 -0
  21. package/src/editor/area-dnd.ts +69 -0
  22. package/src/editor/area-model.ts +71 -0
  23. package/src/editor/area.css +454 -0
  24. package/src/editor/area.tsx +676 -0
  25. package/src/editor/host-surface.tsx +691 -0
  26. package/src/editor/markdown-document-view-data.ts +16 -0
  27. package/src/editor/pane-visibility.ts +130 -0
  28. package/src/editor/resource.ts +36 -0
  29. package/src/editor/state-storage.ts +182 -0
  30. package/src/editor/tab-context-menu.ts +118 -0
  31. package/src/editor/use-editor.ts +138 -0
  32. package/src/editor/view-providers.tsx +504 -0
  33. package/src/editor/workspace-file-availability.ts +15 -0
  34. package/src/editor/workspace-reconcile.tsx +31 -0
  35. package/src/explorer/context-menu.ts +114 -0
  36. package/src/explorer/create-command-workspace-explorer-port.ts +86 -0
  37. package/src/explorer/reveal.ts +112 -0
  38. package/src/explorer/search-view-data.ts +15 -0
  39. package/src/explorer/search-view.tsx +65 -0
  40. package/src/explorer/use-active-workspace-path.ts +15 -0
  41. package/src/explorer/view-data.ts +58 -0
  42. package/src/explorer/view.tsx +227 -0
  43. package/src/extensions/context-menu.ts +52 -0
  44. package/src/extensions/management-model.ts +313 -0
  45. package/src/extensions/use-extension-management.ts +241 -0
  46. package/src/extensions/view-data.ts +19 -0
  47. package/src/extensions/view.tsx +63 -0
  48. package/src/field-remap/chrome-labels.ts +74 -0
  49. package/src/field-remap/convert-note-editor.tsx +206 -0
  50. package/src/field-remap/convert-palette.tsx +126 -0
  51. package/src/field-remap/demo.tsx +105 -0
  52. package/src/field-remap/detail-panel.tsx +659 -0
  53. package/src/field-remap/editor-surface.tsx +36 -0
  54. package/src/field-remap/flow-adapter.ts +838 -0
  55. package/src/field-remap/flow-ops.ts +442 -0
  56. package/src/field-remap/flow.tsx +948 -0
  57. package/src/field-remap/graph.tsx +559 -0
  58. package/src/field-remap/index.ts +88 -0
  59. package/src/field-remap/io-class-browse.tsx +145 -0
  60. package/src/field-remap/jsonata-transform.ts +166 -0
  61. package/src/field-remap/panel.tsx +466 -0
  62. package/src/field-remap/samples.ts +507 -0
  63. package/src/field-remap/shape-io-editor.tsx +170 -0
  64. package/src/field-remap/table-mapping-adapter.ts +59 -0
  65. package/src/field-remap/transform-options-editor.tsx +312 -0
  66. package/src/field-remap/tree.tsx +404 -0
  67. package/src/field-remap/view-data.ts +16 -0
  68. package/src/field-remap/view.css +1367 -0
  69. package/src/field-remap/view.tsx +86 -0
  70. package/src/index.ts +294 -0
  71. package/src/jdw/document-view-data.ts +31 -0
  72. package/src/jdw/json-form-source-patch.ts +323 -0
  73. package/src/jdw/lab-view-data.ts +25 -0
  74. package/src/jdw/lab-view.css +21 -0
  75. package/src/jdw/lab-view.tsx +79 -0
  76. package/src/jdw/widget-form-view.tsx +68 -0
  77. package/src/jdw/widget-preview-view.tsx +54 -0
  78. package/src/management/keybinding-overrides-storage.ts +49 -0
  79. package/src/management/keybinding-settings.tsx +16 -0
  80. package/src/management/palette-commands.ts +38 -0
  81. package/src/management/preference-settings-storage.ts +50 -0
  82. package/src/management/settings-ids.ts +12 -0
  83. package/src/management/settings.tsx +96 -0
  84. package/src/management/use-command-management.ts +191 -0
  85. package/src/management/use-keybinding-management.ts +130 -0
  86. package/src/shell/model.tsx +149 -0
  87. package/src/shell/provider.tsx +606 -0
  88. package/src/shell/secondary-actions.tsx +59 -0
  89. package/src/shell/settings-constants.ts +9 -0
  90. package/src/shell/settings.tsx +599 -0
  91. package/src/shell/shell.tsx +680 -0
  92. package/src/shell/titlebar-layout-controls.tsx +4 -0
  93. package/src/shell/view-host.tsx +212 -0
  94. package/src/workbench/appearance-storage.ts +107 -0
  95. package/src/workbench/command-host.tsx +243 -0
  96. package/src/workbench/command-palette.ts +208 -0
  97. package/src/workbench/keybinding-bridge.ts +54 -0
  98. package/src/workbench/layout-storage.ts +134 -0
  99. package/src/workbench/profile-modal.tsx +118 -0
  100. package/src/workbench/shell-command-registration.ts +58 -0
  101. package/src/workbench/startup-gate.tsx +136 -0
  102. package/src/workbench/status-sections.ts +43 -0
  103. package/src/workbench/use-persisted-appearance.ts +33 -0
  104. package/src/workbench/user-commands.ts +46 -0
  105. 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
+ }