Add a workflow step resolution view (#16315)

You can now click on a step header (the words `Step 3`, etc) to open a
new tab containing a dedicated view for the resolution of that step.
This view looks similar to a context editor, and has sections for the
step input, the streaming tool output, and the interpreted results.

Hitting `cmd-enter` in this view re-resolves the step.


https://github.com/user-attachments/assets/64d82cdb-e70f-4204-8697-b30df5a645d5



Release Notes:

- N/A

---------

Co-authored-by: Nathan <nathan@zed.dev>
This commit is contained in:
Max Brunsfeld 2024-08-15 14:16:58 -07:00 committed by GitHub
parent 583959f82a
commit 776442f3ae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 525 additions and 79 deletions

View file

@ -1,3 +1,5 @@
mod step_view;
use crate::{
prompts::StepResolutionContext, AssistantPanel, Context, InlineAssistId, InlineAssistant,
};
@ -5,8 +7,11 @@ 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 gpui::{
AppContext, Model, ModelContext, Task, UpdateGlobal as _, View, WeakModel, WeakView,
WindowContext,
};
use language::{Anchor, Buffer, BufferSnapshot, SymbolPath};
use language_model::{LanguageModelRegistry, LanguageModelRequestMessage, Role};
use project::Project;
use rope::Point;
@ -17,16 +22,20 @@ use text::{AnchorRangeExt as _, OffsetRangeExt as _};
use util::ResultExt as _;
use workspace::Workspace;
pub use step_view::WorkflowStepView;
pub struct WorkflowStepResolution {
tagged_range: Range<Anchor>,
output: String,
context: WeakModel<Context>,
resolve_task: Option<Task<()>>,
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>>,
pub suggestion_groups: HashMap<Model<Buffer>, Vec<WorkflowSuggestionGroup>>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
@ -67,6 +76,7 @@ impl WorkflowSuggestion {
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum WorkflowSuggestionKind {
Update {
symbol_path: SymbolPath,
range: Range<language::Anchor>,
description: String,
},
@ -74,48 +84,63 @@ pub enum WorkflowSuggestionKind {
description: String,
},
InsertSiblingBefore {
symbol_path: SymbolPath,
position: language::Anchor,
description: String,
},
InsertSiblingAfter {
symbol_path: SymbolPath,
position: language::Anchor,
description: String,
},
PrependChild {
symbol_path: Option<SymbolPath>,
position: language::Anchor,
description: String,
},
AppendChild {
symbol_path: Option<SymbolPath>,
position: language::Anchor,
description: String,
},
Delete {
symbol_path: SymbolPath,
range: Range<language::Anchor>,
},
}
impl WorkflowStepResolution {
pub fn new(range: Range<Anchor>) -> Self {
pub fn new(range: Range<Anchor>, context: WeakModel<Context>) -> Self {
Self {
tagged_range: range,
output: String::new(),
context,
result: None,
resolve_task: None,
}
}
pub fn resolve(
&mut self,
context: &Context,
cx: &mut ModelContext<WorkflowStepResolution>,
) -> Option<Task<()>> {
pub fn step_text(&self, context: &Context, cx: &AppContext) -> String {
context
.buffer()
.clone()
.read(cx)
.text_for_range(self.tagged_range.clone())
.collect::<String>()
}
pub fn resolve(&mut self, cx: &mut ModelContext<WorkflowStepResolution>) -> Option<()> {
let range = self.tagged_range.clone();
let context = self.context.upgrade()?;
let context = context.read(cx);
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 context_buffer = context.buffer();
let step_text = context_buffer
.read(cx)
.text_for_range(self.tagged_range.clone())
.text_for_range(range.clone())
.collect::<String>();
let mut workflow_context = String::new();
@ -127,7 +152,7 @@ impl WorkflowStepResolution {
write!(&mut workflow_context, "</message>").unwrap();
}
Some(cx.spawn(|this, mut cx| async move {
self.resolve_task = Some(cx.spawn(|this, mut cx| async move {
let result = async {
let Some(model) = model else {
return Err(anyhow!("no model selected"));
@ -136,6 +161,7 @@ impl WorkflowStepResolution {
this.update(&mut cx, |this, cx| {
this.output.clear();
this.result = None;
this.result_updated(cx);
cx.notify();
})?;
@ -167,6 +193,11 @@ impl WorkflowStepResolution {
serde_json::from_str::<tool::WorkflowStepResolutionTool>(&this.output)
})??;
this.update(&mut cx, |this, cx| {
this.output = serde_json::to_string_pretty(&resolution).unwrap();
cx.notify();
})?;
// Translate the parsed suggestions to our internal types, which anchor the suggestions to locations in the code.
let suggestion_tasks: Vec<_> = resolution
.suggestions
@ -251,13 +282,28 @@ impl WorkflowStepResolution {
let result = result.await;
this.update(&mut cx, |this, cx| {
this.result = Some(match result {
Ok((title, suggestions)) => Ok(ResolvedWorkflowStep { title, suggestions }),
Ok((title, suggestion_groups)) => Ok(ResolvedWorkflowStep {
title,
suggestion_groups,
}),
Err(error) => Err(Arc::new(error)),
});
this.context
.update(cx, |context, cx| context.workflow_step_updated(range, cx))
.ok();
cx.notify();
})
.ok();
}))
}));
None
}
fn result_updated(&mut self, cx: &mut ModelContext<Self>) {
self.context
.update(cx, |context, cx| {
context.workflow_step_updated(self.tagged_range.clone(), cx)
})
.ok();
}
}
@ -270,7 +316,7 @@ impl WorkflowSuggestionKind {
| Self::InsertSiblingAfter { position, .. }
| Self::PrependChild { position, .. }
| Self::AppendChild { position, .. } => *position..*position,
Self::Delete { range } => range.clone(),
Self::Delete { range, .. } => range.clone(),
}
}
@ -298,6 +344,30 @@ impl WorkflowSuggestionKind {
}
}
fn symbol_path(&self) -> Option<&SymbolPath> {
match self {
Self::Update { symbol_path, .. } => Some(symbol_path),
Self::InsertSiblingBefore { symbol_path, .. } => Some(symbol_path),
Self::InsertSiblingAfter { symbol_path, .. } => Some(symbol_path),
Self::PrependChild { symbol_path, .. } => symbol_path.as_ref(),
Self::AppendChild { symbol_path, .. } => symbol_path.as_ref(),
Self::Delete { symbol_path, .. } => Some(symbol_path),
Self::CreateFile { .. } => None,
}
}
fn kind(&self) -> &str {
match self {
Self::Update { .. } => "Update",
Self::CreateFile { .. } => "CreateFile",
Self::InsertSiblingBefore { .. } => "InsertSiblingBefore",
Self::InsertSiblingAfter { .. } => "InsertSiblingAfter",
Self::PrependChild { .. } => "PrependChild",
Self::AppendChild { .. } => "AppendChild",
Self::Delete { .. } => "Delete",
}
}
fn try_merge(&mut self, other: &Self, buffer: &BufferSnapshot) -> bool {
let range = self.range();
let other_range = other.range();
@ -333,7 +403,9 @@ impl WorkflowSuggestionKind {
let snapshot = buffer.read(cx).snapshot(cx);
match self {
Self::Update { range, description } => {
Self::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)?;
@ -345,6 +417,7 @@ impl WorkflowSuggestionKind {
Self::InsertSiblingBefore {
position,
description,
..
} => {
let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
initial_prompt = description.clone();
@ -361,6 +434,7 @@ impl WorkflowSuggestionKind {
Self::InsertSiblingAfter {
position,
description,
..
} => {
let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
initial_prompt = description.clone();
@ -377,6 +451,7 @@ impl WorkflowSuggestionKind {
Self::PrependChild {
position,
description,
..
} => {
let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
initial_prompt = description.clone();
@ -393,6 +468,7 @@ impl WorkflowSuggestionKind {
Self::AppendChild {
position,
description,
..
} => {
let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
initial_prompt = description.clone();
@ -406,7 +482,7 @@ impl WorkflowSuggestionKind {
line_start..line_start
});
}
Self::Delete { range } => {
Self::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)?;
@ -528,10 +604,10 @@ pub mod tool {
symbol,
description,
} => {
let symbol = outline
let (symbol_path, symbol) = outline
.find_most_similar(&symbol)
.with_context(|| format!("symbol not found: {:?}", symbol))?
.to_point(&snapshot);
.with_context(|| format!("symbol not found: {:?}", symbol))?;
let symbol = symbol.to_point(&snapshot);
let start = symbol
.annotation_range
.map_or(symbol.range.start, |range| range.start);
@ -541,7 +617,11 @@ pub mod tool {
snapshot.line_len(symbol.range.end.row),
);
let range = snapshot.anchor_before(start)..snapshot.anchor_after(end);
WorkflowSuggestionKind::Update { range, description }
WorkflowSuggestionKind::Update {
range,
description,
symbol_path,
}
}
WorkflowSuggestionToolKind::Create { description } => {
WorkflowSuggestionKind::CreateFile { description }
@ -550,10 +630,10 @@ pub mod tool {
symbol,
description,
} => {
let symbol = outline
let (symbol_path, symbol) = outline
.find_most_similar(&symbol)
.with_context(|| format!("symbol not found: {:?}", symbol))?
.to_point(&snapshot);
.with_context(|| format!("symbol not found: {:?}", symbol))?;
let symbol = symbol.to_point(&snapshot);
let position = snapshot.anchor_before(
symbol
.annotation_range
@ -564,20 +644,22 @@ pub mod tool {
WorkflowSuggestionKind::InsertSiblingBefore {
position,
description,
symbol_path,
}
}
WorkflowSuggestionToolKind::InsertSiblingAfter {
symbol,
description,
} => {
let symbol = outline
let (symbol_path, symbol) = outline
.find_most_similar(&symbol)
.with_context(|| format!("symbol not found: {:?}", symbol))?
.to_point(&snapshot);
.with_context(|| format!("symbol not found: {:?}", symbol))?;
let symbol = symbol.to_point(&snapshot);
let position = snapshot.anchor_after(symbol.range.end);
WorkflowSuggestionKind::InsertSiblingAfter {
position,
description,
symbol_path,
}
}
WorkflowSuggestionToolKind::PrependChild {
@ -585,10 +667,10 @@ pub mod tool {
description,
} => {
if let Some(symbol) = symbol {
let symbol = outline
let (symbol_path, symbol) = outline
.find_most_similar(&symbol)
.with_context(|| format!("symbol not found: {:?}", symbol))?
.to_point(&snapshot);
.with_context(|| format!("symbol not found: {:?}", symbol))?;
let symbol = symbol.to_point(&snapshot);
let position = snapshot.anchor_after(
symbol
@ -598,11 +680,13 @@ pub mod tool {
WorkflowSuggestionKind::PrependChild {
position,
description,
symbol_path: Some(symbol_path),
}
} else {
WorkflowSuggestionKind::PrependChild {
position: language::Anchor::MIN,
description,
symbol_path: None,
}
}
}
@ -611,10 +695,10 @@ pub mod tool {
description,
} => {
if let Some(symbol) = symbol {
let symbol = outline
let (symbol_path, symbol) = outline
.find_most_similar(&symbol)
.with_context(|| format!("symbol not found: {:?}", symbol))?
.to_point(&snapshot);
.with_context(|| format!("symbol not found: {:?}", symbol))?;
let symbol = symbol.to_point(&snapshot);
let position = snapshot.anchor_before(
symbol
@ -624,19 +708,21 @@ pub mod tool {
WorkflowSuggestionKind::AppendChild {
position,
description,
symbol_path: Some(symbol_path),
}
} else {
WorkflowSuggestionKind::PrependChild {
position: language::Anchor::MAX,
description,
symbol_path: None,
}
}
}
WorkflowSuggestionToolKind::Delete { symbol } => {
let symbol = outline
let (symbol_path, symbol) = outline
.find_most_similar(&symbol)
.with_context(|| format!("symbol not found: {:?}", symbol))?
.to_point(&snapshot);
.with_context(|| format!("symbol not found: {:?}", symbol))?;
let symbol = symbol.to_point(&snapshot);
let start = symbol
.annotation_range
.map_or(symbol.range.start, |range| range.start);
@ -646,7 +732,7 @@ pub mod tool {
snapshot.line_len(symbol.range.end.row),
);
let range = snapshot.anchor_before(start)..snapshot.anchor_after(end);
WorkflowSuggestionKind::Delete { range }
WorkflowSuggestionKind::Delete { range, symbol_path }
}
};