@bamptee/aia-code 2.0.12 → 2.0.14

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.
@@ -99,6 +99,189 @@ const STATUS_CLASSES = {
99
99
 
100
100
  const STATUS_ICONS = { done: '\u2713', pending: '\u00b7', 'in-progress': '\u25b6', error: '\u2717' };
101
101
 
102
+ const FEATURE_TYPES = ['feature', 'bug'];
103
+ const DEFAULT_FEATURE_TYPE = 'feature';
104
+
105
+ /**
106
+ * Confirmation dialog for delete actions
107
+ */
108
+ function ConfirmDialog({ title, message, confirmText, confirmClass, onConfirm, onCancel }) {
109
+ React.useEffect(() => {
110
+ const handleKeyDown = (e) => {
111
+ if (e.key === 'Escape') onCancel();
112
+ };
113
+ document.addEventListener('keydown', handleKeyDown);
114
+ return () => document.removeEventListener('keydown', handleKeyDown);
115
+ }, [onCancel]);
116
+
117
+ return React.createElement('div', {
118
+ className: 'fixed inset-0 bg-black/50 flex items-center justify-center z-50',
119
+ onClick: (e) => e.target === e.currentTarget && onCancel(),
120
+ },
121
+ React.createElement('div', {
122
+ className: 'bg-aia-card border border-aia-border rounded-lg p-6 w-full max-w-md space-y-4',
123
+ },
124
+ React.createElement('h3', { className: 'text-lg font-semibold text-slate-100' }, title),
125
+ React.createElement('p', { className: 'text-sm text-slate-400' }, message),
126
+ React.createElement('div', { className: 'flex justify-end gap-3 pt-2' },
127
+ React.createElement('button', {
128
+ onClick: onCancel,
129
+ className: 'text-slate-400 hover:text-slate-200 text-sm px-4 py-2',
130
+ }, 'Cancel'),
131
+ React.createElement('button', {
132
+ onClick: onConfirm,
133
+ className: confirmClass || 'bg-red-500/20 text-red-400 border border-red-500/30 rounded px-4 py-2 text-sm hover:bg-red-500/30',
134
+ }, confirmText || 'Confirm'),
135
+ ),
136
+ )
137
+ );
138
+ }
139
+
140
+ function TypeBadgeEditable({ name, currentType, onChanged }) {
141
+ const [showPopover, setShowPopover] = React.useState(false);
142
+ const [saving, setSaving] = React.useState(false);
143
+ const type = currentType || DEFAULT_FEATURE_TYPE;
144
+ const isFeature = type !== 'bug';
145
+
146
+ const handleChange = async (newType) => {
147
+ if (newType === type) {
148
+ setShowPopover(false);
149
+ return;
150
+ }
151
+
152
+ // Confirm if changing to bug (will switch to quick flow)
153
+ if (newType === 'bug') {
154
+ const confirmed = window.confirm('Changing to Bug will set the flow to Quick. Continue?');
155
+ if (!confirmed) {
156
+ setShowPopover(false);
157
+ return;
158
+ }
159
+ }
160
+
161
+ setSaving(true);
162
+ try {
163
+ await api.patch(`/features/${name}/type`, { type: newType });
164
+ if (onChanged) onChanged(newType);
165
+ } catch (e) {
166
+ console.error('Failed to update type:', e);
167
+ }
168
+ setSaving(false);
169
+ setShowPopover(false);
170
+ };
171
+
172
+ return React.createElement('div', { className: 'relative' },
173
+ React.createElement('button', {
174
+ onClick: () => setShowPopover(!showPopover),
175
+ disabled: saving,
176
+ className: `text-xs px-2 py-1 rounded border transition-colors ${
177
+ isFeature ? 'type-feature' : 'type-bug'
178
+ } hover:brightness-110`,
179
+ title: 'Click to change type',
180
+ }, saving ? '...' : (isFeature ? '\u2728 Feature' : '\uD83D\uDC1B Bug')),
181
+
182
+ showPopover && React.createElement('div', {
183
+ className: 'absolute top-full left-0 mt-1 bg-aia-card border border-aia-border rounded shadow-lg z-10',
184
+ },
185
+ ...FEATURE_TYPES.map(t =>
186
+ React.createElement('button', {
187
+ key: t,
188
+ onClick: () => handleChange(t),
189
+ className: `block w-full px-3 py-1.5 text-xs text-left hover:bg-slate-700 ${
190
+ t === type ? 'text-aia-accent' : 'text-slate-300'
191
+ }`,
192
+ }, t === 'feature' ? '\u2728 Feature' : '\uD83D\uDC1B Bug')
193
+ )
194
+ ),
195
+ );
196
+ }
197
+
198
+ function ScopeEditor({ name, currentApps, availableApps, onChanged }) {
199
+ const [showDropdown, setShowDropdown] = React.useState(false);
200
+ const [saving, setSaving] = React.useState(false);
201
+ const selectedApps = currentApps || [];
202
+
203
+ const enabledApps = availableApps.filter(a => a.enabled !== false);
204
+ const availableToAdd = enabledApps.filter(a => !selectedApps.includes(a.name));
205
+
206
+ const handleRemove = async (appName) => {
207
+ setSaving(true);
208
+ const newApps = selectedApps.filter(n => n !== appName);
209
+ try {
210
+ await api.patch(`/features/${name}/apps`, { apps: newApps });
211
+ if (onChanged) onChanged(newApps);
212
+ } catch (e) {
213
+ console.error('Failed to update apps:', e);
214
+ }
215
+ setSaving(false);
216
+ };
217
+
218
+ const handleAdd = async (appName) => {
219
+ setSaving(true);
220
+ const newApps = [...selectedApps, appName];
221
+ try {
222
+ await api.patch(`/features/${name}/apps`, { apps: newApps });
223
+ if (onChanged) onChanged(newApps);
224
+ } catch (e) {
225
+ console.error('Failed to update apps:', e);
226
+ }
227
+ setSaving(false);
228
+ setShowDropdown(false);
229
+ };
230
+
231
+ // Map app names to full objects for icons
232
+ const selectedAppObjects = selectedApps.map(appName => {
233
+ const found = availableApps.find(a => a.name === appName);
234
+ return found || { name: appName, icon: '\uD83D\uDCC1' };
235
+ });
236
+
237
+ if (enabledApps.length === 0) return null;
238
+
239
+ return React.createElement('div', { className: 'flex items-center gap-2 flex-wrap' },
240
+ React.createElement('span', { className: 'text-xs text-slate-500' }, 'Scope:'),
241
+
242
+ // Selected apps as chips with remove button
243
+ ...selectedAppObjects.map(app =>
244
+ React.createElement('span', {
245
+ key: app.name,
246
+ className: 'app-chip-selected flex items-center gap-1',
247
+ },
248
+ `${app.icon || '\uD83D\uDCC1'} ${app.name}`,
249
+ React.createElement('button', {
250
+ onClick: () => handleRemove(app.name),
251
+ disabled: saving,
252
+ className: 'text-slate-400 hover:text-red-400 ml-0.5',
253
+ title: 'Remove',
254
+ }, '\u00D7')
255
+ )
256
+ ),
257
+
258
+ // Add button with dropdown
259
+ availableToAdd.length > 0 && React.createElement('div', { className: 'relative' },
260
+ React.createElement('button', {
261
+ onClick: () => setShowDropdown(!showDropdown),
262
+ disabled: saving,
263
+ className: 'app-chip text-slate-400 hover:text-slate-200',
264
+ }, saving ? '...' : '+'),
265
+
266
+ showDropdown && React.createElement('div', {
267
+ className: 'absolute top-full left-0 mt-1 bg-aia-card border border-aia-border rounded shadow-lg z-10 min-w-32',
268
+ },
269
+ ...availableToAdd.map(app =>
270
+ React.createElement('button', {
271
+ key: app.name,
272
+ onClick: () => handleAdd(app.name),
273
+ className: 'block w-full px-3 py-1.5 text-xs text-left text-slate-300 hover:bg-slate-700',
274
+ }, `${app.icon || '\uD83D\uDCC1'} ${app.name}`)
275
+ )
276
+ ),
277
+ ),
278
+
279
+ selectedApps.length === 0 && React.createElement('span', {
280
+ className: 'text-xs text-slate-600 italic',
281
+ }, 'No scope defined'),
282
+ );
283
+ }
284
+
102
285
  function StepPill({ step, status, active, onClick }) {
103
286
  return React.createElement('button', {
104
287
  onClick,
@@ -201,46 +384,61 @@ function LogViewer({ logs }) {
201
384
  }, logs.join(''));
202
385
  }
203
386
 
204
- function VerbosePanel({ logs, expanded, onToggle }) {
205
- if (!logs.length) return null;
206
- return React.createElement('div', { className: 'mt-2' },
207
- React.createElement('button', {
208
- onClick: onToggle,
209
- 'aria-expanded': expanded, // F13: Accessibility
210
- 'aria-label': `Toggle verbose logs, ${logs.length} lines`,
211
- className: 'text-xs text-slate-500 hover:text-slate-300 flex items-center gap-1',
212
- }, expanded ? '\u25BC' : '\u25B6', `Verbose (${logs.length} lines)`),
213
- expanded && React.createElement('pre', {
214
- className: 'bg-black/70 border border-slate-700 rounded p-2 mt-1 text-xs text-slate-500 overflow-auto max-h-48 whitespace-pre-wrap',
215
- role: 'log',
216
- 'aria-label': 'Verbose output',
217
- }, logs.join(''))
218
- );
219
- }
220
-
221
- function ChatLog({ messages }) {
387
+ function StreamingOutputPanel({ output }) {
222
388
  const ref = React.useRef(null);
389
+ const [elapsed, setElapsed] = React.useState(0);
390
+ const startTime = React.useRef(Date.now());
391
+
223
392
  React.useEffect(() => {
224
393
  if (ref.current) ref.current.scrollTop = ref.current.scrollHeight;
225
- }, [messages]);
394
+ }, [output]);
226
395
 
227
- if (!messages.length) return null;
396
+ // Timer to show elapsed time
397
+ React.useEffect(() => {
398
+ startTime.current = Date.now();
399
+ setElapsed(0);
400
+ const interval = setInterval(() => {
401
+ setElapsed(Math.floor((Date.now() - startTime.current) / 1000));
402
+ }, 1000);
403
+ return () => clearInterval(interval);
404
+ }, []);
405
+
406
+ const formatTime = (seconds) => {
407
+ if (seconds < 60) return `${seconds}s`;
408
+ const mins = Math.floor(seconds / 60);
409
+ const secs = seconds % 60;
410
+ return `${mins}m ${secs}s`;
411
+ };
228
412
 
229
413
  return React.createElement('div', {
230
- ref,
231
- className: 'bg-slate-900 border border-aia-border rounded p-3 font-mono text-sm overflow-auto max-h-80',
232
- role: 'log', // F13: Accessibility
233
- 'aria-label': 'Chat history',
234
- }, messages.map((msg) =>
235
- React.createElement('div', { key: msg.id || msg.content.slice(0, 20), className: 'mb-2' }, // F11: Unique key
236
- React.createElement('span', {
237
- className: msg.role === 'user' ? 'text-cyan-400' : 'text-emerald-400'
238
- }, msg.role === 'user' ? '> you: ' : 'agent: '),
239
- React.createElement('span', { className: 'text-slate-300 whitespace-pre-wrap' }, msg.content)
240
- )
241
- ));
414
+ className: 'bg-slate-900 border border-blue-500/30 rounded p-3',
415
+ },
416
+ React.createElement('div', { className: 'flex items-center justify-between mb-2' },
417
+ React.createElement('div', { className: 'flex items-center gap-2' },
418
+ React.createElement('div', { className: 'animate-spin w-3 h-3 border-2 border-blue-400 border-t-transparent rounded-full' }),
419
+ React.createElement('span', { className: 'text-xs text-blue-400 font-medium' }, 'Output'),
420
+ ),
421
+ React.createElement('span', { className: 'text-xs text-slate-500 font-mono' }, formatTime(elapsed)),
422
+ ),
423
+ output
424
+ ? React.createElement('pre', {
425
+ ref,
426
+ className: 'text-xs text-slate-300 whitespace-pre-wrap overflow-auto max-h-80 font-mono bg-black/30 p-2 rounded',
427
+ }, output.slice(-5000))
428
+ : React.createElement('div', { className: 'flex flex-col items-center gap-3 py-6' },
429
+ React.createElement('div', { className: 'flex gap-1' },
430
+ React.createElement('span', { className: 'w-2 h-2 bg-blue-400 rounded-full animate-bounce', style: { animationDelay: '0ms' } }),
431
+ React.createElement('span', { className: 'w-2 h-2 bg-blue-400 rounded-full animate-bounce', style: { animationDelay: '150ms' } }),
432
+ React.createElement('span', { className: 'w-2 h-2 bg-blue-400 rounded-full animate-bounce', style: { animationDelay: '300ms' } }),
433
+ ),
434
+ React.createElement('span', { className: 'text-sm text-blue-400' }, 'Agent is working...'),
435
+ React.createElement('span', { className: 'text-xs text-slate-600' }, 'Claude CLI buffers output until complete'),
436
+ ),
437
+ );
242
438
  }
243
439
 
440
+
441
+
244
442
  function ModelSelect({ model, onChange, disabled }) {
245
443
  const [models, setModels] = React.useState([]);
246
444
 
@@ -259,29 +457,54 @@ function ModelSelect({ model, onChange, disabled }) {
259
457
  );
260
458
  }
261
459
 
262
- function InitPanel({ name, onFlowSelected, onCancel, onEnriched }) {
460
+ function InitPanel({ name, featureType, onFlowSelected, onCancel, onEnriched }) {
263
461
  const [description, setDescription] = React.useState('');
264
462
  const [suggestion, setSuggestion] = React.useState(null);
265
463
  const [loading, setLoading] = React.useState(false);
266
464
  const [statusMsg, setStatusMsg] = React.useState('');
267
465
  const [logs, setLogs] = React.useState([]);
268
466
  const [err, setErr] = React.useState(null);
467
+ const [attachments, setAttachments] = React.useState([]);
468
+
469
+ const isBug = featureType === 'bug';
470
+
471
+ const handleAttachmentUpload = (files) => {
472
+ setAttachments(prev => [...prev, ...files]);
473
+ };
474
+
475
+ const handleAttachmentRemove = (filename) => {
476
+ setAttachments(prev => prev.filter(a => a.filename !== filename));
477
+ };
269
478
 
270
479
  const handleSubmit = async () => {
271
480
  setLoading(true);
272
481
  setErr(null);
273
482
  setLogs([]);
274
- setStatusMsg('Structuring your description...');
483
+ setStatusMsg(isBug ? 'Structuring your bug report...' : 'Structuring your description...');
275
484
 
276
- const res = await streamPost(`/features/${name}/init`, { description }, {
485
+ // Note: Attachments are already uploaded via AttachmentZone to /api/features/:name/attachments
486
+ // Passing filenames here for potential future use in AI enrichment
487
+ const res = await streamPost(`/features/${name}/init`, {
488
+ description,
489
+ attachments: attachments.map(a => ({ filename: a.filename, path: a.path })),
490
+ }, {
277
491
  onLog: (text) => setLogs(prev => [...prev, text]),
278
492
  onStatus: (data) => setStatusMsg(data.message || data.status),
279
493
  });
280
494
 
281
495
  if (res.ok) {
282
- setSuggestion(res.suggestion);
283
- setStatusMsg('');
284
- if (onEnriched) onEnriched();
496
+ // For bugs, skip flow selection and go directly to quick flow
497
+ if (isBug) {
498
+ setStatusMsg('');
499
+ setAttachments([]);
500
+ if (onEnriched) onEnriched();
501
+ onFlowSelected('quick');
502
+ } else {
503
+ setSuggestion(res.suggestion);
504
+ setStatusMsg('');
505
+ setAttachments([]);
506
+ if (onEnriched) onEnriched();
507
+ }
285
508
  } else {
286
509
  setErr(res.error || 'Failed to enrich description');
287
510
  }
@@ -292,16 +515,28 @@ function InitPanel({ name, onFlowSelected, onCancel, onEnriched }) {
292
515
  onFlowSelected(flow);
293
516
  };
294
517
 
295
- return React.createElement('div', { className: 'bg-aia-card border border-aia-border rounded p-4 space-y-4' },
296
- React.createElement('h3', { className: 'text-sm font-semibold text-cyan-400' }, 'Describe your feature'),
518
+ return React.createElement('div', { className: `bg-aia-card border rounded p-4 space-y-4 ${isBug ? 'border-red-500/30' : 'border-aia-border'}` },
519
+ React.createElement('h3', { className: `text-sm font-semibold ${isBug ? 'text-red-400' : 'text-cyan-400'}` },
520
+ isBug ? '\uD83D\uDC1B Describe the bug & fix' : 'Describe your feature'
521
+ ),
522
+
523
+ // Attachment zone (hidden when loading or has suggestion)
524
+ !loading && !suggestion && React.createElement(AttachmentZone, {
525
+ feature: name,
526
+ attachments,
527
+ onUpload: handleAttachmentUpload,
528
+ onRemove: handleAttachmentRemove,
529
+ }),
297
530
 
298
531
  // Textarea (hidden when loading or has suggestion)
299
532
  !loading && !suggestion && React.createElement('textarea', {
300
533
  value: description,
301
534
  onChange: e => setDescription(e.target.value),
302
- placeholder: 'Describe what you want to build...\n\nBe as detailed as needed. The AI will structure your description.',
535
+ placeholder: isBug
536
+ ? 'Describe the bug and how to fix it...\n\n- What is the current behavior?\n- What is the expected behavior?\n- How should it be fixed?'
537
+ : 'Describe what you want to build...\n\nBe as detailed as needed. The AI will structure your description.',
303
538
  rows: 8,
304
- className: 'w-full bg-aia-card border border-aia-border rounded px-3 py-2 text-sm text-slate-200 placeholder-slate-500 focus:border-aia-accent focus:outline-none resize-y max-h-96 overflow-auto',
539
+ className: `w-full bg-aia-card border border-aia-border rounded px-3 py-2 text-sm text-slate-200 placeholder-slate-500 focus:outline-none resize-y max-h-96 overflow-auto ${isBug ? 'focus:border-red-400' : 'focus:border-aia-accent'}`,
305
540
  }),
306
541
 
307
542
  // Character count and buttons
@@ -315,16 +550,18 @@ function InitPanel({ name, onFlowSelected, onCancel, onEnriched }) {
315
550
  React.createElement('button', {
316
551
  onClick: handleSubmit,
317
552
  disabled: !description.trim(),
318
- className: 'bg-aia-accent/20 text-aia-accent border border-aia-accent/30 rounded px-4 py-2 text-sm hover:bg-aia-accent/30 disabled:opacity-40',
319
- }, 'Continue'),
553
+ className: isBug
554
+ ? 'bg-red-500/20 text-red-400 border border-red-500/30 rounded px-4 py-2 text-sm hover:bg-red-500/30 disabled:opacity-40'
555
+ : 'bg-aia-accent/20 text-aia-accent border border-aia-accent/30 rounded px-4 py-2 text-sm hover:bg-aia-accent/30 disabled:opacity-40',
556
+ }, isBug ? 'Start Fix' : 'Continue'),
320
557
  ),
321
558
  ),
322
559
 
323
560
  // Loading state with logs
324
561
  loading && React.createElement('div', { className: 'space-y-3' },
325
562
  React.createElement('div', { className: 'flex items-center gap-2' },
326
- React.createElement('div', { className: 'animate-spin w-4 h-4 border-2 border-cyan-400 border-t-transparent rounded-full' }),
327
- React.createElement('span', { className: 'text-sm text-cyan-400' }, statusMsg || 'Processing...'),
563
+ React.createElement('div', { className: `animate-spin w-4 h-4 border-2 ${isBug ? 'border-red-400' : 'border-cyan-400'} border-t-transparent rounded-full` }),
564
+ React.createElement('span', { className: `text-sm ${isBug ? 'text-red-400' : 'text-cyan-400'}` }, statusMsg || 'Processing...'),
328
565
  ),
329
566
  logs.length > 0 && React.createElement(LogViewer, { logs }),
330
567
  ),
@@ -332,8 +569,8 @@ function InitPanel({ name, onFlowSelected, onCancel, onEnriched }) {
332
569
  // Error
333
570
  err && React.createElement('p', { className: 'text-red-400 text-xs' }, err),
334
571
 
335
- // Flow suggestion
336
- suggestion && React.createElement('div', { className: 'space-y-3' },
572
+ // Flow suggestion (only for features, bugs go directly to quick flow)
573
+ !isBug && suggestion && React.createElement('div', { className: 'space-y-3' },
337
574
  React.createElement('div', { className: 'flex items-center gap-2' },
338
575
  React.createElement('span', { className: 'text-emerald-400' }, '\u2713'),
339
576
  React.createElement('span', { className: 'text-sm text-slate-300' }, 'Feature spec created in init.md'),
@@ -380,9 +617,6 @@ function StepGuidance({ step, feature }) {
380
617
  );
381
618
  }
382
619
 
383
- const MAX_MESSAGES = 100;
384
- const MAX_VERBOSE_LOGS = 500;
385
-
386
620
  function RunPanel({ name, step, stepStatus, onDone }) {
387
621
  const isDone = stepStatus === 'done';
388
622
  const [inputText, setInputText] = React.useState('');
@@ -390,16 +624,85 @@ function RunPanel({ name, step, stepStatus, onDone }) {
390
624
  const [model, setModel] = React.useState('');
391
625
  const [apply, setApply] = React.useState(false);
392
626
  const [running, setRunning] = React.useState(false);
627
+ const [serverRunning, setServerRunning] = React.useState(false);
628
+ const [reconnecting, setReconnecting] = React.useState(true);
393
629
  const [result, setResult] = React.useState(null);
394
630
  const [err, setErr] = React.useState(null);
395
- const [messages, setMessages] = React.useState([]);
396
- const [verboseLogs, setVerboseLogs] = React.useState([]);
397
- const [verboseExpanded, setVerboseExpanded] = React.useState(false);
398
- const [history, setHistory] = React.useState([]);
399
631
  const [attachments, setAttachments] = React.useState([]);
400
632
  const agentBuffer = React.useRef('');
633
+ const [streamingOutput, setStreamingOutput] = React.useState(''); // Live agent output display
401
634
  const requestId = React.useRef(0); // F4: Track request to prevent race conditions
402
635
  const [resetting, setResetting] = React.useState(false); // F14: Track reset state
636
+ const eventSourceRef = React.useRef(null);
637
+
638
+ // Check agent status at mount and reconnect to SSE if running
639
+ React.useEffect(() => {
640
+ let cancelled = false;
641
+ setReconnecting(true);
642
+
643
+ api.get(`/features/${name}/agent-status`)
644
+ .then(data => {
645
+ if (cancelled) return;
646
+ if (data.running) {
647
+ setServerRunning(true);
648
+ setRunning(true);
649
+ // Connect to SSE stream for live updates (stream will replay buffered logs)
650
+ const evtSource = new EventSource(`/api/features/${name}/agent-stream`);
651
+ eventSourceRef.current = evtSource;
652
+
653
+ // Timeout: close if no response after 30s
654
+ const timeoutId = setTimeout(() => {
655
+ if (eventSourceRef.current === evtSource) {
656
+ evtSource.close();
657
+ eventSourceRef.current = null;
658
+ setServerRunning(false);
659
+ setRunning(false);
660
+ }
661
+ }, 30000);
662
+
663
+ evtSource.addEventListener('log', (e) => {
664
+ clearTimeout(timeoutId);
665
+ const { text, type } = JSON.parse(e.data);
666
+ if (type !== 'stderr') {
667
+ agentBuffer.current += text;
668
+ }
669
+ // Update streaming output for real-time display (both stdout and stderr)
670
+ setStreamingOutput(prev => (prev + text).slice(-10000));
671
+ });
672
+
673
+ evtSource.addEventListener('done', () => {
674
+ clearTimeout(timeoutId);
675
+ setServerRunning(false);
676
+ setRunning(false);
677
+ setStreamingOutput('');
678
+ evtSource.close();
679
+ eventSourceRef.current = null;
680
+ if (onDone) onDone();
681
+ });
682
+
683
+ evtSource.onerror = () => {
684
+ clearTimeout(timeoutId);
685
+ evtSource.close();
686
+ eventSourceRef.current = null;
687
+ setServerRunning(false);
688
+ setRunning(false);
689
+ };
690
+ }
691
+ })
692
+ .catch(() => {})
693
+ .finally(() => {
694
+ if (!cancelled) setReconnecting(false);
695
+ });
696
+
697
+ // Cleanup on unmount or step change
698
+ return () => {
699
+ cancelled = true;
700
+ if (eventSourceRef.current) {
701
+ eventSourceRef.current.close();
702
+ eventSourceRef.current = null;
703
+ }
704
+ };
705
+ }, [name, step]);
403
706
 
404
707
  // Load attachments when step changes
405
708
  React.useEffect(() => {
@@ -408,15 +711,13 @@ function RunPanel({ name, step, stepStatus, onDone }) {
408
711
  .catch(() => setAttachments([]));
409
712
  }, [name, step]);
410
713
 
411
- // Reset chat state when step changes
714
+ // Reset state when step changes
412
715
  React.useEffect(() => {
413
716
  requestId.current++; // F5: Invalidate pending requests on step change
414
- setMessages([]);
415
- setVerboseLogs([]);
416
- setHistory([]);
417
717
  setResult(null);
418
718
  setErr(null);
419
719
  agentBuffer.current = '';
720
+ setStreamingOutput('');
420
721
  }, [step]);
421
722
 
422
723
  const handleAttachmentUpload = (files) => {
@@ -431,51 +732,43 @@ function RunPanel({ name, step, stepStatus, onDone }) {
431
732
  const createSseCallbacks = (currentRequestId) => ({
432
733
  onLog: (text, type) => {
433
734
  if (requestId.current !== currentRequestId) return; // Ignore stale callbacks
434
- if (type === 'stderr') {
435
- setVerboseLogs(prev => [...prev, text].slice(-MAX_VERBOSE_LOGS)); // F8: Limit size
436
- } else {
735
+ if (type !== 'stderr') {
437
736
  agentBuffer.current += text;
438
737
  }
738
+ // Update streaming output for real-time display (both stdout and stderr)
739
+ setStreamingOutput(prev => (prev + text).slice(-10000));
439
740
  },
440
741
  onStatus: (data) => {
441
742
  if (requestId.current !== currentRequestId) return;
442
- setVerboseLogs(prev => [...prev, `[${data.status}] ${data.step || ''}\n`].slice(-MAX_VERBOSE_LOGS));
743
+ const statusLine = `[${data.status}] ${data.step || ''}\n`;
744
+ setStreamingOutput(prev => (prev + statusLine).slice(-10000));
443
745
  },
444
746
  });
445
747
 
446
748
  async function handleSend() {
447
- if (!inputText.trim() || running) return;
749
+ if (running) return;
448
750
 
449
- const userMessage = inputText.trim();
450
- const msgId = Date.now(); // F11: Unique key for messages
751
+ const userMessage = inputText.trim() || `Run ${step} step`;
451
752
  setInputText('');
452
- setMessages(prev => [...prev, { id: msgId, role: 'user', content: userMessage }].slice(-MAX_MESSAGES));
453
753
  setRunning(true);
454
754
  setResult(null);
455
755
  setErr(null);
456
- setVerboseLogs([]);
457
756
  agentBuffer.current = '';
757
+ setStreamingOutput('');
458
758
 
459
759
  const currentRequestId = ++requestId.current; // F4: New request ID
460
- const newHistory = [...history, { role: 'user', content: userMessage }];
461
- setHistory(newHistory);
462
760
 
463
761
  const res = await streamPost(`/features/${name}/run/${step}`, {
464
762
  description: userMessage,
465
763
  apply,
466
764
  model: model || undefined,
467
- history: newHistory.slice(0, -1),
468
765
  attachments: attachments.map(a => ({ filename: a.filename, path: a.path })),
469
766
  }, createSseCallbacks(currentRequestId));
470
767
 
471
768
  // F4: Check if this request is still current
472
769
  if (requestId.current !== currentRequestId) return;
473
770
 
474
- if (agentBuffer.current.trim()) {
475
- const agentResponse = agentBuffer.current.trim();
476
- setMessages(prev => [...prev, { id: Date.now(), role: 'agent', content: agentResponse }].slice(-MAX_MESSAGES));
477
- setHistory(prev => [...prev, { role: 'agent', content: agentResponse }]);
478
- }
771
+ setStreamingOutput('');
479
772
 
480
773
  if (res.ok) {
481
774
  setResult('Step completed.');
@@ -487,36 +780,27 @@ function RunPanel({ name, step, stepStatus, onDone }) {
487
780
  }
488
781
 
489
782
  async function iterate() {
490
- if (!instructions.trim() || running) return;
783
+ if (running) return;
491
784
 
492
- const msgId = Date.now();
493
- const iterationInstructions = instructions;
494
- setMessages(prev => [...prev, { id: msgId, role: 'user', content: iterationInstructions }].slice(-MAX_MESSAGES));
785
+ const iterationInstructions = instructions.trim() || 'Re-run this step';
495
786
  setRunning(true);
496
787
  setResult(null);
497
788
  setErr(null);
498
- setVerboseLogs([]);
499
789
  agentBuffer.current = '';
790
+ setStreamingOutput('');
500
791
 
501
792
  const currentRequestId = ++requestId.current;
502
- // F7: Pass history in iterate mode too
503
- const newHistory = [...history, { role: 'user', content: iterationInstructions }];
504
- setHistory(newHistory);
505
793
 
506
794
  const res = await streamPost(`/features/${name}/iterate/${step}`, {
507
795
  instructions: iterationInstructions,
508
796
  apply,
509
797
  model: model || undefined,
510
- history: newHistory.slice(0, -1),
511
798
  attachments: attachments.map(a => ({ filename: a.filename, path: a.path })),
512
799
  }, createSseCallbacks(currentRequestId));
513
800
 
514
801
  if (requestId.current !== currentRequestId) return;
515
802
 
516
- if (agentBuffer.current.trim()) {
517
- setMessages(prev => [...prev, { id: Date.now(), role: 'agent', content: agentBuffer.current.trim() }].slice(-MAX_MESSAGES));
518
- setHistory(prev => [...prev, { role: 'agent', content: agentBuffer.current.trim() }]);
519
- }
803
+ setStreamingOutput('');
520
804
 
521
805
  if (res.ok) {
522
806
  setResult('Iteration completed.');
@@ -535,10 +819,8 @@ function RunPanel({ name, step, stepStatus, onDone }) {
535
819
  try {
536
820
  await api.post(`/features/${name}/reset/${step}`);
537
821
  requestId.current++;
538
- setMessages([]);
539
- setVerboseLogs([]);
540
- setHistory([]);
541
822
  setResult(null);
823
+ setStreamingOutput('');
542
824
  if (onDone) onDone();
543
825
  } catch (e) {
544
826
  setErr(`Reset failed: ${e.message}`);
@@ -556,9 +838,6 @@ function RunPanel({ name, step, stepStatus, onDone }) {
556
838
 
557
839
  return React.createElement('div', { className: 'space-y-3' },
558
840
 
559
- // --- Chat log ---
560
- React.createElement(ChatLog, { messages }),
561
-
562
841
  // --- Attachments zone ---
563
842
  React.createElement(AttachmentZone, {
564
843
  feature: name,
@@ -567,6 +846,22 @@ function RunPanel({ name, step, stepStatus, onDone }) {
567
846
  onRemove: handleAttachmentRemove,
568
847
  }),
569
848
 
849
+ // --- Reconnecting indicator ---
850
+ reconnecting && React.createElement('div', {
851
+ className: 'bg-slate-500/10 border border-slate-500/30 rounded-lg p-3 flex items-center gap-2',
852
+ },
853
+ React.createElement('div', { className: 'animate-spin w-4 h-4 border-2 border-slate-400 border-t-transparent rounded-full' }),
854
+ React.createElement('span', { className: 'text-slate-400 text-sm' }, 'Checking agent status...'),
855
+ ),
856
+
857
+ // --- Server running banner ---
858
+ !reconnecting && serverRunning && React.createElement('div', {
859
+ className: 'bg-blue-500/10 border border-blue-500/30 rounded-lg p-3 flex items-center gap-2',
860
+ },
861
+ React.createElement('span', { className: 'animate-pulse text-blue-400' }, '\u25CF'),
862
+ React.createElement('span', { className: 'text-blue-400 text-sm' }, 'Agent is running on this feature...'),
863
+ ),
864
+
570
865
  // --- Run block (when step is not done) ---
571
866
  !isDone && React.createElement('div', { className: 'bg-slate-900 border border-aia-border rounded p-4 space-y-3' },
572
867
  React.createElement('h4', { className: 'text-sm font-semibold text-emerald-400' }, `Run: ${step}`),
@@ -574,22 +869,26 @@ function RunPanel({ name, step, stepStatus, onDone }) {
574
869
  value: inputText,
575
870
  onChange: e => setInputText(e.target.value),
576
871
  onKeyDown: e => handleKeyDown(e, handleSend),
577
- placeholder: 'Describe what you want... (Enter to send, Shift+Enter for newline)',
578
- disabled: running,
872
+ placeholder: serverRunning ? 'Agent is running...' : 'Optional: add instructions... (Enter to run, Shift+Enter for newline)',
873
+ disabled: running || serverRunning,
579
874
  rows: 3,
580
875
  className: 'w-full bg-aia-card border border-aia-border rounded px-3 py-2 text-sm text-slate-200 placeholder-slate-500 focus:border-emerald-400 focus:outline-none resize-y max-h-96 overflow-auto',
581
876
  }),
582
877
  inputText.length > 0 && React.createElement('span', { className: 'text-xs text-slate-500' }, `${inputText.length} characters`),
583
878
  React.createElement('div', { className: 'flex items-center gap-4 flex-wrap' },
584
- React.createElement(ModelSelect, { model, onChange: setModel, disabled: running }),
585
- React.createElement('label', { className: 'flex items-center gap-2 text-xs text-slate-400 cursor-pointer', title: 'Allow AI to edit files in your project' },
586
- React.createElement('input', { type: 'checkbox', checked: apply, onChange: e => setApply(e.target.checked), disabled: running, className: 'rounded' }),
587
- 'Agent mode'
879
+ React.createElement(ModelSelect, { model, onChange: setModel, disabled: running || serverRunning }),
880
+ React.createElement('label', {
881
+ className: 'flex items-center gap-2 text-xs cursor-pointer group',
882
+ title: 'When enabled, the AI can read/write files and execute commands in your project',
883
+ },
884
+ React.createElement('input', { type: 'checkbox', checked: apply, onChange: e => setApply(e.target.checked), disabled: running || serverRunning, className: 'rounded' }),
885
+ React.createElement('span', { className: apply ? 'text-amber-400' : 'text-slate-400' }, apply ? '🤖 Agent mode ON' : 'Agent mode'),
886
+ React.createElement('span', { className: 'text-slate-600 text-[10px] hidden group-hover:inline' }, '(can edit files)')
588
887
  ),
589
888
  React.createElement('button', {
590
- onClick: handleSend, disabled: running || !inputText.trim(),
889
+ onClick: handleSend, disabled: running || serverRunning,
591
890
  className: 'bg-emerald-500/20 text-emerald-400 border border-emerald-500/30 rounded px-4 py-1.5 text-sm hover:bg-emerald-500/30 disabled:opacity-40',
592
- }, running ? 'Running...' : 'Send'),
891
+ }, running || serverRunning ? 'Running...' : (inputText.trim() ? 'Send' : 'Run')),
593
892
  ),
594
893
  ),
595
894
 
@@ -605,24 +904,28 @@ function RunPanel({ name, step, stepStatus, onDone }) {
605
904
  value: instructions,
606
905
  onChange: e => setInstructions(e.target.value),
607
906
  onKeyDown: e => handleKeyDown(e, iterate),
608
- placeholder: 'e.g. "Add error handling for edge cases"... (Enter to send, Shift+Enter for newline)',
609
- disabled: running,
907
+ placeholder: serverRunning ? 'Agent is running...' : 'Optional: iteration instructions... (Enter to run, Shift+Enter for newline)',
908
+ disabled: running || serverRunning,
610
909
  rows: 3,
611
910
  className: 'w-full bg-aia-card border border-aia-border rounded px-3 py-2 text-sm text-slate-200 placeholder-slate-500 focus:border-violet-400 focus:outline-none resize-y max-h-96 overflow-auto',
612
911
  }),
613
912
  instructions.length > 0 && React.createElement('span', { className: 'text-xs text-slate-500' }, `${instructions.length} characters`),
614
913
  React.createElement('div', { className: 'flex items-center gap-4 flex-wrap' },
615
- React.createElement(ModelSelect, { model, onChange: setModel, disabled: running }),
616
- React.createElement('label', { className: 'flex items-center gap-2 text-xs text-slate-400 cursor-pointer', title: 'Allow AI to edit files in your project' },
617
- React.createElement('input', { type: 'checkbox', checked: apply, onChange: e => setApply(e.target.checked), disabled: running, className: 'rounded' }),
618
- 'Agent mode'
914
+ React.createElement(ModelSelect, { model, onChange: setModel, disabled: running || serverRunning }),
915
+ React.createElement('label', {
916
+ className: 'flex items-center gap-2 text-xs cursor-pointer group',
917
+ title: 'When enabled, the AI can read/write files and execute commands in your project',
918
+ },
919
+ React.createElement('input', { type: 'checkbox', checked: apply, onChange: e => setApply(e.target.checked), disabled: running || serverRunning, className: 'rounded' }),
920
+ React.createElement('span', { className: apply ? 'text-amber-400' : 'text-slate-400' }, apply ? '🤖 Agent mode ON' : 'Agent mode'),
921
+ React.createElement('span', { className: 'text-slate-600 text-[10px] hidden group-hover:inline' }, '(can edit files)')
619
922
  ),
620
923
  React.createElement('button', {
621
- onClick: iterate, disabled: running || !instructions.trim(),
924
+ onClick: iterate, disabled: running || serverRunning,
622
925
  className: 'bg-violet-500/20 text-violet-400 border border-violet-500/30 rounded px-4 py-1.5 text-sm hover:bg-violet-500/30 disabled:opacity-40',
623
- }, running ? 'Iterating...' : 'Iterate'),
926
+ }, running || serverRunning ? 'Running...' : (instructions.trim() ? 'Iterate' : 'Re-run')),
624
927
  React.createElement('button', {
625
- onClick: reset, disabled: running || resetting,
928
+ onClick: reset, disabled: running || serverRunning || resetting,
626
929
  className: 'text-slate-500 hover:text-slate-300 text-xs disabled:opacity-40',
627
930
  }, resetting ? 'Resetting...' : 'Reset to pending'),
628
931
  ),
@@ -630,12 +933,9 @@ function RunPanel({ name, step, stepStatus, onDone }) {
630
933
  React.createElement(StepGuidance, { step, feature: name }),
631
934
  ),
632
935
 
633
- // --- Verbose panel (stderr logs) ---
634
- React.createElement(VerbosePanel, {
635
- logs: verboseLogs,
636
- expanded: verboseExpanded,
637
- onToggle: () => setVerboseExpanded(!verboseExpanded),
638
- }),
936
+ // --- Streaming output panel (live agent output) ---
937
+ (running || serverRunning) && React.createElement(StreamingOutputPanel, { output: streamingOutput }),
938
+
639
939
 
640
940
  // --- Results ---
641
941
  result && React.createElement('p', { className: 'text-emerald-400 text-xs' }, result),
@@ -645,16 +945,24 @@ function RunPanel({ name, step, stepStatus, onDone }) {
645
945
 
646
946
  export function FeatureDetail({ name }) {
647
947
  const [feature, setFeature] = React.useState(null);
948
+ const [availableApps, setAvailableApps] = React.useState([]);
648
949
  const [loading, setLoading] = React.useState(true);
649
950
  const [activeFile, setActiveFile] = React.useState('init.md');
650
951
  const [activeStep, setActiveStep] = React.useState(null);
651
952
  const [showInitPanel, setShowInitPanel] = React.useState(false);
652
953
  const [selectedFlow, setSelectedFlow] = React.useState(null);
653
954
  const [fileVersion, setFileVersion] = React.useState(0);
955
+ const [showDeleteConfirm, setShowDeleteConfirm] = React.useState(false);
956
+ const [deleting, setDeleting] = React.useState(false);
957
+ const [restoring, setRestoring] = React.useState(false);
654
958
 
655
959
  async function load(checkInitPanel = true) {
656
960
  try {
657
- const data = await api.get(`/features/${name}`);
961
+ const [data, appsData] = await Promise.all([
962
+ api.get(`/features/${name}`),
963
+ api.get('/apps'),
964
+ ]);
965
+ setAvailableApps(appsData);
658
966
  setFeature(data);
659
967
  const steps = data.steps || {};
660
968
 
@@ -668,7 +976,7 @@ export function FeatureDetail({ name }) {
668
976
  const allPending = Object.values(steps).every(s => s === 'pending');
669
977
  const persistedFlow = data.flow || selectedFlow;
670
978
 
671
- if (allPending && !persistedFlow) {
979
+ if (allPending) {
672
980
  // Check if init.md has content (enriched)
673
981
  try {
674
982
  const initFile = await api.get(`/features/${name}/files/init.md`);
@@ -752,17 +1060,94 @@ export function FeatureDetail({ name }) {
752
1060
 
753
1061
  const steps = feature.steps || {};
754
1062
 
1063
+ const handleTypeChanged = (newType) => {
1064
+ setFeature(prev => ({ ...prev, type: newType }));
1065
+ // If changed to bug, flow should switch to quick
1066
+ if (newType === 'bug') {
1067
+ setSelectedFlow('quick');
1068
+ }
1069
+ load(false);
1070
+ };
1071
+
1072
+ const handleAppsChanged = (newApps) => {
1073
+ setFeature(prev => ({ ...prev, apps: newApps }));
1074
+ };
1075
+
1076
+ const handleDelete = async () => {
1077
+ setDeleting(true);
1078
+ try {
1079
+ await api.delete(`/features/${name}`);
1080
+ // Navigate back to dashboard
1081
+ window.location.hash = '#/';
1082
+ } catch (e) {
1083
+ console.error('Failed to delete feature:', e);
1084
+ setDeleting(false);
1085
+ setShowDeleteConfirm(false);
1086
+ }
1087
+ };
1088
+
1089
+ const handleRestore = async () => {
1090
+ setRestoring(true);
1091
+ try {
1092
+ await api.post(`/features/${name}/restore`);
1093
+ setFeature(prev => ({ ...prev, deletedAt: null, isDeleted: false }));
1094
+ } catch (e) {
1095
+ console.error('Failed to restore feature:', e);
1096
+ }
1097
+ setRestoring(false);
1098
+ };
1099
+
1100
+ const isDeleted = feature.deletedAt != null || feature.isDeleted;
1101
+
755
1102
  return React.createElement('div', { className: 'space-y-6' },
1103
+ // Delete confirmation dialog
1104
+ showDeleteConfirm && React.createElement(ConfirmDialog, {
1105
+ title: 'Delete Feature',
1106
+ message: `Are you sure you want to delete "${name}"? The feature will be moved to the deleted items and can be restored later.`,
1107
+ confirmText: deleting ? 'Deleting...' : 'Delete',
1108
+ onConfirm: handleDelete,
1109
+ onCancel: () => setShowDeleteConfirm(false),
1110
+ }),
1111
+
1112
+ // Deleted banner
1113
+ isDeleted && React.createElement('div', {
1114
+ className: 'bg-red-500/10 border border-red-500/30 rounded-lg p-4 flex items-center justify-between',
1115
+ },
1116
+ React.createElement('div', { className: 'flex items-center gap-2' },
1117
+ React.createElement('span', { className: 'text-red-400 text-lg' }, '\uD83D\uDDD1'),
1118
+ React.createElement('span', { className: 'text-red-400 text-sm' }, 'This feature has been deleted'),
1119
+ ),
1120
+ React.createElement('button', {
1121
+ onClick: handleRestore,
1122
+ disabled: restoring,
1123
+ className: 'bg-emerald-500/20 text-emerald-400 border border-emerald-500/30 rounded px-4 py-2 text-sm hover:bg-emerald-500/30 disabled:opacity-40',
1124
+ }, restoring ? 'Restoring...' : 'Restore Feature'),
1125
+ ),
1126
+
756
1127
  // Header
757
- React.createElement('div', { className: 'flex items-center gap-3' },
758
- React.createElement('a', { href: '#/', className: 'text-slate-500 hover:text-slate-300' }, '\u2190'),
759
- React.createElement('h1', { className: 'text-xl font-bold text-slate-100' }, name),
760
- feature.current_step && React.createElement('span', { className: 'text-xs bg-aia-accent/20 text-aia-accent px-2 py-0.5 rounded' }, feature.current_step),
1128
+ React.createElement('div', { className: 'flex items-center justify-between' },
1129
+ React.createElement('div', { className: 'flex items-center gap-3' },
1130
+ React.createElement('a', { href: '#/', className: 'text-slate-500 hover:text-slate-300' }, '\u2190'),
1131
+ React.createElement('h1', { className: `text-xl font-bold ${isDeleted ? 'text-slate-500 line-through' : 'text-slate-100'}` }, name),
1132
+ React.createElement(TypeBadgeEditable, {
1133
+ name,
1134
+ currentType: feature.type,
1135
+ onChanged: handleTypeChanged,
1136
+ }),
1137
+ feature.current_step && !isDeleted && React.createElement('span', { className: 'text-xs bg-aia-accent/20 text-aia-accent px-2 py-0.5 rounded' }, feature.current_step),
1138
+ ),
1139
+ // Delete button (only show if not deleted)
1140
+ !isDeleted && React.createElement('button', {
1141
+ onClick: () => setShowDeleteConfirm(true),
1142
+ className: 'bg-red-500/20 text-red-400 border border-red-500/30 rounded px-4 py-2 text-sm hover:bg-red-500/30 transition-colors',
1143
+ 'aria-label': `Delete feature ${name}`,
1144
+ }, 'Remove'),
761
1145
  ),
762
1146
 
763
1147
  // Init panel (when no steps started)
764
1148
  showInitPanel && React.createElement(InitPanel, {
765
1149
  name,
1150
+ featureType: feature.type || DEFAULT_FEATURE_TYPE,
766
1151
  onFlowSelected: handleFlowSelected,
767
1152
  onCancel: () => setShowInitPanel(false),
768
1153
  onEnriched: () => {
@@ -778,6 +1163,14 @@ export function FeatureDetail({ name }) {
778
1163
  onFlowChanged: handleFlowChanged,
779
1164
  }),
780
1165
 
1166
+ // Scope editor (only show if not showing init panel and apps are available)
1167
+ !showInitPanel && availableApps.length > 0 && React.createElement(ScopeEditor, {
1168
+ name,
1169
+ currentApps: feature.apps,
1170
+ availableApps,
1171
+ onChanged: handleAppsChanged,
1172
+ }),
1173
+
781
1174
  // Worktrunk panel
782
1175
  !showInitPanel && React.createElement(WorktrunkPanel, { featureName: name }),
783
1176