agent: Collapse code blocks in the active thread (#28467)
Release Notes: - N/A --------- Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
This commit is contained in:
parent
ed7c55a04e
commit
2f4b48129b
2 changed files with 186 additions and 68 deletions
|
@ -57,6 +57,7 @@ pub struct ActiveThread {
|
||||||
editing_message: Option<(MessageId, EditMessageState)>,
|
editing_message: Option<(MessageId, EditMessageState)>,
|
||||||
expanded_tool_uses: HashMap<LanguageModelToolUseId, bool>,
|
expanded_tool_uses: HashMap<LanguageModelToolUseId, bool>,
|
||||||
expanded_thinking_segments: HashMap<(MessageId, usize), bool>,
|
expanded_thinking_segments: HashMap<(MessageId, usize), bool>,
|
||||||
|
expanded_code_blocks: HashMap<(MessageId, usize), bool>,
|
||||||
last_error: Option<ThreadError>,
|
last_error: Option<ThreadError>,
|
||||||
notifications: Vec<WindowHandle<AgentNotification>>,
|
notifications: Vec<WindowHandle<AgentNotification>>,
|
||||||
copied_code_block_ids: HashSet<(MessageId, usize)>,
|
copied_code_block_ids: HashSet<(MessageId, usize)>,
|
||||||
|
@ -297,7 +298,7 @@ fn render_markdown_code_block(
|
||||||
codeblock_range: Range<usize>,
|
codeblock_range: Range<usize>,
|
||||||
active_thread: Entity<ActiveThread>,
|
active_thread: Entity<ActiveThread>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
_window: &mut Window,
|
_window: &Window,
|
||||||
cx: &App,
|
cx: &App,
|
||||||
) -> Div {
|
) -> Div {
|
||||||
let label = match kind {
|
let label = match kind {
|
||||||
|
@ -377,16 +378,20 @@ fn render_markdown_code_block(
|
||||||
.rounded_sm()
|
.rounded_sm()
|
||||||
.hover(|item| item.bg(cx.theme().colors().element_hover.opacity(0.5)))
|
.hover(|item| item.bg(cx.theme().colors().element_hover.opacity(0.5)))
|
||||||
.tooltip(Tooltip::text("Jump to File"))
|
.tooltip(Tooltip::text("Jump to File"))
|
||||||
.children(
|
|
||||||
file_icons::FileIcons::get_icon(&path_range.path, cx)
|
|
||||||
.map(Icon::from_path)
|
|
||||||
.map(|icon| icon.color(Color::Muted).size(IconSize::XSmall)),
|
|
||||||
)
|
|
||||||
.child(content)
|
|
||||||
.child(
|
.child(
|
||||||
Icon::new(IconName::ArrowUpRight)
|
h_flex()
|
||||||
.size(IconSize::XSmall)
|
.gap_0p5()
|
||||||
.color(Color::Ignored),
|
.children(
|
||||||
|
file_icons::FileIcons::get_icon(&path_range.path, cx)
|
||||||
|
.map(Icon::from_path)
|
||||||
|
.map(|icon| icon.color(Color::Muted).size(IconSize::XSmall)),
|
||||||
|
)
|
||||||
|
.child(content)
|
||||||
|
.child(
|
||||||
|
Icon::new(IconName::ArrowUpRight)
|
||||||
|
.size(IconSize::XSmall)
|
||||||
|
.color(Color::Ignored),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.on_click({
|
.on_click({
|
||||||
let path_range = path_range.clone();
|
let path_range = path_range.clone();
|
||||||
|
@ -444,16 +449,29 @@ fn render_markdown_code_block(
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let codeblock_was_copied = active_thread
|
||||||
|
.read(cx)
|
||||||
|
.copied_code_block_ids
|
||||||
|
.contains(&(message_id, ix));
|
||||||
|
|
||||||
|
let is_expanded = active_thread
|
||||||
|
.read(cx)
|
||||||
|
.expanded_code_blocks
|
||||||
|
.get(&(message_id, ix))
|
||||||
|
.copied()
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
let codeblock_header_bg = cx
|
let codeblock_header_bg = cx
|
||||||
.theme()
|
.theme()
|
||||||
.colors()
|
.colors()
|
||||||
.element_background
|
.element_background
|
||||||
.blend(cx.theme().colors().editor_foreground.opacity(0.01));
|
.blend(cx.theme().colors().editor_foreground.opacity(0.01));
|
||||||
|
|
||||||
let codeblock_was_copied = active_thread
|
let line_count = without_fences(&parsed_markdown.source()[codeblock_range.clone()])
|
||||||
.read(cx)
|
.lines()
|
||||||
.copied_code_block_ids
|
.count();
|
||||||
.contains(&(message_id, ix));
|
|
||||||
|
const MAX_COLLAPSED_LINES: usize = 5;
|
||||||
|
|
||||||
let codeblock_header = h_flex()
|
let codeblock_header = h_flex()
|
||||||
.group("codeblock_header")
|
.group("codeblock_header")
|
||||||
|
@ -466,57 +484,104 @@ fn render_markdown_code_block(
|
||||||
.rounded_t_md()
|
.rounded_t_md()
|
||||||
.children(label)
|
.children(label)
|
||||||
.child(
|
.child(
|
||||||
div().visible_on_hover("codeblock_header").child(
|
h_flex()
|
||||||
IconButton::new(
|
.gap_1()
|
||||||
("copy-markdown-code", ix),
|
.child(
|
||||||
if codeblock_was_copied {
|
div().visible_on_hover("codeblock_header").child(
|
||||||
IconName::Check
|
IconButton::new(
|
||||||
} else {
|
("copy-markdown-code", ix),
|
||||||
IconName::Copy
|
if codeblock_was_copied {
|
||||||
},
|
IconName::Check
|
||||||
)
|
} else {
|
||||||
.icon_color(Color::Muted)
|
IconName::Copy
|
||||||
.shape(ui::IconButtonShape::Square)
|
},
|
||||||
.tooltip(Tooltip::text("Copy Code"))
|
)
|
||||||
.on_click({
|
.icon_color(Color::Muted)
|
||||||
let active_thread = active_thread.clone();
|
.shape(ui::IconButtonShape::Square)
|
||||||
let parsed_markdown = parsed_markdown.clone();
|
.tooltip(Tooltip::text("Copy Code"))
|
||||||
move |_event, _window, cx| {
|
.on_click({
|
||||||
active_thread.update(cx, |this, cx| {
|
let active_thread = active_thread.clone();
|
||||||
this.copied_code_block_ids.insert((message_id, ix));
|
let parsed_markdown = parsed_markdown.clone();
|
||||||
|
move |_event, _window, cx| {
|
||||||
|
active_thread.update(cx, |this, cx| {
|
||||||
|
this.copied_code_block_ids.insert((message_id, ix));
|
||||||
|
|
||||||
let code =
|
let code = without_fences(
|
||||||
without_fences(&parsed_markdown.source()[codeblock_range.clone()])
|
&parsed_markdown.source()[codeblock_range.clone()],
|
||||||
|
)
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
cx.write_to_clipboard(ClipboardItem::new_string(code.clone()));
|
cx.write_to_clipboard(ClipboardItem::new_string(code.clone()));
|
||||||
|
|
||||||
cx.spawn(async move |this, cx| {
|
cx.spawn(async move |this, cx| {
|
||||||
cx.background_executor().timer(Duration::from_secs(2)).await;
|
cx.background_executor()
|
||||||
|
.timer(Duration::from_secs(2))
|
||||||
|
.await;
|
||||||
|
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.copied_code_block_ids.remove(&(message_id, ix));
|
this.copied_code_block_ids
|
||||||
cx.notify();
|
.remove(&(message_id, ix));
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
})
|
})
|
||||||
})
|
.detach();
|
||||||
.ok();
|
});
|
||||||
})
|
}
|
||||||
.detach();
|
}),
|
||||||
});
|
),
|
||||||
}
|
)
|
||||||
|
.when(line_count > MAX_COLLAPSED_LINES, |header| {
|
||||||
|
header.child(
|
||||||
|
IconButton::new(
|
||||||
|
("expand-collapse-code", ix),
|
||||||
|
if is_expanded {
|
||||||
|
IconName::ChevronUp
|
||||||
|
} else {
|
||||||
|
IconName::ChevronDown
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.icon_color(Color::Muted)
|
||||||
|
.shape(ui::IconButtonShape::Square)
|
||||||
|
.tooltip(Tooltip::text(if is_expanded {
|
||||||
|
"Collapse Code"
|
||||||
|
} else {
|
||||||
|
"Expand Code"
|
||||||
|
}))
|
||||||
|
.on_click({
|
||||||
|
let active_thread = active_thread.clone();
|
||||||
|
move |_event, _window, cx| {
|
||||||
|
active_thread.update(cx, |this, cx| {
|
||||||
|
let is_expanded = this
|
||||||
|
.expanded_code_blocks
|
||||||
|
.entry((message_id, ix))
|
||||||
|
.or_insert(false);
|
||||||
|
*is_expanded = !*is_expanded;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.mb_2()
|
.my_2()
|
||||||
.relative()
|
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.rounded_lg()
|
.rounded_lg()
|
||||||
.border_1()
|
.border_1()
|
||||||
.border_color(cx.theme().colors().border_variant)
|
.border_color(cx.theme().colors().border_variant)
|
||||||
|
.bg(cx.theme().colors().editor_background)
|
||||||
.child(codeblock_header)
|
.child(codeblock_header)
|
||||||
|
.when(line_count > MAX_COLLAPSED_LINES, |this| {
|
||||||
|
if is_expanded {
|
||||||
|
this.h_full()
|
||||||
|
} else {
|
||||||
|
this.max_h_40()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open_markdown_link(
|
fn open_markdown_link(
|
||||||
|
@ -626,6 +691,7 @@ impl ActiveThread {
|
||||||
rendered_tool_uses: HashMap::default(),
|
rendered_tool_uses: HashMap::default(),
|
||||||
expanded_tool_uses: HashMap::default(),
|
expanded_tool_uses: HashMap::default(),
|
||||||
expanded_thinking_segments: HashMap::default(),
|
expanded_thinking_segments: HashMap::default(),
|
||||||
|
expanded_code_blocks: HashMap::default(),
|
||||||
list_state: list_state.clone(),
|
list_state: list_state.clone(),
|
||||||
scrollbar_state: ScrollbarState::new(list_state),
|
scrollbar_state: ScrollbarState::new(list_state),
|
||||||
show_scrollbar: false,
|
show_scrollbar: false,
|
||||||
|
@ -1835,10 +1901,10 @@ impl ActiveThread {
|
||||||
render: Arc::new({
|
render: Arc::new({
|
||||||
let workspace = workspace.clone();
|
let workspace = workspace.clone();
|
||||||
let active_thread = cx.entity();
|
let active_thread = cx.entity();
|
||||||
move |id, kind, parsed_markdown, range, window, cx| {
|
move |kind, parsed_markdown, range, window, cx| {
|
||||||
render_markdown_code_block(
|
render_markdown_code_block(
|
||||||
message_id,
|
message_id,
|
||||||
id,
|
range.start,
|
||||||
kind,
|
kind,
|
||||||
parsed_markdown,
|
parsed_markdown,
|
||||||
range,
|
range,
|
||||||
|
@ -1849,6 +1915,44 @@ impl ActiveThread {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
transform: Some(Arc::new({
|
||||||
|
let active_thread = cx.entity();
|
||||||
|
move |el, range, _, cx| {
|
||||||
|
let is_expanded = active_thread
|
||||||
|
.read(cx)
|
||||||
|
.expanded_code_blocks
|
||||||
|
.get(&(message_id, range.start))
|
||||||
|
.copied()
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if is_expanded {
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
el.child(
|
||||||
|
div()
|
||||||
|
.absolute()
|
||||||
|
.bottom_0()
|
||||||
|
.left_0()
|
||||||
|
.w_full()
|
||||||
|
.h_1_4()
|
||||||
|
.rounded_b_lg()
|
||||||
|
.bg(gpui::linear_gradient(
|
||||||
|
0.,
|
||||||
|
gpui::linear_color_stop(
|
||||||
|
cx.theme().colors().editor_background,
|
||||||
|
0.,
|
||||||
|
),
|
||||||
|
gpui::linear_color_stop(
|
||||||
|
cx.theme()
|
||||||
|
.colors()
|
||||||
|
.editor_background
|
||||||
|
.opacity(0.),
|
||||||
|
1.,
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})),
|
||||||
})
|
})
|
||||||
.on_url_click({
|
.on_url_click({
|
||||||
let workspace = self.workspace.clone();
|
let workspace = self.workspace.clone();
|
||||||
|
|
|
@ -88,12 +88,21 @@ struct Options {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum CodeBlockRenderer {
|
pub enum CodeBlockRenderer {
|
||||||
Default { copy_button: bool },
|
Default {
|
||||||
Custom { render: CodeBlockRenderFn },
|
copy_button: bool,
|
||||||
|
},
|
||||||
|
Custom {
|
||||||
|
render: CodeBlockRenderFn,
|
||||||
|
/// A function that can modify the parent container after the code block
|
||||||
|
/// content has been appended as a child element.
|
||||||
|
transform: Option<CodeBlockTransformFn>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type CodeBlockRenderFn =
|
pub type CodeBlockRenderFn =
|
||||||
Arc<dyn Fn(usize, &CodeBlockKind, &ParsedMarkdown, Range<usize>, &mut Window, &App) -> Div>;
|
Arc<dyn Fn(&CodeBlockKind, &ParsedMarkdown, Range<usize>, &mut Window, &App) -> Div>;
|
||||||
|
|
||||||
|
pub type CodeBlockTransformFn = Arc<dyn Fn(AnyDiv, Range<usize>, &mut Window, &App) -> AnyDiv>;
|
||||||
|
|
||||||
actions!(markdown, [Copy, CopyAsMarkdown]);
|
actions!(markdown, [Copy, CopyAsMarkdown]);
|
||||||
|
|
||||||
|
@ -594,7 +603,7 @@ impl Element for MarkdownElement {
|
||||||
0
|
0
|
||||||
};
|
};
|
||||||
|
|
||||||
for (index, (range, event)) in parsed_markdown.events.iter().enumerate() {
|
for (range, event) in parsed_markdown.events.iter() {
|
||||||
match event {
|
match event {
|
||||||
MarkdownEvent::Start(tag) => {
|
MarkdownEvent::Start(tag) => {
|
||||||
match tag {
|
match tag {
|
||||||
|
@ -676,15 +685,9 @@ impl Element for MarkdownElement {
|
||||||
builder.push_code_block(language);
|
builder.push_code_block(language);
|
||||||
builder.push_div(code_block, range, markdown_end);
|
builder.push_div(code_block, range, markdown_end);
|
||||||
}
|
}
|
||||||
(CodeBlockRenderer::Custom { render }, _) => {
|
(CodeBlockRenderer::Custom { render, .. }, _) => {
|
||||||
let parent_container = render(
|
let parent_container =
|
||||||
index,
|
render(kind, &parsed_markdown, range.clone(), window, cx);
|
||||||
kind,
|
|
||||||
&parsed_markdown,
|
|
||||||
range.clone(),
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
|
|
||||||
builder.push_div(parent_container, range, markdown_end);
|
builder.push_div(parent_container, range, markdown_end);
|
||||||
|
|
||||||
|
@ -695,9 +698,12 @@ impl Element for MarkdownElement {
|
||||||
if self.style.code_block_overflow_x_scroll {
|
if self.style.code_block_overflow_x_scroll {
|
||||||
code_block.style().restrict_scroll_to_axis =
|
code_block.style().restrict_scroll_to_axis =
|
||||||
Some(true);
|
Some(true);
|
||||||
code_block.flex().overflow_x_scroll()
|
code_block
|
||||||
|
.flex()
|
||||||
|
.overflow_x_scroll()
|
||||||
|
.overflow_y_hidden()
|
||||||
} else {
|
} else {
|
||||||
code_block.w_full()
|
code_block.w_full().overflow_hidden()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -846,6 +852,14 @@ impl Element for MarkdownElement {
|
||||||
builder.pop_text_style();
|
builder.pop_text_style();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let CodeBlockRenderer::Custom {
|
||||||
|
transform: Some(modify),
|
||||||
|
..
|
||||||
|
} = &self.code_block_renderer
|
||||||
|
{
|
||||||
|
builder.modify_current_div(|el| modify(el, range.clone(), window, cx));
|
||||||
|
}
|
||||||
|
|
||||||
if matches!(
|
if matches!(
|
||||||
&self.code_block_renderer,
|
&self.code_block_renderer,
|
||||||
CodeBlockRenderer::Default { copy_button: true }
|
CodeBlockRenderer::Default { copy_button: true }
|
||||||
|
@ -1049,7 +1063,7 @@ impl IntoElement for MarkdownElement {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum AnyDiv {
|
pub enum AnyDiv {
|
||||||
Div(Div),
|
Div(Div),
|
||||||
Stateful(Stateful<Div>),
|
Stateful(Stateful<Div>),
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue