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",