@agile-vibe-coding/avc 0.1.1 → 0.3.1
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/cli/agent-loader.js +21 -0
- package/cli/agents/agent-selector.md +152 -0
- package/cli/agents/architecture-recommender.md +418 -0
- package/cli/agents/code-implementer.md +117 -0
- package/cli/agents/code-validator.md +80 -0
- package/cli/agents/context-reviewer-epic.md +101 -0
- package/cli/agents/context-reviewer-story.md +92 -0
- package/cli/agents/context-writer-epic.md +145 -0
- package/cli/agents/context-writer-story.md +111 -0
- package/cli/agents/database-deep-dive.md +470 -0
- package/cli/agents/database-recommender.md +634 -0
- package/cli/agents/doc-distributor.md +176 -0
- package/cli/agents/doc-writer-epic.md +42 -0
- package/cli/agents/doc-writer-story.md +43 -0
- package/cli/agents/documentation-updater.md +203 -0
- package/cli/agents/duplicate-detector.md +110 -0
- package/cli/agents/epic-story-decomposer.md +559 -0
- package/cli/agents/feature-context-generator.md +91 -0
- package/cli/agents/gap-checker-epic.md +52 -0
- package/cli/agents/impact-checker-story.md +51 -0
- package/cli/agents/migration-guide-generator.md +305 -0
- package/cli/agents/mission-scope-generator.md +143 -0
- package/cli/agents/mission-scope-validator.md +146 -0
- package/cli/agents/project-context-extractor.md +122 -0
- package/cli/agents/project-documentation-creator.json +226 -0
- package/cli/agents/project-documentation-creator.md +595 -0
- package/cli/agents/question-prefiller.md +269 -0
- package/cli/agents/refiner-epic.md +39 -0
- package/cli/agents/refiner-story.md +42 -0
- package/cli/agents/scaffolding-generator.md +99 -0
- package/cli/agents/seed-validator.md +71 -0
- package/cli/agents/story-doc-enricher.md +133 -0
- package/cli/agents/story-scope-reviewer.md +147 -0
- package/cli/agents/story-splitter.md +83 -0
- package/cli/agents/suggestion-business-analyst.md +88 -0
- package/cli/agents/suggestion-deployment-architect.md +263 -0
- package/cli/agents/suggestion-product-manager.md +129 -0
- package/cli/agents/suggestion-security-specialist.md +156 -0
- package/cli/agents/suggestion-technical-architect.md +269 -0
- package/cli/agents/suggestion-ux-researcher.md +93 -0
- package/cli/agents/task-subtask-decomposer.md +188 -0
- package/cli/agents/validator-documentation.json +183 -0
- package/cli/agents/validator-documentation.md +455 -0
- package/cli/agents/validator-selector.md +211 -0
- package/cli/ansi-colors.js +21 -0
- package/cli/api-reference-tool.js +368 -0
- package/cli/build-docs.js +29 -8
- package/cli/ceremony-history.js +369 -0
- package/cli/checks/catalog.json +76 -0
- package/cli/checks/code/quality.json +26 -0
- package/cli/checks/code/testing.json +14 -0
- package/cli/checks/code/traceability.json +26 -0
- package/cli/checks/cross-refs/epic.json +171 -0
- package/cli/checks/cross-refs/story.json +149 -0
- package/cli/checks/epic/api.json +114 -0
- package/cli/checks/epic/backend.json +126 -0
- package/cli/checks/epic/cloud.json +126 -0
- package/cli/checks/epic/data.json +102 -0
- package/cli/checks/epic/database.json +114 -0
- package/cli/checks/epic/developer.json +182 -0
- package/cli/checks/epic/devops.json +174 -0
- package/cli/checks/epic/frontend.json +162 -0
- package/cli/checks/epic/mobile.json +102 -0
- package/cli/checks/epic/qa.json +90 -0
- package/cli/checks/epic/security.json +184 -0
- package/cli/checks/epic/solution-architect.json +192 -0
- package/cli/checks/epic/test-architect.json +90 -0
- package/cli/checks/epic/ui.json +102 -0
- package/cli/checks/epic/ux.json +90 -0
- package/cli/checks/fixes/epic-fix-template.md +10 -0
- package/cli/checks/fixes/story-fix-template.md +10 -0
- package/cli/checks/story/api.json +186 -0
- package/cli/checks/story/backend.json +102 -0
- package/cli/checks/story/cloud.json +102 -0
- package/cli/checks/story/data.json +210 -0
- package/cli/checks/story/database.json +102 -0
- package/cli/checks/story/developer.json +168 -0
- package/cli/checks/story/devops.json +102 -0
- package/cli/checks/story/frontend.json +174 -0
- package/cli/checks/story/mobile.json +102 -0
- package/cli/checks/story/qa.json +210 -0
- package/cli/checks/story/security.json +198 -0
- package/cli/checks/story/solution-architect.json +230 -0
- package/cli/checks/story/test-architect.json +210 -0
- package/cli/checks/story/ui.json +102 -0
- package/cli/checks/story/ux.json +102 -0
- package/cli/coding-order.js +401 -0
- package/cli/command-logger.js +49 -12
- package/cli/components/static-output.js +63 -0
- package/cli/console-output-manager.js +94 -0
- package/cli/dependency-checker.js +72 -0
- package/cli/docs-sync.js +306 -0
- package/cli/epic-story-validator.js +659 -0
- package/cli/evaluation-prompts.js +1008 -0
- package/cli/execution-context.js +195 -0
- package/cli/generate-summary-table.js +340 -0
- package/cli/init-model-config.js +704 -0
- package/cli/init.js +1737 -278
- package/cli/kanban-server-manager.js +227 -0
- package/cli/llm-claude.js +150 -1
- package/cli/llm-gemini.js +109 -0
- package/cli/llm-local.js +493 -0
- package/cli/llm-mock.js +233 -0
- package/cli/llm-openai.js +454 -0
- package/cli/llm-provider.js +379 -3
- package/cli/llm-token-limits.js +211 -0
- package/cli/llm-verifier.js +662 -0
- package/cli/llm-xiaomi.js +143 -0
- package/cli/message-constants.js +49 -0
- package/cli/message-manager.js +334 -0
- package/cli/message-types.js +96 -0
- package/cli/messaging-api.js +291 -0
- package/cli/micro-check-fixer.js +335 -0
- package/cli/micro-check-runner.js +449 -0
- package/cli/micro-check-scorer.js +148 -0
- package/cli/micro-check-validator.js +538 -0
- package/cli/model-pricing.js +192 -0
- package/cli/model-query-engine.js +468 -0
- package/cli/model-recommendation-analyzer.js +495 -0
- package/cli/model-selector.js +270 -0
- package/cli/output-buffer.js +107 -0
- package/cli/process-manager.js +73 -2
- package/cli/prompt-logger.js +57 -0
- package/cli/repl-ink.js +4625 -1094
- package/cli/repl-old.js +3 -4
- package/cli/seed-processor.js +962 -0
- package/cli/sprint-planning-processor.js +4162 -0
- package/cli/template-processor.js +2149 -105
- package/cli/templates/project.md +25 -8
- package/cli/templates/vitepress-config.mts.template +5 -4
- package/cli/token-tracker.js +547 -0
- package/cli/tools/generate-story-validators.js +317 -0
- package/cli/tools/generate-validators.js +669 -0
- package/cli/update-checker.js +19 -17
- package/cli/update-notifier.js +4 -4
- package/cli/validation-router.js +667 -0
- package/cli/verification-tracker.js +563 -0
- package/cli/worktree-runner.js +654 -0
- package/kanban/README.md +386 -0
- package/kanban/client/README.md +205 -0
- package/kanban/client/components.json +20 -0
- package/kanban/client/dist/assets/index-D_KC5EQT.css +1 -0
- package/kanban/client/dist/assets/index-DjY5zqW7.js +351 -0
- package/kanban/client/dist/index.html +16 -0
- package/kanban/client/dist/vite.svg +1 -0
- package/kanban/client/index.html +15 -0
- package/kanban/client/package-lock.json +9442 -0
- package/kanban/client/package.json +44 -0
- package/kanban/client/postcss.config.js +6 -0
- package/kanban/client/public/vite.svg +1 -0
- package/kanban/client/src/App.jsx +651 -0
- package/kanban/client/src/components/ProjectFileEditorPopup.jsx +117 -0
- package/kanban/client/src/components/ceremony/AskArchPopup.jsx +420 -0
- package/kanban/client/src/components/ceremony/AskModelPopup.jsx +629 -0
- package/kanban/client/src/components/ceremony/CeremonyWorkflowModal.jsx +1133 -0
- package/kanban/client/src/components/ceremony/EpicStorySelectionModal.jsx +254 -0
- package/kanban/client/src/components/ceremony/ProviderSwitcherButton.jsx +290 -0
- package/kanban/client/src/components/ceremony/SponsorCallModal.jsx +686 -0
- package/kanban/client/src/components/ceremony/SprintPlanningModal.jsx +838 -0
- package/kanban/client/src/components/ceremony/steps/ArchitectureStep.jsx +150 -0
- package/kanban/client/src/components/ceremony/steps/CompleteStep.jsx +136 -0
- package/kanban/client/src/components/ceremony/steps/DatabaseStep.jsx +202 -0
- package/kanban/client/src/components/ceremony/steps/DeploymentStep.jsx +123 -0
- package/kanban/client/src/components/ceremony/steps/MissionStep.jsx +106 -0
- package/kanban/client/src/components/ceremony/steps/ReviewAnswersStep.jsx +329 -0
- package/kanban/client/src/components/ceremony/steps/RunningStep.jsx +249 -0
- package/kanban/client/src/components/kanban/CardDetailModal.jsx +646 -0
- package/kanban/client/src/components/kanban/EpicSection.jsx +146 -0
- package/kanban/client/src/components/kanban/FilterToolbar.jsx +222 -0
- package/kanban/client/src/components/kanban/GroupingSelector.jsx +63 -0
- package/kanban/client/src/components/kanban/KanbanBoard.jsx +211 -0
- package/kanban/client/src/components/kanban/KanbanCard.jsx +147 -0
- package/kanban/client/src/components/kanban/KanbanColumn.jsx +90 -0
- package/kanban/client/src/components/kanban/RefineWorkItemPopup.jsx +784 -0
- package/kanban/client/src/components/kanban/RunButton.jsx +162 -0
- package/kanban/client/src/components/kanban/SeedButton.jsx +176 -0
- package/kanban/client/src/components/layout/LoadingScreen.jsx +82 -0
- package/kanban/client/src/components/process/ProcessMonitorBar.jsx +80 -0
- package/kanban/client/src/components/settings/AgentEditorPopup.jsx +171 -0
- package/kanban/client/src/components/settings/AgentsTab.jsx +381 -0
- package/kanban/client/src/components/settings/ApiKeysTab.jsx +142 -0
- package/kanban/client/src/components/settings/CeremonyModelsTab.jsx +105 -0
- package/kanban/client/src/components/settings/CheckEditorPopup.jsx +507 -0
- package/kanban/client/src/components/settings/CostThresholdsTab.jsx +95 -0
- package/kanban/client/src/components/settings/ModelPricingTab.jsx +269 -0
- package/kanban/client/src/components/settings/OpenAIAuthSection.jsx +412 -0
- package/kanban/client/src/components/settings/ServersTab.jsx +121 -0
- package/kanban/client/src/components/settings/SettingsModal.jsx +84 -0
- package/kanban/client/src/components/stats/CostModal.jsx +384 -0
- package/kanban/client/src/components/ui/badge.jsx +27 -0
- package/kanban/client/src/components/ui/dialog.jsx +121 -0
- package/kanban/client/src/components/ui/tabs.jsx +85 -0
- package/kanban/client/src/hooks/__tests__/useGrouping.test.js +232 -0
- package/kanban/client/src/hooks/useGrouping.js +177 -0
- package/kanban/client/src/hooks/useWebSocket.js +120 -0
- package/kanban/client/src/lib/__tests__/api.test.js +196 -0
- package/kanban/client/src/lib/__tests__/status-grouping.test.js +94 -0
- package/kanban/client/src/lib/api.js +515 -0
- package/kanban/client/src/lib/status-grouping.js +154 -0
- package/kanban/client/src/lib/utils.js +11 -0
- package/kanban/client/src/main.jsx +10 -0
- package/kanban/client/src/store/__tests__/kanbanStore.test.js +164 -0
- package/kanban/client/src/store/ceremonyStore.js +172 -0
- package/kanban/client/src/store/filterStore.js +201 -0
- package/kanban/client/src/store/kanbanStore.js +123 -0
- package/kanban/client/src/store/processStore.js +65 -0
- package/kanban/client/src/store/sprintPlanningStore.js +33 -0
- package/kanban/client/src/styles/globals.css +59 -0
- package/kanban/client/tailwind.config.js +77 -0
- package/kanban/client/vite.config.js +28 -0
- package/kanban/client/vitest.config.js +28 -0
- package/kanban/dev-start.sh +47 -0
- package/kanban/package.json +12 -0
- package/kanban/server/index.js +537 -0
- package/kanban/server/routes/ceremony.js +454 -0
- package/kanban/server/routes/costs.js +163 -0
- package/kanban/server/routes/openai-oauth.js +366 -0
- package/kanban/server/routes/processes.js +50 -0
- package/kanban/server/routes/settings.js +736 -0
- package/kanban/server/routes/websocket.js +281 -0
- package/kanban/server/routes/work-items.js +487 -0
- package/kanban/server/services/CeremonyService.js +1441 -0
- package/kanban/server/services/FileSystemScanner.js +95 -0
- package/kanban/server/services/FileWatcher.js +144 -0
- package/kanban/server/services/HierarchyBuilder.js +196 -0
- package/kanban/server/services/ProcessRegistry.js +122 -0
- package/kanban/server/services/TaskRunnerService.js +261 -0
- package/kanban/server/services/WorkItemReader.js +123 -0
- package/kanban/server/services/WorkItemRefineService.js +510 -0
- package/kanban/server/start.js +49 -0
- package/kanban/server/utils/kanban-logger.js +132 -0
- package/kanban/server/utils/markdown.js +91 -0
- package/kanban/server/utils/status-grouping.js +107 -0
- package/kanban/server/workers/run-task-worker.js +121 -0
- package/kanban/server/workers/seed-worker.js +94 -0
- package/kanban/server/workers/sponsor-call-worker.js +92 -0
- package/kanban/server/workers/sprint-planning-worker.js +212 -0
- package/package.json +19 -7
- package/cli/agents/documentation.md +0 -302
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from 'react';
|
|
2
|
+
import { Play, Loader2, AlertTriangle, X, ChevronDown, ChevronUp } from 'lucide-react';
|
|
3
|
+
import { getDependencyStatus, startRunTask } from '../../lib/api';
|
|
4
|
+
import { cn } from '../../lib/utils';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Run Button — runs a task in a git worktree (implement + test + commit).
|
|
8
|
+
* Shows dependency check, and streaming progress log.
|
|
9
|
+
*/
|
|
10
|
+
export function RunButton({ taskId, onStarted }) {
|
|
11
|
+
const [state, setState] = useState('idle'); // idle | checking | blocked | running | complete | error
|
|
12
|
+
const [blockers, setBlockers] = useState([]);
|
|
13
|
+
const [error, setError] = useState(null);
|
|
14
|
+
const [progress, setProgress] = useState([]);
|
|
15
|
+
const [showLog, setShowLog] = useState(false);
|
|
16
|
+
const [processId, setProcessId] = useState(null);
|
|
17
|
+
const logRef = useRef(null);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
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');
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
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
|
+
return (
|
|
145
|
+
<div className="space-y-1">
|
|
146
|
+
<button
|
|
147
|
+
onClick={handleClick}
|
|
148
|
+
disabled={state === 'checking'}
|
|
149
|
+
className={cn(
|
|
150
|
+
'flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm font-medium transition-colors',
|
|
151
|
+
state === 'checking'
|
|
152
|
+
? 'bg-blue-100 text-blue-700 cursor-wait'
|
|
153
|
+
: 'bg-blue-600 text-white hover:bg-blue-700'
|
|
154
|
+
)}
|
|
155
|
+
>
|
|
156
|
+
{state === 'checking' ? <Loader2 className="w-4 h-4 animate-spin" /> : <Play className="w-4 h-4" />}
|
|
157
|
+
{state === 'checking' ? 'Checking...' : 'Run'}
|
|
158
|
+
</button>
|
|
159
|
+
{error && <p className="text-xs text-red-600">{error}</p>}
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from 'react';
|
|
2
|
+
import { Sprout, Loader2, AlertTriangle, X, ChevronDown, ChevronUp, Settings } from 'lucide-react';
|
|
3
|
+
import { getDependencyStatus, startSeedCeremony, getSettings } from '../../lib/api';
|
|
4
|
+
import { cn } from '../../lib/utils';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Seed Button — decomposes a story into tasks/subtasks.
|
|
8
|
+
* Shows dependency check, model selector, and streaming progress log.
|
|
9
|
+
*/
|
|
10
|
+
export function SeedButton({ storyId, onStarted }) {
|
|
11
|
+
const [state, setState] = useState('idle'); // idle | checking | blocked | configuring | seeding | complete | error
|
|
12
|
+
const [blockers, setBlockers] = useState([]);
|
|
13
|
+
const [error, setError] = useState(null);
|
|
14
|
+
const [progress, setProgress] = useState([]);
|
|
15
|
+
const [showLog, setShowLog] = useState(false);
|
|
16
|
+
const [processId, setProcessId] = useState(null);
|
|
17
|
+
const logRef = useRef(null);
|
|
18
|
+
|
|
19
|
+
// Auto-scroll progress log
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight;
|
|
22
|
+
}, [progress]);
|
|
23
|
+
|
|
24
|
+
// Listen for WebSocket seed events dispatched by App.jsx
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (!processId) return;
|
|
27
|
+
|
|
28
|
+
const handler = (event) => {
|
|
29
|
+
const msg = event.detail;
|
|
30
|
+
if (!msg || msg.processId !== processId) return;
|
|
31
|
+
|
|
32
|
+
if (msg.type === 'seed:progress') {
|
|
33
|
+
setProgress(prev => [...prev, msg.message]);
|
|
34
|
+
} else if (msg.type === 'seed:complete') {
|
|
35
|
+
setState('complete');
|
|
36
|
+
setProgress(prev => [...prev, 'Seed complete — tasks created.']);
|
|
37
|
+
onStarted?.();
|
|
38
|
+
} else if (msg.type === 'seed:error') {
|
|
39
|
+
setState('error');
|
|
40
|
+
setError(msg.error || 'Seed failed');
|
|
41
|
+
setProgress(prev => [...prev, `Error: ${msg.error}`]);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
window.addEventListener('avc-ws-message', handler);
|
|
46
|
+
return () => window.removeEventListener('avc-ws-message', handler);
|
|
47
|
+
}, [processId, onStarted]);
|
|
48
|
+
|
|
49
|
+
const handleClick = async () => {
|
|
50
|
+
if (state === 'checking' || state === 'seeding') return;
|
|
51
|
+
|
|
52
|
+
setState('checking');
|
|
53
|
+
setError(null);
|
|
54
|
+
setBlockers([]);
|
|
55
|
+
setProgress([]);
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const depStatus = await getDependencyStatus(storyId);
|
|
59
|
+
if (!depStatus.ready) {
|
|
60
|
+
setState('blocked');
|
|
61
|
+
setBlockers(depStatus.blockers || []);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Go straight to seeding (model config comes from avc.json server-side)
|
|
66
|
+
setState('seeding');
|
|
67
|
+
setShowLog(true);
|
|
68
|
+
setProgress(['Starting seed ceremony...']);
|
|
69
|
+
const result = await startSeedCeremony(storyId);
|
|
70
|
+
setProcessId(result.processId);
|
|
71
|
+
setProgress(prev => [...prev, `Process started: ${result.processId}`]);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
setError(err.message);
|
|
74
|
+
setState('error');
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const dismiss = () => {
|
|
79
|
+
setState('idle');
|
|
80
|
+
setBlockers([]);
|
|
81
|
+
setError(null);
|
|
82
|
+
setProgress([]);
|
|
83
|
+
setShowLog(false);
|
|
84
|
+
setProcessId(null);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Blocked state — show blockers
|
|
88
|
+
if (state === 'blocked') {
|
|
89
|
+
return (
|
|
90
|
+
<div className="space-y-2">
|
|
91
|
+
<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">
|
|
92
|
+
<AlertTriangle className="w-4 h-4 flex-shrink-0" />
|
|
93
|
+
<div>
|
|
94
|
+
<div className="font-medium">Dependencies not met</div>
|
|
95
|
+
<ul className="mt-1 text-xs space-y-0.5">
|
|
96
|
+
{blockers.map((b) => (
|
|
97
|
+
<li key={b.id}>{b.id}: {b.name} ({b.status})</li>
|
|
98
|
+
))}
|
|
99
|
+
</ul>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
<button onClick={dismiss} className="text-xs text-slate-500 hover:text-slate-700">Dismiss</button>
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Seeding/complete/error state — show progress log
|
|
108
|
+
if ((state === 'seeding' || state === 'complete' || state === 'error') && showLog) {
|
|
109
|
+
return (
|
|
110
|
+
<div className="space-y-2">
|
|
111
|
+
<div className="flex items-center justify-between">
|
|
112
|
+
<div className="flex items-center gap-2 text-sm font-medium">
|
|
113
|
+
{state === 'seeding' && <Loader2 className="w-4 h-4 animate-spin text-green-600" />}
|
|
114
|
+
{state === 'complete' && <Sprout className="w-4 h-4 text-green-600" />}
|
|
115
|
+
{state === 'error' && <AlertTriangle className="w-4 h-4 text-red-600" />}
|
|
116
|
+
<span className={cn(
|
|
117
|
+
state === 'seeding' && 'text-green-700',
|
|
118
|
+
state === 'complete' && 'text-green-700',
|
|
119
|
+
state === 'error' && 'text-red-700',
|
|
120
|
+
)}>
|
|
121
|
+
{state === 'seeding' ? 'Seeding...' : state === 'complete' ? 'Seed Complete' : 'Seed Failed'}
|
|
122
|
+
</span>
|
|
123
|
+
</div>
|
|
124
|
+
<div className="flex items-center gap-1">
|
|
125
|
+
<button onClick={() => setShowLog(!showLog)} className="p-1 text-slate-400 hover:text-slate-600">
|
|
126
|
+
{showLog ? <ChevronUp className="w-3.5 h-3.5" /> : <ChevronDown className="w-3.5 h-3.5" />}
|
|
127
|
+
</button>
|
|
128
|
+
{state !== 'seeding' && (
|
|
129
|
+
<button onClick={dismiss} className="p-1 text-slate-400 hover:text-slate-600">
|
|
130
|
+
<X className="w-3.5 h-3.5" />
|
|
131
|
+
</button>
|
|
132
|
+
)}
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
{showLog && (
|
|
136
|
+
<div
|
|
137
|
+
ref={logRef}
|
|
138
|
+
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"
|
|
139
|
+
>
|
|
140
|
+
{progress.map((msg, i) => (
|
|
141
|
+
<div key={i} className={cn(
|
|
142
|
+
msg.startsWith('Error') && 'text-red-400',
|
|
143
|
+
msg.includes('complete') && 'text-green-400',
|
|
144
|
+
)}>{msg}</div>
|
|
145
|
+
))}
|
|
146
|
+
{state === 'seeding' && <div className="text-slate-500 animate-pulse">...</div>}
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Default idle state — seed button
|
|
154
|
+
return (
|
|
155
|
+
<div className="space-y-1">
|
|
156
|
+
<button
|
|
157
|
+
onClick={handleClick}
|
|
158
|
+
disabled={state === 'checking'}
|
|
159
|
+
className={cn(
|
|
160
|
+
'flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm font-medium transition-colors',
|
|
161
|
+
state === 'checking'
|
|
162
|
+
? 'bg-green-100 text-green-700 cursor-wait'
|
|
163
|
+
: 'bg-green-600 text-white hover:bg-green-700'
|
|
164
|
+
)}
|
|
165
|
+
>
|
|
166
|
+
{state === 'checking' ? (
|
|
167
|
+
<Loader2 className="w-4 h-4 animate-spin" />
|
|
168
|
+
) : (
|
|
169
|
+
<Sprout className="w-4 h-4" />
|
|
170
|
+
)}
|
|
171
|
+
{state === 'checking' ? 'Checking...' : 'Seed'}
|
|
172
|
+
</button>
|
|
173
|
+
{error && <p className="text-xs text-red-600">{error}</p>}
|
|
174
|
+
</div>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { motion } from 'framer-motion';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Loading Screen Component
|
|
5
|
+
* Beautiful loading state with animation
|
|
6
|
+
*/
|
|
7
|
+
export function LoadingScreen({ message = 'Loading...' }) {
|
|
8
|
+
return (
|
|
9
|
+
<div className="flex items-center justify-center min-h-screen bg-slate-50">
|
|
10
|
+
<div className="text-center">
|
|
11
|
+
<motion.div
|
|
12
|
+
animate={{ rotate: 360 }}
|
|
13
|
+
transition={{ duration: 1, repeat: Infinity, ease: 'linear' }}
|
|
14
|
+
className="inline-block"
|
|
15
|
+
>
|
|
16
|
+
<div className="w-16 h-16 border-4 border-blue-200 border-t-blue-600 rounded-full" />
|
|
17
|
+
</motion.div>
|
|
18
|
+
<motion.p
|
|
19
|
+
initial={{ opacity: 0 }}
|
|
20
|
+
animate={{ opacity: 1 }}
|
|
21
|
+
transition={{ delay: 0.2 }}
|
|
22
|
+
className="mt-4 text-slate-600 font-medium"
|
|
23
|
+
>
|
|
24
|
+
{message}
|
|
25
|
+
</motion.p>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Card Skeleton Component
|
|
33
|
+
* Skeleton placeholder for kanban cards
|
|
34
|
+
*/
|
|
35
|
+
export function CardSkeleton() {
|
|
36
|
+
return (
|
|
37
|
+
<div className="bg-white rounded-lg border border-slate-200 p-4 animate-pulse">
|
|
38
|
+
<div className="flex items-center justify-between mb-3">
|
|
39
|
+
<div className="h-6 bg-slate-200 rounded w-20"></div>
|
|
40
|
+
<div className="h-6 bg-slate-200 rounded w-16"></div>
|
|
41
|
+
</div>
|
|
42
|
+
<div className="h-4 bg-slate-200 rounded w-full mb-2"></div>
|
|
43
|
+
<div className="h-4 bg-slate-200 rounded w-3/4"></div>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Column Skeleton Component
|
|
50
|
+
* Skeleton placeholder for kanban columns
|
|
51
|
+
*/
|
|
52
|
+
export function ColumnSkeleton() {
|
|
53
|
+
return (
|
|
54
|
+
<div className="min-w-[320px] max-w-[380px] bg-slate-100 rounded-lg p-4">
|
|
55
|
+
<div className="flex items-center justify-between mb-4">
|
|
56
|
+
<div className="h-6 bg-slate-200 rounded w-24 animate-pulse"></div>
|
|
57
|
+
<div className="h-6 bg-slate-200 rounded-full w-16 animate-pulse"></div>
|
|
58
|
+
</div>
|
|
59
|
+
<div className="space-y-3">
|
|
60
|
+
<CardSkeleton />
|
|
61
|
+
<CardSkeleton />
|
|
62
|
+
<CardSkeleton />
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Board Skeleton Component
|
|
70
|
+
* Skeleton placeholder for the entire kanban board
|
|
71
|
+
*/
|
|
72
|
+
export function BoardSkeleton() {
|
|
73
|
+
return (
|
|
74
|
+
<div className="flex gap-4 overflow-x-auto pb-4">
|
|
75
|
+
<ColumnSkeleton />
|
|
76
|
+
<ColumnSkeleton />
|
|
77
|
+
<ColumnSkeleton />
|
|
78
|
+
<ColumnSkeleton />
|
|
79
|
+
<ColumnSkeleton />
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Activity } from 'lucide-react';
|
|
2
|
+
import { useProcessStore } from '../../store/processStore';
|
|
3
|
+
import { useSprintPlanningStore } from '../../store/sprintPlanningStore';
|
|
4
|
+
import { useCeremonyStore } from '../../store/ceremonyStore';
|
|
5
|
+
|
|
6
|
+
const STATUS_PILL = {
|
|
7
|
+
running: 'bg-blue-50 text-blue-700 border-blue-200 hover:bg-blue-100',
|
|
8
|
+
paused: 'bg-amber-50 text-amber-700 border-amber-200 hover:bg-amber-100',
|
|
9
|
+
complete: 'bg-green-50 text-green-700 border-green-200 hover:bg-green-100',
|
|
10
|
+
error: 'bg-red-50 text-red-700 border-red-200 hover:bg-red-100',
|
|
11
|
+
cancelled: 'bg-slate-50 text-slate-500 border-slate-200 hover:bg-slate-100',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const STATUS_DOT = {
|
|
15
|
+
running: <span className="w-1.5 h-1.5 rounded-full bg-blue-500 animate-pulse flex-shrink-0" />,
|
|
16
|
+
paused: <span className="w-1.5 h-1.5 rounded-full bg-amber-500 flex-shrink-0" />,
|
|
17
|
+
complete: <span className="w-1.5 h-1.5 rounded-full bg-green-500 flex-shrink-0" />,
|
|
18
|
+
error: <span className="w-1.5 h-1.5 rounded-full bg-red-500 flex-shrink-0" />,
|
|
19
|
+
cancelled: <span className="w-1.5 h-1.5 rounded-full bg-slate-400 flex-shrink-0" />,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function elapsed(startedAt, endedAt) {
|
|
23
|
+
const ms = (endedAt ?? Date.now()) - startedAt;
|
|
24
|
+
const s = Math.floor(ms / 1000);
|
|
25
|
+
if (s < 60) return `${s}s`;
|
|
26
|
+
return `${Math.floor(s / 60)}m ${s % 60}s`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function ProcessMonitorBar() {
|
|
30
|
+
const { processes, clearCompleted } = useProcessStore();
|
|
31
|
+
|
|
32
|
+
// Sponsor call runs only once — hide its chip once it's no longer running.
|
|
33
|
+
// Sprint planning — hide when cancelled (nothing to review); keep for complete/error.
|
|
34
|
+
const visibleProcesses = processes.filter(p =>
|
|
35
|
+
!(p.type === 'sponsor-call' && p.status !== 'running') &&
|
|
36
|
+
!(p.type === 'sprint-planning' && p.status === 'cancelled')
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
if (visibleProcesses.length === 0) return null;
|
|
40
|
+
|
|
41
|
+
const hasCompleted = visibleProcesses.some(p =>
|
|
42
|
+
['complete', 'error', 'cancelled'].includes(p.status)
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const handleChipClick = (p) => {
|
|
46
|
+
if (p.type === 'sprint-planning') {
|
|
47
|
+
useSprintPlanningStore.getState().reopenModal();
|
|
48
|
+
} else if (p.type === 'sponsor-call') {
|
|
49
|
+
useCeremonyStore.getState().reopenWizard();
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className="flex items-center gap-2 px-4 py-1.5 bg-white border-b border-slate-200 flex-shrink-0 flex-wrap">
|
|
55
|
+
<Activity className="w-4 h-4 text-slate-400 flex-shrink-0" />
|
|
56
|
+
|
|
57
|
+
{visibleProcesses.map(p => (
|
|
58
|
+
<button
|
|
59
|
+
key={p.id}
|
|
60
|
+
onClick={() => handleChipClick(p)}
|
|
61
|
+
title="Click to view"
|
|
62
|
+
className={`flex items-center gap-1.5 text-xs px-2.5 py-1 rounded-full border transition-colors cursor-pointer ${STATUS_PILL[p.status] ?? STATUS_PILL.cancelled}`}
|
|
63
|
+
>
|
|
64
|
+
{STATUS_DOT[p.status] ?? STATUS_DOT.cancelled}
|
|
65
|
+
<span className="font-medium">{p.label}</span>
|
|
66
|
+
<span className="opacity-60">{elapsed(p.startedAt, p.endedAt)}</span>
|
|
67
|
+
</button>
|
|
68
|
+
))}
|
|
69
|
+
|
|
70
|
+
{hasCompleted && (
|
|
71
|
+
<button
|
|
72
|
+
onClick={clearCompleted}
|
|
73
|
+
className="ml-auto text-xs text-slate-400 hover:text-slate-600 px-2 py-0.5 transition-colors"
|
|
74
|
+
>
|
|
75
|
+
Clear done
|
|
76
|
+
</button>
|
|
77
|
+
)}
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { X, RotateCcw } from 'lucide-react';
|
|
3
|
+
import { getAgentContent, saveAgentContent, resetAgent } from '../../lib/api';
|
|
4
|
+
|
|
5
|
+
export function AgentEditorPopup({ agentName, onClose, onSaved, onReset }) {
|
|
6
|
+
const [data, setData] = useState(null); // { content, isCustomized, defaultContent }
|
|
7
|
+
const [editValue, setEditValue] = useState('');
|
|
8
|
+
const [loading, setLoading] = useState(true);
|
|
9
|
+
const [saving, setSaving] = useState(false);
|
|
10
|
+
const [saved, setSaved] = useState(false);
|
|
11
|
+
const [error, setError] = useState(null);
|
|
12
|
+
// pendingReset: editor has been loaded with defaultContent, Save will call resetAgent()
|
|
13
|
+
const [pendingReset, setPendingReset] = useState(false);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
setLoading(true);
|
|
17
|
+
setError(null);
|
|
18
|
+
setPendingReset(false);
|
|
19
|
+
getAgentContent(agentName)
|
|
20
|
+
.then(d => {
|
|
21
|
+
setData(d);
|
|
22
|
+
setEditValue(d.content);
|
|
23
|
+
})
|
|
24
|
+
.catch(err => setError(err.message))
|
|
25
|
+
.finally(() => setLoading(false));
|
|
26
|
+
}, [agentName]);
|
|
27
|
+
|
|
28
|
+
const isDirty = data && editValue !== data.content;
|
|
29
|
+
|
|
30
|
+
const handleSave = async () => {
|
|
31
|
+
if (!data || !isDirty) return;
|
|
32
|
+
setSaving(true);
|
|
33
|
+
setError(null);
|
|
34
|
+
try {
|
|
35
|
+
if (pendingReset) {
|
|
36
|
+
// Reset to default: delete customized file
|
|
37
|
+
await resetAgent(agentName);
|
|
38
|
+
setData(prev => ({ ...prev, content: prev.defaultContent, isCustomized: false }));
|
|
39
|
+
setPendingReset(false);
|
|
40
|
+
onReset?.();
|
|
41
|
+
} else {
|
|
42
|
+
await saveAgentContent(agentName, editValue);
|
|
43
|
+
setData(prev => ({ ...prev, content: editValue, isCustomized: true }));
|
|
44
|
+
onSaved?.();
|
|
45
|
+
}
|
|
46
|
+
setSaved(true);
|
|
47
|
+
setTimeout(() => setSaved(false), 2000);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
setError(err.message);
|
|
50
|
+
} finally {
|
|
51
|
+
setSaving(false);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Cancel closes the popup without saving
|
|
56
|
+
const handleCancel = () => {
|
|
57
|
+
onClose();
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Reset loads default content into the editor — persisted only on Save
|
|
61
|
+
const handleReset = () => {
|
|
62
|
+
if (!data?.defaultContent) return;
|
|
63
|
+
setEditValue(data.defaultContent);
|
|
64
|
+
setPendingReset(true);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const canReset = data?.isCustomized && !pendingReset;
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div
|
|
71
|
+
className="fixed inset-0 z-[90] flex items-center justify-center bg-black/50 backdrop-blur-sm p-4"
|
|
72
|
+
onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
|
|
73
|
+
>
|
|
74
|
+
<div
|
|
75
|
+
className="w-full max-w-4xl bg-white rounded-2xl shadow-2xl flex flex-col overflow-hidden"
|
|
76
|
+
style={{ height: '85vh' }}
|
|
77
|
+
>
|
|
78
|
+
{/* Header */}
|
|
79
|
+
<div className="flex items-center justify-between px-5 py-3 border-b border-slate-100 flex-shrink-0">
|
|
80
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
81
|
+
<span className="text-sm font-mono font-medium text-slate-700 truncate">
|
|
82
|
+
{agentName}
|
|
83
|
+
</span>
|
|
84
|
+
{data?.isCustomized && !pendingReset && (
|
|
85
|
+
<span className="flex-shrink-0 text-xs font-medium px-1.5 py-0.5 rounded bg-amber-100 text-amber-700">
|
|
86
|
+
Custom
|
|
87
|
+
</span>
|
|
88
|
+
)}
|
|
89
|
+
{pendingReset && (
|
|
90
|
+
<span className="flex-shrink-0 text-xs font-medium px-1.5 py-0.5 rounded bg-blue-100 text-blue-700">
|
|
91
|
+
Reset pending — save to apply
|
|
92
|
+
</span>
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
<button
|
|
96
|
+
type="button"
|
|
97
|
+
onClick={onClose}
|
|
98
|
+
className="text-slate-400 hover:text-slate-600 transition-colors ml-4 flex-shrink-0"
|
|
99
|
+
aria-label="Close"
|
|
100
|
+
>
|
|
101
|
+
<X className="w-4 h-4" />
|
|
102
|
+
</button>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
{/* Body */}
|
|
106
|
+
{loading ? (
|
|
107
|
+
<div className="flex-1 flex items-center justify-center text-sm text-slate-400">
|
|
108
|
+
<span className="w-4 h-4 border border-slate-300 border-t-slate-600 rounded-full animate-spin mr-2" />
|
|
109
|
+
Loading…
|
|
110
|
+
</div>
|
|
111
|
+
) : error && !data ? (
|
|
112
|
+
<div className="flex-1 flex items-center justify-center text-sm text-red-500 px-6 text-center">
|
|
113
|
+
{error}
|
|
114
|
+
</div>
|
|
115
|
+
) : (
|
|
116
|
+
<textarea
|
|
117
|
+
value={editValue}
|
|
118
|
+
onChange={e => { setEditValue(e.target.value); setPendingReset(false); }}
|
|
119
|
+
className="flex-1 resize-none font-mono text-xs text-slate-800 leading-relaxed px-5 py-4 focus:outline-none"
|
|
120
|
+
spellCheck={false}
|
|
121
|
+
/>
|
|
122
|
+
)}
|
|
123
|
+
|
|
124
|
+
{/* Footer */}
|
|
125
|
+
<div className="flex items-center justify-between px-5 py-3 border-t border-slate-100 flex-shrink-0">
|
|
126
|
+
<div>
|
|
127
|
+
{error && !loading && (
|
|
128
|
+
<p className="text-xs text-red-600">{error}</p>
|
|
129
|
+
)}
|
|
130
|
+
{saved && (
|
|
131
|
+
<p className="text-xs text-green-600 font-medium">Saved ✓</p>
|
|
132
|
+
)}
|
|
133
|
+
</div>
|
|
134
|
+
<div className="flex items-center gap-2">
|
|
135
|
+
{/* Reset: loads default content into editor; Save then applies it */}
|
|
136
|
+
<button
|
|
137
|
+
type="button"
|
|
138
|
+
onClick={handleReset}
|
|
139
|
+
disabled={!canReset}
|
|
140
|
+
className="flex items-center gap-1.5 px-3 py-1.5 text-xs text-slate-500 hover:text-amber-600 transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
|
141
|
+
title={canReset ? 'Load default content into editor' : 'Only available for customized agents'}
|
|
142
|
+
>
|
|
143
|
+
<RotateCcw className="w-3 h-3" />
|
|
144
|
+
Reset to default
|
|
145
|
+
</button>
|
|
146
|
+
<button
|
|
147
|
+
type="button"
|
|
148
|
+
onClick={handleCancel}
|
|
149
|
+
className="px-3 py-1.5 text-xs font-medium text-slate-500 hover:text-slate-700 transition-colors"
|
|
150
|
+
>
|
|
151
|
+
Cancel
|
|
152
|
+
</button>
|
|
153
|
+
<button
|
|
154
|
+
type="button"
|
|
155
|
+
onClick={handleSave}
|
|
156
|
+
disabled={!isDirty || saving}
|
|
157
|
+
className="px-3 py-1.5 text-xs font-medium bg-slate-900 text-white rounded-md hover:bg-slate-700 transition-colors disabled:opacity-40"
|
|
158
|
+
>
|
|
159
|
+
{saving ? (
|
|
160
|
+
<span className="inline-flex items-center gap-1">
|
|
161
|
+
<span className="w-3 h-3 border border-white/40 border-t-white rounded-full animate-spin" />
|
|
162
|
+
Saving
|
|
163
|
+
</span>
|
|
164
|
+
) : pendingReset ? 'Save & Reset' : 'Save'}
|
|
165
|
+
</button>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
);
|
|
171
|
+
}
|