@bamptee/aia-code 2.0.1 → 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,12 +194,53 @@ 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
 
204
+ function VerbosePanel({ logs, expanded, onToggle }) {
205
+ if (!logs.length) return null;
206
+ return React.createElement('div', { className: 'mt-2' },
207
+ React.createElement('button', {
208
+ onClick: onToggle,
209
+ 'aria-expanded': expanded, // F13: Accessibility
210
+ 'aria-label': `Toggle verbose logs, ${logs.length} lines`,
211
+ className: 'text-xs text-slate-500 hover:text-slate-300 flex items-center gap-1',
212
+ }, expanded ? '\u25BC' : '\u25B6', `Verbose (${logs.length} lines)`),
213
+ expanded && React.createElement('pre', {
214
+ className: 'bg-black/70 border border-slate-700 rounded p-2 mt-1 text-xs text-slate-500 overflow-auto max-h-48 whitespace-pre-wrap',
215
+ role: 'log',
216
+ 'aria-label': 'Verbose output',
217
+ }, logs.join(''))
218
+ );
219
+ }
220
+
221
+ function ChatLog({ messages }) {
222
+ const ref = React.useRef(null);
223
+ React.useEffect(() => {
224
+ if (ref.current) ref.current.scrollTop = ref.current.scrollHeight;
225
+ }, [messages]);
226
+
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
+
141
244
  function ModelSelect({ model, onChange, disabled }) {
142
245
  const [models, setModels] = React.useState([]);
143
246
 
@@ -156,66 +259,337 @@ function ModelSelect({ model, onChange, disabled }) {
156
259
  );
157
260
  }
158
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
+
159
386
  function RunPanel({ name, step, stepStatus, onDone }) {
160
387
  const isDone = stepStatus === 'done';
161
- const [description, setDescription] = React.useState('');
388
+ const [inputText, setInputText] = React.useState('');
162
389
  const [instructions, setInstructions] = React.useState('');
163
390
  const [model, setModel] = React.useState('');
164
391
  const [apply, setApply] = React.useState(false);
165
392
  const [running, setRunning] = React.useState(false);
166
393
  const [result, setResult] = React.useState(null);
167
394
  const [err, setErr] = React.useState(null);
168
- 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
+ };
169
425
 
170
- const sseCallbacks = {
171
- onLog: (text) => setLogs(prev => [...prev, text]),
172
- onStatus: (data) => setLogs(prev => [...prev, `[${data.status}] ${data.step || ''}\n`]),
426
+ const handleAttachmentRemove = (filename) => {
427
+ setAttachments(prev => prev.filter(a => a.filename !== filename));
173
428
  };
174
429
 
175
- async function run() {
176
- setRunning(true); setResult(null); setErr(null); setLogs([]);
177
- const res = await streamPost(`/features/${name}/run/${step}`, { description, apply, model: model || undefined }, sseCallbacks);
178
- if (res.ok) { setResult('Step completed.'); if (onDone) onDone(); }
179
- 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
+ }
180
486
  setRunning(false);
181
487
  }
182
488
 
183
489
  async function iterate() {
184
- setRunning(true); setResult(null); setErr(null); setLogs([]);
185
- const res = await streamPost(`/features/${name}/iterate/${step}`, { instructions, apply, model: model || undefined }, sseCallbacks);
186
- if (res.ok) { setResult('Iteration completed.'); setInstructions(''); if (onDone) onDone(); }
187
- 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
+ }
188
528
  setRunning(false);
189
529
  }
190
530
 
531
+ // F14: Better reset with state tracking
191
532
  async function reset() {
192
- try { await api.post(`/features/${name}/reset/${step}`); if (onDone) onDone(); }
193
- 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
+ }
194
548
  }
195
549
 
