agent: Render diffs for the edit file tool (#29234)

This PR implements the `ToolCard` for the edit file tool, which allow us
to display an editor with a diff in the thread view with the changes
performed by the model.

- [x] Fix buffer sometimes displaying empty
- [x] Stop buffer from scrolling together with the thread
- [x] Fix multibuffer header sometimes appearing
- [x] Fix buffer height issue
- [x] Implement "full height" expand button
- [x] Add "Jump To File" functionality
- [x] Polish and refine styles

Release Notes:

- agent: Added diff preview cards in the thread view for edits performed
by the agent.

---------

Co-authored-by: João Marcos <marcospb19@hotmail.com>
Co-authored-by: Richard Feldman <oss@rtfeldman.com>
Co-authored-by: Agus Zubiaga <hi@aguz.me>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
This commit is contained in:
Danilo Leal
2025-04-23 15:43:33 -03:00
committed by GitHub
co-authored by João Marcos Richard Feldman Agus Zubiaga Conrad Irwin
parent f6774ae60d
commit 8366cd0b52
40 changed files with 1003 additions and 316 deletions
+46 -52
View File
@@ -433,47 +433,39 @@ fn render_markdown_code_block(
workspace
.update(cx, {
|workspace, cx| {
if let Some(project_path) = workspace
let Some(project_path) = workspace
.project()
.read(cx)
.find_project_path(&path_range.path, cx)
{
let target = path_range.range.as_ref().map(|range| {
Point::new(
// Line number is 1-based
range.start.line.saturating_sub(1),
range.start.col.unwrap_or(0),
)
});
let open_task = workspace.open_path(
project_path,
None,
true,
window,
cx,
);
window
.spawn(cx, async move |cx| {
let item = open_task.await?;
if let Some(target) = target {
if let Some(active_editor) =
item.downcast::<Editor>()
{
active_editor
.downgrade()
.update_in(cx, |editor, window, cx| {
editor
.go_to_singleton_buffer_point(
target, window, cx,
);
})
.log_err();
}
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
else {
return;
};
let Some(target) = path_range.range.as_ref().map(|range| {
Point::new(
// Line number is 1-based
range.start.line.saturating_sub(1),
range.start.col.unwrap_or(0),
)
}) else {
return;
};
let open_task =
workspace.open_path(project_path, None, true, window, cx);
window
.spawn(cx, async move |cx| {
let item = open_task.await?;
if let Some(active_editor) = item.downcast::<Editor>() {
active_editor
.update_in(cx, |editor, window, cx| {
editor.go_to_singleton_buffer_point(
target, window, cx,
);
})
.ok();
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
})
.ok();
@@ -807,10 +799,11 @@ impl ActiveThread {
self.thread.read(cx).summary_or_default()
}
pub fn cancel_last_completion(&mut self, cx: &mut App) -> bool {
pub fn cancel_last_completion(&mut self, window: &mut Window, cx: &mut App) -> bool {
self.last_error.take();
self.thread
.update(cx, |thread, cx| thread.cancel_last_completion(cx))
self.thread.update(cx, |thread, cx| {
thread.cancel_last_completion(Some(window.window_handle()), cx)
})
}
pub fn last_error(&self) -> Option<ThreadError> {
@@ -1314,7 +1307,7 @@ impl ActiveThread {
fn confirm_editing_message(
&mut self,
_: &menu::Confirm,
_: &mut Window,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some((message_id, state)) = self.editing_message.take() else {
@@ -1344,7 +1337,7 @@ impl ActiveThread {
self.thread.update(cx, |thread, cx| {
thread.advance_prompt_id();
thread.send_to_model(model.model, cx)
thread.send_to_model(model.model, Some(window.window_handle()), cx);
});
cx.notify();
}
@@ -1850,11 +1843,9 @@ impl ActiveThread {
.gap_2()
.children(message_content)
.when(has_tool_uses, |parent| {
parent.children(
tool_uses
.into_iter()
.map(|tool_use| self.render_tool_use(tool_use, window, cx)),
)
parent.children(tool_uses.into_iter().map(|tool_use| {
self.render_tool_use(tool_use, window, workspace.clone(), cx)
}))
}),
Role::System => div().id(("message-container", ix)).py_1().px_2().child(
v_flex()
@@ -2447,10 +2438,11 @@ impl ActiveThread {
&self,
tool_use: ToolUse,
window: &mut Window,
workspace: WeakEntity<Workspace>,
cx: &mut Context<Self>,
) -> impl IntoElement + use<> {
if let Some(card) = self.thread.read(cx).card_for_tool(&tool_use.id) {
return card.render(&tool_use.status, window, cx);
return card.render(&tool_use.status, window, workspace, cx);
}
let is_open = self
@@ -3047,7 +3039,7 @@ impl ActiveThread {
&mut self,
tool_use_id: LanguageModelToolUseId,
_: &ClickEvent,
_window: &mut Window,
window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(PendingToolUseStatus::NeedsConfirmation(c)) = self
@@ -3063,6 +3055,7 @@ impl ActiveThread {
c.input.clone(),
&c.messages,
c.tool.clone(),
Some(window.window_handle()),
cx,
);
});
@@ -3074,11 +3067,12 @@ impl ActiveThread {
tool_use_id: LanguageModelToolUseId,
tool_name: Arc<str>,
_: &ClickEvent,
_window: &mut Window,
window: &mut Window,
cx: &mut Context<Self>,
) {
let window_handle = window.window_handle();
self.thread.update(cx, |thread, cx| {
thread.deny_tool_use(tool_use_id, tool_name, cx);
thread.deny_tool_use(tool_use_id, tool_name, Some(window_handle), cx);
});
}
+2 -7
View File
@@ -356,14 +356,9 @@ impl AssistantPanel {
&self.thread_store
}
fn cancel(
&mut self,
_: &editor::actions::Cancel,
_window: &mut Window,
cx: &mut Context<Self>,
) {
fn cancel(&mut self, _: &editor::actions::Cancel, window: &mut Window, cx: &mut Context<Self>) {
self.thread
.update(cx, |thread, cx| thread.cancel_last_completion(cx));
.update(cx, |thread, cx| thread.cancel_last_completion(window, cx));
}
fn new_thread(&mut self, action: &NewThread, window: &mut Window, cx: &mut Context<Self>) {
+6 -4
View File
@@ -195,6 +195,7 @@ impl MessageEditor {
editor.set_mode(EditorMode::Full {
scale_ui_elements_with_buffer_font_size: false,
show_active_line_background: false,
sized_by_content: false,
})
} else {
editor.set_mode(EditorMode::AutoHeight {
@@ -277,6 +278,7 @@ impl MessageEditor {
let context_store = self.context_store.clone();
let git_store = self.project.read(cx).git_store().clone();
let checkpoint = git_store.update(cx, |git_store, cx| git_store.checkpoint(cx));
let window_handle = window.window_handle();
cx.spawn(async move |this, cx| {
let checkpoint = checkpoint.await.ok();
@@ -333,7 +335,7 @@ impl MessageEditor {
thread
.update(cx, |thread, cx| {
thread.advance_prompt_id();
thread.send_to_model(model, cx);
thread.send_to_model(model, Some(window_handle), cx);
})
.log_err();
})
@@ -341,9 +343,9 @@ impl MessageEditor {
}
fn stop_current_and_send_new_message(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let cancelled = self
.thread
.update(cx, |thread, cx| thread.cancel_last_completion(cx));
let cancelled = self.thread.update(cx, |thread, cx| {
thread.cancel_last_completion(Some(window.window_handle()), cx)
});
if cancelled {
self.set_editor_is_expanded(false, cx);
+34 -11
View File
@@ -13,7 +13,9 @@ use feature_flags::{self, FeatureFlagAppExt};
use futures::future::Shared;
use futures::{FutureExt, StreamExt as _};
use git::repository::DiffType;
use gpui::{App, AppContext, Context, Entity, EventEmitter, SharedString, Task, WeakEntity};
use gpui::{
AnyWindowHandle, App, AppContext, Context, Entity, EventEmitter, SharedString, Task, WeakEntity,
};
use language_model::{
ConfiguredModel, LanguageModel, LanguageModelCompletionEvent, LanguageModelId,
LanguageModelImage, LanguageModelKnownError, LanguageModelRegistry, LanguageModelRequest,
@@ -951,7 +953,12 @@ impl Thread {
self.remaining_turns = remaining_turns;
}
pub fn send_to_model(&mut self, model: Arc<dyn LanguageModel>, cx: &mut Context<Self>) {
pub fn send_to_model(
&mut self,
model: Arc<dyn LanguageModel>,
window: Option<AnyWindowHandle>,
cx: &mut Context<Self>,
) {
if self.remaining_turns == 0 {
return;
}
@@ -982,7 +989,7 @@ impl Thread {
};
}
self.stream_completion(request, model, cx);
self.stream_completion(request, model, window, cx);
}
pub fn used_tools_since_last_user_message(&self) -> bool {
@@ -1201,6 +1208,7 @@ impl Thread {
&mut self,
request: LanguageModelRequest,
model: Arc<dyn LanguageModel>,
window: Option<AnyWindowHandle>,
cx: &mut Context<Self>,
) {
let pending_completion_id = post_inc(&mut self.completion_count);
@@ -1382,7 +1390,7 @@ impl Thread {
match result.as_ref() {
Ok(stop_reason) => match stop_reason {
StopReason::ToolUse => {
let tool_uses = thread.use_pending_tools(cx);
let tool_uses = thread.use_pending_tools(window, cx);
cx.emit(ThreadEvent::UsePendingTools { tool_uses });
}
StopReason::EndTurn => {}
@@ -1427,7 +1435,7 @@ impl Thread {
}));
}
thread.cancel_last_completion(cx);
thread.cancel_last_completion(window, cx);
}
}
cx.emit(ThreadEvent::Stopped(result.map_err(Arc::new)));
@@ -1596,7 +1604,11 @@ impl Thread {
)
}
pub fn use_pending_tools(&mut self, cx: &mut Context<Self>) -> Vec<PendingToolUse> {
pub fn use_pending_tools(
&mut self,
window: Option<AnyWindowHandle>,
cx: &mut Context<Self>,
) -> Vec<PendingToolUse> {
self.auto_capture_telemetry(cx);
let request = self.to_completion_request(cx);
let messages = Arc::new(request.messages);
@@ -1628,6 +1640,7 @@ impl Thread {
tool_use.input.clone(),
&messages,
tool,
window,
cx,
);
}
@@ -1644,9 +1657,10 @@ impl Thread {
input: serde_json::Value,
messages: &[LanguageModelRequestMessage],
tool: Arc<dyn Tool>,
window: Option<AnyWindowHandle>,
cx: &mut Context<Thread>,
) {
let task = self.spawn_tool_use(tool_use_id.clone(), messages, input, tool, cx);
let task = self.spawn_tool_use(tool_use_id.clone(), messages, input, tool, window, cx);
self.tool_use
.run_pending_tool(tool_use_id, ui_text.into(), task);
}
@@ -1657,6 +1671,7 @@ impl Thread {
messages: &[LanguageModelRequestMessage],
input: serde_json::Value,
tool: Arc<dyn Tool>,
window: Option<AnyWindowHandle>,
cx: &mut Context<Thread>,
) -> Task<()> {
let tool_name: Arc<str> = tool.name().into();
@@ -1669,6 +1684,7 @@ impl Thread {
messages,
self.project.clone(),
self.action_log.clone(),
window,
cx,
)
};
@@ -1691,7 +1707,7 @@ impl Thread {
output,
cx,
);
thread.tool_finished(tool_use_id, pending_tool_use, false, cx);
thread.tool_finished(tool_use_id, pending_tool_use, false, window, cx);
})
.ok();
}
@@ -1703,6 +1719,7 @@ impl Thread {
tool_use_id: LanguageModelToolUseId,
pending_tool_use: Option<PendingToolUse>,
canceled: bool,
window: Option<AnyWindowHandle>,
cx: &mut Context<Self>,
) {
if self.all_tools_finished() {
@@ -1710,7 +1727,7 @@ impl Thread {
if let Some(ConfiguredModel { model, .. }) = model_registry.default_model() {
self.attach_tool_results(cx);
if !canceled {
self.send_to_model(model, cx);
self.send_to_model(model, window, cx);
}
}
}
@@ -1732,7 +1749,11 @@ impl Thread {
/// Cancels the last pending completion, if there are any pending.
///
/// Returns whether a completion was canceled.
pub fn cancel_last_completion(&mut self, cx: &mut Context<Self>) -> bool {
pub fn cancel_last_completion(
&mut self,
window: Option<AnyWindowHandle>,
cx: &mut Context<Self>,
) -> bool {
let canceled = if self.pending_completions.pop().is_some() {
true
} else {
@@ -1743,6 +1764,7 @@ impl Thread {
pending_tool_use.id.clone(),
Some(pending_tool_use),
true,
window,
cx,
);
}
@@ -2199,6 +2221,7 @@ impl Thread {
&mut self,
tool_use_id: LanguageModelToolUseId,
tool_name: Arc<str>,
window: Option<AnyWindowHandle>,
cx: &mut Context<Self>,
) {
let err = Err(anyhow::anyhow!(
@@ -2207,7 +2230,7 @@ impl Thread {
self.tool_use
.insert_tool_output(tool_use_id.clone(), tool_name, err, cx);
self.tool_finished(tool_use_id.clone(), None, true, cx);
self.tool_finished(tool_use_id.clone(), None, true, window, cx);
}
}
+1
View File
@@ -28,6 +28,7 @@ serde.workspace = true
serde_json.workspace = true
text.workspace = true
util.workspace = true
workspace.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
+18 -4
View File
@@ -10,14 +10,16 @@ use std::sync::Arc;
use anyhow::Result;
use gpui::AnyElement;
use gpui::AnyWindowHandle;
use gpui::Context;
use gpui::IntoElement;
use gpui::Window;
use gpui::{App, Entity, SharedString, Task};
use gpui::{App, Entity, SharedString, Task, WeakEntity};
use icons::IconName;
use language_model::LanguageModelRequestMessage;
use language_model::LanguageModelToolSchemaFormat;
use project::Project;
use workspace::Workspace;
pub use crate::action_log::*;
pub use crate::tool_registry::*;
@@ -65,6 +67,7 @@ pub trait ToolCard: 'static + Sized {
&mut self,
status: &ToolUseStatus,
window: &mut Window,
workspace: WeakEntity<Workspace>,
cx: &mut Context<Self>,
) -> impl IntoElement;
}
@@ -76,6 +79,7 @@ pub struct AnyToolCard {
entity: gpui::AnyEntity,
status: &ToolUseStatus,
window: &mut Window,
workspace: WeakEntity<Workspace>,
cx: &mut App,
) -> AnyElement,
}
@@ -86,11 +90,14 @@ impl<T: ToolCard> From<Entity<T>> for AnyToolCard {
entity: gpui::AnyEntity,
status: &ToolUseStatus,
window: &mut Window,
workspace: WeakEntity<Workspace>,
cx: &mut App,
) -> AnyElement {
let entity = entity.downcast::<T>().unwrap();
entity.update(cx, |entity, cx| {
entity.render(status, window, cx).into_any_element()
entity
.render(status, window, workspace, cx)
.into_any_element()
})
}
@@ -102,8 +109,14 @@ impl<T: ToolCard> From<Entity<T>> for AnyToolCard {
}
impl AnyToolCard {
pub fn render(&self, status: &ToolUseStatus, window: &mut Window, cx: &mut App) -> AnyElement {
(self.render)(self.entity.clone(), status, window, cx)
pub fn render(
&self,
status: &ToolUseStatus,
window: &mut Window,
workspace: WeakEntity<Workspace>,
cx: &mut App,
) -> AnyElement {
(self.render)(self.entity.clone(), status, window, workspace, cx)
}
}
@@ -163,6 +176,7 @@ pub trait Tool: 'static + Send + Sync {
messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult;
}
+3
View File
@@ -14,9 +14,11 @@ path = "src/assistant_tools.rs"
[dependencies]
anyhow.workspace = true
assistant_tool.workspace = true
buffer_diff.workspace = true
chrono.workspace = true
collections.workspace = true
component.workspace = true
editor.workspace = true
futures.workspace = true
gpui.workspace = true
html_to_markdown.workspace = true
@@ -35,6 +37,7 @@ serde_json.workspace = true
ui.workspace = true
util.workspace = true
web_search.workspace = true
workspace.workspace = true
workspace-hack.workspace = true
worktree.workspace = true
zed_llm_client.workspace = true
+5 -2
View File
@@ -2,7 +2,7 @@ use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult, ToolWorkingSet};
use futures::future::join_all;
use gpui::{App, AppContext, Entity, Task};
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
@@ -218,6 +218,7 @@ impl Tool for BatchTool {
messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<BatchToolInput>(input) {
@@ -258,7 +259,9 @@ impl Tool for BatchTool {
let action_log = action_log.clone();
let messages = messages.clone();
let tool_result = cx
.update(|cx| tool.run(invocation.input, &messages, project, action_log, cx))
.update(|cx| {
tool.run(invocation.input, &messages, project, action_log, window, cx)
})
.map_err(|err| anyhow!("Failed to start tool '{}': {}", tool_name, err))?;
tasks.push(tool_result.output);
@@ -1,6 +1,6 @@
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, Entity, Task};
use gpui::{AnyWindowHandle, App, Entity, Task};
use language::{self, Anchor, Buffer, ToPointUtf16};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::{self, LspAction, Project};
@@ -140,6 +140,7 @@ impl Tool for CodeActionTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<CodeActionToolInput>(input) {
@@ -6,7 +6,7 @@ use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use collections::IndexMap;
use gpui::{App, AsyncApp, Entity, Task};
use gpui::{AnyWindowHandle, App, AsyncApp, Entity, Task};
use language::{OutlineItem, ParseStatus, Point};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::{Project, Symbol};
@@ -128,6 +128,7 @@ impl Tool for CodeSymbolsTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<CodeSymbolsInput>(input) {
+2 -1
View File
@@ -3,7 +3,7 @@ use std::sync::Arc;
use crate::{code_symbols_tool::file_outline, schema::json_schema_for};
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, Entity, Task};
use gpui::{AnyWindowHandle, App, Entity, Task};
use itertools::Itertools;
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
@@ -102,6 +102,7 @@ impl Tool for ContentsTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<ContentsToolInput>(input) {
@@ -1,6 +1,7 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::AnyWindowHandle;
use gpui::{App, AppContext, Entity, Task};
use language_model::LanguageModelRequestMessage;
use language_model::LanguageModelToolSchemaFormat;
@@ -76,6 +77,7 @@ impl Tool for CopyPathTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<CopyPathToolInput>(input) {
@@ -1,6 +1,7 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::AnyWindowHandle;
use gpui::{App, Entity, Task};
use language_model::LanguageModelRequestMessage;
use language_model::LanguageModelToolSchemaFormat;
@@ -67,6 +68,7 @@ impl Tool for CreateDirectoryTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<CreateDirectoryToolInput>(input) {
@@ -1,6 +1,7 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::AnyWindowHandle;
use gpui::{App, Entity, Task};
use language_model::LanguageModelRequestMessage;
use language_model::LanguageModelToolSchemaFormat;
@@ -89,6 +90,7 @@ impl Tool for CreateFileTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<CreateFileToolInput>(input) {
@@ -2,7 +2,7 @@ use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use futures::{SinkExt, StreamExt, channel::mpsc};
use gpui::{App, AppContext, Entity, Task};
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::{Project, ProjectPath};
use schemars::JsonSchema;
@@ -62,6 +62,7 @@ impl Tool for DeletePathTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let path_str = match serde_json::from_value::<DeletePathToolInput>(input) {
@@ -1,7 +1,7 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, Entity, Task};
use gpui::{AnyWindowHandle, App, Entity, Task};
use language::{DiagnosticSeverity, OffsetRangeExt};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
@@ -82,6 +82,7 @@ impl Tool for DiagnosticsTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
match serde_json::from_value::<DiagnosticsToolInput>(input)
+435 -30
View File
@@ -1,15 +1,28 @@
use crate::{replace::replace_with_flexible_indent, schema::json_schema_for};
use crate::{
replace::{replace_exact, replace_with_flexible_indent},
schema::json_schema_for,
};
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, AppContext, AsyncApp, Entity, Task};
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolCard, ToolResult, ToolUseStatus};
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
use editor::{Editor, EditorMode, MultiBuffer, PathKey};
use gpui::{
AnyWindowHandle, App, AppContext, AsyncApp, Context, Entity, EntityId, Task, WeakEntity,
};
use language::{
Anchor, Buffer, Capability, LanguageRegistry, LineEnding, OffsetRangeExt, Rope, TextBuffer,
};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{path::PathBuf, sync::Arc};
use ui::IconName;
use crate::replace::replace_exact;
use std::{
path::{Path, PathBuf},
sync::Arc,
};
use ui::{Disclosure, Tooltip, Window, prelude::*};
use util::ResultExt;
use workspace::Workspace;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct EditFileToolInput {
@@ -113,6 +126,7 @@ impl Tool for EditFileTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<EditFileToolInput>(input) {
@@ -120,7 +134,18 @@ impl Tool for EditFileTool {
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
cx.spawn(async move |cx: &mut AsyncApp| {
let card = window.and_then(|window| {
window
.update(cx, |_, window, cx| {
cx.new(|cx| {
EditFileToolCard::new(input.path.clone(), project.clone(), window, cx)
})
})
.ok()
});
let card_clone = card.clone();
let task = cx.spawn(async move |cx: &mut AsyncApp| {
let project_path = project.read_with(cx, |project, cx| {
project
.find_project_path(&input.path, cx)
@@ -128,26 +153,38 @@ impl Tool for EditFileTool {
})??;
let buffer = project
.update(cx, |project, cx| project.open_buffer(project_path, cx))?
.update(cx, |project, cx| {
project.open_buffer(project_path.clone(), cx)
})?
.await?;
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
if input.old_string.is_empty() {
return Err(anyhow!("`old_string` cannot be empty. Use a different tool if you want to create a file."));
return Err(anyhow!(
"`old_string` can't be empty, use another tool if you want to create a file."
));
}
if input.old_string == input.new_string {
return Err(anyhow!("The `old_string` and `new_string` are identical, so no changes would be made."));
return Err(anyhow!(
"The `old_string` and `new_string` are identical, so no changes would be made."
));
}
let result = cx
.background_spawn(async move {
// Try to match exactly
let diff = replace_exact(&input.old_string, &input.new_string, &snapshot)
.await
// If that fails, try being flexible about indentation
.or_else(|| replace_with_flexible_indent(&input.old_string, &input.new_string, &snapshot))?;
.await
// If that fails, try being flexible about indentation
.or_else(|| {
replace_with_flexible_indent(
&input.old_string,
&input.new_string,
&snapshot,
)
})?;
if diff.edits.is_empty() {
return None;
@@ -177,41 +214,409 @@ impl Tool for EditFileTool {
}
})?;
return Err(err)
return Err(err);
};
let snapshot = cx.update(|cx| {
action_log.update(cx, |log, cx| {
log.track_buffer(buffer.clone(), cx)
});
action_log.update(cx, |log, cx| log.track_buffer(buffer.clone(), cx));
let snapshot = buffer.update(cx, |buffer, cx| {
buffer.finalize_last_transaction();
buffer.apply_diff(diff, cx);
buffer.finalize_last_transaction();
buffer.snapshot()
});
action_log.update(cx, |log, cx| {
log.buffer_edited(buffer.clone(), cx)
});
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
snapshot
})?;
project.update( cx, |project, cx| {
project.save_buffer(buffer, cx)
})?.await?;
project
.update(cx, |project, cx| project.save_buffer(buffer, cx))?
.await?;
let diff_str = cx.background_spawn(async move {
let new_text = snapshot.text();
language::unified_diff(&old_text, &new_text)
}).await;
let new_text = snapshot.text();
let diff_str = cx
.background_spawn({
let old_text = old_text.clone();
let new_text = new_text.clone();
async move { language::unified_diff(&old_text, &new_text) }
})
.await;
if let Some(card) = card_clone {
card.update(cx, |card, cx| {
card.set_diff(project_path.path.clone(), old_text, new_text, cx);
})
.log_err();
}
Ok(format!("Edited {}:\n\n```diff\n{}\n```", input.path.display(), diff_str))
Ok(format!(
"Edited {}:\n\n```diff\n{}\n```",
input.path.display(),
diff_str
))
});
}).into()
ToolResult {
output: task,
card: card.map(AnyToolCard::from),
}
}
}
pub struct EditFileToolCard {
path: PathBuf,
editor: Entity<Editor>,
multibuffer: Entity<MultiBuffer>,
project: Entity<Project>,
diff_task: Option<Task<Result<()>>>,
preview_expanded: bool,
full_height_expanded: bool,
editor_unique_id: EntityId,
}
impl EditFileToolCard {
fn new(path: PathBuf, project: Entity<Project>, window: &mut Window, cx: &mut App) -> Self {
let multibuffer = cx.new(|_| MultiBuffer::without_headers(Capability::ReadOnly));
let editor = cx.new(|cx| {
let mut editor = Editor::new(
EditorMode::Full {
scale_ui_elements_with_buffer_font_size: false,
show_active_line_background: false,
sized_by_content: true,
},
multibuffer.clone(),
Some(project.clone()),
window,
cx,
);
editor.set_show_scrollbars(false, cx);
editor.set_show_gutter(false, cx);
editor.disable_inline_diagnostics();
editor.disable_scrolling(cx);
editor.disable_expand_excerpt_buttons(cx);
editor.set_show_breakpoints(false, cx);
editor.set_show_code_actions(false, cx);
editor.set_show_git_diff_gutter(false, cx);
editor.set_expand_all_diff_hunks(cx);
editor
});
Self {
editor_unique_id: editor.entity_id(),
path,
project,
editor,
multibuffer,
diff_task: None,
preview_expanded: true,
full_height_expanded: false,
}
}
fn set_diff(
&mut self,
path: Arc<Path>,
old_text: String,
new_text: String,
cx: &mut Context<Self>,
) {
let language_registry = self.project.read(cx).languages().clone();
self.diff_task = Some(cx.spawn(async move |this, cx| {
let buffer = build_buffer(new_text, path.clone(), &language_registry, cx).await?;
let buffer_diff = build_buffer_diff(old_text, &buffer, &language_registry, cx).await?;
this.update(cx, |this, cx| {
this.multibuffer.update(cx, |multibuffer, cx| {
let snapshot = buffer.read(cx).snapshot();
let diff = buffer_diff.read(cx);
let diff_hunk_ranges = diff
.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx)
.map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot))
.collect::<Vec<_>>();
let (_, is_newly_added) = multibuffer.set_excerpts_for_path(
PathKey::for_buffer(&buffer, cx),
buffer,
diff_hunk_ranges,
editor::DEFAULT_MULTIBUFFER_CONTEXT,
cx,
);
debug_assert!(is_newly_added);
multibuffer.add_diff(buffer_diff, cx);
});
cx.notify();
})
}));
}
}
impl ToolCard for EditFileToolCard {
fn render(
&mut self,
status: &ToolUseStatus,
window: &mut Window,
workspace: WeakEntity<Workspace>,
cx: &mut Context<Self>,
) -> impl IntoElement {
let failed = matches!(status, ToolUseStatus::Error(_));
let path_label_button = h_flex()
.id(("edit-tool-path-label-button", self.editor_unique_id))
.w_full()
.max_w_full()
.px_1()
.gap_0p5()
.cursor_pointer()
.rounded_sm()
.opacity(0.8)
.hover(|label| {
label
.opacity(1.)
.bg(cx.theme().colors().element_hover.opacity(0.5))
})
.tooltip(Tooltip::text("Jump to File"))
.child(
h_flex()
.child(
Icon::new(IconName::Pencil)
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(
div()
.text_size(rems(0.8125))
.child(self.path.display().to_string())
.ml_1p5()
.mr_0p5(),
)
.child(
Icon::new(IconName::ArrowUpRight)
.size(IconSize::XSmall)
.color(Color::Ignored),
),
)
.on_click({
let path = self.path.clone();
let workspace = workspace.clone();
move |_, window, cx| {
workspace
.update(cx, {
|workspace, cx| {
let Some(project_path) =
workspace.project().read(cx).find_project_path(&path, cx)
else {
return;
};
let open_task =
workspace.open_path(project_path, None, true, window, cx);
window
.spawn(cx, async move |cx| {
let item = open_task.await?;
if let Some(active_editor) = item.downcast::<Editor>() {
active_editor
.update_in(cx, |editor, window, cx| {
editor.go_to_singleton_buffer_point(
language::Point::new(0, 0),
window,
cx,
);
})
.log_err();
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
})
.ok();
}
})
.into_any_element();
let codeblock_header_bg = cx
.theme()
.colors()
.element_background
.blend(cx.theme().colors().editor_foreground.opacity(0.025));
let codeblock_header = h_flex()
.flex_none()
.p_1()
.gap_1()
.justify_between()
.rounded_t_md()
.when(!failed, |header| header.bg(codeblock_header_bg))
.child(path_label_button)
.map(|container| {
if failed {
container.child(
Icon::new(IconName::Close)
.size(IconSize::Small)
.color(Color::Error),
)
} else {
container.child(
Disclosure::new(
("edit-file-disclosure", self.editor_unique_id),
self.preview_expanded,
)
.opened_icon(IconName::ChevronUp)
.closed_icon(IconName::ChevronDown)
.on_click(cx.listener(
move |this, _event, _window, _cx| {
this.preview_expanded = !this.preview_expanded;
},
)),
)
}
});
let editor = self.editor.update(cx, |editor, cx| {
editor.render(window, cx).into_any_element()
});
let (full_height_icon, full_height_tooltip_label) = if self.full_height_expanded {
(IconName::ChevronUp, "Collapse Code Block")
} else {
(IconName::ChevronDown, "Expand Code Block")
};
let gradient_overlay = div()
.absolute()
.bottom_0()
.left_0()
.w_full()
.h_2_5()
.rounded_b_lg()
.bg(gpui::linear_gradient(
0.,
gpui::linear_color_stop(cx.theme().colors().editor_background, 0.),
gpui::linear_color_stop(cx.theme().colors().editor_background.opacity(0.), 1.),
));
let border_color = cx.theme().colors().border.opacity(0.6);
v_flex()
.mb_2()
.border_1()
.when(failed, |card| card.border_dashed())
.border_color(border_color)
.rounded_lg()
.overflow_hidden()
.child(codeblock_header)
.when(!failed && self.preview_expanded, |card| {
card.child(
v_flex()
.relative()
.overflow_hidden()
.border_t_1()
.border_color(border_color)
.bg(cx.theme().colors().editor_background)
.map(|editor_container| {
if self.full_height_expanded {
editor_container.h_full()
} else {
editor_container.max_h_64()
}
})
.child(div().pl_1().child(editor))
.when(!self.full_height_expanded, |editor_container| {
editor_container.child(gradient_overlay)
}),
)
})
.when(!failed && self.preview_expanded, |card| {
card.child(
h_flex()
.id(("edit-tool-card-inner-hflex", self.editor_unique_id))
.flex_none()
.cursor_pointer()
.h_5()
.justify_center()
.rounded_b_md()
.border_t_1()
.border_color(border_color)
.bg(cx.theme().colors().editor_background)
.hover(|style| style.bg(cx.theme().colors().element_hover.opacity(0.1)))
.child(
Icon::new(full_height_icon)
.size(IconSize::Small)
.color(Color::Muted),
)
.tooltip(Tooltip::text(full_height_tooltip_label))
.on_click(cx.listener(move |this, _event, _window, _cx| {
this.full_height_expanded = !this.full_height_expanded;
})),
)
})
}
}
async fn build_buffer(
mut text: String,
path: Arc<Path>,
language_registry: &Arc<language::LanguageRegistry>,
cx: &mut AsyncApp,
) -> Result<Entity<Buffer>> {
let line_ending = LineEnding::detect(&text);
LineEnding::normalize(&mut text);
let text = Rope::from(text);
let language = cx
.update(|_cx| language_registry.language_for_file_path(&path))?
.await
.ok();
let buffer = cx.new(|cx| {
let buffer = TextBuffer::new_normalized(
0,
cx.entity_id().as_non_zero_u64().into(),
line_ending,
text,
);
let mut buffer = Buffer::build(buffer, None, Capability::ReadWrite);
buffer.set_language(language, cx);
buffer
})?;
Ok(buffer)
}
async fn build_buffer_diff(
mut old_text: String,
buffer: &Entity<Buffer>,
language_registry: &Arc<LanguageRegistry>,
cx: &mut AsyncApp,
) -> Result<Entity<BufferDiff>> {
LineEnding::normalize(&mut old_text);
let buffer = cx.update(|cx| buffer.read(cx).snapshot())?;
let base_buffer = cx
.update(|cx| {
Buffer::build_snapshot(
old_text.clone().into(),
buffer.language().cloned(),
Some(language_registry.clone()),
cx,
)
})?
.await;
let diff_snapshot = cx
.update(|cx| {
BufferDiffSnapshot::new_with_base_buffer(
buffer.text.clone(),
Some(old_text.into()),
base_buffer,
cx,
)
})?
.await;
cx.new(|cx| {
let mut diff = BufferDiff::new(&buffer.text, cx);
diff.set_snapshot(diff_snapshot, &buffer.text, cx);
diff
})
}
#[cfg(test)]
mod tests {
use super::*;
+2 -1
View File
@@ -6,7 +6,7 @@ use crate::schema::json_schema_for;
use anyhow::{Context as _, Result, anyhow, bail};
use assistant_tool::{ActionLog, Tool, ToolResult};
use futures::AsyncReadExt as _;
use gpui::{App, AppContext as _, Entity, Task};
use gpui::{AnyWindowHandle, App, AppContext as _, Entity, Task};
use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown};
use http_client::{AsyncBody, HttpClientWithUrl};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
@@ -145,6 +145,7 @@ impl Tool for FetchTool {
_messages: &[LanguageModelRequestMessage],
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<FetchToolInput>(input) {
+3 -2
View File
@@ -2,7 +2,7 @@ use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use futures::StreamExt;
use gpui::{App, Entity, Task};
use gpui::{AnyWindowHandle, App, Entity, Task};
use language::OffsetRangeExt;
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::{
@@ -96,6 +96,7 @@ impl Tool for GrepTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
const CONTEXT_LINES: u32 = 2;
@@ -405,7 +406,7 @@ mod tests {
) -> String {
let tool = Arc::new(GrepTool);
let action_log = cx.new(|_cx| ActionLog::new(project.clone()));
let task = cx.update(|cx| tool.run(input, &[], project, action_log, cx));
let task = cx.update(|cx| tool.run(input, &[], project, action_log, None, cx));
match task.output.await {
Ok(result) => result,
@@ -1,7 +1,7 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, Entity, Task};
use gpui::{AnyWindowHandle, App, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
@@ -76,6 +76,7 @@ impl Tool for ListDirectoryTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<ListDirectoryToolInput>(input) {
+2 -1
View File
@@ -1,7 +1,7 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, AppContext, Entity, Task};
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
@@ -89,6 +89,7 @@ impl Tool for MovePathTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<MovePathToolInput>(input) {
+2 -1
View File
@@ -4,7 +4,7 @@ use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use chrono::{Local, Utc};
use gpui::{App, Entity, Task};
use gpui::{AnyWindowHandle, App, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
@@ -59,6 +59,7 @@ impl Tool for NowTool {
_messages: &[LanguageModelRequestMessage],
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
_cx: &mut App,
) -> ToolResult {
let input: NowToolInput = match serde_json::from_value(input) {
+2 -1
View File
@@ -1,7 +1,7 @@
use crate::schema::json_schema_for;
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, AppContext, Entity, Task};
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
@@ -52,6 +52,7 @@ impl Tool for OpenTool {
_messages: &[LanguageModelRequestMessage],
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input: OpenToolInput = match serde_json::from_value(input) {
@@ -1,7 +1,7 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, AppContext, Entity, Task};
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
@@ -70,6 +70,7 @@ impl Tool for PathSearchTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let (offset, glob) = match serde_json::from_value::<PathSearchToolInput>(input) {
+8 -6
View File
@@ -1,7 +1,8 @@
use crate::{code_symbols_tool::file_outline, schema::json_schema_for};
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, Entity, Task};
use gpui::{AnyWindowHandle, App, Entity, Task};
use indoc::formatdoc;
use itertools::Itertools;
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
@@ -87,6 +88,7 @@ impl Tool for ReadFileTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<ReadFileToolInput>(input) {
@@ -193,7 +195,7 @@ mod test {
"path": "root/nonexistent_file.txt"
});
Arc::new(ReadFileTool)
.run(input, &[], project.clone(), action_log, cx)
.run(input, &[], project.clone(), action_log, None, cx)
.output
})
.await;
@@ -223,7 +225,7 @@ mod test {
"path": "root/small_file.txt"
});
Arc::new(ReadFileTool)
.run(input, &[], project.clone(), action_log, cx)
.run(input, &[], project.clone(), action_log, None, cx)
.output
})
.await;
@@ -253,7 +255,7 @@ mod test {
"path": "root/large_file.rs"
});
Arc::new(ReadFileTool)
.run(input, &[], project.clone(), action_log.clone(), cx)
.run(input, &[], project.clone(), action_log.clone(), None, cx)
.output
})
.await;
@@ -277,7 +279,7 @@ mod test {
"offset": 1
});
Arc::new(ReadFileTool)
.run(input, &[], project.clone(), action_log, cx)
.run(input, &[], project.clone(), action_log, None, cx)
.output
})
.await;
@@ -323,7 +325,7 @@ mod test {
"end_line": 4
});
Arc::new(ReadFileTool)
.run(input, &[], project.clone(), action_log, cx)
.run(input, &[], project.clone(), action_log, None, cx)
.output
})
.await;
+2 -1
View File
@@ -1,6 +1,6 @@
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, Entity, Task};
use gpui::{AnyWindowHandle, App, Entity, Task};
use language::{self, Buffer, ToPointUtf16};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
@@ -87,6 +87,7 @@ impl Tool for RenameTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<RenameToolInput>(input) {
@@ -1,6 +1,6 @@
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, AsyncApp, Entity, Task};
use gpui::{AnyWindowHandle, App, AsyncApp, Entity, Task};
use language::{self, Anchor, Buffer, BufferSnapshot, Location, Point, ToPoint, ToPointUtf16};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
@@ -121,6 +121,7 @@ impl Tool for SymbolInfoTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<SymbolInfoToolInput>(input) {
+2 -1
View File
@@ -3,7 +3,7 @@ use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use futures::io::BufReader;
use futures::{AsyncBufReadExt, AsyncReadExt, FutureExt};
use gpui::{App, AppContext, Entity, Task};
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
@@ -78,6 +78,7 @@ impl Tool for TerminalTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input: TerminalToolInput = match serde_json::from_value(input) {
+2 -1
View File
@@ -3,7 +3,7 @@ use std::sync::Arc;
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, Entity, Task};
use gpui::{AnyWindowHandle, App, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
@@ -50,6 +50,7 @@ impl Tool for ThinkingTool {
_messages: &[LanguageModelRequestMessage],
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
_cx: &mut App,
) -> ToolResult {
// This tool just "thinks out loud" and doesn't perform any actions.
+27 -7
View File
@@ -5,13 +5,16 @@ use crate::ui::ToolCallCardHeader;
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolCard, ToolResult, ToolUseStatus};
use futures::{Future, FutureExt, TryFutureExt};
use gpui::{App, AppContext, Context, Entity, IntoElement, Task, Window};
use gpui::{
AnyWindowHandle, App, AppContext, Context, Entity, IntoElement, Task, WeakEntity, Window,
};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use ui::{IconName, Tooltip, prelude::*};
use web_search::WebSearchRegistry;
use workspace::Workspace;
use zed_llm_client::{WebSearchCitation, WebSearchResponse};
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
@@ -54,6 +57,7 @@ impl Tool for WebSearchTool {
_messages: &[LanguageModelRequestMessage],
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<WebSearchToolInput>(input) {
@@ -111,6 +115,7 @@ impl ToolCard for WebSearchToolCard {
&mut self,
_status: &ToolUseStatus,
_window: &mut Window,
_workspace: WeakEntity<Workspace>,
cx: &mut Context<Self>,
) -> impl IntoElement {
let header = match self.response.as_ref() {
@@ -220,8 +225,13 @@ impl Component for WebSearchTool {
div()
.size_full()
.child(in_progress_search.update(cx, |tool, cx| {
tool.render(&ToolUseStatus::Pending, window, cx)
.into_any_element()
tool.render(
&ToolUseStatus::Pending,
window,
WeakEntity::new_invalid(),
cx,
)
.into_any_element()
}))
.into_any_element(),
),
@@ -230,8 +240,13 @@ impl Component for WebSearchTool {
div()
.size_full()
.child(successful_search.update(cx, |tool, cx| {
tool.render(&ToolUseStatus::Finished("".into()), window, cx)
.into_any_element()
tool.render(
&ToolUseStatus::Finished("".into()),
window,
WeakEntity::new_invalid(),
cx,
)
.into_any_element()
}))
.into_any_element(),
),
@@ -240,8 +255,13 @@ impl Component for WebSearchTool {
div()
.size_full()
.child(error_search.update(cx, |tool, cx| {
tool.render(&ToolUseStatus::Error("".into()), window, cx)
.into_any_element()
tool.render(
&ToolUseStatus::Error("".into()),
window,
WeakEntity::new_invalid(),
cx,
)
.into_any_element()
}))
.into_any_element(),
),
@@ -2,7 +2,7 @@ use std::sync::Arc;
use anyhow::{Result, anyhow, bail};
use assistant_tool::{ActionLog, Tool, ToolResult, ToolSource};
use gpui::{App, Entity, Task};
use gpui::{AnyWindowHandle, App, Entity, Task};
use icons::IconName;
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
@@ -77,6 +77,7 @@ impl Tool for ContextServerTool {
_messages: &[LanguageModelRequestMessage],
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
if let Some(server) = self.server_manager.read(cx).get_server(&self.server_id) {
+17
View File
@@ -433,6 +433,8 @@ pub enum EditorMode {
scale_ui_elements_with_buffer_font_size: bool,
/// When set to `true`, the editor will render a background for the active line.
show_active_line_background: bool,
/// When set to `true`, the editor's height will be determined by its content.
sized_by_content: bool,
},
}
@@ -441,6 +443,7 @@ impl EditorMode {
Self::Full {
scale_ui_elements_with_buffer_font_size: true,
show_active_line_background: true,
sized_by_content: false,
}
}
@@ -798,6 +801,8 @@ pub struct Editor {
show_breadcrumbs: bool,
show_gutter: bool,
show_scrollbars: bool,
disable_scrolling: bool,
disable_expand_excerpt_buttons: bool,
show_line_numbers: Option<bool>,
use_relative_line_numbers: Option<bool>,
show_git_diff_gutter: Option<bool>,
@@ -1589,11 +1594,13 @@ impl Editor {
blink_manager: blink_manager.clone(),
show_local_selections: true,
show_scrollbars: true,
disable_scrolling: true,
mode,
show_breadcrumbs: EditorSettings::get_global(cx).toolbar.breadcrumbs,
show_gutter: mode.is_full(),
show_line_numbers: None,
use_relative_line_numbers: None,
disable_expand_excerpt_buttons: false,
show_git_diff_gutter: None,
show_code_actions: None,
show_runnables: None,
@@ -16175,11 +16182,21 @@ impl Editor {
cx.notify();
}
pub fn disable_scrolling(&mut self, cx: &mut Context<Self>) {
self.disable_scrolling = true;
cx.notify();
}
pub fn set_show_line_numbers(&mut self, show_line_numbers: bool, cx: &mut Context<Self>) {
self.show_line_numbers = Some(show_line_numbers);
cx.notify();
}
pub fn disable_expand_excerpt_buttons(&mut self, cx: &mut Context<Self>) {
self.disable_expand_excerpt_buttons = true;
cx.notify();
}
pub fn set_show_git_diff_gutter(&mut self, show_git_diff_gutter: bool, cx: &mut Context<Self>) {
self.show_git_diff_gutter = Some(show_git_diff_gutter);
cx.notify();
+1
View File
@@ -10419,6 +10419,7 @@ async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppConte
EditorMode::Full {
scale_ui_elements_with_buffer_font_size: false,
show_active_line_background: false,
sized_by_content: false,
},
multi_buffer.clone(),
Some(project.clone()),
+20 -3
View File
@@ -2184,6 +2184,10 @@ impl EditorElement {
window: &mut Window,
cx: &mut App,
) -> Vec<Option<(AnyElement, gpui::Point<Pixels>)>> {
if self.editor.read(cx).disable_expand_excerpt_buttons {
return vec![];
}
let editor_font_size = self.style.text.font_size.to_pixels(window.rem_size()) * 1.2;
let scroll_top = scroll_position.y * line_height;
@@ -5537,7 +5541,9 @@ impl EditorElement {
}
fn paint_mouse_listeners(&mut self, layout: &EditorLayout, window: &mut Window, cx: &mut App) {
self.paint_scroll_wheel_listener(layout, window, cx);
if !self.editor.read(cx).disable_scrolling {
self.paint_scroll_wheel_listener(layout, window, cx);
}
window.on_mouse_event({
let position_map = layout.position_map.clone();
@@ -6588,10 +6594,21 @@ impl Element for EditorElement {
},
)
}
EditorMode::Full { .. } => {
EditorMode::Full {
sized_by_content, ..
} => {
let mut style = Style::default();
style.size.width = relative(1.).into();
style.size.height = relative(1.).into();
if sized_by_content {
let snapshot = editor.snapshot(window, cx);
let line_height =
self.style.text.line_height_in_pixels(window.rem_size());
let scroll_height =
(snapshot.max_point().row().next_row().0 as f32) * line_height;
style.size.height = scroll_height.into();
} else {
style.size.height = relative(1.).into();
}
window.request_layout(style, None, cx)
}
};
+1 -1
View File
@@ -278,7 +278,7 @@ impl ExampleContext {
let message_count_before = self.app.update_entity(&self.agent_thread, |thread, cx| {
thread.set_remaining_turns(iterations);
thread.send_to_model(model, cx);
thread.send_to_model(model, None, cx);
thread.messages().len()
})?;
+33 -1
View File
@@ -15,7 +15,7 @@ use std::{
num::NonZeroU64,
sync::{
Arc, Weak,
atomic::{AtomicUsize, Ordering::SeqCst},
atomic::{AtomicU64, AtomicUsize, Ordering::SeqCst},
},
thread::panicking,
};
@@ -572,6 +572,30 @@ impl AnyWeakEntity {
)
}
}
/// Creates a weak entity that can never be upgraded.
pub fn new_invalid() -> Self {
/// To hold the invariant that all ids are unique, and considering that slotmap
/// increases their IDs from `0`, we can decrease ours from `u64::MAX` so these
/// two will never conflict (u64 is way too large).
static UNIQUE_NON_CONFLICTING_ID_GENERATOR: AtomicU64 = AtomicU64::new(u64::MAX);
let entity_id = UNIQUE_NON_CONFLICTING_ID_GENERATOR.fetch_sub(1, SeqCst);
Self {
// Safety:
// Docs say this is safe but can be unspecified if slotmap changes the representation
// after `1.0.7`, that said, providing a valid entity_id here is not necessary as long
// as we guarantee that that `entity_id` is never used if `entity_ref_counts` equals
// to `Weak::new()` (that is, it's unable to upgrade), that is the invariant that
// actually needs to be hold true.
//
// And there is no sane reason to read an entity slot if `entity_ref_counts` can't be
// read in the first place, so we're good!
entity_id: entity_id.into(),
entity_type: TypeId::of::<()>(),
entity_ref_counts: Weak::new(),
}
}
}
impl std::fmt::Debug for AnyWeakEntity {
@@ -707,6 +731,14 @@ impl<T: 'static> WeakEntity<T> {
.map(|this| cx.read_entity(&this, read)),
)
}
/// Create a new weak entity that can never be upgraded.
pub fn new_invalid() -> Self {
Self {
any_entity: AnyWeakEntity::new_invalid(),
entity_type: PhantomData,
}
}
}
impl<T> Hash for WeakEntity<T> {
-1
View File
@@ -83,7 +83,6 @@ impl VsCodeDebugTaskDefinition {
}
}
/// blah
#[derive(Clone, Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct VsCodeDebugTaskFile {
+1 -1
View File
@@ -30,7 +30,7 @@ itertools.workspace = true
language.workspace = true
log.workspace = true
multi_buffer.workspace = true
nvim-rs = { git = "https://github.com/KillTheMule/nvim-rs", branch = "master", features = ["use_tokio"], optional = true }
nvim-rs = { git = "https://github.com/KillTheMule/nvim-rs", rev = "764dd270c642f77f10f3e19d05cc178a6cbe69f3", features = ["use_tokio"], optional = true }
picker.workspace = true
project.workspace = true
regex.workspace = true