added pause/run button
This commit is contained in:
@@ -1062,8 +1062,12 @@ class EventLoopNode(NodeProtocol):
|
||||
mcp_tool_calls = [
|
||||
tc
|
||||
for tc in logged_tool_calls
|
||||
if tc.get("tool_name") not in (
|
||||
"set_output", "ask_user", "ask_user_multiple", "escalate",
|
||||
if tc.get("tool_name")
|
||||
not in (
|
||||
"set_output",
|
||||
"ask_user",
|
||||
"ask_user_multiple",
|
||||
"escalate",
|
||||
)
|
||||
]
|
||||
if mcp_tool_calls:
|
||||
@@ -1262,7 +1266,9 @@ class EventLoopNode(NodeProtocol):
|
||||
multi_qs = getattr(self, "_pending_multi_questions", None)
|
||||
self._pending_multi_questions = None
|
||||
got_input = await self._await_user_input(
|
||||
ctx, prompt=_cf_prompt, options=ask_user_options,
|
||||
ctx,
|
||||
prompt=_cf_prompt,
|
||||
options=ask_user_options,
|
||||
questions=multi_qs,
|
||||
)
|
||||
# Emit deferred tool_call_completed for ask_user / ask_user_multiple
|
||||
@@ -2193,7 +2199,7 @@ class EventLoopNode(NodeProtocol):
|
||||
for i, q in enumerate(raw_questions):
|
||||
if not isinstance(q, dict):
|
||||
continue
|
||||
qid = str(q.get("id", f"q{i+1}"))
|
||||
qid = str(q.get("id", f"q{i + 1}"))
|
||||
prompt = str(q.get("prompt", ""))
|
||||
opts = q.get("options", None)
|
||||
if isinstance(opts, list):
|
||||
@@ -2202,11 +2208,13 @@ class EventLoopNode(NodeProtocol):
|
||||
opts = None
|
||||
else:
|
||||
opts = None
|
||||
questions.append({
|
||||
"id": qid,
|
||||
"prompt": prompt,
|
||||
**({"options": opts} if opts else {}),
|
||||
})
|
||||
questions.append(
|
||||
{
|
||||
"id": qid,
|
||||
"prompt": prompt,
|
||||
**({"options": opts} if opts else {}),
|
||||
}
|
||||
)
|
||||
|
||||
# Store as multi-question prompt/options for
|
||||
# the event emission path
|
||||
@@ -2710,8 +2718,7 @@ class EventLoopNode(NodeProtocol):
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": (
|
||||
"Short identifier for this question "
|
||||
"(used in the response)."
|
||||
"Short identifier for this question (used in the response)."
|
||||
),
|
||||
},
|
||||
"prompt": {
|
||||
|
||||
@@ -429,10 +429,7 @@ def _dissolve_planning_nodes(
|
||||
|
||||
# Decision clause: prefer decision_clause, fall back to description/name
|
||||
clause = (
|
||||
d_node.get("decision_clause")
|
||||
or d_node.get("description")
|
||||
or d_node.get("name")
|
||||
or d_id
|
||||
d_node.get("decision_clause") or d_node.get("description") or d_node.get("name") or d_id
|
||||
).strip()
|
||||
|
||||
predecessors = [node_by_id[e["source"]] for e in in_edges if e["source"] in node_by_id]
|
||||
@@ -1079,14 +1076,16 @@ def register_queen_lifecycle_tools(
|
||||
}
|
||||
)
|
||||
edge_counter += 1
|
||||
edges.append({
|
||||
"id": f"edge-subagent-{edge_counter}",
|
||||
"source": sa_id,
|
||||
"target": node["id"],
|
||||
"condition": "always",
|
||||
"description": "sub-agent report back",
|
||||
"label": "report",
|
||||
})
|
||||
edges.append(
|
||||
{
|
||||
"id": f"edge-subagent-{edge_counter}",
|
||||
"source": sa_id,
|
||||
"target": node["id"],
|
||||
"condition": "always",
|
||||
"description": "sub-agent report back",
|
||||
"label": "report",
|
||||
}
|
||||
)
|
||||
edge_counter += 1
|
||||
|
||||
# Group sub-agent nodes under their parent in the flowchart map
|
||||
@@ -1627,14 +1626,8 @@ def register_queen_lifecycle_tools(
|
||||
if leaf_node_ids:
|
||||
for leaf_id in leaf_node_ids:
|
||||
# Find edges where this leaf node is the source
|
||||
out_edges = [
|
||||
e for e in validated_edges
|
||||
if e["source"] == leaf_id
|
||||
]
|
||||
in_edges = [
|
||||
e for e in validated_edges
|
||||
if e["target"] == leaf_id
|
||||
]
|
||||
out_edges = [e for e in validated_edges if e["source"] == leaf_id]
|
||||
in_edges = [e for e in validated_edges if e["target"] == leaf_id]
|
||||
if not out_edges:
|
||||
continue # already a proper leaf
|
||||
|
||||
@@ -1653,7 +1646,8 @@ def register_queen_lifecycle_tools(
|
||||
"GCU/subagent node '%s' has illegal outgoing "
|
||||
"edges to %s — stripping them. GCU nodes "
|
||||
"must be leaf sub-agents.",
|
||||
leaf_id, illegal_targets,
|
||||
leaf_id,
|
||||
illegal_targets,
|
||||
)
|
||||
topology_corrections.append(
|
||||
f"GCU node '{leaf_id}' had illegal edges to "
|
||||
@@ -1663,19 +1657,21 @@ def register_queen_lifecycle_tools(
|
||||
# Rewire: predecessor → leaf's targets (skip leaf)
|
||||
for parent_id in parent_ids:
|
||||
for tgt_id in illegal_targets:
|
||||
validated_edges.append({
|
||||
"id": f"edge-rewire-{len(validated_edges)}",
|
||||
"source": parent_id,
|
||||
"target": tgt_id,
|
||||
"condition": "on_success",
|
||||
"description": "",
|
||||
"label": "",
|
||||
})
|
||||
validated_edges.append(
|
||||
{
|
||||
"id": f"edge-rewire-{len(validated_edges)}",
|
||||
"source": parent_id,
|
||||
"target": tgt_id,
|
||||
"condition": "on_success",
|
||||
"description": "",
|
||||
"label": "",
|
||||
}
|
||||
)
|
||||
# Remove the illegal edges
|
||||
validated_edges[:] = [
|
||||
e for e in validated_edges
|
||||
if not (e["source"] == leaf_id
|
||||
and e["target"] in set(illegal_targets))
|
||||
e
|
||||
for e in validated_edges
|
||||
if not (e["source"] == leaf_id and e["target"] in set(illegal_targets))
|
||||
]
|
||||
|
||||
# Ensure the leaf is in its parent's sub_agents list
|
||||
@@ -1719,9 +1715,7 @@ def register_queen_lifecycle_tools(
|
||||
f"removed. Add it to a parent node's sub_agents "
|
||||
f"list and re-save the draft."
|
||||
)
|
||||
validated_nodes[:] = [
|
||||
n for n in validated_nodes if n["id"] not in set(orphaned_ids)
|
||||
]
|
||||
validated_nodes[:] = [n for n in validated_nodes if n["id"] not in set(orphaned_ids)]
|
||||
node_by_id_v = {n["id"]: n for n in validated_nodes}
|
||||
|
||||
# Synthesize visual edges for sub-agents that are referenced in
|
||||
@@ -1734,25 +1728,29 @@ def register_queen_lifecycle_tools(
|
||||
if sa_id not in node_id_set:
|
||||
continue
|
||||
if (n["id"], sa_id) not in existing_edge_pairs:
|
||||
validated_edges.append({
|
||||
"id": f"edge-subagent-{edge_counter}",
|
||||
"source": n["id"],
|
||||
"target": sa_id,
|
||||
"condition": "always",
|
||||
"description": "sub-agent delegation",
|
||||
"label": "delegate",
|
||||
})
|
||||
validated_edges.append(
|
||||
{
|
||||
"id": f"edge-subagent-{edge_counter}",
|
||||
"source": n["id"],
|
||||
"target": sa_id,
|
||||
"condition": "always",
|
||||
"description": "sub-agent delegation",
|
||||
"label": "delegate",
|
||||
}
|
||||
)
|
||||
edge_counter += 1
|
||||
existing_edge_pairs.add((n["id"], sa_id))
|
||||
if (sa_id, n["id"]) not in existing_edge_pairs:
|
||||
validated_edges.append({
|
||||
"id": f"edge-subagent-{edge_counter}",
|
||||
"source": sa_id,
|
||||
"target": n["id"],
|
||||
"condition": "always",
|
||||
"description": "sub-agent report back",
|
||||
"label": "report",
|
||||
})
|
||||
validated_edges.append(
|
||||
{
|
||||
"id": f"edge-subagent-{edge_counter}",
|
||||
"source": sa_id,
|
||||
"target": n["id"],
|
||||
"condition": "always",
|
||||
"description": "sub-agent report back",
|
||||
"label": "report",
|
||||
}
|
||||
)
|
||||
edge_counter += 1
|
||||
existing_edge_pairs.add((sa_id, n["id"]))
|
||||
|
||||
@@ -1930,7 +1928,8 @@ def register_queen_lifecycle_tools(
|
||||
if topology_corrections:
|
||||
correction_warning = (
|
||||
" WARNING — your draft had topology errors that were "
|
||||
"auto-corrected: " + "; ".join(topology_corrections)
|
||||
"auto-corrected: "
|
||||
+ "; ".join(topology_corrections)
|
||||
+ " Review the corrected flowchart and do NOT repeat "
|
||||
"this pattern. GCU nodes are ALWAYS leaf sub-agents."
|
||||
)
|
||||
@@ -1940,16 +1939,14 @@ def register_queen_lifecycle_tools(
|
||||
"Draft flowchart updated during building. "
|
||||
"Planning-only nodes dissolved automatically. "
|
||||
"The user can see the updated flowchart. "
|
||||
"Continue building — no re-confirmation needed."
|
||||
+ correction_warning
|
||||
"Continue building — no re-confirmation needed." + correction_warning
|
||||
)
|
||||
else:
|
||||
msg = (
|
||||
"Draft graph saved and sent to the visualizer. "
|
||||
"The user can now see the color-coded flowchart. "
|
||||
"Present this design to the user and get their approval. "
|
||||
"When the user confirms, call confirm_and_build() to proceed."
|
||||
+ correction_warning
|
||||
"When the user confirms, call confirm_and_build() to proceed." + correction_warning
|
||||
)
|
||||
|
||||
result: dict = {
|
||||
|
||||
@@ -20,7 +20,7 @@ export interface GraphNode {
|
||||
edgeLabels?: Record<string, string>;
|
||||
}
|
||||
|
||||
type RunState = "idle" | "deploying" | "running";
|
||||
export type RunState = "idle" | "deploying" | "running";
|
||||
|
||||
interface AgentGraphProps {
|
||||
nodes: GraphNode[];
|
||||
@@ -35,7 +35,7 @@ interface AgentGraphProps {
|
||||
}
|
||||
|
||||
// --- Extracted RunButton so hover state survives parent re-renders ---
|
||||
interface RunButtonProps {
|
||||
export interface RunButtonProps {
|
||||
runState: RunState;
|
||||
disabled: boolean;
|
||||
onRun: () => void;
|
||||
@@ -43,7 +43,7 @@ interface RunButtonProps {
|
||||
btnRef: React.Ref<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
const RunButton = memo(function RunButton({ runState, disabled, onRun, onPause, btnRef }: RunButtonProps) {
|
||||
export const RunButton = memo(function RunButton({ runState, disabled, onRun, onPause, btnRef }: RunButtonProps) {
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const showPause = runState === "running" && hovered;
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useEffect, useMemo, useRef, useState, useCallback } from "react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import type { DraftGraph as DraftGraphData, DraftNode } from "@/api/types";
|
||||
import type { GraphNode } from "./AgentGraph";
|
||||
import { RunButton } from "./AgentGraph";
|
||||
import type { GraphNode, RunState } from "./AgentGraph";
|
||||
|
||||
// Read a CSS custom property value (space-separated HSL components)
|
||||
function cssVar(name: string): string {
|
||||
@@ -82,6 +83,12 @@ interface DraftGraphProps {
|
||||
onRuntimeNodeClick?: (runtimeNodeId: string) => void;
|
||||
/** True while the queen is building the agent from the draft. */
|
||||
building?: boolean;
|
||||
/** Called when the user clicks Run. */
|
||||
onRun?: () => void;
|
||||
/** Called when the user clicks Pause. */
|
||||
onPause?: () => void;
|
||||
/** Current run state — drives the RunButton appearance. */
|
||||
runState?: RunState;
|
||||
}
|
||||
|
||||
// Layout constants — tuned for a ~500px panel (484px after px-2 padding)
|
||||
@@ -349,9 +356,10 @@ function Tooltip({ node, style }: { node: DraftNode; style: React.CSSProperties
|
||||
);
|
||||
}
|
||||
|
||||
export default function DraftGraph({ draft, onNodeClick, flowchartMap, runtimeNodes, onRuntimeNodeClick, building }: DraftGraphProps) {
|
||||
export default function DraftGraph({ draft, onNodeClick, flowchartMap, runtimeNodes, onRuntimeNodeClick, building, onRun, onPause, runState = "idle" }: DraftGraphProps) {
|
||||
const [hoveredNode, setHoveredNode] = useState<string | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const runBtnRef = useRef<HTMLButtonElement>(null);
|
||||
const [containerW, setContainerW] = useState(484);
|
||||
const chrome = useDraftChromeColors();
|
||||
|
||||
@@ -960,19 +968,24 @@ export default function DraftGraph({ draft, onNodeClick, flowchartMap, runtimeNo
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Header */}
|
||||
<div className="px-4 pt-3 pb-1.5 flex items-center gap-2">
|
||||
<p className="text-[11px] text-muted-foreground font-medium uppercase tracking-wider">
|
||||
{hasStatusOverlay ? "Flowchart" : "Draft"}
|
||||
</p>
|
||||
{building ? (
|
||||
<span className="text-[9px] font-mono font-medium rounded px-1 py-0.5 leading-none border text-primary/60 border-primary/20 flex items-center gap-1">
|
||||
<Loader2 className="w-2.5 h-2.5 animate-spin" />
|
||||
building
|
||||
</span>
|
||||
) : (
|
||||
<span className={`text-[9px] font-mono font-medium rounded px-1 py-0.5 leading-none border ${hasStatusOverlay ? "text-emerald-500/60 border-emerald-500/20" : "text-amber-500/60 border-amber-500/20"}`}>
|
||||
{hasStatusOverlay ? "live" : "planning"}
|
||||
</span>
|
||||
<div className="px-4 pt-3 pb-1.5 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-[11px] text-muted-foreground font-medium uppercase tracking-wider">
|
||||
{hasStatusOverlay ? "Flowchart" : "Draft"}
|
||||
</p>
|
||||
{building ? (
|
||||
<span className="text-[9px] font-mono font-medium rounded px-1 py-0.5 leading-none border text-primary/60 border-primary/20 flex items-center gap-1">
|
||||
<Loader2 className="w-2.5 h-2.5 animate-spin" />
|
||||
building
|
||||
</span>
|
||||
) : (
|
||||
<span className={`text-[9px] font-mono font-medium rounded px-1 py-0.5 leading-none border ${hasStatusOverlay ? "text-emerald-500/60 border-emerald-500/20" : "text-amber-500/60 border-amber-500/20"}`}>
|
||||
{hasStatusOverlay ? "live" : "planning"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{onRun && (
|
||||
<RunButton runState={runState} disabled={draft.nodes.length === 0} onRun={onRun} onPause={onPause ?? (() => {})} btnRef={runBtnRef} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2493,11 +2493,14 @@ export default function Workspace() {
|
||||
<div className={`${((activeAgentState?.queenPhase === "planning" || activeAgentState?.queenPhase === "building") && activeAgentState?.draftGraph) || activeAgentState?.originalDraft ? "w-[500px] min-w-[400px]" : "w-[300px] min-w-[240px]"} bg-card/30 flex flex-col border-r border-border/30 transition-[width] duration-200`}>
|
||||
<div className="flex-1 min-h-0">
|
||||
{(activeAgentState?.queenPhase === "planning" || activeAgentState?.queenPhase === "building") && activeAgentState?.draftGraph ? (
|
||||
<DraftGraph draft={activeAgentState.draftGraph} building={activeAgentState?.queenBuilding} />
|
||||
<DraftGraph draft={activeAgentState.draftGraph} building={activeAgentState?.queenBuilding} onRun={handleRun} onPause={handlePause} runState={activeAgentState?.workerRunState ?? "idle"} />
|
||||
) : activeAgentState?.originalDraft ? (
|
||||
<DraftGraph
|
||||
draft={activeAgentState.originalDraft}
|
||||
building={activeAgentState?.queenBuilding}
|
||||
onRun={handleRun}
|
||||
onPause={handlePause}
|
||||
runState={activeAgentState?.workerRunState ?? "idle"}
|
||||
flowchartMap={activeAgentState.flowchartMap ?? undefined}
|
||||
runtimeNodes={currentGraph.nodes}
|
||||
onRuntimeNodeClick={(runtimeNodeId) => {
|
||||
|
||||
Reference in New Issue
Block a user