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