@ifc-lite/viewer 1.14.2 → 1.14.4

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 (80) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/assets/{Arrow.dom-CSgnLhN4.js → Arrow.dom-_vGzMMKs.js} +1 -1
  3. package/dist/assets/basketViewActivator-BZcoCL3V.js +1 -0
  4. package/dist/assets/{browser-qSKWrKQW.js → browser-Czmf34bo.js} +1 -1
  5. package/dist/assets/ifc-lite_bg-DyBKoGgk.wasm +0 -0
  6. package/dist/assets/index-CMQ_Dgkr.css +1 -0
  7. package/dist/assets/index-D7nEDctQ.js +229 -0
  8. package/dist/assets/{index-4Y4XaV8N.js → index-DX-Qf5fA.js} +72669 -61673
  9. package/dist/assets/{native-bridge-CSFDsEkg.js → native-bridge-DAOWftxE.js} +1 -1
  10. package/dist/assets/{wasm-bridge-Zf90ysEm.js → wasm-bridge-D7jYpn8a.js} +1 -1
  11. package/dist/index.html +2 -2
  12. package/package.json +21 -20
  13. package/src/App.tsx +17 -1
  14. package/src/components/viewer/BasketPresentationDock.tsx +8 -4
  15. package/src/components/viewer/ChatPanel.tsx +1402 -0
  16. package/src/components/viewer/CodeEditor.tsx +70 -4
  17. package/src/components/viewer/CommandPalette.tsx +1 -0
  18. package/src/components/viewer/HierarchyPanel.tsx +28 -13
  19. package/src/components/viewer/MainToolbar.tsx +113 -95
  20. package/src/components/viewer/ScriptPanel.tsx +351 -184
  21. package/src/components/viewer/UpgradePage.tsx +69 -0
  22. package/src/components/viewer/Viewport.tsx +23 -0
  23. package/src/components/viewer/chat/ChatMessage.tsx +144 -0
  24. package/src/components/viewer/chat/ExecutableCodeBlock.tsx +416 -0
  25. package/src/components/viewer/chat/ModelSelector.tsx +102 -0
  26. package/src/components/viewer/chat/renderTextContent.test.ts +23 -0
  27. package/src/components/viewer/chat/renderTextContent.ts +19 -0
  28. package/src/components/viewer/hierarchy/HierarchyNode.tsx +10 -3
  29. package/src/components/viewer/hierarchy/treeDataBuilder.test.ts +126 -0
  30. package/src/components/viewer/hierarchy/treeDataBuilder.ts +139 -38
  31. package/src/components/viewer/hierarchy/types.ts +6 -1
  32. package/src/components/viewer/hierarchy/useHierarchyTree.ts +27 -12
  33. package/src/hooks/useIfcCache.ts +1 -2
  34. package/src/hooks/useSandbox.ts +122 -6
  35. package/src/index.css +10 -0
  36. package/src/lib/attachments.ts +46 -0
  37. package/src/lib/llm/ClerkChatSync.tsx +74 -0
  38. package/src/lib/llm/clerk-auth.ts +62 -0
  39. package/src/lib/llm/code-extractor.ts +50 -0
  40. package/src/lib/llm/context-builder.test.ts +18 -0
  41. package/src/lib/llm/context-builder.ts +305 -0
  42. package/src/lib/llm/free-models.test.ts +118 -0
  43. package/src/lib/llm/message-capabilities.test.ts +131 -0
  44. package/src/lib/llm/message-capabilities.ts +94 -0
  45. package/src/lib/llm/models.ts +197 -0
  46. package/src/lib/llm/repair-loop.test.ts +91 -0
  47. package/src/lib/llm/repair-loop.ts +76 -0
  48. package/src/lib/llm/script-diagnostics.ts +445 -0
  49. package/src/lib/llm/script-edit-ops.test.ts +399 -0
  50. package/src/lib/llm/script-edit-ops.ts +954 -0
  51. package/src/lib/llm/script-preflight.test.ts +513 -0
  52. package/src/lib/llm/script-preflight.ts +990 -0
  53. package/src/lib/llm/script-preservation.test.ts +128 -0
  54. package/src/lib/llm/script-preservation.ts +152 -0
  55. package/src/lib/llm/stream-client.test.ts +97 -0
  56. package/src/lib/llm/stream-client.ts +410 -0
  57. package/src/lib/llm/system-prompt.test.ts +181 -0
  58. package/src/lib/llm/system-prompt.ts +665 -0
  59. package/src/lib/llm/types.ts +150 -0
  60. package/src/lib/scripts/templates/bim-globals.d.ts +226 -7
  61. package/src/lib/scripts/templates/create-building.ts +12 -12
  62. package/src/main.tsx +10 -1
  63. package/src/sdk/adapters/export-adapter.test.ts +24 -0
  64. package/src/sdk/adapters/export-adapter.ts +40 -16
  65. package/src/sdk/adapters/files-adapter.ts +39 -0
  66. package/src/sdk/adapters/model-compat.ts +1 -1
  67. package/src/sdk/adapters/mutate-adapter.ts +20 -6
  68. package/src/sdk/adapters/mutation-view.ts +112 -0
  69. package/src/sdk/adapters/query-adapter.ts +100 -4
  70. package/src/sdk/local-backend.ts +4 -0
  71. package/src/store/index.ts +15 -1
  72. package/src/store/slices/chatSlice.test.ts +325 -0
  73. package/src/store/slices/chatSlice.ts +468 -0
  74. package/src/store/slices/scriptSlice.test.ts +75 -0
  75. package/src/store/slices/scriptSlice.ts +256 -9
  76. package/src/vite-env.d.ts +10 -0
  77. package/vite.config.ts +21 -2
  78. package/dist/assets/ifc-lite_bg-BOvNXJA_.wasm +0 -0
  79. package/dist/assets/index-ByrFvN5A.css +0 -1
  80. package/dist/assets/index-CN7qDq7G.js +0 -216
