groove-dev 0.16.3 → 0.17.0

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 (38) hide show
  1. package/README.md +18 -16
  2. package/node_modules/@groove-dev/daemon/integrations-registry.json +321 -0
  3. package/node_modules/@groove-dev/daemon/src/api.js +152 -0
  4. package/node_modules/@groove-dev/daemon/src/index.js +13 -1
  5. package/node_modules/@groove-dev/daemon/src/integrations.js +389 -0
  6. package/node_modules/@groove-dev/daemon/src/introducer.js +23 -0
  7. package/node_modules/@groove-dev/daemon/src/process.js +59 -0
  8. package/node_modules/@groove-dev/daemon/src/registry.js +2 -1
  9. package/node_modules/@groove-dev/daemon/src/scheduler.js +336 -0
  10. package/node_modules/@groove-dev/daemon/src/terminal-pty.js +119 -54
  11. package/node_modules/@groove-dev/daemon/src/validate.js +10 -0
  12. package/node_modules/@groove-dev/gui/dist/assets/index-C5k-qSwi.js +153 -0
  13. package/node_modules/@groove-dev/gui/dist/index.html +1 -1
  14. package/node_modules/@groove-dev/gui/src/App.jsx +6 -0
  15. package/node_modules/@groove-dev/gui/src/components/SpawnPanel.jsx +98 -7
  16. package/node_modules/@groove-dev/gui/src/components/Terminal.jsx +29 -12
  17. package/node_modules/@groove-dev/gui/src/views/IntegrationsStore.jsx +954 -0
  18. package/node_modules/@groove-dev/gui/src/views/ScheduleManager.jsx +614 -0
  19. package/package.json +2 -2
  20. package/packages/daemon/integrations-registry.json +321 -0
  21. package/packages/daemon/src/api.js +152 -0
  22. package/packages/daemon/src/index.js +13 -1
  23. package/packages/daemon/src/integrations.js +389 -0
  24. package/packages/daemon/src/introducer.js +23 -0
  25. package/packages/daemon/src/process.js +59 -0
  26. package/packages/daemon/src/registry.js +2 -1
  27. package/packages/daemon/src/scheduler.js +336 -0
  28. package/packages/daemon/src/terminal-pty.js +119 -54
  29. package/packages/daemon/src/validate.js +10 -0
  30. package/packages/gui/dist/assets/index-C5k-qSwi.js +153 -0
  31. package/packages/gui/dist/index.html +1 -1
  32. package/packages/gui/src/App.jsx +6 -0
  33. package/packages/gui/src/components/SpawnPanel.jsx +98 -7
  34. package/packages/gui/src/components/Terminal.jsx +29 -12
  35. package/packages/gui/src/views/IntegrationsStore.jsx +954 -0
  36. package/packages/gui/src/views/ScheduleManager.jsx +614 -0
  37. package/node_modules/@groove-dev/gui/dist/assets/index-CFeltwTB.js +0 -153
  38. package/packages/gui/dist/assets/index-CFeltwTB.js +0 -153
