Skip to content

Commit f3140f5

Browse files
authored
assistant2: Wire up error messages (#21426)
This PR wires up the error messages for Assistant 2 so that they are shown to the user: <img width="1138" alt="Screenshot 2024-12-02 at 4 28 02 PM" src="https://github.com/user-attachments/assets/d8a5b9bd-0cef-4304-b561-b2edadbc70ef"> <img width="1138" alt="Screenshot 2024-12-02 at 4 29 09 PM" src="https://github.com/user-attachments/assets/0dd70841-0d5a-4de6-bebe-82c563246b65"> <img width="1138" alt="Screenshot 2024-12-02 at 4 32 49 PM" src="https://github.com/user-attachments/assets/a8838866-fad1-43a9-8935-490dc1936016"> @danilo-leal I kept the existing UX from Assistant 1, as I didn't see any errors in the design prototype, but we can revisit if another approach would work better. Release Notes: - N/A
1 parent 72afe68 commit f3140f5

File tree

5 files changed

+194
-28
lines changed

5 files changed

+194
-28
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/assistant2/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ doctest = false
1515
[dependencies]
1616
anyhow.workspace = true
1717
assistant_tool.workspace = true
18+
client.workspace = true
1819
collections.workspace = true
1920
command_palette_hooks.workspace = true
2021
context_server.workspace = true
@@ -24,6 +25,7 @@ futures.workspace = true
2425
gpui.workspace = true
2526
language_model.workspace = true
2627
language_model_selector.workspace = true
28+
language_models.workspace = true
2729
log.workspace = true
2830
project.workspace = true
2931
proto.workspace = true

crates/assistant2/src/assistant_panel.rs

Lines changed: 157 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ use std::sync::Arc;
22

33
use anyhow::Result;
44
use assistant_tool::ToolWorkingSet;
5+
use client::zed_urls;
56
use gpui::{
6-
prelude::*, px, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle,
7-
FocusableView, Model, Pixels, Subscription, Task, View, ViewContext, WeakView, WindowContext,
7+
prelude::*, px, Action, AnyElement, AppContext, AsyncWindowContext, EventEmitter, FocusHandle,
8+
FocusableView, FontWeight, Model, Pixels, Subscription, Task, View, ViewContext, WeakView,
9+
WindowContext,
810
};
911
use language_model::{LanguageModelRegistry, Role};
1012
use language_model_selector::LanguageModelSelector;
@@ -13,7 +15,7 @@ use workspace::dock::{DockPosition, Panel, PanelEvent};
1315
use workspace::Workspace;
1416

1517
use crate::message_editor::MessageEditor;
16-
use crate::thread::{Message, Thread, ThreadEvent};
18+
use crate::thread::{Message, Thread, ThreadError, ThreadEvent};
1719
use crate::thread_store::ThreadStore;
1820
use crate::{NewThread, ToggleFocus, ToggleModelSelector};
1921

@@ -35,6 +37,7 @@ pub struct AssistantPanel {
3537
thread: Model<Thread>,
3638
message_editor: View<MessageEditor>,
3739
tools: Arc<ToolWorkingSet>,
40+
last_error: Option<ThreadError>,
3841
_subscriptions: Vec<Subscription>,
3942
}
4043

@@ -76,6 +79,7 @@ impl AssistantPanel {
7679
thread: thread.clone(),
7780
message_editor: cx.new_view(|cx| MessageEditor::new(thread, cx)),
7881
tools,
82+
last_error: None,
7983
_subscriptions: subscriptions,
8084
}
8185
}
@@ -102,6 +106,9 @@ impl AssistantPanel {
102106
cx: &mut ViewContext<Self>,
103107
) {
104108
match event {
109+
ThreadEvent::ShowError(error) => {
110+
self.last_error = Some(error.clone());
111+
}
105112
ThreadEvent::StreamedCompletion => {}
106113
ThreadEvent::UsePendingTools => {
107114
let pending_tool_uses = self
@@ -320,6 +327,152 @@ impl AssistantPanel {
320327
)
321328
.child(v_flex().p_1p5().child(Label::new(message.text.clone())))
322329
}
330+
331+
fn render_last_error(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
332+
let last_error = self.last_error.as_ref()?;
333+
334+
Some(
335+
div()
336+
.absolute()
337+
.right_3()
338+
.bottom_12()
339+
.max_w_96()
340+
.py_2()
341+
.px_3()
342+
.elevation_2(cx)
343+
.occlude()
344+
.child(match last_error {
345+
ThreadError::PaymentRequired => self.render_payment_required_error(cx),
346+
ThreadError::MaxMonthlySpendReached => {
347+
self.render_max_monthly_spend_reached_error(cx)
348+
}
349+
ThreadError::Message(error_message) => {
350+
self.render_error_message(error_message, cx)
351+
}
352+
})
353+
.into_any(),
354+
)
355+
}
356+
357+
fn render_payment_required_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
358+
const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used.";
359+
360+
v_flex()
361+
.gap_0p5()
362+
.child(
363+
h_flex()
364+
.gap_1p5()
365+
.items_center()
366+
.child(Icon::new(IconName::XCircle).color(Color::Error))
367+
.child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
368+
)
369+
.child(
370+
div()
371+
.id("error-message")
372+
.max_h_24()
373+
.overflow_y_scroll()
374+
.child(Label::new(ERROR_MESSAGE)),
375+
)
376+
.child(
377+
h_flex()
378+
.justify_end()
379+
.mt_1()
380+
.child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
381+
|this, _, cx| {
382+
this.last_error = None;
383+
cx.open_url(&zed_urls::account_url(cx));
384+
cx.notify();
385+
},
386+
)))
387+
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
388+
|this, _, cx| {
389+
this.last_error = None;
390+
cx.notify();
391+
},
392+
))),
393+
)
394+
.into_any()
395+
}
396+
397+
fn render_max_monthly_spend_reached_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
398+
const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
399+
400+
v_flex()
401+
.gap_0p5()
402+
.child(
403+
h_flex()
404+
.gap_1p5()
405+
.items_center()
406+
.child(Icon::new(IconName::XCircle).color(Color::Error))
407+
.child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)),
408+
)
409+
.child(
410+
div()
411+
.id("error-message")
412+
.max_h_24()
413+
.overflow_y_scroll()
414+
.child(Label::new(ERROR_MESSAGE)),
415+
)
416+
.child(
417+
h_flex()
418+
.justify_end()
419+
.mt_1()
420+
.child(
421+
Button::new("subscribe", "Update Monthly Spend Limit").on_click(
422+
cx.listener(|this, _, cx| {
423+
this.last_error = None;
424+
cx.open_url(&zed_urls::account_url(cx));
425+
cx.notify();
426+
}),
427+
),
428+
)
429+
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
430+
|this, _, cx| {
431+
this.last_error = None;
432+
cx.notify();
433+
},
434+
))),
435+
)
436+
.into_any()
437+
}
438+
439+
fn render_error_message(
440+
&self,
441+
error_message: &SharedString,
442+
cx: &mut ViewContext<Self>,
443+
) -> AnyElement {
444+
v_flex()
445+
.gap_0p5()
446+
.child(
447+
h_flex()
448+
.gap_1p5()
449+
.items_center()
450+
.child(Icon::new(IconName::XCircle).color(Color::Error))
451+
.child(
452+
Label::new("Error interacting with language model")
453+
.weight(FontWeight::MEDIUM),
454+
),
455+
)
456+
.child(
457+
div()
458+
.id("error-message")
459+
.max_h_32()
460+
.overflow_y_scroll()
461+
.child(Label::new(error_message.clone())),
462+
)
463+
.child(
464+
h_flex()
465+
.justify_end()
466+
.mt_1()
467+
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
468+
|this, _, cx| {
469+
this.last_error = None;
470+
cx.notify();
471+
},
472+
))),
473+
)
474+
.into_any()
475+
}
323476
}
324477

