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