@aion0/forge 0.6.1 → 0.8.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/.forge/mcp.json +8 -0
- package/.forge/worktrees/pipeline-0a33c50d/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-0a33c50d/lib/help-docs/07-projects.md +1 -1
- package/.forge/worktrees/pipeline-2ba01c10/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-2ba01c10/lib/help-docs/07-projects.md +1 -1
- package/.forge/worktrees/pipeline-3156a8b3/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-3156a8b3/lib/help-docs/07-projects.md +1 -1
- package/.forge/worktrees/pipeline-316c6574/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-316c6574/lib/help-docs/07-projects.md +1 -1
- package/.forge/worktrees/pipeline-44a94121/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-44a94121/lib/help-docs/07-projects.md +1 -1
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/07-projects.md +1 -1
- package/.forge/worktrees/pipeline-d1757a50/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-d1757a50/lib/help-docs/07-projects.md +1 -1
- package/.forge/worktrees/pipeline-d59c2fe2/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-d59c2fe2/lib/help-docs/07-projects.md +1 -1
- package/.forge/worktrees/pipeline-d6a6ef23/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-d6a6ef23/lib/help-docs/07-projects.md +1 -1
- package/.forge/worktrees/pipeline-e7f78b7a/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-e7f78b7a/lib/help-docs/07-projects.md +1 -1
- package/.forge/worktrees/pipeline-e97c13c7/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-e97c13c7/lib/help-docs/07-projects.md +1 -1
- package/.forge/worktrees/pipeline-ecd7cb0f/lib/help-docs/01-settings.md +5 -5
- package/.forge/worktrees/pipeline-ecd7cb0f/lib/help-docs/07-projects.md +1 -1
- package/CLAUDE.md +2 -2
- package/RELEASE_NOTES.md +101 -5
- package/app/api/auth/check/route.ts +18 -0
- package/app/api/browser-bridge/route.ts +70 -0
- package/app/api/chat/sessions/[id]/events/route.ts +17 -0
- package/app/api/chat/sessions/[id]/fork/route.ts +15 -0
- package/app/api/chat/sessions/[id]/messages/route.ts +21 -0
- package/app/api/chat/sessions/[id]/route.ts +23 -0
- package/app/api/chat/sessions/route.ts +12 -0
- package/app/api/chat/temper-ping/route.ts +18 -0
- package/app/api/chat-proxy/[...path]/route.ts +83 -0
- package/app/api/connector-tool/route.ts +38 -0
- package/app/api/connectors/[id]/settings/route.ts +112 -0
- package/app/api/connectors/route.ts +108 -0
- package/app/api/health/tools/route.ts +14 -0
- package/app/api/issue-scanner-gitlab/route.ts +95 -0
- package/app/api/jobs/[id]/reset_dedup/route.ts +15 -0
- package/app/api/jobs/[id]/route.ts +31 -0
- package/app/api/jobs/[id]/run/route.ts +44 -0
- package/app/api/jobs/[id]/runs/[runId]/route.ts +15 -0
- package/app/api/jobs/[id]/runs/route.ts +15 -0
- package/app/api/jobs/preview/route.ts +193 -0
- package/app/api/jobs/route.ts +36 -0
- package/app/api/notify/test/route.ts +39 -7
- package/app/api/pipelines/[id]/route.ts +10 -1
- package/app/api/pipelines/route.ts +16 -2
- package/app/api/plugins/route.ts +40 -8
- package/app/api/project-sessions/route.ts +50 -10
- package/app/api/settings/route.ts +13 -0
- package/app/chat/page.tsx +531 -0
- package/bin/forge-server.mjs +3 -1
- package/cli/chat.ts +283 -0
- package/cli/jobs.ts +176 -0
- package/cli/mw.ts +28 -1
- package/cli/worktree.ts +245 -0
- package/components/ConnectorsPanel.tsx +275 -0
- package/components/Dashboard.tsx +90 -37
- package/components/JobsView.tsx +361 -0
- package/components/LogViewer.tsx +12 -2
- package/components/PipelineView.tsx +275 -56
- package/components/PluginsPanel.tsx +3 -1
- package/components/SettingsModal.tsx +229 -40
- package/components/SkillsPanel.tsx +12 -4
- package/components/TerminalLauncher.tsx +3 -1
- package/components/WebTerminal.tsx +32 -9
- package/components/WorkspaceView.tsx +18 -10
- package/docs/Connector-DeclarativeExtract-Handoff.md +471 -0
- package/docs/Connector-DeclarativeExtract-Spec.md +364 -0
- package/docs/Implementation-Plan-Browser-Agent.md +487 -0
- package/docs/Jobs-Design.md +240 -0
- package/docs/LOCAL-DEPLOY.md +3 -3
- package/docs/RFC-Browser-Connectors.md +509 -0
- package/lib/agents/index.ts +44 -6
- package/lib/agents/types.ts +1 -1
- package/lib/browser-bridge-standalone.ts +317 -0
- package/lib/builtin-plugins/github-api.yaml +93 -0
- package/lib/builtin-plugins/gitlab.yaml +860 -0
- package/lib/builtin-plugins/mantis.probe.js +176 -0
- package/lib/builtin-plugins/mantis.yaml +964 -0
- package/lib/builtin-plugins/pmdb.yaml +178 -0
- package/lib/builtin-plugins/teams.yaml +913 -0
- package/lib/chat/__test__/smoke.ts +30 -0
- package/lib/chat/agent-loop.ts +523 -0
- package/lib/chat/bridge-client.ts +59 -0
- package/lib/chat/llm/anthropic.ts +99 -0
- package/lib/chat/llm/index.ts +20 -0
- package/lib/chat/llm/openai.ts +215 -0
- package/lib/chat/llm/types.ts +42 -0
- package/lib/chat/local-memory.ts +300 -0
- package/lib/chat/memory-store.ts +87 -0
- package/lib/chat/memory-tools.ts +157 -0
- package/lib/chat/protocols/http.ts +118 -0
- package/lib/chat/protocols/shell.ts +101 -0
- package/lib/chat/proxy.ts +51 -0
- package/lib/chat/session-store.ts +272 -0
- package/lib/chat/telegram-bridge.ts +276 -0
- package/lib/chat/temper.ts +281 -0
- package/lib/chat/tool-dispatcher.ts +190 -0
- package/lib/chat/types.ts +50 -0
- package/lib/chat-standalone.ts +286 -0
- package/lib/crypto.ts +1 -1
- package/lib/health.ts +131 -0
- package/lib/help-docs/00-overview.md +2 -1
- package/lib/help-docs/01-settings.md +46 -25
- package/lib/help-docs/07-projects.md +1 -1
- package/lib/help-docs/10-troubleshooting.md +10 -2
- package/lib/help-docs/16-gitlab-autofix.md +114 -0
- package/lib/help-docs/17-connectors.md +322 -0
- package/lib/help-docs/18-chrome-mcp.md +134 -0
- package/lib/help-docs/19-jobs.md +140 -0
- package/lib/help-docs/20-mantis-bug-fix.md +115 -0
- package/lib/help-docs/CLAUDE.md +10 -0
- package/lib/init.ts +137 -50
- package/lib/iso-time.ts +30 -0
- package/lib/issue-scanner-gitlab.ts +281 -0
- package/lib/jobs/dispatcher.ts +217 -0
- package/lib/jobs/scheduler.ts +334 -0
- package/lib/jobs/store.ts +319 -0
- package/lib/jobs/types.ts +117 -0
- package/lib/pipeline-scheduler.ts +1 -6
- package/lib/pipeline.ts +790 -10
- package/lib/plugins/registry.ts +133 -8
- package/lib/plugins/templates.ts +83 -0
- package/lib/plugins/types.ts +140 -1
- package/lib/session-watcher.ts +36 -10
- package/lib/settings.ts +65 -33
- package/lib/skills.ts +3 -1
- package/lib/task-manager.ts +50 -22
- package/lib/telegram-bot.ts +71 -0
- package/lib/terminal-standalone.ts +58 -36
- package/lib/workspace/orchestrator.ts +1 -0
- package/middleware.ts +10 -0
- package/package.json +3 -2
- package/scripts/bench/README.md +1 -1
- package/scripts/bench/tasks/01-text-utils/validator.sh +1 -1
- package/scripts/bench/tasks/02-pagination/setup.sh +1 -1
- package/scripts/bench/tasks/02-pagination/validator.sh +1 -1
- package/scripts/bench/tasks/03-bug-fix/setup.sh +1 -1
- package/scripts/bench/tasks/03-bug-fix/validator.sh +1 -1
- package/src/core/db/database.ts +21 -12
|
@@ -149,14 +149,16 @@ const STATUS_COLOR: Record<string, string> = {
|
|
|
149
149
|
|
|
150
150
|
// ─── DAG Node Card with live logs ─────────────────────────
|
|
151
151
|
|
|
152
|
-
function DagNodeCard({ nodeId, node, nodeDef, onViewTask }: {
|
|
152
|
+
function DagNodeCard({ nodeId, node, nodeDef, onViewTask, onRetry }: {
|
|
153
153
|
nodeId: string;
|
|
154
154
|
node: PipelineNodeState;
|
|
155
155
|
nodeDef?: WorkflowNode;
|
|
156
156
|
onViewTask?: (taskId: string) => void;
|
|
157
|
+
onRetry?: (nodeId: string) => void;
|
|
157
158
|
}) {
|
|
158
159
|
const isRunning = node.status === 'running';
|
|
159
160
|
const { log } = useTaskStream(node.taskId, isRunning);
|
|
161
|
+
const [retryBusy, setRetryBusy] = useState(false);
|
|
160
162
|
|
|
161
163
|
return (
|
|
162
164
|
<div className={`border rounded-lg p-3 ${
|
|
@@ -177,6 +179,16 @@ function DagNodeCard({ nodeId, node, nodeDef, onViewTask }: {
|
|
|
177
179
|
</button>
|
|
178
180
|
)}
|
|
179
181
|
{node.iterations > 1 && <span className="text-[9px] text-yellow-400">iter {node.iterations}</span>}
|
|
182
|
+
{node.status === 'failed' && onRetry && (
|
|
183
|
+
<button
|
|
184
|
+
onClick={async () => { setRetryBusy(true); try { await onRetry(nodeId); } finally { setRetryBusy(false); } }}
|
|
185
|
+
disabled={retryBusy}
|
|
186
|
+
className="text-[9px] px-1.5 py-0.5 rounded bg-yellow-500/20 hover:bg-yellow-500/30 text-yellow-300 disabled:opacity-50"
|
|
187
|
+
title="Reset this node + all downstream to pending and re-run. Useful when an upstream node like fix-code already produced a valid worktree commit and only the later step (push-and-mr / notify-teams) needs to be re-tried."
|
|
188
|
+
>
|
|
189
|
+
{retryBusy ? 'retrying…' : '↻ retry'}
|
|
190
|
+
</button>
|
|
191
|
+
)}
|
|
180
192
|
<span className="text-[9px] text-[var(--text-secondary)] ml-auto">{node.status}</span>
|
|
181
193
|
</div>
|
|
182
194
|
|
|
@@ -449,38 +461,117 @@ export default function PipelineView({ onViewTask, focusPipelineId, onFocusHandl
|
|
|
449
461
|
const [importYaml, setImportYaml] = useState('');
|
|
450
462
|
const [agents, setAgents] = useState<{ id: string; name: string; detected?: boolean }[]>([]);
|
|
451
463
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
464
|
+
// "Load older runs" state, per-workflow. The initial /api/pipelines fetch
|
|
465
|
+
// returns the newest 100 across ALL workflows; if a particular workflow
|
|
466
|
+
// had older history beyond that window, the user can expand it and click
|
|
467
|
+
// "Load older runs". `expanded` = the user opted in to show all loaded
|
|
468
|
+
// runs (not just slice 20); `exhausted` = last load returned <50 → no more.
|
|
469
|
+
const [expandedHistory, setExpandedHistory] = useState<Set<string>>(new Set());
|
|
470
|
+
const [exhaustedHistory, setExhaustedHistory] = useState<Set<string>>(new Set());
|
|
471
|
+
const [loadingMoreFor, setLoadingMoreFor] = useState<string | null>(null);
|
|
472
|
+
|
|
473
|
+
async function loadOlderRuns(workflowName: string) {
|
|
474
|
+
const existing = pipelines.filter(p => p.workflowName === workflowName);
|
|
475
|
+
let oldest = existing[0]?.createdAt;
|
|
476
|
+
for (const p of existing) if (p.createdAt < oldest) oldest = p.createdAt;
|
|
477
|
+
if (!oldest) return;
|
|
478
|
+
setLoadingMoreFor(workflowName);
|
|
479
|
+
try {
|
|
480
|
+
const res = await fetch(`/api/pipelines?workflow=${encodeURIComponent(workflowName)}&before=${encodeURIComponent(oldest)}&limit=50`);
|
|
481
|
+
const more: Pipeline[] = await res.json();
|
|
482
|
+
if (Array.isArray(more) && more.length > 0) {
|
|
483
|
+
setPipelines(prev => {
|
|
484
|
+
const seen = new Set(prev.map(p => p.id));
|
|
485
|
+
return [...prev, ...more.filter(p => !seen.has(p.id))];
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
setExpandedHistory(prev => new Set(prev).add(workflowName));
|
|
489
|
+
if (!Array.isArray(more) || more.length < 50) {
|
|
490
|
+
setExhaustedHistory(prev => new Set(prev).add(workflowName));
|
|
491
|
+
}
|
|
492
|
+
} finally {
|
|
493
|
+
setLoadingMoreFor(null);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Lightweight metadata only — no pipeline runs. Runs are lazy-loaded
|
|
498
|
+
// per-workflow on click. Reduces initial page to a single workflow
|
|
499
|
+
// table fetch + projects + agents, even with hundreds of runs on disk.
|
|
500
|
+
const fetchMeta = useCallback(async () => {
|
|
501
|
+
const [wRes, projRes, agentRes] = await Promise.all([
|
|
455
502
|
fetch('/api/pipelines?type=workflows'),
|
|
456
503
|
fetch('/api/projects'),
|
|
457
504
|
fetch('/api/agents'),
|
|
458
505
|
]);
|
|
459
|
-
const
|
|
460
|
-
const wData = await wRes.json();
|
|
461
|
-
const projData = await projRes.json();
|
|
462
|
-
const agentData = await agentRes.json();
|
|
463
|
-
if (Array.isArray(pData)) setPipelines(pData);
|
|
506
|
+
const [wData, projData, agentData] = await Promise.all([wRes.json(), projRes.json(), agentRes.json()]);
|
|
464
507
|
if (Array.isArray(wData)) setWorkflows(wData);
|
|
465
508
|
if (Array.isArray(projData)) setProjects(projData.map((p: any) => ({ name: p.name, path: p.path })));
|
|
466
509
|
if (Array.isArray(agentData?.agents)) setAgents(agentData.agents);
|
|
467
510
|
}, []);
|
|
468
511
|
|
|
512
|
+
// Fetch the run history for ONE workflow. Used on first expand + on
|
|
513
|
+
// the 5s polling tick (only for the currently active workflow).
|
|
514
|
+
const fetchWorkflowRuns = useCallback(async (workflowName: string, opts: { append?: boolean } = {}) => {
|
|
515
|
+
try {
|
|
516
|
+
const res = await fetch(`/api/pipelines?workflow=${encodeURIComponent(workflowName)}&limit=100`);
|
|
517
|
+
const data: Pipeline[] = await res.json();
|
|
518
|
+
if (!Array.isArray(data)) return;
|
|
519
|
+
setPipelines(prev => {
|
|
520
|
+
if (!opts.append) {
|
|
521
|
+
// Replace this workflow's runs (covers status changes during polling),
|
|
522
|
+
// keep other workflows' loaded runs untouched.
|
|
523
|
+
const others = prev.filter(p => p.workflowName !== workflowName);
|
|
524
|
+
return [...others, ...data];
|
|
525
|
+
}
|
|
526
|
+
const seen = new Set(prev.map(p => p.id));
|
|
527
|
+
return [...prev, ...data.filter(p => !seen.has(p.id))];
|
|
528
|
+
});
|
|
529
|
+
} catch { /* ignore — polling will retry */ }
|
|
530
|
+
}, []);
|
|
531
|
+
|
|
532
|
+
// fetchData kept for places that explicitly want "refresh everything"
|
|
533
|
+
// (e.g. after Cancel/Delete actions). Just wraps the two pieces.
|
|
534
|
+
const fetchData = useCallback(async () => {
|
|
535
|
+
await fetchMeta();
|
|
536
|
+
if (activeWorkflow) await fetchWorkflowRuns(activeWorkflow);
|
|
537
|
+
}, [fetchMeta, fetchWorkflowRuns, activeWorkflow]);
|
|
538
|
+
|
|
539
|
+
useEffect(() => { void fetchMeta(); }, [fetchMeta]);
|
|
540
|
+
|
|
541
|
+
// Polling — refresh ONLY the active workflow (5s while expanded).
|
|
542
|
+
// Stops polling entirely when nothing is expanded — no background
|
|
543
|
+
// network traffic just from having the page open.
|
|
469
544
|
useEffect(() => {
|
|
470
|
-
|
|
471
|
-
|
|
545
|
+
if (!activeWorkflow) return;
|
|
546
|
+
void fetchWorkflowRuns(activeWorkflow);
|
|
547
|
+
const timer = setInterval(() => { void fetchWorkflowRuns(activeWorkflow); }, 5000);
|
|
472
548
|
return () => clearInterval(timer);
|
|
473
|
-
}, [
|
|
549
|
+
}, [activeWorkflow, fetchWorkflowRuns]);
|
|
474
550
|
|
|
475
551
|
// Focus on a specific pipeline (from external navigation)
|
|
476
552
|
useEffect(() => {
|
|
477
|
-
if (!focusPipelineId
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
553
|
+
if (!focusPipelineId) return;
|
|
554
|
+
// External navigation may target a pipeline whose workflow isn't
|
|
555
|
+
// expanded yet (and so its runs aren't in `pipelines`). Look it up
|
|
556
|
+
// directly via /api/pipelines/:id, set as selected, and also expand
|
|
557
|
+
// its workflow rail-side so the user can see context.
|
|
558
|
+
// Always fetch the FULL pipeline — the in-memory cache only holds
|
|
559
|
+
// summary projections (no node.outputs, no conversation.messages),
|
|
560
|
+
// and the detail panel needs the heavy fields.
|
|
561
|
+
let cancelled = false;
|
|
562
|
+
void (async () => {
|
|
563
|
+
try {
|
|
564
|
+
const res = await fetch(`/api/pipelines/${encodeURIComponent(focusPipelineId)}`);
|
|
565
|
+
if (!res.ok) return;
|
|
566
|
+
const data: Pipeline = await res.json();
|
|
567
|
+
if (cancelled || !data?.id) return;
|
|
568
|
+
setSelectedPipeline(data);
|
|
569
|
+
setShowEditor(false);
|
|
570
|
+
if (data.workflowName) setActiveWorkflow(data.workflowName);
|
|
571
|
+
onFocusHandled?.();
|
|
572
|
+
} catch { /* ignore */ }
|
|
573
|
+
})();
|
|
574
|
+
return () => { cancelled = true; };
|
|
484
575
|
}, [focusPipelineId, pipelines, onFocusHandled]);
|
|
485
576
|
|
|
486
577
|
// Refresh selected pipeline
|
|
@@ -527,6 +618,24 @@ export default function PipelineView({ onViewTask, focusPipelineId, onFocusHandl
|
|
|
527
618
|
}
|
|
528
619
|
};
|
|
529
620
|
|
|
621
|
+
const handleRetryNode = async (pipelineId: string, nodeId: string) => {
|
|
622
|
+
const res = await fetch(`/api/pipelines/${pipelineId}`, {
|
|
623
|
+
method: 'POST',
|
|
624
|
+
headers: { 'Content-Type': 'application/json' },
|
|
625
|
+
body: JSON.stringify({ action: 'retry-node', nodeId }),
|
|
626
|
+
});
|
|
627
|
+
if (!res.ok) {
|
|
628
|
+
const j = await res.json().catch(() => ({}));
|
|
629
|
+
alert(`Retry failed: ${j?.error || res.statusText}`);
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
fetchData();
|
|
633
|
+
if (selectedPipeline?.id === pipelineId) {
|
|
634
|
+
const r = await fetch(`/api/pipelines/${pipelineId}`);
|
|
635
|
+
setSelectedPipeline(await r.json());
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
|
|
530
639
|
const handleDelete = async (id: string) => {
|
|
531
640
|
if (!confirm('Delete this pipeline?')) return;
|
|
532
641
|
await fetch(`/api/pipelines/${id}`, {
|
|
@@ -566,7 +675,9 @@ initial_prompt: "{{input.task}}"
|
|
|
566
675
|
const currentWorkflow = workflows.find(w => w.name === selectedWorkflow);
|
|
567
676
|
|
|
568
677
|
return (
|
|
569
|
-
<div className="flex-1 flex min-h-0">
|
|
678
|
+
<div className="flex-1 flex flex-col min-h-0">
|
|
679
|
+
<MissingToolsBanner />
|
|
680
|
+
<div className="flex-1 flex min-h-0">
|
|
570
681
|
{/* Left — Workflow list */}
|
|
571
682
|
<aside style={{ width: sidebarWidth }} className="flex flex-col shrink-0 overflow-hidden">
|
|
572
683
|
<div className="px-3 py-2 border-b border-[var(--border)] flex items-center gap-1.5">
|
|
@@ -741,44 +852,85 @@ initial_prompt: "{{input.task}}"
|
|
|
741
852
|
>{w.builtin ? 'View' : 'Edit'}</button>
|
|
742
853
|
</div>
|
|
743
854
|
{/* Execution history for this workflow */}
|
|
744
|
-
{isActive && (
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
<
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
855
|
+
{isActive && (() => {
|
|
856
|
+
const sorted = runs.slice().sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
857
|
+
// After clicking "Load older runs", show all loaded for this
|
|
858
|
+
// workflow. Otherwise slice to 20 to keep the rail compact.
|
|
859
|
+
const expanded = expandedHistory.has(w.name);
|
|
860
|
+
const visible = expanded ? sorted : sorted.slice(0, 20);
|
|
861
|
+
const showLoadMore = !exhaustedHistory.has(w.name) && (expanded || sorted.length >= 20);
|
|
862
|
+
return (
|
|
863
|
+
<div className="bg-[var(--bg-tertiary)]/50">
|
|
864
|
+
{sorted.length === 0 ? (
|
|
865
|
+
<div className="px-4 py-2 text-[9px] text-[var(--text-secondary)]">No runs yet</div>
|
|
866
|
+
) : (
|
|
867
|
+
visible.map(p => (
|
|
868
|
+
<button
|
|
869
|
+
key={p.id}
|
|
870
|
+
onClick={async () => {
|
|
871
|
+
// The list payload is a LIGHT summary — no
|
|
872
|
+
// node.outputs, no conversation.messages.
|
|
873
|
+
// The detail panel walks those (e.g.
|
|
874
|
+
// Object.entries(p.nodes[id].outputs)) and
|
|
875
|
+
// crashes with "Cannot convert undefined to
|
|
876
|
+
// object" when handed a summary. Fetch the
|
|
877
|
+
// full pipeline before selecting.
|
|
878
|
+
try {
|
|
879
|
+
const res = await fetch(`/api/pipelines/${encodeURIComponent(p.id)}`);
|
|
880
|
+
if (res.ok) {
|
|
881
|
+
const full = await res.json();
|
|
882
|
+
setSelectedPipeline(full);
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
} catch { /* fall back to summary */ }
|
|
886
|
+
setSelectedPipeline(p);
|
|
887
|
+
}}
|
|
888
|
+
className={`w-full text-left px-4 py-1.5 border-b border-[var(--border)]/20 hover:bg-[var(--bg-tertiary)] ${
|
|
889
|
+
selectedPipeline?.id === p.id ? 'bg-[var(--accent)]/5' : ''
|
|
890
|
+
}`}
|
|
891
|
+
>
|
|
892
|
+
<div className="flex items-center gap-1.5">
|
|
893
|
+
<span className={`text-[9px] ${STATUS_COLOR[p.status]}`}>●</span>
|
|
894
|
+
<span className="text-[9px] text-[var(--text-secondary)] font-mono">{p.id.slice(0, 8)}</span>
|
|
895
|
+
{p.type === 'conversation' ? (
|
|
896
|
+
<span className="text-[7px] px-1 rounded bg-[var(--accent)]/15 text-[var(--accent)]">
|
|
897
|
+
R{p.conversation?.currentRound || 0}/{p.conversation?.config.maxRounds || '?'}
|
|
769
898
|
</span>
|
|
770
|
-
)
|
|
899
|
+
) : (
|
|
900
|
+
<div className="flex gap-0.5 ml-1">
|
|
901
|
+
{p.nodeOrder.map(nodeId => (
|
|
902
|
+
<span key={nodeId} className={`text-[8px] ${STATUS_COLOR[p.nodes[nodeId]?.status || 'pending']}`}>
|
|
903
|
+
{STATUS_ICON[p.nodes[nodeId]?.status || 'pending']}
|
|
904
|
+
</span>
|
|
905
|
+
))}
|
|
906
|
+
</div>
|
|
907
|
+
)}
|
|
908
|
+
<span className="text-[8px] text-[var(--text-secondary)] ml-auto">
|
|
909
|
+
{new Date(p.createdAt).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}
|
|
910
|
+
</span>
|
|
771
911
|
</div>
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
912
|
+
</button>
|
|
913
|
+
))
|
|
914
|
+
)}
|
|
915
|
+
{showLoadMore && (
|
|
916
|
+
<button
|
|
917
|
+
onClick={() => void loadOlderRuns(w.name)}
|
|
918
|
+
disabled={loadingMoreFor === w.name}
|
|
919
|
+
className="w-full text-center px-4 py-1.5 text-[9px] text-[var(--accent)] hover:bg-[var(--bg-tertiary)] disabled:opacity-50 border-b border-[var(--border)]/20"
|
|
920
|
+
>
|
|
921
|
+
{loadingMoreFor === w.name
|
|
922
|
+
? 'Loading…'
|
|
923
|
+
: expanded
|
|
924
|
+
? `↓ Load more (currently showing ${sorted.length})`
|
|
925
|
+
: `↓ Load older runs (more than 20)`}
|
|
777
926
|
</button>
|
|
778
|
-
)
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
927
|
+
)}
|
|
928
|
+
{exhaustedHistory.has(w.name) && expanded && (
|
|
929
|
+
<div className="px-4 py-1.5 text-[8px] text-center text-[var(--text-secondary)]">— end of history ({sorted.length} runs) —</div>
|
|
930
|
+
)}
|
|
931
|
+
</div>
|
|
932
|
+
);
|
|
933
|
+
})()}
|
|
782
934
|
</div>
|
|
783
935
|
);
|
|
784
936
|
})}
|
|
@@ -892,7 +1044,13 @@ initial_prompt: "{{input.task}}"
|
|
|
892
1044
|
<div className="w-px h-4 bg-[var(--border)]" />
|
|
893
1045
|
</div>
|
|
894
1046
|
)}
|
|
895
|
-
<DagNodeCard
|
|
1047
|
+
<DagNodeCard
|
|
1048
|
+
nodeId={nodeId}
|
|
1049
|
+
node={node}
|
|
1050
|
+
nodeDef={nodeDef}
|
|
1051
|
+
onViewTask={onViewTask}
|
|
1052
|
+
onRetry={(nid) => handleRetryNode(selectedPipeline.id, nid)}
|
|
1053
|
+
/>
|
|
896
1054
|
</div>
|
|
897
1055
|
);
|
|
898
1056
|
})}
|
|
@@ -1013,6 +1171,67 @@ initial_prompt: "{{input.task}}"
|
|
|
1013
1171
|
</div>
|
|
1014
1172
|
)}
|
|
1015
1173
|
</main>
|
|
1174
|
+
</div>
|
|
1175
|
+
</div>
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
/**
|
|
1180
|
+
* Small banner shown atop PipelineView when an external CLI a built-in
|
|
1181
|
+
* workflow depends on (glab, gh, jq, git) isn't installed. Polls /api/health/tools
|
|
1182
|
+
* once on mount + lets the user expand each row for the copy-pasteable
|
|
1183
|
+
* install hint.
|
|
1184
|
+
*/
|
|
1185
|
+
function MissingToolsBanner() {
|
|
1186
|
+
const [tools, setTools] = useState<Array<{ name: string; label: string; needed_for: string; installed: boolean; install_hint: string; version: string }>>([]);
|
|
1187
|
+
const [expanded, setExpanded] = useState<string>('');
|
|
1188
|
+
const [dismissed, setDismissed] = useState(false);
|
|
1189
|
+
|
|
1190
|
+
useEffect(() => {
|
|
1191
|
+
fetch('/api/health/tools')
|
|
1192
|
+
.then((r) => r.json())
|
|
1193
|
+
.then((j) => setTools(j.tools || []))
|
|
1194
|
+
.catch(() => {});
|
|
1195
|
+
}, []);
|
|
1196
|
+
|
|
1197
|
+
if (dismissed) return null;
|
|
1198
|
+
const missing = tools.filter((t) => !t.installed);
|
|
1199
|
+
if (missing.length === 0) return null;
|
|
1200
|
+
|
|
1201
|
+
return (
|
|
1202
|
+
<div className="border-b border-[var(--border)] bg-yellow-500/10 px-4 py-2">
|
|
1203
|
+
<div className="flex items-center gap-2">
|
|
1204
|
+
<span className="text-[11px] font-semibold text-yellow-500">⚠ Missing tools</span>
|
|
1205
|
+
<span className="text-[10.5px] text-[var(--text-secondary)]">
|
|
1206
|
+
Some built-in pipelines need these CLIs. Click a name for install instructions.
|
|
1207
|
+
</span>
|
|
1208
|
+
<span className="flex-1" />
|
|
1209
|
+
<button
|
|
1210
|
+
onClick={() => setDismissed(true)}
|
|
1211
|
+
className="text-[10px] text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
|
|
1212
|
+
title="Dismiss until next reload"
|
|
1213
|
+
>
|
|
1214
|
+
×
|
|
1215
|
+
</button>
|
|
1216
|
+
</div>
|
|
1217
|
+
<div className="mt-1.5 flex flex-wrap gap-1.5">
|
|
1218
|
+
{missing.map((t) => (
|
|
1219
|
+
<div key={t.name} className="flex flex-col">
|
|
1220
|
+
<button
|
|
1221
|
+
onClick={() => setExpanded(expanded === t.name ? '' : t.name)}
|
|
1222
|
+
className="text-[10.5px] px-2 py-0.5 border border-yellow-500/40 text-yellow-500 rounded hover:bg-yellow-500/10"
|
|
1223
|
+
title={t.needed_for}
|
|
1224
|
+
>
|
|
1225
|
+
{t.name} {expanded === t.name ? '▾' : '▸'}
|
|
1226
|
+
</button>
|
|
1227
|
+
{expanded === t.name && (
|
|
1228
|
+
<pre className="text-[10px] font-mono whitespace-pre-wrap mt-1 p-2 bg-black/30 text-[var(--text-secondary)] border border-[var(--border)] rounded max-w-[520px]">
|
|
1229
|
+
{`# ${t.label} — needed for: ${t.needed_for}\n\n${t.install_hint}`}
|
|
1230
|
+
</pre>
|
|
1231
|
+
)}
|
|
1232
|
+
</div>
|
|
1233
|
+
))}
|
|
1234
|
+
</div>
|
|
1016
1235
|
</div>
|
|
1017
1236
|
);
|
|
1018
1237
|
}
|
|
@@ -29,6 +29,7 @@ interface PluginDetail {
|
|
|
29
29
|
version: string;
|
|
30
30
|
author?: string;
|
|
31
31
|
description?: string;
|
|
32
|
+
category?: string;
|
|
32
33
|
config: Record<string, { type: string; label?: string; description?: string; required?: boolean; default?: any; options?: string[] }>;
|
|
33
34
|
params: Record<string, { type: string; label?: string; description?: string; required?: boolean; default?: any }>;
|
|
34
35
|
actions: Record<string, { run: string; method?: string; url?: string; command?: string }>;
|
|
@@ -63,7 +64,8 @@ export default function PluginsPanel() {
|
|
|
63
64
|
]);
|
|
64
65
|
const allData = await allRes.json();
|
|
65
66
|
const instData = await instRes.json();
|
|
66
|
-
|
|
67
|
+
// Connectors have their own panel — keep this one focused on pipeline plugins.
|
|
68
|
+
setPlugins((allData.plugins || []).filter((p: any) => p.category !== 'connector'));
|
|
67
69
|
// Build instances list from installed plugins
|
|
68
70
|
const inst: PluginInstance[] = (instData.plugins || []).map((p: any) => ({
|
|
69
71
|
id: p.id,
|