@bamptee/aia-code 2.0.11 → 2.0.13
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.
- package/README.md +408 -34
- package/package.json +11 -2
- package/src/constants.js +24 -0
- package/src/providers/anthropic.js +2 -21
- package/src/providers/cli-runner.js +5 -3
- package/src/services/agent-sessions.js +110 -0
- package/src/services/apps.js +132 -0
- package/src/services/config.js +41 -0
- package/src/services/feature.js +28 -7
- package/src/services/model-call.js +2 -2
- package/src/services/runner.js +23 -1
- package/src/services/status.js +69 -1
- package/src/services/test-quick.js +229 -0
- package/src/services/worktrunk.js +135 -21
- package/src/types/test-quick.js +88 -0
- package/src/ui/api/config.js +28 -1
- package/src/ui/api/features.js +160 -50
- package/src/ui/api/index.js +2 -0
- package/src/ui/api/test-quick.js +207 -0
- package/src/ui/api/worktrunk.js +63 -25
- package/src/ui/public/components/config-view.js +95 -0
- package/src/ui/public/components/dashboard.js +823 -163
- package/src/ui/public/components/feature-detail.js +517 -124
- package/src/ui/public/components/test-quick.js +276 -0
- package/src/ui/public/components/worktrunk-panel.js +187 -25
- package/src/ui/public/index.html +13 -0
- package/src/ui/public/main.js +5 -1
- package/src/ui/server.js +97 -67
|
@@ -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
|
|
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
|
-
}, [
|
|
394
|
+
}, [output]);
|
|
226
395
|
|
|
227
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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:
|
|
296
|
-
React.createElement('h3', { className:
|
|
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:
|
|
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:
|
|
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:
|
|
319
|
-
|
|
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:
|
|
327
|
-
React.createElement('span', { className:
|
|
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
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
783
|
+
if (running) return;
|
|
491
784
|
|
|
492
|
-
const
|
|
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
|
-
|
|
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: '
|
|
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', {
|
|
586
|
-
|
|
587
|
-
'
|
|
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 ||
|
|
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: '
|
|
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', {
|
|
617
|
-
|
|
618
|
-
'
|
|
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 ||
|
|
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 ? '
|
|
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
|
-
// ---
|
|
634
|
-
React.createElement(
|
|
635
|
-
|
|
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
|
|
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
|
|
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
|
|
758
|
-
React.createElement('
|
|
759
|
-
|
|
760
|
-
|
|
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
|
|