create-ekka-desktop-app 0.2.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 (96) hide show
  1. package/README.md +137 -0
  2. package/bin/cli.js +72 -0
  3. package/package.json +23 -0
  4. package/template/branding/app.json +6 -0
  5. package/template/branding/icon.icns +0 -0
  6. package/template/eslint.config.js +98 -0
  7. package/template/index.html +29 -0
  8. package/template/package.json +40 -0
  9. package/template/src/app/App.tsx +24 -0
  10. package/template/src/demo/DemoApp.tsx +260 -0
  11. package/template/src/demo/components/Banner.tsx +82 -0
  12. package/template/src/demo/components/EmptyState.tsx +61 -0
  13. package/template/src/demo/components/InfoPopover.tsx +171 -0
  14. package/template/src/demo/components/InfoTooltip.tsx +76 -0
  15. package/template/src/demo/components/LearnMore.tsx +98 -0
  16. package/template/src/demo/components/NodeCredentialsOnboarding.tsx +219 -0
  17. package/template/src/demo/components/SetupWizard.tsx +48 -0
  18. package/template/src/demo/components/StatusBadge.tsx +83 -0
  19. package/template/src/demo/components/index.ts +10 -0
  20. package/template/src/demo/hooks/index.ts +6 -0
  21. package/template/src/demo/hooks/useAuditEvents.ts +30 -0
  22. package/template/src/demo/layout/Shell.tsx +110 -0
  23. package/template/src/demo/layout/Sidebar.tsx +192 -0
  24. package/template/src/demo/pages/AuditLogPage.tsx +235 -0
  25. package/template/src/demo/pages/DocGenPage.tsx +874 -0
  26. package/template/src/demo/pages/HomeSetupPage.tsx +182 -0
  27. package/template/src/demo/pages/LoginPage.tsx +192 -0
  28. package/template/src/demo/pages/PathPermissionsPage.tsx +873 -0
  29. package/template/src/demo/pages/RunnerPage.tsx +445 -0
  30. package/template/src/demo/pages/SystemPage.tsx +557 -0
  31. package/template/src/demo/pages/VaultPage.tsx +805 -0
  32. package/template/src/ekka/__tests__/demo-backend.test.ts +187 -0
  33. package/template/src/ekka/audit/index.ts +7 -0
  34. package/template/src/ekka/audit/store.ts +68 -0
  35. package/template/src/ekka/audit/types.ts +22 -0
  36. package/template/src/ekka/auth/client.ts +212 -0
  37. package/template/src/ekka/auth/index.ts +30 -0
  38. package/template/src/ekka/auth/storage.ts +114 -0
  39. package/template/src/ekka/auth/types.ts +67 -0
  40. package/template/src/ekka/backend/demo.ts +151 -0
  41. package/template/src/ekka/backend/interface.ts +36 -0
  42. package/template/src/ekka/config.ts +48 -0
  43. package/template/src/ekka/constants.ts +143 -0
  44. package/template/src/ekka/errors.ts +54 -0
  45. package/template/src/ekka/index.ts +516 -0
  46. package/template/src/ekka/internal/backend.ts +156 -0
  47. package/template/src/ekka/internal/index.ts +7 -0
  48. package/template/src/ekka/ops/auth.ts +29 -0
  49. package/template/src/ekka/ops/debug.ts +68 -0
  50. package/template/src/ekka/ops/home.ts +101 -0
  51. package/template/src/ekka/ops/index.ts +16 -0
  52. package/template/src/ekka/ops/nodeCredentials.ts +131 -0
  53. package/template/src/ekka/ops/nodeSession.ts +145 -0
  54. package/template/src/ekka/ops/paths.ts +183 -0
  55. package/template/src/ekka/ops/runner.ts +86 -0
  56. package/template/src/ekka/ops/runtime.ts +31 -0
  57. package/template/src/ekka/ops/setup.ts +47 -0
  58. package/template/src/ekka/ops/vault.ts +459 -0
  59. package/template/src/ekka/ops/workflowRuns.ts +116 -0
  60. package/template/src/ekka/types.ts +82 -0
  61. package/template/src/ekka/utils/idempotency.ts +14 -0
  62. package/template/src/ekka/utils/index.ts +7 -0
  63. package/template/src/ekka/utils/time.ts +77 -0
  64. package/template/src/main.tsx +12 -0
  65. package/template/src/vite-env.d.ts +12 -0
  66. package/template/src-tauri/Cargo.toml +41 -0
  67. package/template/src-tauri/build.rs +3 -0
  68. package/template/src-tauri/capabilities/default.json +11 -0
  69. package/template/src-tauri/icons/icon.icns +0 -0
  70. package/template/src-tauri/icons/icon.png +0 -0
  71. package/template/src-tauri/resources/ekka-engine-bootstrap +0 -0
  72. package/template/src-tauri/src/bootstrap.rs +37 -0
  73. package/template/src-tauri/src/commands.rs +1215 -0
  74. package/template/src-tauri/src/device_secret.rs +111 -0
  75. package/template/src-tauri/src/engine_process.rs +538 -0
  76. package/template/src-tauri/src/grants.rs +129 -0
  77. package/template/src-tauri/src/handlers/home.rs +65 -0
  78. package/template/src-tauri/src/handlers/mod.rs +7 -0
  79. package/template/src-tauri/src/handlers/paths.rs +128 -0
  80. package/template/src-tauri/src/handlers/vault.rs +680 -0
  81. package/template/src-tauri/src/main.rs +243 -0
  82. package/template/src-tauri/src/node_auth.rs +858 -0
  83. package/template/src-tauri/src/node_credentials.rs +541 -0
  84. package/template/src-tauri/src/node_runner.rs +882 -0
  85. package/template/src-tauri/src/node_vault_crypto.rs +113 -0
  86. package/template/src-tauri/src/node_vault_store.rs +267 -0
  87. package/template/src-tauri/src/ops/auth.rs +50 -0
  88. package/template/src-tauri/src/ops/home.rs +251 -0
  89. package/template/src-tauri/src/ops/mod.rs +7 -0
  90. package/template/src-tauri/src/ops/runtime.rs +21 -0
  91. package/template/src-tauri/src/state.rs +639 -0
  92. package/template/src-tauri/src/types.rs +84 -0
  93. package/template/src-tauri/tauri.conf.json +41 -0
  94. package/template/tsconfig.json +26 -0
  95. package/template/tsconfig.tsbuildinfo +1 -0
  96. package/template/vite.config.ts +34 -0
