@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.
@@ -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 QuickRunButton({ name, onDone }) {
69
- const [running, setRunning] = React.useState(false);
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
- if (res.ok) {
86
- if (onDone) onDone();
87
- } else {
88
- setErr(res.error);
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
- setRunning(false);
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: 'bg-slate-900 border border-amber-500/30 rounded p-4 space-y-3' },
101
- React.createElement('h4', { className: 'text-sm font-semibold text-amber-400' }, 'Quick Ticket'),
102
- React.createElement('p', { className: 'text-xs text-slate-500' }, 'Skips early steps, runs dev-plan \u2192 implement \u2192 review'),
103
- React.createElement('input', {
104
- type: 'text',
105
- value: description,
106
- onChange: e => setDescription(e.target.value),
107
- placeholder: 'Optional description...',
108
- disabled: running,
109
- className: 'w-full bg-aia-card border border-aia-border rounded px-3 py-1.5 text-sm text-slate-200 placeholder-slate-500 focus:border-amber-400 focus:outline-none',
110
- }),
111
- React.createElement('div', { className: 'flex gap-2' },
112
- React.createElement('button', {
113
- onClick: run,
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 ModelSelect({ step, model, onChange, disabled }) {
142
- const [models, setModels] = React.useState([]);
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
- api.get('/models').then(data => {
146
- const stepModels = data[step] || [];
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 (models.length <= 1) return null;
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 [description, setDescription] = React.useState('');
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 [logs, setLogs] = React.useState([]);
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 sseCallbacks = {
176
- onLog: (text) => setLogs(prev => [...prev, text]),
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
- async function run() {
181
- setRunning(true); setResult(null); setErr(null); setLogs([]);
182
- const res = await streamPost(`/features/${name}/run/${step}`, { description, apply, model: model || undefined }, sseCallbacks);
183
- if (res.ok) { setResult('Step completed.'); if (onDone) onDone(); }
184
- else setErr(res.error);
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
- setRunning(true); setResult(null); setErr(null); setLogs([]);
190
- const res = await streamPost(`/features/${name}/iterate/${step}`, { instructions, apply, model: model || undefined }, sseCallbacks);
191
- if (res.ok) { setResult('Iteration completed.'); setInstructions(''); if (onDone) onDone(); }
192
- else setErr(res.error);
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
- try { await api.post(`/features/${name}/reset/${step}`); if (onDone) onDone(); }
198
- catch (e) { setErr(e.message); }
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('input', {
207
- type: 'text',
208
- value: description,
209
- onChange: e => setDescription(e.target.value),
210
- placeholder: 'Optional description...',
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
- className: 'w-full bg-aia-card border border-aia-border rounded px-3 py-1.5 text-sm text-slate-200 placeholder-slate-500 focus:border-emerald-400 focus:outline-none',
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, { step, model, onChange: setModel, disabled: running }),
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 (--apply)'
587
+ 'Agent mode'
219
588
  ),
220
589
  React.createElement('button', {
221
- onClick: run, disabled: running,
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...' : 'Run Step'),
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
- placeholder: 'e.g. "Add error handling for edge cases", "Focus more on mobile", "Split into smaller functions"...',
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, { step, model, onChange: setModel, disabled: running }),
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 (--apply)'
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
- // --- Shared log viewer + results ---
261
- React.createElement(LogViewer, { logs }),
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
- // Quick run
297
- React.createElement(QuickRunButton, { name, onDone: load }),
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
  }