assistant2: Make context pills clickable (#27680)

Release Notes:

- N/A
This commit is contained in:
Bennet Bo Fenner 2025-03-28 20:05:30 +01:00 committed by GitHub
parent 94ed0b7767
commit fcadcbb510
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 189 additions and 53 deletions

View file

@ -1,3 +1,4 @@
use crate::context::{AssistantContext, ContextId};
use crate::thread::{ use crate::thread::{
LastRestoreCheckpoint, MessageId, MessageSegment, RequestKind, Thread, ThreadError, LastRestoreCheckpoint, MessageId, MessageSegment, RequestKind, Thread, ThreadError,
ThreadEvent, ThreadFeedback, ThreadEvent, ThreadFeedback,
@ -19,9 +20,12 @@ use gpui::{
use language::{Buffer, LanguageRegistry}; use language::{Buffer, LanguageRegistry};
use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role}; use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role};
use markdown::{Markdown, MarkdownStyle}; use markdown::{Markdown, MarkdownStyle};
use project::ProjectItem as _;
use settings::Settings as _; use settings::Settings as _;
use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use text::ToPoint;
use theme::ThemeSettings; use theme::ThemeSettings;
use ui::{prelude::*, Disclosure, IconButton, KeyBinding, Scrollbar, ScrollbarState, Tooltip}; use ui::{prelude::*, Disclosure, IconButton, KeyBinding, Scrollbar, ScrollbarState, Tooltip};
use util::ResultExt as _; use util::ResultExt as _;
@ -778,6 +782,9 @@ impl ActiveThread {
return Empty.into_any(); return Empty.into_any();
}; };
let context_store = self.context_store.clone();
let workspace = self.workspace.clone();
let thread = self.thread.read(cx); let thread = self.thread.read(cx);
// Get all the data we need from thread before we start using it in closures // Get all the data we need from thread before we start using it in closures
let checkpoint = thread.checkpoint_for_message(message_id); let checkpoint = thread.checkpoint_for_message(message_id);
@ -901,36 +908,53 @@ impl ActiveThread {
.into_any_element(), .into_any_element(),
}; };
let message_content = v_flex() let message_content =
.gap_1p5() v_flex()
.child( .gap_1p5()
if let Some(edit_message_editor) = edit_message_editor.clone() { .child(
div() if let Some(edit_message_editor) = edit_message_editor.clone() {
.key_context("EditMessageEditor") div()
.on_action(cx.listener(Self::cancel_editing_message)) .key_context("EditMessageEditor")
.on_action(cx.listener(Self::confirm_editing_message)) .on_action(cx.listener(Self::cancel_editing_message))
.min_h_6() .on_action(cx.listener(Self::confirm_editing_message))
.child(edit_message_editor) .min_h_6()
} else { .child(edit_message_editor)
div() } else {
.min_h_6() div()
.text_ui(cx) .min_h_6()
.child(self.render_message_content(message_id, rendered_message, cx)) .text_ui(cx)
}, .child(self.render_message_content(message_id, rendered_message, cx))
) },
.when_some(context, |parent, context| { )
if !context.is_empty() { .when_some(context, |parent, context| {
parent.child( if !context.is_empty() {
h_flex().flex_wrap().gap_1().children( parent.child(h_flex().flex_wrap().gap_1().children(
context context.into_iter().map(|context| {
.into_iter() let context_id = context.id;
.map(|context| ContextPill::added(context, false, false, None)), ContextPill::added(context, false, false, None).on_click(Rc::new(
), cx.listener({
) let workspace = workspace.clone();
} else { let context_store = context_store.clone();
parent move |_, _, window, cx| {
} if let Some(workspace) = workspace.upgrade() {
}); open_context(
context_id,
context_store.clone(),
workspace,
window,
cx,
);
cx.notify();
}
}
}),
))
}),
))
} else {
parent
}
});
let styled_message = match message.role { let styled_message = match message.role {
Role::User => v_flex() Role::User => v_flex()
@ -1823,3 +1847,93 @@ impl Render for ActiveThread {
.child(self.render_vertical_scrollbar(cx)) .child(self.render_vertical_scrollbar(cx))
} }
} }
pub(crate) fn open_context(
id: ContextId,
context_store: Entity<ContextStore>,
workspace: Entity<Workspace>,
window: &mut Window,
cx: &mut App,
) {
let Some(context) = context_store.read(cx).context_for_id(id) else {
return;
};
match context {
AssistantContext::File(file_context) => {
if let Some(project_path) = file_context.context_buffer.buffer.read(cx).project_path(cx)
{
workspace.update(cx, |workspace, cx| {
workspace
.open_path(project_path, None, true, window, cx)
.detach_and_log_err(cx);
});
}
}
AssistantContext::Directory(directory_context) => {
let path = directory_context.path.clone();
workspace.update(cx, |workspace, cx| {
workspace.project().update(cx, |project, cx| {
if let Some(entry) = project.entry_for_path(&path, cx) {
cx.emit(project::Event::RevealInProjectPanel(entry.id));
}
})
})
}
AssistantContext::Symbol(symbol_context) => {
if let Some(project_path) = symbol_context
.context_symbol
.buffer
.read(cx)
.project_path(cx)
{
let snapshot = symbol_context.context_symbol.buffer.read(cx).snapshot();
let target_position = symbol_context
.context_symbol
.id
.range
.start
.to_point(&snapshot);
let open_task = workspace.update(cx, |workspace, cx| {
workspace.open_path(project_path, None, true, window, cx)
});
window
.spawn(cx, async move |cx| {
if let Some(active_editor) = open_task
.await
.log_err()
.and_then(|item| item.downcast::<Editor>())
{
active_editor
.downgrade()
.update_in(cx, |editor, window, cx| {
editor.go_to_singleton_buffer_point(
target_position,
window,
cx,
);
})
.log_err();
}
})
.detach();
}
}
AssistantContext::FetchedUrl(fetched_url_context) => {
cx.open_url(&fetched_url_context.url);
}
AssistantContext::Thread(thread_context) => {
let thread_id = thread_context.thread.read(cx).id().clone();
workspace.update(cx, |workspace, cx| {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
panel.update(cx, |panel, cx| {
panel
.open_thread(&thread_id, window, cx)
.detach_and_log_err(cx)
});
}
})
}
}
}

View file

@ -1,5 +1,4 @@
use std::rc::Rc; use std::ops::Range;
use std::{ops::Range, path::Path};
use file_icons::FileIcons; use file_icons::FileIcons;
use gpui::{App, Entity, SharedString}; use gpui::{App, Entity, SharedString};
@ -85,7 +84,7 @@ pub struct FileContext {
#[derive(Debug)] #[derive(Debug)]
pub struct DirectoryContext { pub struct DirectoryContext {
pub path: Rc<Path>, pub path: ProjectPath,
pub context_buffers: Vec<ContextBuffer>, pub context_buffers: Vec<ContextBuffer>,
pub snapshot: ContextSnapshot, pub snapshot: ContextSnapshot,
} }
@ -185,17 +184,18 @@ impl FileContext {
impl DirectoryContext { impl DirectoryContext {
pub fn new( pub fn new(
id: ContextId, id: ContextId,
path: &Path, project_path: ProjectPath,
context_buffers: Vec<ContextBuffer>, context_buffers: Vec<ContextBuffer>,
) -> DirectoryContext { ) -> DirectoryContext {
let full_path: SharedString = path.to_string_lossy().into_owned().into(); let full_path: SharedString = project_path.path.to_string_lossy().into_owned().into();
let name = match path.file_name() { let name = match project_path.path.file_name() {
Some(name) => name.to_string_lossy().into_owned().into(), Some(name) => name.to_string_lossy().into_owned().into(),
None => full_path.clone(), None => full_path.clone(),
}; };
let parent = path let parent = project_path
.path
.parent() .parent()
.and_then(|p| p.file_name()) .and_then(|p| p.file_name())
.map(|p| p.to_string_lossy().into_owned().into()); .map(|p| p.to_string_lossy().into_owned().into());
@ -208,7 +208,7 @@ impl DirectoryContext {
.into(); .into();
DirectoryContext { DirectoryContext {
path: path.into(), path: project_path,
context_buffers, context_buffers,
snapshot: ContextSnapshot { snapshot: ContextSnapshot {
id, id,

View file

@ -60,6 +60,10 @@ impl ContextStore {
&self.context &self.context
} }
pub fn context_for_id(&self, id: ContextId) -> Option<&AssistantContext> {
self.context().iter().find(|context| context.id() == id)
}
pub fn clear(&mut self) { pub fn clear(&mut self) {
self.context.clear(); self.context.clear();
self.files.clear(); self.files.clear();
@ -253,21 +257,21 @@ impl ContextStore {
} }
this.update(cx, |this, _| { this.update(cx, |this, _| {
this.insert_directory(&project_path.path, context_buffers); this.insert_directory(project_path, context_buffers);
})?; })?;
anyhow::Ok(()) anyhow::Ok(())
}) })
} }
fn insert_directory(&mut self, path: &Path, context_buffers: Vec<ContextBuffer>) { fn insert_directory(&mut self, project_path: ProjectPath, context_buffers: Vec<ContextBuffer>) {
let id = self.next_context_id.post_inc(); let id = self.next_context_id.post_inc();
self.directories.insert(path.to_path_buf(), id); self.directories.insert(project_path.path.to_path_buf(), id);
self.context self.context
.push(AssistantContext::Directory(DirectoryContext::new( .push(AssistantContext::Directory(DirectoryContext::new(
id, id,
path, project_path,
context_buffers, context_buffers,
))); )));
} }
@ -704,8 +708,9 @@ pub fn refresh_context_store_text(
|| changed_buffers.iter().any(|buffer| { || changed_buffers.iter().any(|buffer| {
let buffer = buffer.read(cx); let buffer = buffer.read(cx);
buffer_path_log_err(&buffer) buffer_path_log_err(&buffer).map_or(false, |path| {
.map_or(false, |path| path.starts_with(&directory_context.path)) path.starts_with(&directory_context.path.path)
})
}); });
if should_refresh { if should_refresh {
@ -797,7 +802,7 @@ fn refresh_directory_text(
let context_buffers = context_buffers.await; let context_buffers = context_buffers.await;
context_store context_store
.update(cx, |context_store, _| { .update(cx, |context_store, _| {
let new_directory_context = DirectoryContext::new(id, &path, context_buffers); let new_directory_context = DirectoryContext::new(id, path, context_buffers);
context_store.replace_context(AssistantContext::Directory(new_directory_context)); context_store.replace_context(AssistantContext::Directory(new_directory_context));
}) })
.ok(); .ok();

View file

@ -4,15 +4,15 @@ use collections::HashSet;
use editor::Editor; use editor::Editor;
use file_icons::FileIcons; use file_icons::FileIcons;
use gpui::{ use gpui::{
App, Bounds, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription, App, Bounds, ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
WeakEntity, Subscription, WeakEntity,
}; };
use itertools::Itertools; use itertools::Itertools;
use language::Buffer; use language::Buffer;
use ui::{prelude::*, KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip}; use ui::{prelude::*, KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip};
use workspace::{notifications::NotifyResultExt, Workspace}; use workspace::{notifications::NotifyResultExt, Workspace};
use crate::context::ContextKind; use crate::context::{ContextId, ContextKind};
use crate::context_picker::{ConfirmBehavior, ContextPicker}; use crate::context_picker::{ConfirmBehavior, ContextPicker};
use crate::context_store::ContextStore; use crate::context_store::ContextStore;
use crate::thread::Thread; use crate::thread::Thread;
@ -277,6 +277,14 @@ impl ContextStrip {
best.map(|(index, _, _)| index) best.map(|(index, _, _)| index)
} }
fn open_context(&mut self, id: ContextId, window: &mut Window, cx: &mut App) {
let Some(workspace) = self.workspace.upgrade() else {
return;
};
crate::active_thread::open_context(id, self.context_store.clone(), workspace, window, cx);
}
fn remove_focused_context( fn remove_focused_context(
&mut self, &mut self,
_: &RemoveFocusedContext, _: &RemoveFocusedContext,
@ -458,6 +466,7 @@ impl Render for ContextStrip {
} }
}) })
.children(context.iter().enumerate().map(|(i, context)| { .children(context.iter().enumerate().map(|(i, context)| {
let id = context.id;
ContextPill::added( ContextPill::added(
context.clone(), context.clone(),
dupe_names.contains(&context.name), dupe_names.contains(&context.name),
@ -473,10 +482,16 @@ impl Render for ContextStrip {
})) }))
}), }),
) )
.on_click(Rc::new(cx.listener(move |this, _, _window, cx| { .on_click(Rc::new(cx.listener(
this.focused_index = Some(i); move |this, event: &ClickEvent, window, cx| {
cx.notify(); if event.down.click_count > 1 {
}))) this.open_context(id, window, cx);
} else {
this.focused_index = Some(i);
}
cx.notify();
},
)))
})) }))
.when_some(suggested_context, |el, suggested| { .when_some(suggested_context, |el, suggested| {
el.child( el.child(

View file

@ -162,7 +162,9 @@ impl RenderOnce for ContextPill {
}) })
.when_some(on_click.as_ref(), |element, on_click| { .when_some(on_click.as_ref(), |element, on_click| {
let on_click = on_click.clone(); let on_click = on_click.clone();
element.on_click(move |event, window, cx| on_click(event, window, cx)) element
.cursor_pointer()
.on_click(move |event, window, cx| on_click(event, window, cx))
}), }),
ContextPill::Suggested { ContextPill::Suggested {
name, name,