feat(dashboard): add observability features and fix event flow bugs - #53
Conversation
) Add 5 observability features to the dashboard and fix 3 bugs that prevented events from reaching the frontend. Features: - Execution Tree: visual tree of iterations, tool calls, and subagent spawns with layout algorithm and node selection - Diff Viewer: inline diff display for Edit/Write tool invocations - Proto wiring: GetExecutionDetail RPC, SendInput RPC, heartbeat and stdin proto messages - Heartbeat: daemon emits periodic LogMessage events so frontend can detect liveness vs stalled executions - Stdin foundation: child_stdin field and send_input() method on ExecutionHandle for future interactive terminal support Bug fixes: - Revert Stdio::piped() to Stdio::null() for Claude CLI stdin — piped stdin with no writer caused the CLI to hang producing no output - Filter stderr batching to only emit ErrorOccurred events for lines containing error/panic/fatal keywords, preventing normal stderr (MCP connection info, debug logs) from polluting the event stream - Build execution tree from historical events when expanding a completed execution, fixing "No tree data yet" for finished runs Also updates README.md: correct select_agent.py line count (406→551), add new dashboard features to feature list, fix stale Docs/ directory structure, expand crates/dashboard/ tree detail. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reviewer's GuideAdds an execution tree visualization and diff viewer to the dashboard, wires new observability-related RPCs (execution detail, send_input, heartbeat) from daemon through gRPC/Tauri to the Leptos frontend, and fixes several event handling and stderr/stdio bugs to make executions observable and non-hanging. Sequence diagram for SendInput RPC path from dashboard to child stdinsequenceDiagram
actor User
participant Frontend as DashboardFrontend
participant Tauri as TauriCommands
participant DClient as DashboardGrpcClient
participant Svc as SuperClaudeService
participant Handle as ExecutionHandle
participant Child as ChildProcess
User->>Frontend: Trigger send_execution_input(execution_id, input)
Frontend->>Tauri: tauri_invoke send_execution_input
Tauri->>DClient: send_input(SendInputRequest)
DClient->>Svc: gRPC send_input(SendInputRequest)
Svc->>Svc: lookup execution handle in executions map
alt handle found
Svc->>Handle: send_input(input)
Handle->>Handle: child_stdin.write()
Handle->>Child: write input to stdin pipe
Handle-->>Svc: Result Ok
Svc-->>DClient: SendInputResponse success=true message="Input sent"
DClient-->>Tauri: SendInputResponse
Tauri-->>Frontend: Ok(message)
Frontend-->>User: Show success toast or status
else handle missing
Svc-->>DClient: gRPC error not_found
DClient-->>Tauri: Err(Status)
Tauri-->>Frontend: Err("gRPC error: ...")
Frontend-->>User: Show error "Execution not found"
end
Sequence diagram for heartbeat events and thinking indicatorsequenceDiagram
participant Exec as ExecutionInner
participant Daemon as SuperClaudeDaemon
participant Stream as EventStreamSubscribeEvents
participant DBackend as DashboardBackend
participant Frontend as DashboardFrontend
participant Monitor as MonitorPage
loop while ExecutionState is Running
Exec->>Exec: heartbeat task interval tick (5s)
Exec->>Daemon: emit_event(LogMessage{source="heartbeat", message="Processing..."})
Daemon-->>Stream: stream AgentEvent(LogMessage)
Stream-->>DBackend: AgentEvent
DBackend-->>Frontend: push AgentEventDto over websocket
Frontend->>Frontend: on_event update last_event_source="heartbeat"
Frontend->>Monitor: reactive state change
Monitor->>Monitor: compute is_thinking (has_active && last_event_source=="heartbeat")
Monitor-->>User: Render thinking_indicator("Claude is thinking...")
end
Exec->>Exec: execution finishes
Exec->>Exec: heartbeat_handle.abort()
Frontend->>Monitor: last_event_source changes away from heartbeat
Monitor-->>User: Hide thinking_indicator
Class diagram for execution observability and dashboard stateclassDiagram
class ExecutionInner {
+id: String
+state: RwLock~ExecutionState~
+process_pid: RwLock~Option~u32~~
+child_stdin: RwLock~Option~ChildStdin~~
+event_tx: Sender~AgentEvent~
+event_history: RwLock~VecDeque~AgentEvent~~
+evidence: RwLock~ExecutionEvidence~
+emit_event(event: AgentEvent)
+parse_stream_json_line(line: &str)
+handle_tool_invoked(tool_name: String, input: Value, parent_node_id: String, id: String)
+handle_tool_result(tool_name: String, tool_output: String, pending: PendingTool)
}
class ExecutionHandle {
-inner: Arc~ExecutionInner~
+start()
+stop()
+resume()
+send_input(input: &str) Result~()~
+get_status() ExecutionStatus
}
class SuperClaudeService {
-executions: DashMap~String, ExecutionHandle~
+send_input(request: Request~SendInputRequest~) Response~SendInputResponse~
+get_execution_detail(...)
}
class GrpcClient {
-client: superclaude_client::SuperClaudeClient
+subscribe_events(...)
+get_execution_detail(req: GetExecutionDetailRequest) Result~GetExecutionDetailResponse~
+send_input(req: SendInputRequest) Result~SendInputResponse~
}
class AppState {
+executions: RwSignal~Vec~ExecutionSummaryDto~~
+expanded_execution: RwSignal~Option~String~~
+execution_detail: RwSignal~Option~ExecutionDetailDto~~
+detail_loading: RwSignal~bool~
+execution_tree: RwSignal~ExecutionTree~
+selected_tree_node: RwSignal~Option~String~~
+last_event_source: RwSignal~String~
+new() AppState
}
class ExecutionDetailDto {
+execution_id: String
+events: Vec~AgentEventDto~
+files_written: Vec~String~
+files_edited: Vec~String~
+score_breakdown: Vec~ScoreDimensionDto~
}
class ExecutionTree {
+nodes: Vec~TreeNode~
+edges: Vec~TreeEdge~
}
class TreeNode {
+node_id: String
+parent_node_id: Option~String~
+node_type: TreeNodeType
+label: String
+summary: String
+status: TreeNodeStatus
+x: f64
+y: f64
+event_data: Option~Value~
}
class TreeEdge {
+from_id: String
+to_id: String
}
class TreeNodeType {
<<enum>>
Iteration
ToolCall
SubagentSpawn
}
class TreeNodeStatus {
<<enum>>
Running
Success
Failed
Pending
}
class ExecutionTreeComponent {
+ExecutionTree()
-layout_tree(tree: &mut ExecutionTree)
-status_class(status: TreeNodeStatus) String
-type_icon(node_type: TreeNodeType) String
}
class FilesChangedTabComponent {
+FilesChangedTab(files_written: Vec~String~, files_edited: Vec~String~, events: Vec~AgentEventDto~)
-extract_diff_entries(events: &[AgentEventDto]) Vec~FileDiffEntry~
}
class DiffViewComponent {
+DiffView(file_path: String, action: String, old_string: String, new_string: String, content: String)
}
class FileDiffEntry {
+file_path: String
+action: String
+old_string: Option~String~
+new_string: Option~String~
+content: Option~String~
}
ExecutionHandle --> ExecutionInner : holds
SuperClaudeService --> ExecutionHandle : uses
GrpcClient --> SuperClaudeService : gRPC client
AppState --> ExecutionTree : owns
ExecutionTree --> TreeNode : contains
ExecutionTree --> TreeEdge : contains
TreeNode --> TreeNodeType : uses
TreeNode --> TreeNodeStatus : uses
ExecutionTreeComponent --> AppState : reads signals
ExecutionTreeComponent --> ExecutionTree : renders
FilesChangedTabComponent --> ExecutionDetailDto : uses events
FilesChangedTabComponent --> DiffViewComponent : renders
FilesChangedTabComponent --> FileDiffEntry : builds
ExecutionDetailDto --> AgentEventDto : contains events
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
📝 WalkthroughWalkthroughThis pull request introduces execution tree visualization with interactive pan/zoom navigation, file diff viewing capabilities, a heartbeat indicator for active processing, and interactive input functionality for executions. It adds supporting state types, event-driven tree construction logic across frontend pages, new gRPC service methods, and enhanced event formatting with node and hierarchy metadata. Changes
Sequence Diagram(s)sequenceDiagram
participant Agent as Agent<br/>(Daemon)
participant AppState as AppState<br/>(Frontend)
participant TreeUI as ExecutionTree<br/>Component
participant User as User<br/>Browser
Agent->>AppState: Emit iteration_started<br/>(node_id, depth)
AppState->>AppState: Create TreeNode<br/>(Iteration type)
AppState->>AppState: Add to execution_tree
Agent->>AppState: Emit tool_invoked<br/>(node_id, parent_id)
AppState->>AppState: Create TreeNode<br/>(ToolCall type)
AppState->>AppState: Add TreeEdge<br/>(parent→tool)
Agent->>AppState: Emit iteration_completed
AppState->>AppState: Update node status<br/>to Success
AppState->>TreeUI: Signal execution_tree<br/>updated
TreeUI->>TreeUI: Layout nodes via DFS<br/>algorithm
TreeUI->>TreeUI: Render SVG with pan/zoom
TreeUI->>User: Display interactive tree
User->>TreeUI: Click node
TreeUI->>TreeUI: Show detail overlay<br/>with event_data
User->>TreeUI: Pan/Zoom interactions
sequenceDiagram
participant User as User<br/>Browser
participant TauriCmd as Tauri Command<br/>send_execution_input
participant GrpcClient as gRPC Client
participant Daemon as SuperClaude<br/>Daemon
participant Process as Child<br/>Process stdin
User->>TauriCmd: Invoke with<br/>execution_id, input
TauriCmd->>GrpcClient: send_input(request)
GrpcClient->>Daemon: SendInput RPC
Daemon->>Daemon: Lookup ExecutionHandle
Daemon->>Process: Write input to stdin<br/>via send_input
Process->>Daemon: stdin receives data
Daemon->>GrpcClient: SendInputResponse<br/>(success)
GrpcClient->>TauriCmd: Return response
TauriCmd->>User: Display confirmation
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes The changes span multiple layers (frontend components, state management, gRPC integration, daemon modifications) with new complex logic (tree layout algorithm, event-driven state synchronization, diff extraction). Notable concerns include duplicate send_input implementation in server.rs, CSS style duplication, and the interplay between event emission, tree construction, and rendering across multiple files requires careful verification. Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Hey - I've found 4 issues, and left some high level feedback:
- The execution-tree construction logic is duplicated between the real-time handler in
app.rsand the historical replay inbuild_tree_from_eventsincontrol.rs; consider extracting shared helpers for mappingAgentEventDto→TreeNode/TreeEdgeto keep behavior in sync and reduce maintenance overhead. - In
main.css,.tree-node rectusestransition: stroke-color 150ms ease;, butstroke-coloris not a valid animatable property in most browsers; switching this totransition: stroke 150ms ease;(or similar) will make the hover border animation work reliably.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The execution-tree construction logic is duplicated between the real-time handler in `app.rs` and the historical replay in `build_tree_from_events` in `control.rs`; consider extracting shared helpers for mapping `AgentEventDto` → `TreeNode`/`TreeEdge` to keep behavior in sync and reduce maintenance overhead.
- In `main.css`, `.tree-node rect` uses `transition: stroke-color 150ms ease;`, but `stroke-color` is not a valid animatable property in most browsers; switching this to `transition: stroke 150ms ease;` (or similar) will make the hover border animation work reliably.
## Individual Comments
### Comment 1
<location> `crates/dashboard/frontend/style/main.css:942-947` </location>
<code_context>
+ stroke-width: 1.5;
+}
+
+.tree-node rect {
+ fill: var(--bg-card);
+ stroke: var(--border);
+ stroke-width: 1;
+ cursor: pointer;
+ transition: stroke-color 150ms ease;
+}
+
</code_context>
<issue_to_address>
**issue (bug_risk):** The `transition` property uses `stroke-color`, which isn't a valid SVG/CSS property.
`stroke-color` is not a recognized property, so this transition will be ignored. Use `stroke` instead, e.g. `transition: stroke 150ms ease;` (and add `fill` if you also want the fill color animated).
</issue_to_address>
### Comment 2
<location> `crates/superclaude-daemon/src/execution.rs:837-846` </location>
<code_context>
});
}
+
+ // Emit SubagentCompleted when a Task tool result arrives
+ if is_task_tool {
+ self.emit_event(AgentEvent {
+ execution_id: self.id.clone(),
+ timestamp: Self::now_timestamp(),
+ event: Some(agent_event::Event::SubagentCompleted(SubagentCompleted {
+ subagent_id: pending.node_id.clone(),
+ success: true,
+ result_summary: truncate_str(&tool_output, 200),
+ node_id: format!("subagent-{}", pending.node_id),
+ })),
+ });
</code_context>
<issue_to_address>
**issue (bug_risk):** SubagentCompleted `node_id` format is inconsistent with SubagentSpawned, likely breaking tree linking.
In `SubagentSpawned` for `Task` tools, `node_id` is `format!("subagent-{}", id)` and `subagent_id` is `id.to_string()`. Here, `SubagentCompleted` uses `subagent_id: pending.node_id.clone()` and `node_id: format!("subagent-{}", pending.node_id)`, which will yield `subagent-subagent-…` if `pending.node_id` already has the prefix. Since the frontend looks up the node by `event.data["node_id"]`, this mismatch will prevent it from finding the original node and applying the completion state. Please align `subagent_id`/`node_id` with the spawn event (e.g. `subagent_id: id.to_string()`, `node_id: format!("subagent-{}", id)`).
</issue_to_address>
### Comment 3
<location> `crates/dashboard/frontend/src/app.rs:105` </location>
<code_context>
+ state.last_event_source.set(event.event_type.clone());
+ }
+
+ // Build the execution tree incrementally
+ if let Some(expanded_eid) = state.expanded_execution.get_untracked() {
+ if expanded_eid == eid {
</code_context>
<issue_to_address>
**issue (complexity):** Consider extracting the execution-tree event matching and mutation logic into a shared helper function that both `App` and `build_tree_from_events` call instead of duplicating it inline.
You’ve effectively duplicated the tree‑building rules inline in `App`, which does increase complexity and coupling. You can keep all functionality but centralize the logic in a reusable “tree updater” that both `App` and `build_tree_from_events` call.
### 1. Extract a reusable tree update function
In a shared module (e.g. `state::execution_tree` or `pages::control::tree`), define something like:
```rust
pub fn apply_event_to_tree(
tree: &mut ExecutionTree,
event: &AgentEventDto,
) {
match event.event_type.as_str() {
"iteration_started" => {
let node_id = event.data.get("node_id")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let iteration = event.data.get("iteration")
.and_then(|v| v.as_i64())
.unwrap_or(0);
if !node_id.is_empty() && !tree.nodes.iter().any(|n| n.node_id == node_id) {
tree.nodes.push(TreeNode {
node_id,
parent_node_id: None,
node_type: TreeNodeType::Iteration,
label: format!("Iteration {}", iteration),
summary: String::new(),
status: TreeNodeStatus::Running,
x: 0.0,
y: 0.0,
event_data: Some(event.data.clone()),
});
}
}
"tool_invoked" => {
let node_id = event.data.get("node_id")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let parent_node_id = event.data.get("parent_node_id")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let summary_val = event.data.get("summary")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
if summary_val == "(result)" {
// existing result logic
let base_id = node_id.strip_suffix("-result").unwrap_or(&node_id).to_string();
if let Some(node) = tree.nodes.iter_mut().find(|n| n.node_id == base_id) {
node.status = TreeNodeStatus::Success;
if let Some(output) = event.data.get("tool_output") {
if let Some(ref mut data) = node.event_data {
if let Some(obj) = data.as_object_mut() {
obj.insert("tool_output".to_string(), output.clone());
}
}
}
}
} else if !node_id.is_empty() && !tree.nodes.iter().any(|n| n.node_id == node_id) {
let tool_name = event.data.get("tool_name")
.and_then(|v| v.as_str())
.unwrap_or("Tool")
.to_string();
tree.nodes.push(TreeNode {
node_id: node_id.clone(),
parent_node_id: Some(parent_node_id.clone()),
node_type: TreeNodeType::ToolCall,
label: tool_name,
summary: summary_val,
status: TreeNodeStatus::Running,
x: 0.0,
y: 0.0,
event_data: Some(event.data.clone()),
});
if !parent_node_id.is_empty() {
tree.edges.push(TreeEdge {
from_id: parent_node_id,
to_id: node_id,
});
}
}
}
"iteration_completed" => {
let node_id = event.data.get("node_id")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
if !node_id.is_empty() {
if let Some(node) = tree.nodes.iter_mut().find(|n| n.node_id == node_id) {
node.status = TreeNodeStatus::Success;
}
}
}
"subagent_spawned" => {
// move your existing subagent_spawned logic here
}
"subagent_completed" => {
// move your existing subagent_completed logic here
}
_ => {}
}
}
```
You can iteratively migrate the remaining branches (`subagent_spawned`, `subagent_completed`) into this function, preserving the exact behavior you added.
### 2. Use it in `App`’s event handler
Replace the large `match` block in `App` with a narrow call that only gates on `expanded_eid`:
```rust
// Build the execution tree incrementally
if let Some(expanded_eid) = state.expanded_execution.get_untracked() {
if expanded_eid == eid {
state.execution_tree.update(|tree| {
apply_event_to_tree(tree, &event);
});
}
}
```
That keeps the event loop in `App` focused on “when to update the tree” rather than “how to update the tree,” which makes the UI code easier to follow.
### 3. Reuse in `build_tree_from_events`
In `control.rs`, `build_tree_from_events` can then delegate to the same helper instead of duplicating logic:
```rust
pub fn build_tree_from_events(events: &[AgentEventDto]) -> ExecutionTree {
let mut tree = ExecutionTree::default();
for event in events {
apply_event_to_tree(&mut tree, event);
}
tree
}
```
This removes the duplicated rules for node creation, edges, and status updates, and ensures future changes to tree semantics only need to be done in one place.
</issue_to_address>
### Comment 4
<location> `crates/dashboard/frontend/src/pages/control.rs:48` </location>
<code_context>
+/// Build an ExecutionTree from a list of historical events.
+/// This replays the same logic that app.rs uses for real-time events,
+/// ensuring the tree is populated when viewing completed executions.
+fn build_tree_from_events(events: &[AgentEventDto]) -> ExecutionTree {
+ let mut tree = ExecutionTree::default();
+
</code_context>
<issue_to_address>
**issue (complexity):** Consider extracting the shared event-to-execution-tree match logic into a reusable helper so both historical and real-time paths use the same code.
You can avoid maintaining two divergent implementations of the same event → tree logic by extracting the matching/branching into a shared helper and having both `build_tree_from_events` and the real-time handler in `app.rs` call it.
Concretely:
1. Extract the event-handling logic into a single helper (e.g. in `state` or a shared module):
```rust
pub fn update_execution_tree(tree: &mut ExecutionTree, event: &AgentEventDto) {
match event.event_type.as_str() {
"iteration_started" => {
let node_id = event.data.get("node_id")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let iteration = event.data.get("iteration")
.and_then(|v| v.as_i64())
.unwrap_or(0);
if !node_id.is_empty() && !tree.nodes.iter().any(|n| n.node_id == node_id) {
tree.nodes.push(TreeNode {
node_id,
parent_node_id: None,
node_type: TreeNodeType::Iteration,
label: format!("Iteration {}", iteration),
summary: String::new(),
status: TreeNodeStatus::Running,
x: 0.0,
y: 0.0,
event_data: Some(event.data.clone()),
});
}
}
"tool_invoked" => {
let node_id = event.data.get("node_id")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let parent_node_id = event.data.get("parent_node_id")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let summary_val = event.data.get("summary")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
if summary_val == "(result)" {
let base_id = node_id.strip_suffix("-result").unwrap_or(&node_id).to_string();
if let Some(node) = tree.nodes.iter_mut().find(|n| n.node_id == base_id) {
node.status = TreeNodeStatus::Success;
if let Some(output) = event.data.get("tool_output") {
if let Some(ref mut data) = node.event_data {
if let Some(obj) = data.as_object_mut() {
obj.insert("tool_output".to_string(), output.clone());
}
}
}
}
} else if !node_id.is_empty() && !tree.nodes.iter().any(|n| n.node_id == node_id) {
let tool_name = event.data.get("tool_name")
.and_then(|v| v.as_str())
.unwrap_or("Tool")
.to_string();
tree.nodes.push(TreeNode {
node_id: node_id.clone(),
parent_node_id: Some(parent_node_id.clone()),
node_type: TreeNodeType::ToolCall,
label: tool_name,
summary: summary_val,
status: TreeNodeStatus::Running,
x: 0.0,
y: 0.0,
event_data: Some(event.data.clone()),
});
if !parent_node_id.is_empty() {
tree.edges.push(TreeEdge {
from_id: parent_node_id,
to_id: node_id,
});
}
}
}
"iteration_completed" => {
let node_id = event.data.get("node_id")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
if let Some(node) = tree.nodes.iter_mut().find(|n| n.node_id == node_id) {
node.status = TreeNodeStatus::Success;
}
}
"subagent_spawned" => {
let node_id = event.data.get("node_id")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let parent_node_id = event.data.get("parent_node_id")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let subagent_type = event.data.get("subagent_type")
.and_then(|v| v.as_str())
.unwrap_or("Subagent")
.to_string();
let task_summary = event.data.get("task_summary")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
if !node_id.is_empty() && !tree.nodes.iter().any(|n| n.node_id == node_id) {
tree.nodes.push(TreeNode {
node_id: node_id.clone(),
parent_node_id: Some(parent_node_id.clone()),
node_type: TreeNodeType::SubagentSpawn,
label: subagent_type,
summary: task_summary,
status: TreeNodeStatus::Running,
x: 0.0,
y: 0.0,
event_data: Some(event.data.clone()),
});
if !parent_node_id.is_empty() {
tree.edges.push(TreeEdge {
from_id: parent_node_id,
to_id: node_id,
});
}
}
}
"subagent_completed" => {
let node_id = event.data.get("node_id")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let success = event.data.get("success")
.and_then(|v| v.as_bool())
.unwrap_or(false);
if let Some(node) = tree.nodes.iter_mut().find(|n| n.node_id == node_id) {
node.status = if success {
TreeNodeStatus::Success
} else {
TreeNodeStatus::Failed
};
}
}
_ => {}
}
}
```
2. Make `build_tree_from_events` a simple loop that reuses this helper:
```rust
fn build_tree_from_events(events: &[AgentEventDto]) -> ExecutionTree {
let mut tree = ExecutionTree::default();
for event in events {
update_execution_tree(&mut tree, event);
}
tree
}
```
3. In `app.rs`, call the same helper for real-time updates instead of re-implementing the `match`:
```rust
// wherever you currently handle live AgentEventDto events:
fn on_agent_event(state: &AppState, event: AgentEventDto) {
let mut tree = state.execution_tree.get();
update_execution_tree(&mut tree, &event);
state.execution_tree.set(tree);
// ...existing detail/state updates...
}
```
This keeps the new historical-tree feature intact while removing the duplication and ensuring any future changes to tree semantics are made once in `update_execution_tree`.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| .tree-node rect { | ||
| fill: var(--bg-card); | ||
| stroke: var(--border); | ||
| stroke-width: 1; | ||
| cursor: pointer; | ||
| transition: stroke-color 150ms ease; |
There was a problem hiding this comment.
issue (bug_risk): The transition property uses stroke-color, which isn't a valid SVG/CSS property.
stroke-color is not a recognized property, so this transition will be ignored. Use stroke instead, e.g. transition: stroke 150ms ease; (and add fill if you also want the fill color animated).
| // Emit SubagentCompleted when a Task tool result arrives | ||
| if is_task_tool { | ||
| self.emit_event(AgentEvent { | ||
| execution_id: self.id.clone(), | ||
| timestamp: Self::now_timestamp(), | ||
| event: Some(agent_event::Event::SubagentCompleted(SubagentCompleted { | ||
| subagent_id: pending.node_id.clone(), | ||
| success: true, | ||
| result_summary: truncate_str(&tool_output, 200), | ||
| node_id: format!("subagent-{}", pending.node_id), |
There was a problem hiding this comment.
issue (bug_risk): SubagentCompleted node_id format is inconsistent with SubagentSpawned, likely breaking tree linking.
In SubagentSpawned for Task tools, node_id is format!("subagent-{}", id) and subagent_id is id.to_string(). Here, SubagentCompleted uses subagent_id: pending.node_id.clone() and node_id: format!("subagent-{}", pending.node_id), which will yield subagent-subagent-… if pending.node_id already has the prefix. Since the frontend looks up the node by event.data["node_id"], this mismatch will prevent it from finding the original node and applying the completion state. Please align subagent_id/node_id with the spawn event (e.g. subagent_id: id.to_string(), node_id: format!("subagent-{}", id)).
| state.last_event_source.set(event.event_type.clone()); | ||
| } | ||
|
|
||
| // Build the execution tree incrementally |
There was a problem hiding this comment.
issue (complexity): Consider extracting the execution-tree event matching and mutation logic into a shared helper function that both App and build_tree_from_events call instead of duplicating it inline.
You’ve effectively duplicated the tree‑building rules inline in App, which does increase complexity and coupling. You can keep all functionality but centralize the logic in a reusable “tree updater” that both App and build_tree_from_events call.
1. Extract a reusable tree update function
In a shared module (e.g. state::execution_tree or pages::control::tree), define something like:
pub fn apply_event_to_tree(
tree: &mut ExecutionTree,
event: &AgentEventDto,
) {
match event.event_type.as_str() {
"iteration_started" => {
let node_id = event.data.get("node_id")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let iteration = event.data.get("iteration")
.and_then(|v| v.as_i64())
.unwrap_or(0);
if !node_id.is_empty() && !tree.nodes.iter().any(|n| n.node_id == node_id) {
tree.nodes.push(TreeNode {
node_id,
parent_node_id: None,
node_type: TreeNodeType::Iteration,
label: format!("Iteration {}", iteration),
summary: String::new(),
status: TreeNodeStatus::Running,
x: 0.0,
y: 0.0,
event_data: Some(event.data.clone()),
});
}
}
"tool_invoked" => {
let node_id = event.data.get("node_id")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let parent_node_id = event.data.get("parent_node_id")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let summary_val = event.data.get("summary")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
if summary_val == "(result)" {
// existing result logic
let base_id = node_id.strip_suffix("-result").unwrap_or(&node_id).to_string();
if let Some(node) = tree.nodes.iter_mut().find(|n| n.node_id == base_id) {
node.status = TreeNodeStatus::Success;
if let Some(output) = event.data.get("tool_output") {
if let Some(ref mut data) = node.event_data {
if let Some(obj) = data.as_object_mut() {
obj.insert("tool_output".to_string(), output.clone());
}
}
}
}
} else if !node_id.is_empty() && !tree.nodes.iter().any(|n| n.node_id == node_id) {
let tool_name = event.data.get("tool_name")
.and_then(|v| v.as_str())
.unwrap_or("Tool")
.to_string();
tree.nodes.push(TreeNode {
node_id: node_id.clone(),
parent_node_id: Some(parent_node_id.clone()),
node_type: TreeNodeType::ToolCall,
label: tool_name,
summary: summary_val,
status: TreeNodeStatus::Running,
x: 0.0,
y: 0.0,
event_data: Some(event.data.clone()),
});
if !parent_node_id.is_empty() {
tree.edges.push(TreeEdge {
from_id: parent_node_id,
to_id: node_id,
});
}
}
}
"iteration_completed" => {
let node_id = event.data.get("node_id")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
if !node_id.is_empty() {
if let Some(node) = tree.nodes.iter_mut().find(|n| n.node_id == node_id) {
node.status = TreeNodeStatus::Success;
}
}
}
"subagent_spawned" => {
// move your existing subagent_spawned logic here
}
"subagent_completed" => {
// move your existing subagent_completed logic here
}
_ => {}
}
}You can iteratively migrate the remaining branches (subagent_spawned, subagent_completed) into this function, preserving the exact behavior you added.
2. Use it in App’s event handler
Replace the large match block in App with a narrow call that only gates on expanded_eid:
// Build the execution tree incrementally
if let Some(expanded_eid) = state.expanded_execution.get_untracked() {
if expanded_eid == eid {
state.execution_tree.update(|tree| {
apply_event_to_tree(tree, &event);
});
}
}That keeps the event loop in App focused on “when to update the tree” rather than “how to update the tree,” which makes the UI code easier to follow.
3. Reuse in build_tree_from_events
In control.rs, build_tree_from_events can then delegate to the same helper instead of duplicating logic:
pub fn build_tree_from_events(events: &[AgentEventDto]) -> ExecutionTree {
let mut tree = ExecutionTree::default();
for event in events {
apply_event_to_tree(&mut tree, event);
}
tree
}This removes the duplicated rules for node creation, edges, and status updates, and ensures future changes to tree semantics only need to be done in one place.
| /// Build an ExecutionTree from a list of historical events. | ||
| /// This replays the same logic that app.rs uses for real-time events, | ||
| /// ensuring the tree is populated when viewing completed executions. | ||
| fn build_tree_from_events(events: &[AgentEventDto]) -> ExecutionTree { |
There was a problem hiding this comment.
issue (complexity): Consider extracting the shared event-to-execution-tree match logic into a reusable helper so both historical and real-time paths use the same code.
You can avoid maintaining two divergent implementations of the same event → tree logic by extracting the matching/branching into a shared helper and having both build_tree_from_events and the real-time handler in app.rs call it.
Concretely:
- Extract the event-handling logic into a single helper (e.g. in
stateor a shared module):
pub fn update_execution_tree(tree: &mut ExecutionTree, event: &AgentEventDto) {
match event.event_type.as_str() {
"iteration_started" => {
let node_id = event.data.get("node_id")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let iteration = event.data.get("iteration")
.and_then(|v| v.as_i64())
.unwrap_or(0);
if !node_id.is_empty() && !tree.nodes.iter().any(|n| n.node_id == node_id) {
tree.nodes.push(TreeNode {
node_id,
parent_node_id: None,
node_type: TreeNodeType::Iteration,
label: format!("Iteration {}", iteration),
summary: String::new(),
status: TreeNodeStatus::Running,
x: 0.0,
y: 0.0,
event_data: Some(event.data.clone()),
});
}
}
"tool_invoked" => {
let node_id = event.data.get("node_id")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let parent_node_id = event.data.get("parent_node_id")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let summary_val = event.data.get("summary")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
if summary_val == "(result)" {
let base_id = node_id.strip_suffix("-result").unwrap_or(&node_id).to_string();
if let Some(node) = tree.nodes.iter_mut().find(|n| n.node_id == base_id) {
node.status = TreeNodeStatus::Success;
if let Some(output) = event.data.get("tool_output") {
if let Some(ref mut data) = node.event_data {
if let Some(obj) = data.as_object_mut() {
obj.insert("tool_output".to_string(), output.clone());
}
}
}
}
} else if !node_id.is_empty() && !tree.nodes.iter().any(|n| n.node_id == node_id) {
let tool_name = event.data.get("tool_name")
.and_then(|v| v.as_str())
.unwrap_or("Tool")
.to_string();
tree.nodes.push(TreeNode {
node_id: node_id.clone(),
parent_node_id: Some(parent_node_id.clone()),
node_type: TreeNodeType::ToolCall,
label: tool_name,
summary: summary_val,
status: TreeNodeStatus::Running,
x: 0.0,
y: 0.0,
event_data: Some(event.data.clone()),
});
if !parent_node_id.is_empty() {
tree.edges.push(TreeEdge {
from_id: parent_node_id,
to_id: node_id,
});
}
}
}
"iteration_completed" => {
let node_id = event.data.get("node_id")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
if let Some(node) = tree.nodes.iter_mut().find(|n| n.node_id == node_id) {
node.status = TreeNodeStatus::Success;
}
}
"subagent_spawned" => {
let node_id = event.data.get("node_id")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let parent_node_id = event.data.get("parent_node_id")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let subagent_type = event.data.get("subagent_type")
.and_then(|v| v.as_str())
.unwrap_or("Subagent")
.to_string();
let task_summary = event.data.get("task_summary")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
if !node_id.is_empty() && !tree.nodes.iter().any(|n| n.node_id == node_id) {
tree.nodes.push(TreeNode {
node_id: node_id.clone(),
parent_node_id: Some(parent_node_id.clone()),
node_type: TreeNodeType::SubagentSpawn,
label: subagent_type,
summary: task_summary,
status: TreeNodeStatus::Running,
x: 0.0,
y: 0.0,
event_data: Some(event.data.clone()),
});
if !parent_node_id.is_empty() {
tree.edges.push(TreeEdge {
from_id: parent_node_id,
to_id: node_id,
});
}
}
}
"subagent_completed" => {
let node_id = event.data.get("node_id")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let success = event.data.get("success")
.and_then(|v| v.as_bool())
.unwrap_or(false);
if let Some(node) = tree.nodes.iter_mut().find(|n| n.node_id == node_id) {
node.status = if success {
TreeNodeStatus::Success
} else {
TreeNodeStatus::Failed
};
}
}
_ => {}
}
}- Make
build_tree_from_eventsa simple loop that reuses this helper:
fn build_tree_from_events(events: &[AgentEventDto]) -> ExecutionTree {
let mut tree = ExecutionTree::default();
for event in events {
update_execution_tree(&mut tree, event);
}
tree
}- In
app.rs, call the same helper for real-time updates instead of re-implementing thematch:
// wherever you currently handle live AgentEventDto events:
fn on_agent_event(state: &AppState, event: AgentEventDto) {
let mut tree = state.execution_tree.get();
update_execution_tree(&mut tree, &event);
state.execution_tree.set(tree);
// ...existing detail/state updates...
}This keeps the new historical-tree feature intact while removing the duplication and ensuring any future changes to tree semantics are made once in update_execution_tree.
PAL MCP Consensus Code Review (Manual Multi-Model Analysis)OverviewThis PR adds comprehensive observability features to the SuperClaude dashboard, including:
Files Changed: 16 files (Rust frontend/backend, protobuf, CSS) Critical IssuesNone identified - No blocking security vulnerabilities or critical bugs that prevent merge. High Priority Issues1. Potential Memory Leak in Event HistoryFile: crates/superclaude-daemon/src/execution.rs 2. Stdin Pipe Not InitializedFile: crates/superclaude-daemon/src/execution.rs:465-466 3. Race Condition in Tree BuildingFile: crates/dashboard/frontend/src/app.rs:76-237 Medium Priority Issues4. String Slicing Panic RiskFile: execution_tree.rs:217-221 5. Inefficient Tree TraversalFile: execution_tree.rs:191-209 6. Duplicate Tree Building LogicFiles: app.rs vs control.rs 7. Error Events May Overwhelm UIFile: execution.rs:492-555 Positive Observations✅ Excellent event architecture Review Summary
Overall: Strong PR. Fix stdin pipe initialization (issue 2) before merge. Recommended Actions
Manual multi-model consensus review by Claude Sonnet 4.5 |
Claude Code Review (via AWS Bedrock)OverviewThis PR adds comprehensive observability features to the Tauri dashboard and fixes critical event flow bugs. The changes span 16 files with ~1,344 insertions, introducing execution tree visualization, diff viewer UI, heartbeat monitoring, and stdin infrastructure for future interactive support. Three key bugs were fixed: stdin hanging, stderr noise pollution, and missing tree data for completed executions. Critical Issues1. Security: Unvalidated User Input in
|
| Category | Rating | Notes |
|---|---|---|
| Security | Critical: Unvalidated stdin input | |
| Code Quality | 3/5 | DRY violations, missing tests |
| Architecture | 4/5 | Solid design, minor coupling |
| Testing | 1/5 | Zero test coverage |
| Performance | 3/5 | O(n²) algorithm, unbounded memory |
| Documentation | 3/5 | Good commit, thin inline docs |
| Overall | 3/5 | Solid work, but fix security/testing |
Recommendations
Before Merging:
- Fix
child_stdininitialization bug (chore: cleanup temp files and update .gitignore #3) - Add input validation to
send_input()(Welcome to SuperClaude Discussions! #1) - Implement bounded tree memory (remove deepwiki mcp support #2)
Post-Merge:
4. Extract duplicated tree logic (#5)
5. Add comprehensive unit tests (#13)
6. Optimize tree layout algorithm (#4)
Generated by Claude Code Review (AWS Bedrock) - Sonnet 4.5
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@crates/dashboard/frontend/src/components/execution_tree.rs`:
- Around line 217-221: The current logic slices node.summary by bytes (using
&node.summary[..20]) which panics on multi-byte UTF‑8; replace this byte-slice
truncation with the existing safe helper (truncate_str) to cut by character
boundaries—e.g., when building summary in the execution tree component use
truncate_str(&node.summary, 20) (or equivalent) instead of the byte slice and
keep the fallback node.summary.clone() when not exceeding the limit; update the
summary assignment that references node.summary to call truncate_str so
multi-byte chars are handled safely.
In `@crates/dashboard/frontend/style/main.css`:
- Around line 942-948: In the .tree-node rect rule replace the invalid
transition target "stroke-color" with the proper SVG/CSS property "stroke" so
the border color animates; update the transition declaration inside the
.tree-node rect selector to use "stroke 150ms ease" so animations work as
intended.
In `@crates/superclaude-daemon/src/execution.rs`:
- Around line 162-163: The send_input RPC always fails because child_stdin
(tokio::sync::RwLock<Option<ChildStdin>>) is never populated — the process is
spawned with Stdio::null() and child.stdin.take() is never stored, so
ExecutionHandle::send_input always bails; fix by either populating child_stdin
(change spawn to use Stdio::piped(), after spawning call child.stdin.take() and
store it into self.child_stdin via self.child_stdin.write().await, and ensure
send_input writes to that handle) or, if the feature is intentionally
unimplemented, remove the send_input RPC/Tauri command or add a clear TODO
comment where Stdio::null() is used referencing child_stdin, send_input and
ExecutionHandle to avoid misleading “Input sent” success messages.
🧹 Nitpick comments (11)
README.md (1)
601-604: Line 602 exceeds ~100-character wrap guideline.This line is nearly 200 characters. Consider breaking it into multiple lines or shortening the parenthetical list for readability.
As per coding guidelines, "Markdown guidance (README, Docs/,
.codex-os/) uses ATX headings, wraps near 100 characters, and should link to decisions or specs when behavior changes".crates/dashboard/frontend/style/main.css (2)
970-973: Keyframe names should use kebab-case per Stylelint rules.
treePulseandthinkingPulseshould betree-pulseandthinking-pulserespectively. Note that the existingslideDownkeyframe (Line 687) has the same issue, but since only the new code is in scope, update the new keyframes and their references.Proposed fix
-.tree-node-running rect { - animation: treePulse 2s ease-in-out infinite; +.tree-node-running rect { + animation: tree-pulse 2s ease-in-out infinite; } -@keyframes treePulse { +@keyframes tree-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.6; } }.thinking-dot { - animation: thinkingPulse 1.4s ease-in-out infinite; + animation: thinking-pulse 1.4s ease-in-out infinite; } -@keyframes thinkingPulse { +@keyframes thinking-pulse { 0%, 80%, 100% { opacity: 0.3; transform: scale(0.8); } 40% { opacity: 1; transform: scale(1.0); } }Also applies to: 1106-1108
1067-1071: Useoverflowshorthand and split multi-declaration lines.Stylelint flags
overflow-y+overflow-xas redundant longhand (Line 1068-1069). Also, lines 1107-1108 have multiple declarations per line.Proposed fix for overflow
.diff-content { padding: 8px 12px; font-family: var(--font-mono); font-size: 11px; color: var(--text-secondary); background: var(--bg-primary); max-height: 300px; - overflow-y: auto; - overflow-x: auto; + overflow: auto; white-space: pre-wrap; word-break: break-all; margin: 0; }crates/dashboard/frontend/src/pages/control.rs (2)
89-101: Fragile sentinel-based result detection.The
"(result)"string match and"-result"suffix stripping to associate a tool completion with its originating node is a brittle convention. If the daemon's event format changes (e.g., uses a different summary or ID scheme), this silently breaks without error. Consider using a dedicated event type (e.g.,tool_completed) or an explicit boolean field inToolInvokedto distinguish invocations from results.
45-193: Extract duplicated event-to-tree logic into a shared helper function.The
build_tree_from_eventsfunction duplicates the exact event-handling logic thatapp.rsimplements for real-time updates (lines 109–256 in app.rs). Both files extract the same fields, perform identical deduplication checks, handle the "(result)" sentinel in the same way, and construct TreeNode/TreeEdge structures with the same field assignments.Extract a shared function (e.g.,
apply_event_to_tree(&mut ExecutionTree, &AgentEventDto)) that both files can call. This eliminates the DRY violation and ensures that new event types or logic changes stay synchronized.crates/proto/proto/superclaude.proto (2)
406-422: Nit: "Interactive Input" block splits the "Health Check" header from its messages.The
// Health Checksection comment (Line 406-408) is now separated fromPingRequest/PingResponse(Line 424-430) by the newSendInputmessages. Consider moving theSendInputblock above the Health Check section to keep the Health Check header adjacent to its messages, or move the Health Check header down.Suggested reordering
// ============================================================================ -// Health Check -// ============================================================================ - -// ============================================================================ // Interactive Input // ============================================================================ message SendInputRequest { ... } message SendInputResponse { ... } +// ============================================================================ +// Health Check +// ============================================================================ + message PingRequest {} message PingResponse { ... }
414-422: Consider adding an input size constraint or documentation note.
SendInputRequest.inputis an unbounded string that gets written directly to a child process's stdin (per the server implementation inexecution.rs). While this is a local-only daemon, a very large input could cause issues. A comment documenting the expected usage (e.g., single-line terminal input) or an optionalmax_lengthvalidation on the server side would be a useful guard.crates/dashboard/frontend/src/components/diff_view.rs (1)
13-13: Unnecessary clone —file_pathis already an ownedStringand unused afterward.
path_displaycan just befile_pathdirectly since it's only consumed inside the match.Proposed fix
- let path_display = file_path.clone(); - - match action.as_str() { + match action.as_str() {Then replace
path_displaywithfile_pathin lines 19 and 48.crates/dashboard/frontend/src/components/execution_tree.rs (1)
123-128:laid_out_tree()is re-computed multiple times per reactive cycle.Each call to
laid_out_tree()clones the entire tree from the signal and re-runslayout_tree. Within a single render pass, it's invoked from the main closure (line 171),viewbox()(line 132→171 again vialaid_out_tree()), and the detail overlay (line 252). Consider computing it once with aMemoor a derived signal.Sketch
- // Compute the laid-out tree - let laid_out_tree = move || { - let mut tree = state.execution_tree.get(); - layout_tree(&mut tree); - tree - }; + // Compute the laid-out tree once per change + let laid_out_tree = Memo::new(move |_| { + let mut tree = state.execution_tree.get(); + layout_tree(&mut tree); + tree + });Then use
laid_out_tree.get()at call sites instead oflaid_out_tree().Also applies to: 131-138, 170-176, 249-252
crates/superclaude-daemon/src/execution.rs (1)
334-334: Raising stdout log level fromdebugtoinfomay be noisy in production.Every line from Claude's stdout (which can be voluminous with stream-json output) will now be logged at
infolevel. This could generate significant log volume. Consider keeping this atdebugor gating behind a verbose flag.Proposed change
- info!(execution_id = %inner.id, len = line.len(), "claude stdout line"); + debug!(execution_id = %inner.id, len = line.len(), "claude stdout line");crates/dashboard/frontend/src/state/app_state.rs (1)
158-169: Consider excluding layout fields (x,y) from serialization.
TreeNodederivesSerialize/Deserialize, butxandyare layout-computed values that are overwritten bylayout_treebefore every render. Serializing them is harmless but adds noise if these structs are ever logged or persisted.
| let summary = if node.summary.len() > 20 { | ||
| format!("{}...", &node.summary[..20]) | ||
| } else { | ||
| node.summary.clone() | ||
| }; |
There was a problem hiding this comment.
Panic on multi-byte UTF-8 summaries: &node.summary[..20] slices by byte index.
If node.summary contains any multi-byte UTF-8 character within the first 20 bytes, this will panic at runtime with "byte index is not a char boundary". The daemon already has a safe truncate_str helper that uses char_indices.
🐛 Proposed fix
- let summary = if node.summary.len() > 20 {
- format!("{}...", &node.summary[..20])
+ let summary = if node.summary.chars().count() > 20 {
+ let end = node.summary.char_indices().nth(20).map(|(i, _)| i).unwrap_or(node.summary.len());
+ format!("{}…", &node.summary[..end])
} else {
node.summary.clone()
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| let summary = if node.summary.len() > 20 { | |
| format!("{}...", &node.summary[..20]) | |
| } else { | |
| node.summary.clone() | |
| }; | |
| let summary = if node.summary.chars().count() > 20 { | |
| let end = node.summary.char_indices().nth(20).map(|(i, _)| i).unwrap_or(node.summary.len()); | |
| format!("{}…", &node.summary[..end]) | |
| } else { | |
| node.summary.clone() | |
| }; |
🤖 Prompt for AI Agents
In `@crates/dashboard/frontend/src/components/execution_tree.rs` around lines 217
- 221, The current logic slices node.summary by bytes (using
&node.summary[..20]) which panics on multi-byte UTF‑8; replace this byte-slice
truncation with the existing safe helper (truncate_str) to cut by character
boundaries—e.g., when building summary in the execution tree component use
truncate_str(&node.summary, 20) (or equivalent) instead of the byte slice and
keep the fallback node.summary.clone() when not exceeding the limit; update the
summary assignment that references node.summary to call truncate_str so
multi-byte chars are handled safely.
| .tree-node rect { | ||
| fill: var(--bg-card); | ||
| stroke: var(--border); | ||
| stroke-width: 1; | ||
| cursor: pointer; | ||
| transition: stroke-color 150ms ease; | ||
| } |
There was a problem hiding this comment.
stroke-color is not a valid CSS transition property — use stroke.
Line 947: transition: stroke-color 150ms ease; won't animate because stroke-color is not a recognized CSS property. The correct property name for SVG stroke transitions is stroke.
Proposed fix
.tree-node rect {
fill: var(--bg-card);
stroke: var(--border);
stroke-width: 1;
cursor: pointer;
- transition: stroke-color 150ms ease;
+ transition: stroke 150ms ease;
}🤖 Prompt for AI Agents
In `@crates/dashboard/frontend/style/main.css` around lines 942 - 948, In the
.tree-node rect rule replace the invalid transition target "stroke-color" with
the proper SVG/CSS property "stroke" so the border color animates; update the
transition declaration inside the .tree-node rect selector to use "stroke 150ms
ease" so animations work as intended.
| /// Piped stdin handle for interactive input via SendInput RPC. | ||
| child_stdin: tokio::sync::RwLock<Option<tokio::process::ChildStdin>>, |
There was a problem hiding this comment.
send_input will always fail — child_stdin is never populated.
The child_stdin field is initialized as None (line 206) and stdin is set to Stdio::null() (line 269), so child.stdin.take() would return None even if it were called — but it's never called anywhere. The send_input method (line 1253) will always bail with "stdin pipe not available".
If this is intentionally deferred (the PR summary calls it "Stdin foundation"), consider either:
- Removing the
send_inputRPC endpoint and Tauri command until it's functional, to avoid confusing "Input sent" success messages that never actually reach the process. - Or adding a TODO comment at line 269 documenting why
Stdio::null()is used and what's needed to complete the feature.
Currently the full pipeline (frontend → Tauri → gRPC → ExecutionHandle::send_input) is wired up but will always return an error at the last mile.
Also applies to: 206-206, 269-269
🤖 Prompt for AI Agents
In `@crates/superclaude-daemon/src/execution.rs` around lines 162 - 163, The
send_input RPC always fails because child_stdin
(tokio::sync::RwLock<Option<ChildStdin>>) is never populated — the process is
spawned with Stdio::null() and child.stdin.take() is never stored, so
ExecutionHandle::send_input always bails; fix by either populating child_stdin
(change spawn to use Stdio::piped(), after spawning call child.stdin.take() and
store it into self.child_stdin via self.child_stdin.write().await, and ensure
send_input writes to that handle) or, if the feature is intentionally
unimplemented, remove the send_input RPC/Tauri command or add a clear TODO
comment where Stdio::null() is used referencing child_stdin, send_input and
ExecutionHandle to avoid misleading “Input sent” success messages.
Summary
GetExecutionDetailRPC with events,SendInputRPC, heartbeat and stdin proto messagesLogMessageevents for frontend liveness detectionchild_stdinfield andsend_input()onExecutionHandlefor future interactive terminalStdio::piped()→Stdio::null()— piped stdin with no writer caused the Claude CLI to hang, producing no outputErrorOccurredfor lines containing error/panic/fatal keywords, preventing normal stderr from polluting the event logselect_agent.pyline count, add new dashboard features, fix staleDocs/directory structureTest plan
cargo check --workspacepassescargo checkincrates/dashboard/frontend/passes (independent workspace)cargo test --manifest-path crates/superclaude-daemon/Cargo.toml— 17 tests passcargo tauri dev, create execution, verify events stream to dashboard🤖 Generated with Claude Code
Summary by Sourcery
Add richer observability to the dashboard and daemon, including execution tree visualization, file diff viewing, and interactive input support, while tightening stderr error reporting and heartbeat-based liveness signals.
New Features:
Bug Fixes:
Enhancements:
Documentation:
Summary by CodeRabbit
Release Notes
New Features
Improvements