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

3
Cargo.lock generated
View file

@ -456,6 +456,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"assistant_tool", "assistant_tool",
"chrono",
"client", "client",
"collections", "collections",
"command_palette_hooks", "command_palette_hooks",
@ -478,6 +479,8 @@ dependencies = [
"settings", "settings",
"smol", "smol",
"theme", "theme",
"time",
"time_format",
"ui", "ui",
"unindent", "unindent",
"util", "util",

View file

@ -15,6 +15,7 @@ doctest = false
[dependencies] [dependencies]
anyhow.workspace = true anyhow.workspace = true
assistant_tool.workspace = true assistant_tool.workspace = true
chrono.workspace = true
client.workspace = true client.workspace = true
collections.workspace = true collections.workspace = true
command_palette_hooks.workspace = true command_palette_hooks.workspace = true
@ -37,6 +38,8 @@ serde_json.workspace = true
settings.workspace = true settings.workspace = true
smol.workspace = true smol.workspace = true
theme.workspace = true theme.workspace = true
time.workspace = true
time_format.workspace = true
ui.workspace = true ui.workspace = true
unindent.workspace = true unindent.workspace = true
util.workspace = true util.workspace = true

View file

@ -3,8 +3,8 @@ use std::sync::Arc;
use assistant_tool::ToolWorkingSet; use assistant_tool::ToolWorkingSet;
use collections::HashMap; use collections::HashMap;
use gpui::{ use gpui::{
list, AnyElement, Empty, ListAlignment, ListState, Model, StyleRefinement, Subscription, list, AnyElement, AppContext, Empty, ListAlignment, ListState, Model, StyleRefinement,
TextStyleRefinement, View, WeakView, Subscription, TextStyleRefinement, View, WeakView,
}; };
use language::LanguageRegistry; use language::LanguageRegistry;
use language_model::Role; use language_model::Role;
@ -70,6 +70,10 @@ impl ActiveThread {
self.messages.is_empty() self.messages.is_empty()
} }
pub fn summary(&self, cx: &AppContext) -> Option<SharedString> {
self.thread.read(cx).summary()
}
pub fn last_error(&self) -> Option<ThreadError> { pub fn last_error(&self) -> Option<ThreadError> {
self.last_error.clone() self.last_error.clone()
} }
@ -139,6 +143,7 @@ impl ActiveThread {
self.last_error = Some(error.clone()); self.last_error = Some(error.clone());
} }
ThreadEvent::StreamedCompletion => {} ThreadEvent::StreamedCompletion => {}
ThreadEvent::SummaryChanged => {}
ThreadEvent::StreamedAssistantText(message_id, text) => { ThreadEvent::StreamedAssistantText(message_id, text) => {
if let Some(markdown) = self.rendered_messages_by_id.get_mut(&message_id) { if let Some(markdown) = self.rendered_messages_by_id.get_mut(&message_id) {
markdown.update(cx, |markdown, cx| { markdown.update(cx, |markdown, cx| {

View file

@ -3,6 +3,7 @@ mod assistant_panel;
mod context_picker; mod context_picker;
mod message_editor; mod message_editor;
mod thread; mod thread;
mod thread_history;
mod thread_store; mod thread_store;
use command_palette_hooks::CommandPaletteFilter; use command_palette_hooks::CommandPaletteFilter;

View file

@ -11,13 +11,15 @@ use gpui::{
use language::LanguageRegistry; use language::LanguageRegistry;
use language_model::LanguageModelRegistry; use language_model::LanguageModelRegistry;
use language_model_selector::LanguageModelSelector; 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::dock::{DockPosition, Panel, PanelEvent};
use workspace::Workspace; use workspace::Workspace;
use crate::active_thread::ActiveThread; use crate::active_thread::ActiveThread;
use crate::message_editor::MessageEditor; 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::thread_store::ThreadStore;
use crate::{NewThread, OpenHistory, ToggleFocus, ToggleModelSelector}; use crate::{NewThread, OpenHistory, ToggleFocus, ToggleModelSelector};
@ -32,13 +34,21 @@ pub fn init(cx: &mut AppContext) {
.detach(); .detach();
} }
enum ActiveView {
Thread,
History,
}
pub struct AssistantPanel { pub struct AssistantPanel {
workspace: WeakView<Workspace>, workspace: WeakView<Workspace>,
language_registry: Arc<LanguageRegistry>, language_registry: Arc<LanguageRegistry>,
thread_store: Model<ThreadStore>, thread_store: Model<ThreadStore>,
thread: Option<View<ActiveThread>>, thread: View<ActiveThread>,
message_editor: View<MessageEditor>, message_editor: View<MessageEditor>,
tools: Arc<ToolWorkingSet>, tools: Arc<ToolWorkingSet>,
local_timezone: UtcOffset,
active_view: ActiveView,
history: View<ThreadHistory>,
} }
impl AssistantPanel { impl AssistantPanel {
@ -68,14 +78,31 @@ impl AssistantPanel {
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Self { ) -> Self {
let thread = thread_store.update(cx, |this, cx| this.create_thread(cx)); 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 { Self {
workspace: workspace.weak_handle(), active_view: ActiveView::Thread,
language_registry: workspace.project().read(cx).languages().clone(), workspace: workspace.clone(),
thread_store, language_registry: language_registry.clone(),
thread: None, thread_store: thread_store.clone(),
message_editor: cx.new_view(|cx| MessageEditor::new(thread, cx)), 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, 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 .thread_store
.update(cx, |this, cx| this.create_thread(cx)); .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( ActiveThread::new(
thread.clone(), thread.clone(),
self.workspace.clone(), self.workspace.clone(),
@ -92,12 +120,12 @@ impl AssistantPanel {
self.tools.clone(), self.tools.clone(),
cx, cx,
) )
})); });
self.message_editor = cx.new_view(|cx| MessageEditor::new(thread, cx)); self.message_editor = cx.new_view(|cx| MessageEditor::new(thread, cx));
self.message_editor.focus_handle(cx).focus(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 let Some(thread) = self
.thread_store .thread_store
.update(cx, |this, cx| this.open_thread(thread_id, cx)) .update(cx, |this, cx| this.open_thread(thread_id, cx))
@ -105,7 +133,8 @@ impl AssistantPanel {
return; return;
}; };
self.thread = Some(cx.new_view(|cx| { self.active_view = ActiveView::Thread;
self.thread = cx.new_view(|cx| {
ActiveThread::new( ActiveThread::new(
thread.clone(), thread.clone(),
self.workspace.clone(), self.workspace.clone(),
@ -113,15 +142,22 @@ impl AssistantPanel {
self.tools.clone(), self.tools.clone(),
cx, cx,
) )
})); });
self.message_editor = cx.new_view(|cx| MessageEditor::new(thread, cx)); self.message_editor = cx.new_view(|cx| MessageEditor::new(thread, cx));
self.message_editor.focus_handle(cx).focus(cx); self.message_editor.focus_handle(cx).focus(cx);
} }
pub(crate) fn local_timezone(&self) -> UtcOffset {
self.local_timezone
}
} }
impl FocusableView for AssistantPanel { impl FocusableView for AssistantPanel {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle { 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) .bg(cx.theme().colors().tab_bar_background)
.border_b_1() .border_b_1()
.border_color(cx.theme().colors().border_variant) .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( .child(
h_flex() h_flex()
.gap(DynamicSpacing::Base08.rems(cx)) .gap(DynamicSpacing::Base08.rems(cx))
@ -291,15 +327,11 @@ impl AssistantPanel {
} }
fn render_active_thread_or_empty_state(&self, cx: &mut ViewContext<Self>) -> AnyElement { fn render_active_thread_or_empty_state(&self, cx: &mut ViewContext<Self>) -> AnyElement {
let Some(thread) = self.thread.as_ref() else { if self.thread.read(cx).is_empty() {
return self.render_thread_empty_state(cx).into_any_element();
};
if thread.read(cx).is_empty() {
return self.render_thread_empty_state(cx).into_any_element(); 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 { fn render_thread_empty_state(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
@ -361,6 +393,8 @@ impl AssistantPanel {
.child(Label::new("/src/components").size(LabelSize::Small)), .child(Label::new("/src/components").size(LabelSize::Small)),
), ),
) )
.when(!recent_threads.is_empty(), |parent| {
parent
.child( .child(
h_flex() h_flex()
.w_full() .w_full()
@ -371,7 +405,7 @@ impl AssistantPanel {
v_flex().gap_2().children( v_flex().gap_2().children(
recent_threads recent_threads
.into_iter() .into_iter()
.map(|thread| self.render_past_thread(thread, cx)), .map(|thread| PastThread::new(thread, cx.view().downgrade())),
), ),
) )
.child( .child(
@ -389,35 +423,11 @@ impl AssistantPanel {
}), }),
), ),
) )
} })
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))
.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);
}))
} }
fn render_last_error(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> { 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( Some(
div() div()
@ -467,11 +477,9 @@ impl AssistantPanel {
.mt_1() .mt_1()
.child(Button::new("subscribe", "Subscribe").on_click(cx.listener( .child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
|this, _, cx| { |this, _, cx| {
if let Some(thread) = this.thread.as_ref() { this.thread.update(cx, |this, _cx| {
thread.update(cx, |this, _cx| {
this.clear_last_error(); this.clear_last_error();
}); });
}
cx.open_url(&zed_urls::account_url(cx)); cx.open_url(&zed_urls::account_url(cx));
cx.notify(); cx.notify();
@ -479,11 +487,9 @@ impl AssistantPanel {
))) )))
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener( .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|this, _, cx| { |this, _, cx| {
if let Some(thread) = this.thread.as_ref() { this.thread.update(cx, |this, _cx| {
thread.update(cx, |this, _cx| {
this.clear_last_error(); this.clear_last_error();
}); });
}
cx.notify(); cx.notify();
}, },
@ -518,11 +524,9 @@ impl AssistantPanel {
.child( .child(
Button::new("subscribe", "Update Monthly Spend Limit").on_click( Button::new("subscribe", "Update Monthly Spend Limit").on_click(
cx.listener(|this, _, cx| { cx.listener(|this, _, cx| {
if let Some(thread) = this.thread.as_ref() { this.thread.update(cx, |this, _cx| {
thread.update(cx, |this, _cx| {
this.clear_last_error(); this.clear_last_error();
}); });
}
cx.open_url(&zed_urls::account_url(cx)); cx.open_url(&zed_urls::account_url(cx));
cx.notify(); cx.notify();
@ -531,11 +535,9 @@ impl AssistantPanel {
) )
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener( .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|this, _, cx| { |this, _, cx| {
if let Some(thread) = this.thread.as_ref() { this.thread.update(cx, |this, _cx| {
thread.update(cx, |this, _cx| {
this.clear_last_error(); this.clear_last_error();
}); });
}
cx.notify(); cx.notify();
}, },
@ -574,11 +576,9 @@ impl AssistantPanel {
.mt_1() .mt_1()
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener( .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|this, _, cx| { |this, _, cx| {
if let Some(thread) = this.thread.as_ref() { this.thread.update(cx, |this, _cx| {
thread.update(cx, |this, _cx| {
this.clear_last_error(); this.clear_last_error();
}); });
}
cx.notify(); cx.notify();
}, },
@ -597,10 +597,14 @@ impl Render for AssistantPanel {
.on_action(cx.listener(|this, _: &NewThread, cx| { .on_action(cx.listener(|this, _: &NewThread, cx| {
this.new_thread(cx); this.new_thread(cx);
})) }))
.on_action(cx.listener(|_this, _: &OpenHistory, _cx| { .on_action(cx.listener(|this, _: &OpenHistory, cx| {
println!("Open History"); this.active_view = ActiveView::History;
this.history.focus_handle(cx).focus(cx);
cx.notify();
})) }))
.child(self.render_toolbar(cx)) .child(self.render_toolbar(cx))
.map(|parent| match self.active_view {
ActiveView::Thread => parent
.child(self.render_active_thread_or_empty_state(cx)) .child(self.render_active_thread_or_empty_state(cx))
.child( .child(
h_flex() h_flex()
@ -608,6 +612,8 @@ impl Render for AssistantPanel {
.border_color(cx.theme().colors().border_variant) .border_color(cx.theme().colors().border_variant)
.child(self.message_editor.clone()), .child(self.message_editor.clone()),
) )
.children(self.render_last_error(cx)) .children(self.render_last_error(cx)),
ActiveView::History => parent.child(self.history.clone()),
})
} }
} }

View file

@ -2,18 +2,19 @@ use std::sync::Arc;
use anyhow::Result; use anyhow::Result;
use assistant_tool::ToolWorkingSet; use assistant_tool::ToolWorkingSet;
use chrono::{DateTime, Utc};
use collections::HashMap; use collections::HashMap;
use futures::future::Shared; use futures::future::Shared;
use futures::{FutureExt as _, StreamExt as _}; use futures::{FutureExt as _, StreamExt as _};
use gpui::{AppContext, EventEmitter, ModelContext, SharedString, Task}; use gpui::{AppContext, EventEmitter, ModelContext, SharedString, Task};
use language_model::{ use language_model::{
LanguageModel, LanguageModelCompletionEvent, LanguageModelRequest, LanguageModelRequestMessage, LanguageModel, LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest,
LanguageModelToolResult, LanguageModelToolUse, LanguageModelToolUseId, MessageContent, Role, LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelToolUse,
StopReason, LanguageModelToolUseId, MessageContent, Role, StopReason,
}; };
use language_models::provider::cloud::{MaxMonthlySpendReachedError, PaymentRequiredError}; use language_models::provider::cloud::{MaxMonthlySpendReachedError, PaymentRequiredError};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use util::post_inc; use util::{post_inc, TryFutureExt as _};
use uuid::Uuid; use uuid::Uuid;
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@ -56,6 +57,9 @@ pub struct Message {
/// A thread of conversation with the LLM. /// A thread of conversation with the LLM.
pub struct Thread { pub struct Thread {
id: ThreadId, id: ThreadId,
updated_at: DateTime<Utc>,
summary: Option<SharedString>,
pending_summary: Task<Option<()>>,
messages: Vec<Message>, messages: Vec<Message>,
next_message_id: MessageId, next_message_id: MessageId,
completion_count: usize, completion_count: usize,
@ -70,6 +74,9 @@ impl Thread {
pub fn new(tools: Arc<ToolWorkingSet>, _cx: &mut ModelContext<Self>) -> Self { pub fn new(tools: Arc<ToolWorkingSet>, _cx: &mut ModelContext<Self>) -> Self {
Self { Self {
id: ThreadId::new(), id: ThreadId::new(),
updated_at: Utc::now(),
summary: None,
pending_summary: Task::ready(None),
messages: Vec::new(), messages: Vec::new(),
next_message_id: MessageId(0), next_message_id: MessageId(0),
completion_count: 0, completion_count: 0,
@ -89,6 +96,23 @@ impl Thread {
self.messages.is_empty() self.messages.is_empty()
} }
pub fn updated_at(&self) -> DateTime<Utc> {
self.updated_at
}
pub fn touch_updated_at(&mut self) {
self.updated_at = Utc::now();
}
pub fn summary(&self) -> Option<SharedString> {
self.summary.clone()
}
pub fn set_summary(&mut self, summary: impl Into<SharedString>, cx: &mut ModelContext<Self>) {
self.summary = Some(summary.into());
cx.emit(ThreadEvent::SummaryChanged);
}
pub fn message(&self, id: MessageId) -> Option<&Message> { pub fn message(&self, id: MessageId) -> Option<&Message> {
self.messages.iter().find(|message| message.id == id) self.messages.iter().find(|message| message.id == id)
} }
@ -121,6 +145,7 @@ impl Thread {
role, role,
text: text.into(), text: text.into(),
}); });
self.touch_updated_at();
cx.emit(ThreadEvent::MessageAdded(id)); cx.emit(ThreadEvent::MessageAdded(id));
} }
@ -191,13 +216,7 @@ impl Thread {
thread.update(&mut cx, |thread, cx| { thread.update(&mut cx, |thread, cx| {
match event { match event {
LanguageModelCompletionEvent::StartMessage { .. } => { LanguageModelCompletionEvent::StartMessage { .. } => {
let id = thread.next_message_id.post_inc(); thread.insert_message(Role::Assistant, String::new(), cx);
thread.messages.push(Message {
id,
role: Role::Assistant,
text: String::new(),
});
cx.emit(ThreadEvent::MessageAdded(id));
} }
LanguageModelCompletionEvent::Stop(reason) => { LanguageModelCompletionEvent::Stop(reason) => {
stop_reason = reason; stop_reason = reason;
@ -239,6 +258,7 @@ impl Thread {
} }
} }
thread.touch_updated_at();
cx.emit(ThreadEvent::StreamedCompletion); cx.emit(ThreadEvent::StreamedCompletion);
cx.notify(); cx.notify();
})?; })?;
@ -246,10 +266,14 @@ impl Thread {
smol::future::yield_now().await; smol::future::yield_now().await;
} }
thread.update(&mut cx, |thread, _cx| { thread.update(&mut cx, |thread, cx| {
thread thread
.pending_completions .pending_completions
.retain(|completion| completion.id != pending_completion_id); .retain(|completion| completion.id != pending_completion_id);
if thread.summary.is_none() && thread.messages.len() >= 2 {
thread.summarize(cx);
}
})?; })?;
anyhow::Ok(stop_reason) anyhow::Ok(stop_reason)
@ -292,6 +316,59 @@ impl Thread {
}); });
} }
pub fn summarize(&mut self, cx: &mut ModelContext<Self>) {
let Some(provider) = LanguageModelRegistry::read_global(cx).active_provider() else {
return;
};
let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
return;
};
if !provider.is_authenticated(cx) {
return;
}
let mut request = self.to_completion_request(RequestKind::Chat, cx);
request.messages.push(LanguageModelRequestMessage {
role: Role::User,
content: vec![
"Generate a concise 3-7 word title for this conversation, omitting punctuation. Go straight to the title, without any preamble and prefix like `Here's a concise suggestion:...` or `Title:`"
.into(),
],
cache: false,
});
self.pending_summary = cx.spawn(|this, mut cx| {
async move {
let stream = model.stream_completion_text(request, &cx);
let mut messages = stream.await?;
let mut new_summary = String::new();
while let Some(message) = messages.stream.next().await {
let text = message?;
let mut lines = text.lines();
new_summary.extend(lines.next());
// Stop if the LLM generated multiple lines.
if lines.next().is_some() {
break;
}
}
this.update(&mut cx, |this, cx| {
if !new_summary.is_empty() {
this.summary = Some(new_summary.into());
}
cx.emit(ThreadEvent::SummaryChanged);
})?;
anyhow::Ok(())
}
.log_err()
});
}
pub fn insert_tool_output( pub fn insert_tool_output(
&mut self, &mut self,
assistant_message_id: MessageId, assistant_message_id: MessageId,
@ -365,6 +442,7 @@ pub enum ThreadEvent {
StreamedCompletion, StreamedCompletion,
StreamedAssistantText(MessageId, String), StreamedAssistantText(MessageId, String),
MessageAdded(MessageId), MessageAdded(MessageId),
SummaryChanged,
UsePendingTools, UsePendingTools,
ToolFinished { ToolFinished {
#[allow(unused)] #[allow(unused)]

View file

@ -0,0 +1,144 @@
use gpui::{
uniform_list, AppContext, FocusHandle, FocusableView, Model, UniformListScrollHandle, WeakView,
};
use time::{OffsetDateTime, UtcOffset};
use ui::{prelude::*, IconButtonShape, ListItem};
use crate::thread::Thread;
use crate::thread_store::ThreadStore;
use crate::AssistantPanel;
pub struct ThreadHistory {
focus_handle: FocusHandle,
assistant_panel: WeakView<AssistantPanel>,
thread_store: Model<ThreadStore>,
scroll_handle: UniformListScrollHandle,
}
impl ThreadHistory {
pub(crate) fn new(
assistant_panel: WeakView<AssistantPanel>,
thread_store: Model<ThreadStore>,
cx: &mut ViewContext<Self>,
) -> Self {
Self {
focus_handle: cx.focus_handle(),
assistant_panel,
thread_store,
scroll_handle: UniformListScrollHandle::default(),
}
}
}
impl FocusableView for ThreadHistory {
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Render for ThreadHistory {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let threads = self.thread_store.update(cx, |this, cx| this.threads(cx));
v_flex()
.id("thread-history-container")
.track_focus(&self.focus_handle)
.overflow_y_scroll()
.size_full()
.p_1()
.map(|history| {
if threads.is_empty() {
history
.justify_center()
.child(
h_flex().w_full().justify_center().child(
Label::new("You don't have any past threads yet.")
.size(LabelSize::Small),
),
)
} else {
history.child(
uniform_list(
cx.view().clone(),
"thread-history",
threads.len(),
move |history, range, _cx| {
threads[range]
.iter()
.map(|thread| {
PastThread::new(
thread.clone(),
history.assistant_panel.clone(),
)
})
.collect()
},
)
.track_scroll(self.scroll_handle.clone())
.flex_grow(),
)
}
})
}
}
#[derive(IntoElement)]
pub struct PastThread {
thread: Model<Thread>,
assistant_panel: WeakView<AssistantPanel>,
}
impl PastThread {
pub fn new(thread: Model<Thread>, assistant_panel: WeakView<AssistantPanel>) -> Self {
Self {
thread,
assistant_panel,
}
}
}
impl RenderOnce for PastThread {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let (id, summary) = {
const DEFAULT_SUMMARY: SharedString = SharedString::new_static("New Thread");
let thread = self.thread.read(cx);
(
thread.id().clone(),
thread.summary().unwrap_or(DEFAULT_SUMMARY),
)
};
let thread_timestamp = time_format::format_localized_timestamp(
OffsetDateTime::from_unix_timestamp(self.thread.read(cx).updated_at().timestamp())
.unwrap(),
OffsetDateTime::now_utc(),
self.assistant_panel
.update(cx, |this, _cx| this.local_timezone())
.unwrap_or(UtcOffset::UTC),
time_format::TimestampFormat::EnhancedAbsolute,
);
ListItem::new(("past-thread", self.thread.entity_id()))
.start_slot(Icon::new(IconName::MessageBubbles))
.child(Label::new(summary))
.end_slot(
h_flex()
.gap_2()
.child(Label::new(thread_timestamp).color(Color::Disabled))
.child(
IconButton::new("delete", IconName::TrashAlt)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small),
),
)
.on_click({
let assistant_panel = self.assistant_panel.clone();
move |_event, cx| {
assistant_panel
.update(cx, |this, cx| {
this.open_thread(&id, cx);
})
.ok();
}
})
}
}

View file

@ -52,13 +52,19 @@ impl ThreadStore {
}) })
} }
pub fn recent_threads(&self, limit: usize, cx: &ModelContext<Self>) -> Vec<Model<Thread>> { pub fn threads(&self, cx: &ModelContext<Self>) -> Vec<Model<Thread>> {
self.threads let mut threads = self
.threads
.iter() .iter()
.filter(|thread| !thread.read(cx).is_empty()) .filter(|thread| !thread.read(cx).is_empty())
.take(limit)
.cloned() .cloned()
.collect() .collect::<Vec<_>>();
threads.sort_unstable_by_key(|thread| std::cmp::Reverse(thread.read(cx).updated_at()));
threads
}
pub fn recent_threads(&self, limit: usize, cx: &ModelContext<Self>) -> Vec<Model<Thread>> {
self.threads(cx).into_iter().take(limit).collect()
} }
pub fn create_thread(&mut self, cx: &mut ModelContext<Self>) -> Model<Thread> { pub fn create_thread(&mut self, cx: &mut ModelContext<Self>) -> Model<Thread> {
@ -148,6 +154,7 @@ impl ThreadStore {
self.threads.push(cx.new_model(|cx| { self.threads.push(cx.new_model(|cx| {
let mut thread = Thread::new(self.tools.clone(), cx); let mut thread = Thread::new(self.tools.clone(), cx);
thread.set_summary("Introduction to quantum computing", cx);
thread.insert_user_message("Hello! Can you help me understand quantum computing?", cx); thread.insert_user_message("Hello! Can you help me understand quantum computing?", cx);
thread.insert_message(Role::Assistant, "Of course! I'd be happy to help you understand quantum computing. Quantum computing is a fascinating field that uses the principles of quantum mechanics to process information. Unlike classical computers that use bits (0s and 1s), quantum computers use quantum bits or 'qubits'. These qubits can exist in multiple states simultaneously, a property called superposition. This allows quantum computers to perform certain calculations much faster than classical computers. What specific aspect of quantum computing would you like to know more about?", cx); thread.insert_message(Role::Assistant, "Of course! I'd be happy to help you understand quantum computing. Quantum computing is a fascinating field that uses the principles of quantum mechanics to process information. Unlike classical computers that use bits (0s and 1s), quantum computers use quantum bits or 'qubits'. These qubits can exist in multiple states simultaneously, a property called superposition. This allows quantum computers to perform certain calculations much faster than classical computers. What specific aspect of quantum computing would you like to know more about?", cx);
thread.insert_user_message("That's interesting! Can you explain how quantum entanglement is used in quantum computing?", cx); thread.insert_user_message("That's interesting! Can you explain how quantum entanglement is used in quantum computing?", cx);
@ -157,6 +164,7 @@ impl ThreadStore {
self.threads.push(cx.new_model(|cx| { self.threads.push(cx.new_model(|cx| {
let mut thread = Thread::new(self.tools.clone(), cx); let mut thread = Thread::new(self.tools.clone(), cx);
thread.set_summary("Rust web development and async programming", cx);
thread.insert_user_message("Can you show me an example of Rust code for a simple web server?", cx); thread.insert_user_message("Can you show me an example of Rust code for a simple web server?", cx);
thread.insert_message(Role::Assistant, "Certainly! Here's an example of a simple web server in Rust using the `actix-web` framework: thread.insert_message(Role::Assistant, "Certainly! Here's an example of a simple web server in Rust using the `actix-web` framework: