@dfosco/storyboard-react 4.2.0-beta.1 → 4.2.0-beta.18

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 (85) hide show
  1. package/package.json +5 -4
  2. package/src/AuthModal/AuthModal.jsx +6 -2
  3. package/src/BranchBar/BranchBar.jsx +20 -6
  4. package/src/BranchBar/BranchBar.module.css +13 -4
  5. package/src/BranchBar/useBranches.js +20 -6
  6. package/src/BranchBar/useBranches.test.js +68 -0
  7. package/src/CommandPalette/CommandPalette.jsx +478 -186
  8. package/src/CommandPalette/command-palette.css +142 -78
  9. package/src/Icon.jsx +157 -58
  10. package/src/Viewfinder.jsx +561 -191
  11. package/src/Viewfinder.module.css +434 -93
  12. package/src/Workspace.jsx +7 -0
  13. package/src/canvas/CanvasPage.bridge.test.jsx +14 -6
  14. package/src/canvas/CanvasPage.dragdrop.test.jsx +10 -6
  15. package/src/canvas/CanvasPage.jsx +738 -216
  16. package/src/canvas/CanvasPage.module.css +13 -15
  17. package/src/canvas/CanvasPage.multiselect.test.jsx +17 -6
  18. package/src/canvas/ConnectorLayer.jsx +121 -153
  19. package/src/canvas/ConnectorLayer.module.css +69 -0
  20. package/src/canvas/PageSelector.test.jsx +15 -6
  21. package/src/canvas/canvasApi.js +68 -2
  22. package/src/canvas/connectorGeometry.js +132 -0
  23. package/src/canvas/hotPoolDevLogs.js +25 -0
  24. package/src/canvas/useCanvas.js +1 -1
  25. package/src/canvas/useMarqueeSelect.js +30 -4
  26. package/src/canvas/widgets/CodePenEmbed.jsx +1 -0
  27. package/src/canvas/widgets/ComponentSetWidget.jsx +199 -0
  28. package/src/canvas/widgets/ComponentSetWidget.module.css +89 -0
  29. package/src/canvas/widgets/ComponentWidget.jsx +1 -0
  30. package/src/canvas/widgets/CropOverlay.jsx +219 -0
  31. package/src/canvas/widgets/CropOverlay.module.css +118 -0
  32. package/src/canvas/widgets/ExpandedPane.jsx +472 -0
  33. package/src/canvas/widgets/ExpandedPane.module.css +179 -0
  34. package/src/canvas/widgets/ExpandedPane.test.jsx +240 -0
  35. package/src/canvas/widgets/ExpandedPaneTopBar.jsx +111 -0
  36. package/src/canvas/widgets/ExpandedPaneTopBar.module.css +59 -0
  37. package/src/canvas/widgets/ExpandedPaneTopBar.test.jsx +45 -0
  38. package/src/canvas/widgets/FigmaEmbed.jsx +62 -47
  39. package/src/canvas/widgets/FigmaEmbed.module.css +61 -0
  40. package/src/canvas/widgets/ImageWidget.jsx +130 -9
  41. package/src/canvas/widgets/ImageWidget.module.css +30 -0
  42. package/src/canvas/widgets/LinkPreview.jsx +112 -4
  43. package/src/canvas/widgets/LinkPreview.module.css +127 -0
  44. package/src/canvas/widgets/MarkdownBlock.jsx +164 -17
  45. package/src/canvas/widgets/MarkdownBlock.module.css +148 -0
  46. package/src/canvas/widgets/PromptWidget.jsx +414 -0
  47. package/src/canvas/widgets/PromptWidget.module.css +273 -0
  48. package/src/canvas/widgets/PrototypeEmbed.jsx +77 -38
  49. package/src/canvas/widgets/PrototypeEmbed.module.css +117 -0
  50. package/src/canvas/widgets/PrototypeEmbed.test.jsx +2 -2
  51. package/src/canvas/widgets/ResizeHandle.jsx +17 -6
  52. package/src/canvas/widgets/StoryWidget.jsx +72 -15
  53. package/src/canvas/widgets/TerminalReadWidget.jsx +146 -0
  54. package/src/canvas/widgets/TerminalReadWidget.module.css +94 -0
  55. package/src/canvas/widgets/TerminalWidget.jsx +496 -69
  56. package/src/canvas/widgets/TerminalWidget.module.css +271 -8
  57. package/src/canvas/widgets/TilesWidget.jsx +302 -0
  58. package/src/canvas/widgets/TilesWidget.module.css +133 -0
  59. package/src/canvas/widgets/WidgetChrome.jsx +73 -153
  60. package/src/canvas/widgets/WidgetChrome.module.css +30 -1
  61. package/src/canvas/widgets/embedInteraction.test.jsx +24 -26
  62. package/src/canvas/widgets/expandUtils.js +557 -0
  63. package/src/canvas/widgets/expandUtils.test.js +155 -0
  64. package/src/canvas/widgets/index.js +9 -0
  65. package/src/canvas/widgets/snapshotDisplay.test.jsx +23 -71
  66. package/src/canvas/widgets/tilePool.js +23 -0
  67. package/src/canvas/widgets/tiles/diagonal-bl.png +0 -0
  68. package/src/canvas/widgets/tiles/diagonal-br.png +0 -0
  69. package/src/canvas/widgets/tiles/diagonal-tl.png +0 -0
  70. package/src/canvas/widgets/tiles/leaf.png +0 -0
  71. package/src/canvas/widgets/tiles/quarter-tl.png +0 -0
  72. package/src/canvas/widgets/tiles/quarter-tr.png +0 -0
  73. package/src/canvas/widgets/tiles/solid-a.png +0 -0
  74. package/src/canvas/widgets/tiles/solid-b.png +0 -0
  75. package/src/canvas/widgets/widgetConfig.js +55 -4
  76. package/src/canvas/widgets/widgetIcons.jsx +190 -0
  77. package/src/canvas/widgets/widgetProps.js +1 -0
  78. package/src/context.jsx +47 -19
  79. package/src/hooks/useConfig.js +14 -0
  80. package/src/hooks/usePrototypeReloadGuard.js +64 -0
  81. package/src/index.js +8 -2
  82. package/src/story/ComponentSetPage.jsx +186 -0
  83. package/src/story/ComponentSetPage.module.css +121 -0
  84. package/src/story/StoryPage.jsx +32 -2
  85. package/src/vite/data-plugin.js +324 -30
