fix: remove deprecated graphs
This commit is contained in:
@@ -36,7 +36,10 @@
|
|||||||
"Bash(pkill -f \"pytest.*test_event_loop_node\")",
|
"Bash(pkill -f \"pytest.*test_event_loop_node\")",
|
||||||
"Bash(pkill -f \"pytest.*TestToolConcurrency\")",
|
"Bash(pkill -f \"pytest.*TestToolConcurrency\")",
|
||||||
"Bash(grep -n \"def.*discover\\\\|/api/agents\\\\|agents_discover\" /home/timothy/aden/hive/core/framework/server/*.py)",
|
"Bash(grep -n \"def.*discover\\\\|/api/agents\\\\|agents_discover\" /home/timothy/aden/hive/core/framework/server/*.py)",
|
||||||
"Bash(bun run:*)"
|
"Bash(bun run:*)",
|
||||||
|
"Bash(npx eslint:*)",
|
||||||
|
"Bash(npm run:*)",
|
||||||
|
"Bash(npm test:*)"
|
||||||
],
|
],
|
||||||
"additionalDirectories": [
|
"additionalDirectories": [
|
||||||
"/home/timothy/.hive/skills/writing-hive-skills",
|
"/home/timothy/.hive/skills/writing-hive-skills",
|
||||||
|
|||||||
@@ -176,56 +176,6 @@ export interface GraphTopology {
|
|||||||
entry_points?: EntryPoint[];
|
entry_points?: EntryPoint[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Draft graph types (planning phase) ---
|
|
||||||
|
|
||||||
export interface DraftNode {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
node_type: string;
|
|
||||||
tools: string[];
|
|
||||||
input_keys: string[];
|
|
||||||
output_keys: string[];
|
|
||||||
success_criteria: string;
|
|
||||||
sub_agents: string[];
|
|
||||||
/** For decision nodes: the yes/no question evaluated during dissolution. */
|
|
||||||
decision_clause?: string;
|
|
||||||
flowchart_type: string;
|
|
||||||
flowchart_shape: string;
|
|
||||||
flowchart_color: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DraftEdge {
|
|
||||||
id: string;
|
|
||||||
source: string;
|
|
||||||
target: string;
|
|
||||||
condition: string;
|
|
||||||
description: string;
|
|
||||||
/** Short label shown on the flowchart edge (e.g. "Yes", "No"). */
|
|
||||||
label?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DraftGraph {
|
|
||||||
agent_name: string;
|
|
||||||
goal: string;
|
|
||||||
description: string;
|
|
||||||
success_criteria: string[];
|
|
||||||
constraints: string[];
|
|
||||||
nodes: DraftNode[];
|
|
||||||
edges: DraftEdge[];
|
|
||||||
entry_node: string;
|
|
||||||
terminal_nodes: string[];
|
|
||||||
flowchart_legend: Record<string, { shape: string; color: string }>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Mapping from runtime graph nodes → original flowchart draft nodes. */
|
|
||||||
export interface FlowchartMap {
|
|
||||||
/** runtime_node_id → list of original draft node IDs it absorbed. */
|
|
||||||
map: Record<string, string[]> | null;
|
|
||||||
/** Original draft graph preserved before planning-node dissolution (decision + subagent). */
|
|
||||||
original_draft: DraftGraph | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NodeCriteria {
|
export interface NodeCriteria {
|
||||||
node_id: string;
|
node_id: string;
|
||||||
success_criteria: string | null;
|
success_criteria: string | null;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,143 @@
|
|||||||
|
import { Clock, Webhook, Zap, ArrowRight, Activity } from "lucide-react";
|
||||||
|
import type { GraphNode } from "./graph-types";
|
||||||
|
import { cronToLabel } from "@/lib/graphUtils";
|
||||||
|
|
||||||
|
interface TriggersPanelProps {
|
||||||
|
triggers: GraphNode[];
|
||||||
|
selectedId?: string | null;
|
||||||
|
onSelect?: (trigger: GraphNode) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TriggerIcon({ type }: { type?: string }) {
|
||||||
|
const cls = "w-3.5 h-3.5";
|
||||||
|
switch (type) {
|
||||||
|
case "webhook":
|
||||||
|
return <Webhook className={cls} />;
|
||||||
|
case "timer":
|
||||||
|
return <Clock className={cls} />;
|
||||||
|
case "api":
|
||||||
|
return <ArrowRight className={cls} />;
|
||||||
|
case "event":
|
||||||
|
return <Activity className={cls} />;
|
||||||
|
default:
|
||||||
|
return <Zap className={cls} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleLabel(config: Record<string, unknown> | undefined): string | null {
|
||||||
|
if (!config) return null;
|
||||||
|
const cron = config.cron as string | undefined;
|
||||||
|
if (cron) return cronToLabel(cron);
|
||||||
|
const interval = config.interval_minutes as number | undefined;
|
||||||
|
if (interval != null) {
|
||||||
|
if (interval >= 60) return `Every ${interval / 60}h`;
|
||||||
|
return `Every ${interval}m`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function countdownLabel(nextFireIn: number | undefined): string | null {
|
||||||
|
if (nextFireIn == null || nextFireIn <= 0) return null;
|
||||||
|
const h = Math.floor(nextFireIn / 3600);
|
||||||
|
const m = Math.floor((nextFireIn % 3600) / 60);
|
||||||
|
const s = Math.floor(nextFireIn % 60);
|
||||||
|
return h > 0
|
||||||
|
? `next in ${h}h ${String(m).padStart(2, "0")}m`
|
||||||
|
: `next in ${m}m ${String(s).padStart(2, "0")}s`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TriggerCard({
|
||||||
|
trigger,
|
||||||
|
selected,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
trigger: GraphNode;
|
||||||
|
selected: boolean;
|
||||||
|
onClick?: () => void;
|
||||||
|
}) {
|
||||||
|
const isActive = trigger.status === "running" || trigger.status === "complete";
|
||||||
|
const schedule = scheduleLabel(trigger.triggerConfig);
|
||||||
|
const nextFireIn = trigger.triggerConfig?.next_fire_in as number | undefined;
|
||||||
|
const countdown = isActive ? countdownLabel(nextFireIn) : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClick}
|
||||||
|
className={[
|
||||||
|
"w-full text-left rounded-lg border px-3 py-2.5 transition-colors",
|
||||||
|
selected
|
||||||
|
? "bg-primary/10 border-primary/30"
|
||||||
|
: "bg-background/60 border-border/30 hover:bg-muted/40 hover:border-border/50",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
className={[
|
||||||
|
"flex-shrink-0 w-6 h-6 rounded-full flex items-center justify-center",
|
||||||
|
isActive ? "bg-primary/15 text-primary" : "bg-muted/60 text-muted-foreground",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
<TriggerIcon type={trigger.triggerType} />
|
||||||
|
</span>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="text-xs font-medium text-foreground truncate">{trigger.label}</p>
|
||||||
|
{schedule && schedule !== trigger.label && (
|
||||||
|
<p className="text-[10.5px] text-muted-foreground truncate mt-0.5">{schedule}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={[
|
||||||
|
"flex-shrink-0 text-[10px] font-medium px-1.5 py-0.5 rounded-full",
|
||||||
|
isActive
|
||||||
|
? "bg-emerald-500/15 text-emerald-400"
|
||||||
|
: "bg-muted/60 text-muted-foreground",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
{isActive ? "active" : "inactive"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{countdown && (
|
||||||
|
<p className="text-[10px] text-muted-foreground mt-1.5 italic pl-8">{countdown}</p>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TriggersPanel({ triggers, selectedId, onSelect }: TriggersPanelProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full bg-card/30 border-l border-border/30">
|
||||||
|
<div className="px-4 py-3 border-b border-border/30 flex items-center gap-2">
|
||||||
|
<Clock className="w-3.5 h-3.5 text-muted-foreground" />
|
||||||
|
<h3 className="text-xs font-semibold text-foreground uppercase tracking-wide">
|
||||||
|
Triggers
|
||||||
|
</h3>
|
||||||
|
{triggers.length > 0 && (
|
||||||
|
<span className="ml-auto text-[10px] text-muted-foreground">
|
||||||
|
{triggers.length}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 overflow-y-auto px-3 py-3 space-y-2">
|
||||||
|
{triggers.length === 0 ? (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<Clock className="w-6 h-6 mx-auto text-muted-foreground/40 mb-2" />
|
||||||
|
<p className="text-[11px] text-muted-foreground">No triggers configured</p>
|
||||||
|
<p className="text-[10px] text-muted-foreground/70 mt-1 px-2">
|
||||||
|
Ask the queen to set a schedule or webhook
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
triggers.map((t) => (
|
||||||
|
<TriggerCard
|
||||||
|
key={t.id}
|
||||||
|
trigger={t}
|
||||||
|
selected={selectedId === t.id}
|
||||||
|
onClick={onSelect ? () => onSelect(t) : undefined}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,375 +0,0 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
|
||||||
import { topologyToGraphNodes } from "./graph-converter";
|
|
||||||
import type { GraphTopology, NodeSpec } from "@/api/types";
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Helpers
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
function makeNode(id: string, overrides: Partial<NodeSpec> = {}): NodeSpec {
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
name: id,
|
|
||||||
description: "",
|
|
||||||
node_type: "event_loop",
|
|
||||||
input_keys: [],
|
|
||||||
output_keys: [],
|
|
||||||
nullable_output_keys: [],
|
|
||||||
tools: [],
|
|
||||||
routes: {},
|
|
||||||
max_retries: 3,
|
|
||||||
max_node_visits: 0,
|
|
||||||
client_facing: false,
|
|
||||||
success_criteria: null,
|
|
||||||
system_prompt: "",
|
|
||||||
...overrides,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Edge classification
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
describe("edge classification", () => {
|
|
||||||
it("linear chain: all edges in next[], no backEdges", () => {
|
|
||||||
const topology: GraphTopology = {
|
|
||||||
nodes: [makeNode("A"), makeNode("B"), makeNode("C")],
|
|
||||||
edges: [
|
|
||||||
{ source: "A", target: "B", condition: "on_success", priority: 0 },
|
|
||||||
{ source: "B", target: "C", condition: "on_success", priority: 0 },
|
|
||||||
],
|
|
||||||
entry_node: "A",
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = topologyToGraphNodes(topology);
|
|
||||||
expect(result).toHaveLength(3);
|
|
||||||
|
|
||||||
const a = result.find((n) => n.id === "A")!;
|
|
||||||
const b = result.find((n) => n.id === "B")!;
|
|
||||||
const c = result.find((n) => n.id === "C")!;
|
|
||||||
|
|
||||||
expect(a.next).toEqual(["B"]);
|
|
||||||
expect(a.backEdges).toBeUndefined();
|
|
||||||
expect(b.next).toEqual(["C"]);
|
|
||||||
expect(b.backEdges).toBeUndefined();
|
|
||||||
expect(c.next).toBeUndefined();
|
|
||||||
expect(c.backEdges).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("loop edge: classified as backEdge", () => {
|
|
||||||
const topology: GraphTopology = {
|
|
||||||
nodes: [makeNode("A"), makeNode("B"), makeNode("C")],
|
|
||||||
edges: [
|
|
||||||
{ source: "A", target: "B", condition: "on_success", priority: 0 },
|
|
||||||
{ source: "B", target: "C", condition: "on_success", priority: 0 },
|
|
||||||
{ source: "C", target: "A", condition: "on_success", priority: 0 },
|
|
||||||
],
|
|
||||||
entry_node: "A",
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = topologyToGraphNodes(topology);
|
|
||||||
const c = result.find((n) => n.id === "C")!;
|
|
||||||
|
|
||||||
expect(c.next).toBeUndefined();
|
|
||||||
expect(c.backEdges).toEqual(["A"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("diamond/fan-out: multiple next targets", () => {
|
|
||||||
const topology: GraphTopology = {
|
|
||||||
nodes: [makeNode("A"), makeNode("B"), makeNode("C"), makeNode("D")],
|
|
||||||
edges: [
|
|
||||||
{ source: "A", target: "B", condition: "on_success", priority: 0 },
|
|
||||||
{ source: "A", target: "C", condition: "on_failure", priority: 1 },
|
|
||||||
{ source: "B", target: "D", condition: "on_success", priority: 0 },
|
|
||||||
{ source: "C", target: "D", condition: "on_success", priority: 0 },
|
|
||||||
],
|
|
||||||
entry_node: "A",
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = topologyToGraphNodes(topology);
|
|
||||||
const a = result.find((n) => n.id === "A")!;
|
|
||||||
|
|
||||||
expect(a.next).toEqual(expect.arrayContaining(["B", "C"]));
|
|
||||||
expect(a.next).toHaveLength(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Status mapping
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
describe("status mapping", () => {
|
|
||||||
it("no enrichment: all nodes pending", () => {
|
|
||||||
const topology: GraphTopology = {
|
|
||||||
nodes: [makeNode("A"), makeNode("B")],
|
|
||||||
edges: [
|
|
||||||
{ source: "A", target: "B", condition: "on_success", priority: 0 },
|
|
||||||
],
|
|
||||||
entry_node: "A",
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = topologyToGraphNodes(topology);
|
|
||||||
expect(result.every((n) => n.status === "pending")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("is_current: running", () => {
|
|
||||||
const topology: GraphTopology = {
|
|
||||||
nodes: [makeNode("A", { is_current: true, visit_count: 1, in_path: true })],
|
|
||||||
edges: [],
|
|
||||||
entry_node: "A",
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = topologyToGraphNodes(topology);
|
|
||||||
expect(result[0].status).toBe("running");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("is_current + visit_count > 1: looping", () => {
|
|
||||||
const topology: GraphTopology = {
|
|
||||||
nodes: [makeNode("A", { is_current: true, visit_count: 3, in_path: true })],
|
|
||||||
edges: [],
|
|
||||||
entry_node: "A",
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = topologyToGraphNodes(topology);
|
|
||||||
expect(result[0].status).toBe("looping");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("in_path + visited + not current: complete", () => {
|
|
||||||
const topology: GraphTopology = {
|
|
||||||
nodes: [makeNode("A", { in_path: true, visit_count: 1, is_current: false })],
|
|
||||||
edges: [],
|
|
||||||
entry_node: "A",
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = topologyToGraphNodes(topology);
|
|
||||||
expect(result[0].status).toBe("complete");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("has_failures: error", () => {
|
|
||||||
const topology: GraphTopology = {
|
|
||||||
nodes: [makeNode("A", { has_failures: true, in_path: true, visit_count: 1 })],
|
|
||||||
edges: [],
|
|
||||||
entry_node: "A",
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = topologyToGraphNodes(topology);
|
|
||||||
expect(result[0].status).toBe("error");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Iteration tracking
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
describe("iteration tracking", () => {
|
|
||||||
it("visit_count maps to iterations", () => {
|
|
||||||
const topology: GraphTopology = {
|
|
||||||
nodes: [makeNode("A", { visit_count: 3, in_path: true })],
|
|
||||||
edges: [],
|
|
||||||
entry_node: "A",
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = topologyToGraphNodes(topology);
|
|
||||||
expect(result[0].iterations).toBe(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("max_node_visits maps to maxIterations", () => {
|
|
||||||
const topology: GraphTopology = {
|
|
||||||
nodes: [makeNode("A", { max_node_visits: 5, visit_count: 1, in_path: true })],
|
|
||||||
edges: [],
|
|
||||||
entry_node: "A",
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = topologyToGraphNodes(topology);
|
|
||||||
expect(result[0].maxIterations).toBe(5);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("max_node_visits == 0 (unlimited): maxIterations omitted", () => {
|
|
||||||
const topology: GraphTopology = {
|
|
||||||
nodes: [makeNode("A", { max_node_visits: 0, visit_count: 1, in_path: true })],
|
|
||||||
edges: [],
|
|
||||||
entry_node: "A",
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = topologyToGraphNodes(topology);
|
|
||||||
expect(result[0].maxIterations).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Edge labels
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
describe("edge labels", () => {
|
|
||||||
it("conditional edges produce edgeLabels, on_success/always do not", () => {
|
|
||||||
const topology: GraphTopology = {
|
|
||||||
nodes: [makeNode("A"), makeNode("B"), makeNode("C"), makeNode("D")],
|
|
||||||
edges: [
|
|
||||||
{ source: "A", target: "B", condition: "conditional", priority: 0 },
|
|
||||||
{ source: "A", target: "C", condition: "on_failure", priority: 1 },
|
|
||||||
{ source: "B", target: "D", condition: "on_success", priority: 0 },
|
|
||||||
{ source: "C", target: "D", condition: "always", priority: 0 },
|
|
||||||
],
|
|
||||||
entry_node: "A",
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = topologyToGraphNodes(topology);
|
|
||||||
const a = result.find((n) => n.id === "A")!;
|
|
||||||
const b = result.find((n) => n.id === "B")!;
|
|
||||||
const c = result.find((n) => n.id === "C")!;
|
|
||||||
|
|
||||||
// A has conditional + on_failure edges → both get labels
|
|
||||||
expect(a.edgeLabels).toEqual({ B: "conditional", C: "on_failure" });
|
|
||||||
// B has on_success → no label
|
|
||||||
expect(b.edgeLabels).toBeUndefined();
|
|
||||||
// C has always → no label
|
|
||||||
expect(c.edgeLabels).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Node ordering
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
describe("node ordering", () => {
|
|
||||||
it("nodes returned in BFS walk order from entry_node, not input order", () => {
|
|
||||||
const topology: GraphTopology = {
|
|
||||||
// Input order: C, A, B — but BFS from A should yield A, B, C
|
|
||||||
nodes: [makeNode("C"), makeNode("A"), makeNode("B")],
|
|
||||||
edges: [
|
|
||||||
{ source: "A", target: "B", condition: "on_success", priority: 0 },
|
|
||||||
{ source: "B", target: "C", condition: "on_success", priority: 0 },
|
|
||||||
],
|
|
||||||
entry_node: "A",
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = topologyToGraphNodes(topology);
|
|
||||||
expect(result.map((n) => n.id)).toEqual(["A", "B", "C"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("empty topology returns empty array", () => {
|
|
||||||
const topology: GraphTopology = {
|
|
||||||
nodes: [],
|
|
||||||
edges: [],
|
|
||||||
entry_node: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = topologyToGraphNodes(topology);
|
|
||||||
expect(result).toEqual([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Trigger node synthesis from entry_points
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
describe("trigger node synthesis", () => {
|
|
||||||
it("single non-manual entry point: trigger node prepended before entry_node", () => {
|
|
||||||
const topology: GraphTopology = {
|
|
||||||
nodes: [makeNode("A"), makeNode("B")],
|
|
||||||
edges: [
|
|
||||||
{ source: "A", target: "B", condition: "on_success", priority: 0 },
|
|
||||||
],
|
|
||||||
entry_node: "A",
|
|
||||||
entry_points: [
|
|
||||||
{ id: "webhook", name: "Webhook Handler", entry_node: "A", trigger_type: "webhook", trigger_config: { url: "/hook" } },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = topologyToGraphNodes(topology);
|
|
||||||
expect(result).toHaveLength(3);
|
|
||||||
|
|
||||||
const trigger = result[0];
|
|
||||||
expect(trigger.id).toBe("__trigger_webhook");
|
|
||||||
expect(trigger.nodeType).toBe("trigger");
|
|
||||||
expect(trigger.triggerType).toBe("webhook");
|
|
||||||
expect(trigger.triggerConfig).toEqual({ url: "/hook" });
|
|
||||||
expect(trigger.label).toBe("Webhook Handler");
|
|
||||||
expect(trigger.status).toBe("pending");
|
|
||||||
expect(trigger.next).toEqual(["A"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("trigger_config is threaded through for timer triggers", () => {
|
|
||||||
const topology: GraphTopology = {
|
|
||||||
nodes: [makeNode("A")],
|
|
||||||
edges: [],
|
|
||||||
entry_node: "A",
|
|
||||||
entry_points: [
|
|
||||||
{ id: "timer", name: "Daily Check", entry_node: "A", trigger_type: "timer", trigger_config: { cron: "0 9 * * *" } },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = topologyToGraphNodes(topology);
|
|
||||||
const trigger = result[0];
|
|
||||||
expect(trigger.triggerConfig).toEqual({ cron: "0 9 * * *" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("no entry_points: no trigger nodes added", () => {
|
|
||||||
const topology: GraphTopology = {
|
|
||||||
nodes: [makeNode("A")],
|
|
||||||
edges: [],
|
|
||||||
entry_node: "A",
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = topologyToGraphNodes(topology);
|
|
||||||
expect(result).toHaveLength(1);
|
|
||||||
expect(result[0].nodeType).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("only manual entry points: no trigger nodes added", () => {
|
|
||||||
const topology: GraphTopology = {
|
|
||||||
nodes: [makeNode("A")],
|
|
||||||
edges: [],
|
|
||||||
entry_node: "A",
|
|
||||||
entry_points: [
|
|
||||||
{ id: "main", name: "Main", entry_node: "A", trigger_type: "manual" },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = topologyToGraphNodes(topology);
|
|
||||||
expect(result).toHaveLength(1);
|
|
||||||
expect(result[0].id).toBe("A");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("multiple non-manual entry points: multiple trigger nodes", () => {
|
|
||||||
const topology: GraphTopology = {
|
|
||||||
nodes: [makeNode("A"), makeNode("B"), makeNode("C")],
|
|
||||||
edges: [
|
|
||||||
{ source: "A", target: "C", condition: "on_success", priority: 0 },
|
|
||||||
{ source: "B", target: "C", condition: "on_success", priority: 0 },
|
|
||||||
],
|
|
||||||
entry_node: "A",
|
|
||||||
entry_points: [
|
|
||||||
{ id: "webhook", name: "Webhook", entry_node: "A", trigger_type: "webhook" },
|
|
||||||
{ id: "timer", name: "Daily Timer", entry_node: "B", trigger_type: "timer" },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = topologyToGraphNodes(topology);
|
|
||||||
expect(result).toHaveLength(5); // 2 triggers + 3 nodes
|
|
||||||
const triggers = result.filter((n) => n.nodeType === "trigger");
|
|
||||||
expect(triggers).toHaveLength(2);
|
|
||||||
expect(triggers[0].next).toEqual(["A"]);
|
|
||||||
expect(triggers[1].next).toEqual(["B"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("mix of manual and non-manual: only non-manual become trigger nodes", () => {
|
|
||||||
const topology: GraphTopology = {
|
|
||||||
nodes: [makeNode("A"), makeNode("B")],
|
|
||||||
edges: [
|
|
||||||
{ source: "A", target: "B", condition: "on_success", priority: 0 },
|
|
||||||
],
|
|
||||||
entry_node: "A",
|
|
||||||
entry_points: [
|
|
||||||
{ id: "main", name: "Main", entry_node: "A", trigger_type: "manual" },
|
|
||||||
{ id: "webhook", name: "Webhook", entry_node: "A", trigger_type: "webhook" },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = topologyToGraphNodes(topology);
|
|
||||||
expect(result).toHaveLength(3); // 1 trigger + 2 nodes
|
|
||||||
const triggers = result.filter((n) => n.nodeType === "trigger");
|
|
||||||
expect(triggers).toHaveLength(1);
|
|
||||||
expect(triggers[0].triggerType).toBe("webhook");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
import type { GraphTopology, NodeSpec } from "@/api/types";
|
|
||||||
import type { GraphNode, NodeStatus } from "@/components/graph-types";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a backend GraphTopology (nodes + edges + entry_node) into
|
|
||||||
* the GraphNode[] shape that DraftGraph renders.
|
|
||||||
*
|
|
||||||
* Four jobs:
|
|
||||||
* 1. Synthesize trigger nodes from non-manual entry_points
|
|
||||||
* 2. Order nodes via BFS from trigger/entry_node
|
|
||||||
* 3. Classify edges as forward (next) or backward (backEdges)
|
|
||||||
* 4. Map session enrichment fields to NodeStatus
|
|
||||||
*/
|
|
||||||
export function topologyToGraphNodes(topology: GraphTopology): GraphNode[] {
|
|
||||||
const { nodes: allNodes, edges, entry_node, entry_points } = topology;
|
|
||||||
if (allNodes.length === 0) return [];
|
|
||||||
|
|
||||||
// Filter out subagent-only nodes (referenced in sub_agents but not in any edge)
|
|
||||||
const subagentIds = new Set<string>();
|
|
||||||
for (const n of allNodes) {
|
|
||||||
for (const sa of n.sub_agents ?? []) {
|
|
||||||
subagentIds.add(sa);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const edgeParticipants = new Set<string>();
|
|
||||||
for (const e of edges) {
|
|
||||||
edgeParticipants.add(e.source);
|
|
||||||
edgeParticipants.add(e.target);
|
|
||||||
}
|
|
||||||
const nodes = allNodes.filter(
|
|
||||||
(n) =>
|
|
||||||
!subagentIds.has(n.id) ||
|
|
||||||
edgeParticipants.has(n.id) ||
|
|
||||||
n.id === entry_node,
|
|
||||||
);
|
|
||||||
|
|
||||||
// --- Synthesize trigger nodes for non-manual entry points ---
|
|
||||||
const schedulerEntryPoints = (entry_points || []).filter(
|
|
||||||
(ep) => ep.trigger_type !== "manual",
|
|
||||||
);
|
|
||||||
const triggerMap = new Map<string, GraphNode>();
|
|
||||||
|
|
||||||
for (const ep of schedulerEntryPoints) {
|
|
||||||
const triggerId = `__trigger_${ep.id}`;
|
|
||||||
triggerMap.set(triggerId, {
|
|
||||||
id: triggerId,
|
|
||||||
label: ep.name,
|
|
||||||
status: "pending",
|
|
||||||
nodeType: "trigger",
|
|
||||||
triggerType: ep.trigger_type,
|
|
||||||
triggerConfig: {
|
|
||||||
...ep.trigger_config,
|
|
||||||
...(ep.next_fire_in != null ? { next_fire_in: ep.next_fire_in } : {}),
|
|
||||||
...(ep.task ? { task: ep.task } : {}),
|
|
||||||
},
|
|
||||||
next: [ep.entry_node],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build adjacency list: source → [target, ...] (includes trigger edges)
|
|
||||||
const adj = new Map<string, string[]>();
|
|
||||||
for (const e of edges) {
|
|
||||||
const list = adj.get(e.source) || [];
|
|
||||||
list.push(e.target);
|
|
||||||
adj.set(e.source, list);
|
|
||||||
}
|
|
||||||
for (const [triggerId, triggerNode] of triggerMap) {
|
|
||||||
adj.set(triggerId, triggerNode.next!);
|
|
||||||
}
|
|
||||||
|
|
||||||
// BFS — start from trigger nodes (if any), then entry_node.
|
|
||||||
// Always include entry_node so the DAG ordering stays correct
|
|
||||||
// even when triggers target a node other than entry.
|
|
||||||
const order: string[] = [];
|
|
||||||
const position = new Map<string, number>();
|
|
||||||
const visited = new Set<string>();
|
|
||||||
|
|
||||||
const entryStart = entry_node || nodes[0].id;
|
|
||||||
const starts =
|
|
||||||
triggerMap.size > 0
|
|
||||||
? [...triggerMap.keys(), entryStart]
|
|
||||||
: [entryStart];
|
|
||||||
const queue = [...starts];
|
|
||||||
for (const s of starts) visited.add(s);
|
|
||||||
|
|
||||||
while (queue.length > 0) {
|
|
||||||
const id = queue.shift()!;
|
|
||||||
position.set(id, order.length);
|
|
||||||
order.push(id);
|
|
||||||
|
|
||||||
for (const target of adj.get(id) || []) {
|
|
||||||
if (!visited.has(target)) {
|
|
||||||
visited.add(target);
|
|
||||||
queue.push(target);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add any nodes not reachable from entry (shouldn't happen in valid graphs)
|
|
||||||
for (const n of nodes) {
|
|
||||||
if (!visited.has(n.id)) {
|
|
||||||
position.set(n.id, order.length);
|
|
||||||
order.push(n.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build a node lookup
|
|
||||||
const nodeMap = new Map<string, NodeSpec>();
|
|
||||||
for (const n of nodes) {
|
|
||||||
nodeMap.set(n.id, n);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Classify edges per source node
|
|
||||||
const nextMap = new Map<string, string[]>();
|
|
||||||
const backMap = new Map<string, string[]>();
|
|
||||||
|
|
||||||
for (const e of edges) {
|
|
||||||
const srcPos = position.get(e.source) ?? 0;
|
|
||||||
const tgtPos = position.get(e.target) ?? 0;
|
|
||||||
|
|
||||||
if (tgtPos <= srcPos) {
|
|
||||||
// Back edge (target is at same or earlier position in BFS)
|
|
||||||
const list = backMap.get(e.source) || [];
|
|
||||||
list.push(e.target);
|
|
||||||
backMap.set(e.source, list);
|
|
||||||
} else {
|
|
||||||
// Forward edge
|
|
||||||
const list = nextMap.get(e.source) || [];
|
|
||||||
list.push(e.target);
|
|
||||||
nextMap.set(e.source, list);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build edge condition labels (only for non-trivial conditions)
|
|
||||||
const edgeLabelMap = new Map<string, Record<string, string>>();
|
|
||||||
for (const e of edges) {
|
|
||||||
if (e.condition !== "always" && e.condition !== "on_success") {
|
|
||||||
const labels = edgeLabelMap.get(e.source) || {};
|
|
||||||
labels[e.target] = e.condition;
|
|
||||||
edgeLabelMap.set(e.source, labels);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build GraphNode[] in BFS order
|
|
||||||
return order.map((id) => {
|
|
||||||
// Synthetic trigger nodes are returned directly
|
|
||||||
const trigger = triggerMap.get(id);
|
|
||||||
if (trigger) return trigger;
|
|
||||||
|
|
||||||
const spec = nodeMap.get(id);
|
|
||||||
const next = nextMap.get(id);
|
|
||||||
const back = backMap.get(id);
|
|
||||||
const labels = edgeLabelMap.get(id);
|
|
||||||
|
|
||||||
const result: GraphNode = {
|
|
||||||
id,
|
|
||||||
label: spec?.name || id,
|
|
||||||
status: mapStatus(spec),
|
|
||||||
...(next && next.length > 0 ? { next } : {}),
|
|
||||||
...(back && back.length > 0 ? { backEdges: back } : {}),
|
|
||||||
...(labels ? { edgeLabels: labels } : {}),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Iteration tracking from session enrichment
|
|
||||||
if (spec?.visit_count !== undefined && spec.visit_count > 0) {
|
|
||||||
result.iterations = spec.visit_count;
|
|
||||||
}
|
|
||||||
if (spec?.max_node_visits !== undefined && spec.max_node_visits > 0) {
|
|
||||||
result.maxIterations = spec.max_node_visits;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapStatus(spec: NodeSpec | undefined): NodeStatus {
|
|
||||||
if (!spec) return "pending";
|
|
||||||
|
|
||||||
if (spec.has_failures) return "error";
|
|
||||||
if (spec.is_current) {
|
|
||||||
return (spec.visit_count ?? 0) > 1 ? "looping" : "running";
|
|
||||||
}
|
|
||||||
if (spec.in_path && (spec.visit_count ?? 0) > 0) return "complete";
|
|
||||||
|
|
||||||
return "pending";
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
// ── Shared graph utilities ──
|
// ── Shared graph utilities ──
|
||||||
// Common helpers used by both AgentGraph and DraftGraph.
|
// Shared helpers for graph-like components (TriggersPanel, etc.).
|
||||||
// AgentGraph still has its own copies for now (separate cleanup PR).
|
|
||||||
|
|
||||||
/** Read a CSS custom property value (space-separated HSL components). */
|
/** Read a CSS custom property value (space-separated HSL components). */
|
||||||
export function cssVar(name: string): string {
|
export function cssVar(name: string): string {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useState, useCallback, useRef, useEffect, useMemo } from "react";
|
|||||||
import { useParams, useLocation } from "react-router-dom";
|
import { useParams, useLocation } from "react-router-dom";
|
||||||
import { Loader2, WifiOff, KeyRound, FolderOpen, X } from "lucide-react";
|
import { Loader2, WifiOff, KeyRound, FolderOpen, X } from "lucide-react";
|
||||||
import type { GraphNode, NodeStatus } from "@/components/graph-types";
|
import type { GraphNode, NodeStatus } from "@/components/graph-types";
|
||||||
import DraftGraph from "@/components/DraftGraph";
|
import TriggersPanel from "@/components/TriggersPanel";
|
||||||
import ChatPanel, { type ChatMessage, type ImageContent } from "@/components/ChatPanel";
|
import ChatPanel, { type ChatMessage, type ImageContent } from "@/components/ChatPanel";
|
||||||
import NodeDetailPanel from "@/components/NodeDetailPanel";
|
import NodeDetailPanel from "@/components/NodeDetailPanel";
|
||||||
import CredentialsModal, {
|
import CredentialsModal, {
|
||||||
@@ -10,17 +10,10 @@ import CredentialsModal, {
|
|||||||
clearCredentialCache,
|
clearCredentialCache,
|
||||||
} from "@/components/CredentialsModal";
|
} from "@/components/CredentialsModal";
|
||||||
import { executionApi } from "@/api/execution";
|
import { executionApi } from "@/api/execution";
|
||||||
import { workersApi } from "@/api/workers";
|
|
||||||
import { sessionsApi } from "@/api/sessions";
|
import { sessionsApi } from "@/api/sessions";
|
||||||
import { useMultiSSE } from "@/hooks/use-sse";
|
import { useMultiSSE } from "@/hooks/use-sse";
|
||||||
import type {
|
import type { LiveSession, AgentEvent } from "@/api/types";
|
||||||
LiveSession,
|
|
||||||
AgentEvent,
|
|
||||||
NodeSpec,
|
|
||||||
DraftGraph as DraftGraphData,
|
|
||||||
} from "@/api/types";
|
|
||||||
import { sseEventToChatMessage, formatAgentDisplayName } from "@/lib/chat-helpers";
|
import { sseEventToChatMessage, formatAgentDisplayName } from "@/lib/chat-helpers";
|
||||||
import { topologyToGraphNodes } from "@/lib/graph-converter";
|
|
||||||
import { cronToLabel } from "@/lib/graphUtils";
|
import { cronToLabel } from "@/lib/graphUtils";
|
||||||
import { ApiError } from "@/api/client";
|
import { ApiError } from "@/api/client";
|
||||||
import { useColony } from "@/context/ColonyContext";
|
import { useColony } from "@/context/ColonyContext";
|
||||||
@@ -48,8 +41,6 @@ function truncate(s: string, max: number): string {
|
|||||||
type SessionRestoreResult = {
|
type SessionRestoreResult = {
|
||||||
messages: ChatMessage[];
|
messages: ChatMessage[];
|
||||||
restoredPhase: "planning" | "building" | "staging" | "running" | "independent" | null;
|
restoredPhase: "planning" | "building" | "staging" | "running" | "independent" | null;
|
||||||
flowchartMap: Record<string, string[]> | null;
|
|
||||||
originalDraft: DraftGraphData | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
async function restoreSessionMessages(
|
async function restoreSessionMessages(
|
||||||
@@ -62,8 +53,6 @@ async function restoreSessionMessages(
|
|||||||
if (events.length > 0) {
|
if (events.length > 0) {
|
||||||
const messages: ChatMessage[] = [];
|
const messages: ChatMessage[] = [];
|
||||||
let runningPhase: ChatMessage["phase"] = undefined;
|
let runningPhase: ChatMessage["phase"] = undefined;
|
||||||
let flowchartMap: Record<string, string[]> | null = null;
|
|
||||||
let originalDraft: DraftGraphData | null = null;
|
|
||||||
for (const evt of events) {
|
for (const evt of events) {
|
||||||
const p =
|
const p =
|
||||||
evt.type === "queen_phase_changed"
|
evt.type === "queen_phase_changed"
|
||||||
@@ -74,14 +63,6 @@ async function restoreSessionMessages(
|
|||||||
if (p && ["planning", "building", "staging", "running"].includes(p)) {
|
if (p && ["planning", "building", "staging", "running"].includes(p)) {
|
||||||
runningPhase = p as ChatMessage["phase"];
|
runningPhase = p as ChatMessage["phase"];
|
||||||
}
|
}
|
||||||
if (evt.type === "custom" && (evt.data as Record<string, unknown>)?.event === "flowchart_updated" && evt.data) {
|
|
||||||
const mapData = evt.data as {
|
|
||||||
map?: Record<string, string[]>;
|
|
||||||
original_draft?: DraftGraphData;
|
|
||||||
};
|
|
||||||
flowchartMap = mapData.map ?? null;
|
|
||||||
originalDraft = mapData.original_draft ?? null;
|
|
||||||
}
|
|
||||||
const msg = sseEventToChatMessage(evt, thread, agentDisplayName);
|
const msg = sseEventToChatMessage(evt, thread, agentDisplayName);
|
||||||
if (!msg) continue;
|
if (!msg) continue;
|
||||||
if (evt.stream_id === "queen") {
|
if (evt.stream_id === "queen") {
|
||||||
@@ -90,12 +71,12 @@ async function restoreSessionMessages(
|
|||||||
}
|
}
|
||||||
messages.push(msg);
|
messages.push(msg);
|
||||||
}
|
}
|
||||||
return { messages, restoredPhase: runningPhase ?? null, flowchartMap, originalDraft };
|
return { messages, restoredPhase: runningPhase ?? null };
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Event log not available
|
// Event log not available
|
||||||
}
|
}
|
||||||
return { messages: [], restoredPhase: null, flowchartMap: null, originalDraft: null };
|
return { messages: [], restoredPhase: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Agent backend state ──────────────────────────────────────────────────────
|
// ── Agent backend state ──────────────────────────────────────────────────────
|
||||||
@@ -107,18 +88,10 @@ interface AgentState {
|
|||||||
queenReady: boolean;
|
queenReady: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
displayName: string | null;
|
displayName: string | null;
|
||||||
colonyId: string | null; nodeSpecs: NodeSpec[];
|
|
||||||
awaitingInput: boolean;
|
awaitingInput: boolean;
|
||||||
workerInputMessageId: string | null;
|
workerInputMessageId: string | null;
|
||||||
queenBuilding: boolean;
|
|
||||||
queenPhase: "planning" | "building" | "staging" | "running" | "independent";
|
queenPhase: "planning" | "building" | "staging" | "running" | "independent";
|
||||||
designingDraft: boolean;
|
|
||||||
draftGraph: DraftGraphData | null;
|
|
||||||
originalDraft: DraftGraphData | null;
|
|
||||||
flowchartMap: Record<string, string[]> | null;
|
|
||||||
agentPath: string | null;
|
agentPath: string | null;
|
||||||
workerRunState: "idle" | "deploying" | "running";
|
|
||||||
currentExecutionId: string | null;
|
|
||||||
currentRunId: string | null;
|
currentRunId: string | null;
|
||||||
nodeLogs: Record<string, string[]>;
|
nodeLogs: Record<string, string[]>;
|
||||||
nodeActionPlans: Record<string, string>;
|
nodeActionPlans: Record<string, string>;
|
||||||
@@ -153,19 +126,10 @@ function defaultAgentState(): AgentState {
|
|||||||
queenReady: false,
|
queenReady: false,
|
||||||
error: null,
|
error: null,
|
||||||
displayName: null,
|
displayName: null,
|
||||||
colonyId: null,
|
|
||||||
nodeSpecs: [],
|
|
||||||
awaitingInput: false,
|
awaitingInput: false,
|
||||||
workerInputMessageId: null,
|
workerInputMessageId: null,
|
||||||
queenBuilding: false,
|
|
||||||
queenPhase: "planning",
|
queenPhase: "planning",
|
||||||
designingDraft: false,
|
|
||||||
draftGraph: null,
|
|
||||||
originalDraft: null,
|
|
||||||
flowchartMap: null,
|
|
||||||
agentPath: null,
|
agentPath: null,
|
||||||
workerRunState: "idle",
|
|
||||||
currentExecutionId: null,
|
|
||||||
currentRunId: null,
|
currentRunId: null,
|
||||||
nodeLogs: {},
|
nodeLogs: {},
|
||||||
nodeActionPlans: {},
|
nodeActionPlans: {},
|
||||||
@@ -239,9 +203,6 @@ export default function ColonyChat() {
|
|||||||
const [credentialAgentPath, setCredentialAgentPath] = useState<string | null>(null);
|
const [credentialAgentPath, setCredentialAgentPath] = useState<string | null>(null);
|
||||||
const [dismissedBanner, setDismissedBanner] = useState<string | null>(null);
|
const [dismissedBanner, setDismissedBanner] = useState<string | null>(null);
|
||||||
const [selectedNode, setSelectedNode] = useState<GraphNode | null>(null);
|
const [selectedNode, setSelectedNode] = useState<GraphNode | null>(null);
|
||||||
const [graphPanelPct, setGraphPanelPct] = useState(30);
|
|
||||||
const savedGraphPanelPct = useRef(30);
|
|
||||||
const resizing = useRef(false);
|
|
||||||
|
|
||||||
// ── Header actions (Credentials, Data, Browser) ─────────────────────────
|
// ── Header actions (Credentials, Data, Browser) ─────────────────────────
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -281,8 +242,6 @@ export default function ColonyChat() {
|
|||||||
const queenIterTextRef = useRef<Record<string, Record<number, string>>>({});
|
const queenIterTextRef = useRef<Record<string, Record<number, string>>>({});
|
||||||
const suppressIntroRef = useRef(false);
|
const suppressIntroRef = useRef(false);
|
||||||
const loadingRef = useRef(false);
|
const loadingRef = useRef(false);
|
||||||
const designingDraftSinceRef = useRef(0);
|
|
||||||
const designingDraftTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
||||||
|
|
||||||
// ── Helpers ──────────────────────────────────────────────────────────────
|
// ── Helpers ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -346,71 +305,11 @@ export default function ColonyChat() {
|
|||||||
}));
|
}));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// ── Drag-to-resize graph panel ──────────────────────────────────────────
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const onMouseMove = (e: MouseEvent) => {
|
|
||||||
if (!resizing.current) return;
|
|
||||||
const sidebarWidth = 240;
|
|
||||||
const pct = 100 - ((e.clientX - sidebarWidth) / (window.innerWidth - sidebarWidth)) * 100;
|
|
||||||
setGraphPanelPct(Math.max(15, Math.min(50, pct)));
|
|
||||||
};
|
|
||||||
const onMouseUp = () => {
|
|
||||||
resizing.current = false;
|
|
||||||
document.body.style.cursor = "";
|
|
||||||
};
|
|
||||||
window.addEventListener("mousemove", onMouseMove);
|
|
||||||
window.addEventListener("mouseup", onMouseUp);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("mousemove", onMouseMove);
|
|
||||||
window.removeEventListener("mouseup", onMouseUp);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const nodeIsSelected = selectedNode !== null;
|
|
||||||
useEffect(() => {
|
|
||||||
if (nodeIsSelected) {
|
|
||||||
savedGraphPanelPct.current = graphPanelPct;
|
|
||||||
setGraphPanelPct((prev) => Math.min(prev, 30));
|
|
||||||
} else {
|
|
||||||
setGraphPanelPct(savedGraphPanelPct.current);
|
|
||||||
}
|
|
||||||
}, [nodeIsSelected]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
||||||
|
|
||||||
// Reset dismissed banner when the error clears
|
// Reset dismissed banner when the error clears
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!agentState.error) setDismissedBanner(null);
|
if (!agentState.error) setDismissedBanner(null);
|
||||||
}, [agentState.error]);
|
}, [agentState.error]);
|
||||||
|
|
||||||
// ── Graph fetching ─────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const fetchGraph = useCallback(
|
|
||||||
async (sessionId: string, knownGraphId?: string) => {
|
|
||||||
try {
|
|
||||||
let colonyId = knownGraphId;
|
|
||||||
if (!colonyId) {
|
|
||||||
// Try session detail first (colony_id is always set when worker is loaded)
|
|
||||||
try {
|
|
||||||
const detail = await sessionsApi.get(sessionId);
|
|
||||||
colonyId = detail.colony_id ?? undefined;
|
|
||||||
} catch { /* fall through */ }
|
|
||||||
}
|
|
||||||
if (!colonyId) {
|
|
||||||
const { colonies } = await sessionsApi.colonies(sessionId);
|
|
||||||
if (!colonies.length) return;
|
|
||||||
colonyId = colonies[0];
|
|
||||||
}
|
|
||||||
const topology = await workersApi.nodes(sessionId, colonyId);
|
|
||||||
updateState({ colonyId, nodeSpecs: topology.nodes });
|
|
||||||
const nodes = topologyToGraphNodes(topology);
|
|
||||||
if (nodes.length > 0) setGraphNodes(nodes);
|
|
||||||
} catch {
|
|
||||||
// Graph fetch failed
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[updateState],
|
|
||||||
);
|
|
||||||
|
|
||||||
// ── Session loading ────────────────────────────────────────────────────
|
// ── Session loading ────────────────────────────────────────────────────
|
||||||
|
|
||||||
const loadSession = useCallback(async () => {
|
const loadSession = useCallback(async () => {
|
||||||
@@ -473,8 +372,6 @@ export default function ColonyChat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let restoredPhase: "planning" | "building" | "staging" | "running" | "independent" | null = null;
|
let restoredPhase: "planning" | "building" | "staging" | "running" | "independent" | null = null;
|
||||||
let restoredFlowchartMap: Record<string, string[]> | null = null;
|
|
||||||
let restoredOriginalDraft: DraftGraphData | null = null;
|
|
||||||
|
|
||||||
if (!liveSession) {
|
if (!liveSession) {
|
||||||
// Pre-fetch messages from cold session
|
// Pre-fetch messages from cold session
|
||||||
@@ -484,8 +381,6 @@ export default function ColonyChat() {
|
|||||||
const restored = await restoreSessionMessages(coldRestoreId, agentPath, displayName);
|
const restored = await restoreSessionMessages(coldRestoreId, agentPath, displayName);
|
||||||
preRestoredMsgs = restored.messages;
|
preRestoredMsgs = restored.messages;
|
||||||
restoredPhase = restored.restoredPhase;
|
restoredPhase = restored.restoredPhase;
|
||||||
restoredFlowchartMap = restored.flowchartMap;
|
|
||||||
restoredOriginalDraft = restored.originalDraft;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (coldRestoreId || preRestoredMsgs.length > 0) {
|
if (coldRestoreId || preRestoredMsgs.length > 0) {
|
||||||
@@ -511,10 +406,7 @@ export default function ColonyChat() {
|
|||||||
sessionId: session.session_id,
|
sessionId: session.session_id,
|
||||||
displayName,
|
displayName,
|
||||||
queenPhase: initialPhase,
|
queenPhase: initialPhase,
|
||||||
queenBuilding: initialPhase === "building",
|
|
||||||
queenSupportsImages: session.queen_supports_images !== false,
|
queenSupportsImages: session.queen_supports_images !== false,
|
||||||
...(restoredFlowchartMap ? { flowchartMap: restoredFlowchartMap } : {}),
|
|
||||||
...(restoredOriginalDraft ? { originalDraft: restoredOriginalDraft, draftGraph: null } : {}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Restore messages for live resume
|
// Restore messages for live resume
|
||||||
@@ -528,10 +420,6 @@ export default function ColonyChat() {
|
|||||||
restored.messages.sort((a, b) => (a.createdAt ?? 0) - (b.createdAt ?? 0));
|
restored.messages.sort((a, b) => (a.createdAt ?? 0) - (b.createdAt ?? 0));
|
||||||
setMessages(restored.messages);
|
setMessages(restored.messages);
|
||||||
}
|
}
|
||||||
if (restored.flowchartMap && !restoredFlowchartMap) {
|
|
||||||
restoredFlowchartMap = restored.flowchartMap;
|
|
||||||
restoredOriginalDraft = restored.originalDraft;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasRestoredContent = isResumedSession || !!coldRestoreId;
|
const hasRestoredContent = isResumedSession || !!coldRestoreId;
|
||||||
@@ -543,8 +431,6 @@ export default function ColonyChat() {
|
|||||||
ready: true,
|
ready: true,
|
||||||
loading: false,
|
loading: false,
|
||||||
queenReady: hasRestoredContent,
|
queenReady: hasRestoredContent,
|
||||||
...(restoredFlowchartMap ? { flowchartMap: restoredFlowchartMap } : {}),
|
|
||||||
...(restoredOriginalDraft ? { originalDraft: restoredOriginalDraft, draftGraph: null } : {}),
|
|
||||||
});
|
});
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
if (err instanceof ApiError && err.status === 424) {
|
if (err instanceof ApiError && err.status === 424) {
|
||||||
@@ -578,13 +464,6 @@ export default function ColonyChat() {
|
|||||||
}
|
}
|
||||||
}, [agentPath, isNewChat]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [agentPath, isNewChat]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
// Fetch graph when session becomes ready
|
|
||||||
useEffect(() => {
|
|
||||||
if (agentState.sessionId && agentState.ready && !agentState.colonyId) {
|
|
||||||
fetchGraph(agentState.sessionId);
|
|
||||||
}
|
|
||||||
}, [agentState.sessionId, agentState.ready, agentState.colonyId, fetchGraph]);
|
|
||||||
|
|
||||||
// ── SSE event handler ──────────────────────────────────────────────────
|
// ── SSE event handler ──────────────────────────────────────────────────
|
||||||
|
|
||||||
const handleSSEEvent = useCallback(
|
const handleSSEEvent = useCallback(
|
||||||
@@ -635,8 +514,6 @@ export default function ColonyChat() {
|
|||||||
isStreaming: false,
|
isStreaming: false,
|
||||||
workerIsTyping: true,
|
workerIsTyping: true,
|
||||||
awaitingInput: false,
|
awaitingInput: false,
|
||||||
workerRunState: "running",
|
|
||||||
currentExecutionId: event.execution_id || state.currentExecutionId || null,
|
|
||||||
currentRunId: incomingRunId,
|
currentRunId: incomingRunId,
|
||||||
nodeLogs: {},
|
nodeLogs: {},
|
||||||
subagentReports: [],
|
subagentReports: [],
|
||||||
@@ -662,8 +539,6 @@ export default function ColonyChat() {
|
|||||||
workerIsTyping: false,
|
workerIsTyping: false,
|
||||||
awaitingInput: false,
|
awaitingInput: false,
|
||||||
workerInputMessageId: null,
|
workerInputMessageId: null,
|
||||||
workerRunState: "idle",
|
|
||||||
currentExecutionId: null,
|
|
||||||
llmSnapshots: {},
|
llmSnapshots: {},
|
||||||
pendingQuestion: null,
|
pendingQuestion: null,
|
||||||
pendingOptions: null,
|
pendingOptions: null,
|
||||||
@@ -671,7 +546,6 @@ export default function ColonyChat() {
|
|||||||
pendingQuestionSource: null,
|
pendingQuestionSource: null,
|
||||||
});
|
});
|
||||||
markAllNodesAs(["running", "looping"], "complete");
|
markAllNodesAs(["running", "looping"], "complete");
|
||||||
if (state.sessionId) fetchGraph(state.sessionId, state.colonyId || undefined);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -745,7 +619,6 @@ export default function ColonyChat() {
|
|||||||
isTyping: false,
|
isTyping: false,
|
||||||
isStreaming: false,
|
isStreaming: false,
|
||||||
queenIsTyping: false,
|
queenIsTyping: false,
|
||||||
queenBuilding: false,
|
|
||||||
pendingQuestion: prompt || null,
|
pendingQuestion: prompt || null,
|
||||||
pendingOptions: options,
|
pendingOptions: options,
|
||||||
pendingQuestions: questions,
|
pendingQuestions: questions,
|
||||||
@@ -767,7 +640,6 @@ export default function ColonyChat() {
|
|||||||
pendingQuestionSource: null,
|
pendingQuestionSource: null,
|
||||||
});
|
});
|
||||||
if (!isQueen) {
|
if (!isQueen) {
|
||||||
updateState({ workerRunState: "idle", currentExecutionId: null });
|
|
||||||
markAllNodesAs(["running", "looping"], "pending");
|
markAllNodesAs(["running", "looping"], "pending");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -785,7 +657,6 @@ export default function ColonyChat() {
|
|||||||
pendingQuestionSource: null,
|
pendingQuestionSource: null,
|
||||||
});
|
});
|
||||||
if (!isQueen) {
|
if (!isQueen) {
|
||||||
updateState({ workerRunState: "idle", currentExecutionId: null });
|
|
||||||
if (event.node_id) {
|
if (event.node_id) {
|
||||||
updateGraphNodeStatus(event.node_id, "error");
|
updateGraphNodeStatus(event.node_id, "error");
|
||||||
const errMsg = (event.data?.error as string) || "unknown error";
|
const errMsg = (event.data?.error as string) || "unknown error";
|
||||||
@@ -898,12 +769,6 @@ export default function ColonyChat() {
|
|||||||
const toolName = (event.data?.tool_name as string) || "unknown";
|
const toolName = (event.data?.tool_name as string) || "unknown";
|
||||||
const toolUseId = (event.data?.tool_use_id as string) || "";
|
const toolUseId = (event.data?.tool_use_id as string) || "";
|
||||||
|
|
||||||
if (isQueen && toolName === "save_agent_draft") {
|
|
||||||
designingDraftSinceRef.current = Date.now();
|
|
||||||
if (designingDraftTimerRef.current) clearTimeout(designingDraftTimerRef.current);
|
|
||||||
updateState({ designingDraft: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
const sid = event.stream_id;
|
const sid = event.stream_id;
|
||||||
setAgentState((prev) => {
|
setAgentState((prev) => {
|
||||||
const newActive = {
|
const newActive = {
|
||||||
@@ -1002,7 +867,7 @@ export default function ColonyChat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "credentials_required": {
|
case "credentials_required": {
|
||||||
updateState({ workerRunState: "idle", error: "credentials_required" });
|
updateState({ error: "credentials_required" });
|
||||||
const credAgentPath = event.data?.agent_path as string | undefined;
|
const credAgentPath = event.data?.agent_path as string | undefined;
|
||||||
if (credAgentPath) setCredentialAgentPath(credAgentPath);
|
if (credAgentPath) setCredentialAgentPath(credAgentPath);
|
||||||
setCredentialsOpen(true);
|
setCredentialsOpen(true);
|
||||||
@@ -1025,64 +890,20 @@ export default function ColonyChat() {
|
|||||||
queenPhaseRef.current = newPhase;
|
queenPhaseRef.current = newPhase;
|
||||||
updateState({
|
updateState({
|
||||||
queenPhase: newPhase,
|
queenPhase: newPhase,
|
||||||
queenBuilding: newPhase === "building",
|
|
||||||
workerRunState: newPhase === "running" ? "running" : "idle",
|
|
||||||
...(newPhase === "planning" ? { originalDraft: null, flowchartMap: null } : {}),
|
|
||||||
...(eventAgentPath ? { agentPath: eventAgentPath } : {}),
|
...(eventAgentPath ? { agentPath: eventAgentPath } : {}),
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "custom": {
|
|
||||||
const customEvent = event.data as Record<string, unknown>;
|
|
||||||
if (customEvent?.event === "draft_updated") {
|
|
||||||
const draft = customEvent as unknown as DraftGraphData | undefined;
|
|
||||||
if (draft?.nodes) {
|
|
||||||
const MIN_SPINNER_MS = 600;
|
|
||||||
const since = designingDraftSinceRef.current;
|
|
||||||
const elapsed = Date.now() - since;
|
|
||||||
const remaining = Math.max(0, MIN_SPINNER_MS - elapsed);
|
|
||||||
if (remaining > 0 && since > 0) {
|
|
||||||
updateState({ draftGraph: draft });
|
|
||||||
designingDraftTimerRef.current = setTimeout(() => {
|
|
||||||
updateState({ designingDraft: false });
|
|
||||||
}, remaining);
|
|
||||||
} else {
|
|
||||||
updateState({ draftGraph: draft, designingDraft: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (customEvent?.event === "flowchart_updated") {
|
|
||||||
const mapData = customEvent as {
|
|
||||||
map?: Record<string, string[]>;
|
|
||||||
original_draft?: DraftGraphData;
|
|
||||||
};
|
|
||||||
if (mapData) {
|
|
||||||
updateState({
|
|
||||||
flowchartMap: mapData.map ?? null,
|
|
||||||
originalDraft: mapData.original_draft ?? null,
|
|
||||||
draftGraph: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "worker_colony_loaded": {
|
case "worker_colony_loaded": {
|
||||||
const graphName = event.data?.colony_name as string | undefined;
|
const graphName = event.data?.colony_name as string | undefined;
|
||||||
const agentPathFromEvent = event.data?.agent_path as string | undefined;
|
const agentPathFromEvent = event.data?.agent_path as string | undefined;
|
||||||
const dn = formatAgentDisplayName(graphName || agentSlug(agentPath));
|
const dn = formatAgentDisplayName(graphName || agentSlug(agentPath));
|
||||||
clearCredentialCache(agentPathFromEvent);
|
clearCredentialCache(agentPathFromEvent);
|
||||||
updateState({
|
updateState({ displayName: dn });
|
||||||
displayName: dn,
|
|
||||||
queenBuilding: false,
|
|
||||||
workerRunState: "idle",
|
|
||||||
colonyId: null,
|
|
||||||
nodeSpecs: [],
|
|
||||||
});
|
|
||||||
setGraphNodes([]);
|
setGraphNodes([]);
|
||||||
// Remove old worker messages
|
// Remove old worker messages
|
||||||
setMessages((prev) => prev.filter((m) => m.role !== "worker"));
|
setMessages((prev) => prev.filter((m) => m.role !== "worker"));
|
||||||
if (state.sessionId) fetchGraph(state.sessionId);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1165,7 +986,7 @@ export default function ColonyChat() {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[agentPath, queenInfo.name, updateState, upsertMessage, updateGraphNodeStatus, markAllNodesAs, appendNodeLog, fetchGraph, graphNodes],
|
[agentPath, queenInfo.name, updateState, upsertMessage, updateGraphNodeStatus, markAllNodesAs, appendNodeLog, graphNodes],
|
||||||
);
|
);
|
||||||
|
|
||||||
// ── SSE subscription ───────────────────────────────────────────────────
|
// ── SSE subscription ───────────────────────────────────────────────────
|
||||||
@@ -1181,46 +1002,6 @@ export default function ColonyChat() {
|
|||||||
|
|
||||||
// ── Action handlers ────────────────────────────────────────────────────
|
// ── Action handlers ────────────────────────────────────────────────────
|
||||||
|
|
||||||
const handleRun = useCallback(async () => {
|
|
||||||
if (!agentState.sessionId || !agentState.ready) return;
|
|
||||||
setDismissedBanner(null);
|
|
||||||
try {
|
|
||||||
updateState({ workerRunState: "deploying" });
|
|
||||||
const result = await executionApi.trigger(agentState.sessionId, "default", {});
|
|
||||||
updateState({ currentExecutionId: result.execution_id });
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof ApiError && err.status === 424) {
|
|
||||||
const errBody = (err as ApiError).body as Record<string, unknown>;
|
|
||||||
const credPath = (errBody?.agent_path as string) || null;
|
|
||||||
if (credPath) setCredentialAgentPath(credPath);
|
|
||||||
updateState({ workerRunState: "idle", error: "credentials_required" });
|
|
||||||
setCredentialsOpen(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const errMsg = err instanceof Error ? err.message : String(err);
|
|
||||||
upsertMessage({
|
|
||||||
id: makeId(),
|
|
||||||
agent: "System",
|
|
||||||
agentColor: "",
|
|
||||||
content: `Failed to trigger run: ${errMsg}`,
|
|
||||||
timestamp: "",
|
|
||||||
type: "system",
|
|
||||||
thread: agentPath,
|
|
||||||
createdAt: Date.now(),
|
|
||||||
});
|
|
||||||
updateState({ workerRunState: "idle" });
|
|
||||||
}
|
|
||||||
}, [agentState.sessionId, agentState.ready, agentPath, updateState, upsertMessage]);
|
|
||||||
|
|
||||||
const handlePause = useCallback(async () => {
|
|
||||||
if (!agentState.sessionId || !agentState.currentExecutionId) return;
|
|
||||||
try {
|
|
||||||
await executionApi.pause(agentState.sessionId, agentState.currentExecutionId);
|
|
||||||
} catch {
|
|
||||||
// fire-and-forget
|
|
||||||
}
|
|
||||||
}, [agentState.sessionId, agentState.currentExecutionId]);
|
|
||||||
|
|
||||||
const handleCancelQueen = useCallback(async () => {
|
const handleCancelQueen = useCallback(async () => {
|
||||||
if (!agentState.sessionId) return;
|
if (!agentState.sessionId) return;
|
||||||
try {
|
try {
|
||||||
@@ -1416,44 +1197,14 @@ export default function ColonyChat() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Pipeline graph panel */}
|
{/* Triggers sidebar */}
|
||||||
<div
|
<div className="w-[260px] flex-shrink-0">
|
||||||
className="bg-card/30 flex flex-col border-l border-border/30 relative"
|
<TriggersPanel
|
||||||
style={{ width: `${graphPanelPct}%`, minWidth: 240, flexShrink: 0 }}
|
triggers={graphNodes.filter((n) => n.nodeType === "trigger")}
|
||||||
>
|
selectedId={resolvedSelectedNode?.id ?? null}
|
||||||
<div className="flex-1 min-h-0">
|
onSelect={(trigger) =>
|
||||||
<DraftGraph
|
setSelectedNode((prev) => (prev?.id === trigger.id ? null : trigger))
|
||||||
key={colonyId}
|
}
|
||||||
draft={agentState.originalDraft ?? agentState.draftGraph ?? null}
|
|
||||||
originalDraft={agentState.originalDraft ?? null}
|
|
||||||
loadingMessage={
|
|
||||||
agentState.designingDraft
|
|
||||||
? "Designing flowchart..."
|
|
||||||
: !agentState.originalDraft &&
|
|
||||||
!agentState.draftGraph &&
|
|
||||||
agentState.queenPhase !== "planning"
|
|
||||||
? "Loading flowchart..."
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
building={agentState.queenBuilding}
|
|
||||||
onRun={handleRun}
|
|
||||||
onPause={handlePause}
|
|
||||||
runState={agentState.workerRunState}
|
|
||||||
flowchartMap={agentState.flowchartMap ?? undefined}
|
|
||||||
runtimeNodes={graphNodes}
|
|
||||||
onRuntimeNodeClick={(runtimeNodeId) => {
|
|
||||||
const node = graphNodes.find((n) => n.id === runtimeNodeId);
|
|
||||||
if (node) setSelectedNode((prev) => (prev?.id === node.id ? null : node));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/* Resize handle */}
|
|
||||||
<div
|
|
||||||
className="absolute top-0 left-0 w-1 h-full cursor-col-resize hover:bg-primary/30 active:bg-primary/40 transition-colors z-10"
|
|
||||||
onMouseDown={() => {
|
|
||||||
resizing.current = true;
|
|
||||||
document.body.style.cursor = "col-resize";
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1463,7 +1214,6 @@ export default function ColonyChat() {
|
|||||||
<NodeDetailPanel
|
<NodeDetailPanel
|
||||||
node={resolvedSelectedNode}
|
node={resolvedSelectedNode}
|
||||||
sessionId={agentState.sessionId || ""}
|
sessionId={agentState.sessionId || ""}
|
||||||
colonyId={agentState.colonyId || ""}
|
|
||||||
nodeLogs={agentState.nodeLogs[resolvedSelectedNode.id] || []}
|
nodeLogs={agentState.nodeLogs[resolvedSelectedNode.id] || []}
|
||||||
actionPlan={agentState.nodeActionPlans[resolvedSelectedNode.id]}
|
actionPlan={agentState.nodeActionPlans[resolvedSelectedNode.id]}
|
||||||
onClose={() => setSelectedNode(null)}
|
onClose={() => setSelectedNode(null)}
|
||||||
|
|||||||
Reference in New Issue
Block a user