Show inline assistant errors

This commit is contained in:
Antonio Scandurra 2023-08-30 11:04:48 +02:00
parent 87e25c8c23
commit 5c498c8610

View File

@ -40,7 +40,7 @@ use std::{
cell::{Cell, RefCell},
cmp, env,
fmt::Write,
iter,
future, iter,
ops::Range,
path::{Path, PathBuf},
rc::Rc,
@ -55,7 +55,7 @@ use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt};
use workspace::{
dock::{DockPosition, Panel},
searchable::Direction,
Save, ToggleZoom, Toolbar, Workspace,
Save, Toast, ToggleZoom, Toolbar, Workspace,
};
actions!(
@ -290,6 +290,7 @@ impl AssistantPanel {
has_focus: false,
include_conversation: self.include_conversation_in_next_inline_assist,
measurements: measurements.clone(),
error: None,
};
cx.focus_self();
assistant
@ -331,7 +332,7 @@ impl AssistantPanel {
editor: editor.downgrade(),
range,
highlighted_ranges: Default::default(),
inline_assistant_block_id: Some(block_id),
inline_assistant: Some((block_id, inline_assistant.clone())),
code_generation: Task::ready(None),
transaction_id: None,
_subscriptions: vec![
@ -477,7 +478,7 @@ impl AssistantPanel {
fn hide_inline_assist(&mut self, assist_id: usize, cx: &mut ViewContext<Self>) {
if let Some(pending_assist) = self.pending_inline_assists.get_mut(&assist_id) {
if let Some(editor) = pending_assist.editor.upgrade(cx) {
if let Some(block_id) = pending_assist.inline_assistant_block_id.take() {
if let Some((block_id, _)) = pending_assist.inline_assistant.take() {
editor.update(cx, |editor, cx| {
editor.remove_blocks(HashSet::from_iter([block_id]), None, cx);
});
@ -699,22 +700,17 @@ impl AssistantPanel {
pending_assist.code_generation = cx.spawn(|this, mut cx| {
async move {
let _cleanup = util::defer({
let mut cx = cx.clone();
let this = this.clone();
move || {
let _ = this.update(&mut cx, |this, cx| {
this.close_inline_assist(inline_assist_id, false, cx)
});
}
});
let mut edit_start = range.start.to_offset(&snapshot);
let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);
let diff = cx.background().spawn(async move {
let chunks = strip_markdown_codeblock(response.await?.filter_map(
|message| async move { message.ok()?.choices.pop()?.delta.content },
|message| async move {
match message {
Ok(mut message) => Some(Ok(message.choices.pop()?.delta.content?)),
Err(error) => Some(Err(error)),
}
},
));
futures::pin_mut!(chunks);
let mut diff = StreamingDiff::new(selected_text.to_string());
@ -737,6 +733,7 @@ impl AssistantPanel {
let mut new_text = String::new();
while let Some(chunk) = chunks.next().await {
let chunk = chunk?;
if first_chunk && (chunk.starts_with(' ') || chunk.starts_with('\t')) {
autoindent = false;
}
@ -771,9 +768,17 @@ impl AssistantPanel {
});
while let Some(hunks) = hunks_rx.next().await {
let editor = editor
.upgrade(&cx)
.ok_or_else(|| anyhow!("editor was dropped"))?;
let editor = if let Some(editor) = editor.upgrade(&cx) {
editor
} else {
break;
};
let this = if let Some(this) = this.upgrade(&cx) {
this
} else {
break;
};
this.update(&mut cx, |this, cx| {
let pending_assist = if let Some(pending_assist) =
@ -840,9 +845,42 @@ impl AssistantPanel {
});
this.update_highlights_for_editor(&editor, cx);
})?;
});
}
if let Err(error) = diff.await {
this.update(&mut cx, |this, cx| {
let pending_assist = if let Some(pending_assist) =
this.pending_inline_assists.get_mut(&inline_assist_id)
{
pending_assist
} else {
return;
};
if let Some((_, inline_assistant)) =
pending_assist.inline_assistant.as_ref()
{
inline_assistant.update(cx, |inline_assistant, cx| {
inline_assistant.set_error(error, cx);
});
} else if let Some(workspace) = this.workspace.upgrade(cx) {
workspace.update(cx, |workspace, cx| {
workspace.show_toast(
Toast::new(
inline_assist_id,
format!("Inline assistant error: {}", error),
),
cx,
);
})
}
})?;
} else {
let _ = this.update(&mut cx, |this, cx| {
this.close_inline_assist(inline_assist_id, false, cx)
});
}
diff.await?;
anyhow::Ok(())
}
@ -2856,6 +2894,7 @@ struct InlineAssistant {
has_focus: bool,
include_conversation: bool,
measurements: Rc<Cell<BlockMeasurements>>,
error: Option<anyhow::Error>,
}
impl Entity for InlineAssistant {
@ -2868,17 +2907,42 @@ impl View for InlineAssistant {
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
enum ErrorIcon {}
let theme = theme::current(cx);
Flex::row()
.with_child(
Button::action(ToggleIncludeConversation)
.with_tooltip("Include Conversation", theme.tooltip.clone())
.with_id(self.id)
.with_contents(theme::components::svg::Svg::new("icons/ai.svg"))
.toggleable(self.include_conversation)
.with_style(theme.assistant.inline.include_conversation.clone())
.element()
Flex::row()
.with_child(
Button::action(ToggleIncludeConversation)
.with_tooltip("Include Conversation", theme.tooltip.clone())
.with_id(self.id)
.with_contents(theme::components::svg::Svg::new("icons/ai.svg"))
.toggleable(self.include_conversation)
.with_style(theme.assistant.inline.include_conversation.clone())
.element()
.aligned(),
)
.with_children(if let Some(error) = self.error.as_ref() {
Some(
Svg::new("icons/circle_x_mark_12.svg")
.with_color(theme.assistant.error_icon.color)
.constrained()
.with_width(theme.assistant.error_icon.width)
.contained()
.with_style(theme.assistant.error_icon.container)
.with_tooltip::<ErrorIcon>(
self.id,
error.to_string(),
None,
theme.tooltip.clone(),
cx,
)
.aligned(),
)
} else {
None
})
.aligned()
.constrained()
.dynamically({
@ -2954,6 +3018,8 @@ impl InlineAssistant {
include_conversation: self.include_conversation,
});
self.confirmed = true;
self.error = None;
cx.notify();
}
}
@ -2968,6 +3034,19 @@ impl InlineAssistant {
});
cx.notify();
}
fn set_error(&mut self, error: anyhow::Error, cx: &mut ViewContext<Self>) {
self.error = Some(error);
self.confirmed = false;
self.prompt_editor.update(cx, |editor, cx| {
editor.set_read_only(false);
editor.set_field_editor_style(
Some(Arc::new(|theme| theme.assistant.inline.editor.clone())),
cx,
);
});
cx.notify();
}
}
// This wouldn't need to exist if we could pass parameters when rendering child views.
@ -2982,7 +3061,7 @@ struct PendingInlineAssist {
editor: WeakViewHandle<Editor>,
range: Range<Anchor>,
highlighted_ranges: Vec<Range<Anchor>>,
inline_assistant_block_id: Option<BlockId>,
inline_assistant: Option<(BlockId, ViewHandle<InlineAssistant>)>,
code_generation: Task<Option<()>>,
transaction_id: Option<TransactionId>,
_subscriptions: Vec<Subscription>,
@ -3010,23 +3089,29 @@ fn merge_ranges(ranges: &mut Vec<Range<Anchor>>, buffer: &MultiBufferSnapshot) {
}
}
fn strip_markdown_codeblock(stream: impl Stream<Item = String>) -> impl Stream<Item = String> {
fn strip_markdown_codeblock(
stream: impl Stream<Item = Result<String>>,
) -> impl Stream<Item = Result<String>> {
let mut first_line = true;
let mut buffer = String::new();
let mut starts_with_fenced_code_block = false;
stream.filter_map(move |chunk| {
let chunk = match chunk {
Ok(chunk) => chunk,
Err(err) => return future::ready(Some(Err(err))),
};
buffer.push_str(&chunk);
if first_line {
if buffer == "" || buffer == "`" || buffer == "``" {
return futures::future::ready(None);
return future::ready(None);
} else if buffer.starts_with("```") {
starts_with_fenced_code_block = true;
if let Some(newline_ix) = buffer.find('\n') {
buffer.replace_range(..newline_ix + 1, "");
first_line = false;
} else {
return futures::future::ready(None);
return future::ready(None);
}
}
}
@ -3050,10 +3135,10 @@ fn strip_markdown_codeblock(stream: impl Stream<Item = String>) -> impl Stream<I
let result = if buffer.is_empty() {
None
} else {
Some(buffer.clone())
Some(Ok(buffer.clone()))
};
buffer = remainder;
futures::future::ready(result)
future::ready(result)
})
}
@ -3434,41 +3519,46 @@ mod tests {
async fn test_strip_markdown_codeblock() {
assert_eq!(
strip_markdown_codeblock(chunks("Lorem ipsum dolor", 2))
.map(|chunk| chunk.unwrap())
.collect::<String>()
.await,
"Lorem ipsum dolor"
);
assert_eq!(
strip_markdown_codeblock(chunks("```\nLorem ipsum dolor", 2))
.map(|chunk| chunk.unwrap())
.collect::<String>()
.await,
"Lorem ipsum dolor"
);
assert_eq!(
strip_markdown_codeblock(chunks("```\nLorem ipsum dolor\n```", 2))
.map(|chunk| chunk.unwrap())
.collect::<String>()
.await,
"Lorem ipsum dolor"
);
assert_eq!(
strip_markdown_codeblock(chunks("```html\n```js\nLorem ipsum dolor\n```\n```", 2))
.map(|chunk| chunk.unwrap())
.collect::<String>()
.await,
"```js\nLorem ipsum dolor\n```"
);
assert_eq!(
strip_markdown_codeblock(chunks("``\nLorem ipsum dolor\n```", 2))
.map(|chunk| chunk.unwrap())
.collect::<String>()
.await,
"``\nLorem ipsum dolor\n```"
);
fn chunks(text: &str, size: usize) -> impl Stream<Item = String> {
fn chunks(text: &str, size: usize) -> impl Stream<Item = Result<String>> {
stream::iter(
text.chars()
.collect::<Vec<_>>()
.chunks(size)
.map(|chunk| chunk.iter().collect::<String>())
.map(|chunk| Ok(chunk.iter().collect::<String>()))
.collect::<Vec<_>>(),
)
}