Make slash command output streamable (#19632)
This PR adds support for streaming output from slash commands In this PR we are focused primarily on the interface of the `SlashCommand` trait to support streaming the output. We will follow up later with support for extensions and context servers to take advantage of the streaming nature. Release Notes: - N/A --------- Co-authored-by: David Soria Parra <davidsp@anthropic.com> Co-authored-by: Antonio Scandurra <me@as-cii.com> Co-authored-by: David <david@anthropic.com> Co-authored-by: Antonio <antonio@zed.dev> Co-authored-by: Max <max@zed.dev> Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com> Co-authored-by: Will <will@zed.dev>
This commit is contained in:
parent
f6fbf662b4
commit
b129e18396
14 changed files with 1130 additions and 501 deletions
|
@ -1,3 +1,4 @@
|
|||
use crate::slash_command::file_command::codeblock_fence_for_path;
|
||||
use crate::{
|
||||
assistant_settings::{AssistantDockPosition, AssistantSettings},
|
||||
humanize_token_count,
|
||||
|
@ -6,24 +7,23 @@ use crate::{
|
|||
slash_command::{
|
||||
default_command::DefaultSlashCommand,
|
||||
docs_command::{DocsSlashCommand, DocsSlashCommandArgs},
|
||||
file_command::{self, codeblock_fence_for_path},
|
||||
SlashCommandCompletionProvider, SlashCommandRegistry,
|
||||
file_command, SlashCommandCompletionProvider, SlashCommandRegistry,
|
||||
},
|
||||
slash_command_picker,
|
||||
terminal_inline_assistant::TerminalInlineAssistant,
|
||||
Assist, AssistantPatch, AssistantPatchStatus, CacheStatus, ConfirmCommand, Content, Context,
|
||||
ContextEvent, ContextId, ContextStore, ContextStoreEvent, CopyCode, CycleMessageRole,
|
||||
DeployHistory, DeployPromptLibrary, Edit, InlineAssistant, InsertDraggedFiles,
|
||||
InsertIntoEditor, Message, MessageId, MessageMetadata, MessageStatus, ModelPickerDelegate,
|
||||
ModelSelector, NewContext, PendingSlashCommand, PendingSlashCommandStatus, QuoteSelection,
|
||||
RemoteContextMetadata, RequestType, SavedContextMetadata, Split, ToggleFocus,
|
||||
ToggleModelSelector,
|
||||
InsertIntoEditor, InvokedSlashCommandStatus, Message, MessageId, MessageMetadata,
|
||||
MessageStatus, ModelPickerDelegate, ModelSelector, NewContext, ParsedSlashCommand,
|
||||
PendingSlashCommandStatus, QuoteSelection, RemoteContextMetadata, RequestType,
|
||||
SavedContextMetadata, SlashCommandId, Split, ToggleFocus, ToggleModelSelector,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection};
|
||||
use assistant_tool::ToolRegistry;
|
||||
use client::{proto, zed_urls, Client, Status};
|
||||
use collections::{BTreeSet, HashMap, HashSet};
|
||||
use collections::{hash_map, BTreeSet, HashMap, HashSet};
|
||||
use editor::{
|
||||
actions::{FoldAt, MoveToEndOfLine, Newline, ShowCompletions, UnfoldAt},
|
||||
display_map::{
|
||||
|
@ -38,12 +38,12 @@ use editor::{display_map::CreaseId, FoldPlaceholder};
|
|||
use fs::Fs;
|
||||
use futures::FutureExt;
|
||||
use gpui::{
|
||||
canvas, div, img, percentage, point, pulsating_between, size, Action, Animation, AnimationExt,
|
||||
AnyElement, AnyView, AppContext, AsyncWindowContext, ClipboardEntry, ClipboardItem,
|
||||
CursorStyle, Empty, Entity, EventEmitter, ExternalPaths, FocusHandle, FocusableView,
|
||||
FontWeight, InteractiveElement, IntoElement, Model, ParentElement, Pixels, Render, RenderImage,
|
||||
SharedString, Size, StatefulInteractiveElement, Styled, Subscription, Task, Transformation,
|
||||
UpdateGlobal, View, VisualContext, WeakView, WindowContext,
|
||||
canvas, div, img, percentage, point, prelude::*, pulsating_between, size, Action, Animation,
|
||||
AnimationExt, AnyElement, AnyView, AppContext, AsyncWindowContext, ClipboardEntry,
|
||||
ClipboardItem, CursorStyle, Empty, Entity, EventEmitter, ExternalPaths, FocusHandle,
|
||||
FocusableView, FontWeight, InteractiveElement, IntoElement, Model, ParentElement, Pixels,
|
||||
Render, RenderImage, SharedString, Size, StatefulInteractiveElement, Styled, Subscription,
|
||||
Task, Transformation, UpdateGlobal, View, WeakModel, WeakView,
|
||||
};
|
||||
use indexed_docs::IndexedDocsStore;
|
||||
use language::{
|
||||
|
@ -77,8 +77,8 @@ use text::SelectionGoal;
|
|||
use ui::{
|
||||
prelude::*,
|
||||
utils::{format_distance_from_now, DateTimeType},
|
||||
Avatar, ButtonLike, ContextMenu, Disclosure, ElevationIndex, KeyBinding, ListItem,
|
||||
ListItemSpacing, PopoverMenu, PopoverMenuHandle, TintColor, Tooltip,
|
||||
Avatar, ButtonLike, ContextMenu, Disclosure, ElevationIndex, IconButtonShape, KeyBinding,
|
||||
ListItem, ListItemSpacing, PopoverMenu, PopoverMenuHandle, TintColor, Tooltip,
|
||||
};
|
||||
use util::{maybe, ResultExt};
|
||||
use workspace::{
|
||||
|
@ -1477,7 +1477,7 @@ pub struct ContextEditor {
|
|||
scroll_position: Option<ScrollPosition>,
|
||||
remote_id: Option<workspace::ViewId>,
|
||||
pending_slash_command_creases: HashMap<Range<language::Anchor>, CreaseId>,
|
||||
pending_slash_command_blocks: HashMap<Range<language::Anchor>, CustomBlockId>,
|
||||
invoked_slash_command_creases: HashMap<SlashCommandId, CreaseId>,
|
||||
pending_tool_use_creases: HashMap<Range<language::Anchor>, CreaseId>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
patches: HashMap<Range<language::Anchor>, PatchViewState>,
|
||||
|
@ -1548,7 +1548,7 @@ impl ContextEditor {
|
|||
workspace,
|
||||
project,
|
||||
pending_slash_command_creases: HashMap::default(),
|
||||
pending_slash_command_blocks: HashMap::default(),
|
||||
invoked_slash_command_creases: HashMap::default(),
|
||||
pending_tool_use_creases: HashMap::default(),
|
||||
_subscriptions,
|
||||
patches: HashMap::default(),
|
||||
|
@ -1573,14 +1573,13 @@ impl ContextEditor {
|
|||
});
|
||||
let command = self.context.update(cx, |context, cx| {
|
||||
context.reparse(cx);
|
||||
context.pending_slash_commands()[0].clone()
|
||||
context.parsed_slash_commands()[0].clone()
|
||||
});
|
||||
self.run_command(
|
||||
command.source_range,
|
||||
&command.name,
|
||||
&command.arguments,
|
||||
false,
|
||||
false,
|
||||
self.workspace.clone(),
|
||||
cx,
|
||||
);
|
||||
|
@ -1753,7 +1752,6 @@ impl ContextEditor {
|
|||
&command.name,
|
||||
&command.arguments,
|
||||
true,
|
||||
false,
|
||||
workspace.clone(),
|
||||
cx,
|
||||
);
|
||||
|
@ -1769,7 +1767,6 @@ impl ContextEditor {
|
|||
name: &str,
|
||||
arguments: &[String],
|
||||
ensure_trailing_newline: bool,
|
||||
expand_result: bool,
|
||||
workspace: WeakView<Workspace>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
|
@ -1793,9 +1790,9 @@ impl ContextEditor {
|
|||
self.context.update(cx, |context, cx| {
|
||||
context.insert_command_output(
|
||||
command_range,
|
||||
name,
|
||||
output,
|
||||
ensure_trailing_newline,
|
||||
expand_result,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
@ -1865,8 +1862,7 @@ impl ContextEditor {
|
|||
IconName::PocketKnife,
|
||||
tool_use.name.clone().into(),
|
||||
),
|
||||
constrain_width: false,
|
||||
merge_adjacent: false,
|
||||
..Default::default()
|
||||
};
|
||||
let render_trailer =
|
||||
move |_row, _unfold, _cx: &mut WindowContext| Empty.into_any();
|
||||
|
@ -1921,11 +1917,10 @@ impl ContextEditor {
|
|||
ContextEvent::PatchesUpdated { removed, updated } => {
|
||||
self.patches_updated(removed, updated, cx);
|
||||
}
|
||||
ContextEvent::PendingSlashCommandsUpdated { removed, updated } => {
|
||||
ContextEvent::ParsedSlashCommandsUpdated { removed, updated } => {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let buffer = editor.buffer().read(cx).snapshot(cx);
|
||||
let (excerpt_id, buffer_id, _) = buffer.as_singleton().unwrap();
|
||||
let excerpt_id = *excerpt_id;
|
||||
let (&excerpt_id, _, _) = buffer.as_singleton().unwrap();
|
||||
|
||||
editor.remove_creases(
|
||||
removed
|
||||
|
@ -1934,16 +1929,6 @@ impl ContextEditor {
|
|||
cx,
|
||||
);
|
||||
|
||||
editor.remove_blocks(
|
||||
HashSet::from_iter(
|
||||
removed.iter().filter_map(|range| {
|
||||
self.pending_slash_command_blocks.remove(range)
|
||||
}),
|
||||
),
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
|
||||
let crease_ids = editor.insert_creases(
|
||||
updated.iter().map(|command| {
|
||||
let workspace = self.workspace.clone();
|
||||
|
@ -1958,7 +1943,6 @@ impl ContextEditor {
|
|||
&command.name,
|
||||
&command.arguments,
|
||||
false,
|
||||
false,
|
||||
workspace.clone(),
|
||||
cx,
|
||||
);
|
||||
|
@ -1968,8 +1952,7 @@ impl ContextEditor {
|
|||
});
|
||||
let placeholder = FoldPlaceholder {
|
||||
render: Arc::new(move |_, _, _| Empty.into_any()),
|
||||
constrain_width: false,
|
||||
merge_adjacent: false,
|
||||
..Default::default()
|
||||
};
|
||||
let render_toggle = {
|
||||
let confirm_command = confirm_command.clone();
|
||||
|
@ -2011,62 +1994,29 @@ impl ContextEditor {
|
|||
cx,
|
||||
);
|
||||
|
||||
let block_ids = editor.insert_blocks(
|
||||
updated
|
||||
.iter()
|
||||
.filter_map(|command| match &command.status {
|
||||
PendingSlashCommandStatus::Error(error) => {
|
||||
Some((command, error.clone()))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.map(|(command, error_message)| BlockProperties {
|
||||
style: BlockStyle::Fixed,
|
||||
height: 1,
|
||||
placement: BlockPlacement::Below(Anchor {
|
||||
buffer_id: Some(buffer_id),
|
||||
excerpt_id,
|
||||
text_anchor: command.source_range.start,
|
||||
}),
|
||||
render: slash_command_error_block_renderer(error_message),
|
||||
priority: 0,
|
||||
}),
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
|
||||
self.pending_slash_command_creases.extend(
|
||||
updated
|
||||
.iter()
|
||||
.map(|command| command.source_range.clone())
|
||||
.zip(crease_ids),
|
||||
);
|
||||
|
||||
self.pending_slash_command_blocks.extend(
|
||||
updated
|
||||
.iter()
|
||||
.map(|command| command.source_range.clone())
|
||||
.zip(block_ids),
|
||||
);
|
||||
})
|
||||
}
|
||||
ContextEvent::InvokedSlashCommandChanged { command_id } => {
|
||||
self.update_invoked_slash_command(*command_id, cx);
|
||||
}
|
||||
ContextEvent::SlashCommandOutputSectionAdded { section } => {
|
||||
self.insert_slash_command_output_sections([section.clone()], false, cx);
|
||||
}
|
||||
ContextEvent::SlashCommandFinished {
|
||||
output_range,
|
||||
sections,
|
||||
run_commands_in_output,
|
||||
expand_result,
|
||||
output_range: _output_range,
|
||||
run_commands_in_ranges,
|
||||
} => {
|
||||
self.insert_slash_command_output_sections(
|
||||
sections.iter().cloned(),
|
||||
*expand_result,
|
||||
cx,
|
||||
);
|
||||
|
||||
if *run_commands_in_output {
|
||||
for range in run_commands_in_ranges {
|
||||
let commands = self.context.update(cx, |context, cx| {
|
||||
context.reparse(cx);
|
||||
context
|
||||
.pending_commands_for_range(output_range.clone(), cx)
|
||||
.pending_commands_for_range(range.clone(), cx)
|
||||
.to_vec()
|
||||
});
|
||||
|
||||
|
@ -2076,7 +2026,6 @@ impl ContextEditor {
|
|||
&command.name,
|
||||
&command.arguments,
|
||||
false,
|
||||
false,
|
||||
self.workspace.clone(),
|
||||
cx,
|
||||
);
|
||||
|
@ -2119,8 +2068,7 @@ impl ContextEditor {
|
|||
IconName::PocketKnife,
|
||||
format!("Tool Result: {tool_use_id}").into(),
|
||||
),
|
||||
constrain_width: false,
|
||||
merge_adjacent: false,
|
||||
..Default::default()
|
||||
};
|
||||
let render_trailer =
|
||||
move |_row, _unfold, _cx: &mut WindowContext| Empty.into_any();
|
||||
|
@ -2158,6 +2106,77 @@ impl ContextEditor {
|
|||
}
|
||||
}
|
||||
|
||||
fn update_invoked_slash_command(
|
||||
&mut self,
|
||||
command_id: SlashCommandId,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let context_editor = cx.view().downgrade();
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
if let Some(invoked_slash_command) =
|
||||
self.context.read(cx).invoked_slash_command(&command_id)
|
||||
{
|
||||
if let InvokedSlashCommandStatus::Finished = invoked_slash_command.status {
|
||||
let buffer = editor.buffer().read(cx).snapshot(cx);
|
||||
let (&excerpt_id, _buffer_id, _buffer_snapshot) =
|
||||
buffer.as_singleton().unwrap();
|
||||
|
||||
let start = buffer
|
||||
.anchor_in_excerpt(excerpt_id, invoked_slash_command.range.start)
|
||||
.unwrap();
|
||||
let end = buffer
|
||||
.anchor_in_excerpt(excerpt_id, invoked_slash_command.range.end)
|
||||
.unwrap();
|
||||
editor.remove_folds_with_type(
|
||||
&[start..end],
|
||||
TypeId::of::<PendingSlashCommand>(),
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
|
||||
editor.remove_creases(
|
||||
HashSet::from_iter(self.invoked_slash_command_creases.remove(&command_id)),
|
||||
cx,
|
||||
);
|
||||
} else if let hash_map::Entry::Vacant(entry) =
|
||||
self.invoked_slash_command_creases.entry(command_id)
|
||||
{
|
||||
let buffer = editor.buffer().read(cx).snapshot(cx);
|
||||
let (&excerpt_id, _buffer_id, _buffer_snapshot) =
|
||||
buffer.as_singleton().unwrap();
|
||||
let context = self.context.downgrade();
|
||||
let crease_start = buffer
|
||||
.anchor_in_excerpt(excerpt_id, invoked_slash_command.range.start)
|
||||
.unwrap();
|
||||
let crease_end = buffer
|
||||
.anchor_in_excerpt(excerpt_id, invoked_slash_command.range.end)
|
||||
.unwrap();
|
||||
let fold_placeholder =
|
||||
invoked_slash_command_fold_placeholder(command_id, context, context_editor);
|
||||
let crease_ids = editor.insert_creases(
|
||||
[Crease::new(
|
||||
crease_start..crease_end,
|
||||
fold_placeholder.clone(),
|
||||
fold_toggle("invoked-slash-command"),
|
||||
|_row, _folded, _cx| Empty.into_any(),
|
||||
)],
|
||||
cx,
|
||||
);
|
||||
editor.fold_ranges([(crease_start..crease_end, fold_placeholder)], false, cx);
|
||||
entry.insert(crease_ids[0]);
|
||||
} else {
|
||||
cx.notify()
|
||||
}
|
||||
} else {
|
||||
editor.remove_creases(
|
||||
HashSet::from_iter(self.invoked_slash_command_creases.remove(&command_id)),
|
||||
cx,
|
||||
);
|
||||
cx.notify();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
fn patches_updated(
|
||||
&mut self,
|
||||
removed: &Vec<Range<text::Anchor>>,
|
||||
|
@ -2229,8 +2248,7 @@ impl ContextEditor {
|
|||
.unwrap_or_else(|| Empty.into_any())
|
||||
})
|
||||
},
|
||||
constrain_width: false,
|
||||
merge_adjacent: false,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let should_refold;
|
||||
|
@ -2288,7 +2306,7 @@ impl ContextEditor {
|
|||
}
|
||||
|
||||
if should_refold {
|
||||
editor.unfold_ranges([patch_start..patch_end], true, false, cx);
|
||||
editor.unfold_ranges(&[patch_start..patch_end], true, false, cx);
|
||||
editor.fold_ranges([(patch_start..patch_end, header_placeholder)], false, cx);
|
||||
}
|
||||
}
|
||||
|
@ -2334,8 +2352,7 @@ impl ContextEditor {
|
|||
section.icon,
|
||||
section.label.clone(),
|
||||
),
|
||||
constrain_width: false,
|
||||
merge_adjacent: false,
|
||||
..Default::default()
|
||||
},
|
||||
render_slash_command_output_toggle,
|
||||
|_, _, _| Empty.into_any_element(),
|
||||
|
@ -3275,13 +3292,12 @@ impl ContextEditor {
|
|||
Crease::new(
|
||||
start..end,
|
||||
FoldPlaceholder {
|
||||
constrain_width: false,
|
||||
render: render_fold_icon_button(
|
||||
weak_editor.clone(),
|
||||
metadata.crease.icon,
|
||||
metadata.crease.label.clone(),
|
||||
),
|
||||
merge_adjacent: false,
|
||||
..Default::default()
|
||||
},
|
||||
render_slash_command_output_toggle,
|
||||
|_, _, _| Empty.into_any(),
|
||||
|
@ -4947,8 +4963,7 @@ fn quote_selection_fold_placeholder(title: String, editor: WeakView<Editor>) ->
|
|||
.into_any_element()
|
||||
}
|
||||
}),
|
||||
constrain_width: false,
|
||||
merge_adjacent: false,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4992,7 +5007,7 @@ fn render_pending_slash_command_gutter_decoration(
|
|||
|
||||
fn render_docs_slash_command_trailer(
|
||||
row: MultiBufferRow,
|
||||
command: PendingSlashCommand,
|
||||
command: ParsedSlashCommand,
|
||||
cx: &mut WindowContext,
|
||||
) -> AnyElement {
|
||||
if command.arguments.is_empty() {
|
||||
|
@ -5076,17 +5091,78 @@ fn make_lsp_adapter_delegate(
|
|||
})
|
||||
}
|
||||
|
||||
fn slash_command_error_block_renderer(message: String) -> RenderBlock {
|
||||
Box::new(move |_| {
|
||||
div()
|
||||
.pl_6()
|
||||
.child(
|
||||
Label::new(format!("error: {}", message))
|
||||
.single_line()
|
||||
.color(Color::Error),
|
||||
)
|
||||
.into_any()
|
||||
})
|
||||
enum PendingSlashCommand {}
|
||||
|
||||
fn invoked_slash_command_fold_placeholder(
|
||||
command_id: SlashCommandId,
|
||||
context: WeakModel<Context>,
|
||||
context_editor: WeakView<ContextEditor>,
|
||||
) -> FoldPlaceholder {
|
||||
FoldPlaceholder {
|
||||
constrain_width: false,
|
||||
merge_adjacent: false,
|
||||
render: Arc::new(move |fold_id, _, cx| {
|
||||
let Some(context) = context.upgrade() else {
|
||||
return Empty.into_any();
|
||||
};
|
||||
|
||||
let Some(command) = context.read(cx).invoked_slash_command(&command_id) else {
|
||||
return Empty.into_any();
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.id(fold_id)
|
||||
.px_1()
|
||||
.ml_6()
|
||||
.gap_2()
|
||||
.bg(cx.theme().colors().surface_background)
|
||||
.rounded_md()
|
||||
.child(Label::new(format!("/{}", command.name.clone())))
|
||||
.map(|parent| match &command.status {
|
||||
InvokedSlashCommandStatus::Running(_) => {
|
||||
parent.child(Icon::new(IconName::ArrowCircle).with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(4)).repeat(),
|
||||
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||
))
|
||||
}
|
||||
InvokedSlashCommandStatus::Error(message) => parent
|
||||
.child(
|
||||
Label::new(format!("error: {message}"))
|
||||
.single_line()
|
||||
.color(Color::Error),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("dismiss-error", IconName::Close)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.on_click({
|
||||
let context_editor = context_editor.clone();
|
||||
move |_event, cx| {
|
||||
context_editor
|
||||
.update(cx, |context_editor, cx| {
|
||||
context_editor.editor.update(cx, |editor, cx| {
|
||||
editor.remove_creases(
|
||||
HashSet::from_iter(
|
||||
context_editor
|
||||
.invoked_slash_command_creases
|
||||
.remove(&command_id),
|
||||
),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}),
|
||||
),
|
||||
InvokedSlashCommandStatus::Finished => parent,
|
||||
})
|
||||
.into_any_element()
|
||||
}),
|
||||
type_tag: Some(TypeId::of::<PendingSlashCommand>()),
|
||||
}
|
||||
}
|
||||
|
||||
enum TokenState {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue