@workbench-kit/react 0.0.1-prototype.0 → 0.0.2-prototype.0.1.2

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 (155) hide show
  1. package/package.json +33 -9
  2. package/src/index.ts +545 -43
  3. package/src/jdw/JdwPreview.tsx +38 -0
  4. package/src/jdw/JsonCodeEditorPane.tsx +274 -0
  5. package/src/jdw/builtins/renderBuiltinWidgetLeaf.tsx +29 -0
  6. package/src/jdw/builtins/renderBuiltinWidgetNode.tsx +126 -0
  7. package/src/jdw/createBuiltinJdwRegistry.ts +111 -0
  8. package/src/jdw/cssRenderBackend.tsx +134 -0
  9. package/src/jdw/document.ts +38 -0
  10. package/src/jdw/fixtures/jdw-fixtures.ts +52 -0
  11. package/src/jdw/fixtures/jdw-sample-screens.ts +522 -0
  12. package/src/jdw/index.ts +45 -0
  13. package/src/jdw/parse.ts +1 -0
  14. package/src/jdw/renderJdw.tsx +32 -0
  15. package/src/json-config/JsonConfigWorkbench.tsx +316 -0
  16. package/src/json-config/index.ts +6 -0
  17. package/src/json-config/json-config-editor-state.ts +42 -0
  18. package/src/layout/Panel.tsx +146 -1
  19. package/src/layout/SideBarViewFrame.tsx +367 -328
  20. package/src/layout/WorkbenchCanvas.tsx +849 -0
  21. package/src/layout/WorkbenchFullscreen.tsx +368 -0
  22. package/src/layout/WorkbenchLayout.tsx +5 -0
  23. package/src/layout/WorkbenchLayoutBase.tsx +294 -0
  24. package/src/layout/WorkbenchPropertyPanel.tsx +497 -0
  25. package/src/layout/WorkbenchSidebarActions.tsx +274 -0
  26. package/src/layout/WorkbenchTree.tsx +351 -0
  27. package/src/layout/layoutHelpers.ts +25 -0
  28. package/src/modal/ConfirmDialog.tsx +61 -53
  29. package/src/modal/Modal.tsx +295 -35
  30. package/src/overlay/ContextMenu.tsx +146 -145
  31. package/src/primitives/AbsoluteBox.tsx +46 -0
  32. package/src/primitives/Badge.tsx +12 -12
  33. package/src/primitives/Button.tsx +44 -14
  34. package/src/primitives/Checkbox.tsx +11 -5
  35. package/src/primitives/EmptyState.tsx +27 -26
  36. package/src/primitives/Field.tsx +38 -38
  37. package/src/primitives/IconButton.tsx +5 -2
  38. package/src/primitives/List.tsx +111 -0
  39. package/src/primitives/NumberInput.tsx +38 -0
  40. package/src/primitives/ScrollArea.tsx +42 -0
  41. package/src/primitives/Select.tsx +1 -12
  42. package/src/primitives/StatusBar.tsx +46 -0
  43. package/src/primitives/TextArea.tsx +36 -0
  44. package/src/primitives/TextInput.tsx +11 -2
  45. package/src/primitives/WorkbenchChrome.tsx +424 -0
  46. package/src/primitives/WorkbenchEditor.tsx +394 -0
  47. package/src/primitives/index.ts +105 -0
  48. package/src/primitives/select/Select.tsx +266 -0
  49. package/src/primitives/select/index.ts +2 -0
  50. package/src/primitives/select/options.ts +38 -0
  51. package/src/primitives/select/overlay.ts +63 -0
  52. package/src/primitives/select/select.app.css +9 -0
  53. package/src/primitives/select/select.css +140 -0
  54. package/src/primitives/select/types.ts +18 -0
  55. package/src/primitives/styles.css +955 -0
  56. package/src/scrollbars.css +69 -0
  57. package/src/styles.css +7738 -1762
  58. package/src/utils/codicon.ts +17 -0
  59. package/src/widget-asset/WidgetAssetEditor.tsx +140 -0
  60. package/src/widget-asset/WidgetAssetMetadataForm.tsx +82 -0
  61. package/src/widget-asset/WidgetAssetModeControls.tsx +42 -0
  62. package/src/widget-asset/WidgetAssetSourceEditor.tsx +50 -0
  63. package/src/widget-asset/WidgetAssetWorkbench.tsx +112 -0
  64. package/src/widget-asset/index.ts +17 -0
  65. package/src/widget-asset/widget-asset-document.ts +37 -0
  66. package/src/widget-asset/widget-asset-mode.ts +3 -0
  67. package/src/widget-studio/builtin-widget-asset-catalog.ts +12 -0
  68. package/src/widget-studio/create-widget-studio-workspace-editor.tsx +79 -0
  69. package/src/widget-studio/index.ts +7 -0
  70. package/src/widget-studio/resolve-widget-studio-asset-catalog.ts +26 -0
  71. package/src/widget-tree/WidgetAssetPalette.tsx +77 -0
  72. package/src/widget-tree/WidgetInspectorPanel.tsx +266 -0
  73. package/src/widget-tree/WidgetSourceEditor.tsx +56 -0
  74. package/src/widget-tree/WidgetTreeLab.tsx +272 -0
  75. package/src/widget-tree/WidgetTreeModeControls.tsx +44 -0
  76. package/src/widget-tree/WidgetTreeSidePanel.tsx +49 -0
  77. package/src/widget-tree/WidgetTreeView.tsx +97 -0
  78. package/src/widget-tree/WidgetTreeWorkbench.tsx +111 -0
  79. package/src/widget-tree/WidgetTreeWorkspaceShell.tsx +154 -0
  80. package/src/widget-tree/demo-registry.ts +32 -0
  81. package/src/widget-tree/demo-widget-assets.ts +4 -0
  82. package/src/widget-tree/index.ts +27 -0
  83. package/src/widget-tree/widget-tree-document.ts +20 -0
  84. package/src/widget-tree/widget-tree-layout.ts +31 -0
  85. package/src/widget-tree/widget-tree-mode.ts +7 -0
  86. package/src/widget-tree/widget-tree-play-helpers.ts +74 -0
  87. package/src/workbench/ActivityBar.tsx +52 -52
  88. package/src/workbench/ArtifactShell.tsx +397 -0
  89. package/src/workbench/CommandPalette.tsx +819 -0
  90. package/src/workbench/ConfirmationFlow.tsx +245 -0
  91. package/src/workbench/MarkdownPreview.tsx +151 -0
  92. package/src/workbench/ShortcutCommandBridge.tsx +368 -0
  93. package/src/workbench/SplitView.tsx +136 -136
  94. package/src/workbench/StatusBar.tsx +178 -123
  95. package/src/workbench/StructuredArtifactEditor.tsx +205 -0
  96. package/src/workbench/Timeline.tsx +302 -0
  97. package/src/workbench/WorkbenchCanvasShell.tsx +941 -0
  98. package/src/workbench/WorkbenchShell.tsx +74 -70
  99. package/src/workbench/WorkbenchStandaloneShell.tsx +318 -291
  100. package/src/workbench/auth/WorkbenchAuthGate.tsx +61 -0
  101. package/src/workbench/auth/WorkbenchLoginView.tsx +635 -0
  102. package/src/workbench/auth/index.ts +19 -0
  103. package/src/workbench/chat/ChatComposer.tsx +165 -148
  104. package/src/workbench/chat/ChatMessageItem.tsx +10 -1
  105. package/src/workbench/chat/index.ts +9 -0
  106. package/src/workbench/chat/slashCommand.ts +28 -0
  107. package/src/workbench/chat/useChatRuntimeState.ts +76 -0
  108. package/src/workbench/chat/useSlashCommandSuggest.ts +101 -0
  109. package/src/workbench/commands.ts +520 -490
  110. package/src/workbench/index.ts +443 -98
  111. package/src/workbench/keyboard.ts +53 -0
  112. package/src/workbench/schema/index.ts +48 -0
  113. package/src/workbench/schema/workbenchDocument.ts +31 -0
  114. package/src/workbench/schema/workbenchDocumentActions.ts +6 -0
  115. package/src/workbench/schema/workbenchDocumentAdapter.ts +9 -0
  116. package/src/workbench/schema/workbenchDocumentPatch.ts +13 -0
  117. package/src/workbench/schema/workbenchDocumentRenderer.tsx +702 -0
  118. package/src/workbench/settings/NavigationPanel.tsx +47 -0
  119. package/src/workbench/settings/SchemaForm.tsx +420 -0
  120. package/src/workbench/settings/SectionedPanel.tsx +415 -0
  121. package/src/workbench/settings/StructuredDataForm.tsx +539 -0
  122. package/src/workbench/settings/StructuredDataSchemaPanel.tsx +559 -0
  123. package/src/workbench/settings/StructuredDataSchemaPanelEmbed.tsx +21 -0
  124. package/src/workbench/settings/StructuredDataSchemaPanelFrame.tsx +76 -0
  125. package/src/workbench/settings/StructuredDataTableView.tsx +127 -0
  126. package/src/workbench/settings/WorkbenchPanelRegion.tsx +46 -0
  127. package/src/workbench/settings/WorkbenchSettingsModal.tsx +21 -12
  128. package/src/workbench/settings/WorkbenchSettingsNav.tsx +38 -22
  129. package/src/workbench/settings/index.ts +135 -0
  130. package/src/workbench/settings/panel-region.css +21 -0
  131. package/src/workbench/settings/sectionedPanelScrollSpy.ts +143 -0
  132. package/src/workbench/settings/structured-data-schema-panel.css +330 -0
  133. package/src/workbench/settings/structured-data-table-view.css +85 -0
  134. package/src/workbench/settings/structuredDataSchema.ts +886 -0
  135. package/src/workbench/shell.ts +15 -0
  136. package/src/workbench/shellState.ts +203 -203
  137. package/src/workbench/standalone.ts +101 -94
  138. package/src/workbench/status.ts +129 -0
  139. package/src/workbench/theme.ts +44 -0
  140. package/src/workbench/workspace/MultiProviderExplorer.tsx +625 -0
  141. package/src/workbench/workspace/WorkspaceDraftsContext.tsx +111 -0
  142. package/src/workbench/workspace/WorkspaceEditor.tsx +47 -12
  143. package/src/workbench/workspace/WorkspaceEditorPanel.tsx +334 -353
  144. package/src/workbench/workspace/WorkspaceExplorer.tsx +532 -524
  145. package/src/workbench/workspace/WorkspaceFileIcon.tsx +32 -2
  146. package/src/workbench/workspace/WorkspaceSearchPanel.tsx +113 -113
  147. package/src/workbench/workspace/WorkspaceSearchResults.tsx +47 -47
  148. package/src/workbench/workspace/index.ts +156 -98
  149. package/src/workbench/workspace/mimeType.ts +45 -0
  150. package/src/workbench/workspace/path.ts +10 -10
  151. package/src/workbench/workspace/search.ts +6 -6
  152. package/src/workbench/workspace/tree.ts +1 -1
  153. package/src/workbench/workspace/types.ts +10 -10
  154. package/src/workbench/workspace/useVirtualWorkspace.ts +111 -98
  155. package/src/workbench/workspace/workspaceJsonDiagnostics.ts +83 -0
@@ -1,148 +1,165 @@
1
- import {
2
- useLayoutEffect,
3
- useRef,
4
- type KeyboardEvent,
5
- type ReactNode,
6
- type TextareaHTMLAttributes,
7
- } from 'react';
8
- import { cx } from '../../utils/cx';
9
-
10
- export interface ChatComposerProps extends Omit<
11
- TextareaHTMLAttributes<HTMLTextAreaElement>,
12
- 'onChange' | 'onSubmit' | 'value'
13
- > {
14
- cancelLabel?: string;
15
- commandLabel?: string;
16
- contextLabel?: string;
17
- isRunning?: boolean;
18
- onCancel?: () => void;
19
- onSubmit: (message: string) => void;
20
- onValueChange: (value: string) => void;
21
- showTools?: boolean;
22
- submitLabel?: string;
23
- toolbarStart?: ReactNode;
24
- value: string;
25
- }
26
-
27
- export function ChatComposer({
28
- cancelLabel = 'Stop response',
29
- className,
30
- commandLabel = 'Open commands',
31
- contextLabel = 'Add context',
32
- disabled,
33
- isRunning = false,
34
- onCancel,
35
- onSubmit,
36
- onValueChange,
37
- placeholder = 'Type a message...',
38
- showTools = true,
39
- submitLabel = 'Send message',
40
- toolbarStart,
41
- value,
42
- ...props
43
- }: ChatComposerProps) {
44
- const textareaRef = useRef<HTMLTextAreaElement>(null);
45
-
46
- const resizeTextarea = () => {
47
- const element = textareaRef.current;
48
- if (!element) return;
49
-
50
- element.style.height = 'auto';
51
- element.style.height = `${Math.min(element.scrollHeight, 160)}px`;
52
- };
53
-
54
- useLayoutEffect(() => {
55
- resizeTextarea();
56
- }, [value]);
57
-
58
- const handleSubmit = () => {
59
- const trimmed = value.trim();
60
- if (!trimmed || disabled || isRunning) return;
61
-
62
- onSubmit(trimmed);
63
- window.requestAnimationFrame(() => {
64
- if (!textareaRef.current) return;
65
-
66
- textareaRef.current.style.height = 'auto';
67
- textareaRef.current.focus();
68
- });
69
- };
70
-
71
- const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
72
- props.onKeyDown?.(event);
73
- if (event.defaultPrevented) return;
74
-
75
- if (event.key === 'Enter' && !event.shiftKey) {
76
- event.preventDefault();
77
- handleSubmit();
78
- }
79
- };
80
-
81
- return (
82
- <div className="composer">
83
- <div className="composer__box">
84
- <textarea
85
- ref={textareaRef}
86
- {...props}
87
- className={cx('composer__textarea', className)}
88
- disabled={disabled}
89
- placeholder={placeholder}
90
- rows={1}
91
- value={value}
92
- onChange={(event) => onValueChange(event.currentTarget.value)}
93
- onInput={resizeTextarea}
94
- onKeyDown={handleKeyDown}
95
- />
96
- <div className="composer__toolbar">
97
- <div className="composer__toolbar-left">
98
- {toolbarStart}
99
- {showTools ? (
100
- <>
101
- <button
102
- aria-label={contextLabel}
103
- className="composer__tool-btn"
104
- title={contextLabel}
105
- type="button"
106
- >
107
- <i className="codicon codicon-add" />
108
- </button>
109
- <button
110
- aria-label={commandLabel}
111
- className="composer__tool-btn"
112
- title={commandLabel}
113
- type="button"
114
- >
115
- <i className="codicon codicon-terminal" />
116
- </button>
117
- </>
118
- ) : null}
119
- </div>
120
- <div className="composer__toolbar-right">
121
- {isRunning ? (
122
- <button
123
- aria-label={cancelLabel}
124
- className="composer__send-btn composer__send-btn--cancel"
125
- title={cancelLabel}
126
- type="button"
127
- onClick={onCancel}
128
- >
129
- <i className="codicon codicon-stop-circle" />
130
- </button>
131
- ) : (
132
- <button
133
- aria-label={submitLabel}
134
- className="composer__send-btn"
135
- disabled={disabled || !value.trim()}
136
- title={submitLabel}
137
- type="button"
138
- onClick={handleSubmit}
139
- >
140
- <i className="codicon codicon-send" />
141
- </button>
142
- )}
143
- </div>
144
- </div>
145
- </div>
146
- </div>
147
- );
148
- }
1
+ import {
2
+ forwardRef,
3
+ useImperativeHandle,
4
+ useLayoutEffect,
5
+ useRef,
6
+ type KeyboardEvent,
7
+ type ReactNode,
8
+ type TextareaHTMLAttributes,
9
+ } from 'react';
10
+ import { cx } from '../../utils/cx';
11
+
12
+ export interface ChatComposerProps extends Omit<
13
+ TextareaHTMLAttributes<HTMLTextAreaElement>,
14
+ 'onChange' | 'onSubmit' | 'value'
15
+ > {
16
+ cancelLabel?: string;
17
+ commandLabel?: string;
18
+ commandSuggestPopover?: ReactNode;
19
+ contextLabel?: string;
20
+ isRunning?: boolean;
21
+ onCancel?: () => void;
22
+ onCommandClick?: () => void;
23
+ onContextClick?: () => void;
24
+ onSubmit: (message: string) => void;
25
+ onValueChange: (value: string) => void;
26
+ showTools?: boolean;
27
+ submitLabel?: string;
28
+ toolbarStart?: ReactNode;
29
+ value: string;
30
+ }
31
+
32
+ export const ChatComposer = forwardRef<HTMLTextAreaElement, ChatComposerProps>(
33
+ function ChatComposer(
34
+ {
35
+ cancelLabel = 'Stop response',
36
+ className,
37
+ commandLabel = 'Open commands',
38
+ commandSuggestPopover,
39
+ contextLabel = 'Add context',
40
+ disabled,
41
+ isRunning = false,
42
+ onCancel,
43
+ onCommandClick,
44
+ onContextClick,
45
+ onSubmit,
46
+ onValueChange,
47
+ placeholder = 'Type a message...',
48
+ showTools = true,
49
+ submitLabel = 'Send message',
50
+ toolbarStart,
51
+ value,
52
+ ...props
53
+ }: ChatComposerProps,
54
+ ref,
55
+ ) {
56
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
57
+ useImperativeHandle(ref, () => textareaRef.current!);
58
+
59
+ const resizeTextarea = () => {
60
+ const element = textareaRef.current;
61
+ if (!element) return;
62
+
63
+ element.style.height = 'auto';
64
+ element.style.height = `${Math.min(element.scrollHeight, 160)}px`;
65
+ };
66
+
67
+ useLayoutEffect(() => {
68
+ resizeTextarea();
69
+ }, [value]);
70
+
71
+ const handleSubmit = () => {
72
+ const trimmed = value.trim();
73
+ if (!trimmed || disabled || isRunning) return;
74
+
75
+ onSubmit(trimmed);
76
+ window.requestAnimationFrame(() => {
77
+ if (!textareaRef.current) return;
78
+
79
+ textareaRef.current.style.height = 'auto';
80
+ textareaRef.current.focus();
81
+ });
82
+ };
83
+
84
+ const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
85
+ props.onKeyDown?.(event);
86
+ if (event.defaultPrevented) return;
87
+
88
+ if (event.key === 'Enter' && !event.shiftKey) {
89
+ event.preventDefault();
90
+ handleSubmit();
91
+ }
92
+ };
93
+
94
+ return (
95
+ <div className="composer">
96
+ {commandSuggestPopover}
97
+ <div className="composer__box">
98
+ <textarea
99
+ ref={textareaRef}
100
+ {...props}
101
+ className={cx('composer__textarea', 'ui-workbench-scrollbar', className)}
102
+ disabled={disabled}
103
+ placeholder={placeholder}
104
+ rows={1}
105
+ value={value}
106
+ onChange={(event) => onValueChange(event.currentTarget.value)}
107
+ onInput={resizeTextarea}
108
+ onKeyDown={handleKeyDown}
109
+ />
110
+ <div className="composer__toolbar">
111
+ <div className="composer__toolbar-left">
112
+ {toolbarStart}
113
+ {showTools ? (
114
+ <>
115
+ <button
116
+ aria-label={contextLabel}
117
+ className="composer__tool-btn"
118
+ title={contextLabel}
119
+ type="button"
120
+ onClick={onContextClick}
121
+ >
122
+ <i className="codicon codicon-add" />
123
+ </button>
124
+ <button
125
+ aria-label={commandLabel}
126
+ className="composer__tool-btn"
127
+ title={commandLabel}
128
+ type="button"
129
+ onClick={onCommandClick}
130
+ >
131
+ <i className="codicon codicon-terminal" />
132
+ </button>
133
+ </>
134
+ ) : null}
135
+ </div>
136
+ <div className="composer__toolbar-right">
137
+ {isRunning ? (
138
+ <button
139
+ aria-label={cancelLabel}
140
+ className="composer__send-btn composer__send-btn--cancel"
141
+ title={cancelLabel}
142
+ type="button"
143
+ onClick={onCancel}
144
+ >
145
+ <i className="codicon codicon-stop-circle" />
146
+ </button>
147
+ ) : (
148
+ <button
149
+ aria-label={submitLabel}
150
+ className="composer__send-btn"
151
+ disabled={disabled || !value.trim()}
152
+ title={submitLabel}
153
+ type="button"
154
+ onClick={handleSubmit}
155
+ >
156
+ <i className="codicon codicon-send" />
157
+ </button>
158
+ )}
159
+ </div>
160
+ </div>
161
+ </div>
162
+ </div>
163
+ );
164
+ },
165
+ );
@@ -1,4 +1,5 @@
1
1
  import Markdown from 'react-markdown';