325478
impl Render for AssistantPanel {
@@ -354,5 +507,6 @@ impl Render for AssistantPanel {
354507
.border_color(cx.theme().colors().border_variant)
355508
.child(self.message_editor.clone()),
356509
)
510+
.children(self.render_last_error(cx))
357511
}
358512
}

crates/assistant2/src/thread.rs

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@ use assistant_tool::ToolWorkingSet;
55
use collections::HashMap;
66
use futures::future::Shared;
77
use futures::{FutureExt as _, StreamExt as _};
8-
use gpui::{AppContext, EventEmitter, ModelContext, Task};
8+
use gpui::{AppContext, EventEmitter, ModelContext, SharedString, Task};
99
use language_model::{
1010
LanguageModel, LanguageModelCompletionEvent, LanguageModelRequest, LanguageModelRequestMessage,
1111
LanguageModelToolResult, LanguageModelToolUse, LanguageModelToolUseId, MessageContent, Role,
1212
StopReason,
1313
};
14+
use language_models::provider::cloud::{MaxMonthlySpendReachedError, PaymentRequiredError};
1415
use serde::{Deserialize, Serialize};
1516
use util::post_inc;
1617

@@ -210,29 +211,28 @@ impl Thread {
210211
let result = stream_completion.await;
211212

212213
thread
213-
.update(&mut cx, |_thread, cx| {
214-
let error_message = if let Some(error) = result.as_ref().err() {
215-
let error_message = error
216-
.chain()
217-
.map(|err| err.to_string())
218-
.collect::<Vec<_>>()
219-
.join("\n");
220-
Some(error_message)
221-
} else {
222-
None
223-
};
224-
225-
if let Some(error_message) = error_message {
226-
eprintln!("Completion failed: {error_message:?}");
227-
}
228-
229-
if let Ok(stop_reason) = result {
230-
match stop_reason {
231-
StopReason::ToolUse => {
232-
cx.emit(ThreadEvent::UsePendingTools);
233-
}
234-
StopReason::EndTurn => {}
235-
StopReason::MaxTokens => {}
214+
.update(&mut cx, |_thread, cx| match result.as_ref() {
215+
Ok(stop_reason) => match stop_reason {
216+
StopReason::ToolUse => {
217+
cx.emit(ThreadEvent::UsePendingTools);
218+
}
219+
StopReason::EndTurn => {}
220+
StopReason::MaxTokens => {}
221+
},
222+
Err(error) => {
223+
if error.is::<PaymentRequiredError>() {
224+
cx.emit(ThreadEvent::ShowError(ThreadError::PaymentRequired));
225+
} else if error.is::<MaxMonthlySpendReachedError>() {
226+
cx.emit(ThreadEvent::ShowError(ThreadError::MaxMonthlySpendReached));
227+
} else {
228+
let error_message = error
229+
.chain()
230+
.map(|err| err.to_string())
231+
.collect::<Vec<_>>()
232+
.join("\n");
233+
cx.emit(ThreadEvent::ShowError(ThreadError::Message(
234+
SharedString::from(error_message.clone()),
235+
)));
236236
}
237237
}
238238
})
@@ -305,8 +305,16 @@ impl Thread {
305305
}
306306
}
307307

308+
#[derive(Debug, Clone)]
309+
pub enum ThreadError {
310+
PaymentRequired,
311+
MaxMonthlySpendReached,
312+
Message(SharedString),
313+
}
314+
308315
#[derive(Debug, Clone)]
309316
pub enum ThreadEvent {
317+
ShowError(ThreadError),
310318
StreamedCompletion,
311319
UsePendingTools,
312320
ToolFinished {

crates/language_model/src/language_model.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ pub enum LanguageModelCompletionEvent {
5555
StartMessage { message_id: String },
5656
}
5757

58-
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
58+
#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
5959
#[serde(rename_all = "snake_case")]
6060
pub enum StopReason {
6161
EndTurn,

0 commit comments

Comments
 (0)