@agile-vibe-coding/avc 0.3.4 → 0.4.0
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 +86 -12
- package/cli/agents/code-implementer.md +33 -46
- package/cli/init.js +4 -3
- package/cli/llm-claude.js +72 -0
- package/cli/llm-gemini.js +76 -0
- package/cli/llm-local.js +52 -0
- package/cli/llm-openai.js +52 -0
- package/cli/llm-provider.js +12 -0
- package/cli/llm-xiaomi.js +51 -0
- package/cli/seed-processor.js +31 -0
- package/cli/worktree-runner.js +268 -26
- package/cli/worktree-tools.js +322 -0
- package/kanban/client/dist/assets/index-BSm2Zo5j.js +380 -0
- package/kanban/client/dist/assets/index-BevZLADh.css +1 -0
- package/kanban/client/dist/index.html +2 -2
- package/kanban/client/src/App.jsx +37 -5
- package/kanban/client/src/components/ceremony/RunModal.jsx +329 -0
- package/kanban/client/src/components/ceremony/SeedModal.jsx +2 -2
- package/kanban/client/src/components/kanban/CardDetailModal.jsx +95 -21
- package/kanban/client/src/components/kanban/RunButton.jsx +34 -153
- package/kanban/client/src/components/process/ProcessMonitorBar.jsx +4 -0
- package/kanban/client/src/lib/api.js +10 -0
- package/kanban/client/src/store/filterStore.js +10 -3
- package/kanban/client/src/store/runStore.js +103 -0
- package/kanban/server/routes/work-items.js +101 -2
- package/kanban/server/workers/run-task-worker.js +60 -11
- package/package.json +1 -1
- package/kanban/client/dist/assets/index-BfLDUxPS.js +0 -353
- package/kanban/client/dist/assets/index-C7W_e4ik.css +0 -1
|
@@ -169,12 +169,12 @@ export function CardDetailModal({ workItem, open, onOpenChange, onNavigate, onIt
|
|
|
169
169
|
loadFullDetails();
|
|
170
170
|
};
|
|
171
171
|
|
|
172
|
-
// Load full details when modal opens
|
|
172
|
+
// Load full details when modal opens or work item status changes
|
|
173
173
|
useEffect(() => {
|
|
174
174
|
if (open && workItem) {
|
|
175
175
|
loadFullDetails();
|
|
176
176
|
}
|
|
177
|
-
}, [open, workItem?.id]);
|
|
177
|
+
}, [open, workItem?.id, workItem?.status]);
|
|
178
178
|
|
|
179
179
|
// Fall back to 'details' tab if no doc.md available
|
|
180
180
|
useEffect(() => {
|
|
@@ -304,7 +304,7 @@ export function CardDetailModal({ workItem, open, onOpenChange, onNavigate, onIt
|
|
|
304
304
|
<FileText className="w-4 h-4 mr-2" />
|
|
305
305
|
Details
|
|
306
306
|
</TabsTrigger>
|
|
307
|
-
{fullDetails?.children && fullDetails.children.length > 0 && (
|
|
307
|
+
{fullDetails?.children && fullDetails.children.length > 0 && workItem.type !== 'task' && (
|
|
308
308
|
<TabsTrigger value="children">
|
|
309
309
|
<Users className="w-4 h-4 mr-2" />
|
|
310
310
|
Children ({fullDetails.children.length})
|
|
@@ -355,7 +355,7 @@ export function CardDetailModal({ workItem, open, onOpenChange, onNavigate, onIt
|
|
|
355
355
|
)}
|
|
356
356
|
{/* Run button — tasks that are planned, ready, or failed */}
|
|
357
357
|
{workItem.type === 'task' && (workItem.status === 'planned' || workItem.status === 'ready' || workItem.status === 'failed') && (
|
|
358
|
-
<RunButton taskId={workItem.id}
|
|
358
|
+
<RunButton taskId={workItem.id} taskName={workItem.name} />
|
|
359
359
|
)}
|
|
360
360
|
<button
|
|
361
361
|
onClick={() => setRefineOpen(true)}
|
|
@@ -482,33 +482,107 @@ export function CardDetailModal({ workItem, open, onOpenChange, onNavigate, onIt
|
|
|
482
482
|
? fullDetails?.features
|
|
483
483
|
: fullDetails?.acceptance;
|
|
484
484
|
if (!items || items.length === 0) return null;
|
|
485
|
+
const acStatus = fullDetails?.acceptanceStatus;
|
|
486
|
+
const hasPassed = acStatus?.some(s => s.passed);
|
|
485
487
|
return (
|
|
486
488
|
<div>
|
|
487
489
|
<div className="flex items-center gap-2 text-sm font-semibold text-slate-700 mb-2">
|
|
488
490
|
<ListChecks className="w-4 h-4" />
|
|
489
491
|
<span>Acceptance Criteria</span>
|
|
490
|
-
|
|
491
|
-
<
|
|
492
|
-
|
|
493
|
-
|
|
492
|
+
{hasPassed ? (
|
|
493
|
+
<span className="ml-auto text-xs font-normal text-green-600">verified by tests</span>
|
|
494
|
+
) : (
|
|
495
|
+
<span className="ml-auto flex items-center gap-1 text-xs font-normal text-slate-400">
|
|
496
|
+
<Lock className="w-3 h-3" />
|
|
497
|
+
updated by tests
|
|
498
|
+
</span>
|
|
499
|
+
)}
|
|
494
500
|
</div>
|
|
495
501
|
<ul className="space-y-1.5">
|
|
496
|
-
{items.map((ac, idx) =>
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
502
|
+
{items.map((ac, idx) => {
|
|
503
|
+
const passed = acStatus?.[idx]?.passed;
|
|
504
|
+
return (
|
|
505
|
+
<li key={idx} className="flex items-start gap-2.5 text-sm text-slate-700">
|
|
506
|
+
<input
|
|
507
|
+
type="checkbox"
|
|
508
|
+
checked={!!passed}
|
|
509
|
+
readOnly
|
|
510
|
+
disabled
|
|
511
|
+
className={`mt-0.5 h-4 w-4 flex-shrink-0 rounded border-slate-300 cursor-not-allowed ${passed ? 'text-green-600 opacity-100' : 'text-indigo-600 opacity-60'}`}
|
|
512
|
+
/>
|
|
513
|
+
<span className={`leading-snug ${passed ? 'text-green-700' : ''}`}>{ac}</span>
|
|
514
|
+
</li>
|
|
515
|
+
);
|
|
516
|
+
})}
|
|
507
517
|
</ul>
|
|
508
518
|
</div>
|
|
509
519
|
);
|
|
510
520
|
})()}
|
|
511
521
|
|
|
522
|
+
{/* Test Results (shown after Run ceremony for review) */}
|
|
523
|
+
{fullDetails?.testResults && (
|
|
524
|
+
<div>
|
|
525
|
+
<div className="flex items-center gap-2 text-sm font-semibold text-slate-700 mb-2">
|
|
526
|
+
<span>{fullDetails.testResults.passed ? '✅' : '❌'}</span>
|
|
527
|
+
<span>Test Results</span>
|
|
528
|
+
<span className={`ml-auto text-xs font-normal ${fullDetails.testResults.passed ? 'text-green-600' : 'text-red-600'}`}>
|
|
529
|
+
{fullDetails.testResults.passed ? 'All tests passed' : 'Tests failed'}
|
|
530
|
+
</span>
|
|
531
|
+
</div>
|
|
532
|
+
{fullDetails.testResults.command && (
|
|
533
|
+
<p className="text-xs text-slate-500 mb-1 font-mono">$ {fullDetails.testResults.command}</p>
|
|
534
|
+
)}
|
|
535
|
+
<pre className="bg-slate-900 text-slate-300 text-xs font-mono rounded-lg p-3 max-h-40 overflow-y-auto whitespace-pre-wrap">
|
|
536
|
+
{fullDetails.testResults.output || '(no output)'}
|
|
537
|
+
</pre>
|
|
538
|
+
</div>
|
|
539
|
+
)}
|
|
540
|
+
|
|
541
|
+
{/* Subtasks (inline checklist for task cards) */}
|
|
542
|
+
{workItem.type === 'task' && fullDetails?.children && fullDetails.children.length > 0 && (
|
|
543
|
+
<div>
|
|
544
|
+
<div className="flex items-center gap-2 text-sm font-semibold text-slate-700 mb-2">
|
|
545
|
+
<ListChecks className="w-4 h-4" />
|
|
546
|
+
<span>Subtasks</span>
|
|
547
|
+
<span className="ml-auto text-xs font-normal text-slate-400">
|
|
548
|
+
{fullDetails.children.filter(c => c.status === 'completed').length}/{fullDetails.children.length} done
|
|
549
|
+
</span>
|
|
550
|
+
</div>
|
|
551
|
+
<div className="space-y-2">
|
|
552
|
+
{fullDetails.children.map((child) => {
|
|
553
|
+
const done = child.status === 'completed';
|
|
554
|
+
const fullChild = allItems?.find((i) => i.id === child.id);
|
|
555
|
+
return (
|
|
556
|
+
<button
|
|
557
|
+
key={child.id}
|
|
558
|
+
onClick={() => fullChild && onItemClick?.(fullChild)}
|
|
559
|
+
className={`w-full text-left border rounded-lg p-3 transition-colors cursor-pointer ${done ? 'border-green-200 bg-green-50/50 hover:border-green-300' : 'border-slate-200 hover:border-blue-300 hover:bg-blue-50'}`}
|
|
560
|
+
>
|
|
561
|
+
<div className="flex items-start gap-2.5">
|
|
562
|
+
<input type="checkbox" checked={done} readOnly tabIndex={-1}
|
|
563
|
+
className={`mt-0.5 h-4 w-4 flex-shrink-0 rounded pointer-events-none ${done ? 'text-green-600 opacity-100' : 'text-slate-400 opacity-60'}`}
|
|
564
|
+
/>
|
|
565
|
+
<div className="flex-1 min-w-0">
|
|
566
|
+
<p className={`text-sm font-medium ${done ? 'text-green-700' : 'text-slate-700'}`}>{child.name}</p>
|
|
567
|
+
{child.acceptance && child.acceptance.length > 0 && (
|
|
568
|
+
<ul className="mt-1 space-y-0.5">
|
|
569
|
+
{child.acceptance.map((ac, i) => (
|
|
570
|
+
<li key={i} className="flex items-start gap-1.5 text-xs text-slate-500">
|
|
571
|
+
<span className="flex-shrink-0 mt-0.5">{done ? '✓' : '·'}</span>
|
|
572
|
+
<span>{ac}</span>
|
|
573
|
+
</li>
|
|
574
|
+
))}
|
|
575
|
+
</ul>
|
|
576
|
+
)}
|
|
577
|
+
</div>
|
|
578
|
+
</div>
|
|
579
|
+
</button>
|
|
580
|
+
);
|
|
581
|
+
})}
|
|
582
|
+
</div>
|
|
583
|
+
</div>
|
|
584
|
+
)}
|
|
585
|
+
|
|
512
586
|
{/* Dependencies */}
|
|
513
587
|
{fullDetails?.dependencies && fullDetails.dependencies.length > 0 && (
|
|
514
588
|
<div>
|
|
@@ -527,8 +601,8 @@ export function CardDetailModal({ workItem, open, onOpenChange, onNavigate, onIt
|
|
|
527
601
|
</div>
|
|
528
602
|
</TabsContent>
|
|
529
603
|
|
|
530
|
-
{/* Children Tab */}
|
|
531
|
-
{fullDetails?.children && fullDetails.children.length > 0 && (
|
|
604
|
+
{/* Children Tab (epics/stories only — subtasks shown inline for tasks) */}
|
|
605
|
+
{fullDetails?.children && fullDetails.children.length > 0 && workItem.type !== 'task' && (
|
|
532
606
|
<TabsContent value="children">
|
|
533
607
|
<div className="space-y-2">
|
|
534
608
|
{fullDetails.children.map((child) => {
|
|
@@ -1,162 +1,43 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { getDependencyStatus, startRunTask } from '../../lib/api';
|
|
4
|
-
import { cn } from '../../lib/utils';
|
|
1
|
+
import { Play, Loader2 } from 'lucide-react';
|
|
2
|
+
import { useRunStore } from '../../store/runStore';
|
|
5
3
|
|
|
6
4
|
/**
|
|
7
|
-
* Run Button —
|
|
8
|
-
*
|
|
5
|
+
* Run Button — thin trigger that opens the RunModal.
|
|
6
|
+
* If a run is already active for this task, shows "View Run" instead.
|
|
9
7
|
*/
|
|
10
|
-
export function RunButton({ taskId,
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight;
|
|
21
|
-
}, [progress]);
|
|
22
|
-
|
|
23
|
-
// Listen for WebSocket run-task events dispatched by App.jsx
|
|
24
|
-
useEffect(() => {
|
|
25
|
-
if (!processId) return;
|
|
26
|
-
|
|
27
|
-
const handler = (event) => {
|
|
28
|
-
const msg = event.detail;
|
|
29
|
-
if (!msg || msg.processId !== processId) return;
|
|
30
|
-
|
|
31
|
-
if (msg.type === 'run-task:progress') {
|
|
32
|
-
setProgress(prev => [...prev, msg.message]);
|
|
33
|
-
} else if (msg.type === 'run-task:complete') {
|
|
34
|
-
setState('complete');
|
|
35
|
-
setProgress(prev => [...prev, 'Task complete — code committed.']);
|
|
36
|
-
onStarted?.();
|
|
37
|
-
} else if (msg.type === 'run-task:error') {
|
|
38
|
-
setState('error');
|
|
39
|
-
setError(msg.error || 'Run failed');
|
|
40
|
-
setProgress(prev => [...prev, `Error: ${msg.error}`]);
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
window.addEventListener('avc-ws-message', handler);
|
|
45
|
-
return () => window.removeEventListener('avc-ws-message', handler);
|
|
46
|
-
}, [processId, onStarted]);
|
|
47
|
-
|
|
48
|
-
const handleClick = async () => {
|
|
49
|
-
if (state === 'checking' || state === 'running') return;
|
|
50
|
-
|
|
51
|
-
setState('checking');
|
|
52
|
-
setError(null);
|
|
53
|
-
setBlockers([]);
|
|
54
|
-
setProgress([]);
|
|
55
|
-
|
|
56
|
-
try {
|
|
57
|
-
const depStatus = await getDependencyStatus(taskId);
|
|
58
|
-
if (!depStatus.ready) {
|
|
59
|
-
setState('blocked');
|
|
60
|
-
setBlockers(depStatus.blockers || []);
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
setState('running');
|
|
65
|
-
setShowLog(true);
|
|
66
|
-
setProgress(['Starting task implementation...']);
|
|
67
|
-
const result = await startRunTask(taskId);
|
|
68
|
-
setProcessId(result.processId);
|
|
69
|
-
setProgress(prev => [...prev, `Process started: ${result.processId}`]);
|
|
70
|
-
} catch (err) {
|
|
71
|
-
setError(err.message);
|
|
72
|
-
setState('error');
|
|
8
|
+
export function RunButton({ taskId, taskName }) {
|
|
9
|
+
const { sessions, openModal, reopenModal } = useRunStore();
|
|
10
|
+
const session = sessions[taskId];
|
|
11
|
+
const isActive = session && (session.status === 'running' || session.status === 'checking');
|
|
12
|
+
|
|
13
|
+
const handleClick = () => {
|
|
14
|
+
if (isActive) {
|
|
15
|
+
reopenModal(taskId);
|
|
16
|
+
} else {
|
|
17
|
+
openModal(taskId, taskName || taskId);
|
|
73
18
|
}
|
|
74
19
|
};
|
|
75
20
|
|
|
76
|
-
const dismiss = () => {
|
|
77
|
-
setState('idle');
|
|
78
|
-
setBlockers([]);
|
|
79
|
-
setError(null);
|
|
80
|
-
setProgress([]);
|
|
81
|
-
setShowLog(false);
|
|
82
|
-
setProcessId(null);
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
if (state === 'blocked') {
|
|
86
|
-
return (
|
|
87
|
-
<div className="space-y-2">
|
|
88
|
-
<div className="flex items-center gap-2 text-amber-700 text-sm bg-amber-50 border border-amber-200 rounded-lg px-3 py-2">
|
|
89
|
-
<AlertTriangle className="w-4 h-4 flex-shrink-0" />
|
|
90
|
-
<div>
|
|
91
|
-
<div className="font-medium">Dependencies not met</div>
|
|
92
|
-
<ul className="mt-1 text-xs space-y-0.5">
|
|
93
|
-
{blockers.map((b) => (<li key={b.id}>{b.id}: {b.name} ({b.status})</li>))}
|
|
94
|
-
</ul>
|
|
95
|
-
</div>
|
|
96
|
-
</div>
|
|
97
|
-
<button onClick={dismiss} className="text-xs text-slate-500 hover:text-slate-700">Dismiss</button>
|
|
98
|
-
</div>
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if ((state === 'running' || state === 'complete' || state === 'error') && showLog) {
|
|
103
|
-
return (
|
|
104
|
-
<div className="space-y-2">
|
|
105
|
-
<div className="flex items-center justify-between">
|
|
106
|
-
<div className="flex items-center gap-2 text-sm font-medium">
|
|
107
|
-
{state === 'running' && <Loader2 className="w-4 h-4 animate-spin text-blue-600" />}
|
|
108
|
-
{state === 'complete' && <Play className="w-4 h-4 text-green-600" />}
|
|
109
|
-
{state === 'error' && <AlertTriangle className="w-4 h-4 text-red-600" />}
|
|
110
|
-
<span className={cn(
|
|
111
|
-
state === 'running' && 'text-blue-700',
|
|
112
|
-
state === 'complete' && 'text-green-700',
|
|
113
|
-
state === 'error' && 'text-red-700',
|
|
114
|
-
)}>
|
|
115
|
-
{state === 'running' ? 'Implementing...' : state === 'complete' ? 'Complete' : 'Failed'}
|
|
116
|
-
</span>
|
|
117
|
-
</div>
|
|
118
|
-
<div className="flex items-center gap-1">
|
|
119
|
-
<button onClick={() => setShowLog(!showLog)} className="p-1 text-slate-400 hover:text-slate-600">
|
|
120
|
-
{showLog ? <ChevronUp className="w-3.5 h-3.5" /> : <ChevronDown className="w-3.5 h-3.5" />}
|
|
121
|
-
</button>
|
|
122
|
-
{state !== 'running' && (
|
|
123
|
-
<button onClick={dismiss} className="p-1 text-slate-400 hover:text-slate-600">
|
|
124
|
-
<X className="w-3.5 h-3.5" />
|
|
125
|
-
</button>
|
|
126
|
-
)}
|
|
127
|
-
</div>
|
|
128
|
-
</div>
|
|
129
|
-
{showLog && (
|
|
130
|
-
<div ref={logRef} className="max-h-32 overflow-y-auto bg-slate-900 text-slate-300 text-xs font-mono rounded-md p-2 space-y-0.5">
|
|
131
|
-
{progress.map((msg, i) => (
|
|
132
|
-
<div key={i} className={cn(
|
|
133
|
-
msg.startsWith('Error') && 'text-red-400',
|
|
134
|
-
msg.includes('complete') && 'text-green-400',
|
|
135
|
-
)}>{msg}</div>
|
|
136
|
-
))}
|
|
137
|
-
{state === 'running' && <div className="text-slate-500 animate-pulse">...</div>}
|
|
138
|
-
</div>
|
|
139
|
-
)}
|
|
140
|
-
</div>
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
21
|
return (
|
|
145
|
-
<
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
'
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
22
|
+
<button
|
|
23
|
+
onClick={handleClick}
|
|
24
|
+
className={`flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
|
|
25
|
+
isActive
|
|
26
|
+
? 'bg-blue-100 text-blue-700 hover:bg-blue-200'
|
|
27
|
+
: 'bg-blue-600 text-white hover:bg-blue-700'
|
|
28
|
+
}`}
|
|
29
|
+
>
|
|
30
|
+
{isActive ? (
|
|
31
|
+
<>
|
|
32
|
+
<Loader2 className="w-4 h-4 animate-spin" />
|
|
33
|
+
View Run
|
|
34
|
+
</>
|
|
35
|
+
) : (
|
|
36
|
+
<>
|
|
37
|
+
<Play className="w-4 h-4" />
|
|
38
|
+
Run
|
|
39
|
+
</>
|
|
40
|
+
)}
|
|
41
|
+
</button>
|
|
161
42
|
);
|
|
162
43
|
}
|
|
@@ -3,6 +3,7 @@ import { useProcessStore } from '../../store/processStore';
|
|
|
3
3
|
import { useSprintPlanningStore } from '../../store/sprintPlanningStore';
|
|
4
4
|
import { useCeremonyStore } from '../../store/ceremonyStore';
|
|
5
5
|
import { useSeedStore } from '../../store/seedStore';
|
|
6
|
+
import { useRunStore } from '../../store/runStore';
|
|
6
7
|
|
|
7
8
|
const STATUS_PILL = {
|
|
8
9
|
running: 'bg-blue-50 text-blue-700 border-blue-200 hover:bg-blue-100',
|
|
@@ -51,6 +52,9 @@ export function ProcessMonitorBar() {
|
|
|
51
52
|
} else if (p.type === 'seed') {
|
|
52
53
|
const storyId = useSeedStore.getState().getStoryIdByProcessId(p.id);
|
|
53
54
|
if (storyId) useSeedStore.getState().reopenModal(storyId);
|
|
55
|
+
} else if (p.type === 'run-task') {
|
|
56
|
+
const taskId = useRunStore.getState().getTaskIdByProcessId(p.id);
|
|
57
|
+
if (taskId) useRunStore.getState().reopenModal(taskId);
|
|
54
58
|
}
|
|
55
59
|
};
|
|
56
60
|
|
|
@@ -95,6 +95,16 @@ export async function getWorkItem(id) {
|
|
|
95
95
|
return apiFetch(`/work-items/${id}`);
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Get file content from a task's worktree
|
|
100
|
+
* @param {string} taskId - Task ID
|
|
101
|
+
* @param {string} filePath - Relative file path
|
|
102
|
+
* @returns {Promise<{path, content, size}>}
|
|
103
|
+
*/
|
|
104
|
+
export async function getWorktreeFile(taskId, filePath) {
|
|
105
|
+
return apiFetch(`/work-items/${taskId}/worktree-file?path=${encodeURIComponent(filePath)}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
98
108
|
/**
|
|
99
109
|
* Get rendered documentation (doc.md) as HTML
|
|
100
110
|
* @param {string} id - Work item ID
|
|
@@ -14,7 +14,7 @@ export const useFilterStore = create(
|
|
|
14
14
|
epic: true,
|
|
15
15
|
story: true,
|
|
16
16
|
task: true,
|
|
17
|
-
subtask:
|
|
17
|
+
subtask: false,
|
|
18
18
|
},
|
|
19
19
|
columnVisibility: {
|
|
20
20
|
Backlog: true,
|
|
@@ -174,7 +174,7 @@ export const useFilterStore = create(
|
|
|
174
174
|
epic: true,
|
|
175
175
|
story: true,
|
|
176
176
|
task: true,
|
|
177
|
-
subtask:
|
|
177
|
+
subtask: false,
|
|
178
178
|
},
|
|
179
179
|
columnVisibility: {
|
|
180
180
|
Backlog: true,
|
|
@@ -190,12 +190,19 @@ export const useFilterStore = create(
|
|
|
190
190
|
}),
|
|
191
191
|
{
|
|
192
192
|
name: 'avc-kanban-filters', // localStorage key
|
|
193
|
+
version: 2, // bump to reset persisted subtask:true → false
|
|
193
194
|
partialize: (state) => ({
|
|
194
|
-
// Only persist these fields
|
|
195
195
|
typeFilters: state.typeFilters,
|
|
196
196
|
columnVisibility: state.columnVisibility,
|
|
197
197
|
groupBy: state.groupBy,
|
|
198
198
|
}),
|
|
199
|
+
migrate: (persisted, version) => {
|
|
200
|
+
if (version < 2) {
|
|
201
|
+
// Force subtask filter to false on upgrade
|
|
202
|
+
return { ...persisted, typeFilters: { ...persisted.typeFilters, subtask: false } };
|
|
203
|
+
}
|
|
204
|
+
return persisted;
|
|
205
|
+
},
|
|
199
206
|
}
|
|
200
207
|
)
|
|
201
208
|
);
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Run Ceremony Store — manages multiple concurrent run sessions.
|
|
5
|
+
* Each session is keyed by taskId and tracks its own progress/status.
|
|
6
|
+
*/
|
|
7
|
+
export const useRunStore = create((set, get) => ({
|
|
8
|
+
sessions: {}, // { [taskId]: { status, progressLog, processId, taskName, error, reviewInfo, startedAt } }
|
|
9
|
+
activeTaskId: null, // which run modal is showing
|
|
10
|
+
isOpen: false, // modal visibility
|
|
11
|
+
|
|
12
|
+
openModal: (taskId, taskName) => set((s) => ({
|
|
13
|
+
isOpen: true,
|
|
14
|
+
activeTaskId: taskId,
|
|
15
|
+
sessions: {
|
|
16
|
+
...s.sessions,
|
|
17
|
+
[taskId]: s.sessions[taskId] || {
|
|
18
|
+
status: 'idle',
|
|
19
|
+
progressLog: [],
|
|
20
|
+
processId: null,
|
|
21
|
+
taskName: taskName || taskId,
|
|
22
|
+
error: null,
|
|
23
|
+
reviewInfo: null,
|
|
24
|
+
startedAt: null,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
})),
|
|
28
|
+
|
|
29
|
+
closeModal: () => set({ isOpen: false }),
|
|
30
|
+
|
|
31
|
+
reopenModal: (taskId) => set({ isOpen: true, activeTaskId: taskId }),
|
|
32
|
+
|
|
33
|
+
setStatus: (taskId, status) => set((s) => {
|
|
34
|
+
const session = s.sessions[taskId];
|
|
35
|
+
if (!session) return {};
|
|
36
|
+
return {
|
|
37
|
+
sessions: {
|
|
38
|
+
...s.sessions,
|
|
39
|
+
[taskId]: { ...session, status, ...(status === 'running' ? { startedAt: Date.now() } : {}) },
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}),
|
|
43
|
+
|
|
44
|
+
appendProgress: (taskId, message) => set((s) => {
|
|
45
|
+
const session = s.sessions[taskId];
|
|
46
|
+
if (!session) return {};
|
|
47
|
+
return {
|
|
48
|
+
sessions: {
|
|
49
|
+
...s.sessions,
|
|
50
|
+
[taskId]: { ...session, progressLog: [...session.progressLog, message] },
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}),
|
|
54
|
+
|
|
55
|
+
setProcessId: (taskId, processId) => set((s) => {
|
|
56
|
+
const session = s.sessions[taskId];
|
|
57
|
+
if (!session) return {};
|
|
58
|
+
return {
|
|
59
|
+
sessions: {
|
|
60
|
+
...s.sessions,
|
|
61
|
+
[taskId]: { ...session, processId },
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}),
|
|
65
|
+
|
|
66
|
+
setError: (taskId, error) => set((s) => {
|
|
67
|
+
const session = s.sessions[taskId];
|
|
68
|
+
if (!session) return {};
|
|
69
|
+
return {
|
|
70
|
+
sessions: {
|
|
71
|
+
...s.sessions,
|
|
72
|
+
[taskId]: { ...session, error, status: 'error' },
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}),
|
|
76
|
+
|
|
77
|
+
setReviewInfo: (taskId, reviewInfo) => set((s) => {
|
|
78
|
+
const session = s.sessions[taskId];
|
|
79
|
+
if (!session) return {};
|
|
80
|
+
return {
|
|
81
|
+
sessions: {
|
|
82
|
+
...s.sessions,
|
|
83
|
+
[taskId]: { ...session, reviewInfo },
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}),
|
|
87
|
+
|
|
88
|
+
removeSession: (taskId) => set((s) => {
|
|
89
|
+
const { [taskId]: _, ...rest } = s.sessions;
|
|
90
|
+
return {
|
|
91
|
+
sessions: rest,
|
|
92
|
+
...(s.activeTaskId === taskId ? { activeTaskId: null, isOpen: false } : {}),
|
|
93
|
+
};
|
|
94
|
+
}),
|
|
95
|
+
|
|
96
|
+
getTaskIdByProcessId: (processId) => {
|
|
97
|
+
const sessions = get().sessions;
|
|
98
|
+
for (const [taskId, session] of Object.entries(sessions)) {
|
|
99
|
+
if (session.processId === processId) return taskId;
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
},
|
|
103
|
+
}));
|