2
+ import { cx } from '../../utils/cx';
2
3
  import type { ChatMessage } from './types';
3
4
 
4
5
  export interface ChatMessageItemProps {
@@ -27,7 +28,15 @@ export function ChatMessageItem({
27
28
  {message.label ?? assistantLabel}
28
29
  </div>
29
30
  <div className="md-content">
30
- <Markdown>{message.content}</Markdown>
31
+ <Markdown
32
+ components={{
33
+ code: ({ className, ...props }) => (
34
+ <code className={cx('ui-workbench-scrollbar', className)} {...props} />
35
+ ),
36
+ }}
37
+ >
38
+ {message.content}
39
+ </Markdown>
31
40
  {isStreaming ? <span aria-hidden="true" className="message__cursor" /> : null}
32
41
  </div>
33
42
  </div>
@@ -7,3 +7,12 @@ export type { ChatMessageListProps } from './ChatMessageList';
7
7
  export { ChatPanel } from './ChatPanel';
8
8
  export type { ChatPanelProps } from './ChatPanel';
9
9
  export type { ChatMessage, ChatMessageSource } from './types';
10
+ export { useChatRuntimeState } from './useChatRuntimeState';
11
+ export type { UseChatRuntimeStateInput, UseChatRuntimeStateResult } from './useChatRuntimeState';
12
+ export { useSlashCommandSuggest } from './useSlashCommandSuggest';
13
+ export type {
14
+ UseSlashCommandSuggestOptions,
15
+ UseSlashCommandSuggestResult,
16
+ } from './useSlashCommandSuggest';
17
+ export { getSlashCommandQuery, isSlashCommandInput, parseSlashCommand } from './slashCommand';
18
+ export type { SlashCommandParseResult } from './slashCommand';
@@ -0,0 +1,28 @@
1
+ export type SlashCommandParseResult =
2
+ | { type: 'none' }
3
+ | { type: 'run'; commandId: string }
4
+ | { type: 'unknown'; token: string };
5
+
6
+ export function isSlashCommandInput(value: string): boolean {
7
+ return value.trimStart().startsWith('/');
8
+ }
9
+
10
+ export function getSlashCommandQuery(value: string): string {
11
+ const trimmed = value.trimStart();
12
+ if (!trimmed.startsWith('/')) return '';
13
+ return trimmed.slice(1);
14
+ }
15
+
16
+ export function parseSlashCommand(
17
+ input: string,
18
+ isValidCommandId: (id: string) => boolean,
19
+ ): SlashCommandParseResult {
20
+ const trimmed = input.trim();
21
+ if (!trimmed.startsWith('/')) return { type: 'none' };
22
+
23
+ const [rawToken] = trimmed.slice(1).split(/\s+/, 1);
24
+ if (!rawToken) return { type: 'unknown', token: '' };
25
+
26
+ if (isValidCommandId(rawToken)) return { type: 'run', commandId: rawToken };
27
+ return { type: 'unknown', token: rawToken };
28
+ }
@@ -0,0 +1,76 @@
1
+ import { useEffect, useReducer } from 'react';
2
+ import type {
3
+ RuntimeChatMessage,
4
+ RuntimeStatus,
5
+ WorkbenchRuntimeEvent,
6
+ WorkbenchRuntimeListener,
7
+ } from '@workbench-kit/runtime';
8
+
9
+ interface ChatRuntimeState {
10
+ messages: RuntimeChatMessage[];
11
+ status: RuntimeStatus;
12
+ }
13
+
14
+ function chatRuntimeReducer(
15
+ state: ChatRuntimeState,
16
+ action: WorkbenchRuntimeEvent,
17
+ ): ChatRuntimeState {
18
+ if (action.type === 'message') {
19
+ const exists = state.messages.some((m) => m.id === action.message.id);
20
+ return {
21
+ ...state,
22
+ messages: exists
23
+ ? state.messages.map((m) => (m.id === action.message.id ? action.message : m))
24
+ : [...state.messages, action.message],
25
+ };
26
+ }
27
+
28
+ if (action.type === 'message-delta') {
29
+ const exists = state.messages.some((m) => m.id === action.message.id);
30
+ return {
31
+ ...state,
32
+ messages: exists
33
+ ? state.messages.map((m) => (m.id === action.message.id ? action.message : m))
34
+ : [...state.messages, action.message],
35
+ };
36
+ }
37
+
38
+ if (action.type === 'status') {
39
+ return { ...state, status: action.status };
40
+ }
41
+
42
+ return state;
43
+ }
44
+
45
+ const INITIAL_STATE: ChatRuntimeState = {
46
+ messages: [],
47
+ status: 'idle',
48
+ };
49
+
50
+ export interface UseChatRuntimeStateInput {
51
+ subscribe: (listener: WorkbenchRuntimeListener) => () => void;
52
+ }
53
+
54
+ export interface UseChatRuntimeStateResult {
55
+ isStreaming: boolean;
56
+ messages: RuntimeChatMessage[];
57
+ status: RuntimeStatus;
58
+ }
59
+
60
+ export function useChatRuntimeState({
61
+ subscribe,
62
+ }: UseChatRuntimeStateInput): UseChatRuntimeStateResult {
63
+ const [state, dispatch] = useReducer(chatRuntimeReducer, INITIAL_STATE);
64
+
65
+ useEffect(() => {
66
+ return subscribe((event) => {
67
+ dispatch(event);
68
+ });
69
+ }, [subscribe]);
70
+
71
+ return {
72
+ isStreaming: state.status === 'running',
73
+ messages: state.messages,
74
+ status: state.status,
75
+ };
76
+ }
@@ -0,0 +1,101 @@
1
+ import { useCallback, useMemo, useState } from 'react';
2
+ import type { KeyboardEvent } from 'react';
3
+ import {
4
+ filterWorkbenchCommands,
5
+ getNextWorkbenchCommandIndex,
6
+ type WorkbenchCommandDescriptor,
7
+ } from '../CommandPalette';
8
+ import { getSlashCommandQuery, isSlashCommandInput } from './slashCommand';
9
+
10
+ export interface UseSlashCommandSuggestOptions {
11
+ commands: readonly WorkbenchCommandDescriptor[];
12
+ limit?: number;
13
+ value: string;
14
+ }
15
+
16
+ export interface UseSlashCommandSuggestResult {
17
+ activeCommand: WorkbenchCommandDescriptor | undefined;
18
+ activeCommandIndex: number;
19
+ commandQuery: string;
20
+ isOpen: boolean;
21
+ suggestedCommands: readonly WorkbenchCommandDescriptor[];
22
+ handleKeyDown: (event: KeyboardEvent, onSelect: (commandId: string) => void) => void;
23
+ setActiveCommandByKey: (commandId: string) => void;
24
+ setActiveCommandIndex: (index: number) => void;
25
+ }
26
+
27
+ export function useSlashCommandSuggest({
28
+ commands,
29
+ limit = 8,
30
+ value,
31
+ }: UseSlashCommandSuggestOptions): UseSlashCommandSuggestResult {
32
+ const [activeCommandIndex, setActiveCommandIndex] = useState(0);
33
+
34
+ const isOpen = isSlashCommandInput(value);
35
+ const commandQuery = getSlashCommandQuery(value);
36
+
37
+ const suggestedCommands = useMemo(
38
+ () => (isOpen ? filterWorkbenchCommands({ commands, limit, query: commandQuery }) : []),
39
+ [commands, commandQuery, isOpen, limit],
40
+ );
41
+
42
+ const resolvedActiveIndex =
43
+ suggestedCommands.length > 0 ? Math.min(activeCommandIndex, suggestedCommands.length - 1) : -1;
44
+ const activeCommand =
45
+ resolvedActiveIndex >= 0 ? suggestedCommands[resolvedActiveIndex] : undefined;
46
+
47
+ const handleKeyDown = useCallback(
48
+ (event: KeyboardEvent, onSelect: (commandId: string) => void) => {
49
+ if (!isOpen) return;
50
+
51
+ if (event.key === 'ArrowDown') {
52
+ event.preventDefault();
53
+ setActiveCommandIndex((current) =>
54
+ getNextWorkbenchCommandIndex({
55
+ commands: suggestedCommands,
56
+ currentIndex: current,
57
+ direction: 'next',
58
+ }),
59
+ );
60
+ return;
61
+ }
62
+
63
+ if (event.key === 'ArrowUp') {
64
+ event.preventDefault();
65
+ setActiveCommandIndex((current) =>
66
+ getNextWorkbenchCommandIndex({
67
+ commands: suggestedCommands,
68
+ currentIndex: current,
69
+ direction: 'previous',
70
+ }),
71
+ );
72
+ return;
73
+ }
74
+
75
+ if (event.key === 'Enter' && !event.shiftKey && activeCommand) {
76
+ event.preventDefault();
77
+ onSelect(activeCommand.id);
78
+ }
79
+ },
80
+ [activeCommand, isOpen, suggestedCommands],
81
+ );
82
+
83
+ const setActiveCommandByKey = useCallback(
84
+ (commandId: string) => {
85
+ const index = suggestedCommands.findIndex((cmd) => cmd.id === commandId);
86
+ setActiveCommandIndex(Math.max(0, index));
87
+ },
88
+ [suggestedCommands],
89
+ );
90
+
91
+ return {
92
+ activeCommand,
93
+ activeCommandIndex: resolvedActiveIndex,
94
+ commandQuery,
95
+ handleKeyDown,
96
+ isOpen,
97
+ setActiveCommandByKey,
98
+ setActiveCommandIndex,
99
+ suggestedCommands,
100
+ };
101
+ }