550
+ const handleKeyDown = (e, submitFn) => {
551
+ if (e.key === 'Enter' && !e.shiftKey) {
552
+ e.preventDefault();
553
+ submitFn();
554
+ }
555
+ };
556
+
196
557
  return React.createElement('div', { className: 'space-y-3' },
197
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
+
198
570
  // --- Run block (when step is not done) ---
199
571
  !isDone && React.createElement('div', { className: 'bg-slate-900 border border-aia-border rounded p-4 space-y-3' },
200
572
  React.createElement('h4', { className: 'text-sm font-semibold text-emerald-400' }, `Run: ${step}`),
201
- React.createElement('input', {
202
- type: 'text',
203
- value: description,
204
- onChange: e => setDescription(e.target.value),
205
- 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)',
206
578
  disabled: running,
207
- 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',
208
581
  }),
582
+ inputText.length > 0 && React.createElement('span', { className: 'text-xs text-slate-500' }, `${inputText.length} characters`),
209
583
  React.createElement('div', { className: 'flex items-center gap-4 flex-wrap' },
210
584
  React.createElement(ModelSelect, { model, onChange: setModel, disabled: running }),
211
- React.createElement('label', { className: 'flex items-center gap-2 text-xs text-slate-400 cursor-pointer' },
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' },
212
586
  React.createElement('input', { type: 'checkbox', checked: apply, onChange: e => setApply(e.target.checked), disabled: running, className: 'rounded' }),
213
- 'Agent mode (--apply)'
587
+ 'Agent mode'
214
588
  ),
215
589
  React.createElement('button', {
216
- onClick: run, disabled: running,
590
+ onClick: handleSend, disabled: running || !inputText.trim(),
217
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',
218
- }, running ? 'Running...' : 'Run Step'),
592
+ }, running ? 'Running...' : 'Send'),
219
593
  ),
220
594
  ),
221
595
 
@@ -230,30 +604,40 @@ function RunPanel({ name, step, stepStatus, onDone }) {
230
604
  React.createElement('textarea', {
231
605
  value: instructions,
232
606
  onChange: e => setInstructions(e.target.value),
233
- 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)',
234
609
  disabled: running,
235
610
  rows: 3,
236
- 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',
237
612
  }),
613
+ instructions.length > 0 && React.createElement('span', { className: 'text-xs text-slate-500' }, `${instructions.length} characters`),
238
614
  React.createElement('div', { className: 'flex items-center gap-4 flex-wrap' },
239
615
  React.createElement(ModelSelect, { model, onChange: setModel, disabled: running }),
240
- React.createElement('label', { className: 'flex items-center gap-2 text-xs text-slate-400 cursor-pointer' },
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' },
241
617
  React.createElement('input', { type: 'checkbox', checked: apply, onChange: e => setApply(e.target.checked), disabled: running, className: 'rounded' }),
242
- 'Agent mode (--apply)'
618
+ 'Agent mode'
243
619
  ),
244
620
  React.createElement('button', {
245
621
  onClick: iterate, disabled: running || !instructions.trim(),
246
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',
247
623
  }, running ? 'Iterating...' : 'Iterate'),
248
624
  React.createElement('button', {
249
- onClick: reset, disabled: running,
250
- className: 'text-slate-500 hover:text-slate-300 text-xs',
251
- }, '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'),
252
628
  ),
629
+ // Step guidance for completed steps
630
+ React.createElement(StepGuidance, { step, feature: name }),
253
631
  ),
254
632
 
255
- // --- Shared log viewer + results ---
256
- 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 ---
257
641
  result && React.createElement('p', { className: 'text-emerald-400 text-xs' }, result),
258
642
  err && React.createElement('p', { className: 'text-red-400 text-xs' }, err),
259
643
  );
@@ -264,16 +648,104 @@ export function FeatureDetail({ name }) {
264
648
  const [loading, setLoading] = React.useState(true);
265
649
  const [activeFile, setActiveFile] = React.useState('init.md');
266
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);
267
654
 
268
- async function load() {
655
+ async function load(checkInitPanel = true) {
269
656
  try {
270
657
  const data = await api.get(`/features/${name}`);
271
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
+ }
272
717
  } catch {}
273
718
  setLoading(false);
274
719
  }
275
720
 
276
- 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
+ };
277
749
 
278
750
  if (loading) return React.createElement('p', { className: 'text-slate-500' }, 'Loading...');
279
751
  if (!feature) return React.createElement('p', { className: 'text-red-400' }, `Feature "${name}" not found.`);
@@ -288,8 +760,26 @@ export function FeatureDetail({ name }) {
288
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),
289
761
  ),
290
762
 
291
- // Quick run
292
- 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 }),
293
783
 
294
784
  // Pipeline
295
785
  React.createElement('div', { className: 'flex flex-wrap gap-2' },
@@ -305,7 +795,7 @@ export function FeatureDetail({ name }) {
305
795
  ),
306
796
 
307
797
  // Run / Iterate panel
308
- 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); } }),
309
799
 
310
800
  // File tabs
311
801
  React.createElement('div', { className: 'flex gap-1 border-b border-aia-border' },
@@ -319,6 +809,6 @@ export function FeatureDetail({ name }) {
319
809
  ),
320
810
 
321
811
  // Editor
322
- 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); } }),
323
813
  );
324
814
  }