added pause/run button

This commit is contained in:
bryan
2026-03-12 20:07:28 -07:00
parent 44bf191f53
commit 2bcb0cacee
5 changed files with 106 additions and 86 deletions
+18 -11
View File
@@ -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": {
+53 -56
View File
@@ -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 = {
+3 -3
View File
@@ -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;
+28 -15
View File
@@ -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>
+4 -1
View File
@@ -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) => {