assistant: Add debug inspector (#16105)
I went with inline approach directly within the panel. First, enable workflow debugging in the hamburger menu (this works retroactively as well):  This enables debug buttons in the header of each step:  Enabling one pretty-prints the workflow step internals:  Release Notes: - N/A
This commit is contained in:
parent
b6b081596a
commit
98f314ba21
7 changed files with 354 additions and 19 deletions
1
assets/icons/microscope.svg
Normal file
1
assets/icons/microscope.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-microscope"><path d="M6 18h8"/><path d="M3 22h18"/><path d="M14 22a7 7 0 1 0 0-14h-1"/><path d="M9 14h2"/><path d="M9 12a2 2 0 0 1-2-2V6h6v4a2 2 0 0 1-2 2Z"/><path d="M12 6V3a1 1 0 0 0-1-1H9a1 1 0 0 0-1 1v3"/></svg>
|
After Width: | Height: | Size: 418 B |
|
@ -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;
|
||||
|
|
|
@ -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::<ContextEditor>())
|
||||
{
|
||||
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::<ContextEditor>()
|
||||
})
|
||||
{
|
||||
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<ActiveWorkflowStep>,
|
||||
assistant_panel: WeakView<AssistantPanel>,
|
||||
error_message: Option<SharedString>,
|
||||
debug_inspector: Option<ContextInspector>,
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
|
|
223
crates/assistant/src/context_inspector.rs
Normal file
223
crates/assistant/src/context_inspector.rs
Normal file
|
@ -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<language::Anchor>;
|
||||
|
||||
struct DebugInfo {
|
||||
range: Range<editor::Anchor>,
|
||||
block_id: CustomBlockId,
|
||||
}
|
||||
|
||||
pub(crate) struct ContextInspector {
|
||||
active_debug_views: HashMap<Range<language::Anchor>, DebugInfo>,
|
||||
context: Model<Context>,
|
||||
editor: View<Editor>,
|
||||
}
|
||||
|
||||
impl ContextInspector {
|
||||
pub(crate) fn new(editor: View<Editor>, context: Model<Context>) -> 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<Context>,
|
||||
range: StepRange,
|
||||
cx: &mut AppContext,
|
||||
) -> Option<Arc<str>> {
|
||||
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>) {
|
||||
editor.remove_blocks(HashSet::from_iter([debug_data.block_id]), None, cx);
|
||||
editor.edit([(debug_data.range, Arc::<str>::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<language::Anchor>,
|
||||
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();
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -16,7 +16,7 @@ enum ContextMenuItem {
|
|||
Header(SharedString),
|
||||
Label(SharedString),
|
||||
Entry {
|
||||
toggled: Option<bool>,
|
||||
toggle: Option<(IconPosition, bool)>,
|
||||
label: SharedString,
|
||||
icon: Option<IconName>,
|
||||
handler: Rc<dyn Fn(Option<&FocusHandle>, &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<SharedString>,
|
||||
toggled: bool,
|
||||
position: IconPosition,
|
||||
action: Option<Box<dyn Action>>,
|
||||
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<SharedString>, action: Box<dyn Action>) -> 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<SharedString>, action: Box<dyn Action>) -> 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()
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue