assistant2: Combine history views into one (#25293)

This PR combines the two history views in Assistant2 into one.

<img width="1309" alt="Screenshot 2025-02-20 at 5 34 37 PM"
src="https://github.com/user-attachments/assets/fbb08542-58b5-4930-8a20-254234e335fa"
/>

<img width="1309" alt="Screenshot 2025-02-20 at 5 34 41 PM"
src="https://github.com/user-attachments/assets/1174849e-edad-4e02-8bf3-bb92aafba4f8"
/>


Release Notes:

- N/A
This commit is contained in:
Marshall Bowers 2025-02-20 17:53:58 -05:00 committed by GitHub
parent 3a222f0666
commit 74c581b9f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 265 additions and 155 deletions

View file

@ -7,6 +7,7 @@ mod context;
mod context_picker;
mod context_store;
mod context_strip;
mod history_store;
mod inline_assistant;
mod inline_prompt_editor;
mod message_editor;
@ -40,7 +41,6 @@ actions!(
ToggleModelSelector,
RemoveAllContext,
OpenHistory,
OpenPromptEditorHistory,
OpenConfiguration,
RemoveSelectedThread,
Chat,

View file

@ -4,7 +4,7 @@ use std::sync::Arc;
use anyhow::{anyhow, Result};
use assistant_context_editor::{
make_lsp_adapter_delegate, render_remaining_tokens, AssistantPanelDelegate, ConfigurationError,
ContextEditor, ContextHistory, SlashCommandCompletionProvider,
ContextEditor, SlashCommandCompletionProvider,
};
use assistant_settings::{AssistantDockPosition, AssistantSettings};
use assistant_slash_command::SlashCommandWorkingSet;
@ -31,14 +31,12 @@ use zed_actions::assistant::{DeployPromptLibrary, ToggleFocus};
use crate::active_thread::ActiveThread;
use crate::assistant_configuration::{AssistantConfiguration, AssistantConfigurationEvent};
use crate::history_store::{HistoryEntry, HistoryStore};
use crate::message_editor::MessageEditor;
use crate::thread::{Thread, ThreadError, ThreadId};
use crate::thread_history::{PastThread, ThreadHistory};
use crate::thread_history::{PastContext, PastThread, ThreadHistory};
use crate::thread_store::ThreadStore;
use crate::{
InlineAssistant, NewPromptEditor, NewThread, OpenConfiguration, OpenHistory,
OpenPromptEditorHistory,
};
use crate::{InlineAssistant, NewPromptEditor, NewThread, OpenConfiguration, OpenHistory};
pub fn init(cx: &mut App) {
cx.observe_new(
@ -62,12 +60,6 @@ pub fn init(cx: &mut App) {
panel.update(cx, |panel, cx| panel.new_prompt_editor(window, cx));
}
})
.register_action(|workspace, _: &OpenPromptEditorHistory, window, cx| {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
workspace.focus_panel::<AssistantPanel>(window, cx);
panel.update(cx, |panel, cx| panel.open_prompt_editor_history(window, cx));
}
})
.register_action(|workspace, _: &OpenConfiguration, window, cx| {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
workspace.focus_panel::<AssistantPanel>(window, cx);
@ -83,7 +75,6 @@ enum ActiveView {
Thread,
PromptEditor,
History,
PromptEditorHistory,
Configuration,
}
@ -97,15 +88,14 @@ pub struct AssistantPanel {
message_editor: Entity<MessageEditor>,
context_store: Entity<assistant_context_editor::ContextStore>,
context_editor: Option<Entity<ContextEditor>>,
context_history: Option<Entity<ContextHistory>>,
configuration: Option<Entity<AssistantConfiguration>>,
configuration_subscription: Option<Subscription>,
tools: Arc<ToolWorkingSet>,
local_timezone: UtcOffset,
active_view: ActiveView,
history_store: Entity<HistoryStore>,
history: Entity<ThreadHistory>,
new_item_context_menu_handle: PopoverMenuHandle<ContextMenu>,
open_history_context_menu_handle: PopoverMenuHandle<ContextMenu>,
width: Option<Pixels>,
height: Option<Pixels>,
}
@ -173,6 +163,9 @@ impl AssistantPanel {
)
});
let history_store =
cx.new(|cx| HistoryStore::new(thread_store.clone(), context_store.clone(), cx));
Self {
active_view: ActiveView::Thread,
workspace: workspace.clone(),
@ -194,7 +187,6 @@ impl AssistantPanel {
message_editor,
context_store,
context_editor: None,
context_history: None,
configuration: None,
configuration_subscription: None,
tools,
@ -202,9 +194,9 @@ impl AssistantPanel {
chrono::Local::now().offset().local_minus_utc(),
)
.unwrap(),
history: cx.new(|cx| ThreadHistory::new(weak_self, thread_store, cx)),
history_store: history_store.clone(),
history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, cx)),
new_item_context_menu_handle: PopoverMenuHandle::default(),
open_history_context_menu_handle: PopoverMenuHandle::default(),
width: None,
height: None,
}
@ -331,26 +323,7 @@ impl AssistantPanel {
cx.notify();
}
fn open_prompt_editor_history(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.active_view = ActiveView::PromptEditorHistory;
self.context_history = Some(cx.new(|cx| {
ContextHistory::new(
self.project.clone(),
self.context_store.clone(),
self.workspace.clone(),
window,
cx,
)
}));
if let Some(context_history) = self.context_history.as_ref() {
context_history.focus_handle(cx).focus(window);
}
cx.notify();
}
fn open_saved_prompt_editor(
pub(crate) fn open_saved_prompt_editor(
&mut self,
path: PathBuf,
window: &mut Window,
@ -499,13 +472,6 @@ impl Focusable for AssistantPanel {
cx.focus_handle()
}
}
ActiveView::PromptEditorHistory => {
if let Some(context_history) = self.context_history.as_ref() {
context_history.focus_handle(cx)
} else {
cx.focus_handle()
}
}
ActiveView::Configuration => {
if let Some(configuration) = self.configuration.as_ref() {
configuration.focus_handle(cx)
@ -618,18 +584,10 @@ impl AssistantPanel {
SharedString::from(context_editor.read(cx).title(cx).to_string())
})
.unwrap_or_else(|| SharedString::from("Loading Summary…")),
ActiveView::History | ActiveView::PromptEditorHistory => "History".into(),
ActiveView::History => "History".into(),
ActiveView::Configuration => "Assistant Settings".into(),
};
let sub_title = match self.active_view {
ActiveView::Thread => None,
ActiveView::PromptEditor => None,
ActiveView::History => Some("Thread"),
ActiveView::PromptEditorHistory => Some("Prompt Editor"),
ActiveView::Configuration => None,
};
h_flex()
.id("assistant-toolbar")
.px(DynamicSpacing::Base08.rems(cx))
@ -645,24 +603,7 @@ impl AssistantPanel {
.w_full()
.gap_1()
.justify_between()
.child(
h_flex()
.child(Label::new(title))
.when(sub_title.is_some(), |this| {
this.child(
h_flex()
.pl_1p5()
.gap_1p5()
.child(
Label::new("/")
.size(LabelSize::Small)
.color(Color::Disabled)
.alpha(0.5),
)
.child(Label::new(sub_title.unwrap())),
)
}),
)
.child(Label::new(title))
.children(if matches!(self.active_view, ActiveView::PromptEditor) {
self.context_editor
.as_ref()
@ -696,23 +637,23 @@ impl AssistantPanel {
}),
)
.child(
PopoverMenu::new("assistant-toolbar-history-popover-menu")
.trigger_with_tooltip(
IconButton::new("open-history", IconName::HistoryRerun)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle),
Tooltip::text("History…"),
)
.anchor(Corner::TopRight)
.with_handle(self.open_history_context_menu_handle.clone())
.menu(move |window, cx| {
Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
menu.action("Thread History", OpenHistory.boxed_clone())
.action(
"Prompt Editor History",
OpenPromptEditorHistory.boxed_clone(),
)
}))
IconButton::new("open-history", IconName::HistoryRerun)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle)
.tooltip({
let focus_handle = self.focus_handle(cx);
move |window, cx| {
Tooltip::for_action_in(
"History",
&OpenHistory,
&focus_handle,
window,
cx,
)
}
})
.on_click(move |_event, window, cx| {
window.dispatch_action(OpenHistory.boxed_clone(), cx);
}),
)
.child(
@ -762,9 +703,9 @@ impl AssistantPanel {
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
let recent_threads = self
.thread_store
.update(cx, |this, _cx| this.recent_threads(3));
let recent_history = self
.history_store
.update(cx, |this, cx| this.recent_entries(3, cx));
let create_welcome_heading = || {
h_flex()
@ -791,7 +732,8 @@ impl AssistantPanel {
)
.map(|parent| {
match configuration_error {
Some(ConfigurationError::ProviderNotAuthenticated) | Some(ConfigurationError::NoProvider) => {
Some(ConfigurationError::ProviderNotAuthenticated)
| Some(ConfigurationError::NoProvider) => {
parent.child(
v_flex()
.gap_0p5()
@ -818,34 +760,24 @@ impl AssistantPanel {
),
)
}
Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
parent.child(
v_flex()
.gap_0p5()
.child(create_welcome_heading())
.children(provider.render_accept_terms(
LanguageModelProviderTosView::ThreadEmptyState,
cx,
)),
)
}
Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => parent
.child(v_flex().gap_0p5().child(create_welcome_heading()).children(
provider.render_accept_terms(
LanguageModelProviderTosView::ThreadEmptyState,
cx,
),
)),
None => parent,
}
})
.when(
recent_threads.is_empty() && no_error,
|parent| {
parent.child(
v_flex().gap_0p5().child(create_welcome_heading()).child(
h_flex().w_full().justify_center().child(
Label::new("Start typing to chat with your codebase")
.color(Color::Muted),
),
),
)
},
)
.when(!recent_threads.is_empty(), |parent| {
.when(recent_history.is_empty() && no_error, |parent| {
parent.child(v_flex().gap_0p5().child(create_welcome_heading()).child(
h_flex().w_full().justify_center().child(
Label::new("Start typing to chat with your codebase").color(Color::Muted),
),
))
})
.when(!recent_history.is_empty(), |parent| {
parent
.child(
h_flex().w_full().justify_center().child(
@ -855,9 +787,18 @@ impl AssistantPanel {
),
)
.child(v_flex().mx_auto().w_4_5().gap_2().children(
recent_threads.into_iter().map(|thread| {
// TODO: keyboard navigation
PastThread::new(thread, cx.entity().downgrade(), false)
recent_history.into_iter().map(|entry| {
// TODO: Add keyboard navigation.
match entry {
HistoryEntry::Thread(thread) => {
PastThread::new(thread, cx.entity().downgrade(), false)
.into_any_element()
}
HistoryEntry::Context(context) => {
PastContext::new(context, cx.entity().downgrade(), false)
.into_any_element()
}
}
}),
))
.child(
@ -869,7 +810,7 @@ impl AssistantPanel {
&OpenHistory,
&self.focus_handle(cx),
window,
cx
cx,
))
.on_click(move |_event, window, cx| {
window.dispatch_action(OpenHistory.boxed_clone(), cx);
@ -1068,7 +1009,6 @@ impl Render for AssistantPanel {
.children(self.render_last_error(cx)),
ActiveView::History => parent.child(self.history.clone()),
ActiveView::PromptEditor => parent.children(self.context_editor.clone()),
ActiveView::PromptEditorHistory => parent.children(self.context_history.clone()),
ActiveView::Configuration => parent.children(self.configuration.clone()),
})
}

View file

@ -0,0 +1,61 @@
use assistant_context_editor::SavedContextMetadata;
use chrono::{DateTime, Utc};
use gpui::{prelude::*, Entity};
use crate::thread_store::{SavedThreadMetadata, ThreadStore};
pub enum HistoryEntry {
Thread(SavedThreadMetadata),
Context(SavedContextMetadata),
}
impl HistoryEntry {
pub fn updated_at(&self) -> DateTime<Utc> {
match self {
HistoryEntry::Thread(thread) => thread.updated_at,
HistoryEntry::Context(context) => context.mtime.to_utc(),
}
}
}
pub struct HistoryStore {
thread_store: Entity<ThreadStore>,
context_store: Entity<assistant_context_editor::ContextStore>,
}
impl HistoryStore {
pub fn new(
thread_store: Entity<ThreadStore>,
context_store: Entity<assistant_context_editor::ContextStore>,
_cx: &mut Context<Self>,
) -> Self {
Self {
thread_store,
context_store,
}
}
/// Returns the number of history entries.
pub fn entry_count(&self, cx: &mut Context<Self>) -> usize {
self.entries(cx).len()
}
pub fn entries(&self, cx: &mut Context<Self>) -> Vec<HistoryEntry> {
let mut history_entries = Vec::new();
for thread in self.thread_store.update(cx, |this, _cx| this.threads()) {
history_entries.push(HistoryEntry::Thread(thread));
}
for context in self.context_store.update(cx, |this, _cx| this.contexts()) {
history_entries.push(HistoryEntry::Context(context));
}
history_entries.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.updated_at()));
history_entries
}
pub fn recent_entries(&self, limit: usize, cx: &mut Context<Self>) -> Vec<HistoryEntry> {
self.entries(cx).into_iter().take(limit).collect()
}
}

View file

@ -1,3 +1,4 @@
use assistant_context_editor::SavedContextMetadata;
use gpui::{
uniform_list, App, Entity, FocusHandle, Focusable, ScrollStrategy, UniformListScrollHandle,
WeakEntity,
@ -5,13 +6,14 @@ use gpui::{
use time::{OffsetDateTime, UtcOffset};
use ui::{prelude::*, IconButtonShape, ListItem, ListItemSpacing, Tooltip};
use crate::thread_store::{SavedThreadMetadata, ThreadStore};
use crate::history_store::{HistoryEntry, HistoryStore};
use crate::thread_store::SavedThreadMetadata;
use crate::{AssistantPanel, RemoveSelectedThread};
pub struct ThreadHistory {
focus_handle: FocusHandle,
assistant_panel: WeakEntity<AssistantPanel>,
thread_store: Entity<ThreadStore>,
history_store: Entity<HistoryStore>,
scroll_handle: UniformListScrollHandle,
selected_index: usize,
}
@ -19,13 +21,13 @@ pub struct ThreadHistory {
impl ThreadHistory {
pub(crate) fn new(
assistant_panel: WeakEntity<AssistantPanel>,
thread_store: Entity<ThreadStore>,
history_store: Entity<HistoryStore>,
cx: &mut Context<Self>,
) -> Self {
Self {
focus_handle: cx.focus_handle(),
assistant_panel,
thread_store,
history_store,
scroll_handle: UniformListScrollHandle::default(),
selected_index: 0,
}
@ -37,7 +39,9 @@ impl ThreadHistory {
window: &mut Window,
cx: &mut Context<Self>,
) {
let count = self.thread_store.read(cx).thread_count();
let count = self
.history_store
.update(cx, |this, cx| this.entry_count(cx));
if count > 0 {
if self.selected_index == 0 {
self.set_selected_index(count - 1, window, cx);
@ -53,7 +57,9 @@ impl ThreadHistory {
window: &mut Window,
cx: &mut Context<Self>,
) {
let count = self.thread_store.read(cx).thread_count();
let count = self
.history_store
.update(cx, |this, cx| this.entry_count(cx));
if count > 0 {
if self.selected_index == count - 1 {
self.set_selected_index(0, window, cx);
@ -64,14 +70,18 @@ impl ThreadHistory {
}
fn select_first(&mut self, _: &menu::SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
let count = self.thread_store.read(cx).thread_count();
let count = self
.history_store
.update(cx, |this, cx| this.entry_count(cx));
if count > 0 {
self.set_selected_index(0, window, cx);
}
}
fn select_last(&mut self, _: &menu::SelectLast, window: &mut Window, cx: &mut Context<Self>) {
let count = self.thread_store.read(cx).thread_count();
let count = self
.history_store
.update(cx, |this, cx| this.entry_count(cx));
if count > 0 {
self.set_selected_index(count - 1, window, cx);
}
@ -85,12 +95,23 @@ impl ThreadHistory {
}
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
let threads = self.thread_store.update(cx, |this, _cx| this.threads());
let entries = self.history_store.update(cx, |this, cx| this.entries(cx));
if let Some(thread) = threads.get(self.selected_index) {
self.assistant_panel
.update(cx, move |this, cx| this.open_thread(&thread.id, window, cx))
.ok();
if let Some(entry) = entries.get(self.selected_index) {
match entry {
HistoryEntry::Thread(thread) => {
self.assistant_panel
.update(cx, move |this, cx| this.open_thread(&thread.id, window, cx))
.ok();
}
HistoryEntry::Context(context) => {
self.assistant_panel
.update(cx, move |this, cx| {
this.open_saved_prompt_editor(context.path.clone(), window, cx)
})
.ok();
}
}
cx.notify();
}
@ -102,14 +123,19 @@ impl ThreadHistory {
_window: &mut Window,
cx: &mut Context<Self>,
) {
let threads = self.thread_store.update(cx, |this, _cx| this.threads());
let entries = self.history_store.update(cx, |this, cx| this.entries(cx));
if let Some(thread) = threads.get(self.selected_index) {
self.assistant_panel
.update(cx, |this, cx| {
this.delete_thread(&thread.id, cx);
})
.ok();
if let Some(entry) = entries.get(self.selected_index) {
match entry {
HistoryEntry::Thread(thread) => {
self.assistant_panel
.update(cx, |this, cx| {
this.delete_thread(&thread.id, cx);
})
.ok();
}
HistoryEntry::Context(_context) => {}
}
cx.notify();
}
@ -124,7 +150,7 @@ impl Focusable for ThreadHistory {
impl Render for ThreadHistory {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let threads = self.thread_store.update(cx, |this, _cx| this.threads());
let history_entries = self.history_store.update(cx, |this, cx| this.entries(cx));
let selected_index = self.selected_index;
v_flex()
@ -141,7 +167,7 @@ impl Render for ThreadHistory {
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::remove_selected_thread))
.map(|history| {
if threads.is_empty() {
if history_entries.is_empty() {
history
.justify_center()
.child(
@ -155,17 +181,26 @@ impl Render for ThreadHistory {
uniform_list(
cx.entity().clone(),
"thread-history",
threads.len(),
history_entries.len(),
move |history, range, _window, _cx| {
threads[range]
history_entries[range]
.iter()
.enumerate()
.map(|(index, thread)| {
h_flex().w_full().pb_1().child(PastThread::new(
thread.clone(),
history.assistant_panel.clone(),
selected_index == index,
))
.map(|(index, entry)| {
h_flex().w_full().pb_1().child(match entry {
HistoryEntry::Thread(thread) => PastThread::new(
thread.clone(),
history.assistant_panel.clone(),
selected_index == index,
)
.into_any_element(),
HistoryEntry::Context(context) => PastContext::new(
context.clone(),
history.assistant_panel.clone(),
selected_index == index,
)
.into_any_element(),
})
})
.collect()
},
@ -261,3 +296,71 @@ impl RenderOnce for PastThread {
})
}
}
#[derive(IntoElement)]
pub struct PastContext {
context: SavedContextMetadata,
assistant_panel: WeakEntity<AssistantPanel>,
selected: bool,
}
impl PastContext {
pub fn new(
context: SavedContextMetadata,
assistant_panel: WeakEntity<AssistantPanel>,
selected: bool,
) -> Self {
Self {
context,
assistant_panel,
selected,
}
}
}
impl RenderOnce for PastContext {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let summary = self.context.title;
let context_timestamp = time_format::format_localized_timestamp(
OffsetDateTime::from_unix_timestamp(self.context.mtime.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(SharedString::from(
self.context.path.to_string_lossy().to_string(),
))
.outlined()
.toggle_state(self.selected)
.start_slot(
Icon::new(IconName::Code)
.size(IconSize::Small)
.color(Color::Muted),
)
.spacing(ListItemSpacing::Sparse)
.child(Label::new(summary).size(LabelSize::Small).text_ellipsis())
.end_slot(
h_flex().gap_1p5().child(
Label::new(context_timestamp)
.color(Color::Muted)
.size(LabelSize::XSmall),
),
)
.on_click({
let assistant_panel = self.assistant_panel.clone();
let path = self.context.path.clone();
move |_event, window, cx| {
assistant_panel
.update(cx, |this, cx| {
this.open_saved_prompt_editor(path.clone(), window, cx)
.detach_and_log_err(cx);
})
.ok();
}
})
}
}

View file

@ -3366,7 +3366,7 @@ impl SavedContextV0_1_0 {
}
}
#[derive(Clone)]
#[derive(Debug, Clone)]
pub struct SavedContextMetadata {
pub title: String,
pub path: PathBuf,

View file

@ -350,6 +350,12 @@ impl ContextStore {
}
}
pub fn contexts(&self) -> Vec<SavedContextMetadata> {
let mut contexts = self.contexts_metadata.iter().cloned().collect::<Vec<_>>();
contexts.sort_unstable_by_key(|thread| std::cmp::Reverse(thread.mtime));
contexts
}
pub fn create(&mut self, cx: &mut Context<Self>) -> Entity<AssistantContext> {
let context = cx.new(|cx| {
AssistantContext::local(