Restore tool cards on thread deserialization (#30053)
Release Notes: - N/A --------- Co-authored-by: Julia Ryan <juliaryan3.14@gmail.com>
This commit is contained in:
parent
ab3e5cdc6c
commit
0cdd8bdded
30 changed files with 307 additions and 135 deletions
|
@ -107,10 +107,9 @@ impl Tool for CopyPathTool {
|
|||
|
||||
cx.background_spawn(async move {
|
||||
match copy_task.await {
|
||||
Ok(_) => Ok(format!(
|
||||
"Copied {} to {}",
|
||||
input.source_path, input.destination_path
|
||||
)),
|
||||
Ok(_) => Ok(
|
||||
format!("Copied {} to {}", input.source_path, input.destination_path).into(),
|
||||
),
|
||||
Err(err) => Err(anyhow!(
|
||||
"Failed to copy {} to {}: {}",
|
||||
input.source_path,
|
||||
|
|
|
@ -88,7 +88,7 @@ impl Tool for CreateDirectoryTool {
|
|||
.await
|
||||
.map_err(|err| anyhow!("Unable to create directory {destination_path}: {err}"))?;
|
||||
|
||||
Ok(format!("Created directory {destination_path}"))
|
||||
Ok(format!("Created directory {destination_path}").into())
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
|
|
@ -131,7 +131,7 @@ impl Tool for CreateFileTool {
|
|||
.await
|
||||
.map_err(|err| anyhow!("Unable to save buffer for {destination_path}: {err}"))?;
|
||||
|
||||
Ok(format!("Created file {destination_path}"))
|
||||
Ok(format!("Created file {destination_path}").into())
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
|
|
@ -127,7 +127,7 @@ impl Tool for DeletePathTool {
|
|||
|
||||
match delete {
|
||||
Some(deletion_task) => match deletion_task.await {
|
||||
Ok(()) => Ok(format!("Deleted {path_str}")),
|
||||
Ok(()) => Ok(format!("Deleted {path_str}").into()),
|
||||
Err(err) => Err(anyhow!("Failed to delete {path_str}: {err}")),
|
||||
},
|
||||
None => Err(anyhow!(
|
||||
|
|
|
@ -122,9 +122,9 @@ impl Tool for DiagnosticsTool {
|
|||
}
|
||||
|
||||
if output.is_empty() {
|
||||
Ok("File doesn't have errors or warnings!".to_string())
|
||||
Ok("File doesn't have errors or warnings!".to_string().into())
|
||||
} else {
|
||||
Ok(output)
|
||||
Ok(output.into())
|
||||
}
|
||||
})
|
||||
.into()
|
||||
|
@ -158,10 +158,12 @@ impl Tool for DiagnosticsTool {
|
|||
});
|
||||
|
||||
if has_diagnostics {
|
||||
Task::ready(Ok(output)).into()
|
||||
Task::ready(Ok(output.into())).into()
|
||||
} else {
|
||||
Task::ready(Ok("No errors or warnings found in the project.".to_string()))
|
||||
.into()
|
||||
Task::ready(Ok("No errors or warnings found in the project."
|
||||
.to_string()
|
||||
.into()))
|
||||
.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -895,6 +895,7 @@ fn tool_result(
|
|||
tool_name: name.into(),
|
||||
is_error: false,
|
||||
content: result.into(),
|
||||
output: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
use crate::{
|
||||
replace::{replace_exact, replace_with_flexible_indent},
|
||||
schema::json_schema_for,
|
||||
streaming_edit_file_tool::StreamingEditFileToolOutput,
|
||||
};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolCard, ToolResult, ToolUseStatus};
|
||||
use assistant_tool::{
|
||||
ActionLog, AnyToolCard, Tool, ToolCard, ToolResult, ToolResultOutput, ToolUseStatus,
|
||||
};
|
||||
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
|
||||
use editor::{Editor, EditorElement, EditorMode, EditorStyle, MultiBuffer, PathKey};
|
||||
use gpui::{
|
||||
|
@ -153,7 +156,7 @@ impl Tool for EditFileTool {
|
|||
});
|
||||
|
||||
let card_clone = card.clone();
|
||||
let task = cx.spawn(async move |cx: &mut AsyncApp| {
|
||||
let task: Task<Result<ToolResultOutput, _>> = cx.spawn(async move |cx: &mut AsyncApp| {
|
||||
let project_path = project.read_with(cx, |project, cx| {
|
||||
project
|
||||
.find_project_path(&input.path, cx)
|
||||
|
@ -281,16 +284,29 @@ impl Tool for EditFileTool {
|
|||
|
||||
if let Some(card) = card_clone {
|
||||
card.update(cx, |card, cx| {
|
||||
card.set_diff(project_path.path.clone(), old_text, new_text, cx);
|
||||
card.set_diff(
|
||||
project_path.path.clone(),
|
||||
old_text.clone(),
|
||||
new_text.clone(),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
|
||||
Ok(format!(
|
||||
"Edited {}:\n\n```diff\n{}\n```",
|
||||
input.path.display(),
|
||||
diff_str
|
||||
))
|
||||
Ok(ToolResultOutput {
|
||||
content: format!(
|
||||
"Edited {}:\n\n```diff\n{}\n```",
|
||||
input.path.display(),
|
||||
diff_str
|
||||
),
|
||||
output: serde_json::to_value(StreamingEditFileToolOutput {
|
||||
original_path: input.path,
|
||||
new_text,
|
||||
old_text,
|
||||
})
|
||||
.ok(),
|
||||
})
|
||||
});
|
||||
|
||||
ToolResult {
|
||||
|
@ -298,6 +314,32 @@ impl Tool for EditFileTool {
|
|||
card: card.map(AnyToolCard::from),
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_card(
|
||||
self: Arc<Self>,
|
||||
output: serde_json::Value,
|
||||
project: Entity<Project>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<AnyToolCard> {
|
||||
let output = match serde_json::from_value::<StreamingEditFileToolOutput>(output) {
|
||||
Ok(output) => output,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
let card = cx.new(|cx| {
|
||||
let mut card = EditFileToolCard::new(output.original_path.clone(), project, window, cx);
|
||||
card.set_diff(
|
||||
output.original_path.into(),
|
||||
output.old_text,
|
||||
output.new_text,
|
||||
cx,
|
||||
);
|
||||
card
|
||||
});
|
||||
|
||||
Some(card.into())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EditFileToolCard {
|
||||
|
|
|
@ -166,7 +166,7 @@ impl Tool for FetchTool {
|
|||
bail!("no textual content found");
|
||||
}
|
||||
|
||||
Ok(text)
|
||||
Ok(text.into())
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
|
|
@ -98,7 +98,7 @@ impl Tool for FindPathTool {
|
|||
sender.send(paginated_matches.to_vec()).log_err();
|
||||
|
||||
if matches.is_empty() {
|
||||
Ok("No matches found".to_string())
|
||||
Ok("No matches found".to_string().into())
|
||||
} else {
|
||||
let mut message = format!("Found {} total matches.", matches.len());
|
||||
if matches.len() > RESULTS_PER_PAGE {
|
||||
|
@ -113,7 +113,7 @@ impl Tool for FindPathTool {
|
|||
for mat in matches.into_iter().skip(offset).take(RESULTS_PER_PAGE) {
|
||||
write!(&mut message, "\n{}", mat.display()).unwrap();
|
||||
}
|
||||
Ok(message)
|
||||
Ok(message.into())
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -260,16 +260,16 @@ impl Tool for GrepTool {
|
|||
}
|
||||
|
||||
if matches_found == 0 {
|
||||
Ok("No matches found".to_string())
|
||||
Ok("No matches found".to_string().into())
|
||||
} else if has_more_matches {
|
||||
Ok(format!(
|
||||
"Showing matches {}-{} (there were more matches found; use offset: {} to see next page):\n{output}",
|
||||
input.offset + 1,
|
||||
input.offset + matches_found,
|
||||
input.offset + RESULTS_PER_PAGE,
|
||||
))
|
||||
).into())
|
||||
} else {
|
||||
Ok(format!("Found {matches_found} matches:\n{output}"))
|
||||
Ok(format!("Found {matches_found} matches:\n{output}").into())
|
||||
}
|
||||
}).into()
|
||||
}
|
||||
|
@ -748,9 +748,9 @@ mod tests {
|
|||
match task.output.await {
|
||||
Ok(result) => {
|
||||
if cfg!(windows) {
|
||||
result.replace("root\\", "root/")
|
||||
result.content.replace("root\\", "root/")
|
||||
} else {
|
||||
result
|
||||
result.content
|
||||
}
|
||||
}
|
||||
Err(e) => panic!("Failed to run grep tool: {}", e),
|
||||
|
|
|
@ -102,7 +102,7 @@ impl Tool for ListDirectoryTool {
|
|||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
return Task::ready(Ok(output)).into();
|
||||
return Task::ready(Ok(output.into())).into();
|
||||
}
|
||||
|
||||
let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
|
||||
|
@ -134,8 +134,8 @@ impl Tool for ListDirectoryTool {
|
|||
.unwrap();
|
||||
}
|
||||
if output.is_empty() {
|
||||
return Task::ready(Ok(format!("{} is empty.", input.path))).into();
|
||||
return Task::ready(Ok(format!("{} is empty.", input.path).into())).into();
|
||||
}
|
||||
Task::ready(Ok(output)).into()
|
||||
Task::ready(Ok(output.into())).into()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -117,10 +117,9 @@ impl Tool for MovePathTool {
|
|||
|
||||
cx.background_spawn(async move {
|
||||
match rename_task.await {
|
||||
Ok(_) => Ok(format!(
|
||||
"Moved {} to {}",
|
||||
input.source_path, input.destination_path
|
||||
)),
|
||||
Ok(_) => {
|
||||
Ok(format!("Moved {} to {}", input.source_path, input.destination_path).into())
|
||||
}
|
||||
Err(err) => Err(anyhow!(
|
||||
"Failed to move {} to {}: {}",
|
||||
input.source_path,
|
||||
|
|
|
@ -73,6 +73,6 @@ impl Tool for NowTool {
|
|||
};
|
||||
let text = format!("The current datetime is {now}.");
|
||||
|
||||
Task::ready(Ok(text)).into()
|
||||
Task::ready(Ok(text.into())).into()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -70,7 +70,7 @@ impl Tool for OpenTool {
|
|||
}
|
||||
.context("Failed to open URL or file path")?;
|
||||
|
||||
Ok(format!("Successfully opened {}", input.path_or_url))
|
||||
Ok(format!("Successfully opened {}", input.path_or_url).into())
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
|
|
@ -145,9 +145,9 @@ impl Tool for ReadFileTool {
|
|||
let lines = text.split('\n').skip(start_row as usize);
|
||||
if let Some(end) = input.end_line {
|
||||
let count = end.saturating_sub(start).saturating_add(1); // Ensure at least 1 line
|
||||
Itertools::intersperse(lines.take(count as usize), "\n").collect()
|
||||
Itertools::intersperse(lines.take(count as usize), "\n").collect::<String>().into()
|
||||
} else {
|
||||
Itertools::intersperse(lines, "\n").collect()
|
||||
Itertools::intersperse(lines, "\n").collect::<String>().into()
|
||||
}
|
||||
})?;
|
||||
|
||||
|
@ -180,7 +180,7 @@ impl Tool for ReadFileTool {
|
|||
log.buffer_read(buffer, cx);
|
||||
})?;
|
||||
|
||||
Ok(result)
|
||||
Ok(result.into())
|
||||
} else {
|
||||
// File is too big, so return the outline
|
||||
// and a suggestion to read again with line numbers.
|
||||
|
@ -192,7 +192,7 @@ impl Tool for ReadFileTool {
|
|||
|
||||
Using the line numbers in this outline, you can call this tool again while specifying
|
||||
the start_line and end_line fields to see the implementations of symbols in the outline."
|
||||
})
|
||||
}.into())
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -258,7 +258,7 @@ mod test {
|
|||
.output
|
||||
})
|
||||
.await;
|
||||
assert_eq!(result.unwrap(), "This is a small file content");
|
||||
assert_eq!(result.unwrap().content, "This is a small file content");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
@ -358,7 +358,7 @@ mod test {
|
|||
.output
|
||||
})
|
||||
.await;
|
||||
assert_eq!(result.unwrap(), "Line 2\nLine 3\nLine 4");
|
||||
assert_eq!(result.unwrap().content, "Line 2\nLine 3\nLine 4");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
@ -389,7 +389,7 @@ mod test {
|
|||
.output
|
||||
})
|
||||
.await;
|
||||
assert_eq!(result.unwrap(), "Line 1\nLine 2");
|
||||
assert_eq!(result.unwrap().content, "Line 1\nLine 2");
|
||||
|
||||
// end_line of 0 should result in at least 1 line
|
||||
let result = cx
|
||||
|
@ -404,7 +404,7 @@ mod test {
|
|||
.output
|
||||
})
|
||||
.await;
|
||||
assert_eq!(result.unwrap(), "Line 1");
|
||||
assert_eq!(result.unwrap().content, "Line 1");
|
||||
|
||||
// when start_line > end_line, should still return at least 1 line
|
||||
let result = cx
|
||||
|
@ -419,7 +419,7 @@ mod test {
|
|||
.output
|
||||
})
|
||||
.await;
|
||||
assert_eq!(result.unwrap(), "Line 3");
|
||||
assert_eq!(result.unwrap().content, "Line 3");
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
|
|
|
@ -5,7 +5,7 @@ use crate::{
|
|||
schema::json_schema_for,
|
||||
};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolResult};
|
||||
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolResult, ToolResultOutput};
|
||||
use futures::StreamExt;
|
||||
use gpui::{AnyWindowHandle, App, AppContext, AsyncApp, Entity, Task};
|
||||
use indoc::formatdoc;
|
||||
|
@ -67,6 +67,13 @@ pub struct StreamingEditFileToolInput {
|
|||
pub create_or_overwrite: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct StreamingEditFileToolOutput {
|
||||
pub original_path: PathBuf,
|
||||
pub new_text: String,
|
||||
pub old_text: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
struct PartialInput {
|
||||
#[serde(default)]
|
||||
|
@ -248,6 +255,12 @@ impl Tool for StreamingEditFileTool {
|
|||
});
|
||||
let (new_text, diff) = futures::join!(new_text, diff);
|
||||
|
||||
let output = StreamingEditFileToolOutput {
|
||||
original_path: project_path.path.to_path_buf(),
|
||||
new_text: new_text.clone(),
|
||||
old_text: old_text.clone(),
|
||||
};
|
||||
|
||||
if let Some(card) = card_clone {
|
||||
card.update(cx, |card, cx| {
|
||||
card.set_diff(project_path.path.clone(), old_text, new_text, cx);
|
||||
|
@ -264,10 +277,13 @@ impl Tool for StreamingEditFileTool {
|
|||
I can perform the requested edits.
|
||||
"}))
|
||||
} else {
|
||||
Ok("No edits were made.".to_string())
|
||||
Ok("No edits were made.".to_string().into())
|
||||
}
|
||||
} else {
|
||||
Ok(format!("Edited {}:\n\n```diff\n{}\n```", input_path, diff))
|
||||
Ok(ToolResultOutput {
|
||||
content: format!("Edited {}:\n\n```diff\n{}\n```", input_path, diff),
|
||||
output: serde_json::to_value(output).ok(),
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -276,6 +292,32 @@ impl Tool for StreamingEditFileTool {
|
|||
card: card.map(AnyToolCard::from),
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_card(
|
||||
self: Arc<Self>,
|
||||
output: serde_json::Value,
|
||||
project: Entity<Project>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<AnyToolCard> {
|
||||
let output = match serde_json::from_value::<StreamingEditFileToolOutput>(output) {
|
||||
Ok(output) => output,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
let card = cx.new(|cx| {
|
||||
let mut card = EditFileToolCard::new(output.original_path.clone(), project, window, cx);
|
||||
card.set_diff(
|
||||
output.original_path.into(),
|
||||
output.old_text,
|
||||
output.new_text,
|
||||
cx,
|
||||
);
|
||||
card
|
||||
});
|
||||
|
||||
Some(card.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -178,7 +178,7 @@ impl Tool for TerminalTool {
|
|||
let exit_status = child.wait()?;
|
||||
let (processed_content, _) =
|
||||
process_content(content, &input.command, Some(exit_status));
|
||||
Ok(processed_content)
|
||||
Ok(processed_content.into())
|
||||
});
|
||||
return ToolResult {
|
||||
output: task,
|
||||
|
@ -266,7 +266,7 @@ impl Tool for TerminalTool {
|
|||
card.elapsed_time = Some(card.start_instant.elapsed());
|
||||
});
|
||||
|
||||
Ok(processed_content)
|
||||
Ok(processed_content.into())
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -661,7 +661,7 @@ mod tests {
|
|||
)
|
||||
});
|
||||
|
||||
let output = result.output.await.log_err();
|
||||
let output = result.output.await.log_err().map(|output| output.content);
|
||||
assert_eq!(output, Some("Command executed successfully.".into()));
|
||||
}
|
||||
|
||||
|
@ -693,7 +693,11 @@ mod tests {
|
|||
cx,
|
||||
);
|
||||
cx.spawn(async move |_| {
|
||||
let output = headless_result.output.await.log_err();
|
||||
let output = headless_result
|
||||
.output
|
||||
.await
|
||||
.log_err()
|
||||
.map(|output| output.content);
|
||||
assert_eq!(output, expected);
|
||||
})
|
||||
};
|
||||
|
|
|
@ -55,7 +55,7 @@ impl Tool for ThinkingTool {
|
|||
) -> ToolResult {
|
||||
// This tool just "thinks out loud" and doesn't perform any actions.
|
||||
Task::ready(match serde_json::from_value::<ThinkingToolInput>(input) {
|
||||
Ok(_input) => Ok("Finished thinking.".to_string()),
|
||||
Ok(_input) => Ok("Finished thinking.".to_string().into()),
|
||||
Err(err) => Err(anyhow!(err)),
|
||||
})
|
||||
.into()
|
||||
|
|
|
@ -72,7 +72,9 @@ impl Tool for WebSearchTool {
|
|||
let search_task = search_task.clone();
|
||||
async move {
|
||||
let response = search_task.await.map_err(|err| anyhow!(err))?;
|
||||
serde_json::to_string(&response).context("Failed to serialize search results")
|
||||
serde_json::to_string(&response)
|
||||
.context("Failed to serialize search results")
|
||||
.map(Into::into)
|
||||
}
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue