assistant2: Add thread history (#21599)

This PR adds support for thread history to the Assistant 2 panel.

We also now generate summaries for the threads.

<img width="986" alt="Screenshot 2024-12-05 at 12 56 53 PM"
src="https://github.com/user-attachments/assets/46cb1309-38a2-4ab9-9fcc-c1275d4b5f2c">

<img width="986" alt="Screenshot 2024-12-05 at 12 56 58 PM"
src="https://github.com/user-attachments/assets/8c91ba57-a6c5-4b88-be05-b22fb615ece5">

Release Notes:

- N/A

---------

Co-authored-by: Piotr <piotr@zed.dev>
This commit is contained in:
Marshall Bowers 2024-12-05 13:22:25 -05:00 committed by GitHub
parent 2d43ad12e6
commit 787c75cbda
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 375 additions and 127 deletions

View file

@ -11,13 +11,15 @@ use gpui::{
use language::LanguageRegistry;
use language_model::LanguageModelRegistry;
use language_model_selector::LanguageModelSelector;
use ui::{prelude::*, ButtonLike, Divider, IconButtonShape, KeyBinding, ListItem, Tab, Tooltip};
use time::UtcOffset;
use ui::{prelude::*, ButtonLike, Divider, IconButtonShape, KeyBinding, Tab, Tooltip};
use workspace::dock::{DockPosition, Panel, PanelEvent};
use workspace::Workspace;
use crate::active_thread::ActiveThread;
use crate::message_editor::MessageEditor;
use crate::thread::{Thread, ThreadError, ThreadId};
use crate::thread::{ThreadError, ThreadId};
use crate::thread_history::{PastThread, ThreadHistory};
use crate::thread_store::ThreadStore;
use crate::{NewThread, OpenHistory, ToggleFocus, ToggleModelSelector};
@ -32,13 +34,21 @@ pub fn init(cx: &mut AppContext) {
.detach();
}
enum ActiveView {
Thread,
History,
}
pub struct AssistantPanel {
workspace: WeakView<Workspace>,
language_registry: Arc<LanguageRegistry>,
thread_store: Model<ThreadStore>,
thread: Option<View<ActiveThread>>,
thread: View<ActiveThread>,
message_editor: View<MessageEditor>,
tools: Arc<ToolWorkingSet>,
local_timezone: UtcOffset,
active_view: ActiveView,
history: View<ThreadHistory>,
}
impl AssistantPanel {
@ -68,14 +78,31 @@ impl AssistantPanel {
cx: &mut ViewContext<Self>,
) -> Self {
let thread = thread_store.update(cx, |this, cx| this.create_thread(cx));
let language_registry = workspace.project().read(cx).languages().clone();
let workspace = workspace.weak_handle();
let weak_self = cx.view().downgrade();
Self {
workspace: workspace.weak_handle(),
language_registry: workspace.project().read(cx).languages().clone(),
thread_store,
thread: None,
message_editor: cx.new_view(|cx| MessageEditor::new(thread, cx)),
active_view: ActiveView::Thread,
workspace: workspace.clone(),
language_registry: language_registry.clone(),
thread_store: thread_store.clone(),
thread: cx.new_view(|cx| {
ActiveThread::new(
thread.clone(),
workspace,
language_registry,
tools.clone(),
cx,
)
}),
message_editor: cx.new_view(|cx| MessageEditor::new(thread.clone(), cx)),
tools,
local_timezone: UtcOffset::from_whole_seconds(
chrono::Local::now().offset().local_minus_utc(),
)
.unwrap(),
history: cx.new_view(|cx| ThreadHistory::new(weak_self, thread_store, cx)),
}
}
@ -84,7 +111,8 @@ impl AssistantPanel {
.thread_store
.update(cx, |this, cx| this.create_thread(cx));
self.thread = Some(cx.new_view(|cx| {
self.active_view = ActiveView::Thread;
self.thread = cx.new_view(|cx| {
ActiveThread::new(
thread.clone(),
self.workspace.clone(),
@ -92,12 +120,12 @@ impl AssistantPanel {
self.tools.clone(),
cx,
)
}));
});
self.message_editor = cx.new_view(|cx| MessageEditor::new(thread, cx));
self.message_editor.focus_handle(cx).focus(cx);
}
fn open_thread(&mut self, thread_id: &ThreadId, cx: &mut ViewContext<Self>) {
pub(crate) fn open_thread(&mut self, thread_id: &ThreadId, cx: &mut ViewContext<Self>) {
let Some(thread) = self
.thread_store
.update(cx, |this, cx| this.open_thread(thread_id, cx))
@ -105,7 +133,8 @@ impl AssistantPanel {
return;
};
self.thread = Some(cx.new_view(|cx| {
self.active_view = ActiveView::Thread;
self.thread = cx.new_view(|cx| {
ActiveThread::new(
thread.clone(),
self.workspace.clone(),
@ -113,15 +142,22 @@ impl AssistantPanel {
self.tools.clone(),
cx,
)
}));
});
self.message_editor = cx.new_view(|cx| MessageEditor::new(thread, cx));
self.message_editor.focus_handle(cx).focus(cx);
}
pub(crate) fn local_timezone(&self) -> UtcOffset {
self.local_timezone
}
}
impl FocusableView for AssistantPanel {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.message_editor.focus_handle(cx)
match self.active_view {
ActiveView::Thread => self.message_editor.focus_handle(cx),
ActiveView::History => self.history.focus_handle(cx),
}
}
}
@ -180,7 +216,7 @@ impl AssistantPanel {
.bg(cx.theme().colors().tab_bar_background)
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.child(h_flex().child(Label::new("Thread Title Goes Here")))
.child(h_flex().children(self.thread.read(cx).summary(cx).map(Label::new)))
.child(
h_flex()
.gap(DynamicSpacing::Base08.rems(cx))
@ -291,15 +327,11 @@ impl AssistantPanel {
}
fn render_active_thread_or_empty_state(&self, cx: &mut ViewContext<Self>) -> AnyElement {
let Some(thread) = self.thread.as_ref() else {
return self.render_thread_empty_state(cx).into_any_element();
};
if thread.read(cx).is_empty() {
if self.thread.read(cx).is_empty() {
return self.render_thread_empty_state(cx).into_any_element();
}
thread.clone().into_any()
self.thread.clone().into_any()
}
fn render_thread_empty_state(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
@ -361,63 +393,41 @@ impl AssistantPanel {
.child(Label::new("/src/components").size(LabelSize::Small)),
),
)
.child(
h_flex()
.w_full()
.justify_center()
.child(Label::new("Recent Threads:").size(LabelSize::Small)),
)
.child(
v_flex().gap_2().children(
recent_threads
.into_iter()
.map(|thread| self.render_past_thread(thread, cx)),
),
)
.child(
h_flex().w_full().justify_center().child(
Button::new("view-all-past-threads", "View All Past Threads")
.style(ButtonStyle::Subtle)
.label_size(LabelSize::Small)
.key_binding(KeyBinding::for_action_in(
&OpenHistory,
&self.focus_handle(cx),
cx,
))
.on_click(move |_event, cx| {
cx.dispatch_action(OpenHistory.boxed_clone());
}),
),
)
}
fn render_past_thread(
&self,
thread: Model<Thread>,
cx: &mut ViewContext<Self>,
) -> impl IntoElement {
let id = thread.read(cx).id().clone();
ListItem::new(("past-thread", thread.entity_id()))
.start_slot(Icon::new(IconName::MessageBubbles))
.child(Label::new(format!("Thread {id}")))
.end_slot(
h_flex()
.gap_2()
.child(Label::new("1 hour ago").color(Color::Disabled))
.when(!recent_threads.is_empty(), |parent| {
parent
.child(
IconButton::new("delete", IconName::TrashAlt)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small),
),
)
.on_click(cx.listener(move |this, _event, cx| {
this.open_thread(&id, cx);
}))
h_flex()
.w_full()
.justify_center()
.child(Label::new("Recent Threads:").size(LabelSize::Small)),
)
.child(
v_flex().gap_2().children(
recent_threads
.into_iter()
.map(|thread| PastThread::new(thread, cx.view().downgrade())),
),
)
.child(
h_flex().w_full().justify_center().child(
Button::new("view-all-past-threads", "View All Past Threads")
.style(ButtonStyle::Subtle)
.label_size(LabelSize::Small)
.key_binding(KeyBinding::for_action_in(
&OpenHistory,
&self.focus_handle(cx),
cx,
))
.on_click(move |_event, cx| {
cx.dispatch_action(OpenHistory.boxed_clone());
}),
),
)
})
}
fn render_last_error(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
let last_error = self.thread.as_ref()?.read(cx).last_error()?;
let last_error = self.thread.read(cx).last_error()?;
Some(
div()
@ -467,11 +477,9 @@ impl AssistantPanel {
.mt_1()
.child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
|this, _, cx| {
if let Some(thread) = this.thread.as_ref() {
thread.update(cx, |this, _cx| {
this.clear_last_error();
});
}
this.thread.update(cx, |this, _cx| {
this.clear_last_error();
});
cx.open_url(&zed_urls::account_url(cx));
cx.notify();
@ -479,11 +487,9 @@ impl AssistantPanel {
)))
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|this, _, cx| {
if let Some(thread) = this.thread.as_ref() {
thread.update(cx, |this, _cx| {
this.clear_last_error();
});
}
this.thread.update(cx, |this, _cx| {
this.clear_last_error();
});
cx.notify();
},
@ -518,11 +524,9 @@ impl AssistantPanel {
.child(
Button::new("subscribe", "Update Monthly Spend Limit").on_click(
cx.listener(|this, _, cx| {
if let Some(thread) = this.thread.as_ref() {
thread.update(cx, |this, _cx| {
this.clear_last_error();
});
}
this.thread.update(cx, |this, _cx| {
this.clear_last_error();
});
cx.open_url(&zed_urls::account_url(cx));
cx.notify();
@ -531,11 +535,9 @@ impl AssistantPanel {
)
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|this, _, cx| {
if let Some(thread) = this.thread.as_ref() {
thread.update(cx, |this, _cx| {
this.clear_last_error();
});
}
this.thread.update(cx, |this, _cx| {
this.clear_last_error();
});
cx.notify();
},
@ -574,11 +576,9 @@ impl AssistantPanel {
.mt_1()
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|this, _, cx| {
if let Some(thread) = this.thread.as_ref() {
thread.update(cx, |this, _cx| {
this.clear_last_error();
});
}
this.thread.update(cx, |this, _cx| {
this.clear_last_error();
});
cx.notify();
},
@ -597,17 +597,23 @@ impl Render for AssistantPanel {
.on_action(cx.listener(|this, _: &NewThread, cx| {
this.new_thread(cx);
}))
.on_action(cx.listener(|_this, _: &OpenHistory, _cx| {
println!("Open History");
.on_action(cx.listener(|this, _: &OpenHistory, cx| {
this.active_view = ActiveView::History;
this.history.focus_handle(cx).focus(cx);
cx.notify();
}))
.child(self.render_toolbar(cx))
.child(self.render_active_thread_or_empty_state(cx))
.child(
h_flex()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.child(self.message_editor.clone()),
)
.children(self.render_last_error(cx))
.map(|parent| match self.active_view {
ActiveView::Thread => parent
.child(self.render_active_thread_or_empty_state(cx))
.child(
h_flex()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.child(self.message_editor.clone()),
)
.children(self.render_last_error(cx)),
ActiveView::History => parent.child(self.history.clone()),
})
}
}