@bamptee/aia-code 2.0.0 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -4
- package/package.json +7 -1
- package/src/commands/init.js +11 -1
- package/src/models.js +19 -2
- package/src/prompt-builder.js +33 -1
- package/src/providers/cli-runner.js +2 -1
- package/src/services/config.js +53 -2
- package/src/services/flow-analyzer.js +32 -0
- package/src/services/runner.js +2 -2
- package/src/services/status.js +14 -0
- package/src/services/suggestions.js +166 -0
- package/src/services/worktrunk.js +197 -0
- package/src/ui/api/config.js +55 -2
- package/src/ui/api/constants.js +9 -3
- package/src/ui/api/features.js +301 -6
- package/src/ui/api/index.js +2 -0
- package/src/ui/api/worktrunk.js +153 -0
- package/src/ui/public/components/config-view.js +196 -2
- package/src/ui/public/components/dashboard.js +64 -4
- package/src/ui/public/components/feature-detail.js +589 -104
- package/src/ui/public/components/terminal.js +197 -0
- package/src/ui/public/components/worktrunk-panel.js +205 -0
- package/src/ui/public/main.js +23 -1
- package/src/ui/router.js +1 -1
- package/src/ui/server.js +85 -0
- package/src/utils/prompt.js +38 -0
|
@@ -1,5 +1,94 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { api, streamPost } from '/main.js';
|
|
3
|
+
import { WorktrunkPanel } from '/components/worktrunk-panel.js';
|
|
4
|
+
|
|
5
|
+
function formatFileSize(bytes) {
|
|
6
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
7
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
8
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function AttachmentZone({ feature, attachments, onUpload, onRemove }) {
|
|
12
|
+
const inputRef = React.useRef(null);
|
|
13
|
+
const [uploading, setUploading] = React.useState(false);
|
|
14
|
+
const [error, setError] = React.useState(null);
|
|
15
|
+
|
|
16
|
+
const handleFiles = async (files) => {
|
|
17
|
+
if (!files.length) return;
|
|
18
|
+
|
|
19
|
+
setUploading(true);
|
|
20
|
+
setError(null);
|
|
21
|
+
|
|
22
|
+
const formData = new FormData();
|
|
23
|
+
for (const file of files) {
|
|
24
|
+
if (file.size > 10 * 1024 * 1024) {
|
|
25
|
+
setError(`File "${file.name}" exceeds 10MB limit`);
|
|
26
|
+
setUploading(false);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
formData.append('files', file);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const res = await api.upload(`/features/${feature}/attachments`, formData);
|
|
34
|
+
if (res.ok && res.files) {
|
|
35
|
+
onUpload(res.files);
|
|
36
|
+
}
|
|
37
|
+
} catch (e) {
|
|
38
|
+
setError(e.message || 'Upload failed');
|
|
39
|
+
}
|
|
40
|
+
setUploading(false);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const handleRemove = async (filename) => {
|
|
44
|
+
try {
|
|
45
|
+
await api.delete(`/features/${feature}/attachments/${filename}`);
|
|
46
|
+
onRemove(filename);
|
|
47
|
+
} catch (e) {
|
|
48
|
+
setError(e.message || 'Delete failed');
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return React.createElement('div', { className: 'border border-dashed border-slate-600 rounded p-3 mb-3' },
|
|
53
|
+
React.createElement('input', {
|
|
54
|
+
ref: inputRef,
|
|
55
|
+
type: 'file',
|
|
56
|
+
multiple: true,
|
|
57
|
+
accept: 'image/*,application/pdf,text/*,.md,.txt,.json,.yaml,.yml',
|
|
58
|
+
className: 'hidden',
|
|
59
|
+
onChange: (e) => handleFiles(Array.from(e.target.files)),
|
|
60
|
+
}),
|
|
61
|
+
React.createElement('div', { className: 'flex items-center gap-2' },
|
|
62
|
+
React.createElement('button', {
|
|
63
|
+
onClick: () => inputRef.current?.click(),
|
|
64
|
+
disabled: uploading,
|
|
65
|
+
className: 'text-xs text-slate-400 hover:text-slate-200 flex items-center gap-1',
|
|
66
|
+
},
|
|
67
|
+
uploading ? React.createElement('span', { className: 'animate-spin' }, '⟳') : '+',
|
|
68
|
+
uploading ? ' Uploading...' : ' Add attachments (images, PDF, text)',
|
|
69
|
+
),
|
|
70
|
+
React.createElement('span', { className: 'text-xs text-slate-600' }, 'Max 10MB/file'),
|
|
71
|
+
),
|
|
72
|
+
error && React.createElement('p', { className: 'text-red-400 text-xs mt-1' }, error),
|
|
73
|
+
attachments.length > 0 && React.createElement('div', { className: 'mt-2 flex flex-wrap gap-2' },
|
|
74
|
+
...attachments.map(a => React.createElement('span', {
|
|
75
|
+
key: a.filename,
|
|
76
|
+
className: 'bg-slate-700 text-slate-300 text-xs px-2 py-1 rounded flex items-center gap-2',
|
|
77
|
+
title: a.path,
|
|
78
|
+
},
|
|
79
|
+
a.mimeType?.startsWith('image/') && React.createElement('span', { className: 'text-emerald-400' }, '🖼'),
|
|
80
|
+
a.mimeType === 'application/pdf' && React.createElement('span', { className: 'text-red-400' }, '📄'),
|
|
81
|
+
a.originalName || a.filename,
|
|
82
|
+
a.size && React.createElement('span', { className: 'text-slate-500' }, `(${formatFileSize(a.size)})`),
|
|
83
|
+
React.createElement('button', {
|
|
84
|
+
onClick: () => handleRemove(a.filename),
|
|
85
|
+
className: 'text-red-400 hover:text-red-300 ml-1',
|
|
86
|
+
title: 'Remove attachment',
|
|
87
|
+
}, '×'),
|
|
88
|
+
)),
|
|
89
|
+
),
|
|
90
|
+
);
|
|
91
|
+
}
|
|
3
92
|
|
|
4
93
|
const STATUS_CLASSES = {
|
|
5
94
|
done: 'step-done',
|
|
@@ -65,63 +154,36 @@ function FileEditor({ name, filename, onSaved }) {
|
|
|
65
154
|
);
|
|
66
155
|
}
|
|
67
156
|
|
|
68
|
-
function
|
|
69
|
-
const [
|
|
70
|
-
const [description, setDescription] = React.useState('');
|
|
71
|
-
const [expanded, setExpanded] = React.useState(false);
|
|
72
|
-
const [err, setErr] = React.useState(null);
|
|
73
|
-
const [logs, setLogs] = React.useState([]);
|
|
74
|
-
|
|
75
|
-
async function run() {
|
|
76
|
-
setRunning(true);
|
|
77
|
-
setErr(null);
|
|
78
|
-
setLogs([]);
|
|
79
|
-
|
|
80
|
-
const res = await streamPost(`/features/${name}/quick`, { description }, {
|
|
81
|
-
onLog: (text) => setLogs(prev => [...prev, text]),
|
|
82
|
-
onStatus: (data) => setLogs(prev => [...prev, `[${data.status}] ${data.mode || ''}\n`]),
|
|
83
|
-
});
|
|
157
|
+
function FlowSelector({ name, currentFlow, onFlowChanged }) {
|
|
158
|
+
const [saving, setSaving] = React.useState(false);
|
|
84
159
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
160
|
+
const handleChange = async (newFlow) => {
|
|
161
|
+
if (newFlow === currentFlow) return;
|
|
162
|
+
setSaving(true);
|
|
163
|
+
try {
|
|
164
|
+
await api.patch(`/features/${name}/flow`, { flow: newFlow });
|
|
165
|
+
if (onFlowChanged) onFlowChanged(newFlow);
|
|
166
|
+
} catch (e) {
|
|
167
|
+
console.error('Failed to update flow:', e);
|
|
89
168
|
}
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (!expanded) {
|
|
94
|
-
return React.createElement('button', {
|
|
95
|
-
onClick: () => setExpanded(true),
|
|
96
|
-
className: 'bg-amber-500/20 text-amber-400 border border-amber-500/30 rounded px-3 py-1.5 text-xs hover:bg-amber-500/30',
|
|
97
|
-
}, 'Quick Ticket (dev-plan \u2192 implement \u2192 review)');
|
|
98
|
-
}
|
|
169
|
+
setSaving(false);
|
|
170
|
+
};
|
|
99
171
|
|
|
100
|
-
return React.createElement('div', { className: '
|
|
101
|
-
React.createElement('
|
|
102
|
-
React.createElement('
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
disabled: running,
|
|
115
|
-
className: 'bg-amber-500/20 text-amber-400 border border-amber-500/30 rounded px-4 py-1.5 text-sm hover:bg-amber-500/30 disabled:opacity-40',
|
|
116
|
-
}, running ? 'Running...' : 'Run Quick'),
|
|
117
|
-
React.createElement('button', {
|
|
118
|
-
onClick: () => setExpanded(false),
|
|
119
|
-
disabled: running,
|
|
120
|
-
className: 'text-slate-500 hover:text-slate-300 text-xs',
|
|
121
|
-
}, 'Cancel'),
|
|
172
|
+
return React.createElement('div', { className: 'flex items-center gap-2' },
|
|
173
|
+
React.createElement('span', { className: 'text-xs text-slate-500' }, 'Flow:'),
|
|
174
|
+
React.createElement('button', {
|
|
175
|
+
onClick: () => handleChange('quick'),
|
|
176
|
+
disabled: saving,
|
|
177
|
+
className: `px-3 py-1 text-xs rounded border transition-all ${currentFlow === 'quick' ? 'bg-amber-500/20 text-amber-400 border-amber-500/30' : 'bg-slate-800 text-slate-400 border-slate-600 hover:border-slate-500'}`,
|
|
178
|
+
}, 'Quick'),
|
|
179
|
+
React.createElement('button', {
|
|
180
|
+
onClick: () => handleChange('full'),
|
|
181
|
+
disabled: saving,
|
|
182
|
+
className: `px-3 py-1 text-xs rounded border transition-all ${currentFlow === 'full' ? 'bg-violet-500/20 text-violet-400 border-violet-500/30' : 'bg-slate-800 text-slate-400 border-slate-600 hover:border-slate-500'}`,
|
|
183
|
+
}, 'Full'),
|
|
184
|
+
React.createElement('span', { className: 'text-xs text-slate-600 ml-2' },
|
|
185
|
+
currentFlow === 'quick' ? '(dev-plan \u2192 implement \u2192 review)' : '(brief \u2192 ... \u2192 review)'
|
|
122
186
|
),
|
|
123
|
-
React.createElement(LogViewer, { logs }),
|
|
124
|
-
err && React.createElement('p', { className: 'text-red-400 text-xs' }, err),
|
|
125
187
|
);
|
|
126
188
|
}
|
|
127
189
|
|
|
@@ -132,23 +194,59 @@ function LogViewer({ logs }) {
|
|
|
132
194
|
}, [logs]);
|
|
133
195
|
|
|
134
196
|
if (!logs.length) return null;
|
|
197
|
+
|
|
135
198
|
return React.createElement('pre', {
|
|
136
199
|
ref,
|
|
137
200
|
className: 'bg-black/50 border border-aia-border rounded p-3 text-xs text-slate-400 overflow-auto max-h-64 whitespace-pre-wrap',
|
|
138
201
|
}, logs.join(''));
|
|
139
202
|
}
|
|
140
203
|
|
|
141
|
-
function
|
|
142
|
-
|
|
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
|
+
}
|
|
143
220
|
|
|
221
|
+
function ChatLog({ messages }) {
|
|
222
|
+
const ref = React.useRef(null);
|
|
144
223
|
React.useEffect(() => {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
setModels(stepModels.map(m => m.model));
|
|
148
|
-
}).catch(() => {});
|
|
149
|
-
}, [step]);
|
|
224
|
+
if (ref.current) ref.current.scrollTop = ref.current.scrollHeight;
|
|
225
|
+
}, [messages]);
|
|
150
226
|
|
|
151
|
-
if (
|
|
227
|
+
if (!messages.length) return null;
|
|
228
|
+
|
|
229
|
+
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
|
+
));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function ModelSelect({ model, onChange, disabled }) {
|
|
245
|
+
const [models, setModels] = React.useState([]);
|
|
246
|
+
|
|
247
|
+
React.useEffect(() => {
|
|
248
|
+
api.get('/models').then(setModels).catch(() => {});
|
|
249
|
+
}, []);
|
|
152
250
|
|
|
153
251
|
return React.createElement('select', {
|
|
154
252
|
value: model,
|
|
@@ -161,66 +259,337 @@ function ModelSelect({ step, model, onChange, disabled }) {
|
|
|
161
259
|
);
|
|
162
260
|
}
|
|
163
261
|
|
|
262
|
+
function InitPanel({ name, onFlowSelected, onCancel, onEnriched }) {
|
|
263
|
+
const [description, setDescription] = React.useState('');
|
|
264
|
+
const [suggestion, setSuggestion] = React.useState(null);
|
|
265
|
+
const [loading, setLoading] = React.useState(false);
|
|
266
|
+
const [statusMsg, setStatusMsg] = React.useState('');
|
|
267
|
+
const [logs, setLogs] = React.useState([]);
|
|
268
|
+
const [err, setErr] = React.useState(null);
|
|
269
|
+
|
|
270
|
+
const handleSubmit = async () => {
|
|
271
|
+
setLoading(true);
|
|
272
|
+
setErr(null);
|
|
273
|
+
setLogs([]);
|
|
274
|
+
setStatusMsg('Structuring your description...');
|
|
275
|
+
|
|
276
|
+
const res = await streamPost(`/features/${name}/init`, { description }, {
|
|
277
|
+
onLog: (text) => setLogs(prev => [...prev, text]),
|
|
278
|
+
onStatus: (data) => setStatusMsg(data.message || data.status),
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
if (res.ok) {
|
|
282
|
+
setSuggestion(res.suggestion);
|
|
283
|
+
setStatusMsg('');
|
|
284
|
+
if (onEnriched) onEnriched();
|
|
285
|
+
} else {
|
|
286
|
+
setErr(res.error || 'Failed to enrich description');
|
|
287
|
+
}
|
|
288
|
+
setLoading(false);
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const handleFlowChoice = (flow) => {
|
|
292
|
+
onFlowSelected(flow);
|
|
293
|
+
};
|
|
294
|
+
|
|
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'),
|
|
297
|
+
|
|
298
|
+
// Textarea (hidden when loading or has suggestion)
|
|
299
|
+
!loading && !suggestion && React.createElement('textarea', {
|
|
300
|
+
value: description,
|
|
301
|
+
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.',
|
|
303
|
+
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',
|
|
305
|
+
}),
|
|
306
|
+
|
|
307
|
+
// Character count and buttons
|
|
308
|
+
!loading && !suggestion && React.createElement('div', { className: 'flex items-center justify-between' },
|
|
309
|
+
React.createElement('span', { className: 'text-xs text-slate-500' }, `${description.length} characters`),
|
|
310
|
+
React.createElement('div', { className: 'flex gap-2' },
|
|
311
|
+
React.createElement('button', {
|
|
312
|
+
onClick: onCancel,
|
|
313
|
+
className: 'text-slate-500 hover:text-slate-300 text-xs',
|
|
314
|
+
}, 'Cancel'),
|
|
315
|
+
React.createElement('button', {
|
|
316
|
+
onClick: handleSubmit,
|
|
317
|
+
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'),
|
|
320
|
+
),
|
|
321
|
+
),
|
|
322
|
+
|
|
323
|
+
// Loading state with logs
|
|
324
|
+
loading && React.createElement('div', { className: 'space-y-3' },
|
|
325
|
+
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...'),
|
|
328
|
+
),
|
|
329
|
+
logs.length > 0 && React.createElement(LogViewer, { logs }),
|
|
330
|
+
),
|
|
331
|
+
|
|
332
|
+
// Error
|
|
333
|
+
err && React.createElement('p', { className: 'text-red-400 text-xs' }, err),
|
|
334
|
+
|
|
335
|
+
// Flow suggestion
|
|
336
|
+
suggestion && React.createElement('div', { className: 'space-y-3' },
|
|
337
|
+
React.createElement('div', { className: 'flex items-center gap-2' },
|
|
338
|
+
React.createElement('span', { className: 'text-emerald-400' }, '\u2713'),
|
|
339
|
+
React.createElement('span', { className: 'text-sm text-slate-300' }, 'Feature spec created in init.md'),
|
|
340
|
+
),
|
|
341
|
+
React.createElement('p', { className: 'text-sm text-slate-400' },
|
|
342
|
+
`Suggested: ${suggestion === 'quick' ? 'Quick Flow (dev-plan \u2192 implement \u2192 review)' : 'Full Flow (8 steps)'}`
|
|
343
|
+
),
|
|
344
|
+
React.createElement('div', { className: 'flex gap-2' },
|
|
345
|
+
React.createElement('button', {
|
|
346
|
+
onClick: () => handleFlowChoice('quick'),
|
|
347
|
+
className: `px-4 py-2 text-sm rounded border ${suggestion === 'quick' ? 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30' : 'bg-slate-700 text-slate-300 border-slate-600'} hover:brightness-110`,
|
|
348
|
+
}, 'Quick Flow'),
|
|
349
|
+
React.createElement('button', {
|
|
350
|
+
onClick: () => handleFlowChoice('full'),
|
|
351
|
+
className: `px-4 py-2 text-sm rounded border ${suggestion === 'full' ? 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30' : 'bg-slate-700 text-slate-300 border-slate-600'} hover:brightness-110`,
|
|
352
|
+
}, 'Full Flow'),
|
|
353
|
+
),
|
|
354
|
+
),
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function StepGuidance({ step, feature }) {
|
|
359
|
+
const [guidance, setGuidance] = React.useState(null);
|
|
360
|
+
|
|
361
|
+
React.useEffect(() => {
|
|
362
|
+
api.get(`/features/${feature}/guidance/${step}`).then(setGuidance).catch(() => {});
|
|
363
|
+
}, [step, feature]);
|
|
364
|
+
|
|
365
|
+
if (!guidance) return null;
|
|
366
|
+
|
|
367
|
+
return React.createElement('div', { className: 'bg-emerald-500/10 border border-emerald-500/30 rounded p-4 mt-4' },
|
|
368
|
+
React.createElement('h4', { className: 'text-emerald-400 font-semibold text-sm mb-2' }, guidance.summary),
|
|
369
|
+
React.createElement('div', { className: 'text-slate-300 text-xs space-y-1' },
|
|
370
|
+
...guidance.actions.map((action, i) =>
|
|
371
|
+
React.createElement('p', { key: i }, `\u2022 ${action.replace('<feature>', feature)}`)
|
|
372
|
+
),
|
|
373
|
+
),
|
|
374
|
+
guidance.next && React.createElement('p', { className: 'text-cyan-400 text-xs mt-2' },
|
|
375
|
+
`Next step: ${guidance.next}`
|
|
376
|
+
),
|
|
377
|
+
guidance.tips.length > 0 && React.createElement('p', { className: 'text-amber-400 text-xs mt-2' },
|
|
378
|
+
`Tip: ${guidance.tips[0]}`
|
|
379
|
+
),
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const MAX_MESSAGES = 100;
|
|
384
|
+
const MAX_VERBOSE_LOGS = 500;
|
|
385
|
+
|
|
164
386
|
function RunPanel({ name, step, stepStatus, onDone }) {
|
|
165
387
|
const isDone = stepStatus === 'done';
|
|
166
|
-
const [
|
|
388
|
+
const [inputText, setInputText] = React.useState('');
|
|
167
389
|
const [instructions, setInstructions] = React.useState('');
|
|
168
390
|
const [model, setModel] = React.useState('');
|
|
169
391
|
const [apply, setApply] = React.useState(false);
|
|
170
392
|
const [running, setRunning] = React.useState(false);
|
|
171
393
|
const [result, setResult] = React.useState(null);
|
|
172
394
|
const [err, setErr] = React.useState(null);
|
|
173
|
-
const [
|
|
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
|
+
const [attachments, setAttachments] = React.useState([]);
|
|
400
|
+
const agentBuffer = React.useRef('');
|
|
401
|
+
const requestId = React.useRef(0); // F4: Track request to prevent race conditions
|
|
402
|
+
const [resetting, setResetting] = React.useState(false); // F14: Track reset state
|
|
403
|
+
|
|
404
|
+
// Load attachments when step changes
|
|
405
|
+
React.useEffect(() => {
|
|
406
|
+
api.get(`/features/${name}/attachments`)
|
|
407
|
+
.then(files => setAttachments(files))
|
|
408
|
+
.catch(() => setAttachments([]));
|
|
409
|
+
}, [name, step]);
|
|
410
|
+
|
|
411
|
+
// Reset chat state when step changes
|
|
412
|
+
React.useEffect(() => {
|
|
413
|
+
requestId.current++; // F5: Invalidate pending requests on step change
|
|
414
|
+
setMessages([]);
|
|
415
|
+
setVerboseLogs([]);
|
|
416
|
+
setHistory([]);
|
|
417
|
+
setResult(null);
|
|
418
|
+
setErr(null);
|
|
419
|
+
agentBuffer.current = '';
|
|
420
|
+
}, [step]);
|
|
421
|
+
|
|
422
|
+
const handleAttachmentUpload = (files) => {
|
|
423
|
+
setAttachments(prev => [...prev, ...files]);
|
|
424
|
+
};
|
|
174
425
|
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
onStatus: (data) => setLogs(prev => [...prev, `[${data.status}] ${data.step || ''}\n`]),
|
|
426
|
+
const handleAttachmentRemove = (filename) => {
|
|
427
|
+
setAttachments(prev => prev.filter(a => a.filename !== filename));
|
|
178
428
|
};
|
|
179
429
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
430
|
+
// F4: Create callbacks bound to current request
|
|
431
|
+
const createSseCallbacks = (currentRequestId) => ({
|
|
432
|
+
onLog: (text, type) => {
|
|
433
|
+
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 {
|
|
437
|
+
agentBuffer.current += text;
|
|
438
|
+
}
|
|
439
|
+
},
|
|
440
|
+
onStatus: (data) => {
|
|
441
|
+
if (requestId.current !== currentRequestId) return;
|
|
442
|
+
setVerboseLogs(prev => [...prev, `[${data.status}] ${data.step || ''}\n`].slice(-MAX_VERBOSE_LOGS));
|
|
443
|
+
},
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
async function handleSend() {
|
|
447
|
+
if (!inputText.trim() || running) return;
|
|
448
|
+
|
|
449
|
+
const userMessage = inputText.trim();
|
|
450
|
+
const msgId = Date.now(); // F11: Unique key for messages
|
|
451
|
+
setInputText('');
|
|
452
|
+
setMessages(prev => [...prev, { id: msgId, role: 'user', content: userMessage }].slice(-MAX_MESSAGES));
|
|
453
|
+
setRunning(true);
|
|
454
|
+
setResult(null);
|
|
455
|
+
setErr(null);
|
|
456
|
+
setVerboseLogs([]);
|
|
457
|
+
agentBuffer.current = '';
|
|
458
|
+
|
|
459
|
+
const currentRequestId = ++requestId.current; // F4: New request ID
|
|
460
|
+
const newHistory = [...history, { role: 'user', content: userMessage }];
|
|
461
|
+
setHistory(newHistory);
|
|
462
|
+
|
|
463
|
+
const res = await streamPost(`/features/${name}/run/${step}`, {
|
|
464
|
+
description: userMessage,
|
|
465
|
+
apply,
|
|
466
|
+
model: model || undefined,
|
|
467
|
+
history: newHistory.slice(0, -1),
|
|
468
|
+
attachments: attachments.map(a => ({ filename: a.filename, path: a.path })),
|
|
469
|
+
}, createSseCallbacks(currentRequestId));
|
|
470
|
+
|
|
471
|
+
// F4: Check if this request is still current
|
|
472
|
+
if (requestId.current !== currentRequestId) return;
|
|
473
|
+
|
|
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
|
+
}
|
|
479
|
+
|
|
480
|
+
if (res.ok) {
|
|
481
|
+
setResult('Step completed.');
|
|
482
|
+
if (onDone) onDone();
|
|
483
|
+
} else {
|
|
484
|
+
setErr(res.error);
|
|
485
|
+
}
|
|
185
486
|
setRunning(false);
|
|
186
487
|
}
|
|
187
488
|
|
|
188
489
|
async function iterate() {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
490
|
+
if (!instructions.trim() || running) return;
|
|
491
|
+
|
|
492
|
+
const msgId = Date.now();
|
|
493
|
+
const iterationInstructions = instructions;
|
|
494
|
+
setMessages(prev => [...prev, { id: msgId, role: 'user', content: iterationInstructions }].slice(-MAX_MESSAGES));
|
|
495
|
+
setRunning(true);
|
|
496
|
+
setResult(null);
|
|
497
|
+
setErr(null);
|
|
498
|
+
setVerboseLogs([]);
|
|
499
|
+
agentBuffer.current = '';
|
|
500
|
+
|
|
501
|
+
const currentRequestId = ++requestId.current;
|
|
502
|
+
// F7: Pass history in iterate mode too
|
|
503
|
+
const newHistory = [...history, { role: 'user', content: iterationInstructions }];
|
|
504
|
+
setHistory(newHistory);
|
|
505
|
+
|
|
506
|
+
const res = await streamPost(`/features/${name}/iterate/${step}`, {
|
|
507
|
+
instructions: iterationInstructions,
|
|
508
|
+
apply,
|
|
509
|
+
model: model || undefined,
|
|
510
|
+
history: newHistory.slice(0, -1),
|
|
511
|
+
attachments: attachments.map(a => ({ filename: a.filename, path: a.path })),
|
|
512
|
+
}, createSseCallbacks(currentRequestId));
|
|
513
|
+
|
|
514
|
+
if (requestId.current !== currentRequestId) return;
|
|
515
|
+
|
|
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
|
+
}
|
|
520
|
+
|
|
521
|
+
if (res.ok) {
|
|
522
|
+
setResult('Iteration completed.');
|
|
523
|
+
setInstructions('');
|
|
524
|
+
if (onDone) onDone();
|
|
525
|
+
} else {
|
|
526
|
+
setErr(res.error);
|
|
527
|
+
}
|
|
193
528
|
setRunning(false);
|
|
194
529
|
}
|
|
195
530
|
|
|
531
|
+
// F14: Better reset with state tracking
|
|
196
532
|
async function reset() {
|
|
197
|
-
|
|
198
|
-
|
|
533
|
+
setResetting(true);
|
|
534
|
+
setErr(null);
|
|
535
|
+
try {
|
|
536
|
+
await api.post(`/features/${name}/reset/${step}`);
|
|
537
|
+
requestId.current++;
|
|
538
|
+
setMessages([]);
|
|
539
|
+
setVerboseLogs([]);
|
|
540
|
+
setHistory([]);
|
|
541
|
+
setResult(null);
|
|
542
|
+
if (onDone) onDone();
|
|
543
|
+
} catch (e) {
|
|
544
|
+
setErr(`Reset failed: ${e.message}`);
|
|
545
|
+
} finally {
|
|
546
|
+
setResetting(false);
|
|
547
|
+
}
|
|
199
548
|
}
|
|
200
549
|
|
|
550
|
+
const handleKeyDown = (e, submitFn) => {
|
|
551
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
552
|
+
e.preventDefault();
|
|
553
|
+
submitFn();
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
|
|
201
557
|
return React.createElement('div', { className: 'space-y-3' },
|
|
202
558
|
|
|
559
|
+
// --- Chat log ---
|
|
560
|
+
React.createElement(ChatLog, { messages }),
|
|
561
|
+
|
|
562
|
+
// --- Attachments zone ---
|
|
563
|
+
React.createElement(AttachmentZone, {
|
|
564
|
+
feature: name,
|
|
565
|
+
attachments,
|
|
566
|
+
onUpload: handleAttachmentUpload,
|
|
567
|
+
onRemove: handleAttachmentRemove,
|
|
568
|
+
}),
|
|
569
|
+
|
|
203
570
|
// --- Run block (when step is not done) ---
|
|
204
571
|
!isDone && React.createElement('div', { className: 'bg-slate-900 border border-aia-border rounded p-4 space-y-3' },
|
|
205
572
|
React.createElement('h4', { className: 'text-sm font-semibold text-emerald-400' }, `Run: ${step}`),
|
|
206
|
-
React.createElement('
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
placeholder: '
|
|
573
|
+
React.createElement('textarea', {
|
|
574
|
+
value: inputText,
|
|
575
|
+
onChange: e => setInputText(e.target.value),
|
|
576
|
+
onKeyDown: e => handleKeyDown(e, handleSend),
|
|
577
|
+
placeholder: 'Describe what you want... (Enter to send, Shift+Enter for newline)',
|
|
211
578
|
disabled: running,
|
|
212
|
-
|
|
579
|
+
rows: 3,
|
|
580
|
+
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',
|
|
213
581
|
}),
|
|
582
|
+
inputText.length > 0 && React.createElement('span', { className: 'text-xs text-slate-500' }, `${inputText.length} characters`),
|
|
214
583
|
React.createElement('div', { className: 'flex items-center gap-4 flex-wrap' },
|
|
215
|
-
React.createElement(ModelSelect, {
|
|
216
|
-
React.createElement('label', { className: 'flex items-center gap-2 text-xs text-slate-400 cursor-pointer' },
|
|
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' },
|
|
217
586
|
React.createElement('input', { type: 'checkbox', checked: apply, onChange: e => setApply(e.target.checked), disabled: running, className: 'rounded' }),
|
|
218
|
-
'Agent mode
|
|
587
|
+
'Agent mode'
|
|
219
588
|
),
|
|
220
589
|
React.createElement('button', {
|
|
221
|
-
onClick:
|
|
590
|
+
onClick: handleSend, disabled: running || !inputText.trim(),
|
|
222
591
|
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',
|
|
223
|
-
}, running ? 'Running...' : '
|
|
592
|
+
}, running ? 'Running...' : 'Send'),
|
|
224
593
|
),
|
|
225
594
|
),
|
|
226
595
|
|
|
@@ -235,30 +604,40 @@ function RunPanel({ name, step, stepStatus, onDone }) {
|
|
|
235
604
|
React.createElement('textarea', {
|
|
236
605
|
value: instructions,
|
|
237
606
|
onChange: e => setInstructions(e.target.value),
|
|
238
|
-
|
|
607
|
+
onKeyDown: e => handleKeyDown(e, iterate),
|
|
608
|
+
placeholder: 'e.g. "Add error handling for edge cases"... (Enter to send, Shift+Enter for newline)',
|
|
239
609
|
disabled: running,
|
|
240
610
|
rows: 3,
|
|
241
|
-
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',
|
|
611
|
+
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',
|
|
242
612
|
}),
|
|
613
|
+
instructions.length > 0 && React.createElement('span', { className: 'text-xs text-slate-500' }, `${instructions.length} characters`),
|
|
243
614
|
React.createElement('div', { className: 'flex items-center gap-4 flex-wrap' },
|
|
244
|
-
React.createElement(ModelSelect, {
|
|
245
|
-
React.createElement('label', { className: 'flex items-center gap-2 text-xs text-slate-400 cursor-pointer' },
|
|
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' },
|
|
246
617
|
React.createElement('input', { type: 'checkbox', checked: apply, onChange: e => setApply(e.target.checked), disabled: running, className: 'rounded' }),
|
|
247
|
-
'Agent mode
|
|
618
|
+
'Agent mode'
|
|
248
619
|
),
|
|
249
620
|
React.createElement('button', {
|
|
250
621
|
onClick: iterate, disabled: running || !instructions.trim(),
|
|
251
622
|
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',
|
|
252
623
|
}, running ? 'Iterating...' : 'Iterate'),
|
|
253
624
|
React.createElement('button', {
|
|
254
|
-
onClick: reset, disabled: running,
|
|
255
|
-
className: 'text-slate-500 hover:text-slate-300 text-xs',
|
|
256
|
-
}, 'Reset to pending'),
|
|
625
|
+
onClick: reset, disabled: running || resetting,
|
|
626
|
+
className: 'text-slate-500 hover:text-slate-300 text-xs disabled:opacity-40',
|
|
627
|
+
}, resetting ? 'Resetting...' : 'Reset to pending'),
|
|
257
628
|
),
|
|
629
|
+
// Step guidance for completed steps
|
|
630
|
+
React.createElement(StepGuidance, { step, feature: name }),
|
|
258
631
|
),
|
|
259
632
|
|
|
260
|
-
// ---
|
|
261
|
-
React.createElement(
|
|
633
|
+
// --- Verbose panel (stderr logs) ---
|
|
634
|
+
React.createElement(VerbosePanel, {
|
|
635
|
+
logs: verboseLogs,
|
|
636
|
+
expanded: verboseExpanded,
|
|
637
|
+
onToggle: () => setVerboseExpanded(!verboseExpanded),
|
|
638
|
+
}),
|
|
639
|
+
|
|
640
|
+
// --- Results ---
|
|
262
641
|
result && React.createElement('p', { className: 'text-emerald-400 text-xs' }, result),
|
|
263
642
|
err && React.createElement('p', { className: 'text-red-400 text-xs' }, err),
|
|
264
643
|
);
|
|
@@ -269,16 +648,104 @@ export function FeatureDetail({ name }) {
|
|
|
269
648
|
const [loading, setLoading] = React.useState(true);
|
|
270
649
|
const [activeFile, setActiveFile] = React.useState('init.md');
|
|
271
650
|
const [activeStep, setActiveStep] = React.useState(null);
|
|
651
|
+
const [showInitPanel, setShowInitPanel] = React.useState(false);
|
|
652
|
+
const [selectedFlow, setSelectedFlow] = React.useState(null);
|
|
653
|
+
const [fileVersion, setFileVersion] = React.useState(0);
|
|
272
654
|
|
|
273
|
-
async function load() {
|
|
655
|
+
async function load(checkInitPanel = true) {
|
|
274
656
|
try {
|
|
275
657
|
const data = await api.get(`/features/${name}`);
|
|
276
658
|
setFeature(data);
|
|
659
|
+
const steps = data.steps || {};
|
|
660
|
+
|
|
661
|
+
// Load persisted flow from status
|
|
662
|
+
if (data.flow && !selectedFlow) {
|
|
663
|
+
setSelectedFlow(data.flow);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Check if no steps have been started and init.md is empty (only on initial load)
|
|
667
|
+
if (checkInitPanel) {
|
|
668
|
+
const allPending = Object.values(steps).every(s => s === 'pending');
|
|
669
|
+
const persistedFlow = data.flow || selectedFlow;
|
|
670
|
+
|
|
671
|
+
if (allPending && !persistedFlow) {
|
|
672
|
+
// Check if init.md has content (enriched)
|
|
673
|
+
try {
|
|
674
|
+
const initFile = await api.get(`/features/${name}/files/init.md`);
|
|
675
|
+
const content = initFile.content || '';
|
|
676
|
+
// If init.md has been enriched (contains ## Summary which is added by the agent), don't show InitPanel
|
|
677
|
+
// The default template only has ## Description, ## Existing specs, ## Constraints
|
|
678
|
+
const isEnriched = content.includes('## Summary') || content.includes('## Problem');
|
|
679
|
+
if (!isEnriched) {
|
|
680
|
+
setShowInitPanel(true);
|
|
681
|
+
} else {
|
|
682
|
+
// Auto-select first pending step based on flow
|
|
683
|
+
const flow = persistedFlow || 'full';
|
|
684
|
+
const firstStep = flow === 'quick' ? 'dev-plan' : 'brief';
|
|
685
|
+
if (!activeStep) {
|
|
686
|
+
setActiveStep(firstStep);
|
|
687
|
+
setActiveFile(`${firstStep}.md`);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
} catch {
|
|
691
|
+
setShowInitPanel(true);
|
|
692
|
+
}
|
|
693
|
+
} else if (!activeStep) {
|
|
694
|
+
// Auto-select current step or first pending based on flow
|
|
695
|
+
const flow = persistedFlow || 'full';
|
|
696
|
+
const currentStep = data.current_step;
|
|
697
|
+
if (currentStep) {
|
|
698
|
+
setActiveStep(currentStep);
|
|
699
|
+
setActiveFile(`${currentStep}.md`);
|
|
700
|
+
} else {
|
|
701
|
+
// Use flow to determine first step
|
|
702
|
+
const firstStep = flow === 'quick' ? 'dev-plan' : 'brief';
|
|
703
|
+
const stepStatus = steps[firstStep];
|
|
704
|
+
if (stepStatus === 'pending') {
|
|
705
|
+
setActiveStep(firstStep);
|
|
706
|
+
setActiveFile(`${firstStep}.md`);
|
|
707
|
+
} else {
|
|
708
|
+
const firstPending = Object.entries(steps).find(([_, status]) => status === 'pending');
|
|
709
|
+
if (firstPending) {
|
|
710
|
+
setActiveStep(firstPending[0]);
|
|
711
|
+
setActiveFile(`${firstPending[0]}.md`);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
277
717
|
} catch {}
|
|
278
718
|
setLoading(false);
|
|
279
719
|
}
|
|
280
720
|
|
|
281
|
-
React.useEffect(() => { load(); }, [name]);
|
|
721
|
+
React.useEffect(() => { load(true); }, [name]);
|
|
722
|
+
|
|
723
|
+
const handleFlowSelected = async (flow) => {
|
|
724
|
+
// Persist flow to status.yaml
|
|
725
|
+
try {
|
|
726
|
+
await api.patch(`/features/${name}/flow`, { flow });
|
|
727
|
+
} catch (e) {
|
|
728
|
+
console.error('Failed to save flow:', e);
|
|
729
|
+
}
|
|
730
|
+
setSelectedFlow(flow);
|
|
731
|
+
setShowInitPanel(false);
|
|
732
|
+
// Select the first step based on flow type
|
|
733
|
+
const firstStep = flow === 'quick' ? 'dev-plan' : 'brief';
|
|
734
|
+
setActiveStep(firstStep);
|
|
735
|
+
setActiveFile(`${firstStep}.md`);
|
|
736
|
+
// Refresh file viewer
|
|
737
|
+
setFileVersion(v => v + 1);
|
|
738
|
+
load(false); // Reload without checking init panel
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
const handleFlowChanged = (newFlow) => {
|
|
742
|
+
setSelectedFlow(newFlow);
|
|
743
|
+
// Select the first step based on new flow type
|
|
744
|
+
const firstStep = newFlow === 'quick' ? 'dev-plan' : 'brief';
|
|
745
|
+
setActiveStep(firstStep);
|
|
746
|
+
setActiveFile(`${firstStep}.md`);
|
|
747
|
+
load(false);
|
|
748
|
+
};
|
|
282
749
|
|
|
283
750
|
if (loading) return React.createElement('p', { className: 'text-slate-500' }, 'Loading...');
|
|
284
751
|
if (!feature) return React.createElement('p', { className: 'text-red-400' }, `Feature "${name}" not found.`);
|
|
@@ -293,8 +760,26 @@ export function FeatureDetail({ name }) {
|
|
|
293
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),
|
|
294
761
|
),
|
|
295
762
|
|
|
296
|
-
//
|
|
297
|
-
React.createElement(
|
|
763
|
+
// Init panel (when no steps started)
|
|
764
|
+
showInitPanel && React.createElement(InitPanel, {
|
|
765
|
+
name,
|
|
766
|
+
onFlowSelected: handleFlowSelected,
|
|
767
|
+
onCancel: () => setShowInitPanel(false),
|
|
768
|
+
onEnriched: () => {
|
|
769
|
+
setActiveFile('init.md');
|
|
770
|
+
setFileVersion(v => v + 1);
|
|
771
|
+
},
|
|
772
|
+
}),
|
|
773
|
+
|
|
774
|
+
// Flow selector (only show if not showing init panel)
|
|
775
|
+
!showInitPanel && React.createElement(FlowSelector, {
|
|
776
|
+
name,
|
|
777
|
+
currentFlow: selectedFlow || 'full',
|
|
778
|
+
onFlowChanged: handleFlowChanged,
|
|
779
|
+
}),
|
|
780
|
+
|
|
781
|
+
// Worktrunk panel
|
|
782
|
+
!showInitPanel && React.createElement(WorktrunkPanel, { featureName: name }),
|
|
298
783
|
|
|
299
784
|
// Pipeline
|
|
300
785
|
React.createElement('div', { className: 'flex flex-wrap gap-2' },
|
|
@@ -310,7 +795,7 @@ export function FeatureDetail({ name }) {
|
|
|
310
795
|
),
|
|
311
796
|
|
|
312
797
|
// Run / Iterate panel
|
|
313
|
-
activeStep && React.createElement(RunPanel, { name, step: activeStep, stepStatus: steps[activeStep], onDone: load }),
|
|
798
|
+
activeStep && React.createElement(RunPanel, { name, step: activeStep, stepStatus: steps[activeStep], onDone: () => { setFileVersion(v => v + 1); load(false); } }),
|
|
314
799
|
|
|
315
800
|
// File tabs
|
|
316
801
|
React.createElement('div', { className: 'flex gap-1 border-b border-aia-border' },
|
|
@@ -324,6 +809,6 @@ export function FeatureDetail({ name }) {
|
|
|
324
809
|
),
|
|
325
810
|
|
|
326
811
|
// Editor
|
|
327
|
-
activeFile && React.createElement(FileEditor, { key: `${name}-${activeFile}`, name, filename: activeFile, onSaved: load }),
|
|
812
|
+
activeFile && React.createElement(FileEditor, { key: `${name}-${activeFile}-${fileVersion}`, name, filename: activeFile, onSaved: () => { setFileVersion(v => v + 1); load(false); } }),
|
|
328
813
|
);
|
|
329
814
|
}
|