@@ -3,14 +3,14 @@
3
3
  * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
4
 
5
5
  /**
6
- * ScriptPanel — Code editor + output console for BIM scripting.
6
+ * ScriptPanel — Code editor + output console + optional AI chat side panel.
7
7
  *
8
8
  * Uses CodeMirror 6 for the code editor with bim.* autocomplete.
9
9
  * Connects to the QuickJS sandbox via useSandbox() and displays results
10
- * in a log console.
10
+ * in a log console. AI chat is integrated as a collapsible side panel.
11
11
  */
12
12
 
13
- import { useCallback, useMemo, useState, memo } from 'react';
13
+ import { useCallback, useEffect, useMemo, useRef, useState, memo } from 'react';
14
14
  import {
15
15
  Play,
16
16
  Save,
@@ -24,6 +24,11 @@ import {
24
24
  CheckCircle2,
25
25
  Info,
26
26
  AlertTriangle,
27
+ Bot,
28
+ PanelRightClose,
29
+ PanelRightOpen,
30
+ Undo2,
31
+ Redo2,
27
32
  } from 'lucide-react';
28
33
  import { Button } from '@/components/ui/button';
29
34
  import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
@@ -48,6 +53,7 @@ import { useViewerStore } from '@/store';
48
53
  import { useSandbox } from '@/hooks/useSandbox';
49
54
  import { SCRIPT_TEMPLATES } from '@/lib/scripts/templates';
50
55
  import { CodeEditor } from './CodeEditor';
56
+ import { ChatPanel } from './ChatPanel';
51
57
  import type { LogEntry } from '@/store/slices/scriptSlice';
52
58
 
53
59
  interface ScriptPanelProps {
@@ -70,6 +76,14 @@ function useScriptState() {
70
76
  const setActiveScriptId = useViewerStore((s) => s.setActiveScriptId);
71
77
  const deleteConfirmId = useViewerStore((s) => s.scriptDeleteConfirmId);
72
78
  const setDeleteConfirmId = useViewerStore((s) => s.setScriptDeleteConfirmId);
79
+ const setScriptCursorContext = useViewerStore((s) => s.setScriptCursorContext);
80
+ const registerScriptEditorApplyAdapter = useViewerStore((s) => s.registerScriptEditorApplyAdapter);
81
+ const scriptCanUndo = useViewerStore((s) => s.scriptCanUndo);
82
+ const scriptCanRedo = useViewerStore((s) => s.scriptCanRedo);
83
+ const setScriptHistoryState = useViewerStore((s) => s.setScriptHistoryState);
84
+ const undoScriptEditor = useViewerStore((s) => s.undoScriptEditor);
85
+ const redoScriptEditor = useViewerStore((s) => s.redoScriptEditor);
86
+ const queueChatRepairRequest = useViewerStore((s) => s.queueChatRepairRequest);
73
87
 
74
88
  return {
75
89
  editorContent,
@@ -86,6 +100,14 @@ function useScriptState() {
86
100
  setActiveScriptId,
87
101
  deleteConfirmId,
88
102
  setDeleteConfirmId,
103
+ setScriptCursorContext,
104
+ registerScriptEditorApplyAdapter,
105
+ scriptCanUndo,
106
+ scriptCanRedo,
107
+ setScriptHistoryState,
108
+ undoScriptEditor,
109
+ redoScriptEditor,
110
+ queueChatRepairRequest,
89
111
  };
90
112
  }
91
113
 
@@ -105,10 +127,66 @@ export function ScriptPanel({ onClose }: ScriptPanelProps) {
105
127
  setActiveScriptId,
106
128
  deleteConfirmId,
107
129
  setDeleteConfirmId,
130
+ setScriptCursorContext,
131
+ registerScriptEditorApplyAdapter,
132
+ scriptCanUndo,
133
+ scriptCanRedo,
134
+ setScriptHistoryState,
135
+ undoScriptEditor,
136
+ redoScriptEditor,
137
+ queueChatRepairRequest,
108
138
  } = useScriptState();
109
139
 
110
140
  const { execute, reset } = useSandbox();
111
141
  const [outputCollapsed, setOutputCollapsed] = useState(false);
142
+ const chatPanelVisible = useViewerStore((s) => s.chatPanelVisible);
143
+ const setChatPanelVisible = useViewerStore((s) => s.setChatPanelVisible);
144
+
145
+ // Chat panel width (px) — resizable via drag handle
146
+ const [chatWidth, setChatWidth] = useState(380);
147
+ const chatDragRef = useRef<{ startX: number; startWidth: number } | null>(null);
148
+ const cleanupChatDragRef = useRef<(() => void) | null>(null);
149
+
150
+ // Open chat by default when script panel mounts
151
+ useEffect(() => {
152
+ try {
153
+ if (localStorage.getItem('ifc-lite-chat-panel-visible') === null) {
154
+ setChatPanelVisible(true);
155
+ }
156
+ } catch {
157
+ setChatPanelVisible(true);
158
+ }
159
+ return () => { cleanupChatDragRef.current?.(); };
160
+ }, [setChatPanelVisible]);
161
+
162
+ const handleChatResizeStart = useCallback((e: React.MouseEvent) => {
163
+ e.preventDefault();
164
+ chatDragRef.current = { startX: e.clientX, startWidth: chatWidth };
165
+
166
+ const onMouseMove = (moveEvent: MouseEvent) => {
167
+ if (!chatDragRef.current) return;
168
+ const delta = chatDragRef.current.startX - moveEvent.clientX;
169
+ const newWidth = Math.min(700, Math.max(240, chatDragRef.current.startWidth + delta));
170
+ setChatWidth(newWidth);
171
+ };
172
+
173
+ const cleanup = () => {
174
+ chatDragRef.current = null;
175
+ document.removeEventListener('mousemove', onMouseMove);
176
+ document.removeEventListener('mouseup', onMouseUp);
177
+ document.body.style.cursor = '';
178
+ document.body.style.userSelect = '';
179
+ cleanupChatDragRef.current = null;
180
+ };
181
+
182
+ const onMouseUp = () => { cleanup(); };
183
+
184
+ document.addEventListener('mousemove', onMouseMove);
185
+ document.addEventListener('mouseup', onMouseUp);
186
+ document.body.style.cursor = 'col-resize';
187
+ document.body.style.userSelect = 'none';
188
+ cleanupChatDragRef.current = cleanup;
189
+ }, [chatWidth]);
112
190
 
113
191
  const activeScript = useMemo(
114
192
  () => savedScripts.find((s) => s.id === activeScriptId),
@@ -143,205 +221,294 @@ export function ScriptPanel({ onClose }: ScriptPanelProps) {
143
221
  }
144
222
  }, [deleteConfirmId, deleteScript]);
145
223
 
224
+ const handleFixWithLlm = useCallback(() => {
225
+ if (!lastError) return;
226
+ setChatPanelVisible(true);
227
+ const state = useViewerStore.getState();
228
+ queueChatRepairRequest({
229
+ error: lastError,
230
+ diagnostics: state.scriptLastDiagnostics,
231
+ reason: lastError.startsWith('Preflight validation failed:') ? 'preflight' : 'runtime',
232
+ });
233
+ }, [lastError, queueChatRepairRequest, setChatPanelVisible]);
234
+
235
+ const toggleChat = useCallback(() => {
236
+ setChatPanelVisible(!chatPanelVisible);
237
+ }, [chatPanelVisible, setChatPanelVisible]);
238
+
146
239
  return (
147
- <div className="h-full flex flex-col bg-background">
148
- {/* Header */}
149
- <div className="flex items-center gap-1 px-2 py-1.5 border-b shrink-0">
150
- <FileCode2 className="h-4 w-4 text-muted-foreground shrink-0" />
151
- <span className="text-sm font-medium truncate">
152
- {activeScript ? activeScript.name : 'Script Editor'}
153
- {editorDirty && <span className="text-muted-foreground ml-1">*</span>}
154
- </span>
155
- <div className="flex-1" />
156
-
157
- {/* Script selector dropdown */}
158
- {savedScripts.length > 0 && (
159
- <DropdownMenu>
160
- <DropdownMenuTrigger asChild>
161
- <Button variant="ghost" size="icon-xs">
162
- <ChevronDown className="h-3.5 w-3.5" />
240
+ <div className="h-full flex bg-background">
241
+ {/* Left side: Script editor + output */}
242
+ <div className={cn('flex flex-col min-w-0', chatPanelVisible ? 'flex-1' : 'w-full')}>
243
+ {/* Header */}
244
+ <div className="flex items-center gap-1 px-2 py-1.5 border-b shrink-0">
245
+ <FileCode2 className="h-4 w-4 text-muted-foreground shrink-0" />
246
+ <span className="text-sm font-medium truncate">
247
+ {activeScript ? activeScript.name : 'Script Editor'}
248
+ {editorDirty && <span className="text-muted-foreground ml-1">*</span>}
249
+ </span>
250
+ <div className="flex-1" />
251
+
252
+ {/* Script selector dropdown */}
253
+ {savedScripts.length > 0 && (
254
+ <DropdownMenu>
255
+ <DropdownMenuTrigger asChild>
256
+ <Button variant="ghost" size="icon-xs">
257
+ <ChevronDown className="h-3.5 w-3.5" />
258
+ </Button>
259
+ </DropdownMenuTrigger>
260
+ <DropdownMenuContent align="end">
261
+ {savedScripts.map((s) => (
262
+ <DropdownMenuItem
263
+ key={s.id}
264
+ onClick={() => setActiveScriptId(s.id)}
265
+ className={cn(s.id === activeScriptId && 'bg-accent')}
266
+ >
267
+ <FileCode2 className="h-3.5 w-3.5 mr-2" />
268
+ {s.name}
269
+ </DropdownMenuItem>
270
+ ))}
271
+ <DropdownMenuSeparator />
272
+ {activeScriptId && (
273
+ <DropdownMenuItem
274
+ onClick={() => setDeleteConfirmId(activeScriptId)}
275
+ className="text-destructive"
276
+ >
277
+ <Trash2 className="h-3.5 w-3.5 mr-2" />
278
+ Delete
279
+ </DropdownMenuItem>
280
+ )}
281
+ </DropdownMenuContent>
282
+ </DropdownMenu>
283
+ )}
284
+
285
+ {/* AI Chat toggle */}
286
+ <Tooltip>
287
+ <TooltipTrigger asChild>
288
+ <Button
289
+ variant={chatPanelVisible ? 'default' : 'ghost'}
290
+ size="icon-xs"
291
+ onClick={toggleChat}
292
+ className={cn(chatPanelVisible && 'bg-blue-500 hover:bg-blue-600 text-white')}
293
+ >
294
+ <Bot className="h-3.5 w-3.5" />
295
+ </Button>
296
+ </TooltipTrigger>
297
+ <TooltipContent>{chatPanelVisible ? 'Hide AI Chat' : 'Show AI Chat'}</TooltipContent>
298
+ </Tooltip>
299
+
300
+ {onClose && (
301
+ <Button variant="ghost" size="icon-xs" onClick={onClose}>
302
+ <X className="h-3.5 w-3.5" />
303
+ </Button>
304
+ )}
305
+ </div>
306
+
307
+ {/* Toolbar */}
308
+ <div className="flex items-center gap-1 px-2 py-1 border-b shrink-0">
309
+ <Tooltip>
310
+ <TooltipTrigger asChild>
311
+ <Button
312
+ variant="default"
313
+ size="sm"
314
+ onClick={handleRun}
315
+ disabled={executionState === 'running'}
316
+ className="gap-1"
317
+ >
318
+ <Play className="h-3.5 w-3.5" />
319
+ Run
163
320
  </Button>
164
- </DropdownMenuTrigger>
165
- <DropdownMenuContent align="end">
166
- {savedScripts.map((s) => (
167
- <DropdownMenuItem
168
- key={s.id}
169
- onClick={() => setActiveScriptId(s.id)}
170
- className={cn(s.id === activeScriptId && 'bg-accent')}
171
- >
321
+ </TooltipTrigger>
322
+ <TooltipContent>Run script (Ctrl+Enter)</TooltipContent>
323
+ </Tooltip>
324
+
325
+ <Tooltip>
326
+ <TooltipTrigger asChild>
327
+ <Button variant="ghost" size="icon-xs" onClick={handleSave}>
328
+ <Save className="h-3.5 w-3.5" />
329
+ </Button>
330
+ </TooltipTrigger>
331
+ <TooltipContent>Save (Ctrl+S)</TooltipContent>
332
+ </Tooltip>
333
+
334
+ <Tooltip>
335
+ <TooltipTrigger asChild>
336
+ <Button
337
+ variant="ghost"
338
+ size="icon-xs"
339
+ onClick={undoScriptEditor}
340
+ disabled={!scriptCanUndo}
341
+ >
342
+ <Undo2 className="h-3.5 w-3.5" />
343
+ </Button>
344
+ </TooltipTrigger>
345
+ <TooltipContent>Undo (Ctrl+Z)</TooltipContent>
346
+ </Tooltip>
347
+
348
+ <Tooltip>
349
+ <TooltipTrigger asChild>
350
+ <Button
351
+ variant="ghost"
352
+ size="icon-xs"
353
+ onClick={redoScriptEditor}
354
+ disabled={!scriptCanRedo}
355
+ >
356
+ <Redo2 className="h-3.5 w-3.5" />
357
+ </Button>
358
+ </TooltipTrigger>
359
+ <TooltipContent>Redo (Ctrl+Shift+Z)</TooltipContent>
360
+ </Tooltip>
361
+
362
+ {/* New script dropdown with templates */}
363
+ <DropdownMenu>
364
+ <Tooltip>
365
+ <TooltipTrigger asChild>
366
+ <DropdownMenuTrigger asChild>
367
+ <Button variant="ghost" size="icon-xs">
368
+ <Plus className="h-3.5 w-3.5" />
369
+ </Button>
370
+ </DropdownMenuTrigger>
371
+ </TooltipTrigger>
372
+ <TooltipContent>New script</TooltipContent>
373
+ </Tooltip>
374
+ <DropdownMenuContent align="start">
375
+ <DropdownMenuItem onClick={() => handleNew('Untitled Script')}>
376
+ <FileCode2 className="h-3.5 w-3.5 mr-2" />
377
+ Blank Script
378
+ </DropdownMenuItem>
379
+ <DropdownMenuSeparator />
380
+ {SCRIPT_TEMPLATES.map((t) => (
381
+ <DropdownMenuItem key={t.name} onClick={() => handleNew(t.name, t.code)}>
172
382
  <FileCode2 className="h-3.5 w-3.5 mr-2" />
173
- {s.name}
383
+ {t.name}
174
384
  </DropdownMenuItem>
175
385
  ))}
176
- <DropdownMenuSeparator />
177
- {activeScriptId && (
178
- <DropdownMenuItem
179
- onClick={() => setDeleteConfirmId(activeScriptId)}
180
- className="text-destructive"
181
- >
182
- <Trash2 className="h-3.5 w-3.5 mr-2" />
183
- Delete
184
- </DropdownMenuItem>
185
- )}
186
386
  </DropdownMenuContent>
187
387
  </DropdownMenu>
188
- )}
189
-
190
- {onClose && (
191
- <Button variant="ghost" size="icon-xs" onClick={onClose}>
192
- <X className="h-3.5 w-3.5" />
193
- </Button>
194
- )}
195
- </div>
196
-
197
- {/* Toolbar */}
198
- <div className="flex items-center gap-1 px-2 py-1 border-b shrink-0">
199
- <Tooltip>
200
- <TooltipTrigger asChild>
201
- <Button
202
- variant="default"
203
- size="sm"
204
- onClick={handleRun}
205
- disabled={executionState === 'running'}
206
- className="gap-1"
207
- >
208
- <Play className="h-3.5 w-3.5" />
209
- Run
210
- </Button>
211
- </TooltipTrigger>
212
- <TooltipContent>Run script (Ctrl+Enter)</TooltipContent>
213
- </Tooltip>
214
-
215
- <Tooltip>
216
- <TooltipTrigger asChild>
217
- <Button variant="ghost" size="icon-xs" onClick={handleSave}>
218
- <Save className="h-3.5 w-3.5" />
219
- </Button>
220
- </TooltipTrigger>
221
- <TooltipContent>Save (Ctrl+S)</TooltipContent>
222
- </Tooltip>
223
388
 
224
- {/* New script dropdown with templates */}
225
- <DropdownMenu>
226
389
  <Tooltip>
227
390
  <TooltipTrigger asChild>
228
- <DropdownMenuTrigger asChild>
229
- <Button variant="ghost" size="icon-xs">
230
- <Plus className="h-3.5 w-3.5" />
231
- </Button>
232
- </DropdownMenuTrigger>
391
+ <Button variant="ghost" size="icon-xs" onClick={reset}>
392
+ <RotateCcw className="h-3.5 w-3.5" />
393
+ </Button>
233
394
  </TooltipTrigger>
234
- <TooltipContent>New script</TooltipContent>
395
+ <TooltipContent>Reset sandbox</TooltipContent>
235
396
  </Tooltip>
236
- <DropdownMenuContent align="start">
237
- <DropdownMenuItem onClick={() => handleNew('Untitled Script')}>
238
- <FileCode2 className="h-3.5 w-3.5 mr-2" />
239
- Blank Script
240
- </DropdownMenuItem>
241
- <DropdownMenuSeparator />
242
- {SCRIPT_TEMPLATES.map((t) => (
243
- <DropdownMenuItem key={t.name} onClick={() => handleNew(t.name, t.code)}>
244
- <FileCode2 className="h-3.5 w-3.5 mr-2" />
245
- {t.name}
246
- </DropdownMenuItem>
247
- ))}
248
- </DropdownMenuContent>
249
- </DropdownMenu>
250
-
251
- <Tooltip>
252
- <TooltipTrigger asChild>
253
- <Button variant="ghost" size="icon-xs" onClick={reset}>
254
- <RotateCcw className="h-3.5 w-3.5" />
255
- </Button>
256
- </TooltipTrigger>
257
- <TooltipContent>Reset sandbox</TooltipContent>
258
- </Tooltip>
259
-
260
- {/* Status indicator */}
261
- <div className="flex-1" />
262
- {executionState === 'running' && (
263
- <span className="text-xs text-muted-foreground animate-pulse">Running...</span>
264
- )}
265
- {executionState === 'success' && lastResult && (
266
- <span className="text-xs text-green-600 dark:text-green-400 flex items-center gap-1">
267
- <CheckCircle2 className="h-3 w-3" />
268
- {formatDuration(lastResult.durationMs)}
269
- </span>
270
- )}
271
- {executionState === 'error' && (
272
- <span className="text-xs text-destructive flex items-center gap-1">
273
- <AlertCircle className="h-3 w-3" />
274
- Error
275
- </span>
276
- )}
277
- </div>
278
397
 
279
- {/* Code Editor */}
280
- <div className="flex-1 min-h-0 overflow-hidden">
281
- <CodeEditor
282
- value={editorContent}
283
- onChange={setEditorContent}
284
- onRun={handleRun}
285
- onSave={handleSave}
286
- className="h-full"
287
- />
288
- </div>
398
+ {/* Status indicator */}
399
+ <div className="flex-1" />
400
+ {executionState === 'running' && (
401
+ <span className="text-xs text-muted-foreground animate-pulse">Running...</span>
402
+ )}
403
+ {executionState === 'success' && lastResult && (
404
+ <span className="text-xs text-green-600 dark:text-green-400 flex items-center gap-1">
405
+ <CheckCircle2 className="h-3 w-3" />
406
+ {formatDuration(lastResult.durationMs)}
407
+ </span>
408
+ )}
409
+ {executionState === 'error' && (
410
+ <span className="text-xs text-destructive flex items-center gap-1">
411
+ <AlertCircle className="h-3 w-3" />
412
+ Error
413
+ </span>
414
+ )}
415
+ </div>
289
416
 
290
- {/* Output Console */}
291
- <div className="shrink-0 border-t">
292
- {/* Output header */}
293
- <button
294
- className="flex items-center gap-1.5 px-2 py-1 w-full hover:bg-muted/50 transition-colors text-left"
295
- onClick={() => setOutputCollapsed(!outputCollapsed)}
296
- >
297
- <ChevronDown
298
- className={cn('h-3 w-3 transition-transform', outputCollapsed && '-rotate-90')}
417
+ {/* Code Editor */}
418
+ <div className="flex-1 min-h-0 overflow-hidden">
419
+ <CodeEditor
420
+ value={editorContent}
421
+ onChange={setEditorContent}
422
+ onSelectionChange={setScriptCursorContext}
423
+ onHistoryChange={setScriptHistoryState}
424
+ registerApplyAdapter={registerScriptEditorApplyAdapter}
425
+ onRun={handleRun}
426
+ onSave={handleSave}
427
+ className="h-full"
299
428
  />
300
- <span className="text-xs font-medium text-muted-foreground">Output</span>
301
- {lastResult && lastResult.logs.length > 0 && (
302
- <span className="text-xs text-muted-foreground">({lastResult.logs.length})</span>
303
- )}
304
- </button>
305
-
306
- {!outputCollapsed && (
307
- <ScrollArea className="h-[140px]">
308
- <div className="px-2 pb-2 font-mono text-xs space-y-0.5">
309
- {/* Error message */}
310
- {lastError && (
311
- <div className="flex items-start gap-1.5 text-destructive">
312
- <AlertCircle className="h-3 w-3 mt-0.5 shrink-0" />
313
- <span className="whitespace-pre-wrap break-all">{lastError}</span>
314
- </div>
315
- )}
316
-
317
- {/* Log entries */}
318
- {lastResult?.logs.map((log, i) => (
319
- <MemoizedLogLine key={i} log={log} />
320
- ))}
429
+ </div>
430
+
431
+ {/* Output Console */}
432
+ <div className="shrink-0 border-t">
433
+ {/* Output header */}
434
+ <button
435
+ className="flex items-center gap-1.5 px-2 py-1 w-full hover:bg-muted/50 transition-colors text-left"
436
+ onClick={() => setOutputCollapsed(!outputCollapsed)}
437
+ >
438
+ <ChevronDown
439
+ className={cn('h-3 w-3 transition-transform', outputCollapsed && '-rotate-90')}
440
+ />
441
+ <span className="text-xs font-medium text-muted-foreground">Output</span>
442
+ {lastResult && lastResult.logs.length > 0 && (
443
+ <span className="text-xs text-muted-foreground">({lastResult.logs.length})</span>
444
+ )}
445
+ </button>
446
+
447
+ {!outputCollapsed && (
448
+ <ScrollArea className="h-[140px]">
449
+ <div className="px-2 pb-2 font-mono text-xs space-y-0.5">
450
+ {/* Error message */}
451
+ {lastError && (
452
+ <div className="flex items-start gap-1.5 text-destructive">
453
+ <AlertCircle className="h-3 w-3 mt-0.5 shrink-0" />
454
+ <div className="min-w-0">
455
+ <span className="whitespace-pre-wrap break-all">{lastError}</span>
456
+ <div className="mt-1">
457
+ <Button
458
+ variant="outline"
459
+ size="sm"
460
+ className="h-6 px-2 text-xs border-destructive/40 text-destructive bg-transparent hover:bg-destructive/10"
461
+ onClick={handleFixWithLlm}
462
+ >
463
+ Fix with LLM
464
+ </Button>
465
+ </div>
466
+ </div>
467
+ </div>
468
+ )}
469
+
470
+ {/* Log entries */}
471
+ {lastResult?.logs.map((log, i) => (
472
+ <MemoizedLogLine key={i} log={log} />
473
+ ))}
321
474
 
322
- {/* Return value */}
323
- {lastResult && lastResult.value !== undefined && lastResult.value !== null && (
324
- <div className="text-muted-foreground mt-1 pt-1 border-t border-border/50">
325
- <span className="opacity-60">Return: </span>
326
- <span className="text-foreground">
327
- {typeof lastResult.value === 'object'
328
- ? JSON.stringify(lastResult.value, null, 2)
329
- : String(lastResult.value)}
330
- </span>
331
- </div>
332
- )}
333
-
334
- {/* Empty state */}
335
- {!lastError && !lastResult && (
336
- <div className="text-muted-foreground py-2 text-center">
337
- Press Run or Ctrl+Enter to execute
338
- </div>
339
- )}
340
- </div>
341
- </ScrollArea>
342
- )}
475
+ {/* Return value */}
476
+ {lastResult && lastResult.value !== undefined && lastResult.value !== null && (
477
+ <div className="text-muted-foreground mt-1 pt-1 border-t border-border/50">
478
+ <span className="opacity-60">Return: </span>
479
+ <span className="text-foreground">
480
+ {typeof lastResult.value === 'object'
481
+ ? JSON.stringify(lastResult.value, null, 2)
482
+ : String(lastResult.value)}
483
+ </span>
484
+ </div>
485
+ )}
486
+
487
+ {/* Empty state */}
488
+ {!lastError && !lastResult && (
489
+ <div className="text-muted-foreground py-2 text-center">
490
+ Press Run or Ctrl+Enter to execute
491
+ </div>
492
+ )}
493
+ </div>
494
+ </ScrollArea>
495
+ )}
496
+ </div>
343
497
  </div>
344
498
 
499
+ {/* Right side: AI Chat panel (collapsible, resizable) */}
500
+ {chatPanelVisible && (
501
+ <>
502
+ <div
503
+ className="w-1.5 bg-border hover:bg-primary/50 active:bg-primary/70 transition-colors cursor-col-resize shrink-0 h-full"
504
+ onMouseDown={handleChatResizeStart}
505
+ />
506
+ <div style={{ width: chatWidth }} className="shrink-0 h-full min-w-0">
507
+ <ChatPanel onClose={() => setChatPanelVisible(false)} />
508
+ </div>
509
+ </>
510
+ )}
511
+
345
512
  {/* Delete confirmation dialog */}
346
513
  <Dialog open={deleteConfirmId !== null} onOpenChange={(open) => { if (!open) setDeleteConfirmId(null); }}>
347
514
  <DialogContent className="sm:max-w-[400px]">