mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-10 05:37:29 +03:00
Make WorkflowStepResolution an entity (#16268)
This PR is just a refactor, to pave the way toward adding a view for workflow step resolution. The entity carries the state of the tool call's streaming output. Release Notes: - N/A
This commit is contained in:
parent
102796979b
commit
e0cabbd142
@ -13,6 +13,7 @@ mod slash_command;
|
||||
pub mod slash_command_settings;
|
||||
mod streaming_diff;
|
||||
mod terminal_inline_assistant;
|
||||
mod workflow;
|
||||
|
||||
pub use assistant_panel::{AssistantPanel, AssistantPanelEvent};
|
||||
use assistant_settings::AssistantSettings;
|
||||
@ -43,6 +44,7 @@ use slash_command::{
|
||||
use std::sync::Arc;
|
||||
pub(crate) use streaming_diff::*;
|
||||
use util::ResultExt;
|
||||
pub use workflow::*;
|
||||
|
||||
use crate::slash_command_settings::SlashCommandSettings;
|
||||
|
||||
|
@ -1884,9 +1884,8 @@ impl ContextEditor {
|
||||
range: Range<language::Anchor>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.context.update(cx, |context, cx| {
|
||||
context.resolve_workflow_step(range, self.project.clone(), cx)
|
||||
});
|
||||
self.context
|
||||
.update(cx, |context, cx| context.resolve_workflow_step(range, cx));
|
||||
}
|
||||
|
||||
fn stop_workflow_step(&mut self, range: Range<language::Anchor>, cx: &mut ViewContext<Self>) {
|
||||
@ -2010,19 +2009,16 @@ impl ContextEditor {
|
||||
.text_for_range(step.tagged_range.clone())
|
||||
.collect::<String>()
|
||||
));
|
||||
match &step.status {
|
||||
crate::WorkflowStepStatus::Resolved(ResolvedWorkflowStep {
|
||||
title,
|
||||
suggestions,
|
||||
}) => {
|
||||
match &step.resolution.read(cx).result {
|
||||
Some(Ok(ResolvedWorkflowStep { title, suggestions })) => {
|
||||
output.push_str("Resolution:\n");
|
||||
output.push_str(&format!(" {:?}\n", title));
|
||||
output.push_str(&format!(" {:?}\n", suggestions));
|
||||
}
|
||||
crate::WorkflowStepStatus::Pending(_) => {
|
||||
None => {
|
||||
output.push_str("Resolution: Pending\n");
|
||||
}
|
||||
crate::WorkflowStepStatus::Error(error) => {
|
||||
Some(Err(error)) => {
|
||||
writeln!(output, "Resolution: Error\n{:?}", error).unwrap();
|
||||
}
|
||||
}
|
||||
@ -2485,7 +2481,7 @@ impl ContextEditor {
|
||||
return;
|
||||
};
|
||||
|
||||
let resolved_step = step.status.into_resolved();
|
||||
let resolved_step = step.resolution.read(cx).result.clone();
|
||||
if let Some(existing_step) = self.workflow_steps.get_mut(&step_range) {
|
||||
existing_step.resolved_step = resolved_step;
|
||||
if let Some(debug) = self.debug_inspector.as_mut() {
|
||||
|
@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
prompts::PromptBuilder, slash_command::SlashCommandLine, AssistantPanel, InlineAssistId,
|
||||
InlineAssistant, MessageId, MessageStatus,
|
||||
prompts::PromptBuilder, slash_command::SlashCommandLine, workflow::WorkflowStepResolution,
|
||||
MessageId, MessageStatus,
|
||||
};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use assistant_slash_command::{
|
||||
@ -9,34 +9,25 @@ use assistant_slash_command::{
|
||||
use client::{self, proto, telemetry::Telemetry};
|
||||
use clock::ReplicaId;
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::Editor;
|
||||
use fs::{Fs, RemoveOptions};
|
||||
use futures::{
|
||||
future::{self, Shared},
|
||||
stream::FuturesUnordered,
|
||||
FutureExt, StreamExt,
|
||||
};
|
||||
use futures::{future::Shared, stream::FuturesUnordered, FutureExt, StreamExt};
|
||||
use gpui::{
|
||||
AppContext, Context as _, EventEmitter, Image, Model, ModelContext, RenderImage, Subscription,
|
||||
Task, UpdateGlobal, View, WeakView,
|
||||
AppContext, Context as _, EventEmitter, Image, Model, ModelContext, RenderImage, SharedString,
|
||||
Subscription, Task,
|
||||
};
|
||||
|
||||
use language::{
|
||||
AnchorRangeExt, Bias, Buffer, BufferSnapshot, LanguageRegistry, OffsetRangeExt, ParseStatus,
|
||||
Point, ToOffset,
|
||||
};
|
||||
use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, Point, ToOffset};
|
||||
use language_model::{
|
||||
LanguageModelImage, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||
LanguageModelTool, Role,
|
||||
Role,
|
||||
};
|
||||
use open_ai::Model as OpenAiModel;
|
||||
use paths::{context_images_dir, contexts_dir};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
cmp::{self, Ordering},
|
||||
cmp::Ordering,
|
||||
collections::hash_map,
|
||||
fmt::Debug,
|
||||
iter, mem,
|
||||
@ -46,10 +37,8 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use telemetry_events::AssistantKind;
|
||||
use ui::{SharedString, WindowContext};
|
||||
use util::{post_inc, ResultExt, TryFutureExt};
|
||||
use uuid::Uuid;
|
||||
use workspace::Workspace;
|
||||
|
||||
#[derive(Clone, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
pub struct ContextId(String);
|
||||
@ -408,250 +397,17 @@ struct PendingCompletion {
|
||||
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
|
||||
pub struct SlashCommandId(clock::Lamport);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WorkflowStep {
|
||||
pub tagged_range: Range<language::Anchor>,
|
||||
pub status: WorkflowStepStatus,
|
||||
pub resolution: Model<WorkflowStepResolution>,
|
||||
pub _task: Option<Task<()>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ResolvedWorkflowStep {
|
||||
pub title: String,
|
||||
pub suggestions: HashMap<Model<Buffer>, Vec<WorkflowSuggestionGroup>>,
|
||||
}
|
||||
|
||||
pub enum WorkflowStepStatus {
|
||||
Pending(Task<Option<()>>),
|
||||
Resolved(ResolvedWorkflowStep),
|
||||
Error(Arc<anyhow::Error>),
|
||||
}
|
||||
|
||||
impl WorkflowStepStatus {
|
||||
pub fn into_resolved(&self) -> Option<Result<ResolvedWorkflowStep, Arc<anyhow::Error>>> {
|
||||
match self {
|
||||
WorkflowStepStatus::Resolved(resolved) => Some(Ok(resolved.clone())),
|
||||
WorkflowStepStatus::Error(error) => Some(Err(error.clone())),
|
||||
WorkflowStepStatus::Pending(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct WorkflowSuggestionGroup {
|
||||
pub context_range: Range<language::Anchor>,
|
||||
pub suggestions: Vec<WorkflowSuggestion>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum WorkflowSuggestion {
|
||||
Update {
|
||||
range: Range<language::Anchor>,
|
||||
description: String,
|
||||
},
|
||||
CreateFile {
|
||||
description: String,
|
||||
},
|
||||
InsertSiblingBefore {
|
||||
position: language::Anchor,
|
||||
description: String,
|
||||
},
|
||||
InsertSiblingAfter {
|
||||
position: language::Anchor,
|
||||
description: String,
|
||||
},
|
||||
PrependChild {
|
||||
position: language::Anchor,
|
||||
description: String,
|
||||
},
|
||||
AppendChild {
|
||||
position: language::Anchor,
|
||||
description: String,
|
||||
},
|
||||
Delete {
|
||||
range: Range<language::Anchor>,
|
||||
},
|
||||
}
|
||||
|
||||
impl WorkflowSuggestion {
|
||||
pub fn range(&self) -> Range<language::Anchor> {
|
||||
match self {
|
||||
WorkflowSuggestion::Update { range, .. } => range.clone(),
|
||||
WorkflowSuggestion::CreateFile { .. } => language::Anchor::MIN..language::Anchor::MAX,
|
||||
WorkflowSuggestion::InsertSiblingBefore { position, .. }
|
||||
| WorkflowSuggestion::InsertSiblingAfter { position, .. }
|
||||
| WorkflowSuggestion::PrependChild { position, .. }
|
||||
| WorkflowSuggestion::AppendChild { position, .. } => *position..*position,
|
||||
WorkflowSuggestion::Delete { range } => range.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn description(&self) -> Option<&str> {
|
||||
match self {
|
||||
WorkflowSuggestion::Update { description, .. }
|
||||
| WorkflowSuggestion::CreateFile { description }
|
||||
| WorkflowSuggestion::InsertSiblingBefore { description, .. }
|
||||
| WorkflowSuggestion::InsertSiblingAfter { description, .. }
|
||||
| WorkflowSuggestion::PrependChild { description, .. }
|
||||
| WorkflowSuggestion::AppendChild { description, .. } => Some(description),
|
||||
WorkflowSuggestion::Delete { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn description_mut(&mut self) -> Option<&mut String> {
|
||||
match self {
|
||||
WorkflowSuggestion::Update { description, .. }
|
||||
| WorkflowSuggestion::CreateFile { description }
|
||||
| WorkflowSuggestion::InsertSiblingBefore { description, .. }
|
||||
| WorkflowSuggestion::InsertSiblingAfter { description, .. }
|
||||
| WorkflowSuggestion::PrependChild { description, .. }
|
||||
| WorkflowSuggestion::AppendChild { description, .. } => Some(description),
|
||||
WorkflowSuggestion::Delete { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn try_merge(&mut self, other: &Self, buffer: &BufferSnapshot) -> bool {
|
||||
let range = self.range();
|
||||
let other_range = other.range();
|
||||
|
||||
// Don't merge if we don't contain the other suggestion.
|
||||
if range.start.cmp(&other_range.start, buffer).is_gt()
|
||||
|| range.end.cmp(&other_range.end, buffer).is_lt()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(description) = self.description_mut() {
|
||||
if let Some(other_description) = other.description() {
|
||||
description.push('\n');
|
||||
description.push_str(other_description);
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn show(
|
||||
&self,
|
||||
editor: &View<Editor>,
|
||||
excerpt_id: editor::ExcerptId,
|
||||
workspace: &WeakView<Workspace>,
|
||||
assistant_panel: &View<AssistantPanel>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<InlineAssistId> {
|
||||
let mut initial_transaction_id = None;
|
||||
let initial_prompt;
|
||||
let suggestion_range;
|
||||
let buffer = editor.read(cx).buffer().clone();
|
||||
let snapshot = buffer.read(cx).snapshot(cx);
|
||||
|
||||
match self {
|
||||
WorkflowSuggestion::Update { range, description } => {
|
||||
initial_prompt = description.clone();
|
||||
suggestion_range = snapshot.anchor_in_excerpt(excerpt_id, range.start)?
|
||||
..snapshot.anchor_in_excerpt(excerpt_id, range.end)?;
|
||||
}
|
||||
WorkflowSuggestion::CreateFile { description } => {
|
||||
initial_prompt = description.clone();
|
||||
suggestion_range = editor::Anchor::min()..editor::Anchor::min();
|
||||
}
|
||||
WorkflowSuggestion::InsertSiblingBefore {
|
||||
position,
|
||||
description,
|
||||
} => {
|
||||
let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
|
||||
initial_prompt = description.clone();
|
||||
suggestion_range = buffer.update(cx, |buffer, cx| {
|
||||
buffer.start_transaction(cx);
|
||||
let line_start = buffer.insert_empty_line(position, true, true, cx);
|
||||
initial_transaction_id = buffer.end_transaction(cx);
|
||||
buffer.refresh_preview(cx);
|
||||
|
||||
let line_start = buffer.read(cx).anchor_before(line_start);
|
||||
line_start..line_start
|
||||
});
|
||||
}
|
||||
WorkflowSuggestion::InsertSiblingAfter {
|
||||
position,
|
||||
description,
|
||||
} => {
|
||||
let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
|
||||
initial_prompt = description.clone();
|
||||
suggestion_range = buffer.update(cx, |buffer, cx| {
|
||||
buffer.start_transaction(cx);
|
||||
let line_start = buffer.insert_empty_line(position, true, true, cx);
|
||||
initial_transaction_id = buffer.end_transaction(cx);
|
||||
buffer.refresh_preview(cx);
|
||||
|
||||
let line_start = buffer.read(cx).anchor_before(line_start);
|
||||
line_start..line_start
|
||||
});
|
||||
}
|
||||
WorkflowSuggestion::PrependChild {
|
||||
position,
|
||||
description,
|
||||
} => {
|
||||
let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
|
||||
initial_prompt = description.clone();
|
||||
suggestion_range = buffer.update(cx, |buffer, cx| {
|
||||
buffer.start_transaction(cx);
|
||||
let line_start = buffer.insert_empty_line(position, false, true, cx);
|
||||
initial_transaction_id = buffer.end_transaction(cx);
|
||||
buffer.refresh_preview(cx);
|
||||
|
||||
let line_start = buffer.read(cx).anchor_before(line_start);
|
||||
line_start..line_start
|
||||
});
|
||||
}
|
||||
WorkflowSuggestion::AppendChild {
|
||||
position,
|
||||
description,
|
||||
} => {
|
||||
let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
|
||||
initial_prompt = description.clone();
|
||||
suggestion_range = buffer.update(cx, |buffer, cx| {
|
||||
buffer.start_transaction(cx);
|
||||
let line_start = buffer.insert_empty_line(position, true, false, cx);
|
||||
initial_transaction_id = buffer.end_transaction(cx);
|
||||
buffer.refresh_preview(cx);
|
||||
|
||||
let line_start = buffer.read(cx).anchor_before(line_start);
|
||||
line_start..line_start
|
||||
});
|
||||
}
|
||||
WorkflowSuggestion::Delete { range } => {
|
||||
initial_prompt = "Delete".to_string();
|
||||
suggestion_range = snapshot.anchor_in_excerpt(excerpt_id, range.start)?
|
||||
..snapshot.anchor_in_excerpt(excerpt_id, range.end)?;
|
||||
}
|
||||
}
|
||||
|
||||
InlineAssistant::update_global(cx, |inline_assistant, cx| {
|
||||
Some(inline_assistant.suggest_assist(
|
||||
editor,
|
||||
suggestion_range,
|
||||
initial_prompt,
|
||||
initial_transaction_id,
|
||||
Some(workspace.clone()),
|
||||
Some(assistant_panel),
|
||||
cx,
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for WorkflowStepStatus {
|
||||
impl Debug for WorkflowStep {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
WorkflowStepStatus::Pending(_) => write!(f, "WorkflowStepStatus::Pending"),
|
||||
WorkflowStepStatus::Resolved(ResolvedWorkflowStep { title, suggestions }) => f
|
||||
.debug_struct("WorkflowStepStatus::Resolved")
|
||||
.field("title", title)
|
||||
.field("suggestions", suggestions)
|
||||
.finish(),
|
||||
WorkflowStepStatus::Error(error) => f
|
||||
.debug_tuple("WorkflowStepStatus::Error")
|
||||
.field(error)
|
||||
.finish(),
|
||||
}
|
||||
f.debug_struct("WorkflowStep")
|
||||
.field("tagged_range", &self.tagged_range)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
@ -1082,6 +838,14 @@ impl Context {
|
||||
&self.buffer
|
||||
}
|
||||
|
||||
pub fn project(&self) -> Option<Model<Project>> {
|
||||
self.project.clone()
|
||||
}
|
||||
|
||||
pub fn prompt_builder(&self) -> Arc<PromptBuilder> {
|
||||
self.prompt_builder.clone()
|
||||
}
|
||||
|
||||
pub fn path(&self) -> Option<&Path> {
|
||||
self.path.as_deref()
|
||||
}
|
||||
@ -1308,12 +1072,7 @@ impl Context {
|
||||
start_ix..end_ix
|
||||
}
|
||||
|
||||
fn parse_workflow_steps_in_range(
|
||||
&mut self,
|
||||
range: Range<usize>,
|
||||
project: Model<Project>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
fn parse_workflow_steps_in_range(&mut self, range: Range<usize>, cx: &mut ModelContext<Self>) {
|
||||
let mut new_edit_steps = Vec::new();
|
||||
let mut edits = Vec::new();
|
||||
|
||||
@ -1356,8 +1115,11 @@ impl Context {
|
||||
new_edit_steps.push((
|
||||
ix,
|
||||
WorkflowStep {
|
||||
resolution: cx.new_model(|_| {
|
||||
WorkflowStepResolution::new(tagged_range.clone())
|
||||
}),
|
||||
tagged_range,
|
||||
status: WorkflowStepStatus::Pending(Task::ready(None)),
|
||||
_task: None,
|
||||
},
|
||||
));
|
||||
}
|
||||
@ -1374,7 +1136,7 @@ impl Context {
|
||||
let step_range = step.tagged_range.clone();
|
||||
updated.push(step_range.clone());
|
||||
self.workflow_steps.insert(index, step);
|
||||
self.resolve_workflow_step(step_range, project.clone(), cx);
|
||||
self.resolve_workflow_step(step_range, cx);
|
||||
}
|
||||
|
||||
// Delete <step> tags, making sure we don't accidentally invalidate
|
||||
@ -1387,7 +1149,6 @@ impl Context {
|
||||
pub fn resolve_workflow_step(
|
||||
&mut self,
|
||||
tagged_range: Range<language::Anchor>,
|
||||
project: Model<Project>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let Ok(step_index) = self
|
||||
@ -1397,152 +1158,22 @@ impl Context {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut request = self.to_completion_request(cx);
|
||||
let Some(edit_step) = self.workflow_steps.get_mut(step_index) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
|
||||
let step_text = self
|
||||
.buffer
|
||||
.read(cx)
|
||||
.text_for_range(tagged_range.clone())
|
||||
.collect::<String>();
|
||||
|
||||
let tagged_range = tagged_range.clone();
|
||||
edit_step.status = WorkflowStepStatus::Pending(cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
let result = async {
|
||||
let mut prompt = this.update(&mut cx, |this, _| {
|
||||
this.prompt_builder.generate_step_resolution_prompt()
|
||||
})??;
|
||||
prompt.push_str(&step_text);
|
||||
|
||||
request.messages.push(LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![prompt.into()],
|
||||
});
|
||||
|
||||
// Invoke the model to get its edit suggestions for this workflow step.
|
||||
let resolution = model
|
||||
.use_tool::<tool::WorkflowStepResolution>(request, &cx)
|
||||
.await?;
|
||||
|
||||
// Translate the parsed suggestions to our internal types, which anchor the suggestions to locations in the code.
|
||||
let suggestion_tasks: Vec<_> = resolution
|
||||
.suggestions
|
||||
.iter()
|
||||
.map(|suggestion| suggestion.resolve(project.clone(), cx.clone()))
|
||||
.collect();
|
||||
|
||||
// Expand the context ranges of each suggestion and group suggestions with overlapping context ranges.
|
||||
let suggestions = future::join_all(suggestion_tasks)
|
||||
.await
|
||||
.into_iter()
|
||||
.filter_map(|task| task.log_err())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut suggestions_by_buffer = HashMap::default();
|
||||
for (buffer, suggestion) in suggestions {
|
||||
suggestions_by_buffer
|
||||
.entry(buffer)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(suggestion);
|
||||
}
|
||||
|
||||
let mut suggestion_groups_by_buffer = HashMap::default();
|
||||
for (buffer, mut suggestions) in suggestions_by_buffer {
|
||||
let mut suggestion_groups = Vec::<WorkflowSuggestionGroup>::new();
|
||||
let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?;
|
||||
// Sort suggestions by their range so that earlier, larger ranges come first
|
||||
suggestions.sort_by(|a, b| a.range().cmp(&b.range(), &snapshot));
|
||||
|
||||
// Merge overlapping suggestions
|
||||
suggestions.dedup_by(|a, b| b.try_merge(&a, &snapshot));
|
||||
|
||||
// Create context ranges for each suggestion
|
||||
for suggestion in suggestions {
|
||||
let context_range = {
|
||||
let suggestion_point_range =
|
||||
suggestion.range().to_point(&snapshot);
|
||||
let start_row =
|
||||
suggestion_point_range.start.row.saturating_sub(5);
|
||||
let end_row = cmp::min(
|
||||
suggestion_point_range.end.row + 5,
|
||||
snapshot.max_point().row,
|
||||
);
|
||||
let start = snapshot.anchor_before(Point::new(start_row, 0));
|
||||
let end = snapshot.anchor_after(Point::new(
|
||||
end_row,
|
||||
snapshot.line_len(end_row),
|
||||
));
|
||||
start..end
|
||||
};
|
||||
|
||||
if let Some(last_group) = suggestion_groups.last_mut() {
|
||||
if last_group
|
||||
.context_range
|
||||
.end
|
||||
.cmp(&context_range.start, &snapshot)
|
||||
.is_ge()
|
||||
{
|
||||
// Merge with the previous group if context ranges overlap
|
||||
last_group.context_range.end = context_range.end;
|
||||
last_group.suggestions.push(suggestion);
|
||||
} else {
|
||||
// Create a new group
|
||||
suggestion_groups.push(WorkflowSuggestionGroup {
|
||||
context_range,
|
||||
suggestions: vec![suggestion],
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Create the first group
|
||||
suggestion_groups.push(WorkflowSuggestionGroup {
|
||||
context_range,
|
||||
suggestions: vec![suggestion],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
suggestion_groups_by_buffer.insert(buffer, suggestion_groups);
|
||||
}
|
||||
|
||||
Ok((resolution.step_title, suggestion_groups_by_buffer))
|
||||
};
|
||||
|
||||
let result = result.await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let step_index = this
|
||||
.workflow_steps
|
||||
.binary_search_by(|step| {
|
||||
step.tagged_range.cmp(&tagged_range, this.buffer.read(cx))
|
||||
})
|
||||
.map_err(|_| anyhow!("edit step not found"))?;
|
||||
if let Some(edit_step) = this.workflow_steps.get_mut(step_index) {
|
||||
edit_step.status = match result {
|
||||
Ok((title, suggestions)) => {
|
||||
WorkflowStepStatus::Resolved(ResolvedWorkflowStep {
|
||||
title,
|
||||
suggestions,
|
||||
})
|
||||
}
|
||||
Err(error) => WorkflowStepStatus::Error(Arc::new(error)),
|
||||
};
|
||||
cx.emit(ContextEvent::WorkflowStepUpdated(tagged_range));
|
||||
cx.notify();
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})?
|
||||
}
|
||||
.log_err()
|
||||
}));
|
||||
} else {
|
||||
edit_step.status = WorkflowStepStatus::Error(Arc::new(anyhow!("no active model")));
|
||||
}
|
||||
|
||||
cx.emit(ContextEvent::WorkflowStepUpdated(tagged_range));
|
||||
cx.emit(ContextEvent::WorkflowStepUpdated(tagged_range.clone()));
|
||||
cx.notify();
|
||||
|
||||
let task = self.workflow_steps[step_index]
|
||||
.resolution
|
||||
.update(cx, |resolution, cx| resolution.resolve(self, cx));
|
||||
self.workflow_steps[step_index]._task = task.map(|task| {
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
task.await;
|
||||
this.update(&mut cx, |_, cx| {
|
||||
cx.emit(ContextEvent::WorkflowStepUpdated(tagged_range));
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
pub fn pending_command_for_position(
|
||||
@ -1762,11 +1393,10 @@ impl Context {
|
||||
);
|
||||
message_start_offset..message_new_end_offset
|
||||
});
|
||||
if let Some(project) = this.project.clone() {
|
||||
// Use `inclusive = false` as edits might occur at the end of a parsed step.
|
||||
this.prune_invalid_workflow_steps(false, cx);
|
||||
this.parse_workflow_steps_in_range(message_range, project, cx);
|
||||
}
|
||||
|
||||
// Use `inclusive = false` as edits might occur at the end of a parsed step.
|
||||
this.prune_invalid_workflow_steps(false, cx);
|
||||
this.parse_workflow_steps_in_range(message_range, cx);
|
||||
cx.emit(ContextEvent::StreamedCompletion);
|
||||
|
||||
Some(())
|
||||
@ -2752,7 +2382,9 @@ pub struct SavedContextMetadata {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{assistant_panel, prompt_library, slash_command::file_command, MessageId};
|
||||
use crate::{
|
||||
assistant_panel, prompt_library, slash_command::file_command, workflow::tool, MessageId,
|
||||
};
|
||||
use assistant_slash_command::{ArgumentCompletion, SlashCommand};
|
||||
use fs::FakeFs;
|
||||
use gpui::{AppContext, TestAppContext, WeakView};
|
||||
@ -3392,10 +3024,10 @@ mod tests {
|
||||
.iter()
|
||||
.map(|step| {
|
||||
let buffer = context.buffer.read(cx);
|
||||
let status = match &step.status {
|
||||
WorkflowStepStatus::Pending(_) => WorkflowStepTestStatus::Pending,
|
||||
WorkflowStepStatus::Resolved { .. } => WorkflowStepTestStatus::Resolved,
|
||||
WorkflowStepStatus::Error(_) => WorkflowStepTestStatus::Error,
|
||||
let status = match &step.resolution.read(cx).result {
|
||||
None => WorkflowStepTestStatus::Pending,
|
||||
Some(Ok(_)) => WorkflowStepTestStatus::Resolved,
|
||||
Some(Err(_)) => WorkflowStepTestStatus::Error,
|
||||
};
|
||||
(step.tagged_range.to_point(buffer), status)
|
||||
})
|
||||
@ -3798,289 +3430,3 @@ mod tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod tool {
|
||||
use gpui::AsyncAppContext;
|
||||
use project::ProjectPath;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct WorkflowStepResolution {
|
||||
/// An extremely short title for the edit step represented by these operations.
|
||||
pub step_title: String,
|
||||
/// A sequence of operations to apply to the codebase.
|
||||
/// When multiple operations are required for a step, be sure to include multiple operations in this list.
|
||||
pub suggestions: Vec<WorkflowSuggestion>,
|
||||
}
|
||||
|
||||
impl LanguageModelTool for WorkflowStepResolution {
|
||||
fn name() -> String {
|
||||
"edit".into()
|
||||
}
|
||||
|
||||
fn description() -> String {
|
||||
"suggest edits to one or more locations in the codebase".into()
|
||||
}
|
||||
}
|
||||
|
||||
/// A description of an operation to apply to one location in the codebase.
|
||||
///
|
||||
/// This object represents a single edit operation that can be performed on a specific file
|
||||
/// in the codebase. It encapsulates both the location (file path) and the nature of the
|
||||
/// edit to be made.
|
||||
///
|
||||
/// # Fields
|
||||
///
|
||||
/// * `path`: A string representing the file path where the edit operation should be applied.
|
||||
/// This path is relative to the root of the project or repository.
|
||||
///
|
||||
/// * `kind`: An enum representing the specific type of edit operation to be performed.
|
||||
///
|
||||
/// # Usage
|
||||
///
|
||||
/// `EditOperation` is used within a code editor to represent and apply
|
||||
/// programmatic changes to source code. It provides a structured way to describe
|
||||
/// edits for features like refactoring tools or AI-assisted coding suggestions.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct WorkflowSuggestion {
|
||||
/// The path to the file containing the relevant operation
|
||||
pub path: String,
|
||||
#[serde(flatten)]
|
||||
pub kind: WorkflowSuggestionKind,
|
||||
}
|
||||
|
||||
impl WorkflowSuggestion {
|
||||
pub(super) async fn resolve(
|
||||
&self,
|
||||
project: Model<Project>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<(Model<Buffer>, super::WorkflowSuggestion)> {
|
||||
let path = self.path.clone();
|
||||
let kind = self.kind.clone();
|
||||
let buffer = project
|
||||
.update(&mut cx, |project, cx| {
|
||||
let project_path = project
|
||||
.find_project_path(Path::new(&path), cx)
|
||||
.or_else(|| {
|
||||
// If we couldn't find a project path for it, put it in the active worktree
|
||||
// so that when we create the buffer, it can be saved.
|
||||
let worktree = project
|
||||
.active_entry()
|
||||
.and_then(|entry_id| project.worktree_for_entry(entry_id, cx))
|
||||
.or_else(|| project.worktrees(cx).next())?;
|
||||
let worktree = worktree.read(cx);
|
||||
|
||||
Some(ProjectPath {
|
||||
worktree_id: worktree.id(),
|
||||
path: Arc::from(Path::new(&path)),
|
||||
})
|
||||
})
|
||||
.with_context(|| format!("worktree not found for {:?}", path))?;
|
||||
anyhow::Ok(project.open_buffer(project_path, cx))
|
||||
})??
|
||||
.await?;
|
||||
|
||||
let mut parse_status = buffer.read_with(&cx, |buffer, _cx| buffer.parse_status())?;
|
||||
while *parse_status.borrow() != ParseStatus::Idle {
|
||||
parse_status.changed().await?;
|
||||
}
|
||||
|
||||
let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?;
|
||||
let outline = snapshot.outline(None).context("no outline for buffer")?;
|
||||
|
||||
let suggestion;
|
||||
match kind {
|
||||
WorkflowSuggestionKind::Update {
|
||||
symbol,
|
||||
description,
|
||||
} => {
|
||||
let symbol = outline
|
||||
.find_most_similar(&symbol)
|
||||
.with_context(|| format!("symbol not found: {:?}", symbol))?
|
||||
.to_point(&snapshot);
|
||||
let start = symbol
|
||||
.annotation_range
|
||||
.map_or(symbol.range.start, |range| range.start);
|
||||
let start = Point::new(start.row, 0);
|
||||
let end = Point::new(
|
||||
symbol.range.end.row,
|
||||
snapshot.line_len(symbol.range.end.row),
|
||||
);
|
||||
let range = snapshot.anchor_before(start)..snapshot.anchor_after(end);
|
||||
suggestion = super::WorkflowSuggestion::Update { range, description };
|
||||
}
|
||||
WorkflowSuggestionKind::Create { description } => {
|
||||
suggestion = super::WorkflowSuggestion::CreateFile { description };
|
||||
}
|
||||
WorkflowSuggestionKind::InsertSiblingBefore {
|
||||
symbol,
|
||||
description,
|
||||
} => {
|
||||
let symbol = outline
|
||||
.find_most_similar(&symbol)
|
||||
.with_context(|| format!("symbol not found: {:?}", symbol))?
|
||||
.to_point(&snapshot);
|
||||
let position = snapshot.anchor_before(
|
||||
symbol
|
||||
.annotation_range
|
||||
.map_or(symbol.range.start, |annotation_range| {
|
||||
annotation_range.start
|
||||
}),
|
||||
);
|
||||
suggestion = super::WorkflowSuggestion::InsertSiblingBefore {
|
||||
position,
|
||||
description,
|
||||
};
|
||||
}
|
||||
WorkflowSuggestionKind::InsertSiblingAfter {
|
||||
symbol,
|
||||
description,
|
||||
} => {
|
||||
let symbol = outline
|
||||
.find_most_similar(&symbol)
|
||||
.with_context(|| format!("symbol not found: {:?}", symbol))?
|
||||
.to_point(&snapshot);
|
||||
let position = snapshot.anchor_after(symbol.range.end);
|
||||
suggestion = super::WorkflowSuggestion::InsertSiblingAfter {
|
||||
position,
|
||||
description,
|
||||
};
|
||||
}
|
||||
WorkflowSuggestionKind::PrependChild {
|
||||
symbol,
|
||||
description,
|
||||
} => {
|
||||
if let Some(symbol) = symbol {
|
||||
let symbol = outline
|
||||
.find_most_similar(&symbol)
|
||||
.with_context(|| format!("symbol not found: {:?}", symbol))?
|
||||
.to_point(&snapshot);
|
||||
|
||||
let position = snapshot.anchor_after(
|
||||
symbol
|
||||
.body_range
|
||||
.map_or(symbol.range.start, |body_range| body_range.start),
|
||||
);
|
||||
suggestion = super::WorkflowSuggestion::PrependChild {
|
||||
position,
|
||||
description,
|
||||
};
|
||||
} else {
|
||||
suggestion = super::WorkflowSuggestion::PrependChild {
|
||||
position: language::Anchor::MIN,
|
||||
description,
|
||||
};
|
||||
}
|
||||
}
|
||||
WorkflowSuggestionKind::AppendChild {
|
||||
symbol,
|
||||
description,
|
||||
} => {
|
||||
if let Some(symbol) = symbol {
|
||||
let symbol = outline
|
||||
.find_most_similar(&symbol)
|
||||
.with_context(|| format!("symbol not found: {:?}", symbol))?
|
||||
.to_point(&snapshot);
|
||||
|
||||
let position = snapshot.anchor_before(
|
||||
symbol
|
||||
.body_range
|
||||
.map_or(symbol.range.end, |body_range| body_range.end),
|
||||
);
|
||||
suggestion = super::WorkflowSuggestion::AppendChild {
|
||||
position,
|
||||
description,
|
||||
};
|
||||
} else {
|
||||
suggestion = super::WorkflowSuggestion::PrependChild {
|
||||
position: language::Anchor::MAX,
|
||||
description,
|
||||
};
|
||||
}
|
||||
}
|
||||
WorkflowSuggestionKind::Delete { symbol } => {
|
||||
let symbol = outline
|
||||
.find_most_similar(&symbol)
|
||||
.with_context(|| format!("symbol not found: {:?}", symbol))?
|
||||
.to_point(&snapshot);
|
||||
let start = symbol
|
||||
.annotation_range
|
||||
.map_or(symbol.range.start, |range| range.start);
|
||||
let start = Point::new(start.row, 0);
|
||||
let end = Point::new(
|
||||
symbol.range.end.row,
|
||||
snapshot.line_len(symbol.range.end.row),
|
||||
);
|
||||
let range = snapshot.anchor_before(start)..snapshot.anchor_after(end);
|
||||
suggestion = super::WorkflowSuggestion::Delete { range };
|
||||
}
|
||||
}
|
||||
|
||||
Ok((buffer, suggestion))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum WorkflowSuggestionKind {
|
||||
/// Rewrites the specified symbol entirely based on the given description.
|
||||
/// This operation completely replaces the existing symbol with new content.
|
||||
Update {
|
||||
/// A fully-qualified reference to the symbol, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`.
|
||||
/// The path should uniquely identify the symbol within the containing file.
|
||||
symbol: String,
|
||||
/// A brief description of the transformation to apply to the symbol.
|
||||
description: String,
|
||||
},
|
||||
/// Creates a new file with the given path based on the provided description.
|
||||
/// This operation adds a new file to the codebase.
|
||||
Create {
|
||||
/// A brief description of the file to be created.
|
||||
description: String,
|
||||
},
|
||||
/// Inserts a new symbol based on the given description before the specified symbol.
|
||||
/// This operation adds new content immediately preceding an existing symbol.
|
||||
InsertSiblingBefore {
|
||||
/// A fully-qualified reference to the symbol, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`.
|
||||
/// The new content will be inserted immediately before this symbol.
|
||||
symbol: String,
|
||||
/// A brief description of the new symbol to be inserted.
|
||||
description: String,
|
||||
},
|
||||
/// Inserts a new symbol based on the given description after the specified symbol.
|
||||
/// This operation adds new content immediately following an existing symbol.
|
||||
InsertSiblingAfter {
|
||||
/// A fully-qualified reference to the symbol, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`.
|
||||
/// The new content will be inserted immediately after this symbol.
|
||||
symbol: String,
|
||||
/// A brief description of the new symbol to be inserted.
|
||||
description: String,
|
||||
},
|
||||
/// Inserts a new symbol as a child of the specified symbol at the start.
|
||||
/// This operation adds new content as the first child of an existing symbol (or file if no symbol is provided).
|
||||
PrependChild {
|
||||
/// An optional fully-qualified reference to the symbol after the code you want to insert, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`.
|
||||
/// If provided, the new content will be inserted as the first child of this symbol.
|
||||
/// If not provided, the new content will be inserted at the top of the file.
|
||||
symbol: Option<String>,
|
||||
/// A brief description of the new symbol to be inserted.
|
||||
description: String,
|
||||
},
|
||||
/// Inserts a new symbol as a child of the specified symbol at the end.
|
||||
/// This operation adds new content as the last child of an existing symbol (or file if no symbol is provided).
|
||||
AppendChild {
|
||||
/// An optional fully-qualified reference to the symbol before the code you want to insert, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`.
|
||||
/// If provided, the new content will be inserted as the last child of this symbol.
|
||||
/// If not provided, the new content will be applied at the bottom of the file.
|
||||
symbol: Option<String>,
|
||||
/// A brief description of the new symbol to be inserted.
|
||||
description: String,
|
||||
},
|
||||
/// Deletes the specified symbol from the containing file.
|
||||
Delete {
|
||||
/// An fully-qualified reference to the symbol to be deleted, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`.
|
||||
symbol: String,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -44,6 +44,7 @@ impl ContextInspector {
|
||||
self.activate_for_step(range.clone(), cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn crease_content(
|
||||
context: &Model<Context>,
|
||||
range: StepRange,
|
||||
@ -52,8 +53,8 @@ impl ContextInspector {
|
||||
use std::fmt::Write;
|
||||
let step = context.read(cx).workflow_step_for_range(range)?;
|
||||
let mut output = String::from("\n\n");
|
||||
match &step.status {
|
||||
crate::WorkflowStepStatus::Resolved(ResolvedWorkflowStep { title, suggestions }) => {
|
||||
match &step.resolution.read(cx).result {
|
||||
Some(Ok(ResolvedWorkflowStep { title, suggestions })) => {
|
||||
writeln!(output, "Resolution:").ok()?;
|
||||
writeln!(output, " {title:?}").ok()?;
|
||||
if suggestions.is_empty() {
|
||||
@ -75,17 +76,18 @@ impl ContextInspector {
|
||||
}
|
||||
}
|
||||
}
|
||||
crate::WorkflowStepStatus::Pending(_) => {
|
||||
writeln!(output, "Resolution: Pending").ok()?;
|
||||
}
|
||||
crate::WorkflowStepStatus::Error(error) => {
|
||||
Some(Err(error)) => {
|
||||
writeln!(output, "Resolution: Error").ok()?;
|
||||
writeln!(output, "{error:?}").ok()?;
|
||||
}
|
||||
None => {
|
||||
writeln!(output, "Resolution: Pending").ok()?;
|
||||
}
|
||||
}
|
||||
|
||||
Some(output.into())
|
||||
}
|
||||
|
||||
pub(crate) fn activate_for_step(&mut self, range: StepRange, cx: &mut WindowContext<'_>) {
|
||||
let text = Self::crease_content(&self.context, range.clone(), cx)
|
||||
.unwrap_or_else(|| Arc::from("Error fetching debug info"));
|
||||
|
672
crates/assistant/src/workflow.rs
Normal file
672
crates/assistant/src/workflow.rs
Normal file
@ -0,0 +1,672 @@
|
||||
use crate::{AssistantPanel, Context, InlineAssistId, InlineAssistant};
|
||||
use anyhow::{anyhow, Error, Result};
|
||||
use collections::HashMap;
|
||||
use editor::Editor;
|
||||
use futures::future;
|
||||
use gpui::{Model, ModelContext, Task, UpdateGlobal as _, View, WeakView, WindowContext};
|
||||
use language::{Anchor, Buffer, BufferSnapshot};
|
||||
use language_model::{LanguageModelRegistry, LanguageModelRequestMessage, Role};
|
||||
use project::Project;
|
||||
use rope::Point;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smol::stream::StreamExt;
|
||||
use std::{cmp, ops::Range, sync::Arc};
|
||||
use text::{AnchorRangeExt as _, OffsetRangeExt as _};
|
||||
use util::ResultExt as _;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub struct WorkflowStepResolution {
|
||||
tagged_range: Range<Anchor>,
|
||||
output: String,
|
||||
pub result: Option<Result<ResolvedWorkflowStep, Arc<Error>>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ResolvedWorkflowStep {
|
||||
pub title: String,
|
||||
pub suggestions: HashMap<Model<Buffer>, Vec<WorkflowSuggestionGroup>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct WorkflowSuggestionGroup {
|
||||
pub context_range: Range<language::Anchor>,
|
||||
pub suggestions: Vec<WorkflowSuggestion>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum WorkflowSuggestion {
|
||||
Update {
|
||||
range: Range<language::Anchor>,
|
||||
description: String,
|
||||
},
|
||||
CreateFile {
|
||||
description: String,
|
||||
},
|
||||
InsertSiblingBefore {
|
||||
position: language::Anchor,
|
||||
description: String,
|
||||
},
|
||||
InsertSiblingAfter {
|
||||
position: language::Anchor,
|
||||
description: String,
|
||||
},
|
||||
PrependChild {
|
||||
position: language::Anchor,
|
||||
description: String,
|
||||
},
|
||||
AppendChild {
|
||||
position: language::Anchor,
|
||||
description: String,
|
||||
},
|
||||
Delete {
|
||||
range: Range<language::Anchor>,
|
||||
},
|
||||
}
|
||||
|
||||
impl WorkflowStepResolution {
|
||||
pub fn new(range: Range<Anchor>) -> Self {
|
||||
Self {
|
||||
tagged_range: range,
|
||||
output: String::new(),
|
||||
result: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
cx: &mut ModelContext<WorkflowStepResolution>,
|
||||
) -> Option<Task<()>> {
|
||||
let project = context.project()?;
|
||||
let context_buffer = context.buffer().clone();
|
||||
let prompt_builder = context.prompt_builder();
|
||||
let mut request = context.to_completion_request(cx);
|
||||
let model = LanguageModelRegistry::read_global(cx).active_model();
|
||||
let step_text = context_buffer
|
||||
.read(cx)
|
||||
.text_for_range(self.tagged_range.clone())
|
||||
.collect::<String>();
|
||||
|
||||
Some(cx.spawn(|this, mut cx| async move {
|
||||
let result = async {
|
||||
let Some(model) = model else {
|
||||
return Err(anyhow!("no model selected"));
|
||||
};
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.output.clear();
|
||||
this.result = None;
|
||||
cx.notify();
|
||||
})?;
|
||||
|
||||
let mut prompt = prompt_builder.generate_step_resolution_prompt()?;
|
||||
prompt.push_str(&step_text);
|
||||
request.messages.push(LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![prompt.into()],
|
||||
});
|
||||
|
||||
// Invoke the model to get its edit suggestions for this workflow step.
|
||||
let mut stream = model
|
||||
.use_tool_stream::<tool::WorkflowStepResolution>(request, &cx)
|
||||
.await?;
|
||||
while let Some(chunk) = stream.next().await {
|
||||
let chunk = chunk?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.output.push_str(&chunk);
|
||||
cx.notify();
|
||||
})?;
|
||||
}
|
||||
|
||||
let resolution = this.update(&mut cx, |this, _| {
|
||||
serde_json::from_str::<tool::WorkflowStepResolution>(&this.output)
|
||||
})??;
|
||||
|
||||
// Translate the parsed suggestions to our internal types, which anchor the suggestions to locations in the code.
|
||||
let suggestion_tasks: Vec<_> = resolution
|
||||
.suggestions
|
||||
.iter()
|
||||
.map(|suggestion| suggestion.resolve(project.clone(), cx.clone()))
|
||||
.collect();
|
||||
|
||||
// Expand the context ranges of each suggestion and group suggestions with overlapping context ranges.
|
||||
let suggestions = future::join_all(suggestion_tasks)
|
||||
.await
|
||||
.into_iter()
|
||||
.filter_map(|task| task.log_err())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut suggestions_by_buffer = HashMap::default();
|
||||
for (buffer, suggestion) in suggestions {
|
||||
suggestions_by_buffer
|
||||
.entry(buffer)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(suggestion);
|
||||
}
|
||||
|
||||
let mut suggestion_groups_by_buffer = HashMap::default();
|
||||
for (buffer, mut suggestions) in suggestions_by_buffer {
|
||||
let mut suggestion_groups = Vec::<WorkflowSuggestionGroup>::new();
|
||||
let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?;
|
||||
// Sort suggestions by their range so that earlier, larger ranges come first
|
||||
suggestions.sort_by(|a, b| a.range().cmp(&b.range(), &snapshot));
|
||||
|
||||
// Merge overlapping suggestions
|
||||
suggestions.dedup_by(|a, b| b.try_merge(&a, &snapshot));
|
||||
|
||||
// Create context ranges for each suggestion
|
||||
for suggestion in suggestions {
|
||||
let context_range = {
|
||||
let suggestion_point_range = suggestion.range().to_point(&snapshot);
|
||||
let start_row = suggestion_point_range.start.row.saturating_sub(5);
|
||||
let end_row = cmp::min(
|
||||
suggestion_point_range.end.row + 5,
|
||||
snapshot.max_point().row,
|
||||
);
|
||||
let start = snapshot.anchor_before(Point::new(start_row, 0));
|
||||
let end = snapshot
|
||||
.anchor_after(Point::new(end_row, snapshot.line_len(end_row)));
|
||||
start..end
|
||||
};
|
||||
|
||||
if let Some(last_group) = suggestion_groups.last_mut() {
|
||||
if last_group
|
||||
.context_range
|
||||
.end
|
||||
.cmp(&context_range.start, &snapshot)
|
||||
.is_ge()
|
||||
{
|
||||
// Merge with the previous group if context ranges overlap
|
||||
last_group.context_range.end = context_range.end;
|
||||
last_group.suggestions.push(suggestion);
|
||||
} else {
|
||||
// Create a new group
|
||||
suggestion_groups.push(WorkflowSuggestionGroup {
|
||||
context_range,
|
||||
suggestions: vec![suggestion],
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Create the first group
|
||||
suggestion_groups.push(WorkflowSuggestionGroup {
|
||||
context_range,
|
||||
suggestions: vec![suggestion],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
suggestion_groups_by_buffer.insert(buffer, suggestion_groups);
|
||||
}
|
||||
|
||||
Ok((resolution.step_title, suggestion_groups_by_buffer))
|
||||
};
|
||||
|
||||
let result = result.await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.result = Some(match result {
|
||||
Ok((title, suggestions)) => Ok(ResolvedWorkflowStep { title, suggestions }),
|
||||
Err(error) => Err(Arc::new(error)),
|
||||
});
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl WorkflowSuggestion {
|
||||
pub fn range(&self) -> Range<language::Anchor> {
|
||||
match self {
|
||||
WorkflowSuggestion::Update { range, .. } => range.clone(),
|
||||
WorkflowSuggestion::CreateFile { .. } => language::Anchor::MIN..language::Anchor::MAX,
|
||||
WorkflowSuggestion::InsertSiblingBefore { position, .. }
|
||||
| WorkflowSuggestion::InsertSiblingAfter { position, .. }
|
||||
| WorkflowSuggestion::PrependChild { position, .. }
|
||||
| WorkflowSuggestion::AppendChild { position, .. } => *position..*position,
|
||||
WorkflowSuggestion::Delete { range } => range.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn description(&self) -> Option<&str> {
|
||||
match self {
|
||||
WorkflowSuggestion::Update { description, .. }
|
||||
| WorkflowSuggestion::CreateFile { description }
|
||||
| WorkflowSuggestion::InsertSiblingBefore { description, .. }
|
||||
| WorkflowSuggestion::InsertSiblingAfter { description, .. }
|
||||
| WorkflowSuggestion::PrependChild { description, .. }
|
||||
| WorkflowSuggestion::AppendChild { description, .. } => Some(description),
|
||||
WorkflowSuggestion::Delete { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn description_mut(&mut self) -> Option<&mut String> {
|
||||
match self {
|
||||
WorkflowSuggestion::Update { description, .. }
|
||||
| WorkflowSuggestion::CreateFile { description }
|
||||
| WorkflowSuggestion::InsertSiblingBefore { description, .. }
|
||||
| WorkflowSuggestion::InsertSiblingAfter { description, .. }
|
||||
| WorkflowSuggestion::PrependChild { description, .. }
|
||||
| WorkflowSuggestion::AppendChild { description, .. } => Some(description),
|
||||
WorkflowSuggestion::Delete { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn try_merge(&mut self, other: &Self, buffer: &BufferSnapshot) -> bool {
|
||||
let range = self.range();
|
||||
let other_range = other.range();
|
||||
|
||||
// Don't merge if we don't contain the other suggestion.
|
||||
if range.start.cmp(&other_range.start, buffer).is_gt()
|
||||
|| range.end.cmp(&other_range.end, buffer).is_lt()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(description) = self.description_mut() {
|
||||
if let Some(other_description) = other.description() {
|
||||
description.push('\n');
|
||||
description.push_str(other_description);
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn show(
|
||||
&self,
|
||||
editor: &View<Editor>,
|
||||
excerpt_id: editor::ExcerptId,
|
||||
workspace: &WeakView<Workspace>,
|
||||
assistant_panel: &View<AssistantPanel>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<InlineAssistId> {
|
||||
let mut initial_transaction_id = None;
|
||||
let initial_prompt;
|
||||
let suggestion_range;
|
||||
let buffer = editor.read(cx).buffer().clone();
|
||||
let snapshot = buffer.read(cx).snapshot(cx);
|
||||
|
||||
match self {
|
||||
WorkflowSuggestion::Update { range, description } => {
|
||||
initial_prompt = description.clone();
|
||||
suggestion_range = snapshot.anchor_in_excerpt(excerpt_id, range.start)?
|
||||
..snapshot.anchor_in_excerpt(excerpt_id, range.end)?;
|
||||
}
|
||||
WorkflowSuggestion::CreateFile { description } => {
|
||||
initial_prompt = description.clone();
|
||||
suggestion_range = editor::Anchor::min()..editor::Anchor::min();
|
||||
}
|
||||
WorkflowSuggestion::InsertSiblingBefore {
|
||||
position,
|
||||
description,
|
||||
} => {
|
||||
let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
|
||||
initial_prompt = description.clone();
|
||||
suggestion_range = buffer.update(cx, |buffer, cx| {
|
||||
buffer.start_transaction(cx);
|
||||
let line_start = buffer.insert_empty_line(position, true, true, cx);
|
||||
initial_transaction_id = buffer.end_transaction(cx);
|
||||
buffer.refresh_preview(cx);
|
||||
|
||||
let line_start = buffer.read(cx).anchor_before(line_start);
|
||||
line_start..line_start
|
||||
});
|
||||
}
|
||||
WorkflowSuggestion::InsertSiblingAfter {
|
||||
position,
|
||||
description,
|
||||
} => {
|
||||
let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
|
||||
initial_prompt = description.clone();
|
||||
suggestion_range = buffer.update(cx, |buffer, cx| {
|
||||
buffer.start_transaction(cx);
|
||||
let line_start = buffer.insert_empty_line(position, true, true, cx);
|
||||
initial_transaction_id = buffer.end_transaction(cx);
|
||||
buffer.refresh_preview(cx);
|
||||
|
||||
let line_start = buffer.read(cx).anchor_before(line_start);
|
||||
line_start..line_start
|
||||
});
|
||||
}
|
||||
WorkflowSuggestion::PrependChild {
|
||||
position,
|
||||
description,
|
||||
} => {
|
||||
let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
|
||||
initial_prompt = description.clone();
|
||||
suggestion_range = buffer.update(cx, |buffer, cx| {
|
||||
buffer.start_transaction(cx);
|
||||
let line_start = buffer.insert_empty_line(position, false, true, cx);
|
||||
initial_transaction_id = buffer.end_transaction(cx);
|
||||
buffer.refresh_preview(cx);
|
||||
|
||||
let line_start = buffer.read(cx).anchor_before(line_start);
|
||||
line_start..line_start
|
||||
});
|
||||
}
|
||||
WorkflowSuggestion::AppendChild {
|
||||
position,
|
||||
description,
|
||||
} => {
|
||||
let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
|
||||
initial_prompt = description.clone();
|
||||
suggestion_range = buffer.update(cx, |buffer, cx| {
|
||||
buffer.start_transaction(cx);
|
||||
let line_start = buffer.insert_empty_line(position, true, false, cx);
|
||||
initial_transaction_id = buffer.end_transaction(cx);
|
||||
buffer.refresh_preview(cx);
|
||||
|
||||
let line_start = buffer.read(cx).anchor_before(line_start);
|
||||
line_start..line_start
|
||||
});
|
||||
}
|
||||
WorkflowSuggestion::Delete { range } => {
|
||||
initial_prompt = "Delete".to_string();
|
||||
suggestion_range = snapshot.anchor_in_excerpt(excerpt_id, range.start)?
|
||||
..snapshot.anchor_in_excerpt(excerpt_id, range.end)?;
|
||||
}
|
||||
}
|
||||
|
||||
InlineAssistant::update_global(cx, |inline_assistant, cx| {
|
||||
Some(inline_assistant.suggest_assist(
|
||||
editor,
|
||||
suggestion_range,
|
||||
initial_prompt,
|
||||
initial_transaction_id,
|
||||
Some(workspace.clone()),
|
||||
Some(assistant_panel),
|
||||
cx,
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub mod tool {
|
||||
use std::path::Path;
|
||||
|
||||
use super::*;
|
||||
use anyhow::Context as _;
|
||||
use gpui::AsyncAppContext;
|
||||
use language::ParseStatus;
|
||||
use language_model::LanguageModelTool;
|
||||
use project::ProjectPath;
|
||||
use schemars::JsonSchema;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct WorkflowStepResolution {
|
||||
/// An extremely short title for the edit step represented by these operations.
|
||||
pub step_title: String,
|
||||
/// A sequence of operations to apply to the codebase.
|
||||
/// When multiple operations are required for a step, be sure to include multiple operations in this list.
|
||||
pub suggestions: Vec<WorkflowSuggestion>,
|
||||
}
|
||||
|
||||
impl LanguageModelTool for WorkflowStepResolution {
|
||||
fn name() -> String {
|
||||
"edit".into()
|
||||
}
|
||||
|
||||
fn description() -> String {
|
||||
"suggest edits to one or more locations in the codebase".into()
|
||||
}
|
||||
}
|
||||
|
||||
/// A description of an operation to apply to one location in the codebase.
|
||||
///
|
||||
/// This object represents a single edit operation that can be performed on a specific file
|
||||
/// in the codebase. It encapsulates both the location (file path) and the nature of the
|
||||
/// edit to be made.
|
||||
///
|
||||
/// # Fields
|
||||
///
|
||||
/// * `path`: A string representing the file path where the edit operation should be applied.
|
||||
/// This path is relative to the root of the project or repository.
|
||||
///
|
||||
/// * `kind`: An enum representing the specific type of edit operation to be performed.
|
||||
///
|
||||
/// # Usage
|
||||
///
|
||||
/// `EditOperation` is used within a code editor to represent and apply
|
||||
/// programmatic changes to source code. It provides a structured way to describe
|
||||
/// edits for features like refactoring tools or AI-assisted coding suggestions.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct WorkflowSuggestion {
|
||||
/// The path to the file containing the relevant operation
|
||||
pub path: String,
|
||||
#[serde(flatten)]
|
||||
pub kind: WorkflowSuggestionKind,
|
||||
}
|
||||
|
||||
impl WorkflowSuggestion {
|
||||
pub(super) async fn resolve(
|
||||
&self,
|
||||
project: Model<Project>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<(Model<Buffer>, super::WorkflowSuggestion)> {
|
||||
let path = self.path.clone();
|
||||
let kind = self.kind.clone();
|
||||
let buffer = project
|
||||
.update(&mut cx, |project, cx| {
|
||||
let project_path = project
|
||||
.find_project_path(Path::new(&path), cx)
|
||||
.or_else(|| {
|
||||
// If we couldn't find a project path for it, put it in the active worktree
|
||||
// so that when we create the buffer, it can be saved.
|
||||
let worktree = project
|
||||
.active_entry()
|
||||
.and_then(|entry_id| project.worktree_for_entry(entry_id, cx))
|
||||
.or_else(|| project.worktrees(cx).next())?;
|
||||
let worktree = worktree.read(cx);
|
||||
|
||||
Some(ProjectPath {
|
||||
worktree_id: worktree.id(),
|
||||
path: Arc::from(Path::new(&path)),
|
||||
})
|
||||
})
|
||||
.with_context(|| format!("worktree not found for {:?}", path))?;
|
||||
anyhow::Ok(project.open_buffer(project_path, cx))
|
||||
})??
|
||||
.await?;
|
||||
|
||||
let mut parse_status = buffer.read_with(&cx, |buffer, _cx| buffer.parse_status())?;
|
||||
while *parse_status.borrow() != ParseStatus::Idle {
|
||||
parse_status.changed().await?;
|
||||
}
|
||||
|
||||
let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?;
|
||||
let outline = snapshot.outline(None).context("no outline for buffer")?;
|
||||
|
||||
let suggestion;
|
||||
match kind {
|
||||
WorkflowSuggestionKind::Update {
|
||||
symbol,
|
||||
description,
|
||||
} => {
|
||||
let symbol = outline
|
||||
.find_most_similar(&symbol)
|
||||
.with_context(|| format!("symbol not found: {:?}", symbol))?
|
||||
.to_point(&snapshot);
|
||||
let start = symbol
|
||||
.annotation_range
|
||||
.map_or(symbol.range.start, |range| range.start);
|
||||
let start = Point::new(start.row, 0);
|
||||
let end = Point::new(
|
||||
symbol.range.end.row,
|
||||
snapshot.line_len(symbol.range.end.row),
|
||||
);
|
||||
let range = snapshot.anchor_before(start)..snapshot.anchor_after(end);
|
||||
suggestion = super::WorkflowSuggestion::Update { range, description };
|
||||
}
|
||||
WorkflowSuggestionKind::Create { description } => {
|
||||
suggestion = super::WorkflowSuggestion::CreateFile { description };
|
||||
}
|
||||
WorkflowSuggestionKind::InsertSiblingBefore {
|
||||
symbol,
|
||||
description,
|
||||
} => {
|
||||
let symbol = outline
|
||||
.find_most_similar(&symbol)
|
||||
.with_context(|| format!("symbol not found: {:?}", symbol))?
|
||||
.to_point(&snapshot);
|
||||
let position = snapshot.anchor_before(
|
||||
symbol
|
||||
.annotation_range
|
||||
.map_or(symbol.range.start, |annotation_range| {
|
||||
annotation_range.start
|
||||
}),
|
||||
);
|
||||
suggestion = super::WorkflowSuggestion::InsertSiblingBefore {
|
||||
position,
|
||||
description,
|
||||
};
|
||||
}
|
||||
WorkflowSuggestionKind::InsertSiblingAfter {
|
||||
symbol,
|
||||
description,
|
||||
} => {
|
||||
let symbol = outline
|
||||
.find_most_similar(&symbol)
|
||||
.with_context(|| format!("symbol not found: {:?}", symbol))?
|
||||
.to_point(&snapshot);
|
||||
let position = snapshot.anchor_after(symbol.range.end);
|
||||
suggestion = super::WorkflowSuggestion::InsertSiblingAfter {
|
||||
position,
|
||||
description,
|
||||
};
|
||||
}
|
||||
WorkflowSuggestionKind::PrependChild {
|
||||
symbol,
|
||||
description,
|
||||
} => {
|
||||
if let Some(symbol) = symbol {
|
||||
let symbol = outline
|
||||
.find_most_similar(&symbol)
|
||||
.with_context(|| format!("symbol not found: {:?}", symbol))?
|
||||
.to_point(&snapshot);
|
||||
|
||||
let position = snapshot.anchor_after(
|
||||
symbol
|
||||
.body_range
|
||||
.map_or(symbol.range.start, |body_range| body_range.start),
|
||||
);
|
||||
suggestion = super::WorkflowSuggestion::PrependChild {
|
||||
position,
|
||||
description,
|
||||
};
|
||||
} else {
|
||||
suggestion = super::WorkflowSuggestion::PrependChild {
|
||||
position: language::Anchor::MIN,
|
||||
description,
|
||||
};
|
||||
}
|
||||
}
|
||||
WorkflowSuggestionKind::AppendChild {
|
||||
symbol,
|
||||
description,
|
||||
} => {
|
||||
if let Some(symbol) = symbol {
|
||||
let symbol = outline
|
||||
.find_most_similar(&symbol)
|
||||
.with_context(|| format!("symbol not found: {:?}", symbol))?
|
||||
.to_point(&snapshot);
|
||||
|
||||
let position = snapshot.anchor_before(
|
||||
symbol
|
||||
.body_range
|
||||
.map_or(symbol.range.end, |body_range| body_range.end),
|
||||
);
|
||||
suggestion = super::WorkflowSuggestion::AppendChild {
|
||||
position,
|
||||
description,
|
||||
};
|
||||
} else {
|
||||
suggestion = super::WorkflowSuggestion::PrependChild {
|
||||
position: language::Anchor::MAX,
|
||||
description,
|
||||
};
|
||||
}
|
||||
}
|
||||
WorkflowSuggestionKind::Delete { symbol } => {
|
||||
let symbol = outline
|
||||
.find_most_similar(&symbol)
|
||||
.with_context(|| format!("symbol not found: {:?}", symbol))?
|
||||
.to_point(&snapshot);
|
||||
let start = symbol
|
||||
.annotation_range
|
||||
.map_or(symbol.range.start, |range| range.start);
|
||||
let start = Point::new(start.row, 0);
|
||||
let end = Point::new(
|
||||
symbol.range.end.row,
|
||||
snapshot.line_len(symbol.range.end.row),
|
||||
);
|
||||
let range = snapshot.anchor_before(start)..snapshot.anchor_after(end);
|
||||
suggestion = super::WorkflowSuggestion::Delete { range };
|
||||
}
|
||||
}
|
||||
|
||||
Ok((buffer, suggestion))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum WorkflowSuggestionKind {
|
||||
/// Rewrites the specified symbol entirely based on the given description.
|
||||
/// This operation completely replaces the existing symbol with new content.
|
||||
Update {
|
||||
/// A fully-qualified reference to the symbol, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`.
|
||||
/// The path should uniquely identify the symbol within the containing file.
|
||||
symbol: String,
|
||||
/// A brief description of the transformation to apply to the symbol.
|
||||
description: String,
|
||||
},
|
||||
/// Creates a new file with the given path based on the provided description.
|
||||
/// This operation adds a new file to the codebase.
|
||||
Create {
|
||||
/// A brief description of the file to be created.
|
||||
description: String,
|
||||
},
|
||||
/// Inserts a new symbol based on the given description before the specified symbol.
|
||||
/// This operation adds new content immediately preceding an existing symbol.
|
||||
InsertSiblingBefore {
|
||||
/// A fully-qualified reference to the symbol, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`.
|
||||
/// The new content will be inserted immediately before this symbol.
|
||||
symbol: String,
|
||||
/// A brief description of the new symbol to be inserted.
|
||||
description: String,
|
||||
},
|
||||
/// Inserts a new symbol based on the given description after the specified symbol.
|
||||
/// This operation adds new content immediately following an existing symbol.
|
||||
InsertSiblingAfter {
|
||||
/// A fully-qualified reference to the symbol, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`.
|
||||
/// The new content will be inserted immediately after this symbol.
|
||||
symbol: String,
|
||||
/// A brief description of the new symbol to be inserted.
|
||||
description: String,
|
||||
},
|
||||
/// Inserts a new symbol as a child of the specified symbol at the start.
|
||||
/// This operation adds new content as the first child of an existing symbol (or file if no symbol is provided).
|
||||
PrependChild {
|
||||
/// An optional fully-qualified reference to the symbol after the code you want to insert, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`.
|
||||
/// If provided, the new content will be inserted as the first child of this symbol.
|
||||
/// If not provided, the new content will be inserted at the top of the file.
|
||||
symbol: Option<String>,
|
||||
/// A brief description of the new symbol to be inserted.
|
||||
description: String,
|
||||
},
|
||||
/// Inserts a new symbol as a child of the specified symbol at the end.
|
||||
/// This operation adds new content as the last child of an existing symbol (or file if no symbol is provided).
|
||||
AppendChild {
|
||||
/// An optional fully-qualified reference to the symbol before the code you want to insert, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`.
|
||||
/// If provided, the new content will be inserted as the last child of this symbol.
|
||||
/// If not provided, the new content will be applied at the bottom of the file.
|
||||
symbol: Option<String>,
|
||||
/// A brief description of the new symbol to be inserted.
|
||||
description: String,
|
||||
},
|
||||
/// Deletes the specified symbol from the containing file.
|
||||
Delete {
|
||||
/// An fully-qualified reference to the symbol to be deleted, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`.
|
||||
symbol: String,
|
||||
},
|
||||
}
|
||||
}
|
@ -99,6 +99,16 @@ impl dyn LanguageModel {
|
||||
Ok(serde_json::from_str(&response)?)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn use_tool_stream<T: LanguageModelTool>(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
cx: &AsyncAppContext,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
|
||||
let schema = schemars::schema_for!(T);
|
||||
let schema_json = serde_json::to_value(&schema).unwrap();
|
||||
self.use_any_tool(request, T::name(), T::description(), schema_json, cx)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait LanguageModelTool: 'static + DeserializeOwned + JsonSchema {
|
||||
|
Loading…
Reference in New Issue
Block a user