@@ -0,0 +1,614 @@
1
+ // GROOVE GUI — Schedule Manager
2
+ // FSL-1.1-Apache-2.0 — see LICENSE
3
+
4
+ import React, { useState, useEffect, useCallback } from 'react';
5
+
6
+ const CRON_PRESETS = [
7
+ { label: 'Every 5 min', cron: '*/5 * * * *' },
8
+ { label: 'Every 15 min', cron: '*/15 * * * *' },
9
+ { label: 'Every hour', cron: '0 * * * *' },
10
+ { label: 'Every 6 hours', cron: '0 */6 * * *' },
11
+ { label: 'Daily 9 AM', cron: '0 9 * * *' },
12
+ { label: 'Weekdays 9 AM', cron: '0 9 * * 1-5' },
13
+ { label: 'Weekly (Mon)', cron: '0 0 * * 1' },
14
+ { label: 'Monthly', cron: '0 0 1 * *' },
15
+ ];
16
+
17
+ const ROLE_PRESETS = [
18
+ { id: 'backend', label: 'Backend' },
19
+ { id: 'frontend', label: 'Frontend' },
20
+ { id: 'fullstack', label: 'Fullstack' },
21
+ { id: 'testing', label: 'Testing' },
22
+ { id: 'devops', label: 'DevOps' },
23
+ { id: 'docs', label: 'Docs' },
24
+ { id: 'cmo', label: 'CMO' },
25
+ { id: 'cfo', label: 'CFO' },
26
+ { id: 'ea', label: 'EA' },
27
+ { id: 'support', label: 'Support' },
28
+ { id: 'analyst', label: 'Analyst' },
29
+ ];
30
+
31
+ function timeAgo(iso) {
32
+ if (!iso) return 'never';
33
+ const ms = Date.now() - new Date(iso).getTime();
34
+ if (ms < 60000) return 'just now';
35
+ if (ms < 3600000) return `${Math.floor(ms / 60000)}m ago`;
36
+ if (ms < 86400000) return `${Math.floor(ms / 3600000)}h ago`;
37
+ return `${Math.floor(ms / 86400000)}d ago`;
38
+ }
39
+
40
+ function StatusDot({ status }) {
41
+ const colors = {
42
+ spawned: 'var(--green)',
43
+ skipped: 'var(--amber)',
44
+ error: 'var(--red)',
45
+ };
46
+ return (
47
+ <span style={{
48
+ width: 6, height: 6, borderRadius: '50%',
49
+ background: colors[status] || 'var(--text-muted)',
50
+ display: 'inline-block', marginRight: 6,
51
+ }} />
52
+ );
53
+ }
54
+
55
+ // -- Create Schedule Form --
56
+ function CreateForm({ onSubmit, onCancel }) {
57
+ const [name, setName] = useState('');
58
+ const [cron, setCron] = useState('0 9 * * *');
59
+ const [role, setRole] = useState('fullstack');
60
+ const [prompt, setPrompt] = useState('');
61
+ const [submitting, setSubmitting] = useState(false);
62
+ const [error, setError] = useState('');
63
+
64
+ async function handleSubmit(e) {
65
+ e.preventDefault();
66
+ if (!name.trim()) { setError('Name is required'); return; }
67
+ if (!prompt.trim()) { setError('Task prompt is required'); return; }
68
+
69
+ setSubmitting(true);
70
+ setError('');
71
+ try {
72
+ await onSubmit({
73
+ name: name.trim(),
74
+ cron,
75
+ agentConfig: { role, prompt: prompt.trim() },
76
+ });
77
+ } catch (err) {
78
+ setError(err.message);
79
+ }
80
+ setSubmitting(false);
81
+ }
82
+
83
+ return (
84
+ <form onSubmit={handleSubmit} style={styles.form}>
85
+ <div style={styles.formTitle}>New Schedule</div>
86
+
87
+ <label style={styles.label}>NAME</label>
88
+ <input
89
+ value={name}
90
+ onChange={(e) => setName(e.target.value)}
91
+ placeholder="e.g., Daily standup summary"
92
+ style={styles.input}
93
+ />
94
+
95
+ <label style={styles.label}>FREQUENCY</label>
96
+ <div style={styles.presetRow}>
97
+ {CRON_PRESETS.map((p) => (
98
+ <button
99
+ key={p.cron}
100
+ type="button"
101
+ onClick={() => setCron(p.cron)}
102
+ style={{
103
+ ...styles.presetBtn,
104
+ borderColor: cron === p.cron ? 'var(--accent)' : 'var(--border)',
105
+ color: cron === p.cron ? 'var(--accent)' : 'var(--text-dim)',
106
+ background: cron === p.cron ? 'rgba(51, 175, 188, 0.08)' : 'transparent',
107
+ }}
108
+ >
109
+ {p.label}
110
+ </button>
111
+ ))}
112
+ </div>
113
+ <input
114
+ value={cron}
115
+ onChange={(e) => setCron(e.target.value)}
116
+ placeholder="* * * * * (min hr day mon wkday)"
117
+ style={{ ...styles.input, fontFamily: 'var(--font)', fontSize: 11 }}
118
+ />
119
+
120
+ <label style={styles.label}>AGENT ROLE</label>
121
+ <div style={styles.presetRow}>
122
+ {ROLE_PRESETS.map((r) => (
123
+ <button
124
+ key={r.id}
125
+ type="button"
126
+ onClick={() => setRole(r.id)}
127
+ style={{
128
+ ...styles.presetBtn,
129
+ borderColor: role === r.id ? 'var(--accent)' : 'var(--border)',
130
+ color: role === r.id ? 'var(--accent)' : 'var(--text-dim)',
131
+ background: role === r.id ? 'rgba(51, 175, 188, 0.08)' : 'transparent',
132
+ }}
133
+ >
134
+ {r.label}
135
+ </button>
136
+ ))}
137
+ </div>
138
+
139
+ <label style={styles.label}>TASK</label>
140
+ <textarea
141
+ value={prompt}
142
+ onChange={(e) => setPrompt(e.target.value)}
143
+ placeholder="What should this agent do each time it runs?"
144
+ rows={3}
145
+ style={styles.textarea}
146
+ />
147
+
148
+ {error && <div style={styles.error}>{error}</div>}
149
+
150
+ <div style={styles.formActions}>
151
+ <button type="button" onClick={onCancel} style={styles.cancelBtn}>Cancel</button>
152
+ <button type="submit" disabled={submitting} style={styles.submitBtn}>
153
+ {submitting ? 'Creating...' : 'Create Schedule'}
154
+ </button>
155
+ </div>
156
+ </form>
157
+ );
158
+ }
159
+
160
+ // -- Schedule Row --
161
+ function ScheduleRow({ schedule, onToggle, onRun, onDelete, onSelect }) {
162
+ const [confirming, setConfirming] = useState(false);
163
+
164
+ return (
165
+ <div
166
+ style={styles.scheduleRow}
167
+ onMouseEnter={(e) => { e.currentTarget.style.background = 'var(--bg-hover)'; }}
168
+ onMouseLeave={(e) => { e.currentTarget.style.background = 'var(--bg-surface)'; }}
169
+ >
170
+ <div style={styles.scheduleMain} onClick={() => onSelect(schedule)}>
171
+ {/* Status indicator */}
172
+ <span style={{
173
+ width: 8, height: 8, borderRadius: '50%',
174
+ background: schedule.enabled
175
+ ? schedule.isRunning ? 'var(--green)' : 'var(--accent)'
176
+ : 'var(--text-muted)',
177
+ flexShrink: 0,
178
+ animation: schedule.isRunning ? 'pulse 2s infinite' : 'none',
179
+ }} />
180
+
181
+ <div style={{ flex: 1, minWidth: 0 }}>
182
+ <div style={styles.scheduleName}>{schedule.name}</div>
183
+ <div style={styles.scheduleMeta}>
184
+ <span>{schedule.cronDescription || schedule.cron}</span>
185
+ <span style={{ color: 'var(--text-muted)' }}>
186
+ {schedule.agentConfig?.role}
187
+ </span>
188
+ {schedule.lastRun && (
189
+ <span style={{ color: 'var(--text-muted)' }}>
190
+ <StatusDot status={schedule.lastRun.status} />
191
+ {timeAgo(schedule.lastRun.timestamp)}
192
+ </span>
193
+ )}
194
+ </div>
195
+ </div>
196
+ </div>
197
+
198
+ <div style={styles.scheduleActions}>
199
+ {/* Toggle */}
200
+ <button
201
+ onClick={() => onToggle(schedule.id, !schedule.enabled)}
202
+ title={schedule.enabled ? 'Disable' : 'Enable'}
203
+ style={{
204
+ ...styles.actionBtn,
205
+ color: schedule.enabled ? 'var(--green)' : 'var(--text-muted)',
206
+ }}
207
+ >
208
+ {schedule.enabled ? 'ON' : 'OFF'}
209
+ </button>
210
+
211
+ {/* Run now */}
212
+ <button
213
+ onClick={() => onRun(schedule.id)}
214
+ title="Run now"
215
+ style={{ ...styles.actionBtn, color: 'var(--accent)' }}
216
+ >
217
+ {'\u25B6'}
218
+ </button>
219
+
220
+ {/* Delete */}
221
+ {confirming ? (
222
+ <button
223
+ onClick={() => { onDelete(schedule.id); setConfirming(false); }}
224
+ style={{ ...styles.actionBtn, color: 'var(--red)' }}
225
+ >
226
+ confirm
227
+ </button>
228
+ ) : (
229
+ <button
230
+ onClick={() => setConfirming(true)}
231
+ style={{ ...styles.actionBtn, color: 'var(--text-muted)' }}
232
+ >
233
+ {'\u2715'}
234
+ </button>
235
+ )}
236
+ </div>
237
+ </div>
238
+ );
239
+ }
240
+
241
+ // -- Schedule Detail --
242
+ function ScheduleDetail({ schedule, onClose }) {
243
+ if (!schedule) return null;
244
+
245
+ return (
246
+ <div style={styles.detailPanel}>
247
+ <div style={styles.detailHeader}>
248
+ <div style={styles.detailTitle}>{schedule.name}</div>
249
+ <button onClick={onClose} style={styles.detailClose}>&times;</button>
250
+ </div>
251
+
252
+ <div style={styles.detailGrid}>
253
+ <div style={styles.detailLabel}>Cron</div>
254
+ <div style={styles.detailValue}>{schedule.cron}</div>
255
+ <div style={styles.detailLabel}>Description</div>
256
+ <div style={styles.detailValue}>{schedule.cronDescription}</div>
257
+ <div style={styles.detailLabel}>Role</div>
258
+ <div style={styles.detailValue}>{schedule.agentConfig?.role}</div>
259
+ <div style={styles.detailLabel}>Status</div>
260
+ <div style={styles.detailValue}>
261
+ <span style={{ color: schedule.enabled ? 'var(--green)' : 'var(--text-muted)' }}>
262
+ {schedule.enabled ? 'Enabled' : 'Disabled'}
263
+ </span>
264
+ </div>
265
+ <div style={styles.detailLabel}>Created</div>
266
+ <div style={styles.detailValue}>{new Date(schedule.createdAt).toLocaleString()}</div>
267
+ </div>
268
+
269
+ {schedule.agentConfig?.prompt && (
270
+ <div style={{ marginTop: 16 }}>
271
+ <div style={styles.detailLabel}>Task Prompt</div>
272
+ <pre style={styles.promptPre}>{schedule.agentConfig.prompt}</pre>
273
+ </div>
274
+ )}
275
+
276
+ {(schedule.history || []).length > 0 && (
277
+ <div style={{ marginTop: 16 }}>
278
+ <div style={{ ...styles.detailLabel, marginBottom: 8 }}>Execution History</div>
279
+ <div style={styles.historyList}>
280
+ {schedule.history.slice(0, 20).map((entry, i) => (
281
+ <div key={i} style={styles.historyEntry}>
282
+ <StatusDot status={entry.status} />
283
+ <span style={{ fontSize: 10, color: 'var(--text-dim)', flex: 1 }}>
284
+ {entry.status}
285
+ {entry.agentId && <span style={{ color: 'var(--text-muted)' }}> ({entry.agentId})</span>}
286
+ </span>
287
+ <span style={{ fontSize: 9, color: 'var(--text-muted)' }}>
288
+ {timeAgo(entry.timestamp)}
289
+ </span>
290
+ </div>
291
+ ))}
292
+ </div>
293
+ </div>
294
+ )}
295
+ </div>
296
+ );
297
+ }
298
+
299
+ // -- Main --
300
+ export default function ScheduleManager() {
301
+ const [schedules, setSchedules] = useState([]);
302
+ const [loading, setLoading] = useState(true);
303
+ const [creating, setCreating] = useState(false);
304
+ const [selected, setSelected] = useState(null);
305
+ const [statusMsg, setStatusMsg] = useState('');
306
+
307
+ const fetchSchedules = useCallback(async () => {
308
+ try {
309
+ const res = await fetch('/api/schedules');
310
+ setSchedules(await res.json());
311
+ } catch { /* ignore */ }
312
+ setLoading(false);
313
+ }, []);
314
+
315
+ useEffect(() => {
316
+ fetchSchedules();
317
+ const interval = setInterval(fetchSchedules, 10000);
318
+ return () => clearInterval(interval);
319
+ }, [fetchSchedules]);
320
+
321
+ async function handleCreate(config) {
322
+ const res = await fetch('/api/schedules', {
323
+ method: 'POST',
324
+ headers: { 'Content-Type': 'application/json' },
325
+ body: JSON.stringify(config),
326
+ });
327
+ if (!res.ok) {
328
+ const data = await res.json();
329
+ throw new Error(data.error || 'Failed to create schedule');
330
+ }
331
+ setCreating(false);
332
+ await fetchSchedules();
333
+ flash('Schedule created');
334
+ }
335
+
336
+ async function handleToggle(id, enabled) {
337
+ await fetch(`/api/schedules/${id}/${enabled ? 'enable' : 'disable'}`, { method: 'POST' });
338
+ await fetchSchedules();
339
+ }
340
+
341
+ async function handleRun(id) {
342
+ try {
343
+ await fetch(`/api/schedules/${id}/run`, { method: 'POST' });
344
+ flash('Agent spawned');
345
+ await fetchSchedules();
346
+ } catch { flash('Failed to run'); }
347
+ }
348
+
349
+ async function handleDelete(id) {
350
+ await fetch(`/api/schedules/${id}`, { method: 'DELETE' });
351
+ if (selected?.id === id) setSelected(null);
352
+ await fetchSchedules();
353
+ flash('Schedule deleted');
354
+ }
355
+
356
+ async function handleSelect(schedule) {
357
+ try {
358
+ const res = await fetch(`/api/schedules/${schedule.id}`);
359
+ setSelected(await res.json());
360
+ } catch { setSelected(schedule); }
361
+ }
362
+
363
+ function flash(msg) {
364
+ setStatusMsg(msg);
365
+ setTimeout(() => setStatusMsg(''), 3000);
366
+ }
367
+
368
+ return (
369
+ <div style={styles.root}>
370
+ {/* Header */}
371
+ <div style={styles.headerBar}>
372
+ <div style={styles.headerLeft}>
373
+ <div style={styles.title}>Schedules</div>
374
+ <span style={styles.headerCount}>
375
+ {schedules.length} schedule{schedules.length !== 1 ? 's' : ''}
376
+ {' \u2022 '}
377
+ {schedules.filter((s) => s.enabled).length} active
378
+ </span>
379
+ </div>
380
+ <button
381
+ onClick={() => { setCreating(true); setSelected(null); }}
382
+ style={styles.createBtn}
383
+ >
384
+ + New Schedule
385
+ </button>
386
+ </div>
387
+
388
+ {statusMsg && (
389
+ <div style={styles.statusMsg}>{statusMsg}</div>
390
+ )}
391
+
392
+ <div style={styles.mainRow}>
393
+ {/* Schedule list */}
394
+ <div style={styles.listArea}>
395
+ {creating && (
396
+ <CreateForm
397
+ onSubmit={handleCreate}
398
+ onCancel={() => setCreating(false)}
399
+ />
400
+ )}
401
+
402
+ {loading && schedules.length === 0 && (
403
+ <div style={styles.empty}>Loading schedules...</div>
404
+ )}
405
+
406
+ {!loading && schedules.length === 0 && !creating && (
407
+ <div style={styles.empty}>
408
+ <div style={{ fontSize: 13, marginBottom: 8 }}>No schedules yet</div>
409
+ <div style={{ fontSize: 11, color: 'var(--text-muted)' }}>
410
+ Create a schedule to run agents on a recurring basis — daily standups, weekly reports, monitoring checks.
411
+ </div>
412
+ </div>
413
+ )}
414
+
415
+ {schedules.map((schedule) => (
416
+ <ScheduleRow
417
+ key={schedule.id}
418
+ schedule={schedule}
419
+ onToggle={handleToggle}
420
+ onRun={handleRun}
421
+ onDelete={handleDelete}
422
+ onSelect={handleSelect}
423
+ />
424
+ ))}
425
+ </div>
426
+
427
+ {/* Detail panel */}
428
+ {selected && (
429
+ <ScheduleDetail
430
+ schedule={selected}
431
+ onClose={() => setSelected(null)}
432
+ />
433
+ )}
434
+ </div>
435
+ </div>
436
+ );
437
+ }
438
+
439
+ // -- Styles --
440
+ const styles = {
441
+ root: {
442
+ height: '100%', display: 'flex', flexDirection: 'column',
443
+ overflow: 'hidden',
444
+ },
445
+ headerBar: {
446
+ padding: '12px 20px',
447
+ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
448
+ flexShrink: 0,
449
+ },
450
+ headerLeft: {
451
+ display: 'flex', alignItems: 'center', gap: 12,
452
+ },
453
+ title: {
454
+ fontSize: 15, fontWeight: 700, color: 'var(--text-bright)',
455
+ letterSpacing: 0.3,
456
+ },
457
+ headerCount: {
458
+ fontSize: 11, color: 'var(--text-muted)',
459
+ },
460
+ createBtn: {
461
+ padding: '6px 14px',
462
+ background: 'var(--accent)', color: 'var(--bg-base)',
463
+ border: 'none', borderRadius: 6,
464
+ fontSize: 11, fontWeight: 600, cursor: 'pointer',
465
+ fontFamily: 'var(--font)',
466
+ },
467
+ statusMsg: {
468
+ padding: '4px 20px', fontSize: 10, color: 'var(--green)',
469
+ flexShrink: 0,
470
+ },
471
+ mainRow: {
472
+ flex: 1, display: 'flex', overflow: 'hidden',
473
+ },
474
+ listArea: {
475
+ flex: 1, overflowY: 'auto', padding: '0 20px 20px',
476
+ },
477
+ empty: {
478
+ padding: '60px 20px', textAlign: 'center',
479
+ color: 'var(--text-dim)', fontSize: 12,
480
+ },
481
+
482
+ // Schedule row
483
+ scheduleRow: {
484
+ display: 'flex', alignItems: 'center',
485
+ padding: '10px 12px',
486
+ background: 'var(--bg-surface)', border: '1px solid var(--border)',
487
+ borderRadius: 6, marginBottom: 4, cursor: 'pointer',
488
+ transition: 'background 0.1s',
489
+ },
490
+ scheduleMain: {
491
+ display: 'flex', alignItems: 'center', gap: 10, flex: 1, minWidth: 0,
492
+ },
493
+ scheduleName: {
494
+ fontSize: 12, fontWeight: 600, color: 'var(--text-bright)',
495
+ },
496
+ scheduleMeta: {
497
+ display: 'flex', gap: 10, fontSize: 10, color: 'var(--text-dim)',
498
+ marginTop: 2,
499
+ },
500
+ scheduleActions: {
501
+ display: 'flex', gap: 4, flexShrink: 0,
502
+ },
503
+ actionBtn: {
504
+ padding: '3px 8px',
505
+ background: 'transparent', border: '1px solid var(--border)',
506
+ borderRadius: 4, fontSize: 9, fontWeight: 600,
507
+ fontFamily: 'var(--font)', cursor: 'pointer',
508
+ },
509
+
510
+ // Create form
511
+ form: {
512
+ padding: '16px',
513
+ background: 'var(--bg-surface)', border: '1px solid var(--border)',
514
+ borderRadius: 8, marginBottom: 12,
515
+ },
516
+ formTitle: {
517
+ fontSize: 13, fontWeight: 700, color: 'var(--text-bright)',
518
+ marginBottom: 12,
519
+ },
520
+ label: {
521
+ display: 'block', fontSize: 9, fontWeight: 700,
522
+ color: 'var(--text-muted)', letterSpacing: 0.5,
523
+ textTransform: 'uppercase', marginTop: 10, marginBottom: 4,
524
+ },
525
+ input: {
526
+ width: '100%', padding: '7px 10px',
527
+ background: 'var(--bg-base)', border: '1px solid var(--border)',
528
+ borderRadius: 5, color: 'var(--text-primary)', fontSize: 12,
529
+ fontFamily: 'var(--font)', outline: 'none',
530
+ boxSizing: 'border-box',
531
+ },
532
+ textarea: {
533
+ width: '100%', padding: '7px 10px',
534
+ background: 'var(--bg-base)', border: '1px solid var(--border)',
535
+ borderRadius: 5, color: 'var(--text-primary)', fontSize: 12,
536
+ fontFamily: 'var(--font)', outline: 'none', resize: 'vertical',
537
+ boxSizing: 'border-box',
538
+ },
539
+ presetRow: {
540
+ display: 'flex', gap: 4, flexWrap: 'wrap', marginBottom: 6,
541
+ },
542
+ presetBtn: {
543
+ padding: '3px 10px',
544
+ background: 'transparent', border: '1px solid var(--border)',
545
+ borderRadius: 10, fontSize: 10, fontWeight: 500,
546
+ fontFamily: 'var(--font)', cursor: 'pointer',
547
+ transition: 'all 0.1s',
548
+ },
549
+ error: {
550
+ fontSize: 11, color: 'var(--red)', marginTop: 8,
551
+ },
552
+ formActions: {
553
+ display: 'flex', gap: 8, justifyContent: 'flex-end', marginTop: 14,
554
+ },
555
+ cancelBtn: {
556
+ padding: '6px 14px',
557
+ background: 'transparent', color: 'var(--text-dim)',
558
+ border: '1px solid var(--border)', borderRadius: 5,
559
+ fontSize: 11, fontWeight: 500, cursor: 'pointer',
560
+ fontFamily: 'var(--font)',
561
+ },
562
+ submitBtn: {
563
+ padding: '6px 14px',
564
+ background: 'var(--accent)', color: 'var(--bg-base)',
565
+ border: 'none', borderRadius: 5,
566
+ fontSize: 11, fontWeight: 600, cursor: 'pointer',
567
+ fontFamily: 'var(--font)',
568
+ },
569
+
570
+ // Detail panel
571
+ detailPanel: {
572
+ width: 320, flexShrink: 0,
573
+ borderLeft: '1px solid var(--border)',
574
+ padding: '16px', overflowY: 'auto',
575
+ },
576
+ detailHeader: {
577
+ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
578
+ marginBottom: 16,
579
+ },
580
+ detailTitle: {
581
+ fontSize: 14, fontWeight: 700, color: 'var(--text-bright)',
582
+ },
583
+ detailClose: {
584
+ background: 'none', border: 'none',
585
+ color: 'var(--text-muted)', fontSize: 18,
586
+ cursor: 'pointer', fontFamily: 'var(--font)',
587
+ },
588
+ detailGrid: {
589
+ display: 'grid', gridTemplateColumns: '80px 1fr', gap: '6px 10px',
590
+ },
591
+ detailLabel: {
592
+ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)',
593
+ textTransform: 'uppercase',
594
+ },
595
+ detailValue: {
596
+ fontSize: 11, color: 'var(--text-primary)',
597
+ },
598
+ promptPre: {
599
+ fontSize: 11, color: 'var(--text-primary)', lineHeight: 1.5,
600
+ padding: '8px 10px', background: 'var(--bg-base)',
601
+ border: '1px solid var(--border)', borderRadius: 4,
602
+ whiteSpace: 'pre-wrap', wordBreak: 'break-word',
603
+ fontFamily: 'var(--font)',
604
+ maxHeight: 200, overflowY: 'auto', marginTop: 4,
605
+ },
606
+ historyList: {
607
+ display: 'flex', flexDirection: 'column', gap: 4,
608
+ },
609
+ historyEntry: {
610
+ display: 'flex', alignItems: 'center', gap: 4,
611
+ padding: '4px 8px', borderRadius: 4,
612
+ background: 'var(--bg-base)',
613
+ },
614
+ };