mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-08 07:35:01 +03:00
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:
parent
583959f82a
commit
776442f3ae
@ -15,7 +15,7 @@ use crate::{
|
||||
DebugWorkflowSteps, DeployHistory, DeployPromptLibrary, InlineAssist, InlineAssistId,
|
||||
InlineAssistant, InsertIntoEditor, MessageStatus, ModelSelector, PendingSlashCommand,
|
||||
PendingSlashCommandStatus, QuoteSelection, RemoteContextMetadata, ResolvedWorkflowStep,
|
||||
SavedContextMetadata, Split, ToggleFocus, ToggleModelSelector,
|
||||
SavedContextMetadata, Split, ToggleFocus, ToggleModelSelector, WorkflowStepView,
|
||||
};
|
||||
use crate::{ContextStoreEvent, ShowConfiguration};
|
||||
use anyhow::{anyhow, Result};
|
||||
@ -36,10 +36,10 @@ use fs::Fs;
|
||||
use gpui::{
|
||||
canvas, div, img, percentage, point, pulsating_between, size, Action, Animation, AnimationExt,
|
||||
AnyElement, AnyView, AppContext, AsyncWindowContext, ClipboardEntry, ClipboardItem,
|
||||
Context as _, DismissEvent, Empty, Entity, EntityId, EventEmitter, FocusHandle, FocusableView,
|
||||
FontWeight, InteractiveElement, IntoElement, Model, ParentElement, Pixels, ReadGlobal, Render,
|
||||
RenderImage, SharedString, Size, StatefulInteractiveElement, Styled, Subscription, Task,
|
||||
Transformation, UpdateGlobal, View, VisualContext, WeakView, WindowContext,
|
||||
Context as _, CursorStyle, DismissEvent, Empty, Entity, EntityId, EventEmitter, FocusHandle,
|
||||
FocusableView, FontWeight, InteractiveElement, IntoElement, Model, ParentElement, Pixels,
|
||||
ReadGlobal, Render, RenderImage, SharedString, Size, StatefulInteractiveElement, Styled,
|
||||
Subscription, Task, Transformation, UpdateGlobal, View, VisualContext, WeakView, WindowContext,
|
||||
};
|
||||
use indexed_docs::IndexedDocsStore;
|
||||
use language::{
|
||||
@ -59,7 +59,7 @@ use std::{
|
||||
borrow::Cow,
|
||||
cmp::{self, Ordering},
|
||||
fmt::Write,
|
||||
ops::Range,
|
||||
ops::{DerefMut, Range},
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
@ -1388,7 +1388,7 @@ impl WorkflowStep {
|
||||
fn status(&self, cx: &AppContext) -> WorkflowStepStatus {
|
||||
match self.resolved_step.as_ref() {
|
||||
Some(Ok(step)) => {
|
||||
if step.suggestions.is_empty() {
|
||||
if step.suggestion_groups.is_empty() {
|
||||
WorkflowStepStatus::Empty
|
||||
} else if let Some(assist) = self.assist.as_ref() {
|
||||
let assistant = InlineAssistant::global(cx);
|
||||
@ -2030,7 +2030,10 @@ impl ContextEditor {
|
||||
.collect::<String>()
|
||||
));
|
||||
match &step.resolution.read(cx).result {
|
||||
Some(Ok(ResolvedWorkflowStep { title, suggestions })) => {
|
||||
Some(Ok(ResolvedWorkflowStep {
|
||||
title,
|
||||
suggestion_groups: suggestions,
|
||||
})) => {
|
||||
output.push_str("Resolution:\n");
|
||||
output.push_str(&format!(" {:?}\n", title));
|
||||
output.push_str(&format!(" {:?}\n", suggestions));
|
||||
@ -2571,16 +2574,33 @@ impl ContextEditor {
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let step_label = if let Some(index) = step_index {
|
||||
|
||||
Label::new(format!("Step {index}")).size(LabelSize::Small)
|
||||
} else {
|
||||
Label::new("Step").size(LabelSize::Small)
|
||||
};
|
||||
|
||||
let step_label = if current_status.as_ref().is_some_and(|status| status.is_confirmed()) {
|
||||
h_flex().items_center().gap_2().child(step_label.strikethrough(true).color(Color::Muted)).child(Icon::new(IconName::Check).size(IconSize::Small).color(Color::Created))
|
||||
} else {
|
||||
div().child(step_label)
|
||||
};
|
||||
|
||||
let step_label = step_label.id("step")
|
||||
.cursor(CursorStyle::PointingHand)
|
||||
.on_click({
|
||||
let this = weak_self.clone();
|
||||
let step_range = step_range.clone();
|
||||
move |_, cx| {
|
||||
this
|
||||
.update(cx, |this, cx| {
|
||||
this.open_workflow_step(
|
||||
step_range.clone(), cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
|
||||
div()
|
||||
.w_full()
|
||||
.px(cx.gutter_dimensions.full_width())
|
||||
@ -2699,6 +2719,30 @@ impl ContextEditor {
|
||||
self.update_active_workflow_step(cx);
|
||||
}
|
||||
|
||||
fn open_workflow_step(
|
||||
&mut self,
|
||||
step_range: Range<language::Anchor>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<()> {
|
||||
let pane = self
|
||||
.assistant_panel
|
||||
.update(cx, |panel, _| panel.pane())
|
||||
.ok()??;
|
||||
let context = self.context.read(cx);
|
||||
let language_registry = context.language_registry();
|
||||
let step = context.workflow_step_for_range(step_range)?;
|
||||
let resolution = step.resolution.clone();
|
||||
let view = cx.new_view(|cx| {
|
||||
WorkflowStepView::new(self.context.clone(), resolution, language_registry, cx)
|
||||
});
|
||||
cx.deref_mut().defer(move |cx| {
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.add_item(Box::new(view), true, true, None, cx);
|
||||
});
|
||||
});
|
||||
None
|
||||
}
|
||||
|
||||
fn update_active_workflow_step(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let new_step = self.active_workflow_step_for_cursor(cx);
|
||||
if new_step.as_ref() != self.active_workflow_step.as_ref() {
|
||||
@ -2820,18 +2864,24 @@ impl ContextEditor {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<WorkflowAssist> {
|
||||
let assistant_panel = assistant_panel.upgrade()?;
|
||||
if resolved_step.suggestions.is_empty() {
|
||||
if resolved_step.suggestion_groups.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let editor;
|
||||
let mut editor_was_open = false;
|
||||
let mut suggestion_groups = Vec::new();
|
||||
if resolved_step.suggestions.len() == 1
|
||||
&& resolved_step.suggestions.values().next().unwrap().len() == 1
|
||||
if resolved_step.suggestion_groups.len() == 1
|
||||
&& resolved_step
|
||||
.suggestion_groups
|
||||
.values()
|
||||
.next()
|
||||
.unwrap()
|
||||
.len()
|
||||
== 1
|
||||
{
|
||||
// If there's only one buffer and one suggestion group, open it directly
|
||||
let (buffer, groups) = resolved_step.suggestions.iter().next().unwrap();
|
||||
let (buffer, groups) = resolved_step.suggestion_groups.iter().next().unwrap();
|
||||
let group = groups.into_iter().next().unwrap();
|
||||
editor = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
@ -2884,7 +2934,7 @@ impl ContextEditor {
|
||||
let replica_id = project.read(cx).replica_id();
|
||||
let mut multibuffer = MultiBuffer::new(replica_id, Capability::ReadWrite)
|
||||
.with_title(resolved_step.title.clone());
|
||||
for (buffer, groups) in &resolved_step.suggestions {
|
||||
for (buffer, groups) in &resolved_step.suggestion_groups {
|
||||
let excerpt_ids = multibuffer.push_excerpts(
|
||||
buffer.clone(),
|
||||
groups.iter().map(|suggestion_group| ExcerptRange {
|
||||
|
@ -838,6 +838,10 @@ impl Context {
|
||||
&self.buffer
|
||||
}
|
||||
|
||||
pub fn language_registry(&self) -> Arc<LanguageRegistry> {
|
||||
self.language_registry.clone()
|
||||
}
|
||||
|
||||
pub fn project(&self) -> Option<Model<Project>> {
|
||||
self.project.clone()
|
||||
}
|
||||
@ -1073,6 +1077,7 @@ impl Context {
|
||||
}
|
||||
|
||||
fn parse_workflow_steps_in_range(&mut self, range: Range<usize>, cx: &mut ModelContext<Self>) {
|
||||
let weak_self = cx.weak_model();
|
||||
let mut new_edit_steps = Vec::new();
|
||||
let mut edits = Vec::new();
|
||||
|
||||
@ -1116,7 +1121,10 @@ impl Context {
|
||||
ix,
|
||||
WorkflowStep {
|
||||
resolution: cx.new_model(|_| {
|
||||
WorkflowStepResolution::new(tagged_range.clone())
|
||||
WorkflowStepResolution::new(
|
||||
tagged_range.clone(),
|
||||
weak_self.clone(),
|
||||
)
|
||||
}),
|
||||
tagged_range,
|
||||
_task: None,
|
||||
@ -1161,21 +1169,21 @@ impl Context {
|
||||
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();
|
||||
})
|
||||
let resolution = self.workflow_steps[step_index].resolution.clone();
|
||||
cx.defer(move |cx| {
|
||||
resolution.update(cx, |resolution, cx| resolution.resolve(cx));
|
||||
});
|
||||
}
|
||||
|
||||
pub fn workflow_step_updated(
|
||||
&mut self,
|
||||
range: Range<language::Anchor>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
cx.emit(ContextEvent::WorkflowStepUpdated(range));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn pending_command_for_position(
|
||||
&mut self,
|
||||
position: language::Anchor,
|
||||
|
@ -54,7 +54,10 @@ impl ContextInspector {
|
||||
let step = context.read(cx).workflow_step_for_range(range)?;
|
||||
let mut output = String::from("\n\n");
|
||||
match &step.resolution.read(cx).result {
|
||||
Some(Ok(ResolvedWorkflowStep { title, suggestions })) => {
|
||||
Some(Ok(ResolvedWorkflowStep {
|
||||
title,
|
||||
suggestion_groups: suggestions,
|
||||
})) => {
|
||||
writeln!(output, "Resolution:").ok()?;
|
||||
writeln!(output, " {title:?}").ok()?;
|
||||
if suggestions.is_empty() {
|
||||
@ -189,27 +192,31 @@ fn pretty_print_workflow_suggestion(
|
||||
) {
|
||||
use std::fmt::Write;
|
||||
let (position, description, range) = match &suggestion.kind {
|
||||
WorkflowSuggestionKind::Update { range, description } => {
|
||||
(None, Some(description), Some(range))
|
||||
}
|
||||
WorkflowSuggestionKind::Update {
|
||||
range, description, ..
|
||||
} => (None, Some(description), Some(range)),
|
||||
WorkflowSuggestionKind::CreateFile { description } => (None, Some(description), None),
|
||||
WorkflowSuggestionKind::AppendChild {
|
||||
position,
|
||||
description,
|
||||
..
|
||||
} => (Some(position), Some(description), None),
|
||||
WorkflowSuggestionKind::InsertSiblingBefore {
|
||||
position,
|
||||
description,
|
||||
..
|
||||
} => (Some(position), Some(description), None),
|
||||
WorkflowSuggestionKind::InsertSiblingAfter {
|
||||
position,
|
||||
description,
|
||||
..
|
||||
} => (Some(position), Some(description), None),
|
||||
WorkflowSuggestionKind::PrependChild {
|
||||
position,
|
||||
description,
|
||||
..
|
||||
} => (Some(position), Some(description), None),
|
||||
WorkflowSuggestionKind::Delete { range } => (None, None, Some(range)),
|
||||
WorkflowSuggestionKind::Delete { range, .. } => (None, None, Some(range)),
|
||||
};
|
||||
writeln!(out, " Tool input: {}", suggestion.tool_input).ok();
|
||||
writeln!(
|
||||
|
@ -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 }
|
||||
}
|
||||
};
|
||||
|
||||
|
290
crates/assistant/src/workflow/step_view.rs
Normal file
290
crates/assistant/src/workflow/step_view.rs
Normal file
@ -0,0 +1,290 @@
|
||||
use super::WorkflowStepResolution;
|
||||
use crate::{Assist, Context};
|
||||
use editor::{
|
||||
display_map::{BlockDisposition, BlockProperties, BlockStyle},
|
||||
Editor, EditorEvent, ExcerptRange, MultiBuffer,
|
||||
};
|
||||
use gpui::{
|
||||
div, AnyElement, AppContext, Context as _, Empty, EventEmitter, FocusableView, IntoElement,
|
||||
Model, ParentElement as _, Render, SharedString, Styled as _, View, ViewContext,
|
||||
VisualContext as _, WeakModel, WindowContext,
|
||||
};
|
||||
use language::{language_settings::SoftWrap, Anchor, Buffer, LanguageRegistry};
|
||||
use std::{ops::DerefMut, sync::Arc};
|
||||
use theme::ActiveTheme as _;
|
||||
use ui::{
|
||||
h_flex, v_flex, ButtonCommon as _, ButtonLike, ButtonStyle, Color, InteractiveElement as _,
|
||||
Label, LabelCommon as _,
|
||||
};
|
||||
use workspace::{
|
||||
item::{self, Item},
|
||||
pane,
|
||||
searchable::SearchableItemHandle,
|
||||
};
|
||||
|
||||
pub struct WorkflowStepView {
|
||||
step: WeakModel<WorkflowStepResolution>,
|
||||
tool_output_buffer: Model<Buffer>,
|
||||
editor: View<Editor>,
|
||||
}
|
||||
|
||||
impl WorkflowStepView {
|
||||
pub fn new(
|
||||
context: Model<Context>,
|
||||
step: Model<WorkflowStepResolution>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let tool_output_buffer = cx.new_model(|cx| Buffer::local(step.read(cx).output.clone(), cx));
|
||||
let buffer = cx.new_model(|cx| {
|
||||
let mut buffer = MultiBuffer::without_headers(0, language::Capability::ReadWrite);
|
||||
buffer.push_excerpts(
|
||||
context.read(cx).buffer().clone(),
|
||||
[ExcerptRange {
|
||||
context: step.read(cx).tagged_range.clone(),
|
||||
primary: None,
|
||||
}],
|
||||
cx,
|
||||
);
|
||||
buffer.push_excerpts(
|
||||
tool_output_buffer.clone(),
|
||||
[ExcerptRange {
|
||||
context: Anchor::MIN..Anchor::MAX,
|
||||
primary: None,
|
||||
}],
|
||||
cx,
|
||||
);
|
||||
buffer
|
||||
});
|
||||
|
||||
let buffer_snapshot = buffer.read(cx).snapshot(cx);
|
||||
let output_excerpt = buffer_snapshot.excerpts().skip(1).next().unwrap().0;
|
||||
let input_start_anchor = multi_buffer::Anchor::min();
|
||||
let output_start_anchor = buffer_snapshot
|
||||
.anchor_in_excerpt(output_excerpt, Anchor::MIN)
|
||||
.unwrap();
|
||||
let output_end_anchor = multi_buffer::Anchor::max();
|
||||
|
||||
let handle = cx.view().downgrade();
|
||||
let editor = cx.new_view(|cx| {
|
||||
let mut editor = Editor::for_multibuffer(buffer.clone(), None, false, cx);
|
||||
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
|
||||
editor.set_show_line_numbers(false, cx);
|
||||
editor.set_show_git_diff_gutter(false, cx);
|
||||
editor.set_show_code_actions(false, cx);
|
||||
editor.set_show_runnables(false, cx);
|
||||
editor.set_show_wrap_guides(false, cx);
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
editor.set_read_only(true);
|
||||
editor.insert_blocks(
|
||||
[
|
||||
BlockProperties {
|
||||
position: input_start_anchor,
|
||||
height: 1,
|
||||
style: BlockStyle::Fixed,
|
||||
render: Box::new(|cx| section_header("Step Input", cx)),
|
||||
disposition: BlockDisposition::Above,
|
||||
priority: 0,
|
||||
},
|
||||
BlockProperties {
|
||||
position: output_start_anchor,
|
||||
height: 1,
|
||||
style: BlockStyle::Fixed,
|
||||
render: Box::new(|cx| section_header("Tool Output", cx)),
|
||||
disposition: BlockDisposition::Above,
|
||||
priority: 0,
|
||||
},
|
||||
BlockProperties {
|
||||
position: output_end_anchor,
|
||||
height: 1,
|
||||
style: BlockStyle::Fixed,
|
||||
render: Box::new(move |cx| {
|
||||
if let Some(result) = handle.upgrade().and_then(|this| {
|
||||
this.update(cx.deref_mut(), |this, cx| this.render_result(cx))
|
||||
}) {
|
||||
v_flex()
|
||||
.child(section_header("Output", cx))
|
||||
.child(
|
||||
div().pl(cx.gutter_dimensions.full_width()).child(result),
|
||||
)
|
||||
.into_any_element()
|
||||
} else {
|
||||
Empty.into_any_element()
|
||||
}
|
||||
}),
|
||||
disposition: BlockDisposition::Below,
|
||||
priority: 0,
|
||||
},
|
||||
],
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
editor
|
||||
});
|
||||
|
||||
cx.observe(&step, Self::step_updated).detach();
|
||||
cx.observe_release(&step, Self::step_released).detach();
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
if let Ok(language) = language_registry.language_for_name("JSON").await {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.tool_output_buffer.update(cx, |buffer, cx| {
|
||||
buffer.set_language(Some(language), cx);
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
tool_output_buffer,
|
||||
step: step.downgrade(),
|
||||
editor,
|
||||
}
|
||||
}
|
||||
|
||||
fn render_result(&mut self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
|
||||
let step = self.step.upgrade()?;
|
||||
let result = step.read(cx).result.as_ref()?;
|
||||
match result {
|
||||
Ok(result) => Some(
|
||||
v_flex()
|
||||
.child(result.title.clone())
|
||||
.children(result.suggestion_groups.iter().filter_map(
|
||||
|(buffer, suggestion_groups)| {
|
||||
let path = buffer.read(cx).file().map(|f| f.path());
|
||||
v_flex()
|
||||
.mb_2()
|
||||
.border_b_1()
|
||||
.children(path.map(|path| format!("path: {}", path.display())))
|
||||
.children(suggestion_groups.iter().map(|group| {
|
||||
v_flex().pl_2().children(group.suggestions.iter().map(
|
||||
|suggestion| {
|
||||
v_flex()
|
||||
.children(
|
||||
suggestion
|
||||
.kind
|
||||
.description()
|
||||
.map(|desc| format!("description: {desc}")),
|
||||
)
|
||||
.child(format!("kind: {}", suggestion.kind.kind()))
|
||||
.children(
|
||||
suggestion.kind.symbol_path().map(|path| {
|
||||
format!("symbol path: {}", path.0)
|
||||
}),
|
||||
)
|
||||
},
|
||||
))
|
||||
}))
|
||||
.into()
|
||||
},
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
Err(error) => Some(format!("{:?}", error).into_any_element()),
|
||||
}
|
||||
}
|
||||
|
||||
fn step_updated(&mut self, step: Model<WorkflowStepResolution>, cx: &mut ViewContext<Self>) {
|
||||
self.tool_output_buffer.update(cx, |buffer, cx| {
|
||||
let text = step.read(cx).output.clone();
|
||||
buffer.set_text(text, cx);
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn step_released(&mut self, _: &mut WorkflowStepResolution, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(EditorEvent::Closed);
|
||||
}
|
||||
|
||||
fn resolve(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
|
||||
self.step
|
||||
.update(cx, |step, cx| {
|
||||
step.resolve(cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
fn section_header(
|
||||
name: &'static str,
|
||||
cx: &mut editor::display_map::BlockContext,
|
||||
) -> gpui::AnyElement {
|
||||
h_flex()
|
||||
.pl(cx.gutter_dimensions.full_width())
|
||||
.h_11()
|
||||
.w_full()
|
||||
.relative()
|
||||
.gap_1()
|
||||
.child(
|
||||
ButtonLike::new("role")
|
||||
.style(ButtonStyle::Filled)
|
||||
.child(Label::new(name).color(Color::Default)),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
impl Render for WorkflowStepView {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.key_context("ContextEditor")
|
||||
.on_action(cx.listener(Self::resolve))
|
||||
.flex_grow()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(self.editor.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<EditorEvent> for WorkflowStepView {}
|
||||
|
||||
impl FocusableView for WorkflowStepView {
|
||||
fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle {
|
||||
self.editor.read(cx).focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Item for WorkflowStepView {
|
||||
type Event = EditorEvent;
|
||||
|
||||
fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
|
||||
Some("workflow step".into())
|
||||
}
|
||||
|
||||
fn to_item_events(event: &Self::Event, mut f: impl FnMut(item::ItemEvent)) {
|
||||
match event {
|
||||
EditorEvent::Edited { .. } => {
|
||||
f(item::ItemEvent::Edit);
|
||||
}
|
||||
EditorEvent::TitleChanged => {
|
||||
f(item::ItemEvent::UpdateTab);
|
||||
}
|
||||
EditorEvent::Closed => f(item::ItemEvent::CloseItem),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn tab_tooltip_text(&self, _cx: &AppContext) -> Option<SharedString> {
|
||||
None
|
||||
}
|
||||
|
||||
fn as_searchable(&self, _handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_nav_history(&mut self, nav_history: pane::ItemNavHistory, cx: &mut ViewContext<Self>) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
Item::set_nav_history(editor, nav_history, cx)
|
||||
})
|
||||
}
|
||||
|
||||
fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| Item::navigate(editor, data, cx))
|
||||
}
|
||||
|
||||
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| Item::deactivated(editor, cx))
|
||||
}
|
||||
}
|
@ -71,7 +71,7 @@ pub use language_registry::{
|
||||
PendingLanguageServer, QUERY_FILENAME_PREFIXES,
|
||||
};
|
||||
pub use lsp::LanguageServerId;
|
||||
pub use outline::{render_item, Outline, OutlineItem};
|
||||
pub use outline::*;
|
||||
pub use syntax_map::{OwnedSyntaxLayer, SyntaxLayer};
|
||||
pub use text::{AnchorRangeExt, LineEnding};
|
||||
pub use tree_sitter::{Node, Parser, Tree, TreeCursor};
|
||||
|
@ -25,6 +25,9 @@ pub struct OutlineItem<T> {
|
||||
pub annotation_range: Option<Range<T>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct SymbolPath(pub String);
|
||||
|
||||
impl<T: ToPoint> OutlineItem<T> {
|
||||
/// Converts to an equivalent outline item, but with parameterized over Points.
|
||||
pub fn to_point(&self, buffer: &BufferSnapshot) -> OutlineItem<Point> {
|
||||
@ -85,7 +88,7 @@ impl<T> Outline<T> {
|
||||
}
|
||||
|
||||
/// Find the most similar symbol to the provided query using normalized Levenshtein distance.
|
||||
pub fn find_most_similar(&self, query: &str) -> Option<&OutlineItem<T>> {
|
||||
pub fn find_most_similar(&self, query: &str) -> Option<(SymbolPath, &OutlineItem<T>)> {
|
||||
const SIMILARITY_THRESHOLD: f64 = 0.6;
|
||||
|
||||
let (position, similarity) = self
|
||||
@ -99,8 +102,10 @@ impl<T> Outline<T> {
|
||||
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())?;
|
||||
|
||||
if similarity >= SIMILARITY_THRESHOLD {
|
||||
let item = self.items.get(position)?;
|
||||
Some(item)
|
||||
self.path_candidates
|
||||
.get(position)
|
||||
.map(|candidate| SymbolPath(candidate.string.clone()))
|
||||
.zip(self.items.get(position))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@ -250,15 +255,15 @@ mod tests {
|
||||
]);
|
||||
assert_eq!(
|
||||
outline.find_most_similar("pub fn process"),
|
||||
Some(&outline.items[0])
|
||||
Some((SymbolPath("fn process".into()), &outline.items[0]))
|
||||
);
|
||||
assert_eq!(
|
||||
outline.find_most_similar("async fn process"),
|
||||
Some(&outline.items[0])
|
||||
Some((SymbolPath("fn process".into()), &outline.items[0])),
|
||||
);
|
||||
assert_eq!(
|
||||
outline.find_most_similar("struct Processor"),
|
||||
Some(&outline.items[1])
|
||||
Some((SymbolPath("struct DataProcessor".into()), &outline.items[1]))
|
||||
);
|
||||
assert_eq!(outline.find_most_similar("struct User"), None);
|
||||
assert_eq!(outline.find_most_similar("struct"), None);
|
||||
|
Loading…
Reference in New Issue
Block a user