@@ -0,0 +1,874 @@
1
+ /**
2
+ * Documentation Generation Page
3
+ *
4
+ * Allows users to select a source folder and generate documentation
5
+ * using the ekka-docgen-basic prompt via wf_prompt_run workflow.
6
+ */
7
+
8
+ import { useState, useEffect, useRef, type CSSProperties, type ReactElement } from 'react';
9
+ import {
10
+ createWorkflowRun,
11
+ getWorkflowRun,
12
+ type WorkflowRun,
13
+ type DebugBundleInfo,
14
+ } from '../../ekka/ops/workflowRuns';
15
+ import * as debugOps from '../../ekka/ops/debug';
16
+
17
+ interface DocGenPageProps {
18
+ darkMode: boolean;
19
+ }
20
+
21
+ type GenerationStatus = 'idle' | 'queued' | 'running' | 'completed' | 'failed';
22
+
23
+ // Hardcoded prompt configuration - NO prompt selection UI
24
+ const PROMPT_CONFIG = {
25
+ provider: 'opik',
26
+ prompt_slug: 'ekka-docgen-basic',
27
+ prompt_version: '1',
28
+ } as const;
29
+
30
+ export function DocGenPage({ darkMode }: DocGenPageProps): ReactElement {
31
+ const [selectedFolder, setSelectedFolder] = useState<string | null>(null);
32
+ const [status, setStatus] = useState<GenerationStatus>('idle');
33
+ const [workflowRunId, setWorkflowRunId] = useState<string | null>(null);
34
+ const [workflowRun, setWorkflowRun] = useState<WorkflowRun | null>(null);
35
+ const [error, setError] = useState<string | null>(null);
36
+ const [copySuccess, setCopySuccess] = useState(false);
37
+ const [isDevMode, setIsDevMode] = useState(false);
38
+ const [pathCopySuccess, setPathCopySuccess] = useState(false);
39
+ const pollingRef = useRef<number | null>(null);
40
+
41
+ const colors = {
42
+ text: darkMode ? '#ffffff' : '#1d1d1f',
43
+ textMuted: darkMode ? '#98989d' : '#6e6e73',
44
+ textDim: darkMode ? '#636366' : '#aeaeb2',
45
+ bg: darkMode ? '#2c2c2e' : '#fafafa',
46
+ bgAlt: darkMode ? '#1c1c1e' : '#ffffff',
47
+ bgInput: darkMode ? '#3a3a3c' : '#ffffff',
48
+ border: darkMode ? '#3a3a3c' : '#e5e5e5',
49
+ accent: darkMode ? '#0a84ff' : '#007aff',
50
+ green: darkMode ? '#30d158' : '#34c759',
51
+ orange: darkMode ? '#ff9f0a' : '#ff9500',
52
+ red: darkMode ? '#ff453a' : '#ff3b30',
53
+ purple: darkMode ? '#bf5af2' : '#af52de',
54
+ };
55
+
56
+ const styles: Record<string, CSSProperties> = {
57
+ container: {
58
+ width: '100%',
59
+ maxWidth: '900px',
60
+ },
61
+ header: {
62
+ marginBottom: '32px',
63
+ },
64
+ title: {
65
+ fontSize: '28px',
66
+ fontWeight: 700,
67
+ color: colors.text,
68
+ marginBottom: '8px',
69
+ letterSpacing: '-0.02em',
70
+ },
71
+ subtitle: {
72
+ fontSize: '14px',
73
+ color: colors.textMuted,
74
+ lineHeight: 1.6,
75
+ maxWidth: '600px',
76
+ },
77
+ section: {
78
+ marginBottom: '28px',
79
+ },
80
+ sectionHeader: {
81
+ display: 'flex',
82
+ alignItems: 'center',
83
+ gap: '8px',
84
+ marginBottom: '12px',
85
+ },
86
+ sectionTitle: {
87
+ fontSize: '11px',
88
+ fontWeight: 600,
89
+ color: colors.textMuted,
90
+ textTransform: 'uppercase' as const,
91
+ letterSpacing: '0.05em',
92
+ },
93
+ sectionLine: {
94
+ flex: 1,
95
+ height: '1px',
96
+ background: colors.border,
97
+ },
98
+ card: {
99
+ background: colors.bg,
100
+ border: `1px solid ${colors.border}`,
101
+ borderRadius: '12px',
102
+ padding: '20px',
103
+ },
104
+ folderSelector: {
105
+ display: 'flex',
106
+ gap: '12px',
107
+ alignItems: 'center',
108
+ },
109
+ selectedFolderBox: {
110
+ flex: 1,
111
+ padding: '12px 16px',
112
+ background: colors.bgInput,
113
+ border: `1px solid ${colors.border}`,
114
+ borderRadius: '8px',
115
+ fontSize: '13px',
116
+ fontFamily: 'SF Mono, Monaco, Consolas, monospace',
117
+ color: colors.text,
118
+ overflow: 'hidden',
119
+ textOverflow: 'ellipsis',
120
+ whiteSpace: 'nowrap' as const,
121
+ },
122
+ placeholderText: {
123
+ color: colors.textMuted,
124
+ fontFamily: '-apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui, sans-serif',
125
+ },
126
+ button: {
127
+ padding: '10px 20px',
128
+ fontSize: '13px',
129
+ fontWeight: 600,
130
+ color: '#ffffff',
131
+ background: colors.accent,
132
+ border: 'none',
133
+ borderRadius: '8px',
134
+ cursor: 'pointer',
135
+ transition: 'opacity 0.15s ease',
136
+ whiteSpace: 'nowrap' as const,
137
+ },
138
+ buttonSecondary: {
139
+ padding: '10px 20px',
140
+ fontSize: '13px',
141
+ fontWeight: 600,
142
+ color: colors.accent,
143
+ background: darkMode ? 'rgba(10, 132, 255, 0.15)' : 'rgba(0, 122, 255, 0.1)',
144
+ border: 'none',
145
+ borderRadius: '8px',
146
+ cursor: 'pointer',
147
+ transition: 'opacity 0.15s ease',
148
+ whiteSpace: 'nowrap' as const,
149
+ },
150
+ buttonDisabled: {
151
+ opacity: 0.5,
152
+ cursor: 'not-allowed',
153
+ },
154
+ error: {
155
+ marginBottom: '20px',
156
+ padding: '12px 14px',
157
+ background: darkMode ? '#3c1618' : '#fef2f2',
158
+ border: `1px solid ${darkMode ? '#7f1d1d' : '#fecaca'}`,
159
+ borderRadius: '8px',
160
+ fontSize: '13px',
161
+ color: darkMode ? '#fca5a5' : '#991b1b',
162
+ },
163
+ progressCard: {
164
+ padding: '20px',
165
+ background: colors.bg,
166
+ border: `1px solid ${colors.border}`,
167
+ borderRadius: '12px',
168
+ },
169
+ progressHeader: {
170
+ display: 'flex',
171
+ alignItems: 'center',
172
+ gap: '12px',
173
+ marginBottom: '16px',
174
+ },
175
+ progressSpinner: {
176
+ width: '20px',
177
+ height: '20px',
178
+ border: `2px solid ${colors.border}`,
179
+ borderTopColor: colors.accent,
180
+ borderRadius: '50%',
181
+ animation: 'spin 1s linear infinite',
182
+ },
183
+ progressTitle: {
184
+ fontSize: '15px',
185
+ fontWeight: 600,
186
+ color: colors.text,
187
+ },
188
+ progressSteps: {
189
+ display: 'flex',
190
+ gap: '8px',
191
+ flexWrap: 'wrap' as const,
192
+ },
193
+ progressStep: {
194
+ display: 'flex',
195
+ alignItems: 'center',
196
+ gap: '6px',
197
+ padding: '6px 12px',
198
+ borderRadius: '6px',
199
+ fontSize: '12px',
200
+ fontWeight: 500,
201
+ },
202
+ stepActive: {
203
+ background: darkMode ? 'rgba(10, 132, 255, 0.15)' : 'rgba(0, 122, 255, 0.1)',
204
+ color: colors.accent,
205
+ },
206
+ stepComplete: {
207
+ background: darkMode ? 'rgba(48, 209, 88, 0.15)' : 'rgba(52, 199, 89, 0.1)',
208
+ color: colors.green,
209
+ },
210
+ stepPending: {
211
+ background: darkMode ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.03)',
212
+ color: colors.textMuted,
213
+ },
214
+ stepFailed: {
215
+ background: darkMode ? 'rgba(255, 69, 58, 0.15)' : 'rgba(255, 59, 48, 0.1)',
216
+ color: colors.red,
217
+ },
218
+ outputCard: {
219
+ background: colors.bg,
220
+ border: `1px solid ${colors.border}`,
221
+ borderRadius: '12px',
222
+ overflow: 'hidden',
223
+ },
224
+ outputHeader: {
225
+ display: 'flex',
226
+ alignItems: 'center',
227
+ justifyContent: 'space-between',
228
+ padding: '12px 16px',
229
+ borderBottom: `1px solid ${colors.border}`,
230
+ background: darkMode ? 'rgba(255, 255, 255, 0.02)' : 'rgba(0, 0, 0, 0.02)',
231
+ },
232
+ outputMeta: {
233
+ display: 'flex',
234
+ gap: '16px',
235
+ fontSize: '12px',
236
+ color: colors.textMuted,
237
+ },
238
+ outputMetaItem: {
239
+ display: 'flex',
240
+ alignItems: 'center',
241
+ gap: '4px',
242
+ },
243
+ outputContent: {
244
+ padding: '20px',
245
+ maxHeight: '500px',
246
+ overflowY: 'auto' as const,
247
+ fontSize: '14px',
248
+ lineHeight: 1.7,
249
+ color: colors.text,
250
+ whiteSpace: 'pre-wrap' as const,
251
+ fontFamily: '-apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui, sans-serif',
252
+ },
253
+ badge: {
254
+ display: 'inline-flex',
255
+ alignItems: 'center',
256
+ gap: '4px',
257
+ padding: '4px 8px',
258
+ borderRadius: '4px',
259
+ fontSize: '11px',
260
+ fontWeight: 600,
261
+ },
262
+ badgeGreen: {
263
+ background: darkMode ? 'rgba(48, 209, 88, 0.15)' : 'rgba(52, 199, 89, 0.12)',
264
+ color: colors.green,
265
+ },
266
+ badgeRed: {
267
+ background: darkMode ? 'rgba(255, 69, 58, 0.15)' : 'rgba(255, 59, 48, 0.12)',
268
+ color: colors.red,
269
+ },
270
+ copyButton: {
271
+ padding: '6px 12px',
272
+ fontSize: '12px',
273
+ fontWeight: 500,
274
+ color: colors.accent,
275
+ background: 'transparent',
276
+ border: `1px solid ${colors.border}`,
277
+ borderRadius: '6px',
278
+ cursor: 'pointer',
279
+ display: 'flex',
280
+ alignItems: 'center',
281
+ gap: '4px',
282
+ },
283
+ };
284
+
285
+ // Check dev mode on mount
286
+ useEffect(() => {
287
+ debugOps.isDevMode().then(setIsDevMode).catch(() => setIsDevMode(false));
288
+ }, []);
289
+
290
+ // Cleanup polling on unmount
291
+ useEffect(() => {
292
+ return () => {
293
+ if (pollingRef.current) {
294
+ clearInterval(pollingRef.current);
295
+ }
296
+ };
297
+ }, []);
298
+
299
+ // Handle folder selection
300
+ const handleSelectFolder = async () => {
301
+ setError(null);
302
+ try {
303
+ const { open } = await import('@tauri-apps/plugin-dialog');
304
+ const selected = await open({ directory: true, multiple: false });
305
+ if (selected && typeof selected === 'string') {
306
+ setSelectedFolder(selected);
307
+ // Reset state when new folder selected
308
+ setStatus('idle');
309
+ setWorkflowRunId(null);
310
+ setWorkflowRun(null);
311
+ }
312
+ } catch (err) {
313
+ const message = err instanceof Error ? err.message : String(err);
314
+ setError(`Failed to open folder picker: ${message}`);
315
+ }
316
+ };
317
+
318
+ // Start generation
319
+ const handleGenerate = async () => {
320
+ if (!selectedFolder) return;
321
+
322
+ // Stop any existing polling
323
+ if (pollingRef.current) {
324
+ clearInterval(pollingRef.current);
325
+ pollingRef.current = null;
326
+ }
327
+
328
+ setError(null);
329
+ setStatus('queued');
330
+ setWorkflowRun(null);
331
+
332
+ try {
333
+ // Create workflow run - DO NOT log the input
334
+ // JWT is handled internally by the ops layer
335
+ const response = await createWorkflowRun({
336
+ type: 'wf_prompt_run',
337
+ confidentiality: 'confidential',
338
+ context: {
339
+ prompt_ref: PROMPT_CONFIG,
340
+ variables: { input: selectedFolder },
341
+ },
342
+ });
343
+
344
+ setWorkflowRunId(response.id);
345
+
346
+ // Start polling
347
+ startPolling(response.id);
348
+ } catch (err) {
349
+ const message = err instanceof Error ? err.message : 'Failed to start generation';
350
+ setError(message);
351
+ setStatus('failed');
352
+ }
353
+ };
354
+
355
+ // Poll for status updates
356
+ const startPolling = (id: string) => {
357
+ const poll = async () => {
358
+ try {
359
+ // JWT is handled internally by the ops layer
360
+ const run = await getWorkflowRun(id);
361
+ setWorkflowRun(run);
362
+
363
+ // Update status based on workflow state
364
+ if (run.status === 'completed') {
365
+ setStatus('completed');
366
+ if (pollingRef.current) {
367
+ clearInterval(pollingRef.current);
368
+ pollingRef.current = null;
369
+ }
370
+ } else if (run.status === 'failed') {
371
+ setStatus('failed');
372
+ setError(run.error || 'Workflow failed');
373
+ if (pollingRef.current) {
374
+ clearInterval(pollingRef.current);
375
+ pollingRef.current = null;
376
+ }
377
+ } else if (run.status === 'running' || run.progress > 0) {
378
+ setStatus('running');
379
+ } else {
380
+ setStatus('queued');
381
+ }
382
+ } catch (err) {
383
+ const message = err instanceof Error ? err.message : 'Failed to fetch status';
384
+ setError(message);
385
+ setStatus('failed');
386
+ if (pollingRef.current) {
387
+ clearInterval(pollingRef.current);
388
+ pollingRef.current = null;
389
+ }
390
+ }
391
+ };
392
+
393
+ // Initial poll
394
+ void poll();
395
+
396
+ // Poll every 1.5 seconds
397
+ pollingRef.current = window.setInterval(poll, 1500);
398
+ };
399
+
400
+ // Copy output to clipboard
401
+ const handleCopyOutput = async () => {
402
+ if (!workflowRun?.result?.output_text) return;
403
+
404
+ try {
405
+ await navigator.clipboard.writeText(workflowRun.result?.output_text || '');
406
+ setCopySuccess(true);
407
+ setTimeout(() => setCopySuccess(false), 2000);
408
+ } catch {
409
+ setError('Failed to copy to clipboard');
410
+ }
411
+ };
412
+
413
+ const isGenerating = status === 'queued' || status === 'running';
414
+ const canGenerate = selectedFolder && !isGenerating;
415
+
416
+ return (
417
+ <div style={styles.container}>
418
+ {/* CSS for spinner animation */}
419
+ <style>
420
+ {`
421
+ @keyframes spin {
422
+ to { transform: rotate(360deg); }
423
+ }
424
+ `}
425
+ </style>
426
+
427
+ <header style={styles.header}>
428
+ <h1 style={styles.title}>Generate Documentation</h1>
429
+ <p style={styles.subtitle}>
430
+ Select a source folder to automatically generate documentation using AI.
431
+ The generated documentation will be displayed below.
432
+ </p>
433
+ </header>
434
+
435
+ {error && <div style={styles.error}>{error}</div>}
436
+
437
+ {/* Section: Folder Selection */}
438
+ <div style={styles.section}>
439
+ <div style={styles.sectionHeader}>
440
+ <span style={styles.sectionTitle}>Source Folder</span>
441
+ <div style={styles.sectionLine} />
442
+ </div>
443
+ <div style={styles.card}>
444
+ <div style={styles.folderSelector}>
445
+ <div style={styles.selectedFolderBox}>
446
+ {selectedFolder ? (
447
+ selectedFolder
448
+ ) : (
449
+ <span style={styles.placeholderText}>No folder selected</span>
450
+ )}
451
+ </div>
452
+ <button
453
+ onClick={() => void handleSelectFolder()}
454
+ style={styles.buttonSecondary}
455
+ disabled={isGenerating}
456
+ >
457
+ Browse...
458
+ </button>
459
+ </div>
460
+
461
+ <div style={{ marginTop: '16px' }}>
462
+ <button
463
+ onClick={() => void handleGenerate()}
464
+ style={{
465
+ ...styles.button,
466
+ ...(!canGenerate ? styles.buttonDisabled : {}),
467
+ }}
468
+ disabled={!canGenerate}
469
+ >
470
+ {isGenerating ? 'Generating...' : 'Generate Documentation'}
471
+ </button>
472
+ </div>
473
+ </div>
474
+ </div>
475
+
476
+ {/* Section: Progress */}
477
+ {(status !== 'idle' || workflowRunId) && (
478
+ <div style={styles.section}>
479
+ <div style={styles.sectionHeader}>
480
+ <span style={styles.sectionTitle}>Progress</span>
481
+ <div style={styles.sectionLine} />
482
+ </div>
483
+ <div style={styles.progressCard}>
484
+ <div style={styles.progressHeader}>
485
+ {isGenerating && <div style={styles.progressSpinner} />}
486
+ {status === 'completed' && <CheckIcon color={colors.green} />}
487
+ {status === 'failed' && <ErrorIcon color={colors.red} />}
488
+ <span style={styles.progressTitle}>
489
+ {status === 'queued' && 'Queued'}
490
+ {status === 'running' && 'Processing...'}
491
+ {status === 'completed' && 'Completed'}
492
+ {status === 'failed' && 'Failed'}
493
+ </span>
494
+ {workflowRunId && (
495
+ <span style={{ fontSize: '11px', color: colors.textMuted, fontFamily: 'monospace' }}>
496
+ ID: {workflowRunId.slice(0, 8)}...
497
+ </span>
498
+ )}
499
+ </div>
500
+ <div style={styles.progressSteps}>
501
+ <div
502
+ style={{
503
+ ...styles.progressStep,
504
+ ...(status === 'queued' ? styles.stepActive : styles.stepComplete),
505
+ }}
506
+ >
507
+ {status !== 'queued' && <CheckIcon color={colors.green} size={12} />}
508
+ Queued
509
+ </div>
510
+ <div
511
+ style={{
512
+ ...styles.progressStep,
513
+ ...(status === 'running'
514
+ ? styles.stepActive
515
+ : status === 'completed' || status === 'failed'
516
+ ? status === 'failed'
517
+ ? styles.stepFailed
518
+ : styles.stepComplete
519
+ : styles.stepPending),
520
+ }}
521
+ >
522
+ {status === 'completed' && <CheckIcon color={colors.green} size={12} />}
523
+ {status === 'failed' && <ErrorIcon color={colors.red} size={12} />}
524
+ Running
525
+ </div>
526
+ <div
527
+ style={{
528
+ ...styles.progressStep,
529
+ ...(status === 'completed'
530
+ ? styles.stepComplete
531
+ : status === 'failed'
532
+ ? styles.stepFailed
533
+ : styles.stepPending),
534
+ }}
535
+ >
536
+ {status === 'completed' && <CheckIcon color={colors.green} size={12} />}
537
+ {status === 'failed' && <ErrorIcon color={colors.red} size={12} />}
538
+ {status === 'completed' ? 'Completed' : status === 'failed' ? 'Failed' : 'Complete'}
539
+ </div>
540
+ </div>
541
+ {workflowRun && (
542
+ <div style={{ marginTop: '12px', fontSize: '12px', color: colors.textMuted }}>
543
+ Progress: {workflowRun.progress}%
544
+ </div>
545
+ )}
546
+ </div>
547
+ </div>
548
+ )}
549
+
550
+ {/* Section: Output */}
551
+ {status === 'completed' && workflowRun?.result?.output_text && (
552
+ <div style={styles.section}>
553
+ <div style={styles.sectionHeader}>
554
+ <span style={styles.sectionTitle}>Generated Documentation</span>
555
+ <div style={styles.sectionLine} />
556
+ </div>
557
+ <div style={styles.outputCard}>
558
+ <div style={styles.outputHeader}>
559
+ <div style={styles.outputMeta}>
560
+ <div style={styles.outputMetaItem}>
561
+ <span style={{ ...styles.badge, ...styles.badgeGreen }}>Completed</span>
562
+ </div>
563
+ <div style={styles.outputMetaItem}>
564
+ <span style={{ color: colors.textDim }}>ID:</span>
565
+ <code style={{ fontFamily: 'monospace', fontSize: '11px' }}>
566
+ {workflowRunId}
567
+ </code>
568
+ </div>
569
+ </div>
570
+ <button
571
+ onClick={() => void handleCopyOutput()}
572
+ style={styles.copyButton}
573
+ >
574
+ {copySuccess ? (
575
+ <>
576
+ <CheckIcon color={colors.green} size={14} />
577
+ Copied!
578
+ </>
579
+ ) : (
580
+ <>
581
+ <CopyIcon />
582
+ Copy Output
583
+ </>
584
+ )}
585
+ </button>
586
+ </div>
587
+ <div style={styles.outputContent}>{workflowRun.result?.output_text}</div>
588
+ </div>
589
+ </div>
590
+ )}
591
+
592
+ {/* Section: Error Details */}
593
+ {status === 'failed' && workflowRun && (
594
+ <div style={styles.section}>
595
+ <div style={styles.sectionHeader}>
596
+ <span style={styles.sectionTitle}>Error Details</span>
597
+ <div style={styles.sectionLine} />
598
+ </div>
599
+ <div style={{ ...styles.card, borderColor: colors.red }}>
600
+ <div style={{ marginBottom: '8px' }}>
601
+ <span style={{ ...styles.badge, ...styles.badgeRed }}>
602
+ ERROR
603
+ </span>
604
+ </div>
605
+ <p style={{ fontSize: '14px', color: colors.text, margin: 0 }}>
606
+ {workflowRun.error || 'An unknown error occurred'}
607
+ </p>
608
+ </div>
609
+ </div>
610
+ )}
611
+
612
+ {/* Section: Debug Bundle (Dev Mode Only) */}
613
+ {status === 'failed' && isDevMode && workflowRun?.result?.debug_bundle && (
614
+ <DebugBundleSection
615
+ debugBundle={workflowRun.result.debug_bundle}
616
+ darkMode={darkMode}
617
+ colors={colors}
618
+ pathCopySuccess={pathCopySuccess}
619
+ onCopyPath={async () => {
620
+ try {
621
+ await navigator.clipboard.writeText(workflowRun.result?.debug_bundle?.debug_bundle_ref || '');
622
+ setPathCopySuccess(true);
623
+ setTimeout(() => setPathCopySuccess(false), 2000);
624
+ } catch {
625
+ setError('Failed to copy path to clipboard');
626
+ }
627
+ }}
628
+ onOpenFolder={async () => {
629
+ try {
630
+ await debugOps.openFolder(workflowRun.result?.debug_bundle?.debug_bundle_ref || '');
631
+ } catch (err) {
632
+ setError(err instanceof Error ? err.message : 'Failed to open folder');
633
+ }
634
+ }}
635
+ />
636
+ )}
637
+ </div>
638
+ );
639
+ }
640
+
641
+ // =============================================================================
642
+ // Debug Bundle Section (Dev Mode Only)
643
+ // =============================================================================
644
+
645
+ interface DebugBundleSectionProps {
646
+ debugBundle: DebugBundleInfo;
647
+ darkMode: boolean;
648
+ colors: Record<string, string>;
649
+ pathCopySuccess: boolean;
650
+ onCopyPath: () => void;
651
+ onOpenFolder: () => void;
652
+ }
653
+
654
+ function DebugBundleSection({
655
+ debugBundle,
656
+ darkMode,
657
+ colors,
658
+ pathCopySuccess,
659
+ onCopyPath,
660
+ onOpenFolder,
661
+ }: DebugBundleSectionProps): ReactElement {
662
+ const styles: Record<string, CSSProperties> = {
663
+ section: {
664
+ marginBottom: '28px',
665
+ },
666
+ sectionHeader: {
667
+ display: 'flex',
668
+ alignItems: 'center',
669
+ gap: '8px',
670
+ marginBottom: '12px',
671
+ },
672
+ sectionTitle: {
673
+ fontSize: '11px',
674
+ fontWeight: 600,
675
+ color: colors.textMuted,
676
+ textTransform: 'uppercase' as const,
677
+ letterSpacing: '0.05em',
678
+ },
679
+ sectionLine: {
680
+ flex: 1,
681
+ height: '1px',
682
+ background: colors.border,
683
+ },
684
+ card: {
685
+ background: darkMode ? '#1c1c1e' : '#fafafa',
686
+ border: `1px solid ${darkMode ? '#48484a' : '#d1d1d6'}`,
687
+ borderRadius: '12px',
688
+ padding: '16px',
689
+ },
690
+ devBadge: {
691
+ display: 'inline-flex',
692
+ alignItems: 'center',
693
+ gap: '4px',
694
+ padding: '3px 8px',
695
+ borderRadius: '4px',
696
+ fontSize: '10px',
697
+ fontWeight: 600,
698
+ background: darkMode ? 'rgba(191, 90, 242, 0.15)' : 'rgba(175, 82, 222, 0.12)',
699
+ color: darkMode ? '#bf5af2' : '#af52de',
700
+ marginBottom: '12px',
701
+ },
702
+ pathBox: {
703
+ padding: '10px 12px',
704
+ background: darkMode ? '#2c2c2e' : '#ffffff',
705
+ border: `1px solid ${darkMode ? '#3a3a3c' : '#e5e5e5'}`,
706
+ borderRadius: '6px',
707
+ fontSize: '12px',
708
+ fontFamily: 'SF Mono, Monaco, Consolas, monospace',
709
+ color: colors.text,
710
+ marginBottom: '12px',
711
+ wordBreak: 'break-all' as const,
712
+ },
713
+ row: {
714
+ display: 'flex',
715
+ gap: '12px',
716
+ marginBottom: '12px',
717
+ },
718
+ metaItem: {
719
+ fontSize: '12px',
720
+ color: colors.textMuted,
721
+ },
722
+ metaLabel: {
723
+ fontWeight: 500,
724
+ marginRight: '4px',
725
+ },
726
+ metaValue: {
727
+ fontFamily: 'SF Mono, Monaco, Consolas, monospace',
728
+ fontSize: '11px',
729
+ },
730
+ filesList: {
731
+ display: 'flex',
732
+ flexWrap: 'wrap' as const,
733
+ gap: '6px',
734
+ marginBottom: '16px',
735
+ },
736
+ fileTag: {
737
+ padding: '4px 8px',
738
+ background: darkMode ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.04)',
739
+ borderRadius: '4px',
740
+ fontSize: '11px',
741
+ fontFamily: 'SF Mono, Monaco, Consolas, monospace',
742
+ color: colors.textMuted,
743
+ },
744
+ buttonRow: {
745
+ display: 'flex',
746
+ gap: '8px',
747
+ },
748
+ button: {
749
+ padding: '8px 14px',
750
+ fontSize: '12px',
751
+ fontWeight: 500,
752
+ color: colors.accent,
753
+ background: 'transparent',
754
+ border: `1px solid ${colors.border}`,
755
+ borderRadius: '6px',
756
+ cursor: 'pointer',
757
+ display: 'flex',
758
+ alignItems: 'center',
759
+ gap: '6px',
760
+ },
761
+ };
762
+
763
+ // Format bytes to human readable
764
+ const formatBytes = (bytes: number): string => {
765
+ if (bytes < 1024) return `${bytes} B`;
766
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
767
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
768
+ };
769
+
770
+ return (
771
+ <div style={styles.section}>
772
+ <div style={styles.sectionHeader}>
773
+ <span style={styles.sectionTitle}>Debug Bundle</span>
774
+ <div style={styles.sectionLine} />
775
+ </div>
776
+ <div style={styles.card}>
777
+ <div style={styles.devBadge}>
778
+ <span>🔧</span>
779
+ DEV MODE ONLY
780
+ </div>
781
+
782
+ <div style={{ fontSize: '11px', color: colors.textMuted, marginBottom: '8px' }}>
783
+ Path (Local)
784
+ </div>
785
+ <div style={styles.pathBox}>
786
+ {debugBundle.debug_bundle_ref}
787
+ </div>
788
+
789
+ <div style={styles.row}>
790
+ <div style={styles.metaItem}>
791
+ <span style={styles.metaLabel}>Size:</span>
792
+ <span style={styles.metaValue}>{formatBytes(debugBundle.raw_output_len)}</span>
793
+ </div>
794
+ <div style={styles.metaItem}>
795
+ <span style={styles.metaLabel}>SHA256:</span>
796
+ <span style={styles.metaValue}>{debugBundle.raw_output_sha256.slice(0, 16)}...</span>
797
+ </div>
798
+ </div>
799
+
800
+ <div style={{ fontSize: '11px', color: colors.textMuted, marginBottom: '6px' }}>
801
+ Files
802
+ </div>
803
+ <div style={styles.filesList}>
804
+ {debugBundle.files.map((file) => (
805
+ <span key={file} style={styles.fileTag}>{file}</span>
806
+ ))}
807
+ </div>
808
+
809
+ <div style={styles.buttonRow}>
810
+ <button onClick={onOpenFolder} style={styles.button}>
811
+ <FolderIcon />
812
+ Open Folder
813
+ </button>
814
+ <button onClick={onCopyPath} style={styles.button}>
815
+ {pathCopySuccess ? (
816
+ <>
817
+ <CheckIcon color={colors.green} size={14} />
818
+ Copied!
819
+ </>
820
+ ) : (
821
+ <>
822
+ <CopyIcon />
823
+ Copy Path
824
+ </>
825
+ )}
826
+ </button>
827
+ </div>
828
+
829
+ <div style={{ marginTop: '12px', fontSize: '11px', color: colors.textDim, fontStyle: 'italic' }}>
830
+ Raw output is NOT displayed here. Open the folder to inspect files.
831
+ </div>
832
+ </div>
833
+ </div>
834
+ );
835
+ }
836
+
837
+ // =============================================================================
838
+ // Icons
839
+ // =============================================================================
840
+
841
+ function FolderIcon(): ReactElement {
842
+ return (
843
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
844
+ <path d="M.54 3.87.5 14a1 1 0 0 0 1 1h13a1 1 0 0 0 1-1V4.5a1 1 0 0 0-1-1H7.414A2 2 0 0 1 6 2.5L5.5 2a2 2 0 0 0-1.414-.586H1.5a1 1 0 0 0-1 1l.04 1.456z"/>
845
+ </svg>
846
+ );
847
+ }
848
+
849
+ // Icons
850
+ function CheckIcon({ color, size = 16 }: { color: string; size?: number }): ReactElement {
851
+ return (
852
+ <svg width={size} height={size} viewBox="0 0 16 16" fill={color}>
853
+ <path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z" />
854
+ </svg>
855
+ );
856
+ }
857
+
858
+ function ErrorIcon({ color, size = 16 }: { color: string; size?: number }): ReactElement {
859
+ return (
860
+ <svg width={size} height={size} viewBox="0 0 16 16" fill={color}>
861
+ <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
862
+ <path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z" />
863
+ </svg>
864
+ );
865
+ }
866
+
867
+ function CopyIcon(): ReactElement {
868
+ return (
869
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
870
+ <path d="M4 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V2zm2-1a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H6z" />
871
+ <path d="M2 6a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6zm2-1a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H4z" />
872
+ </svg>
873
+ );
874
+ }