workflow.rs

  1mod step_view;
  2
  3use crate::{
  4    prompts::StepResolutionContext, AssistantPanel, Context, InlineAssistId, InlineAssistant,
  5};
  6use anyhow::{anyhow, Error, Result};
  7use collections::HashMap;
  8use editor::Editor;
  9use futures::future;
 10use gpui::{
 11    AppContext, Model, ModelContext, Task, UpdateGlobal as _, View, WeakModel, WeakView,
 12    WindowContext,
 13};
 14use language::{Anchor, Buffer, BufferSnapshot, SymbolPath};
 15use language_model::{LanguageModelRegistry, LanguageModelRequestMessage, Role};
 16use project::Project;
 17use rope::Point;
 18use serde::{Deserialize, Serialize};
 19use smol::stream::StreamExt;
 20use std::{cmp, fmt::Write, ops::Range, sync::Arc};
 21use text::{AnchorRangeExt as _, OffsetRangeExt as _};
 22use util::ResultExt as _;
 23use workspace::Workspace;
 24
 25pub use step_view::WorkflowStepView;
 26
 27pub struct WorkflowStepResolution {
 28    tagged_range: Range<Anchor>,
 29    output: String,
 30    context: WeakModel<Context>,
 31    resolve_task: Option<Task<()>>,
 32    pub result: Option<Result<ResolvedWorkflowStep, Arc<Error>>>,
 33}
 34
 35#[derive(Clone, Debug, Eq, PartialEq)]
 36pub struct ResolvedWorkflowStep {
 37    pub title: String,
 38    pub suggestion_groups: HashMap<Model<Buffer>, Vec<WorkflowSuggestionGroup>>,
 39}
 40
 41#[derive(Clone, Debug, Eq, PartialEq)]
 42pub struct WorkflowSuggestionGroup {
 43    pub context_range: Range<language::Anchor>,
 44    pub suggestions: Vec<WorkflowSuggestion>,
 45}
 46
 47#[derive(Clone, Debug, Eq, PartialEq)]
 48pub struct WorkflowSuggestion {
 49    pub kind: WorkflowSuggestionKind,
 50    pub tool_input: String,
 51    pub tool_output: tool::WorkflowSuggestionTool,
 52}
 53
 54impl WorkflowSuggestion {
 55    pub fn range(&self) -> Range<Anchor> {
 56        self.kind.range()
 57    }
 58
 59    pub fn show(
 60        &self,
 61        editor: &View<Editor>,
 62        excerpt_id: editor::ExcerptId,
 63        workspace: &WeakView<Workspace>,
 64        assistant_panel: &View<AssistantPanel>,
 65        cx: &mut ui::ViewContext<crate::assistant_panel::ContextEditor>,
 66    ) -> Option<InlineAssistId> {
 67        self.kind
 68            .show(editor, excerpt_id, workspace, assistant_panel, cx)
 69    }
 70
 71    fn try_merge(&mut self, other: &mut WorkflowSuggestion, snapshot: &BufferSnapshot) -> bool {
 72        self.kind.try_merge(&other.kind, snapshot)
 73    }
 74}
 75
 76#[derive(Clone, Debug, Eq, PartialEq)]
 77pub enum WorkflowSuggestionKind {
 78    Update {
 79        symbol_path: SymbolPath,
 80        range: Range<language::Anchor>,
 81        description: String,
 82    },
 83    CreateFile {
 84        description: String,
 85    },
 86    InsertSiblingBefore {
 87        symbol_path: SymbolPath,
 88        position: language::Anchor,
 89        description: String,
 90    },
 91    InsertSiblingAfter {
 92        symbol_path: SymbolPath,
 93        position: language::Anchor,
 94        description: String,
 95    },
 96    PrependChild {
 97        symbol_path: Option<SymbolPath>,
 98        position: language::Anchor,
 99        description: String,
100    },
101    AppendChild {
102        symbol_path: Option<SymbolPath>,
103        position: language::Anchor,
104        description: String,
105    },
106    Delete {
107        symbol_path: SymbolPath,
108        range: Range<language::Anchor>,
109    },
110}
111
112impl WorkflowStepResolution {
113    pub fn new(range: Range<Anchor>, context: WeakModel<Context>) -> Self {
114        Self {
115            tagged_range: range,
116            output: String::new(),
117            context,
118            result: None,
119            resolve_task: None,
120        }
121    }
122
123    pub fn step_text(&self, context: &Context, cx: &AppContext) -> String {
124        context
125            .buffer()
126            .clone()
127            .read(cx)
128            .text_for_range(self.tagged_range.clone())
129            .collect::<String>()
130    }
131
132    pub fn resolve(&mut self, cx: &mut ModelContext<WorkflowStepResolution>) -> Option<()> {
133        let range = self.tagged_range.clone();
134        let context = self.context.upgrade()?;
135        let context = context.read(cx);
136        let project = context.project()?;
137        let prompt_builder = context.prompt_builder();
138        let mut request = context.to_completion_request(cx);
139        let model = LanguageModelRegistry::read_global(cx).active_model();
140        let context_buffer = context.buffer();
141        let step_text = context_buffer
142            .read(cx)
143            .text_for_range(range.clone())
144            .collect::<String>();
145
146        let mut workflow_context = String::new();
147        for message in context.messages(cx) {
148            write!(&mut workflow_context, "<message role={}>", message.role).unwrap();
149            for chunk in context_buffer.read(cx).text_for_range(message.offset_range) {
150                write!(&mut workflow_context, "{chunk}").unwrap();
151            }
152            write!(&mut workflow_context, "</message>").unwrap();
153        }
154
155        self.resolve_task = Some(cx.spawn(|this, mut cx| async move {
156            let result = async {
157                let Some(model) = model else {
158                    return Err(anyhow!("no model selected"));
159                };
160
161                this.update(&mut cx, |this, cx| {
162                    this.output.clear();
163                    this.result = None;
164                    this.result_updated(cx);
165                    cx.notify();
166                })?;
167
168                let resolution_context = StepResolutionContext {
169                    workflow_context,
170                    step_to_resolve: step_text.clone(),
171                };
172                let mut prompt =
173                    prompt_builder.generate_step_resolution_prompt(&resolution_context)?;
174                prompt.push_str(&step_text);
175                request.messages.push(LanguageModelRequestMessage {
176                    role: Role::User,
177                    content: vec![prompt.into()],
178                });
179
180                // Invoke the model to get its edit suggestions for this workflow step.
181                let mut stream = model
182                    .use_tool_stream::<tool::WorkflowStepResolutionTool>(request, &cx)
183                    .await?;
184                while let Some(chunk) = stream.next().await {
185                    let chunk = chunk?;
186                    this.update(&mut cx, |this, cx| {
187                        this.output.push_str(&chunk);
188                        cx.notify();
189                    })?;
190                }
191
192                let resolution = this.update(&mut cx, |this, _| {
193                    serde_json::from_str::<tool::WorkflowStepResolutionTool>(&this.output)
194                })??;
195
196                this.update(&mut cx, |this, cx| {
197                    this.output = serde_json::to_string_pretty(&resolution).unwrap();
198                    cx.notify();
199                })?;
200
201                // Translate the parsed suggestions to our internal types, which anchor the suggestions to locations in the code.
202                let suggestion_tasks: Vec<_> = resolution
203                    .suggestions
204                    .iter()
205                    .map(|suggestion| {
206                        suggestion.resolve(step_text.clone(), project.clone(), cx.clone())
207                    })
208                    .collect();
209
210                // Expand the context ranges of each suggestion and group suggestions with overlapping context ranges.
211                let suggestions = future::join_all(suggestion_tasks)
212                    .await
213                    .into_iter()
214                    .filter_map(|task| task.log_err())
215                    .collect::<Vec<_>>();
216
217                let mut suggestions_by_buffer = HashMap::default();
218                for (buffer, suggestion) in suggestions {
219                    suggestions_by_buffer
220                        .entry(buffer)
221                        .or_insert_with(Vec::new)
222                        .push(suggestion);
223                }
224
225                let mut suggestion_groups_by_buffer = HashMap::default();
226                for (buffer, mut suggestions) in suggestions_by_buffer {
227                    let mut suggestion_groups = Vec::<WorkflowSuggestionGroup>::new();
228                    let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?;
229                    // Sort suggestions by their range so that earlier, larger ranges come first
230                    suggestions.sort_by(|a, b| a.range().cmp(&b.range(), &snapshot));
231
232                    // Merge overlapping suggestions
233                    suggestions.dedup_by(|a, b| b.try_merge(a, &snapshot));
234
235                    // Create context ranges for each suggestion
236                    for suggestion in suggestions {
237                        let context_range = {
238                            let suggestion_point_range = suggestion.range().to_point(&snapshot);
239                            let start_row = suggestion_point_range.start.row.saturating_sub(5);
240                            let end_row = cmp::min(
241                                suggestion_point_range.end.row + 5,
242                                snapshot.max_point().row,
243                            );
244                            let start = snapshot.anchor_before(Point::new(start_row, 0));
245                            let end = snapshot
246                                .anchor_after(Point::new(end_row, snapshot.line_len(end_row)));
247                            start..end
248                        };
249
250                        if let Some(last_group) = suggestion_groups.last_mut() {
251                            if last_group
252                                .context_range
253                                .end
254                                .cmp(&context_range.start, &snapshot)
255                                .is_ge()
256                            {
257                                // Merge with the previous group if context ranges overlap
258                                last_group.context_range.end = context_range.end;
259                                last_group.suggestions.push(suggestion);
260                            } else {
261                                // Create a new group
262                                suggestion_groups.push(WorkflowSuggestionGroup {
263                                    context_range,
264                                    suggestions: vec![suggestion],
265                                });
266                            }
267                        } else {
268                            // Create the first group
269                            suggestion_groups.push(WorkflowSuggestionGroup {
270                                context_range,
271                                suggestions: vec![suggestion],
272                            });
273                        }
274                    }
275
276                    suggestion_groups_by_buffer.insert(buffer, suggestion_groups);
277                }
278
279                Ok((resolution.step_title, suggestion_groups_by_buffer))
280            };
281
282            let result = result.await;
283            this.update(&mut cx, |this, cx| {
284                this.result = Some(match result {
285                    Ok((title, suggestion_groups)) => Ok(ResolvedWorkflowStep {
286                        title,
287                        suggestion_groups,
288                    }),
289                    Err(error) => Err(Arc::new(error)),
290                });
291                this.context
292                    .update(cx, |context, cx| context.workflow_step_updated(range, cx))
293                    .ok();
294                cx.notify();
295            })
296            .ok();
297        }));
298        None
299    }
300
301    fn result_updated(&mut self, cx: &mut ModelContext<Self>) {
302        self.context
303            .update(cx, |context, cx| {
304                context.workflow_step_updated(self.tagged_range.clone(), cx)
305            })
306            .ok();
307    }
308}
309
310impl WorkflowSuggestionKind {
311    pub fn range(&self) -> Range<language::Anchor> {
312        match self {
313            Self::Update { range, .. } => range.clone(),
314            Self::CreateFile { .. } => language::Anchor::MIN..language::Anchor::MAX,
315            Self::InsertSiblingBefore { position, .. }
316            | Self::InsertSiblingAfter { position, .. }
317            | Self::PrependChild { position, .. }
318            | Self::AppendChild { position, .. } => *position..*position,
319            Self::Delete { range, .. } => range.clone(),
320        }
321    }
322
323    pub fn description(&self) -> Option<&str> {
324        match self {
325            Self::Update { description, .. }
326            | Self::CreateFile { description }
327            | Self::InsertSiblingBefore { description, .. }
328            | Self::InsertSiblingAfter { description, .. }
329            | Self::PrependChild { description, .. }
330            | Self::AppendChild { description, .. } => Some(description),
331            Self::Delete { .. } => None,
332        }
333    }
334
335    fn description_mut(&mut self) -> Option<&mut String> {
336        match self {
337            Self::Update { description, .. }
338            | Self::CreateFile { description }
339            | Self::InsertSiblingBefore { description, .. }
340            | Self::InsertSiblingAfter { description, .. }
341            | Self::PrependChild { description, .. }
342            | Self::AppendChild { description, .. } => Some(description),
343            Self::Delete { .. } => None,
344        }
345    }
346
347    fn symbol_path(&self) -> Option<&SymbolPath> {
348        match self {
349            Self::Update { symbol_path, .. } => Some(symbol_path),
350            Self::InsertSiblingBefore { symbol_path, .. } => Some(symbol_path),
351            Self::InsertSiblingAfter { symbol_path, .. } => Some(symbol_path),
352            Self::PrependChild { symbol_path, .. } => symbol_path.as_ref(),
353            Self::AppendChild { symbol_path, .. } => symbol_path.as_ref(),
354            Self::Delete { symbol_path, .. } => Some(symbol_path),
355            Self::CreateFile { .. } => None,
356        }
357    }
358
359    fn kind(&self) -> &str {
360        match self {
361            Self::Update { .. } => "Update",
362            Self::CreateFile { .. } => "CreateFile",
363            Self::InsertSiblingBefore { .. } => "InsertSiblingBefore",
364            Self::InsertSiblingAfter { .. } => "InsertSiblingAfter",
365            Self::PrependChild { .. } => "PrependChild",
366            Self::AppendChild { .. } => "AppendChild",
367            Self::Delete { .. } => "Delete",
368        }
369    }
370
371    fn try_merge(&mut self, other: &Self, buffer: &BufferSnapshot) -> bool {
372        let range = self.range();
373        let other_range = other.range();
374
375        // Don't merge if we don't contain the other suggestion.
376        if range.start.cmp(&other_range.start, buffer).is_gt()
377            || range.end.cmp(&other_range.end, buffer).is_lt()
378        {
379            return false;
380        }
381
382        if let Some(description) = self.description_mut() {
383            if let Some(other_description) = other.description() {
384                description.push('\n');
385                description.push_str(other_description);
386            }
387        }
388        true
389    }
390
391    pub fn show(
392        &self,
393        editor: &View<Editor>,
394        excerpt_id: editor::ExcerptId,
395        workspace: &WeakView<Workspace>,
396        assistant_panel: &View<AssistantPanel>,
397        cx: &mut WindowContext,
398    ) -> Option<InlineAssistId> {
399        let mut initial_transaction_id = None;
400        let initial_prompt;
401        let suggestion_range;
402        let buffer = editor.read(cx).buffer().clone();
403        let snapshot = buffer.read(cx).snapshot(cx);
404
405        match self {
406            Self::Update {
407                range, description, ..
408            } => {
409                initial_prompt = description.clone();
410                suggestion_range = snapshot.anchor_in_excerpt(excerpt_id, range.start)?
411                    ..snapshot.anchor_in_excerpt(excerpt_id, range.end)?;
412            }
413            Self::CreateFile { description } => {
414                initial_prompt = description.clone();
415                suggestion_range = editor::Anchor::min()..editor::Anchor::min();
416            }
417            Self::InsertSiblingBefore {
418                position,
419                description,
420                ..
421            } => {
422                let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
423                initial_prompt = description.clone();
424                suggestion_range = buffer.update(cx, |buffer, cx| {
425                    buffer.start_transaction(cx);
426                    let line_start = buffer.insert_empty_line(position, true, true, cx);
427                    initial_transaction_id = buffer.end_transaction(cx);
428                    buffer.refresh_preview(cx);
429
430                    let line_start = buffer.read(cx).anchor_before(line_start);
431                    line_start..line_start
432                });
433            }
434            Self::InsertSiblingAfter {
435                position,
436                description,
437                ..
438            } => {
439                let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
440                initial_prompt = description.clone();
441                suggestion_range = buffer.update(cx, |buffer, cx| {
442                    buffer.start_transaction(cx);
443                    let line_start = buffer.insert_empty_line(position, true, true, cx);
444                    initial_transaction_id = buffer.end_transaction(cx);
445                    buffer.refresh_preview(cx);
446
447                    let line_start = buffer.read(cx).anchor_before(line_start);
448                    line_start..line_start
449                });
450            }
451            Self::PrependChild {
452                position,
453                description,
454                ..
455            } => {
456                let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
457                initial_prompt = description.clone();
458                suggestion_range = buffer.update(cx, |buffer, cx| {
459                    buffer.start_transaction(cx);
460                    let line_start = buffer.insert_empty_line(position, false, true, cx);
461                    initial_transaction_id = buffer.end_transaction(cx);
462                    buffer.refresh_preview(cx);
463
464                    let line_start = buffer.read(cx).anchor_before(line_start);
465                    line_start..line_start
466                });
467            }
468            Self::AppendChild {
469                position,
470                description,
471                ..
472            } => {
473                let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
474                initial_prompt = description.clone();
475                suggestion_range = buffer.update(cx, |buffer, cx| {
476                    buffer.start_transaction(cx);
477                    let line_start = buffer.insert_empty_line(position, true, false, cx);
478                    initial_transaction_id = buffer.end_transaction(cx);
479                    buffer.refresh_preview(cx);
480
481                    let line_start = buffer.read(cx).anchor_before(line_start);
482                    line_start..line_start
483                });
484            }
485            Self::Delete { range, .. } => {
486                initial_prompt = "Delete".to_string();
487                suggestion_range = snapshot.anchor_in_excerpt(excerpt_id, range.start)?
488                    ..snapshot.anchor_in_excerpt(excerpt_id, range.end)?;
489            }
490        }
491
492        InlineAssistant::update_global(cx, |inline_assistant, cx| {
493            Some(inline_assistant.suggest_assist(
494                editor,
495                suggestion_range,
496                initial_prompt,
497                initial_transaction_id,
498                Some(workspace.clone()),
499                Some(assistant_panel),
500                cx,
501            ))
502        })
503    }
504}
505
506pub mod tool {
507    use std::path::Path;
508
509    use super::*;
510    use anyhow::Context as _;
511    use gpui::AsyncAppContext;
512    use language::ParseStatus;
513    use language_model::LanguageModelTool;
514    use project::ProjectPath;
515    use schemars::JsonSchema;
516
517    #[derive(Debug, Serialize, Deserialize, JsonSchema)]
518    pub struct WorkflowStepResolutionTool {
519        /// An extremely short title for the edit step represented by these operations.
520        pub step_title: String,
521        /// A sequence of operations to apply to the codebase.
522        /// When multiple operations are required for a step, be sure to include multiple operations in this list.
523        pub suggestions: Vec<WorkflowSuggestionTool>,
524    }
525
526    impl LanguageModelTool for WorkflowStepResolutionTool {
527        fn name() -> String {
528            "edit".into()
529        }
530
531        fn description() -> String {
532            "suggest edits to one or more locations in the codebase".into()
533        }
534    }
535
536    /// A description of an operation to apply to one location in the codebase.
537    ///
538    /// This object represents a single edit operation that can be performed on a specific file
539    /// in the codebase. It encapsulates both the location (file path) and the nature of the
540    /// edit to be made.
541    ///
542    /// # Fields
543    ///
544    /// * `path`: A string representing the file path where the edit operation should be applied.
545    ///           This path is relative to the root of the project or repository.
546    ///
547    /// * `kind`: An enum representing the specific type of edit operation to be performed.
548    ///
549    /// # Usage
550    ///
551    /// `EditOperation` is used within a code editor to represent and apply
552    /// programmatic changes to source code. It provides a structured way to describe
553    /// edits for features like refactoring tools or AI-assisted coding suggestions.
554    #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
555    pub struct WorkflowSuggestionTool {
556        /// The path to the file containing the relevant operation
557        pub path: String,
558        #[serde(flatten)]
559        pub kind: WorkflowSuggestionToolKind,
560    }
561
562    impl WorkflowSuggestionTool {
563        pub(super) async fn resolve(
564            &self,
565            tool_input: String,
566            project: Model<Project>,
567            mut cx: AsyncAppContext,
568        ) -> Result<(Model<Buffer>, super::WorkflowSuggestion)> {
569            let path = self.path.clone();
570            let kind = self.kind.clone();
571            let buffer = project
572                .update(&mut cx, |project, cx| {
573                    let project_path = project
574                        .find_project_path(Path::new(&path), cx)
575                        .or_else(|| {
576                            // If we couldn't find a project path for it, put it in the active worktree
577                            // so that when we create the buffer, it can be saved.
578                            let worktree = project
579                                .active_entry()
580                                .and_then(|entry_id| project.worktree_for_entry(entry_id, cx))
581                                .or_else(|| project.worktrees(cx).next())?;
582                            let worktree = worktree.read(cx);
583
584                            Some(ProjectPath {
585                                worktree_id: worktree.id(),
586                                path: Arc::from(Path::new(&path)),
587                            })
588                        })
589                        .with_context(|| format!("worktree not found for {:?}", path))?;
590                    anyhow::Ok(project.open_buffer(project_path, cx))
591                })??
592                .await?;
593
594            let mut parse_status = buffer.read_with(&cx, |buffer, _cx| buffer.parse_status())?;
595            while *parse_status.borrow() != ParseStatus::Idle {
596                parse_status.changed().await?;
597            }
598
599            let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?;
600            let outline = snapshot.outline(None).context("no outline for buffer")?;
601
602            let kind = match kind {
603                WorkflowSuggestionToolKind::Update {
604                    symbol,
605                    description,
606                } => {
607                    let (symbol_path, symbol) = outline
608                        .find_most_similar(&symbol)
609                        .with_context(|| format!("symbol not found: {:?}", symbol))?;
610                    let symbol = symbol.to_point(&snapshot);
611                    let start = symbol
612                        .annotation_range
613                        .map_or(symbol.range.start, |range| range.start);
614                    let start = Point::new(start.row, 0);
615                    let end = Point::new(
616                        symbol.range.end.row,
617                        snapshot.line_len(symbol.range.end.row),
618                    );
619                    let range = snapshot.anchor_before(start)..snapshot.anchor_after(end);
620                    WorkflowSuggestionKind::Update {
621                        range,
622                        description,
623                        symbol_path,
624                    }
625                }
626                WorkflowSuggestionToolKind::Create { description } => {
627                    WorkflowSuggestionKind::CreateFile { description }
628                }
629                WorkflowSuggestionToolKind::InsertSiblingBefore {
630                    symbol,
631                    description,
632                } => {
633                    let (symbol_path, symbol) = outline
634                        .find_most_similar(&symbol)
635                        .with_context(|| format!("symbol not found: {:?}", symbol))?;
636                    let symbol = symbol.to_point(&snapshot);
637                    let position = snapshot.anchor_before(
638                        symbol
639                            .annotation_range
640                            .map_or(symbol.range.start, |annotation_range| {
641                                annotation_range.start
642                            }),
643                    );
644                    WorkflowSuggestionKind::InsertSiblingBefore {
645                        position,
646                        description,
647                        symbol_path,
648                    }
649                }
650                WorkflowSuggestionToolKind::InsertSiblingAfter {
651                    symbol,
652                    description,
653                } => {
654                    let (symbol_path, symbol) = outline
655                        .find_most_similar(&symbol)
656                        .with_context(|| format!("symbol not found: {:?}", symbol))?;
657                    let symbol = symbol.to_point(&snapshot);
658                    let position = snapshot.anchor_after(symbol.range.end);
659                    WorkflowSuggestionKind::InsertSiblingAfter {
660                        position,
661                        description,
662                        symbol_path,
663                    }
664                }
665                WorkflowSuggestionToolKind::PrependChild {
666                    symbol,
667                    description,
668                } => {
669                    if let Some(symbol) = symbol {
670                        let (symbol_path, symbol) = outline
671                            .find_most_similar(&symbol)
672                            .with_context(|| format!("symbol not found: {:?}", symbol))?;
673                        let symbol = symbol.to_point(&snapshot);
674
675                        let position = snapshot.anchor_after(
676                            symbol
677                                .body_range
678                                .map_or(symbol.range.start, |body_range| body_range.start),
679                        );
680                        WorkflowSuggestionKind::PrependChild {
681                            position,
682                            description,
683                            symbol_path: Some(symbol_path),
684                        }
685                    } else {
686                        WorkflowSuggestionKind::PrependChild {
687                            position: language::Anchor::MIN,
688                            description,
689                            symbol_path: None,
690                        }
691                    }
692                }
693                WorkflowSuggestionToolKind::AppendChild {
694                    symbol,
695                    description,
696                } => {
697                    if let Some(symbol) = symbol {
698                        let (symbol_path, symbol) = outline
699                            .find_most_similar(&symbol)
700                            .with_context(|| format!("symbol not found: {:?}", symbol))?;
701                        let symbol = symbol.to_point(&snapshot);
702
703                        let position = snapshot.anchor_before(
704                            symbol
705                                .body_range
706                                .map_or(symbol.range.end, |body_range| body_range.end),
707                        );
708                        WorkflowSuggestionKind::AppendChild {
709                            position,
710                            description,
711                            symbol_path: Some(symbol_path),
712                        }
713                    } else {
714                        WorkflowSuggestionKind::PrependChild {
715                            position: language::Anchor::MAX,
716                            description,
717                            symbol_path: None,
718                        }
719                    }
720                }
721                WorkflowSuggestionToolKind::Delete { symbol } => {
722                    let (symbol_path, symbol) = outline
723                        .find_most_similar(&symbol)
724                        .with_context(|| format!("symbol not found: {:?}", symbol))?;
725                    let symbol = symbol.to_point(&snapshot);
726                    let start = symbol
727                        .annotation_range
728                        .map_or(symbol.range.start, |range| range.start);
729                    let start = Point::new(start.row, 0);
730                    let end = Point::new(
731                        symbol.range.end.row,
732                        snapshot.line_len(symbol.range.end.row),
733                    );
734                    let range = snapshot.anchor_before(start)..snapshot.anchor_after(end);
735                    WorkflowSuggestionKind::Delete { range, symbol_path }
736                }
737            };
738
739            let suggestion = WorkflowSuggestion {
740                kind,
741                tool_output: self.clone(),
742                tool_input,
743            };
744
745            Ok((buffer, suggestion))
746        }
747    }
748
749    #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
750    #[serde(tag = "kind")]
751    pub enum WorkflowSuggestionToolKind {
752        /// Rewrites the specified symbol entirely based on the given description.
753        /// This operation completely replaces the existing symbol with new content.
754        Update {
755            /// A fully-qualified reference to the symbol, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`.
756            /// The path should uniquely identify the symbol within the containing file.
757            symbol: String,
758            /// A brief description of the transformation to apply to the symbol.
759            description: String,
760        },
761        /// Creates a new file with the given path based on the provided description.
762        /// This operation adds a new file to the codebase.
763        Create {
764            /// A brief description of the file to be created.
765            description: String,
766        },
767        /// Inserts a new symbol based on the given description before the specified symbol.
768        /// This operation adds new content immediately preceding an existing symbol.
769        InsertSiblingBefore {
770            /// A fully-qualified reference to the symbol, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`.
771            /// The new content will be inserted immediately before this symbol.
772            symbol: String,
773            /// A brief description of the new symbol to be inserted.
774            description: String,
775        },
776        /// Inserts a new symbol based on the given description after the specified symbol.
777        /// This operation adds new content immediately following an existing symbol.
778        InsertSiblingAfter {
779            /// A fully-qualified reference to the symbol, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`.
780            /// The new content will be inserted immediately after this symbol.
781            symbol: String,
782            /// A brief description of the new symbol to be inserted.
783            description: String,
784        },
785        /// Inserts a new symbol as a child of the specified symbol at the start.
786        /// This operation adds new content as the first child of an existing symbol (or file if no symbol is provided).
787        PrependChild {
788            /// 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`.
789            /// If provided, the new content will be inserted as the first child of this symbol.
790            /// If not provided, the new content will be inserted at the top of the file.
791            symbol: Option<String>,
792            /// A brief description of the new symbol to be inserted.
793            description: String,
794        },
795        /// Inserts a new symbol as a child of the specified symbol at the end.
796        /// This operation adds new content as the last child of an existing symbol (or file if no symbol is provided).
797        AppendChild {
798            /// 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`.
799            /// If provided, the new content will be inserted as the last child of this symbol.
800            /// If not provided, the new content will be applied at the bottom of the file.
801            symbol: Option<String>,
802            /// A brief description of the new symbol to be inserted.
803            description: String,
804        },
805        /// Deletes the specified symbol from the containing file.
806        Delete {
807            /// An fully-qualified reference to the symbol to be deleted, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`.
808            symbol: String,
809        },
810    }
811}