@aion0/forge 0.4.15 → 0.5.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/CLAUDE.md +1 -1
- package/README.md +2 -2
- package/RELEASE_NOTES.md +170 -13
- package/app/api/agents/route.ts +17 -0
- package/app/api/delivery/[id]/route.ts +62 -0
- package/app/api/delivery/route.ts +40 -0
- package/app/api/mobile-chat/route.ts +13 -7
- package/app/api/monitor/route.ts +10 -6
- package/app/api/pipelines/[id]/route.ts +16 -3
- package/app/api/tasks/route.ts +2 -1
- package/app/api/workspace/[id]/agents/route.ts +35 -0
- package/app/api/workspace/[id]/memory/route.ts +23 -0
- package/app/api/workspace/[id]/smith/route.ts +22 -0
- package/app/api/workspace/[id]/stream/route.ts +28 -0
- package/app/api/workspace/route.ts +100 -0
- package/app/global-error.tsx +10 -4
- package/app/icon.ico +0 -0
- package/app/layout.tsx +2 -2
- package/app/login/LoginForm.tsx +96 -0
- package/app/login/page.tsx +7 -98
- package/app/page.tsx +2 -2
- package/bin/forge-server.mjs +23 -4
- package/check-forge-status.sh +9 -0
- package/cli/mw.ts +2 -2
- package/components/ConversationEditor.tsx +411 -0
- package/components/ConversationGraphView.tsx +347 -0
- package/components/ConversationTerminalView.tsx +303 -0
- package/components/Dashboard.tsx +36 -39
- package/components/DashboardWrapper.tsx +9 -0
- package/components/DeliveryFlowEditor.tsx +491 -0
- package/components/DeliveryList.tsx +230 -0
- package/components/DeliveryWorkspace.tsx +589 -0
- package/components/DocTerminal.tsx +12 -4
- package/components/DocsViewer.tsx +10 -2
- package/components/HelpTerminal.tsx +13 -8
- package/components/InlinePipelineView.tsx +111 -0
- package/components/MobileView.tsx +20 -0
- package/components/MonitorPanel.tsx +9 -4
- package/components/NewTaskModal.tsx +32 -0
- package/components/PipelineEditor.tsx +49 -6
- package/components/PipelineView.tsx +482 -64
- package/components/ProjectDetail.tsx +314 -56
- package/components/ProjectManager.tsx +49 -4
- package/components/SessionView.tsx +27 -13
- package/components/SettingsModal.tsx +790 -124
- package/components/SkillsPanel.tsx +34 -8
- package/components/TaskBoard.tsx +3 -0
- package/components/WebTerminal.tsx +259 -45
- package/components/WorkspaceTree.tsx +221 -0
- package/components/WorkspaceView.tsx +2224 -0
- package/docs/LOCAL-DEPLOY.md +15 -15
- package/install.sh +2 -2
- package/lib/agents/claude-adapter.ts +104 -0
- package/lib/agents/generic-adapter.ts +64 -0
- package/lib/agents/index.ts +242 -0
- package/lib/agents/types.ts +70 -0
- package/lib/artifacts.ts +106 -0
- package/lib/cloudflared.ts +1 -1
- package/lib/delivery.ts +787 -0
- package/lib/forge-skills/forge-inbox.md +37 -0
- package/lib/forge-skills/forge-send.md +40 -0
- package/lib/forge-skills/forge-status.md +32 -0
- package/lib/forge-skills/forge-workspace-sync.md +37 -0
- package/lib/help-docs/00-overview.md +8 -2
- package/lib/help-docs/01-settings.md +159 -2
- package/lib/help-docs/05-pipelines.md +95 -6
- package/lib/help-docs/07-projects.md +35 -1
- package/lib/help-docs/11-workspace.md +204 -0
- package/lib/help-docs/CLAUDE.md +5 -2
- package/lib/init.ts +62 -12
- package/lib/pipeline.ts +537 -1
- package/lib/settings.ts +115 -22
- package/lib/skills.ts +249 -372
- package/lib/task-manager.ts +113 -33
- package/lib/telegram-bot.ts +33 -1
- package/lib/telegram-standalone.ts +1 -1
- package/lib/terminal-server.ts +2 -2
- package/lib/terminal-standalone.ts +1 -1
- package/lib/workspace/__tests__/state-machine.test.ts +388 -0
- package/lib/workspace/__tests__/workspace.test.ts +311 -0
- package/lib/workspace/agent-bus.ts +416 -0
- package/lib/workspace/agent-worker.ts +667 -0
- package/lib/workspace/backends/api-backend.ts +262 -0
- package/lib/workspace/backends/cli-backend.ts +479 -0
- package/lib/workspace/index.ts +82 -0
- package/lib/workspace/manager.ts +136 -0
- package/lib/workspace/orchestrator.ts +1804 -0
- package/lib/workspace/persistence.ts +310 -0
- package/lib/workspace/presets.ts +170 -0
- package/lib/workspace/skill-installer.ts +188 -0
- package/lib/workspace/smith-memory.ts +498 -0
- package/lib/workspace/types.ts +231 -0
- package/lib/workspace/watch-manager.ts +288 -0
- package/lib/workspace-standalone.ts +790 -0
- package/middleware.ts +1 -0
- package/next-env.d.ts +1 -1
- package/package.json +5 -2
- package/src/config/index.ts +13 -2
- package/src/core/db/database.ts +1 -0
- package/start.sh +10 -0
|
@@ -1,15 +1,69 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect, useCallback, lazy, Suspense } from 'react';
|
|
3
|
+
import { useState, useEffect, useCallback, useRef, lazy, Suspense } from 'react';
|
|
4
4
|
import { useSidebarResize } from '@/hooks/useSidebarResize';
|
|
5
|
+
import type { TaskLogEntry } from '@/src/types';
|
|
5
6
|
|
|
6
7
|
const PipelineEditor = lazy(() => import('./PipelineEditor'));
|
|
8
|
+
const ConversationEditor = lazy(() => import('./ConversationEditor'));
|
|
9
|
+
|
|
10
|
+
// ─── Live Task Log Hook ──────────────────────────────────
|
|
11
|
+
// Subscribes to SSE stream for a running task, returns live log entries
|
|
12
|
+
function useTaskStream(taskId: string | undefined, isRunning: boolean) {
|
|
13
|
+
const [log, setLog] = useState<TaskLogEntry[]>([]);
|
|
14
|
+
const [status, setStatus] = useState<string>('');
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (!taskId || !isRunning) { setLog([]); return; }
|
|
18
|
+
|
|
19
|
+
const es = new EventSource(`/api/tasks/${taskId}/stream`);
|
|
20
|
+
es.onmessage = (event) => {
|
|
21
|
+
try {
|
|
22
|
+
const data = JSON.parse(event.data);
|
|
23
|
+
if (data.type === 'log') setLog(prev => [...prev, data.entry]);
|
|
24
|
+
else if (data.type === 'status') setStatus(data.status);
|
|
25
|
+
else if (data.type === 'complete' && data.task) setLog(data.task.log);
|
|
26
|
+
} catch {}
|
|
27
|
+
};
|
|
28
|
+
es.onerror = () => es.close();
|
|
29
|
+
return () => es.close();
|
|
30
|
+
}, [taskId, isRunning]);
|
|
31
|
+
|
|
32
|
+
return { log, status };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ─── Compact log renderer ─────────────────────────────────
|
|
36
|
+
function LiveLog({ log, maxHeight = 200 }: { log: TaskLogEntry[]; maxHeight?: number }) {
|
|
37
|
+
const endRef = useRef<HTMLDivElement>(null);
|
|
38
|
+
useEffect(() => { endRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [log]);
|
|
39
|
+
|
|
40
|
+
if (log.length === 0) return <div className="text-[10px] text-[var(--text-secondary)] italic">Starting...</div>;
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div className="overflow-y-auto text-[9px] font-mono leading-relaxed space-y-0.5" style={{ maxHeight }}>
|
|
44
|
+
{log.slice(-50).map((entry, i) => (
|
|
45
|
+
<div key={i} className={
|
|
46
|
+
entry.type === 'result' ? 'text-green-400' :
|
|
47
|
+
entry.subtype === 'error' ? 'text-red-400' :
|
|
48
|
+
entry.type === 'system' ? 'text-yellow-400/70' :
|
|
49
|
+
'text-[var(--text-secondary)]'
|
|
50
|
+
}>
|
|
51
|
+
{entry.type === 'assistant' && entry.subtype === 'tool_use'
|
|
52
|
+
? `⚙ ${entry.tool || 'tool'}: ${entry.content.slice(0, 80)}${entry.content.length > 80 ? '...' : ''}`
|
|
53
|
+
: entry.content.slice(0, 200)}{entry.content.length > 200 ? '...' : ''}
|
|
54
|
+
</div>
|
|
55
|
+
))}
|
|
56
|
+
<div ref={endRef} />
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
7
60
|
|
|
8
61
|
interface WorkflowNode {
|
|
9
62
|
id: string;
|
|
10
63
|
project: string;
|
|
11
64
|
prompt: string;
|
|
12
65
|
mode?: 'claude' | 'shell';
|
|
66
|
+
agent?: string;
|
|
13
67
|
branch?: string;
|
|
14
68
|
dependsOn: string[];
|
|
15
69
|
outputs: { name: string; extract: string }[];
|
|
@@ -19,11 +73,18 @@ interface WorkflowNode {
|
|
|
19
73
|
|
|
20
74
|
interface Workflow {
|
|
21
75
|
name: string;
|
|
76
|
+
type?: 'dag' | 'conversation';
|
|
22
77
|
description?: string;
|
|
23
78
|
builtin?: boolean;
|
|
24
79
|
vars: Record<string, string>;
|
|
25
80
|
input: Record<string, string>;
|
|
26
81
|
nodes: Record<string, WorkflowNode>;
|
|
82
|
+
conversation?: {
|
|
83
|
+
agents: { id: string; agent: string; role: string }[];
|
|
84
|
+
maxRounds: number;
|
|
85
|
+
stopCondition?: string;
|
|
86
|
+
initialPrompt: string;
|
|
87
|
+
};
|
|
27
88
|
}
|
|
28
89
|
|
|
29
90
|
interface PipelineNodeState {
|
|
@@ -36,9 +97,20 @@ interface PipelineNodeState {
|
|
|
36
97
|
error?: string;
|
|
37
98
|
}
|
|
38
99
|
|
|
100
|
+
interface ConversationMessage {
|
|
101
|
+
round: number;
|
|
102
|
+
agentId: string;
|
|
103
|
+
agentName: string;
|
|
104
|
+
content: string;
|
|
105
|
+
timestamp: string;
|
|
106
|
+
taskId?: string;
|
|
107
|
+
status: 'pending' | 'running' | 'done' | 'failed';
|
|
108
|
+
}
|
|
109
|
+
|
|
39
110
|
interface Pipeline {
|
|
40
111
|
id: string;
|
|
41
112
|
workflowName: string;
|
|
113
|
+
type?: 'dag' | 'conversation';
|
|
42
114
|
status: 'running' | 'done' | 'failed' | 'cancelled';
|
|
43
115
|
input: Record<string, string>;
|
|
44
116
|
vars: Record<string, string>;
|
|
@@ -46,6 +118,17 @@ interface Pipeline {
|
|
|
46
118
|
nodeOrder: string[];
|
|
47
119
|
createdAt: string;
|
|
48
120
|
completedAt?: string;
|
|
121
|
+
conversation?: {
|
|
122
|
+
config: {
|
|
123
|
+
agents: { id: string; agent: string; role: string; project?: string }[];
|
|
124
|
+
maxRounds: number;
|
|
125
|
+
stopCondition?: string;
|
|
126
|
+
initialPrompt: string;
|
|
127
|
+
};
|
|
128
|
+
messages: ConversationMessage[];
|
|
129
|
+
currentRound: number;
|
|
130
|
+
currentAgentIndex: number;
|
|
131
|
+
};
|
|
49
132
|
}
|
|
50
133
|
|
|
51
134
|
const STATUS_ICON: Record<string, string> = {
|
|
@@ -64,6 +147,290 @@ const STATUS_COLOR: Record<string, string> = {
|
|
|
64
147
|
skipped: 'text-gray-500',
|
|
65
148
|
};
|
|
66
149
|
|
|
150
|
+
// ─── DAG Node Card with live logs ─────────────────────────
|
|
151
|
+
|
|
152
|
+
function DagNodeCard({ nodeId, node, nodeDef, onViewTask }: {
|
|
153
|
+
nodeId: string;
|
|
154
|
+
node: PipelineNodeState;
|
|
155
|
+
nodeDef?: WorkflowNode;
|
|
156
|
+
onViewTask?: (taskId: string) => void;
|
|
157
|
+
}) {
|
|
158
|
+
const isRunning = node.status === 'running';
|
|
159
|
+
const { log } = useTaskStream(node.taskId, isRunning);
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<div className={`border rounded-lg p-3 ${
|
|
163
|
+
isRunning ? 'border-yellow-500/50 bg-yellow-500/5' :
|
|
164
|
+
node.status === 'done' ? 'border-green-500/30 bg-green-500/5' :
|
|
165
|
+
node.status === 'failed' ? 'border-red-500/30 bg-red-500/5' :
|
|
166
|
+
'border-[var(--border)]'
|
|
167
|
+
}`}>
|
|
168
|
+
<div className="flex items-center gap-2">
|
|
169
|
+
<span className={STATUS_COLOR[node.status]}>{STATUS_ICON[node.status]}</span>
|
|
170
|
+
<span className="text-xs font-semibold text-[var(--text-primary)]">{nodeId}</span>
|
|
171
|
+
{nodeDef && nodeDef.mode !== 'shell' && (
|
|
172
|
+
<span className="text-[8px] px-1 rounded bg-purple-500/20 text-purple-400">{nodeDef.agent || 'default'}</span>
|
|
173
|
+
)}
|
|
174
|
+
{node.taskId && (
|
|
175
|
+
<button onClick={() => onViewTask?.(node.taskId!)} className="text-[9px] text-[var(--accent)] font-mono hover:underline">
|
|
176
|
+
task:{node.taskId}
|
|
177
|
+
</button>
|
|
178
|
+
)}
|
|
179
|
+
{node.iterations > 1 && <span className="text-[9px] text-yellow-400">iter {node.iterations}</span>}
|
|
180
|
+
<span className="text-[9px] text-[var(--text-secondary)] ml-auto">{node.status}</span>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
{/* Live log for running nodes */}
|
|
184
|
+
{isRunning && (
|
|
185
|
+
<div className="mt-2 p-2 bg-[var(--bg-tertiary)] rounded">
|
|
186
|
+
<LiveLog log={log} maxHeight={160} />
|
|
187
|
+
</div>
|
|
188
|
+
)}
|
|
189
|
+
|
|
190
|
+
{node.error && <div className="text-[10px] text-red-400 mt-1">{node.error}</div>}
|
|
191
|
+
|
|
192
|
+
{/* Outputs */}
|
|
193
|
+
{Object.keys(node.outputs).length > 0 && (
|
|
194
|
+
<div className="mt-2 space-y-1">
|
|
195
|
+
{Object.entries(node.outputs).map(([key, val]) => (
|
|
196
|
+
<details key={key} className="text-[10px]">
|
|
197
|
+
<summary className="cursor-pointer text-[var(--accent)]">output: {key} ({val.length} chars)</summary>
|
|
198
|
+
<pre className="mt-1 p-2 bg-[var(--bg-tertiary)] rounded text-[9px] text-[var(--text-secondary)] max-h-32 overflow-auto whitespace-pre-wrap">
|
|
199
|
+
{val.slice(0, 1000)}{val.length > 1000 ? '...' : ''}
|
|
200
|
+
</pre>
|
|
201
|
+
</details>
|
|
202
|
+
))}
|
|
203
|
+
</div>
|
|
204
|
+
)}
|
|
205
|
+
|
|
206
|
+
{node.startedAt && (
|
|
207
|
+
<div className="text-[8px] text-[var(--text-secondary)] mt-1">
|
|
208
|
+
{`Started: ${new Date(node.startedAt).toLocaleTimeString()}`}
|
|
209
|
+
{node.completedAt && ` · Done: ${new Date(node.completedAt).toLocaleTimeString()}`}
|
|
210
|
+
</div>
|
|
211
|
+
)}
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ─── Agent color palette for conversation bubbles ────────
|
|
217
|
+
const AGENT_COLORS = [
|
|
218
|
+
{ bg: 'bg-blue-500/10', border: 'border-blue-500/30', badge: 'bg-blue-500/20 text-blue-400', dot: 'text-blue-400' },
|
|
219
|
+
{ bg: 'bg-purple-500/10', border: 'border-purple-500/30', badge: 'bg-purple-500/20 text-purple-400', dot: 'text-purple-400' },
|
|
220
|
+
{ bg: 'bg-green-500/10', border: 'border-green-500/30', badge: 'bg-green-500/20 text-green-400', dot: 'text-green-400' },
|
|
221
|
+
{ bg: 'bg-orange-500/10', border: 'border-orange-500/30', badge: 'bg-orange-500/20 text-orange-400', dot: 'text-orange-400' },
|
|
222
|
+
{ bg: 'bg-pink-500/10', border: 'border-pink-500/30', badge: 'bg-pink-500/20 text-pink-400', dot: 'text-pink-400' },
|
|
223
|
+
];
|
|
224
|
+
|
|
225
|
+
function ConversationMessageBubble({ msg, colors, agentDef, isLeft, onViewTask }: {
|
|
226
|
+
msg: ConversationMessage; colors: typeof AGENT_COLORS[0]; agentDef?: { id: string; role: string };
|
|
227
|
+
isLeft: boolean; onViewTask?: (taskId: string) => void;
|
|
228
|
+
}) {
|
|
229
|
+
const isRunning = msg.status === 'running';
|
|
230
|
+
const { log } = useTaskStream(msg.taskId, isRunning);
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
<div className={`flex ${isLeft ? 'justify-start' : 'justify-end'}`}>
|
|
234
|
+
<div className={`max-w-[85%] border rounded-lg p-3 ${colors.bg} ${colors.border}`}>
|
|
235
|
+
{/* Agent header */}
|
|
236
|
+
<div className="flex items-center gap-2 mb-1.5">
|
|
237
|
+
<span className={`text-[9px] px-1.5 py-0.5 rounded font-medium ${colors.badge}`}>{msg.agentName}</span>
|
|
238
|
+
<span className="text-[8px] text-[var(--text-secondary)]">
|
|
239
|
+
{msg.agentId}{agentDef?.role ? ` — ${agentDef.role.slice(0, 40)}${agentDef.role.length > 40 ? '...' : ''}` : ''}
|
|
240
|
+
</span>
|
|
241
|
+
{isRunning && <span className="text-[8px] text-yellow-400 animate-pulse">● running</span>}
|
|
242
|
+
<span className="text-[8px] text-[var(--text-secondary)] ml-auto">R{msg.round}</span>
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
{/* Content */}
|
|
246
|
+
{isRunning ? (
|
|
247
|
+
<LiveLog log={log} maxHeight={250} />
|
|
248
|
+
) : msg.status === 'failed' ? (
|
|
249
|
+
<div className="text-[10px] text-red-400">{msg.content || 'Failed'}</div>
|
|
250
|
+
) : (
|
|
251
|
+
<div className="text-[10px] text-[var(--text-primary)] whitespace-pre-wrap leading-relaxed">
|
|
252
|
+
{msg.content.slice(0, 3000)}{msg.content.length > 3000 ? '\n\n[... truncated]' : ''}
|
|
253
|
+
</div>
|
|
254
|
+
)}
|
|
255
|
+
|
|
256
|
+
{/* Footer */}
|
|
257
|
+
<div className="flex items-center gap-2 mt-1.5">
|
|
258
|
+
{msg.taskId && (
|
|
259
|
+
<button onClick={() => onViewTask?.(msg.taskId!)} className="text-[8px] text-[var(--accent)] font-mono hover:underline">
|
|
260
|
+
task:{msg.taskId}
|
|
261
|
+
</button>
|
|
262
|
+
)}
|
|
263
|
+
<span className="text-[7px] text-[var(--text-secondary)] ml-auto">{new Date(msg.timestamp).toLocaleTimeString()}</span>
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const ConversationGraphView = lazy(() => import('./ConversationGraphView'));
|
|
271
|
+
const ConversationTerminalView = lazy(() => import('./ConversationTerminalView'));
|
|
272
|
+
|
|
273
|
+
function ConversationView({ pipeline, onViewTask }: { pipeline: Pipeline; onViewTask?: (taskId: string) => void }) {
|
|
274
|
+
const conv = pipeline.conversation!;
|
|
275
|
+
const { config, messages, currentRound } = conv;
|
|
276
|
+
const [injectText, setInjectText] = useState('');
|
|
277
|
+
const [injectTarget, setInjectTarget] = useState(config.agents[0]?.id || '');
|
|
278
|
+
const [injecting, setInjecting] = useState(false);
|
|
279
|
+
const [viewMode, setViewMode] = useState<'terminal' | 'graph' | 'chat'>('terminal');
|
|
280
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
281
|
+
|
|
282
|
+
// Assign stable colors per agent
|
|
283
|
+
const agentColorMap: Record<string, typeof AGENT_COLORS[0]> = {};
|
|
284
|
+
config.agents.forEach((a, i) => {
|
|
285
|
+
agentColorMap[a.id] = AGENT_COLORS[i % AGENT_COLORS.length];
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// Auto-scroll on new messages
|
|
289
|
+
useEffect(() => {
|
|
290
|
+
scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight, behavior: 'smooth' });
|
|
291
|
+
}, [messages.length]);
|
|
292
|
+
|
|
293
|
+
const handleInject = async () => {
|
|
294
|
+
if (!injectText.trim() || injecting) return;
|
|
295
|
+
setInjecting(true);
|
|
296
|
+
try {
|
|
297
|
+
await fetch(`/api/pipelines/${pipeline.id}`, {
|
|
298
|
+
method: 'POST',
|
|
299
|
+
headers: { 'Content-Type': 'application/json' },
|
|
300
|
+
body: JSON.stringify({ action: 'inject', agentId: injectTarget, message: injectText }),
|
|
301
|
+
});
|
|
302
|
+
setInjectText('');
|
|
303
|
+
} catch {}
|
|
304
|
+
setInjecting(false);
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
return (
|
|
308
|
+
<div className="flex-1 flex flex-col overflow-hidden">
|
|
309
|
+
{/* Conversation info bar */}
|
|
310
|
+
<div className="px-4 py-2 border-b border-[var(--border)] bg-[var(--bg-tertiary)]/50 shrink-0">
|
|
311
|
+
<div className="flex items-center gap-3">
|
|
312
|
+
<span className="text-[9px] px-1.5 py-0.5 rounded bg-[var(--accent)]/20 text-[var(--accent)] font-medium">Conversation</span>
|
|
313
|
+
<span className="text-[9px] text-[var(--text-secondary)]">Round {currentRound}/{config.maxRounds}</span>
|
|
314
|
+
<div className="flex items-center gap-2 ml-auto">
|
|
315
|
+
{/* View mode toggle */}
|
|
316
|
+
<div className="flex border border-[var(--border)] rounded overflow-hidden">
|
|
317
|
+
{(['terminal', 'graph', 'chat'] as const).map(mode => (
|
|
318
|
+
<button
|
|
319
|
+
key={mode}
|
|
320
|
+
onClick={() => setViewMode(mode)}
|
|
321
|
+
className={`text-[8px] px-2 py-0.5 capitalize ${viewMode === mode ? 'bg-[var(--accent)]/20 text-[var(--accent)]' : 'text-[var(--text-secondary)] hover:text-[var(--text-primary)]'}`}
|
|
322
|
+
>{mode}</button>
|
|
323
|
+
))}
|
|
324
|
+
</div>
|
|
325
|
+
{config.agents.map(a => {
|
|
326
|
+
const colors = agentColorMap[a.id];
|
|
327
|
+
const isRunning = messages.some(m => m.agentId === a.id && m.status === 'running');
|
|
328
|
+
return (
|
|
329
|
+
<span key={a.id} className={`text-[8px] px-1.5 py-0.5 rounded ${colors.badge} ${isRunning ? 'ring-1 ring-yellow-400/50' : ''}`}>
|
|
330
|
+
{isRunning ? '● ' : ''}{a.id} ({a.agent})
|
|
331
|
+
</span>
|
|
332
|
+
);
|
|
333
|
+
})}
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
{config.stopCondition && (
|
|
337
|
+
<div className="text-[8px] text-[var(--text-secondary)] mt-1">Stop: {config.stopCondition}</div>
|
|
338
|
+
)}
|
|
339
|
+
</div>
|
|
340
|
+
|
|
341
|
+
{/* Terminal / Graph / Chat view */}
|
|
342
|
+
{viewMode === 'terminal' ? (
|
|
343
|
+
<div className="flex-1 min-h-0">
|
|
344
|
+
<Suspense fallback={<div className="flex items-center justify-center h-full text-xs text-[var(--text-secondary)]">Loading...</div>}>
|
|
345
|
+
<ConversationTerminalView pipeline={pipeline} onViewTask={onViewTask} />
|
|
346
|
+
</Suspense>
|
|
347
|
+
</div>
|
|
348
|
+
) : viewMode === 'graph' ? (
|
|
349
|
+
<div className="flex-1 min-h-0">
|
|
350
|
+
<Suspense fallback={<div className="flex items-center justify-center h-full text-xs text-[var(--text-secondary)]">Loading graph...</div>}>
|
|
351
|
+
<ConversationGraphView pipeline={pipeline} />
|
|
352
|
+
</Suspense>
|
|
353
|
+
</div>
|
|
354
|
+
) : (
|
|
355
|
+
<>
|
|
356
|
+
{/* Initial prompt */}
|
|
357
|
+
<div className="px-4 pt-3">
|
|
358
|
+
<div className="border border-[var(--border)] rounded-lg p-3 bg-[var(--bg-tertiary)]/50">
|
|
359
|
+
<div className="text-[9px] text-[var(--text-secondary)] font-medium mb-1">Initial Prompt</div>
|
|
360
|
+
<div className="text-[11px] text-[var(--text-primary)] whitespace-pre-wrap">{config.initialPrompt}</div>
|
|
361
|
+
</div>
|
|
362
|
+
</div>
|
|
363
|
+
|
|
364
|
+
{/* Messages — chat-like view with live logs */}
|
|
365
|
+
<div ref={scrollRef} className="flex-1 overflow-y-auto px-4 py-3 space-y-3">
|
|
366
|
+
{messages.map((msg, i) => {
|
|
367
|
+
const colors = agentColorMap[msg.agentId] || AGENT_COLORS[0];
|
|
368
|
+
const agentDef = config.agents.find(a => a.id === msg.agentId);
|
|
369
|
+
const isLeft = config.agents.indexOf(agentDef!) % 2 === 0;
|
|
370
|
+
|
|
371
|
+
return (
|
|
372
|
+
<ConversationMessageBubble
|
|
373
|
+
key={`${msg.taskId || i}-${msg.status}`}
|
|
374
|
+
msg={msg}
|
|
375
|
+
colors={colors}
|
|
376
|
+
agentDef={agentDef}
|
|
377
|
+
isLeft={isLeft}
|
|
378
|
+
onViewTask={onViewTask}
|
|
379
|
+
/>
|
|
380
|
+
);
|
|
381
|
+
})}
|
|
382
|
+
|
|
383
|
+
{/* Completion indicator */}
|
|
384
|
+
{pipeline.status === 'done' && (
|
|
385
|
+
<div className="flex justify-center py-2">
|
|
386
|
+
<span className="text-[10px] px-3 py-1 rounded-full bg-green-500/10 text-green-400 border border-green-500/30">
|
|
387
|
+
Conversation complete — {messages.length} messages in {Math.max(...messages.map(m => m.round), 0)} rounds
|
|
388
|
+
</span>
|
|
389
|
+
</div>
|
|
390
|
+
)}
|
|
391
|
+
{pipeline.status === 'failed' && (
|
|
392
|
+
<div className="flex justify-center py-2">
|
|
393
|
+
<span className="text-[10px] px-3 py-1 rounded-full bg-red-500/10 text-red-400 border border-red-500/30">
|
|
394
|
+
Conversation failed
|
|
395
|
+
</span>
|
|
396
|
+
</div>
|
|
397
|
+
)}
|
|
398
|
+
</div>
|
|
399
|
+
</>
|
|
400
|
+
)}
|
|
401
|
+
|
|
402
|
+
{/* Inject command bar */}
|
|
403
|
+
{pipeline.status === 'running' && (
|
|
404
|
+
<div className="px-4 py-2 border-t border-[var(--border)] bg-[var(--bg-tertiary)]/50 shrink-0">
|
|
405
|
+
<div className="flex items-center gap-2">
|
|
406
|
+
<select
|
|
407
|
+
value={injectTarget}
|
|
408
|
+
onChange={e => setInjectTarget(e.target.value)}
|
|
409
|
+
className="text-[10px] bg-[var(--bg-tertiary)] border border-[var(--border)] rounded px-2 py-1 text-[var(--text-primary)]"
|
|
410
|
+
>
|
|
411
|
+
{config.agents.map(a => (
|
|
412
|
+
<option key={a.id} value={a.id}>@{a.id}</option>
|
|
413
|
+
))}
|
|
414
|
+
</select>
|
|
415
|
+
<input
|
|
416
|
+
value={injectText}
|
|
417
|
+
onChange={e => setInjectText(e.target.value)}
|
|
418
|
+
onKeyDown={e => e.key === 'Enter' && !e.shiftKey && handleInject()}
|
|
419
|
+
placeholder="Send instruction to agent..."
|
|
420
|
+
className="flex-1 text-[10px] bg-[var(--bg-primary)] border border-[var(--border)] rounded px-2 py-1 text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)]"
|
|
421
|
+
/>
|
|
422
|
+
<button
|
|
423
|
+
onClick={handleInject}
|
|
424
|
+
disabled={!injectText.trim() || injecting}
|
|
425
|
+
className="text-[10px] px-3 py-1 bg-[var(--accent)] text-white rounded hover:opacity-90 disabled:opacity-50"
|
|
426
|
+
>Send</button>
|
|
427
|
+
</div>
|
|
428
|
+
</div>
|
|
429
|
+
)}
|
|
430
|
+
</div>
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
67
434
|
export default function PipelineView({ onViewTask, focusPipelineId, onFocusHandled }: { onViewTask?: (taskId: string) => void; focusPipelineId?: string | null; onFocusHandled?: () => void }) {
|
|
68
435
|
const { sidebarWidth, onSidebarDragStart } = useSidebarResize({ defaultWidth: 256, minWidth: 140, maxWidth: 480 });
|
|
69
436
|
const [pipelines, setPipelines] = useState<Pipeline[]>([]);
|
|
@@ -77,21 +444,26 @@ export default function PipelineView({ onViewTask, focusPipelineId, onFocusHandl
|
|
|
77
444
|
const [creating, setCreating] = useState(false);
|
|
78
445
|
const [showEditor, setShowEditor] = useState(false);
|
|
79
446
|
const [editorYaml, setEditorYaml] = useState<string | undefined>(undefined);
|
|
447
|
+
const [editorIsConversation, setEditorIsConversation] = useState(false);
|
|
80
448
|
const [showImport, setShowImport] = useState(false);
|
|
81
449
|
const [importYaml, setImportYaml] = useState('');
|
|
450
|
+
const [agents, setAgents] = useState<{ id: string; name: string; detected?: boolean }[]>([]);
|
|
82
451
|
|
|
83
452
|
const fetchData = useCallback(async () => {
|
|
84
|
-
const [pRes, wRes, projRes] = await Promise.all([
|
|
453
|
+
const [pRes, wRes, projRes, agentRes] = await Promise.all([
|
|
85
454
|
fetch('/api/pipelines'),
|
|
86
455
|
fetch('/api/pipelines?type=workflows'),
|
|
87
456
|
fetch('/api/projects'),
|
|
457
|
+
fetch('/api/agents'),
|
|
88
458
|
]);
|
|
89
459
|
const pData = await pRes.json();
|
|
90
460
|
const wData = await wRes.json();
|
|
91
461
|
const projData = await projRes.json();
|
|
462
|
+
const agentData = await agentRes.json();
|
|
92
463
|
if (Array.isArray(pData)) setPipelines(pData);
|
|
93
464
|
if (Array.isArray(wData)) setWorkflows(wData);
|
|
94
465
|
if (Array.isArray(projData)) setProjects(projData.map((p: any) => ({ name: p.name, path: p.path })));
|
|
466
|
+
if (Array.isArray(agentData?.agents)) setAgents(agentData.agents);
|
|
95
467
|
}, []);
|
|
96
468
|
|
|
97
469
|
useEffect(() => {
|
|
@@ -166,6 +538,31 @@ export default function PipelineView({ onViewTask, focusPipelineId, onFocusHandl
|
|
|
166
538
|
fetchData();
|
|
167
539
|
};
|
|
168
540
|
|
|
541
|
+
const generateConversationTemplate = () => {
|
|
542
|
+
const detectedAgents = agents.filter(a => a.detected);
|
|
543
|
+
const agentEntries = detectedAgents.length >= 2
|
|
544
|
+
? detectedAgents.slice(0, 2)
|
|
545
|
+
: [{ id: 'claude', name: 'Claude Code' }, { id: 'claude', name: 'Claude Code' }];
|
|
546
|
+
|
|
547
|
+
return `name: my-conversation
|
|
548
|
+
type: conversation
|
|
549
|
+
description: "Multi-agent collaboration"
|
|
550
|
+
input:
|
|
551
|
+
project: "Project name"
|
|
552
|
+
task: "Task description"
|
|
553
|
+
agents:
|
|
554
|
+
- id: designer
|
|
555
|
+
agent: ${agentEntries[0].id}
|
|
556
|
+
role: "You are a software architect. Design the solution and review implementations."
|
|
557
|
+
- id: builder
|
|
558
|
+
agent: ${agentEntries[1].id}
|
|
559
|
+
role: "You are a developer. Implement what the designer proposes."
|
|
560
|
+
max_rounds: 5
|
|
561
|
+
stop_condition: "both agents say DONE"
|
|
562
|
+
initial_prompt: "{{input.task}}"
|
|
563
|
+
`;
|
|
564
|
+
};
|
|
565
|
+
|
|
169
566
|
const currentWorkflow = workflows.find(w => w.name === selectedWorkflow);
|
|
170
567
|
|
|
171
568
|
return (
|
|
@@ -179,9 +576,13 @@ export default function PipelineView({ onViewTask, focusPipelineId, onFocusHandl
|
|
|
179
576
|
className="text-[9px] text-green-400 hover:underline"
|
|
180
577
|
>Import</button>
|
|
181
578
|
<button
|
|
182
|
-
onClick={() => { setEditorYaml(undefined); setShowEditor(true); }}
|
|
579
|
+
onClick={() => { setEditorYaml(undefined); setEditorIsConversation(false); setShowEditor(true); }}
|
|
183
580
|
className="text-[9px] text-[var(--accent)] hover:underline"
|
|
184
|
-
>+
|
|
581
|
+
>+ DAG</button>
|
|
582
|
+
<button
|
|
583
|
+
onClick={() => { setImportYaml(generateConversationTemplate()); setShowImport(true); }}
|
|
584
|
+
className="text-[9px] text-purple-400 hover:underline"
|
|
585
|
+
>+ Conversation</button>
|
|
185
586
|
</div>
|
|
186
587
|
|
|
187
588
|
{/* Import form */}
|
|
@@ -331,7 +732,8 @@ export default function PipelineView({ onViewTask, focusPipelineId, onFocusHandl
|
|
|
331
732
|
const res = await fetch(`/api/pipelines?type=workflow-yaml&name=${encodeURIComponent(w.name)}`);
|
|
332
733
|
const data = await res.json();
|
|
333
734
|
setEditorYaml(data.yaml || undefined);
|
|
334
|
-
|
|
735
|
+
setEditorIsConversation(w.type === 'conversation' || (data.yaml || '').includes('type: conversation'));
|
|
736
|
+
} catch { setEditorYaml(undefined); setEditorIsConversation(false); }
|
|
335
737
|
setShowEditor(true);
|
|
336
738
|
}}
|
|
337
739
|
className="text-[8px] text-green-400 hover:underline shrink-0"
|
|
@@ -355,6 +757,11 @@ export default function PipelineView({ onViewTask, focusPipelineId, onFocusHandl
|
|
|
355
757
|
<div className="flex items-center gap-1.5">
|
|
356
758
|
<span className={`text-[9px] ${STATUS_COLOR[p.status]}`}>●</span>
|
|
357
759
|
<span className="text-[9px] text-[var(--text-secondary)] font-mono">{p.id.slice(0, 8)}</span>
|
|
760
|
+
{p.type === 'conversation' ? (
|
|
761
|
+
<span className="text-[7px] px-1 rounded bg-[var(--accent)]/15 text-[var(--accent)]">
|
|
762
|
+
R{p.conversation?.currentRound || 0}/{p.conversation?.config.maxRounds || '?'}
|
|
763
|
+
</span>
|
|
764
|
+
) : (
|
|
358
765
|
<div className="flex gap-0.5 ml-1">
|
|
359
766
|
{p.nodeOrder.map(nodeId => (
|
|
360
767
|
<span key={nodeId} className={`text-[8px] ${STATUS_COLOR[p.nodes[nodeId]?.status || 'pending']}`}>
|
|
@@ -362,6 +769,7 @@ export default function PipelineView({ onViewTask, focusPipelineId, onFocusHandl
|
|
|
362
769
|
</span>
|
|
363
770
|
))}
|
|
364
771
|
</div>
|
|
772
|
+
)}
|
|
365
773
|
<span className="text-[8px] text-[var(--text-secondary)] ml-auto">
|
|
366
774
|
{new Date(p.createdAt).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}
|
|
367
775
|
</span>
|
|
@@ -391,6 +799,23 @@ export default function PipelineView({ onViewTask, focusPipelineId, onFocusHandl
|
|
|
391
799
|
{/* Right — Pipeline detail / Editor */}
|
|
392
800
|
<main className="flex-1 flex flex-col min-w-0 overflow-hidden">
|
|
393
801
|
{showEditor ? (
|
|
802
|
+
editorIsConversation ? (
|
|
803
|
+
<Suspense fallback={<div className="flex-1 flex items-center justify-center text-xs text-[var(--text-secondary)]">Loading editor...</div>}>
|
|
804
|
+
<ConversationEditor
|
|
805
|
+
initialYaml={editorYaml || ''}
|
|
806
|
+
onSave={async (yaml) => {
|
|
807
|
+
await fetch('/api/pipelines', {
|
|
808
|
+
method: 'POST',
|
|
809
|
+
headers: { 'Content-Type': 'application/json' },
|
|
810
|
+
body: JSON.stringify({ action: 'save-workflow', yaml }),
|
|
811
|
+
});
|
|
812
|
+
setShowEditor(false);
|
|
813
|
+
fetchData();
|
|
814
|
+
}}
|
|
815
|
+
onClose={() => setShowEditor(false)}
|
|
816
|
+
/>
|
|
817
|
+
</Suspense>
|
|
818
|
+
) : (
|
|
394
819
|
<Suspense fallback={<div className="flex-1 flex items-center justify-center text-xs text-[var(--text-secondary)]">Loading editor...</div>}>
|
|
395
820
|
<PipelineEditor
|
|
396
821
|
initialYaml={editorYaml}
|
|
@@ -406,6 +831,7 @@ export default function PipelineView({ onViewTask, focusPipelineId, onFocusHandl
|
|
|
406
831
|
onClose={() => setShowEditor(false)}
|
|
407
832
|
/>
|
|
408
833
|
</Suspense>
|
|
834
|
+
)
|
|
409
835
|
) : selectedPipeline ? (
|
|
410
836
|
<>
|
|
411
837
|
{/* Header */}
|
|
@@ -415,6 +841,9 @@ export default function PipelineView({ onViewTask, focusPipelineId, onFocusHandl
|
|
|
415
841
|
{STATUS_ICON[selectedPipeline.status]}
|
|
416
842
|
</span>
|
|
417
843
|
<span className="text-sm font-semibold text-[var(--text-primary)]">{selectedPipeline.workflowName}</span>
|
|
844
|
+
{selectedPipeline.type === 'conversation' && (
|
|
845
|
+
<span className="text-[8px] px-1.5 py-0.5 rounded bg-[var(--accent)]/20 text-[var(--accent)]">conversation</span>
|
|
846
|
+
)}
|
|
418
847
|
<span className="text-[10px] text-[var(--text-secondary)] font-mono">{selectedPipeline.id}</span>
|
|
419
848
|
<div className="flex items-center gap-2 ml-auto">
|
|
420
849
|
{selectedPipeline.status === 'running' && (
|
|
@@ -444,75 +873,31 @@ export default function PipelineView({ onViewTask, focusPipelineId, onFocusHandl
|
|
|
444
873
|
)}
|
|
445
874
|
</div>
|
|
446
875
|
|
|
447
|
-
{/* DAG visualization */}
|
|
448
|
-
|
|
876
|
+
{/* Conversation or DAG visualization */}
|
|
877
|
+
{selectedPipeline.type === 'conversation' && selectedPipeline.conversation ? (
|
|
878
|
+
<ConversationView
|
|
879
|
+
pipeline={selectedPipeline}
|
|
880
|
+
onViewTask={onViewTask}
|
|
881
|
+
/>
|
|
882
|
+
) : (
|
|
883
|
+
<div className="p-4 space-y-2 overflow-y-auto">
|
|
449
884
|
{selectedPipeline.nodeOrder.map((nodeId, idx) => {
|
|
450
885
|
const node = selectedPipeline.nodes[nodeId];
|
|
886
|
+
const wf = workflows.find(w => w.name === selectedPipeline.workflowName);
|
|
887
|
+
const nodeDef = wf?.nodes?.[nodeId];
|
|
451
888
|
return (
|
|
452
889
|
<div key={nodeId}>
|
|
453
|
-
{/* Connection line */}
|
|
454
890
|
{idx > 0 && (
|
|
455
891
|
<div className="flex items-center pl-5 py-1">
|
|
456
892
|
<div className="w-px h-4 bg-[var(--border)]" />
|
|
457
893
|
</div>
|
|
458
894
|
)}
|
|
459
|
-
|
|
460
|
-
{/* Node card */}
|
|
461
|
-
<div className={`border rounded-lg p-3 ${
|
|
462
|
-
node.status === 'running' ? 'border-yellow-500/50 bg-yellow-500/5' :
|
|
463
|
-
node.status === 'done' ? 'border-green-500/30 bg-green-500/5' :
|
|
464
|
-
node.status === 'failed' ? 'border-red-500/30 bg-red-500/5' :
|
|
465
|
-
'border-[var(--border)]'
|
|
466
|
-
}`}>
|
|
467
|
-
<div className="flex items-center gap-2">
|
|
468
|
-
<span className={STATUS_COLOR[node.status]}>{STATUS_ICON[node.status]}</span>
|
|
469
|
-
<span className="text-xs font-semibold text-[var(--text-primary)]">{nodeId}</span>
|
|
470
|
-
{node.taskId && (
|
|
471
|
-
<button
|
|
472
|
-
onClick={() => onViewTask?.(node.taskId!)}
|
|
473
|
-
className="text-[9px] text-[var(--accent)] font-mono hover:underline"
|
|
474
|
-
>
|
|
475
|
-
task:{node.taskId}
|
|
476
|
-
</button>
|
|
477
|
-
)}
|
|
478
|
-
{node.iterations > 1 && (
|
|
479
|
-
<span className="text-[9px] text-yellow-400">iter {node.iterations}</span>
|
|
480
|
-
)}
|
|
481
|
-
<span className="text-[9px] text-[var(--text-secondary)] ml-auto">{node.status}</span>
|
|
482
|
-
</div>
|
|
483
|
-
|
|
484
|
-
{node.error && (
|
|
485
|
-
<div className="text-[10px] text-red-400 mt-1">{node.error}</div>
|
|
486
|
-
)}
|
|
487
|
-
|
|
488
|
-
{/* Outputs */}
|
|
489
|
-
{Object.keys(node.outputs).length > 0 && (
|
|
490
|
-
<div className="mt-2 space-y-1">
|
|
491
|
-
{Object.entries(node.outputs).map(([key, val]) => (
|
|
492
|
-
<details key={key} className="text-[10px]">
|
|
493
|
-
<summary className="cursor-pointer text-[var(--accent)]">
|
|
494
|
-
output: {key} ({val.length} chars)
|
|
495
|
-
</summary>
|
|
496
|
-
<pre className="mt-1 p-2 bg-[var(--bg-tertiary)] rounded text-[9px] text-[var(--text-secondary)] max-h-32 overflow-auto whitespace-pre-wrap">
|
|
497
|
-
{val.slice(0, 1000)}{val.length > 1000 ? '...' : ''}
|
|
498
|
-
</pre>
|
|
499
|
-
</details>
|
|
500
|
-
))}
|
|
501
|
-
</div>
|
|
502
|
-
)}
|
|
503
|
-
|
|
504
|
-
{/* Timing */}
|
|
505
|
-
{node.startedAt && (
|
|
506
|
-
<div className="text-[8px] text-[var(--text-secondary)] mt-1">
|
|
507
|
-
{node.startedAt && `Started: ${new Date(node.startedAt).toLocaleTimeString()}`}
|
|
508
|
-
{node.completedAt && ` · Done: ${new Date(node.completedAt).toLocaleTimeString()}`}
|
|
509
|
-
</div>
|
|
510
|
-
)}
|
|
511
|
-
</div>
|
|
895
|
+
<DagNodeCard nodeId={nodeId} node={node} nodeDef={nodeDef} onViewTask={onViewTask} />
|
|
512
896
|
</div>
|
|
513
897
|
);
|
|
514
898
|
})}
|
|
515
899
|
</div>
|
|
900
|
+
)}
|
|
516
901
|
</>
|
|
517
902
|
) : activeWorkflow ? (() => {
|
|
518
903
|
const w = workflows.find(wf => wf.name === activeWorkflow);
|
|
@@ -524,6 +909,7 @@ export default function PipelineView({ onViewTask, focusPipelineId, onFocusHandl
|
|
|
524
909
|
<div className="px-4 py-3 border-b border-[var(--border)] shrink-0">
|
|
525
910
|
<div className="flex items-center gap-2">
|
|
526
911
|
<span className="text-sm font-semibold text-[var(--text-primary)]">{w.name}</span>
|
|
912
|
+
{w.type === 'conversation' && <span className="text-[8px] px-1.5 py-0.5 rounded bg-[var(--accent)]/20 text-[var(--accent)]">conversation</span>}
|
|
527
913
|
{w.builtin && <span className="text-[8px] px-1.5 py-0.5 rounded bg-[var(--bg-tertiary)] text-[var(--text-secondary)]">built-in</span>}
|
|
528
914
|
<div className="ml-auto flex gap-2">
|
|
529
915
|
<button
|
|
@@ -536,7 +922,8 @@ export default function PipelineView({ onViewTask, focusPipelineId, onFocusHandl
|
|
|
536
922
|
const res = await fetch(`/api/pipelines?type=workflow-yaml&name=${encodeURIComponent(w.name)}`);
|
|
537
923
|
const data = await res.json();
|
|
538
924
|
setEditorYaml(data.yaml || undefined);
|
|
539
|
-
|
|
925
|
+
setEditorIsConversation(w.type === 'conversation' || (data.yaml || '').includes('type: conversation'));
|
|
926
|
+
} catch { setEditorYaml(undefined); setEditorIsConversation(false); }
|
|
540
927
|
setShowEditor(true);
|
|
541
928
|
}}
|
|
542
929
|
className="text-[10px] px-3 py-1 border border-[var(--border)] text-[var(--text-secondary)] rounded hover:text-[var(--text-primary)]"
|
|
@@ -553,7 +940,37 @@ export default function PipelineView({ onViewTask, focusPipelineId, onFocusHandl
|
|
|
553
940
|
)}
|
|
554
941
|
</div>
|
|
555
942
|
|
|
556
|
-
{/* Node flow visualization */}
|
|
943
|
+
{/* Conversation or Node flow visualization */}
|
|
944
|
+
{w.type === 'conversation' && w.conversation ? (
|
|
945
|
+
<div className="p-4 space-y-3">
|
|
946
|
+
{/* Initial prompt */}
|
|
947
|
+
<div className="border border-[var(--border)] rounded-lg p-3 bg-[var(--bg-tertiary)]">
|
|
948
|
+
<div className="text-[9px] text-[var(--text-secondary)] font-medium mb-1">Initial Prompt</div>
|
|
949
|
+
<p className="text-[10px] text-[var(--text-primary)]">{w.conversation.initialPrompt}</p>
|
|
950
|
+
</div>
|
|
951
|
+
{/* Agents */}
|
|
952
|
+
<div className="text-[9px] text-[var(--text-secondary)] font-medium">Agents ({w.conversation.agents.length})</div>
|
|
953
|
+
<div className="space-y-2">
|
|
954
|
+
{w.conversation.agents.map((a, i) => {
|
|
955
|
+
const colors = AGENT_COLORS[i % AGENT_COLORS.length];
|
|
956
|
+
return (
|
|
957
|
+
<div key={a.id} className={`border rounded-lg p-3 ${colors.bg} ${colors.border}`}>
|
|
958
|
+
<div className="flex items-center gap-2">
|
|
959
|
+
<span className={`text-[9px] px-1.5 py-0.5 rounded font-medium ${colors.badge}`}>{a.agent}</span>
|
|
960
|
+
<span className="text-[11px] font-semibold text-[var(--text-primary)]">{a.id}</span>
|
|
961
|
+
</div>
|
|
962
|
+
{a.role && <p className="text-[9px] text-[var(--text-secondary)] mt-1">{a.role}</p>}
|
|
963
|
+
</div>
|
|
964
|
+
);
|
|
965
|
+
})}
|
|
966
|
+
</div>
|
|
967
|
+
{/* Config */}
|
|
968
|
+
<div className="text-[9px] text-[var(--text-secondary)] space-y-0.5">
|
|
969
|
+
<div>Max rounds: {w.conversation.maxRounds}</div>
|
|
970
|
+
{w.conversation.stopCondition && <div>Stop: {w.conversation.stopCondition}</div>}
|
|
971
|
+
</div>
|
|
972
|
+
</div>
|
|
973
|
+
) : (
|
|
557
974
|
<div className="p-4 space-y-2">
|
|
558
975
|
{nodeEntries.map(([nodeId, node], i) => (
|
|
559
976
|
<div key={nodeId}>
|
|
@@ -568,7 +985,7 @@ export default function PipelineView({ onViewTask, focusPipelineId, onFocusHandl
|
|
|
568
985
|
<div className="flex items-center gap-2">
|
|
569
986
|
<span className={`text-[9px] px-1.5 py-0.5 rounded font-medium ${
|
|
570
987
|
node.mode === 'shell' ? 'bg-yellow-500/20 text-yellow-400' : 'bg-purple-500/20 text-purple-400'
|
|
571
|
-
}`}>{node.mode === 'shell' ? 'shell' : '
|
|
988
|
+
}`}>{node.mode === 'shell' ? 'shell' : (node.agent || 'default')}</span>
|
|
572
989
|
<span className="text-[11px] font-semibold text-[var(--text-primary)]">{nodeId}</span>
|
|
573
990
|
{node.project && <span className="text-[9px] text-[var(--text-secondary)] ml-auto">{node.project}</span>}
|
|
574
991
|
</div>
|
|
@@ -587,6 +1004,7 @@ export default function PipelineView({ onViewTask, focusPipelineId, onFocusHandl
|
|
|
587
1004
|
</div>
|
|
588
1005
|
))}
|
|
589
1006
|
</div>
|
|
1007
|
+
)}
|
|
590
1008
|
</div>
|
|
591
1009
|
);
|
|
592
1010
|
})() : (
|