diff --git a/assets/icons/microscope.svg b/assets/icons/microscope.svg new file mode 100644 index 0000000000..2b3009a28b --- /dev/null +++ b/assets/icons/microscope.svg @@ -0,0 +1 @@ + diff --git a/crates/assistant/src/assistant.rs b/crates/assistant/src/assistant.rs index 71d93c0fdd..406cf38089 100644 --- a/crates/assistant/src/assistant.rs +++ b/crates/assistant/src/assistant.rs @@ -3,6 +3,7 @@ pub mod assistant_panel; pub mod assistant_settings; mod context; +pub(crate) mod context_inspector; pub mod context_store; mod inline_assistant; mod model_selector; diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 472ed414d2..9d5218965b 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -1,5 +1,6 @@ use crate::{ assistant_settings::{AssistantDockPosition, AssistantSettings}, + context_inspector::ContextInspector, humanize_token_count, prompt_library::open_prompt_library, prompts::PromptBuilder, @@ -402,13 +403,56 @@ impl AssistantPanel { } else { "Zoom In" }; + let weak_pane = cx.view().downgrade(); let menu = ContextMenu::build(cx, |menu, cx| { - menu.context(pane.focus_handle(cx)) + let menu = menu + .context(pane.focus_handle(cx)) .action("New Context", Box::new(NewFile)) .action("History", Box::new(DeployHistory)) .action("Prompt Library", Box::new(DeployPromptLibrary)) .action("Configure", Box::new(ShowConfiguration)) - .action(zoom_label, Box::new(ToggleZoom)) + .action(zoom_label, Box::new(ToggleZoom)); + + if let Some(editor) = pane + .active_item() + .and_then(|e| e.downcast::()) + { + let is_enabled = editor.read(cx).debug_inspector.is_some(); + menu.separator().toggleable_entry( + "Debug Workflows", + is_enabled, + IconPosition::End, + None, + move |cx| { + weak_pane + .update(cx, |this, cx| { + if let Some(context_editor) = + this.active_item().and_then(|item| { + item.downcast::() + }) + { + context_editor.update(cx, |this, cx| { + if let Some(mut state) = + this.debug_inspector.take() + { + state.deactivate(cx); + } else { + this.debug_inspector = Some( + ContextInspector::new( + this.editor.clone(), + this.context.clone(), + ), + ); + } + }) + } + }) + .ok(); + }, + ) + } else { + menu + } }); cx.subscribe(&menu, |pane, _, _: &DismissEvent, _| { pane.new_item_menu = None; @@ -1667,6 +1711,7 @@ pub struct ContextEditor { active_workflow_step: Option, assistant_panel: WeakView, error_message: Option, + debug_inspector: Option, } const DEFAULT_TAB_TITLE: &str = "New Context"; @@ -1726,6 +1771,7 @@ impl ContextEditor { active_workflow_step: None, assistant_panel, error_message: None, + debug_inspector: None, }; this.update_message_headers(cx); this.insert_slash_command_output_sections(sections, cx); @@ -2389,6 +2435,9 @@ impl ContextEditor { blocks_to_remove.insert(step.header_block_id); blocks_to_remove.insert(step.footer_block_id); } + if let Some(debug) = self.debug_inspector.as_mut() { + debug.deactivate_for(step_range, cx); + } } self.editor.update(cx, |editor, cx| { editor.remove_blocks(blocks_to_remove, None, cx) @@ -2415,6 +2464,9 @@ impl ContextEditor { let resolved_step = step.status.into_resolved(); if let Some(existing_step) = self.workflow_steps.get_mut(&step_range) { existing_step.resolved_step = resolved_step; + if let Some(debug) = self.debug_inspector.as_mut() { + debug.refresh(&step_range, cx); + } } else { let start = buffer_snapshot .anchor_in_excerpt(excerpt_id, step_range.start) @@ -2454,7 +2506,15 @@ impl ContextEditor { } else { theme.info_border }; - + let debug_header = weak_self + .update(&mut **cx, |this, _| { + if let Some(inspector) = this.debug_inspector.as_mut() { + Some(inspector.is_active(&step_range)) + } else { + None + } + }) + .unwrap_or_default(); div() .w_full() .px(cx.gutter_dimensions.full_width()) @@ -2464,14 +2524,52 @@ impl ContextEditor { .border_b_1() .border_color(border_color) .pb_1() - .justify_end() + .justify_between() .gap_2() + .children(debug_header.map(|is_active| { + h_flex().justify_start().child( + Button::new("debug-workflows-toggle", "Debug") + .icon_color(Color::Hidden) + .color(Color::Hidden) + .selected_icon_color(Color::Default) + .selected_label_color(Color::Default) + .icon(IconName::Microscope) + .icon_position(IconPosition::Start) + .icon_size(IconSize::Small) + .label_size(LabelSize::Small) + .selected(is_active) + .on_click({ + let weak_self = weak_self.clone(); + let step_range = step_range.clone(); + move |_, cx| { + weak_self + .update(cx, |this, cx| { + if let Some(inspector) = + this.debug_inspector + .as_mut() + { + if is_active { + + inspector.deactivate_for(&step_range, cx); + } else { + inspector.activate_for_step(step_range.clone(), cx); + } + } + }) + .ok(); + } + }), + ) + // .child(h_flex().w_full()) + })) .children(current_status.as_ref().map(|status| { - status.into_element( - step_range.clone(), - editor_focus_handle.clone(), - weak_self.clone(), - cx, + h_flex().w_full().justify_end().child( + status.into_element( + step_range.clone(), + editor_focus_handle.clone(), + weak_self.clone(), + cx, + ), ) })), ) @@ -3787,7 +3885,6 @@ impl Render for ContextEditorToolbarItem { ) .child(self.model_summary_editor.clone()) }); - let active_provider = LanguageModelRegistry::read_global(cx).active_provider(); let active_model = LanguageModelRegistry::read_global(cx).active_model(); diff --git a/crates/assistant/src/context_inspector.rs b/crates/assistant/src/context_inspector.rs new file mode 100644 index 0000000000..1fde35fdf0 --- /dev/null +++ b/crates/assistant/src/context_inspector.rs @@ -0,0 +1,223 @@ +use std::{ops::Range, sync::Arc}; + +use collections::{HashMap, HashSet}; +use editor::{ + display_map::{BlockDisposition, BlockProperties, BlockStyle, CustomBlockId}, + Editor, +}; +use gpui::{AppContext, Model, View}; +use text::{ToOffset, ToPoint}; +use ui::{ + div, h_flex, Color, Element as _, ParentElement as _, Styled, ViewContext, WindowContext, +}; + +use crate::{Context, ResolvedWorkflowStep, WorkflowSuggestion}; + +type StepRange = Range; + +struct DebugInfo { + range: Range, + block_id: CustomBlockId, +} + +pub(crate) struct ContextInspector { + active_debug_views: HashMap, DebugInfo>, + context: Model, + editor: View, +} + +impl ContextInspector { + pub(crate) fn new(editor: View, context: Model) -> Self { + Self { + editor, + context, + active_debug_views: Default::default(), + } + } + + pub(crate) fn is_active(&self, range: &StepRange) -> bool { + self.active_debug_views.contains_key(range) + } + + pub(crate) fn refresh(&mut self, range: &StepRange, cx: &mut WindowContext<'_>) { + if self.deactivate_for(range, cx) { + self.activate_for_step(range.clone(), cx); + } + } + fn crease_content( + context: &Model, + range: StepRange, + cx: &mut AppContext, + ) -> Option> { + use std::fmt::Write; + let step = context.read(cx).workflow_step_for_range(range)?; + let mut output = String::from("\n\n"); + match &step.status { + crate::WorkflowStepStatus::Resolved(ResolvedWorkflowStep { title, suggestions }) => { + writeln!(output, "Resolution:").ok()?; + writeln!(output, " {title:?}").ok()?; + for (buffer, suggestion_groups) in suggestions { + let buffer = buffer.read(cx); + let buffer_path = buffer + .file() + .and_then(|file| file.path().to_str()) + .unwrap_or("untitled"); + let snapshot = buffer.text_snapshot(); + writeln!(output, " {buffer_path}:").ok()?; + for group in suggestion_groups { + for suggestion in &group.suggestions { + pretty_print_workflow_suggestion(&mut output, suggestion, &snapshot); + } + } + } + } + crate::WorkflowStepStatus::Pending(_) => { + writeln!(output, "Resolution: Pending").ok()?; + } + crate::WorkflowStepStatus::Error(error) => { + writeln!(output, "Resolution: Error").ok()?; + writeln!(output, "{error:?}").ok()?; + } + } + + Some(output.into()) + } + pub(crate) fn activate_for_step(&mut self, range: StepRange, cx: &mut WindowContext<'_>) { + let text = Self::crease_content(&self.context, range.clone(), cx) + .unwrap_or_else(|| Arc::from("Error fetching debug info")); + self.editor.update(cx, |editor, cx| { + let buffer = editor.buffer().read(cx).as_singleton()?; + + let text_len = text.len(); + let snapshot = buffer.update(cx, |this, cx| { + this.edit([(range.end..range.end, text)], None, cx); + this.text_snapshot() + }); + let start_offset = range.end.to_offset(&snapshot); + let end_offset = start_offset + text_len; + let multibuffer_snapshot = editor.buffer().read(cx).snapshot(cx); + let anchor_before = multibuffer_snapshot.anchor_after(start_offset); + let anchor_after = multibuffer_snapshot.anchor_before(end_offset); + + let block_id = editor + .insert_blocks( + [BlockProperties { + position: anchor_after, + height: 0, + style: BlockStyle::Sticky, + render: Box::new(move |cx| { + div() + .w_full() + .px(cx.gutter_dimensions.full_width()) + .child( + h_flex() + .w_full() + .border_t_1() + .border_color(Color::Warning.color(cx)), + ) + .into_any() + }), + disposition: BlockDisposition::Below, + priority: 0, + }], + None, + cx, + ) + .into_iter() + .next()?; + let info = DebugInfo { + range: anchor_before..anchor_after, + block_id, + }; + self.active_debug_views.insert(range, info); + Some(()) + }); + } + + fn deactivate_impl(editor: &mut Editor, debug_data: DebugInfo, cx: &mut ViewContext) { + editor.remove_blocks(HashSet::from_iter([debug_data.block_id]), None, cx); + editor.edit([(debug_data.range, Arc::::default())], cx) + } + pub(crate) fn deactivate_for(&mut self, range: &StepRange, cx: &mut WindowContext<'_>) -> bool { + if let Some(debug_data) = self.active_debug_views.remove(range) { + self.editor.update(cx, |this, cx| { + Self::deactivate_impl(this, debug_data, cx); + }); + true + } else { + false + } + } + + pub(crate) fn deactivate(&mut self, cx: &mut WindowContext<'_>) { + let steps_to_disable = std::mem::take(&mut self.active_debug_views); + + self.editor.update(cx, move |editor, cx| { + for (_, debug_data) in steps_to_disable { + Self::deactivate_impl(editor, debug_data, cx); + } + }); + } +} +fn pretty_print_anchor( + out: &mut String, + anchor: &language::Anchor, + snapshot: &text::BufferSnapshot, +) { + use std::fmt::Write; + let point = anchor.to_point(snapshot); + write!(out, "{}:{}", point.row, point.column).ok(); +} +fn pretty_print_range( + out: &mut String, + range: &Range, + snapshot: &text::BufferSnapshot, +) { + use std::fmt::Write; + write!(out, " Range: ").ok(); + pretty_print_anchor(out, &range.start, snapshot); + write!(out, "..").ok(); + pretty_print_anchor(out, &range.end, snapshot); +} + +fn pretty_print_workflow_suggestion( + out: &mut String, + suggestion: &WorkflowSuggestion, + snapshot: &text::BufferSnapshot, +) { + use std::fmt::Write; + let (range, description, position) = match suggestion { + WorkflowSuggestion::Update { range, description } => (Some(range), Some(description), None), + WorkflowSuggestion::CreateFile { description } => (None, Some(description), None), + WorkflowSuggestion::AppendChild { + position, + description, + } + | WorkflowSuggestion::InsertSiblingBefore { + position, + description, + } + | WorkflowSuggestion::InsertSiblingAfter { + position, + description, + } + | WorkflowSuggestion::PrependChild { + position, + description, + } => (None, Some(description), Some(position)), + + WorkflowSuggestion::Delete { range } => (Some(range), None, None), + }; + if let Some(description) = description { + writeln!(out, " Description: {description}").ok(); + } + if let Some(range) = range { + pretty_print_range(out, range, snapshot); + } + if let Some(position) = position { + write!(out, " Position: ").ok(); + pretty_print_anchor(out, position, snapshot); + write!(out, "\n").ok(); + } + write!(out, "\n").ok(); +} diff --git a/crates/quick_action_bar/src/quick_action_bar.rs b/crates/quick_action_bar/src/quick_action_bar.rs index 78c9d037f0..bda398eee1 100644 --- a/crates/quick_action_bar/src/quick_action_bar.rs +++ b/crates/quick_action_bar/src/quick_action_bar.rs @@ -220,6 +220,7 @@ impl Render for QuickActionBar { menu = menu.toggleable_entry( "Inlay Hints", inlay_hints_enabled, + IconPosition::Start, Some(editor::actions::ToggleInlayHints.boxed_clone()), { let editor = editor.clone(); @@ -238,6 +239,7 @@ impl Render for QuickActionBar { menu = menu.toggleable_entry( "Inline Git Blame", git_blame_inline_enabled, + IconPosition::Start, Some(editor::actions::ToggleGitBlameInline.boxed_clone()), { let editor = editor.clone(); @@ -255,6 +257,7 @@ impl Render for QuickActionBar { menu = menu.toggleable_entry( "Selection Menu", selection_menu_enabled, + IconPosition::Start, Some(editor::actions::ToggleSelectionMenu.boxed_clone()), { let editor = editor.clone(); @@ -272,6 +275,7 @@ impl Render for QuickActionBar { menu = menu.toggleable_entry( "Auto Signature Help", auto_signature_help_enabled, + IconPosition::Start, Some(editor::actions::ToggleAutoSignatureHelp.boxed_clone()), { let editor = editor.clone(); diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index bd02140beb..c93f4e1f32 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/crates/ui/src/components/context_menu.rs @@ -16,7 +16,7 @@ enum ContextMenuItem { Header(SharedString), Label(SharedString), Entry { - toggled: Option, + toggle: Option<(IconPosition, bool)>, label: SharedString, icon: Option, handler: Rc, &mut WindowContext)>, @@ -97,7 +97,7 @@ impl ContextMenu { handler: impl Fn(&mut WindowContext) + 'static, ) -> Self { self.items.push(ContextMenuItem::Entry { - toggled: None, + toggle: None, label: label.into(), handler: Rc::new(move |_, cx| handler(cx)), icon: None, @@ -110,11 +110,12 @@ impl ContextMenu { mut self, label: impl Into, toggled: bool, + position: IconPosition, action: Option>, handler: impl Fn(&mut WindowContext) + 'static, ) -> Self { self.items.push(ContextMenuItem::Entry { - toggled: Some(toggled), + toggle: Some((position, toggled)), label: label.into(), handler: Rc::new(move |_, cx| handler(cx)), icon: None, @@ -155,7 +156,7 @@ impl ContextMenu { pub fn action(mut self, label: impl Into, action: Box) -> Self { self.items.push(ContextMenuItem::Entry { - toggled: None, + toggle: None, label: label.into(), action: Some(action.boxed_clone()), @@ -172,7 +173,7 @@ impl ContextMenu { pub fn link(mut self, label: impl Into, action: Box) -> Self { self.items.push(ContextMenuItem::Entry { - toggled: None, + toggle: None, label: label.into(), action: Some(action.boxed_clone()), @@ -354,7 +355,7 @@ impl Render for ContextMenu { .child(Label::new(label.clone())) .into_any_element(), ContextMenuItem::Entry { - toggled, + toggle, label, handler, icon, @@ -376,8 +377,8 @@ impl Render for ContextMenu { ListItem::new(ix) .inset(true) .selected(Some(ix) == self.selected_index) - .when_some(*toggled, |list_item, toggled| { - list_item.start_slot(if toggled { + .when_some(*toggle, |list_item, (position, toggled)| { + let contents = if toggled { v_flex().flex_none().child( Icon::new(IconName::Check).color(Color::Accent), ) @@ -385,7 +386,13 @@ impl Render for ContextMenu { v_flex() .flex_none() .size(IconSize::default().rems()) - }) + }; + match position { + IconPosition::Start => { + list_item.start_slot(contents) + } + IconPosition::End => list_item.end_slot(contents), + } }) .child( h_flex() diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index 40b51f616d..9e3a0290bd 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -203,6 +203,7 @@ pub enum IconName { MessageBubbles, Mic, MicMute, + Microscope, Minimize, Option, PageDown, @@ -366,6 +367,7 @@ impl IconName { IconName::MessageBubbles => "icons/conversations.svg", IconName::Mic => "icons/mic.svg", IconName::MicMute => "icons/mic_mute.svg", + IconName::Microscope => "icons/microscope.svg", IconName::Minimize => "icons/minimize.svg", IconName::Option => "icons/option.svg", IconName::PageDown => "icons/page_down.svg",