Campaign Graph View
The campaign graph view renders plan execution as a vertical flow of connected nodes. Each node represents a primary action (send email, send SMS, complete plan), with supporting tool calls grouped as collapsible context entries.
Data Pipeline
Section titled “Data Pipeline”The graph transforms a flat list of plan log entries into a grouped, connected node structure:
PlanLogDisplay[] → build_graph_data() → GraphData { nodes, edges }Graph Data Model
Section titled “Graph Data Model”pub struct GraphNode { pub id: Uuid, pub log_entry: PlanLogDisplay, // The primary action pub supporting: Vec<PlanLogDisplay>, // Grouped context entries pub position_index: usize, // 0 = newest pub execution_run: Option<usize>, // Run number for cross-run edges}
pub struct GraphEdge { pub from: Uuid, pub to: Uuid, pub is_cross_run: bool, // True when edge spans a ScheduleNextExecution boundary}
pub struct GraphData { pub nodes: Vec<GraphNode>, pub edges: Vec<GraphEdge>,}Tool Call Roles
Section titled “Tool Call Roles”Every tool call is classified into one of three roles that drive the grouping algorithm:
pub enum ToolCallRole { Primary, // Top-level graph node PostAction, // Attaches to the previous primary node Supporting, // Attaches to the next primary node}| Role | Tool Calls | Behavior |
|---|---|---|
| Primary | SendEmail, SendSms, AskUser, CompletePlan, FailPlan, ScheduleNextExecution | Creates a new graph node |
| PostAction | WriteInteractionNote, UpdateSystemNote | Appends to the previous primary node’s supporting list |
| Supporting | ListPlanContacts, GetContactDetails, GetContactNotes, GetConversationHistory | Buffers until the next primary node, then attaches as context |
Classification lives on PlanLogEntryToolCall via role() and display_name() methods. Adding a new tool call variant requires choosing its role in the exhaustive match — the compiler enforces this.
Grouping Algorithm
Section titled “Grouping Algorithm”build_graph_data() in src/mods/plan/build_graph_data.rs walks log entries chronologically:
- Buffer supporting entries as they arrive
- When a primary entry appears:
- Split the buffer: PostAction entries attach to the previous node, remaining entries attach to the current node
- Create a new
GraphNodewith the primary entry and its supporting context
- Track execution runs — increment when a
ScheduleNextExecutionis followed by anExecutionStartedsystem event - Reverse nodes to newest-first order and assign
position_indexvalues - Build edges between consecutive nodes, marking
is_cross_run: truewhenexecution_rundiffers
Component Structure
Section titled “Component Structure”PlanGraph
Section titled “PlanGraph”plan_graph_component.rs — the main container. Renders a vertical spine line, state-specific indicators, and iterates over GraphData:
- PendingReview → approve/reject start node with amber glow
- Executing → ghost node with spinner animation
- StandBy → badge with clock icon and
next_execution_attimestamp - AwaitingInput → auto-expands the first node so the pending question is visible
Run boundaries appear as labeled dividers between execution runs: “Run 2 · 2026-03-10 14:00”.
PlanGraphNode
Section titled “PlanGraphNode”plan_graph_node_component.rs — an expandable card for each primary action:
- Collapsed: icon (color-coded by tool type), display name, truncated description, status dot, supporting entry count badge
- Expanded: full tool content — email/SMS body rendered as Markdown,
AskUserquestion with option buttons and free-text input, schedule details withscheduled_at/expires_at - Supporting context: listed in a “Context” section with left-border color coding
- Inline actions: approve/reject buttons for pending outbound tools, answer interface for questions
Node styling varies by tool call status:
| Status | Style |
|---|---|
pending | Amber dashed border, breathing glow, action buttons visible |
approved | Blue solid border with inset shadow |
done | Primary border with outer glow |
rejected / expired | Muted, reduced opacity |
failed | Red border with shadow |
PlanGraphEdge
Section titled “PlanGraphEdge”plan_graph_edge_component.rs — SVG vertical connector between nodes with a midpoint dot. Cross-run edges render with a dashed stroke pattern (stroke-dasharray: "4,3").
View Integration
Section titled “View Integration”The plan details view (plan_details_view.rs) uses a tab switcher:
- Graph (default) — the campaign graph visualization
- Timeline — the original chronological audit log, preserved for detailed inspection
Both views share the same use_realtime_plan() hook for live WebSocket updates. New log entries and state changes merge into the graph in real time.
Adding a New Tool
Section titled “Adding a New Tool”When you add a new tool call variant to PlanLogEntryToolCall:
- Add the variant to the enum
- The compiler forces you to handle it in
role()— choosePrimary,PostAction, orSupporting - Add a label in
display_name() - The graph automatically picks it up — no component changes needed