@@ -0,0 +1,414 @@
1
+ import { useState, useCallback, useEffect, useRef, forwardRef, useImperativeHandle } from 'react'
2
+ import { readProp, promptSchema } from './widgetProps.js'
3
+ import { CopilotIcon, SquareFillIcon, CheckCircleIcon, XIcon } from '@primer/octicons-react'
4
+ import ResizeHandle from './ResizeHandle.jsx'
5
+ import styles from './PromptWidget.module.css'
6
+
7
+ function getBase() {
8
+ return (import.meta.env?.BASE_URL || '/').replace(/\/$/, '')
9
+ }
10
+
11
+ async function spawnPromptAgent({ canvasId, widgetId, prompt }) {
12
+ const res = await fetch(`${getBase()}/_storyboard/canvas/prompt/spawn`, {
13
+ method: 'POST',
14
+ headers: { 'Content-Type': 'application/json' },
15
+ body: JSON.stringify({ canvasId, widgetId, prompt }),
16
+ })
17
+ return res.json()
18
+ }
19
+
20
+ async function killSession(widgetId) {
21
+ const res = await fetch(`${getBase()}/_storyboard/canvas/terminal/kill`, {
22
+ method: 'POST',
23
+ headers: { 'Content-Type': 'application/json' },
24
+ body: JSON.stringify({ widgetId }),
25
+ })
26
+ return res.json()
27
+ }
28
+
29
+ function getWsUrl(sessionId, canvasId, readOnly = false) {
30
+ const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'
31
+ const base = (typeof import.meta !== 'undefined' && import.meta.env?.BASE_URL) || '/'
32
+ const baseClean = base.endsWith('/') ? base : base + '/'
33
+ let url = `${protocol}//${location.host}${baseClean}_storyboard/terminal/${sessionId}?canvas=${encodeURIComponent(canvasId)}`
34
+ if (readOnly) url += '&readOnly=1'
35
+ return url
36
+ }
37
+
38
+ let ghosttyPromise = null
39
+ function loadGhostty() {
40
+ if (!ghosttyPromise) {
41
+ ghosttyPromise = import(/* @vite-ignore */ 'ghostty-web')
42
+ .then(async (mod) => {
43
+ if (mod.init) await mod.init()
44
+ return mod
45
+ })
46
+ .catch((err) => {
47
+ ghosttyPromise = null
48
+ console.warn('[PromptWidget] ghostty-web not available:', err.message)
49
+ return null
50
+ })
51
+ }
52
+ return ghosttyPromise
53
+ }
54
+
55
+ const MINI_FONT_SIZE = 11
56
+ const MINI_TERMINAL_HEIGHT = 180
57
+
58
+ const DEFAULT_THEME = {
59
+ background: '#0d1117',
60
+ foreground: '#e6edf3',
61
+ cursor: '#e6edf3',
62
+ selectionBackground: '#264f78',
63
+ }
64
+
65
+ function calcMiniDimensions(widthPx, heightPx) {
66
+ const scale = MINI_FONT_SIZE / 13
67
+ const cellWidth = 7.8 * scale
68
+ const cellHeight = 17 * scale
69
+ const hPad = 18
70
+ const vPad = 18
71
+ const cols = Math.max(10, Math.floor((widthPx - hPad) / cellWidth))
72
+ const rows = Math.max(4, Math.floor((heightPx - vPad) / cellHeight))
73
+ return { cols, rows }
74
+ }
75
+
76
+ const PromptWidget = forwardRef(function PromptWidget({ id, props, onUpdate, resizable }, ref) {
77
+ const persistedText = readProp(props, 'text', promptSchema)
78
+ const persistedStatus = readProp(props, 'status', promptSchema)
79
+ const errorMessage = readProp(props, 'errorMessage', promptSchema)
80
+ const width = readProp(props, 'width', promptSchema)
81
+ const height = readProp(props, 'height', promptSchema)
82
+ const [draftText, setDraftText] = useState('')
83
+ const [execStatus, setExecStatus] = useState(persistedStatus || 'idle')
84
+ const [execError, setExecError] = useState(errorMessage || '')
85
+ const [showOutput, setShowOutput] = useState(false)
86
+ const canEdit = typeof onUpdate === 'function'
87
+
88
+ const containerRef = useRef(null)
89
+ const termContainerRef = useRef(null)
90
+ const termRef = useRef(null)
91
+ const wsRef = useRef(null)
92
+ const termDisposedRef = useRef(false)
93
+ const textareaRef = useRef(null)
94
+
95
+ const onUpdateRef = useRef(onUpdate)
96
+ useEffect(() => { onUpdateRef.current = onUpdate }, [onUpdate])
97
+
98
+ useEffect(() => {
99
+ if (!import.meta.hot) return
100
+
101
+ const handler = (data) => {
102
+ if (data.widgetId !== id) return
103
+ if (data.status === 'done' || data.status === 'completed') {
104
+ setExecStatus('done')
105
+ onUpdateRef.current?.({ status: 'done' })
106
+ } else if (data.status === 'error') {
107
+ setExecStatus('error')
108
+ setExecError(data.message || 'Unknown error')
109
+ onUpdateRef.current?.({ status: 'error', errorMessage: data.message || 'Unknown error' })
110
+ } else if (data.status === 'cancelled') {
111
+ setExecStatus('idle')
112
+ onUpdateRef.current?.({ status: 'idle', sessionId: '', errorMessage: '' })
113
+ }
114
+ }
115
+
116
+ import.meta.hot.on('storyboard:agent-status', handler)
117
+ return () => import.meta.hot.off('storyboard:agent-status', handler)
118
+ }, [id])
119
+
120
+ useImperativeHandle(ref, () => ({
121
+ handleAction(action) {
122
+ if (action === 'expand-output') {
123
+ setShowOutput(prev => !prev)
124
+ return true
125
+ }
126
+ return false
127
+ },
128
+ getState(key) {
129
+ if (key === 'showOutput') return showOutput
130
+ if (key === 'hasSession') return execStatus !== 'idle'
131
+ return undefined
132
+ },
133
+ }), [id, showOutput, execStatus])
134
+
135
+ const handleSubmit = useCallback(async () => {
136
+ if (!draftText.trim() || !canEdit) return
137
+
138
+ setExecStatus('pending')
139
+ setExecError('')
140
+
141
+ const pathParts = window.location.pathname.split('/')
142
+ const canvasIdx = pathParts.indexOf('canvas')
143
+ const canvasId = canvasIdx >= 0 ? pathParts[canvasIdx + 1] : 'default'
144
+
145
+ onUpdate?.({ text: draftText, status: 'pending' })
146
+
147
+ try {
148
+ const result = await spawnPromptAgent({
149
+ canvasId,
150
+ widgetId: id,
151
+ prompt: draftText,
152
+ })
153
+
154
+ if (result.error) {
155
+ setExecStatus('error')
156
+ setExecError(result.error)
157
+ onUpdate?.({ status: 'error', errorMessage: result.error })
158
+ return
159
+ }
160
+
161
+ onUpdate?.({ sessionId: result.tmuxName || '' })
162
+ } catch (err) {
163
+ setExecStatus('error')
164
+ setExecError(err.message)
165
+ onUpdate?.({ status: 'error', errorMessage: err.message })
166
+ }
167
+ }, [draftText, canEdit, id, onUpdate])
168
+
169
+ const handleCancel = useCallback(async () => {
170
+ try {
171
+ await killSession(id)
172
+ setExecStatus('idle')
173
+ setExecError('')
174
+ onUpdate?.({ status: 'idle', sessionId: '', errorMessage: '' })
175
+ } catch (err) {
176
+ setExecError(`Cancel failed: ${err.message}`)
177
+ }
178
+ }, [id, onUpdate])
179
+
180
+ const handleKeyDown = useCallback((e) => {
181
+ if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
182
+ e.preventDefault()
183
+ handleSubmit()
184
+ }
185
+ e.stopPropagation()
186
+ }, [handleSubmit])
187
+
188
+ const handleReset = useCallback(() => {
189
+ setExecStatus('idle')
190
+ setExecError('')
191
+ setDraftText('')
192
+ setShowOutput(false)
193
+ onUpdate?.({ status: 'idle', sessionId: '', errorMessage: '', text: '' })
194
+ }, [onUpdate])
195
+
196
+ const handleResize = useCallback((newWidth, newHeight) => {
197
+ const el = containerRef.current
198
+ const termEl = termContainerRef.current
199
+ if (el && termEl) {
200
+ // height prop controls only the terminal area, not the full wrapper
201
+ const nonTermH = el.offsetHeight - termEl.offsetHeight
202
+ const newTermH = Math.max(80, newHeight - nonTermH)
203
+ onUpdate?.({ width: newWidth, height: newTermH })
204
+ } else {
205
+ onUpdate?.({ width: newWidth })
206
+ }
207
+ }, [onUpdate])
208
+
209
+ // Embedded read-only terminal
210
+ useEffect(() => {
211
+ if (!showOutput || execStatus === 'idle') return
212
+ if (!termContainerRef.current) return
213
+
214
+ termDisposedRef.current = false
215
+ let term = null
216
+ let ws = null
217
+
218
+ async function setup() {
219
+ try {
220
+ const ghostty = await loadGhostty()
221
+ if (termDisposedRef.current) return
222
+ if (!ghostty) return
223
+
224
+ const widthPx = typeof width === 'number' ? width : 320
225
+ const heightPx = typeof height === 'number' ? height : MINI_TERMINAL_HEIGHT
226
+ const dims = calcMiniDimensions(widthPx, heightPx)
227
+
228
+ term = new ghostty.Terminal({
229
+ fontSize: MINI_FONT_SIZE,
230
+ fontFamily: "'Ghostty', 'SF Mono', 'Menlo', 'Monaco', 'Courier New', monospace",
231
+ cursorBlink: false,
232
+ cursorStyle: 'bar',
233
+ cols: dims.cols,
234
+ rows: dims.rows,
235
+ theme: DEFAULT_THEME,
236
+ })
237
+
238
+ term.open(termContainerRef.current)
239
+ termRef.current = term
240
+
241
+ const pathParts = window.location.pathname.split('/')
242
+ const canvasIdx = pathParts.indexOf('canvas')
243
+ const canvasId = canvasIdx >= 0 ? pathParts[canvasIdx + 1] : 'default'
244
+
245
+ const url = getWsUrl(id, canvasId, true)
246
+ ws = new WebSocket(url)
247
+ wsRef.current = ws
248
+
249
+ ws.onopen = () => {
250
+ if (termDisposedRef.current) return
251
+ ws.send(JSON.stringify({ type: 'resize', cols: dims.cols, rows: dims.rows }))
252
+ }
253
+
254
+ ws.onmessage = (e) => {
255
+ if (termDisposedRef.current) return
256
+ const data = typeof e.data === 'string' ? e.data : null
257
+ if (data && data.startsWith('{')) {
258
+ try {
259
+ const msg = JSON.parse(data)
260
+ if (msg.type === 'session-info' || msg.type === 'error') return
261
+ } catch { /* not JSON */ }
262
+ }
263
+ term.write(typeof e.data === 'string' ? e.data : new Uint8Array(e.data))
264
+ }
265
+
266
+ ws.onclose = () => {}
267
+ ws.onerror = () => {}
268
+ } catch (err) {
269
+ console.warn('[PromptWidget] terminal setup failed:', err.message)
270
+ }
271
+ }
272
+
273
+ setup()
274
+
275
+ return () => {
276
+ termDisposedRef.current = true
277
+ if (ws && ws.readyState <= 1) try { ws.close() } catch { /* best effort */ }
278
+ if (term) try { term.dispose() } catch { /* best effort */ }
279
+ termRef.current = null
280
+ wsRef.current = null
281
+ }
282
+ }, [showOutput, execStatus, id, width, height])
283
+
284
+ const isPending = execStatus === 'pending'
285
+ const isDone = execStatus === 'done'
286
+ const isError = execStatus === 'error'
287
+ const hasSession = isPending || isDone || isError
288
+
289
+ return (
290
+ <div
291
+ ref={containerRef}
292
+ className={styles.wrapper}
293
+ style={typeof width === 'number' ? { width: `${width}px` } : undefined}
294
+ >
295
+ {/* ── Idle / Error: input state ── */}
296
+ {(execStatus === 'idle' || isError) && (
297
+ <>
298
+ <div className={styles.header}>
299
+ <span className={styles.avatar}>
300
+ <CopilotIcon size={20} />
301
+ </span>
302
+ <textarea
303
+ ref={textareaRef}
304
+ className={styles.textarea}
305
+ data-canvas-allow-text-selection
306
+ value={draftText}
307
+ onChange={(e) => setDraftText(e.target.value)}
308
+ onKeyDown={handleKeyDown}
309
+ onMouseDown={(e) => e.stopPropagation()}
310
+ onPointerDown={(e) => e.stopPropagation()}
311
+ placeholder="What should I do?"
312
+ rows={1}
313
+ disabled={!canEdit}
314
+ />
315
+ </div>
316
+
317
+ {isError && (
318
+ <p className={styles.errorText} title={execError}>
319
+ {execError}
320
+ </p>
321
+ )}
322
+
323
+ <div className={styles.toolbar}>
324
+ <button
325
+ className={styles.submitBtn}
326
+ onClick={handleSubmit}
327
+ disabled={!draftText.trim() || !canEdit}
328
+ title="Run prompt (⌘+Enter)"
329
+ >
330
+ {isError ? 'Retry' : 'Run'}
331
+ </button>
332
+ </div>
333
+ </>
334
+ )}
335
+
336
+ {/* ── Pending state ── */}
337
+ {isPending && (
338
+ <div className={styles.pendingArea}>
339
+ <div className={styles.pendingHeader}>
340
+ <span className={styles.avatar}>
341
+ <CopilotIcon size={20} />
342
+ </span>
343
+ <p className={styles.pendingText}>{persistedText || draftText}</p>
344
+ </div>
345
+ <div className={styles.pendingFooter}>
346
+ <span className={styles.pendingHint}>Processing…</span>
347
+ <button
348
+ className={styles.cancelBtn}
349
+ onClick={handleCancel}
350
+ title="Cancel (stop agent)"
351
+ >
352
+ <svg className={styles.spinnerSvg} viewBox="0 0 16 16" width="14" height="14">
353
+ <circle cx="8" cy="8" r="6" fill="none" stroke="currentColor" strokeWidth="2" strokeDasharray="28 10" />
354
+ </svg>
355
+ <span className={styles.stopIcon}>
356
+ <SquareFillIcon size={8} />
357
+ </span>
358
+ </button>
359
+ </div>
360
+ </div>
361
+ )}
362
+
363
+ {/* ── Done state ── */}
364
+ {isDone && (
365
+ <div className={styles.doneArea}>
366
+ <div className={styles.doneHeader}>
367
+ <span className={`${styles.avatar} ${styles.avatarDone}`}>
368
+ <CheckCircleIcon size={20} />
369
+ </span>
370
+ <p className={styles.doneText}>{persistedText}</p>
371
+ </div>
372
+ <button className={styles.resetBtn} onClick={handleReset}>
373
+ New prompt
374
+ </button>
375
+ </div>
376
+ )}
377
+
378
+ {/* ── Inline read-only terminal output ── */}
379
+ {hasSession && showOutput && (
380
+ <div className={styles.terminalArea}>
381
+ <div className={styles.terminalHeader}>
382
+ <span className={styles.terminalLabel}>Output</span>
383
+ <button
384
+ className={styles.terminalClose}
385
+ onClick={() => setShowOutput(false)}
386
+ title="Hide output"
387
+ >
388
+ <XIcon size={12} />
389
+ </button>
390
+ </div>
391
+ <div
392
+ ref={termContainerRef}
393
+ className={styles.terminalContainer}
394
+ onMouseDown={(e) => e.stopPropagation()}
395
+ onPointerDown={(e) => e.stopPropagation()}
396
+ style={{
397
+ pointerEvents: 'none',
398
+ height: typeof height === 'number' ? `${height}px` : undefined,
399
+ }}
400
+ />
401
+ </div>
402
+ )}
403
+ {resizable && (
404
+ <ResizeHandle
405
+ targetRef={containerRef}
406
+ minWidth={200}
407
+ onResize={handleResize}
408
+ />
409
+ )}
410
+ </div>
411
+ )
412
+ })
413
+
414
+ export default PromptWidget
@@ -0,0 +1,273 @@
1
+ /* ── Loom-style prompt widget ── */
2
+
3
+ .wrapper {
4
+ background: var(--bgColor-default, #fff);
5
+ border: 1px solid var(--borderColor-default, #d0d7de);
6
+ border-radius: 16px;
7
+ padding: 20px;
8
+ min-width: 200px;
9
+ font-family: var(--fontStack-system, -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif);
10
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08), 0 0 1px rgba(0, 0, 0, 0.06);
11
+ }
12
+
13
+ /* ── Header: avatar + textarea ── */
14
+
15
+ .header {
16
+ display: flex;
17
+ align-items: flex-start;
18
+ gap: 10px;
19
+ margin-bottom: 16px;
20
+ }
21
+
22
+ .avatar {
23
+ width: 32px;
24
+ height: 32px;
25
+ border-radius: 50%;
26
+ background: var(--bgColor-neutral-muted, #eaeef2);
27
+ display: flex;
28
+ align-items: center;
29
+ justify-content: center;
30
+ flex-shrink: 0;
31
+ color: var(--fgColor-muted, #636c76);
32
+ }
33
+
34
+ .avatarDone {
35
+ background: var(--bgColor-success-muted, #dafbe1);
36
+ color: var(--fgColor-success, #1a7f37);
37
+ }
38
+
39
+ .textarea {
40
+ flex: 1;
41
+ border: none;
42
+ background: transparent;
43
+ color: var(--fgColor-default, #1f2328);
44
+ font-size: 14px;
45
+ font-family: inherit;
46
+ line-height: 1.5;
47
+ resize: none;
48
+ padding: 4px 0;
49
+ min-height: 24px;
50
+ field-sizing: content;
51
+ }
52
+
53
+ .textarea:focus {
54
+ outline: none;
55
+ }
56
+
57
+ .textarea::placeholder {
58
+ color: var(--fgColor-muted, #636c76);
59
+ }
60
+
61
+ /* ── Toolbar: icons + submit ── */
62
+
63
+ .toolbar {
64
+ display: flex;
65
+ align-items: center;
66
+ justify-content: flex-end;
67
+ margin-bottom: 4px;
68
+ }
69
+
70
+
71
+ .submitBtn {
72
+ font-size: 12px;
73
+ padding: 4px 14px;
74
+ border-radius: 6px;
75
+ border: 1px solid var(--borderColor-default, #d0d7de);
76
+ background: var(--bgColor-default, #fff);
77
+ color: var(--fgColor-muted, #636c76);
78
+ cursor: pointer;
79
+ white-space: nowrap;
80
+ font-weight: 500;
81
+ transition: background 0.15s, color 0.15s;
82
+ }
83
+
84
+ .submitBtn:hover:not(:disabled) {
85
+ background: var(--bgColor-neutral-muted, rgba(175, 184, 193, 0.2));
86
+ color: var(--fgColor-default, #1f2328);
87
+ }
88
+
89
+ .submitBtn:disabled {
90
+ opacity: 0.4;
91
+ cursor: not-allowed;
92
+ }
93
+
94
+ /* ── Error ── */
95
+
96
+ .errorText {
97
+ margin: 0 0 12px;
98
+ padding: 6px 10px;
99
+ background: var(--bgColor-danger-muted, #ffebe9);
100
+ border-radius: 8px;
101
+ color: var(--fgColor-danger, #cf222e);
102
+ font-size: 12px;
103
+ line-height: 1.4;
104
+ overflow: hidden;
105
+ text-overflow: ellipsis;
106
+ white-space: nowrap;
107
+ }
108
+
109
+ /* ── Pending state ── */
110
+
111
+ .pendingArea {
112
+ display: flex;
113
+ flex-direction: column;
114
+ gap: 12px;
115
+ }
116
+
117
+ .pendingHeader {
118
+ display: flex;
119
+ align-items: flex-start;
120
+ gap: 10px;
121
+ }
122
+
123
+ .pendingText {
124
+ font-size: 14px;
125
+ color: var(--fgColor-default, #1f2328);
126
+ line-height: 1.5;
127
+ margin: 0;
128
+ padding-top: 4px;
129
+ }
130
+
131
+ .pendingFooter {
132
+ display: flex;
133
+ align-items: center;
134
+ justify-content: space-between;
135
+ }
136
+
137
+ .pendingHint {
138
+ font-size: 12px;
139
+ color: var(--fgColor-muted, #636c76);
140
+ margin: 0;
141
+ }
142
+
143
+ .cancelBtn {
144
+ display: flex;
145
+ align-items: center;
146
+ justify-content: center;
147
+ padding: 4px;
148
+ background: transparent;
149
+ border: none;
150
+ border-radius: 6px;
151
+ cursor: pointer;
152
+ color: var(--fgColor-accent, #0969da);
153
+ transition: background 0.15s;
154
+ position: relative;
155
+ width: 24px;
156
+ height: 24px;
157
+ }
158
+
159
+ .cancelBtn:hover {
160
+ background: var(--bgColor-neutral-muted, rgba(175, 184, 193, 0.2));
161
+ }
162
+
163
+ .spinnerSvg {
164
+ animation: spin 1s linear infinite;
165
+ color: var(--fgColor-accent, #0969da);
166
+ }
167
+
168
+ .stopIcon {
169
+ display: none;
170
+ position: absolute;
171
+ color: var(--fgColor-danger, #cf222e);
172
+ }
173
+
174
+ .cancelBtn:hover .spinnerSvg {
175
+ display: none;
176
+ }
177
+
178
+ .cancelBtn:hover .stopIcon {
179
+ display: flex;
180
+ }
181
+
182
+ @keyframes spin {
183
+ from { transform: rotate(0deg); }
184
+ to { transform: rotate(360deg); }
185
+ }
186
+
187
+ /* ── Done state ── */
188
+
189
+ .doneArea {
190
+ display: flex;
191
+ flex-direction: column;
192
+ gap: 12px;
193
+ }
194
+
195
+ .doneHeader {
196
+ display: flex;
197
+ align-items: flex-start;
198
+ gap: 10px;
199
+ }
200
+
201
+ .doneText {
202
+ font-size: 14px;
203
+ color: var(--fgColor-default, #1f2328);
204
+ line-height: 1.5;
205
+ margin: 0;
206
+ padding-top: 4px;
207
+ }
208
+
209
+ .resetBtn {
210
+ align-self: flex-end;
211
+ padding: 4px 14px;
212
+ background: transparent;
213
+ color: var(--fgColor-accent, #0969da);
214
+ border: 1px solid var(--borderColor-default, #d0d7de);
215
+ border-radius: 6px;
216
+ font-size: 12px;
217
+ cursor: pointer;
218
+ transition: background 0.15s;
219
+ }
220
+
221
+ .resetBtn:hover {
222
+ background: var(--bgColor-muted, #f6f8fa);
223
+ }
224
+
225
+ /* ── Inline terminal output (flush to bottom edge) ── */
226
+
227
+ .terminalArea {
228
+ margin: 16px -20px -20px;
229
+ border-top: 1px solid var(--borderColor-muted, #d8dee4);
230
+ overflow: hidden;
231
+ border-radius: 0 0 16px 16px;
232
+ }
233
+
234
+ .terminalHeader {
235
+ display: flex;
236
+ align-items: center;
237
+ justify-content: space-between;
238
+ padding: 4px 10px;
239
+ background: var(--bgColor-muted, #f6f8fa);
240
+ border-bottom: 1px solid var(--borderColor-muted, #d8dee4);
241
+ }
242
+
243
+ .terminalLabel {
244
+ font-size: 11px;
245
+ font-weight: 600;
246
+ color: var(--fgColor-muted, #636c76);
247
+ text-transform: uppercase;
248
+ letter-spacing: 0.5px;
249
+ }
250
+
251
+ .terminalClose {
252
+ display: flex;
253
+ align-items: center;
254
+ justify-content: center;
255
+ padding: 2px;
256
+ background: transparent;
257
+ border: none;
258
+ border-radius: 4px;
259
+ cursor: pointer;
260
+ color: var(--fgColor-muted, #636c76);
261
+ transition: background 0.15s;
262
+ }
263
+
264
+ .terminalClose:hover {
265
+ background: var(--bgColor-neutral-muted, rgba(175, 184, 193, 0.2));
266
+ color: var(--fgColor-default, #1f2328);
267
+ }
268
+
269
+ .terminalContainer {
270
+ height: 180px;
271
+ background: #0d1117;
272
+ overflow: auto;
273
+ }