Allow displaying diffs in the ui. (#8)
Reviewed-on: #8 Co-authored-by: Drew Galbraith <drew@tiramisu.one> Co-committed-by: Drew Galbraith <drew@tiramisu.one>
This commit is contained in:
parent
af080710cc
commit
5a49cba1e6
9 changed files with 481 additions and 114 deletions
|
|
@ -4,8 +4,8 @@ use tracing::{debug, warn};
|
|||
|
||||
use crate::core::history::ConversationHistory;
|
||||
use crate::core::types::{
|
||||
ContentBlock, ConversationMessage, Role, StampedEvent, StreamEvent, ToolDefinition, UIEvent,
|
||||
UserAction,
|
||||
ContentBlock, ConversationMessage, Role, StampedEvent, StreamEvent, ToolDefinition,
|
||||
ToolDisplay, UIEvent, UserAction,
|
||||
};
|
||||
use crate::provider::ModelProvider;
|
||||
use crate::sandbox::Sandbox;
|
||||
|
|
@ -303,6 +303,86 @@ impl<P: ModelProvider> Orchestrator<P> {
|
|||
.await;
|
||||
}
|
||||
|
||||
/// Build a [`ToolDisplay`] from the tool name and its JSON input.
|
||||
///
|
||||
/// Matches on known tool names to extract structured fields; falls back to
|
||||
/// `Generic` with a JSON summary for anything else.
|
||||
fn build_tool_display(&self, tool_name: &str, input: &serde_json::Value) -> ToolDisplay {
|
||||
match tool_name {
|
||||
"write_file" => {
|
||||
let path = input["path"].as_str().unwrap_or("<unknown>").to_string();
|
||||
let new_content = input["content"].as_str().unwrap_or("").to_string();
|
||||
// Try to read existing content for diffing.
|
||||
let old_content = self.sandbox.read_file(&path).ok();
|
||||
ToolDisplay::WriteFile {
|
||||
path,
|
||||
old_content,
|
||||
new_content,
|
||||
}
|
||||
}
|
||||
"shell_exec" => {
|
||||
let command = input["command"].as_str().unwrap_or("").to_string();
|
||||
ToolDisplay::ShellExec { command }
|
||||
}
|
||||
"list_directory" => {
|
||||
let path = input["path"].as_str().unwrap_or(".").to_string();
|
||||
ToolDisplay::ListDirectory { path }
|
||||
}
|
||||
"read_file" => {
|
||||
let path = input["path"].as_str().unwrap_or("<unknown>").to_string();
|
||||
ToolDisplay::ReadFile { path }
|
||||
}
|
||||
_ => ToolDisplay::Generic {
|
||||
summary: serde_json::to_string(input).unwrap_or_default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a [`ToolDisplay`] for a tool result, incorporating the output content.
|
||||
fn build_result_display(
|
||||
&self,
|
||||
tool_name: &str,
|
||||
input: &serde_json::Value,
|
||||
output: &str,
|
||||
) -> ToolDisplay {
|
||||
match tool_name {
|
||||
"write_file" => {
|
||||
let path = input["path"].as_str().unwrap_or("<unknown>").to_string();
|
||||
let new_content = input["content"].as_str().unwrap_or("").to_string();
|
||||
// For results, old_content isn't available post-write. We already
|
||||
// showed the diff at approval/executing time; the result just
|
||||
// confirms the byte count via the display formatter.
|
||||
ToolDisplay::WriteFile {
|
||||
path,
|
||||
old_content: None,
|
||||
new_content,
|
||||
}
|
||||
}
|
||||
"shell_exec" => {
|
||||
let command = input["command"].as_str().unwrap_or("").to_string();
|
||||
ToolDisplay::ShellExec {
|
||||
command: format!("{command}\n{output}"),
|
||||
}
|
||||
}
|
||||
"list_directory" => {
|
||||
let path = input["path"].as_str().unwrap_or(".").to_string();
|
||||
ToolDisplay::ListDirectory {
|
||||
path: format!("{path}\n{output}"),
|
||||
}
|
||||
}
|
||||
"read_file" => {
|
||||
let path = input["path"].as_str().unwrap_or("<unknown>").to_string();
|
||||
let line_count = output.lines().count();
|
||||
ToolDisplay::ReadFile {
|
||||
path: format!("{path} ({line_count} lines)"),
|
||||
}
|
||||
}
|
||||
_ => ToolDisplay::Generic {
|
||||
summary: truncate(output, 200),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute a single tool, handling approval if needed.
|
||||
///
|
||||
/// For auto-approve tools, executes immediately. For tools requiring
|
||||
|
|
@ -325,14 +405,15 @@ impl<P: ModelProvider> Orchestrator<P> {
|
|||
}
|
||||
};
|
||||
|
||||
let input_summary = serde_json::to_string(input).unwrap_or_default();
|
||||
let display = self.build_tool_display(tool_name, input);
|
||||
|
||||
// Check approval.
|
||||
let approved = match risk {
|
||||
RiskLevel::AutoApprove => {
|
||||
self.send(UIEvent::ToolExecuting {
|
||||
tool_use_id: tool_use_id.to_string(),
|
||||
tool_name: tool_name.to_string(),
|
||||
input_summary: input_summary.clone(),
|
||||
display,
|
||||
})
|
||||
.await;
|
||||
true
|
||||
|
|
@ -341,7 +422,7 @@ impl<P: ModelProvider> Orchestrator<P> {
|
|||
self.send(UIEvent::ToolApprovalRequest {
|
||||
tool_use_id: tool_use_id.to_string(),
|
||||
tool_name: tool_name.to_string(),
|
||||
input_summary: input_summary.clone(),
|
||||
display,
|
||||
})
|
||||
.await;
|
||||
|
||||
|
|
@ -361,9 +442,11 @@ impl<P: ModelProvider> Orchestrator<P> {
|
|||
let tool = self.tool_registry.get(tool_name).unwrap();
|
||||
match tool.execute(input, &self.sandbox).await {
|
||||
Ok(output) => {
|
||||
let result_display = self.build_result_display(tool_name, input, &output.content);
|
||||
self.send(UIEvent::ToolResult {
|
||||
tool_use_id: tool_use_id.to_string(),
|
||||
tool_name: tool_name.to_string(),
|
||||
output_summary: truncate(&output.content, 200),
|
||||
display: result_display,
|
||||
is_error: output.is_error,
|
||||
})
|
||||
.await;
|
||||
|
|
@ -372,8 +455,11 @@ impl<P: ModelProvider> Orchestrator<P> {
|
|||
Err(e) => {
|
||||
let msg = e.to_string();
|
||||
self.send(UIEvent::ToolResult {
|
||||
tool_use_id: tool_use_id.to_string(),
|
||||
tool_name: tool_name.to_string(),
|
||||
output_summary: msg.clone(),
|
||||
display: ToolDisplay::Generic {
|
||||
summary: msg.clone(),
|
||||
},
|
||||
is_error: true,
|
||||
})
|
||||
.await;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue