Skip to content

Campaign Graph View

The campaign graph is the primary view for plan details. It renders log entries as a vertical flow of connected nodes — each primary action (send email, send SMS, ask user, complete) becomes a card with inline approve/reject controls and expandable supporting context.

The timeline view remains available as a secondary tab for audit purposes.

The plan details view uses a tab switcher with two views:

TabDefaultContent
GraphVisual flow with connected node cards
TimelineChronological flat list (existing view)

Three structs represent the graph:

src/mods/plan/types/plan_graph_type.rs
pub struct GraphNode {
pub id: Uuid,
pub log_entry: PlanLogDisplay, // The primary action
pub supporting: Vec<PlanLogDisplay>, // Attached context entries
pub position_index: usize, // Display order (0 = newest)
pub execution_run: Option<usize>, // Run boundary tracker
}
pub struct GraphEdge {
pub from: Uuid, // Older node
pub to: Uuid, // Newer node
pub is_cross_run: bool, // True when edge spans a schedule boundary
}
pub struct GraphData {
pub nodes: Vec<GraphNode>, // Newest-first
pub edges: Vec<GraphEdge>,
}

Every PlanLogEntryToolCall variant is classified into one of three roles that drive graph grouping:

pub enum ToolCallRole {
Primary, // Top-level graph nodes
PostAction, // Attach to the previous primary node
Supporting, // Attach to the next primary node
}
RoleTool CallsGraph Behavior
PrimarySendEmail, SendSms, AskUser, CompletePlan, FailPlan, ScheduleNextExecutionBecomes a top-level node card
PostActionWriteInteractionNote, UpdateSystemNoteAttaches to the previous primary node’s supporting list
SupportingGetContactDetails, GetContactNotes, ListPlanContacts, GetConversationHistoryAttaches to the next primary node’s supporting list

Each variant also provides a display_name() for consistent labeling across the UI (e.g., SendEmail → “Send Email”, AskUser → “Question”).

build_graph_data() in src/mods/plan/build_graph_data.rs converts a chronologically-sorted log into a GraphData structure:

  1. Walk entries chronologically, maintaining a buffer of non-primary entries
  2. Skip ExecutionStarted and ExecutionEnded system events (they only track run boundaries)
  3. Track execution runs — increment the run counter when ExecutionStarted follows a ScheduleNextExecution
  4. On primary entry: partition the buffer into post-action and supporting entries. Post-action entries attach to the previous node; supporting entries attach to the current node
  5. Trailing buffer: any remaining entries attach to the last primary node
  6. Reverse nodes to newest-first and assign position_index values
  7. Build edges between consecutive nodes, marking is_cross_run when execution runs differ
Chronological log:
[GetContact] [Reasoning] [SendEmail] [WriteNote] [Schedule] ... [SendSMS]
↓ ↓ ↓ ↓ ↓ ↓
Supporting Supporting Primary PostAction Primary Primary
Graph result (newest-first):
SendSMS → Schedule → SendEmail
│ │
│ supporting: [GetContact, Reasoning]
│ supporting: [WriteNote] (post-action from Schedule)
└── cross-run edge (dashed)

File: src/mods/plan/components/plan_graph_component.rs

The main container. Calls build_graph_data() on the log entries and renders nodes with edges between them. Handles plan state indicators:

Plan StateRendering
PendingReviewStart node with approve/reject buttons
ExecutingGhost node with spinner at the top
StandByBadge showing next execution time
AwaitingInputAuto-expands the first node requiring input
CompletedStatic graph, no action controls
FailedStatic graph with failure indicator

File: src/mods/plan/components/plan_graph_node_component.rs

Renders a single node card with:

  • Status styling — color-coded by tool call output state (pending, done, failed)
  • Expand/collapse — click to reveal supporting entries grouped inside the card
  • Inline actions — approve/reject buttons for pending actions, answer input for AskUser
  • Tool label — uses display_name() for consistent naming

File: src/mods/plan/components/plan_graph_edge_component.rs

SVG vertical connector between nodes. Renders a solid line for same-run edges and a dashed line with a run boundary label for is_cross_run edges.

When you add a new tool call variant to PlanLogEntryToolCall:

  1. Add the display_name() match arm with a human-readable label
  2. Add the role() match arm — decide if it’s Primary, PostAction, or Supporting
  3. If it’s Primary, the graph automatically creates a node for it
  4. If it needs custom rendering in the node card, update PlanGraphNodeComponent
FilePurpose
src/mods/plan/types/plan_graph_type.rsGraphNode, GraphEdge, GraphData structs
src/mods/plan/types/plan_log_entry_type.rsToolCallRole enum, role(), display_name()
src/mods/plan/build_graph_data.rsGrouping algorithm + 10 unit tests
src/mods/plan/components/plan_graph_component.rsMain graph container
src/mods/plan/components/plan_graph_node_component.rsNode card with actions
src/mods/plan/components/plan_graph_edge_component.rsSVG edge connector
src/mods/plan/views/plan_details_view.rsTab switcher (Graph + Timeline)