Remove ACP feature flags (#38055)
This removes the `gemini-and-native` and `claude-code` feature flags. Also, I removed a bunch of unused agent1 code that we do not need anymore. Initially I wanted to remove much more of the `agent` code, but noticed some things that we need to figure out first: - The inline assistant + context strip use `Thread`/`ContextStore` directly - We need some replacement for `ToolWorkingSet`, so we can access available tools (as well as context server tools) in other places, e.g. the agent configuration and the configure profile modal Release Notes: - N/A
This commit is contained in:
Generated
+2
-4
@@ -330,6 +330,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"cloud_llm_client",
|
||||
"collections",
|
||||
"convert_case 0.8.0",
|
||||
"fs",
|
||||
"gpui",
|
||||
"language_model",
|
||||
@@ -339,6 +340,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_json_lenient",
|
||||
"settings",
|
||||
"util",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
@@ -383,7 +385,6 @@ dependencies = [
|
||||
"html_to_markdown",
|
||||
"http_client",
|
||||
"indoc",
|
||||
"inventory",
|
||||
"itertools 0.14.0",
|
||||
"jsonschema",
|
||||
"language",
|
||||
@@ -434,7 +435,6 @@ dependencies = [
|
||||
"url",
|
||||
"urlencoding",
|
||||
"util",
|
||||
"uuid",
|
||||
"watch",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
@@ -20505,7 +20505,6 @@ dependencies = [
|
||||
"ashpd",
|
||||
"askpass",
|
||||
"assets",
|
||||
"assistant_tool",
|
||||
"assistant_tools",
|
||||
"audio",
|
||||
"auto_update",
|
||||
@@ -20550,7 +20549,6 @@ dependencies = [
|
||||
"gpui_tokio",
|
||||
"http_client",
|
||||
"image_viewer",
|
||||
"indoc",
|
||||
"inspector_ui",
|
||||
"install_cli",
|
||||
"itertools 0.14.0",
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use crate::{
|
||||
ThreadId,
|
||||
thread_store::{SerializedThreadMetadata, ThreadStore},
|
||||
};
|
||||
use crate::{ThreadId, thread_store::SerializedThreadMetadata};
|
||||
use anyhow::{Context as _, Result};
|
||||
use assistant_context::SavedContextMetadata;
|
||||
use chrono::{DateTime, Utc};
|
||||
@@ -61,7 +58,6 @@ enum SerializedRecentOpen {
|
||||
}
|
||||
|
||||
pub struct HistoryStore {
|
||||
thread_store: Entity<ThreadStore>,
|
||||
context_store: Entity<assistant_context::ContextStore>,
|
||||
recently_opened_entries: VecDeque<HistoryEntryId>,
|
||||
_subscriptions: Vec<gpui::Subscription>,
|
||||
@@ -70,15 +66,11 @@ pub struct HistoryStore {
|
||||
|
||||
impl HistoryStore {
|
||||
pub fn new(
|
||||
thread_store: Entity<ThreadStore>,
|
||||
context_store: Entity<assistant_context::ContextStore>,
|
||||
initial_recent_entries: impl IntoIterator<Item = HistoryEntryId>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let subscriptions = vec![
|
||||
cx.observe(&thread_store, |_, _, cx| cx.notify()),
|
||||
cx.observe(&context_store, |_, _, cx| cx.notify()),
|
||||
];
|
||||
let subscriptions = vec![cx.observe(&context_store, |_, _, cx| cx.notify())];
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let entries = Self::load_recently_opened_entries(cx).await.log_err()?;
|
||||
@@ -96,7 +88,6 @@ impl HistoryStore {
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
thread_store,
|
||||
context_store,
|
||||
recently_opened_entries: initial_recent_entries.into_iter().collect(),
|
||||
_subscriptions: subscriptions,
|
||||
@@ -112,13 +103,6 @@ impl HistoryStore {
|
||||
return history_entries;
|
||||
}
|
||||
|
||||
history_entries.extend(
|
||||
self.thread_store
|
||||
.read(cx)
|
||||
.reverse_chronological_threads()
|
||||
.cloned()
|
||||
.map(HistoryEntry::Thread),
|
||||
);
|
||||
history_entries.extend(
|
||||
self.context_store
|
||||
.read(cx)
|
||||
@@ -141,22 +125,6 @@ impl HistoryStore {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let thread_entries = self
|
||||
.thread_store
|
||||
.read(cx)
|
||||
.reverse_chronological_threads()
|
||||
.flat_map(|thread| {
|
||||
self.recently_opened_entries
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(index, entry)| match entry {
|
||||
HistoryEntryId::Thread(id) if &thread.id == id => {
|
||||
Some((index, HistoryEntry::Thread(thread.clone())))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
});
|
||||
|
||||
let context_entries =
|
||||
self.context_store
|
||||
.read(cx)
|
||||
@@ -173,8 +141,7 @@ impl HistoryStore {
|
||||
})
|
||||
});
|
||||
|
||||
thread_entries
|
||||
.chain(context_entries)
|
||||
context_entries
|
||||
// optimization to halt iteration early
|
||||
.take(self.recently_opened_entries.len())
|
||||
.sorted_unstable_by_key(|(index, _)| *index)
|
||||
|
||||
@@ -15,11 +15,14 @@ path = "src/agent_settings.rs"
|
||||
anyhow.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
convert_case.workspace = true
|
||||
fs.workspace = true
|
||||
gpui.workspace = true
|
||||
language_model.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
settings.workspace = true
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use collections::IndexMap;
|
||||
use gpui::SharedString;
|
||||
use convert_case::{Case, Casing as _};
|
||||
use fs::Fs;
|
||||
use gpui::{App, SharedString};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings as _, update_settings_file};
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::AgentSettings;
|
||||
|
||||
pub mod builtin_profiles {
|
||||
use super::AgentProfileId;
|
||||
@@ -38,6 +44,69 @@ impl Default for AgentProfileId {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct AgentProfile {
|
||||
id: AgentProfileId,
|
||||
}
|
||||
|
||||
pub type AvailableProfiles = IndexMap<AgentProfileId, SharedString>;
|
||||
|
||||
impl AgentProfile {
|
||||
pub fn new(id: AgentProfileId) -> Self {
|
||||
Self { id }
|
||||
}
|
||||
|
||||
pub fn id(&self) -> &AgentProfileId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
/// Saves a new profile to the settings.
|
||||
pub fn create(
|
||||
name: String,
|
||||
base_profile_id: Option<AgentProfileId>,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &App,
|
||||
) -> AgentProfileId {
|
||||
let id = AgentProfileId(name.to_case(Case::Kebab).into());
|
||||
|
||||
let base_profile =
|
||||
base_profile_id.and_then(|id| AgentSettings::get_global(cx).profiles.get(&id).cloned());
|
||||
|
||||
let profile_settings = AgentProfileSettings {
|
||||
name: name.into(),
|
||||
tools: base_profile
|
||||
.as_ref()
|
||||
.map(|profile| profile.tools.clone())
|
||||
.unwrap_or_default(),
|
||||
enable_all_context_servers: base_profile
|
||||
.as_ref()
|
||||
.map(|profile| profile.enable_all_context_servers)
|
||||
.unwrap_or_default(),
|
||||
context_servers: base_profile
|
||||
.map(|profile| profile.context_servers)
|
||||
.unwrap_or_default(),
|
||||
};
|
||||
|
||||
update_settings_file::<AgentSettings>(fs, cx, {
|
||||
let id = id.clone();
|
||||
move |settings, _cx| {
|
||||
settings.create_profile(id, profile_settings).log_err();
|
||||
}
|
||||
});
|
||||
|
||||
id
|
||||
}
|
||||
|
||||
/// Returns a map of AgentProfileIds to their names
|
||||
pub fn available_profiles(cx: &App) -> AvailableProfiles {
|
||||
let mut profiles = AvailableProfiles::default();
|
||||
for (id, profile) in AgentSettings::get_global(cx).profiles.iter() {
|
||||
profiles.insert(id.clone(), profile.name.clone());
|
||||
}
|
||||
profiles
|
||||
}
|
||||
}
|
||||
|
||||
/// A profile for the Zed Agent that controls its behavior.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AgentProfileSettings {
|
||||
|
||||
@@ -52,7 +52,6 @@ gpui.workspace = true
|
||||
html_to_markdown.workspace = true
|
||||
http_client.workspace = true
|
||||
indoc.workspace = true
|
||||
inventory.workspace = true
|
||||
itertools.workspace = true
|
||||
jsonschema.workspace = true
|
||||
language.workspace = true
|
||||
@@ -98,7 +97,6 @@ ui_input.workspace = true
|
||||
url.workspace = true
|
||||
urlencoding.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
watch.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
@@ -62,9 +62,9 @@ use crate::acp::message_editor::{MessageEditor, MessageEditorEvent};
|
||||
use crate::agent_diff::AgentDiff;
|
||||
use crate::profile_selector::{ProfileProvider, ProfileSelector};
|
||||
|
||||
use crate::ui::preview::UsageCallout;
|
||||
use crate::ui::{
|
||||
AgentNotification, AgentNotificationEvent, BurnModeTooltip, UnavailableEditingTooltip,
|
||||
UsageCallout,
|
||||
};
|
||||
use crate::{
|
||||
AgentDiffPane, AgentPanel, AllowAlways, AllowOnce, ContinueThread, ContinueWithBurnMode,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@ mod profile_modal_header;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use agent_settings::{AgentProfileId, AgentSettings, builtin_profiles};
|
||||
use agent_settings::{AgentProfile, AgentProfileId, AgentSettings, builtin_profiles};
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use editor::Editor;
|
||||
use fs::Fs;
|
||||
@@ -16,7 +16,6 @@ use workspace::{ModalView, Workspace};
|
||||
use crate::agent_configuration::manage_profiles_modal::profile_modal_header::ProfileModalHeader;
|
||||
use crate::agent_configuration::tool_picker::{ToolPicker, ToolPickerDelegate};
|
||||
use crate::{AgentPanel, ManageProfiles};
|
||||
use agent::agent_profile::AgentProfile;
|
||||
|
||||
use super::tool_picker::ToolPickerMode;
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use crate::{Keep, KeepAll, OpenAgentDiff, Reject, RejectAll};
|
||||
use acp_thread::{AcpThread, AcpThreadEvent};
|
||||
use action_log::ActionLog;
|
||||
use agent::{Thread, ThreadEvent, ThreadSummary};
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::Result;
|
||||
use buffer_diff::DiffHunkStatus;
|
||||
@@ -19,7 +18,6 @@ use gpui::{
|
||||
};
|
||||
|
||||
use language::{Buffer, Capability, DiskState, OffsetRangeExt, Point};
|
||||
use language_model::StopReason;
|
||||
use multi_buffer::PathKey;
|
||||
use project::{Project, ProjectItem, ProjectPath};
|
||||
use settings::{Settings, SettingsStore};
|
||||
@@ -51,34 +49,29 @@ pub struct AgentDiffPane {
|
||||
|
||||
#[derive(PartialEq, Eq, Clone)]
|
||||
pub enum AgentDiffThread {
|
||||
Native(Entity<Thread>),
|
||||
AcpThread(Entity<AcpThread>),
|
||||
}
|
||||
|
||||
impl AgentDiffThread {
|
||||
fn project(&self, cx: &App) -> Entity<Project> {
|
||||
match self {
|
||||
AgentDiffThread::Native(thread) => thread.read(cx).project().clone(),
|
||||
AgentDiffThread::AcpThread(thread) => thread.read(cx).project().clone(),
|
||||
}
|
||||
}
|
||||
fn action_log(&self, cx: &App) -> Entity<ActionLog> {
|
||||
match self {
|
||||
AgentDiffThread::Native(thread) => thread.read(cx).action_log().clone(),
|
||||
AgentDiffThread::AcpThread(thread) => thread.read(cx).action_log().clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn summary(&self, cx: &App) -> ThreadSummary {
|
||||
fn title(&self, cx: &App) -> SharedString {
|
||||
match self {
|
||||
AgentDiffThread::Native(thread) => thread.read(cx).summary().clone(),
|
||||
AgentDiffThread::AcpThread(thread) => ThreadSummary::Ready(thread.read(cx).title()),
|
||||
AgentDiffThread::AcpThread(thread) => thread.read(cx).title(),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_generating(&self, cx: &App) -> bool {
|
||||
match self {
|
||||
AgentDiffThread::Native(thread) => thread.read(cx).is_generating(),
|
||||
AgentDiffThread::AcpThread(thread) => {
|
||||
thread.read(cx).status() == acp_thread::ThreadStatus::Generating
|
||||
}
|
||||
@@ -87,14 +80,12 @@ impl AgentDiffThread {
|
||||
|
||||
fn has_pending_edit_tool_uses(&self, cx: &App) -> bool {
|
||||
match self {
|
||||
AgentDiffThread::Native(thread) => thread.read(cx).has_pending_edit_tool_uses(),
|
||||
AgentDiffThread::AcpThread(thread) => thread.read(cx).has_pending_edit_tool_calls(),
|
||||
}
|
||||
}
|
||||
|
||||
fn downgrade(&self) -> WeakAgentDiffThread {
|
||||
match self {
|
||||
AgentDiffThread::Native(thread) => WeakAgentDiffThread::Native(thread.downgrade()),
|
||||
AgentDiffThread::AcpThread(thread) => {
|
||||
WeakAgentDiffThread::AcpThread(thread.downgrade())
|
||||
}
|
||||
@@ -102,12 +93,6 @@ impl AgentDiffThread {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Entity<Thread>> for AgentDiffThread {
|
||||
fn from(entity: Entity<Thread>) -> Self {
|
||||
AgentDiffThread::Native(entity)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Entity<AcpThread>> for AgentDiffThread {
|
||||
fn from(entity: Entity<AcpThread>) -> Self {
|
||||
AgentDiffThread::AcpThread(entity)
|
||||
@@ -116,25 +101,17 @@ impl From<Entity<AcpThread>> for AgentDiffThread {
|
||||
|
||||
#[derive(PartialEq, Eq, Clone)]
|
||||
pub enum WeakAgentDiffThread {
|
||||
Native(WeakEntity<Thread>),
|
||||
AcpThread(WeakEntity<AcpThread>),
|
||||
}
|
||||
|
||||
impl WeakAgentDiffThread {
|
||||
pub fn upgrade(&self) -> Option<AgentDiffThread> {
|
||||
match self {
|
||||
WeakAgentDiffThread::Native(weak) => weak.upgrade().map(AgentDiffThread::Native),
|
||||
WeakAgentDiffThread::AcpThread(weak) => weak.upgrade().map(AgentDiffThread::AcpThread),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WeakEntity<Thread>> for WeakAgentDiffThread {
|
||||
fn from(entity: WeakEntity<Thread>) -> Self {
|
||||
WeakAgentDiffThread::Native(entity)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WeakEntity<AcpThread>> for WeakAgentDiffThread {
|
||||
fn from(entity: WeakEntity<AcpThread>) -> Self {
|
||||
WeakAgentDiffThread::AcpThread(entity)
|
||||
@@ -203,10 +180,6 @@ impl AgentDiffPane {
|
||||
this.update_excerpts(window, cx)
|
||||
}),
|
||||
match &thread {
|
||||
AgentDiffThread::Native(thread) => cx
|
||||
.subscribe(thread, |this, _thread, event, cx| {
|
||||
this.handle_native_thread_event(event, cx)
|
||||
}),
|
||||
AgentDiffThread::AcpThread(thread) => cx
|
||||
.subscribe(thread, |this, _thread, event, cx| {
|
||||
this.handle_acp_thread_event(event, cx)
|
||||
@@ -313,19 +286,13 @@ impl AgentDiffPane {
|
||||
}
|
||||
|
||||
fn update_title(&mut self, cx: &mut Context<Self>) {
|
||||
let new_title = self.thread.summary(cx).unwrap_or("Agent Changes");
|
||||
let new_title = self.thread.title(cx);
|
||||
if new_title != self.title {
|
||||
self.title = new_title;
|
||||
cx.emit(EditorEvent::TitleChanged);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_native_thread_event(&mut self, event: &ThreadEvent, cx: &mut Context<Self>) {
|
||||
if let ThreadEvent::SummaryGenerated = event {
|
||||
self.update_title(cx)
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_acp_thread_event(&mut self, event: &AcpThreadEvent, cx: &mut Context<Self>) {
|
||||
if let AcpThreadEvent::TitleUpdated = event {
|
||||
self.update_title(cx)
|
||||
@@ -569,8 +536,8 @@ impl Item for AgentDiffPane {
|
||||
}
|
||||
|
||||
fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
|
||||
let summary = self.thread.summary(cx).unwrap_or("Agent Changes");
|
||||
Label::new(format!("Review: {}", summary))
|
||||
let title = self.thread.title(cx);
|
||||
Label::new(format!("Review: {}", title))
|
||||
.color(if params.selected {
|
||||
Color::Default
|
||||
} else {
|
||||
@@ -1339,12 +1306,6 @@ impl AgentDiff {
|
||||
});
|
||||
|
||||
let thread_subscription = match &thread {
|
||||
AgentDiffThread::Native(thread) => cx.subscribe_in(thread, window, {
|
||||
let workspace = workspace.clone();
|
||||
move |this, _thread, event, window, cx| {
|
||||
this.handle_native_thread_event(&workspace, event, window, cx)
|
||||
}
|
||||
}),
|
||||
AgentDiffThread::AcpThread(thread) => cx.subscribe_in(thread, window, {
|
||||
let workspace = workspace.clone();
|
||||
move |this, thread, event, window, cx| {
|
||||
@@ -1447,47 +1408,6 @@ impl AgentDiff {
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_native_thread_event(
|
||||
&mut self,
|
||||
workspace: &WeakEntity<Workspace>,
|
||||
event: &ThreadEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
ThreadEvent::NewRequest
|
||||
| ThreadEvent::Stopped(Ok(StopReason::EndTurn))
|
||||
| ThreadEvent::Stopped(Ok(StopReason::MaxTokens))
|
||||
| ThreadEvent::Stopped(Ok(StopReason::Refusal))
|
||||
| ThreadEvent::Stopped(Err(_))
|
||||
| ThreadEvent::ShowError(_)
|
||||
| ThreadEvent::CompletionCanceled => {
|
||||
self.update_reviewing_editors(workspace, window, cx);
|
||||
}
|
||||
// intentionally being exhaustive in case we add a variant we should handle
|
||||
ThreadEvent::Stopped(Ok(StopReason::ToolUse))
|
||||
| ThreadEvent::StreamedCompletion
|
||||
| ThreadEvent::ReceivedTextChunk
|
||||
| ThreadEvent::StreamedAssistantText(_, _)
|
||||
| ThreadEvent::StreamedAssistantThinking(_, _)
|
||||
| ThreadEvent::StreamedToolUse { .. }
|
||||
| ThreadEvent::InvalidToolInput { .. }
|
||||
| ThreadEvent::MissingToolUse { .. }
|
||||
| ThreadEvent::MessageAdded(_)
|
||||
| ThreadEvent::MessageEdited(_)
|
||||
| ThreadEvent::MessageDeleted(_)
|
||||
| ThreadEvent::SummaryGenerated
|
||||
| ThreadEvent::SummaryChanged
|
||||
| ThreadEvent::UsePendingTools { .. }
|
||||
| ThreadEvent::ToolFinished { .. }
|
||||
| ThreadEvent::CheckpointChanged
|
||||
| ThreadEvent::ToolConfirmationNeeded
|
||||
| ThreadEvent::ToolUseLimitReached
|
||||
| ThreadEvent::CancelEditing
|
||||
| ThreadEvent::ProfileChanged => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_acp_thread_event(
|
||||
&mut self,
|
||||
workspace: &WeakEntity<Workspace>,
|
||||
@@ -1890,16 +1810,14 @@ impl editor::Addon for EditorAgentDiffAddon {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::Keep;
|
||||
use agent::thread_store::{self, ThreadStore};
|
||||
use acp_thread::AgentConnection as _;
|
||||
use agent_settings::AgentSettings;
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use editor::EditorSettings;
|
||||
use gpui::{TestAppContext, UpdateGlobal, VisualTestContext};
|
||||
use project::{FakeFs, Project};
|
||||
use prompt_store::PromptBuilder;
|
||||
use serde_json::json;
|
||||
use settings::{Settings, SettingsStore};
|
||||
use std::sync::Arc;
|
||||
use std::{path::Path, rc::Rc};
|
||||
use theme::ThemeSettings;
|
||||
use util::path;
|
||||
|
||||
@@ -1912,7 +1830,6 @@ mod tests {
|
||||
Project::init_settings(cx);
|
||||
AgentSettings::register(cx);
|
||||
prompt_store::init(cx);
|
||||
thread_store::init(cx);
|
||||
workspace::init_settings(cx);
|
||||
ThemeSettings::register(cx);
|
||||
EditorSettings::register(cx);
|
||||
@@ -1932,21 +1849,17 @@ mod tests {
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let prompt_store = None;
|
||||
let thread_store = cx
|
||||
let connection = Rc::new(acp_thread::StubAgentConnection::new());
|
||||
let thread = cx
|
||||
.update(|cx| {
|
||||
ThreadStore::load(
|
||||
project.clone(),
|
||||
cx.new(|_| ToolWorkingSet::default()),
|
||||
prompt_store,
|
||||
Arc::new(PromptBuilder::new(None).unwrap()),
|
||||
cx,
|
||||
)
|
||||
connection
|
||||
.clone()
|
||||
.new_thread(project.clone(), Path::new(path!("/test")), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let thread =
|
||||
AgentDiffThread::Native(thread_store.update(cx, |store, cx| store.create_thread(cx)));
|
||||
|
||||
let thread = AgentDiffThread::AcpThread(thread);
|
||||
let action_log = cx.read(|cx| thread.action_log(cx));
|
||||
|
||||
let (workspace, cx) =
|
||||
@@ -2069,7 +1982,6 @@ mod tests {
|
||||
Project::init_settings(cx);
|
||||
AgentSettings::register(cx);
|
||||
prompt_store::init(cx);
|
||||
thread_store::init(cx);
|
||||
workspace::init_settings(cx);
|
||||
ThemeSettings::register(cx);
|
||||
EditorSettings::register(cx);
|
||||
@@ -2098,22 +2010,6 @@ mod tests {
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let prompt_store = None;
|
||||
let thread_store = cx
|
||||
.update(|cx| {
|
||||
ThreadStore::load(
|
||||
project.clone(),
|
||||
cx.new(|_| ToolWorkingSet::default()),
|
||||
prompt_store,
|
||||
Arc::new(PromptBuilder::new(None).unwrap()),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let thread = thread_store.update(cx, |store, cx| store.create_thread(cx));
|
||||
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
|
||||
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
|
||||
@@ -2132,8 +2028,19 @@ mod tests {
|
||||
}
|
||||
});
|
||||
|
||||
let connection = Rc::new(acp_thread::StubAgentConnection::new());
|
||||
let thread = cx
|
||||
.update(|_, cx| {
|
||||
connection
|
||||
.clone()
|
||||
.new_thread(project.clone(), Path::new(path!("/test")), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
|
||||
|
||||
// Set the active thread
|
||||
let thread = AgentDiffThread::Native(thread);
|
||||
let thread = AgentDiffThread::AcpThread(thread);
|
||||
cx.update(|window, cx| {
|
||||
AgentDiff::set_active_thread(&workspace.downgrade(), thread.clone(), window, cx)
|
||||
});
|
||||
|
||||
@@ -5,7 +5,6 @@ use crate::{
|
||||
use agent_settings::AgentSettings;
|
||||
use fs::Fs;
|
||||
use gpui::{Entity, FocusHandle, SharedString};
|
||||
use language_model::{ConfiguredModel, LanguageModelRegistry};
|
||||
use picker::popover_menu::PickerPopoverMenu;
|
||||
use settings::update_settings_file;
|
||||
use std::sync::Arc;
|
||||
@@ -39,28 +38,6 @@ impl AgentModelSelector {
|
||||
let provider = model.provider_id().0.to_string();
|
||||
let model_id = model.id().0.to_string();
|
||||
match &model_usage_context {
|
||||
ModelUsageContext::Thread(thread) => {
|
||||
thread.update(cx, |thread, cx| {
|
||||
let registry = LanguageModelRegistry::read_global(cx);
|
||||
if let Some(provider) = registry.provider(&model.provider_id())
|
||||
{
|
||||
thread.set_configured_model(
|
||||
Some(ConfiguredModel {
|
||||
provider,
|
||||
model: model.clone(),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
});
|
||||
update_settings_file::<AgentSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _cx| {
|
||||
settings.set_model(model.clone());
|
||||
},
|
||||
);
|
||||
}
|
||||
ModelUsageContext::InlineAssistant => {
|
||||
update_settings_file::<AgentSettings>(
|
||||
fs.clone(),
|
||||
|
||||
+126
-1612
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,4 @@
|
||||
mod acp;
|
||||
mod active_thread;
|
||||
mod agent_configuration;
|
||||
mod agent_diff;
|
||||
mod agent_model_selector;
|
||||
@@ -8,7 +7,6 @@ mod buffer_codegen;
|
||||
mod context_picker;
|
||||
mod context_server_configuration;
|
||||
mod context_strip;
|
||||
mod debug;
|
||||
mod inline_assistant;
|
||||
mod inline_prompt_editor;
|
||||
mod language_model_selector;
|
||||
@@ -20,14 +18,12 @@ mod slash_command_settings;
|
||||
mod terminal_codegen;
|
||||
mod terminal_inline_assistant;
|
||||
mod text_thread_editor;
|
||||
mod thread_history;
|
||||
mod tool_compatibility;
|
||||
mod ui;
|
||||
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use agent::{Thread, ThreadId};
|
||||
use agent::ThreadId;
|
||||
use agent_settings::{AgentProfileId, AgentSettings, LanguageModelSelection};
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use client::Client;
|
||||
@@ -47,14 +43,12 @@ use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings as _, SettingsStore};
|
||||
use std::any::TypeId;
|
||||
|
||||
pub use crate::active_thread::ActiveThread;
|
||||
use crate::agent_configuration::{ConfigureContextServerModal, ManageProfilesModal};
|
||||
pub use crate::agent_panel::{AgentPanel, ConcreteAssistantPanelDelegate};
|
||||
pub use crate::inline_assistant::InlineAssistant;
|
||||
use crate::slash_command_settings::SlashCommandSettings;
|
||||
pub use agent_diff::{AgentDiffPane, AgentDiffToolbar};
|
||||
pub use text_thread_editor::{AgentPanelDelegate, TextThreadEditor};
|
||||
pub use ui::preview::{all_agent_previews, get_agent_preview};
|
||||
use zed_actions;
|
||||
|
||||
actions!(
|
||||
@@ -235,14 +229,12 @@ impl ManageProfiles {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum ModelUsageContext {
|
||||
Thread(Entity<Thread>),
|
||||
InlineAssistant,
|
||||
}
|
||||
|
||||
impl ModelUsageContext {
|
||||
pub fn configured_model(&self, cx: &App) -> Option<ConfiguredModel> {
|
||||
match self {
|
||||
Self::Thread(thread) => thread.read(cx).configured_model(),
|
||||
Self::InlineAssistant => {
|
||||
LanguageModelRegistry::read_global(cx).inline_assistant_model()
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ pub(crate) mod symbol_context_picker;
|
||||
pub(crate) mod thread_context_picker;
|
||||
|
||||
use std::ops::Range;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
@@ -23,9 +23,8 @@ use gpui::{
|
||||
};
|
||||
use language::Buffer;
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use paths::contexts_dir;
|
||||
use project::{Entry, ProjectPath};
|
||||
use prompt_store::{PromptStore, UserPromptId};
|
||||
use project::ProjectPath;
|
||||
use prompt_store::PromptStore;
|
||||
use rules_context_picker::{RulesContextEntry, RulesContextPicker};
|
||||
use symbol_context_picker::SymbolContextPicker;
|
||||
use thread_context_picker::{
|
||||
@@ -34,10 +33,8 @@ use thread_context_picker::{
|
||||
use ui::{
|
||||
ButtonLike, ContextMenu, ContextMenuEntry, ContextMenuItem, Disclosure, TintColor, prelude::*,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
use workspace::{Workspace, notifications::NotifyResultExt};
|
||||
|
||||
use crate::AgentPanel;
|
||||
use agent::{
|
||||
ThreadId,
|
||||
context::RULES_ICON,
|
||||
@@ -664,7 +661,7 @@ pub(crate) fn recent_context_picker_entries(
|
||||
text_thread_store: Option<WeakEntity<TextThreadStore>>,
|
||||
workspace: Entity<Workspace>,
|
||||
exclude_paths: &HashSet<PathBuf>,
|
||||
exclude_threads: &HashSet<ThreadId>,
|
||||
_exclude_threads: &HashSet<ThreadId>,
|
||||
cx: &App,
|
||||
) -> Vec<RecentEntry> {
|
||||
let mut recent = Vec::with_capacity(6);
|
||||
@@ -690,19 +687,13 @@ pub(crate) fn recent_context_picker_entries(
|
||||
}),
|
||||
);
|
||||
|
||||
let active_thread_id = workspace
|
||||
.panel::<AgentPanel>(cx)
|
||||
.and_then(|panel| Some(panel.read(cx).active_thread(cx)?.read(cx).id()));
|
||||
|
||||
if let Some((thread_store, text_thread_store)) = thread_store
|
||||
.and_then(|store| store.upgrade())
|
||||
.zip(text_thread_store.and_then(|store| store.upgrade()))
|
||||
{
|
||||
let mut threads = unordered_thread_entries(thread_store, text_thread_store, cx)
|
||||
.filter(|(_, thread)| match thread {
|
||||
ThreadContextEntry::Thread { id, .. } => {
|
||||
Some(id) != active_thread_id && !exclude_threads.contains(id)
|
||||
}
|
||||
ThreadContextEntry::Thread { .. } => false,
|
||||
ThreadContextEntry::Context { .. } => true,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
@@ -874,15 +865,7 @@ fn fold_toggle(
|
||||
}
|
||||
}
|
||||
|
||||
pub enum MentionLink {
|
||||
File(ProjectPath, Entry),
|
||||
Symbol(ProjectPath, String),
|
||||
Selection(ProjectPath, Range<usize>),
|
||||
Fetch(String),
|
||||
Thread(ThreadId),
|
||||
TextThread(Arc<Path>),
|
||||
Rule(UserPromptId),
|
||||
}
|
||||
pub struct MentionLink;
|
||||
|
||||
impl MentionLink {
|
||||
const FILE: &str = "@file";
|
||||
@@ -894,17 +877,6 @@ impl MentionLink {
|
||||
|
||||
const TEXT_THREAD_URL_PREFIX: &str = "text-thread://";
|
||||
|
||||
const SEPARATOR: &str = ":";
|
||||
|
||||
pub fn is_valid(url: &str) -> bool {
|
||||
url.starts_with(Self::FILE)
|
||||
|| url.starts_with(Self::SYMBOL)
|
||||
|| url.starts_with(Self::FETCH)
|
||||
|| url.starts_with(Self::SELECTION)
|
||||
|| url.starts_with(Self::THREAD)
|
||||
|| url.starts_with(Self::RULE)
|
||||
}
|
||||
|
||||
pub fn for_file(file_name: &str, full_path: &str) -> String {
|
||||
format!("[@{}]({}:{})", file_name, Self::FILE, full_path)
|
||||
}
|
||||
@@ -958,75 +930,4 @@ impl MentionLink {
|
||||
pub fn for_rule(rule: &RulesContextEntry) -> String {
|
||||
format!("[@{}]({}:{})", rule.title, Self::RULE, rule.prompt_id.0)
|
||||
}
|
||||
|
||||
pub fn try_parse(link: &str, workspace: &Entity<Workspace>, cx: &App) -> Option<Self> {
|
||||
fn extract_project_path_from_link(
|
||||
path: &str,
|
||||
workspace: &Entity<Workspace>,
|
||||
cx: &App,
|
||||
) -> Option<ProjectPath> {
|
||||
let path = PathBuf::from(path);
|
||||
let worktree_name = path.iter().next()?;
|
||||
let path: PathBuf = path.iter().skip(1).collect();
|
||||
let worktree_id = workspace
|
||||
.read(cx)
|
||||
.visible_worktrees(cx)
|
||||
.find(|worktree| worktree.read(cx).root_name() == worktree_name)
|
||||
.map(|worktree| worktree.read(cx).id())?;
|
||||
Some(ProjectPath {
|
||||
worktree_id,
|
||||
path: path.into(),
|
||||
})
|
||||
}
|
||||
|
||||
let (prefix, argument) = link.split_once(Self::SEPARATOR)?;
|
||||
match prefix {
|
||||
Self::FILE => {
|
||||
let project_path = extract_project_path_from_link(argument, workspace, cx)?;
|
||||
let entry = workspace
|
||||
.read(cx)
|
||||
.project()
|
||||
.read(cx)
|
||||
.entry_for_path(&project_path, cx)?
|
||||
.clone();
|
||||
Some(MentionLink::File(project_path, entry))
|
||||
}
|
||||
Self::SYMBOL => {
|
||||
let (path, symbol) = argument.split_once(Self::SEPARATOR)?;
|
||||
let project_path = extract_project_path_from_link(path, workspace, cx)?;
|
||||
Some(MentionLink::Symbol(project_path, symbol.to_string()))
|
||||
}
|
||||
Self::SELECTION => {
|
||||
let (path, line_args) = argument.split_once(Self::SEPARATOR)?;
|
||||
let project_path = extract_project_path_from_link(path, workspace, cx)?;
|
||||
|
||||
let line_range = {
|
||||
let (start, end) = line_args
|
||||
.trim_start_matches('(')
|
||||
.trim_end_matches(')')
|
||||
.split_once('-')?;
|
||||
start.parse::<usize>().ok()?..end.parse::<usize>().ok()?
|
||||
};
|
||||
|
||||
Some(MentionLink::Selection(project_path, line_range))
|
||||
}
|
||||
Self::THREAD => {
|
||||
if let Some(encoded_filename) = argument.strip_prefix(Self::TEXT_THREAD_URL_PREFIX)
|
||||
{
|
||||
let filename = urlencoding::decode(encoded_filename).ok()?;
|
||||
let path = contexts_dir().join(filename.as_ref()).into();
|
||||
Some(MentionLink::TextThread(path))
|
||||
} else {
|
||||
let thread_id = ThreadId::from(argument);
|
||||
Some(MentionLink::Thread(thread_id))
|
||||
}
|
||||
}
|
||||
Self::FETCH => Some(MentionLink::Fetch(argument.to_string())),
|
||||
Self::RULE => {
|
||||
let prompt_id = UserPromptId(Uuid::try_parse(argument).ok()?);
|
||||
Some(MentionLink::Rule(prompt_id))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,16 +12,19 @@ use agent::{
|
||||
};
|
||||
use collections::HashSet;
|
||||
use editor::Editor;
|
||||
use file_icons::FileIcons;
|
||||
use gpui::{
|
||||
App, Bounds, ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
Subscription, WeakEntity,
|
||||
Subscription, Task, WeakEntity,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use project::ProjectItem;
|
||||
use std::{path::Path, rc::Rc};
|
||||
use rope::Point;
|
||||
use std::rc::Rc;
|
||||
use text::ToPoint as _;
|
||||
use ui::{PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
|
||||
use util::ResultExt as _;
|
||||
use workspace::Workspace;
|
||||
use zed_actions::assistant::OpenRulesLibrary;
|
||||
|
||||
pub struct ContextStrip {
|
||||
context_store: Entity<ContextStore>,
|
||||
@@ -121,38 +124,10 @@ impl ContextStrip {
|
||||
|
||||
fn suggested_context(&self, cx: &App) -> Option<SuggestedContext> {
|
||||
match self.suggest_context_kind {
|
||||
SuggestContextKind::File => self.suggested_file(cx),
|
||||
SuggestContextKind::Thread => self.suggested_thread(cx),
|
||||
}
|
||||
}
|
||||
|
||||
fn suggested_file(&self, cx: &App) -> Option<SuggestedContext> {
|
||||
let workspace = self.workspace.upgrade()?;
|
||||
let active_item = workspace.read(cx).active_item(cx)?;
|
||||
|
||||
let editor = active_item.to_any().downcast::<Editor>().ok()?.read(cx);
|
||||
let active_buffer_entity = editor.buffer().read(cx).as_singleton()?;
|
||||
let active_buffer = active_buffer_entity.read(cx);
|
||||
let project_path = active_buffer.project_path(cx)?;
|
||||
|
||||
if self
|
||||
.context_store
|
||||
.read(cx)
|
||||
.file_path_included(&project_path, cx)
|
||||
.is_some()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let file_name = active_buffer.file()?.file_name(cx);
|
||||
let icon_path = FileIcons::get_icon(Path::new(&file_name), cx);
|
||||
Some(SuggestedContext::File {
|
||||
name: file_name.to_string_lossy().into_owned().into(),
|
||||
buffer: active_buffer_entity.downgrade(),
|
||||
icon_path,
|
||||
})
|
||||
}
|
||||
|
||||
fn suggested_thread(&self, cx: &App) -> Option<SuggestedContext> {
|
||||
if !self.context_picker.read(cx).allow_threads() {
|
||||
return None;
|
||||
@@ -161,24 +136,7 @@ impl ContextStrip {
|
||||
let workspace = self.workspace.upgrade()?;
|
||||
let panel = workspace.read(cx).panel::<AgentPanel>(cx)?.read(cx);
|
||||
|
||||
if let Some(active_thread) = panel.active_thread(cx) {
|
||||
let weak_active_thread = active_thread.downgrade();
|
||||
|
||||
let active_thread = active_thread.read(cx);
|
||||
|
||||
if self
|
||||
.context_store
|
||||
.read(cx)
|
||||
.includes_thread(active_thread.id())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(SuggestedContext::Thread {
|
||||
name: active_thread.summary().or_default(),
|
||||
thread: weak_active_thread,
|
||||
})
|
||||
} else if let Some(active_context_editor) = panel.active_context_editor() {
|
||||
if let Some(active_context_editor) = panel.active_context_editor() {
|
||||
let context = active_context_editor.read(cx).context();
|
||||
let weak_context = context.downgrade();
|
||||
let context = context.read(cx);
|
||||
@@ -328,7 +286,75 @@ impl ContextStrip {
|
||||
return;
|
||||
};
|
||||
|
||||
crate::active_thread::open_context(context, workspace, window, cx);
|
||||
match context {
|
||||
AgentContextHandle::File(file_context) => {
|
||||
if let Some(project_path) = file_context.project_path(cx) {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.open_path(project_path, None, true, window, cx)
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
AgentContextHandle::Directory(directory_context) => {
|
||||
let entry_id = directory_context.entry_id;
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.project().update(cx, |_project, cx| {
|
||||
cx.emit(project::Event::RevealInProjectPanel(entry_id));
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
AgentContextHandle::Symbol(symbol_context) => {
|
||||
let buffer = symbol_context.buffer.read(cx);
|
||||
if let Some(project_path) = buffer.project_path(cx) {
|
||||
let snapshot = buffer.snapshot();
|
||||
let target_position = symbol_context.range.start.to_point(&snapshot);
|
||||
open_editor_at_position(project_path, target_position, &workspace, window, cx)
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
AgentContextHandle::Selection(selection_context) => {
|
||||
let buffer = selection_context.buffer.read(cx);
|
||||
if let Some(project_path) = buffer.project_path(cx) {
|
||||
let snapshot = buffer.snapshot();
|
||||
let target_position = selection_context.range.start.to_point(&snapshot);
|
||||
|
||||
open_editor_at_position(project_path, target_position, &workspace, window, cx)
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
AgentContextHandle::FetchedUrl(fetched_url_context) => {
|
||||
cx.open_url(&fetched_url_context.url);
|
||||
}
|
||||
|
||||
AgentContextHandle::Thread(_thread_context) => {}
|
||||
|
||||
AgentContextHandle::TextThread(text_thread_context) => {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||
let context = text_thread_context.context.clone();
|
||||
window.defer(cx, move |window, cx| {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.open_prompt_editor(context, window, cx)
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
AgentContextHandle::Rules(rules_context) => window.dispatch_action(
|
||||
Box::new(OpenRulesLibrary {
|
||||
prompt_to_select: Some(rules_context.prompt_id.0),
|
||||
}),
|
||||
cx,
|
||||
),
|
||||
|
||||
AgentContextHandle::Image(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_focused_context(
|
||||
@@ -569,6 +595,31 @@ pub enum ContextStripEvent {
|
||||
impl EventEmitter<ContextStripEvent> for ContextStrip {}
|
||||
|
||||
pub enum SuggestContextKind {
|
||||
File,
|
||||
Thread,
|
||||
}
|
||||
|
||||
fn open_editor_at_position(
|
||||
project_path: project::ProjectPath,
|
||||
target_position: Point,
|
||||
workspace: &Entity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<()> {
|
||||
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();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
#![allow(unused, dead_code)]
|
||||
|
||||
use client::{ModelRequestUsage, RequestUsage};
|
||||
use cloud_llm_client::{Plan, PlanV1, UsageLimit};
|
||||
use gpui::Global;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use ui::prelude::*;
|
||||
|
||||
/// Debug only: Used for testing various account states
|
||||
///
|
||||
/// Use this by initializing it with
|
||||
/// `cx.set_global(DebugAccountState::default());` somewhere
|
||||
///
|
||||
/// Then call `cx.debug_account()` to get access
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DebugAccountState {
|
||||
pub enabled: bool,
|
||||
pub trial_expired: bool,
|
||||
pub plan: Plan,
|
||||
pub custom_prompt_usage: ModelRequestUsage,
|
||||
pub usage_based_billing_enabled: bool,
|
||||
pub monthly_spending_cap: i32,
|
||||
pub custom_edit_prediction_usage: UsageLimit,
|
||||
}
|
||||
|
||||
impl DebugAccountState {
|
||||
pub fn enabled(&self) -> bool {
|
||||
self.enabled
|
||||
}
|
||||
|
||||
pub fn set_enabled(&mut self, enabled: bool) -> &mut Self {
|
||||
self.enabled = enabled;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_trial_expired(&mut self, trial_expired: bool) -> &mut Self {
|
||||
self.trial_expired = trial_expired;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_plan(&mut self, plan: Plan) -> &mut Self {
|
||||
self.plan = plan;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_custom_prompt_usage(&mut self, custom_prompt_usage: ModelRequestUsage) -> &mut Self {
|
||||
self.custom_prompt_usage = custom_prompt_usage;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_usage_based_billing_enabled(
|
||||
&mut self,
|
||||
usage_based_billing_enabled: bool,
|
||||
) -> &mut Self {
|
||||
self.usage_based_billing_enabled = usage_based_billing_enabled;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_monthly_spending_cap(&mut self, monthly_spending_cap: i32) -> &mut Self {
|
||||
self.monthly_spending_cap = monthly_spending_cap;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_custom_edit_prediction_usage(
|
||||
&mut self,
|
||||
custom_edit_prediction_usage: UsageLimit,
|
||||
) -> &mut Self {
|
||||
self.custom_edit_prediction_usage = custom_edit_prediction_usage;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DebugAccountState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
trial_expired: false,
|
||||
plan: Plan::V1(PlanV1::ZedFree),
|
||||
custom_prompt_usage: ModelRequestUsage(RequestUsage {
|
||||
limit: UsageLimit::Unlimited,
|
||||
amount: 0,
|
||||
}),
|
||||
usage_based_billing_enabled: false,
|
||||
// $50.00
|
||||
monthly_spending_cap: 5000,
|
||||
custom_edit_prediction_usage: UsageLimit::Unlimited,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DebugAccountState {
|
||||
pub fn get_global(cx: &App) -> &Self {
|
||||
&cx.global::<GlobalDebugAccountState>().0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GlobalDebugAccountState(pub DebugAccountState);
|
||||
|
||||
impl Global for GlobalDebugAccountState {}
|
||||
|
||||
impl Deref for GlobalDebugAccountState {
|
||||
type Target = DebugAccountState;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for GlobalDebugAccountState {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub trait DebugAccount {
|
||||
fn debug_account(&self) -> &DebugAccountState;
|
||||
}
|
||||
|
||||
impl DebugAccount for App {
|
||||
fn debug_account(&self) -> &DebugAccountState {
|
||||
&self.global::<GlobalDebugAccountState>().0
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,8 @@ use editor::{
|
||||
};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
AnyElement, App, Context, CursorStyle, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
Subscription, TextStyle, WeakEntity, Window,
|
||||
AnyElement, App, ClipboardEntry, Context, CursorStyle, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, Subscription, TextStyle, WeakEntity, Window,
|
||||
};
|
||||
use language_model::{LanguageModel, LanguageModelRegistry};
|
||||
use parking_lot::Mutex;
|
||||
@@ -272,7 +272,31 @@ impl<T: 'static> PromptEditor<T> {
|
||||
}
|
||||
|
||||
fn paste(&mut self, _: &Paste, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
crate::active_thread::attach_pasted_images_as_context(&self.context_store, cx);
|
||||
let images = cx
|
||||
.read_from_clipboard()
|
||||
.map(|item| {
|
||||
item.into_entries()
|
||||
.filter_map(|entry| {
|
||||
if let ClipboardEntry::Image(image) = entry {
|
||||
Some(image)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
if images.is_empty() {
|
||||
return;
|
||||
}
|
||||
cx.stop_propagation();
|
||||
|
||||
self.context_store.update(cx, |store, cx| {
|
||||
for image in images {
|
||||
store.add_image_instance(Arc::new(image), cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_prompt_editor_events(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,8 @@
|
||||
use crate::{ManageProfiles, ToggleProfileSelector};
|
||||
use agent::agent_profile::{AgentProfile, AvailableProfiles};
|
||||
use agent_settings::{AgentDockPosition, AgentProfileId, AgentSettings, builtin_profiles};
|
||||
use agent_settings::{
|
||||
AgentDockPosition, AgentProfile, AgentProfileId, AgentSettings, AvailableProfiles,
|
||||
builtin_profiles,
|
||||
};
|
||||
use fs::Fs;
|
||||
use gpui::{Action, Entity, FocusHandle, Subscription, prelude::*};
|
||||
use settings::{Settings as _, SettingsStore, update_settings_file};
|
||||
|
||||
@@ -1,912 +0,0 @@
|
||||
use crate::{AgentPanel, RemoveSelectedThread};
|
||||
use agent::history_store::{HistoryEntry, HistoryStore};
|
||||
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
|
||||
use editor::{Editor, EditorEvent};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
App, ClickEvent, Empty, Entity, FocusHandle, Focusable, ScrollStrategy, Stateful, Task,
|
||||
UniformListScrollHandle, WeakEntity, Window, uniform_list,
|
||||
};
|
||||
use std::{fmt::Display, ops::Range, sync::Arc};
|
||||
use time::{OffsetDateTime, UtcOffset};
|
||||
use ui::{
|
||||
HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Scrollbar, ScrollbarState,
|
||||
Tooltip, prelude::*,
|
||||
};
|
||||
use util::ResultExt;
|
||||
|
||||
pub struct ThreadHistory {
|
||||
agent_panel: WeakEntity<AgentPanel>,
|
||||
history_store: Entity<HistoryStore>,
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
selected_index: usize,
|
||||
hovered_index: Option<usize>,
|
||||
search_editor: Entity<Editor>,
|
||||
all_entries: Arc<Vec<HistoryEntry>>,
|
||||
// When the search is empty, we display date separators between history entries
|
||||
// This vector contains an enum of either a separator or an actual entry
|
||||
separated_items: Vec<ListItemType>,
|
||||
// Maps entry indexes to list item indexes
|
||||
separated_item_indexes: Vec<u32>,
|
||||
_separated_items_task: Option<Task<()>>,
|
||||
search_state: SearchState,
|
||||
scrollbar_visibility: bool,
|
||||
scrollbar_state: ScrollbarState,
|
||||
_subscriptions: Vec<gpui::Subscription>,
|
||||
}
|
||||
|
||||
enum SearchState {
|
||||
Empty,
|
||||
Searching {
|
||||
query: SharedString,
|
||||
_task: Task<()>,
|
||||
},
|
||||
Searched {
|
||||
query: SharedString,
|
||||
matches: Vec<StringMatch>,
|
||||
},
|
||||
}
|
||||
|
||||
enum ListItemType {
|
||||
BucketSeparator(TimeBucket),
|
||||
Entry {
|
||||
index: usize,
|
||||
format: EntryTimeFormat,
|
||||
},
|
||||
}
|
||||
|
||||
impl ListItemType {
|
||||
fn entry_index(&self) -> Option<usize> {
|
||||
match self {
|
||||
ListItemType::BucketSeparator(_) => None,
|
||||
ListItemType::Entry { index, .. } => Some(*index),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ThreadHistory {
|
||||
pub(crate) fn new(
|
||||
agent_panel: WeakEntity<AgentPanel>,
|
||||
history_store: Entity<HistoryStore>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let search_editor = cx.new(|cx| {
|
||||
let mut editor = Editor::single_line(window, cx);
|
||||
editor.set_placeholder_text("Search threads...", window, cx);
|
||||
editor
|
||||
});
|
||||
|
||||
let search_editor_subscription =
|
||||
cx.subscribe(&search_editor, |this, search_editor, event, cx| {
|
||||
if let EditorEvent::BufferEdited = event {
|
||||
let query = search_editor.read(cx).text(cx);
|
||||
this.search(query.into(), cx);
|
||||
}
|
||||
});
|
||||
|
||||
let history_store_subscription = cx.observe(&history_store, |this, _, cx| {
|
||||
this.update_all_entries(cx);
|
||||
});
|
||||
|
||||
let scroll_handle = UniformListScrollHandle::default();
|
||||
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
|
||||
|
||||
let mut this = Self {
|
||||
agent_panel,
|
||||
history_store,
|
||||
scroll_handle,
|
||||
selected_index: 0,
|
||||
hovered_index: None,
|
||||
search_state: SearchState::Empty,
|
||||
all_entries: Default::default(),
|
||||
separated_items: Default::default(),
|
||||
separated_item_indexes: Default::default(),
|
||||
search_editor,
|
||||
scrollbar_visibility: true,
|
||||
scrollbar_state,
|
||||
_subscriptions: vec![search_editor_subscription, history_store_subscription],
|
||||
_separated_items_task: None,
|
||||
};
|
||||
this.update_all_entries(cx);
|
||||
this
|
||||
}
|
||||
|
||||
fn update_all_entries(&mut self, cx: &mut Context<Self>) {
|
||||
let new_entries: Arc<Vec<HistoryEntry>> = self
|
||||
.history_store
|
||||
.update(cx, |store, cx| store.entries(cx))
|
||||
.into();
|
||||
|
||||
self._separated_items_task.take();
|
||||
|
||||
let mut items = Vec::with_capacity(new_entries.len() + 1);
|
||||
let mut indexes = Vec::with_capacity(new_entries.len() + 1);
|
||||
|
||||
let bg_task = cx.background_spawn(async move {
|
||||
let mut bucket = None;
|
||||
let today = Local::now().naive_local().date();
|
||||
|
||||
for (index, entry) in new_entries.iter().enumerate() {
|
||||
let entry_date = entry
|
||||
.updated_at()
|
||||
.with_timezone(&Local)
|
||||
.naive_local()
|
||||
.date();
|
||||
let entry_bucket = TimeBucket::from_dates(today, entry_date);
|
||||
|
||||
if Some(entry_bucket) != bucket {
|
||||
bucket = Some(entry_bucket);
|
||||
items.push(ListItemType::BucketSeparator(entry_bucket));
|
||||
}
|
||||
|
||||
indexes.push(items.len() as u32);
|
||||
items.push(ListItemType::Entry {
|
||||
index,
|
||||
format: entry_bucket.into(),
|
||||
});
|
||||
}
|
||||
(new_entries, items, indexes)
|
||||
});
|
||||
|
||||
let task = cx.spawn(async move |this, cx| {
|
||||
let (new_entries, items, indexes) = bg_task.await;
|
||||
this.update(cx, |this, cx| {
|
||||
let previously_selected_entry =
|
||||
this.all_entries.get(this.selected_index).map(|e| e.id());
|
||||
|
||||
this.all_entries = new_entries;
|
||||
this.separated_items = items;
|
||||
this.separated_item_indexes = indexes;
|
||||
|
||||
match &this.search_state {
|
||||
SearchState::Empty => {
|
||||
if this.selected_index >= this.all_entries.len() {
|
||||
this.set_selected_entry_index(
|
||||
this.all_entries.len().saturating_sub(1),
|
||||
cx,
|
||||
);
|
||||
} else if let Some(prev_id) = previously_selected_entry
|
||||
&& let Some(new_ix) = this
|
||||
.all_entries
|
||||
.iter()
|
||||
.position(|probe| probe.id() == prev_id)
|
||||
{
|
||||
this.set_selected_entry_index(new_ix, cx);
|
||||
}
|
||||
}
|
||||
SearchState::Searching { query, .. } | SearchState::Searched { query, .. } => {
|
||||
this.search(query.clone(), cx);
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
})
|
||||
.log_err();
|
||||
});
|
||||
self._separated_items_task = Some(task);
|
||||
}
|
||||
|
||||
fn search(&mut self, query: SharedString, cx: &mut Context<Self>) {
|
||||
if query.is_empty() {
|
||||
self.search_state = SearchState::Empty;
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
|
||||
let all_entries = self.all_entries.clone();
|
||||
|
||||
let fuzzy_search_task = cx.background_spawn({
|
||||
let query = query.clone();
|
||||
let executor = cx.background_executor().clone();
|
||||
async move {
|
||||
let mut candidates = Vec::with_capacity(all_entries.len());
|
||||
|
||||
for (idx, entry) in all_entries.iter().enumerate() {
|
||||
match entry {
|
||||
HistoryEntry::Thread(thread) => {
|
||||
candidates.push(StringMatchCandidate::new(idx, &thread.summary));
|
||||
}
|
||||
HistoryEntry::Context(context) => {
|
||||
candidates.push(StringMatchCandidate::new(idx, &context.title));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_MATCHES: usize = 100;
|
||||
|
||||
fuzzy::match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
false,
|
||||
true,
|
||||
MAX_MATCHES,
|
||||
&Default::default(),
|
||||
executor,
|
||||
)
|
||||
.await
|
||||
}
|
||||
});
|
||||
|
||||
let task = cx.spawn({
|
||||
let query = query.clone();
|
||||
async move |this, cx| {
|
||||
let matches = fuzzy_search_task.await;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
let SearchState::Searching {
|
||||
query: current_query,
|
||||
_task,
|
||||
} = &this.search_state
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if &query == current_query {
|
||||
this.search_state = SearchState::Searched {
|
||||
query: query.clone(),
|
||||
matches,
|
||||
};
|
||||
|
||||
this.set_selected_entry_index(0, cx);
|
||||
cx.notify();
|
||||
};
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
});
|
||||
|
||||
self.search_state = SearchState::Searching { query, _task: task };
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn matched_count(&self) -> usize {
|
||||
match &self.search_state {
|
||||
SearchState::Empty => self.all_entries.len(),
|
||||
SearchState::Searching { .. } => 0,
|
||||
SearchState::Searched { matches, .. } => matches.len(),
|
||||
}
|
||||
}
|
||||
|
||||
fn list_item_count(&self) -> usize {
|
||||
match &self.search_state {
|
||||
SearchState::Empty => self.separated_items.len(),
|
||||
SearchState::Searching { .. } => 0,
|
||||
SearchState::Searched { matches, .. } => matches.len(),
|
||||
}
|
||||
}
|
||||
|
||||
fn search_produced_no_matches(&self) -> bool {
|
||||
match &self.search_state {
|
||||
SearchState::Empty => false,
|
||||
SearchState::Searching { .. } => false,
|
||||
SearchState::Searched { matches, .. } => matches.is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_match(&self, ix: usize) -> Option<&HistoryEntry> {
|
||||
match &self.search_state {
|
||||
SearchState::Empty => self.all_entries.get(ix),
|
||||
SearchState::Searching { .. } => None,
|
||||
SearchState::Searched { matches, .. } => matches
|
||||
.get(ix)
|
||||
.and_then(|m| self.all_entries.get(m.candidate_id)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_previous(
|
||||
&mut self,
|
||||
_: &menu::SelectPrevious,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let count = self.matched_count();
|
||||
if count > 0 {
|
||||
if self.selected_index == 0 {
|
||||
self.set_selected_entry_index(count - 1, cx);
|
||||
} else {
|
||||
self.set_selected_entry_index(self.selected_index - 1, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_next(
|
||||
&mut self,
|
||||
_: &menu::SelectNext,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let count = self.matched_count();
|
||||
if count > 0 {
|
||||
if self.selected_index == count - 1 {
|
||||
self.set_selected_entry_index(0, cx);
|
||||
} else {
|
||||
self.set_selected_entry_index(self.selected_index + 1, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn select_first(
|
||||
&mut self,
|
||||
_: &menu::SelectFirst,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let count = self.matched_count();
|
||||
if count > 0 {
|
||||
self.set_selected_entry_index(0, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
let count = self.matched_count();
|
||||
if count > 0 {
|
||||
self.set_selected_entry_index(count - 1, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_selected_entry_index(&mut self, entry_index: usize, cx: &mut Context<Self>) {
|
||||
self.selected_index = entry_index;
|
||||
|
||||
let scroll_ix = match self.search_state {
|
||||
SearchState::Empty | SearchState::Searching { .. } => self
|
||||
.separated_item_indexes
|
||||
.get(entry_index)
|
||||
.map(|ix| *ix as usize)
|
||||
.unwrap_or(entry_index + 1),
|
||||
SearchState::Searched { .. } => entry_index,
|
||||
};
|
||||
|
||||
self.scroll_handle
|
||||
.scroll_to_item(scroll_ix, ScrollStrategy::Top);
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn render_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
|
||||
if !(self.scrollbar_visibility || self.scrollbar_state.is_dragging()) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(
|
||||
div()
|
||||
.occlude()
|
||||
.id("thread-history-scroll")
|
||||
.h_full()
|
||||
.bg(cx.theme().colors().panel_background.opacity(0.8))
|
||||
.border_l_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.absolute()
|
||||
.right_1()
|
||||
.top_0()
|
||||
.bottom_0()
|
||||
.w_4()
|
||||
.pl_1()
|
||||
.cursor_default()
|
||||
.on_mouse_move(cx.listener(|_, _, _window, cx| {
|
||||
cx.notify();
|
||||
cx.stop_propagation()
|
||||
}))
|
||||
.on_hover(|_, _window, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_any_mouse_down(|_, _window, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_scroll_wheel(cx.listener(|_, _, _window, cx| {
|
||||
cx.notify();
|
||||
}))
|
||||
.children(Scrollbar::vertical(self.scrollbar_state.clone())),
|
||||
)
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(entry) = self.get_match(self.selected_index) {
|
||||
let task_result = match entry {
|
||||
HistoryEntry::Thread(thread) => self.agent_panel.update(cx, move |this, cx| {
|
||||
this.open_thread_by_id(&thread.id, window, cx)
|
||||
}),
|
||||
HistoryEntry::Context(context) => self.agent_panel.update(cx, move |this, cx| {
|
||||
this.open_saved_prompt_editor(context.path.clone(), window, cx)
|
||||
}),
|
||||
};
|
||||
|
||||
if let Some(task) = task_result.log_err() {
|
||||
task.detach_and_log_err(cx);
|
||||
};
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_selected_thread(
|
||||
&mut self,
|
||||
_: &RemoveSelectedThread,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(entry) = self.get_match(self.selected_index) {
|
||||
let task_result = match entry {
|
||||
HistoryEntry::Thread(thread) => self
|
||||
.agent_panel
|
||||
.update(cx, |this, cx| this.delete_thread(&thread.id, cx)),
|
||||
HistoryEntry::Context(context) => self
|
||||
.agent_panel
|
||||
.update(cx, |this, cx| this.delete_context(context.path.clone(), cx)),
|
||||
};
|
||||
|
||||
if let Some(task) = task_result.log_err() {
|
||||
task.detach_and_log_err(cx);
|
||||
};
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn list_items(
|
||||
&mut self,
|
||||
range: Range<usize>,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Vec<AnyElement> {
|
||||
let range_start = range.start;
|
||||
|
||||
match &self.search_state {
|
||||
SearchState::Empty => self
|
||||
.separated_items
|
||||
.get(range)
|
||||
.iter()
|
||||
.flat_map(|items| {
|
||||
items
|
||||
.iter()
|
||||
.map(|item| self.render_list_item(item.entry_index(), item, vec![], cx))
|
||||
})
|
||||
.collect(),
|
||||
SearchState::Searched { matches, .. } => matches[range]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, m)| {
|
||||
self.render_list_item(
|
||||
Some(range_start + ix),
|
||||
&ListItemType::Entry {
|
||||
index: m.candidate_id,
|
||||
format: EntryTimeFormat::DateAndTime,
|
||||
},
|
||||
m.positions.clone(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
SearchState::Searching { .. } => {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_list_item(
|
||||
&self,
|
||||
list_entry_ix: Option<usize>,
|
||||
item: &ListItemType,
|
||||
highlight_positions: Vec<usize>,
|
||||
cx: &Context<Self>,
|
||||
) -> AnyElement {
|
||||
match item {
|
||||
ListItemType::Entry { index, format } => match self.all_entries.get(*index) {
|
||||
Some(entry) => h_flex()
|
||||
.w_full()
|
||||
.pb_1()
|
||||
.child(
|
||||
HistoryEntryElement::new(entry.clone(), self.agent_panel.clone())
|
||||
.highlight_positions(highlight_positions)
|
||||
.timestamp_format(*format)
|
||||
.selected(list_entry_ix == Some(self.selected_index))
|
||||
.hovered(list_entry_ix == self.hovered_index)
|
||||
.on_hover(cx.listener(move |this, is_hovered, _window, cx| {
|
||||
if *is_hovered {
|
||||
this.hovered_index = list_entry_ix;
|
||||
} else if this.hovered_index == list_entry_ix {
|
||||
this.hovered_index = None;
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}))
|
||||
.into_any_element(),
|
||||
)
|
||||
.into_any(),
|
||||
None => Empty.into_any_element(),
|
||||
},
|
||||
ListItemType::BucketSeparator(bucket) => div()
|
||||
.px(DynamicSpacing::Base06.rems(cx))
|
||||
.pt_2()
|
||||
.pb_1()
|
||||
.child(
|
||||
Label::new(bucket.to_string())
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.into_any_element(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for ThreadHistory {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
self.search_editor.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ThreadHistory {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.key_context("ThreadHistory")
|
||||
.size_full()
|
||||
.bg(cx.theme().colors().panel_background)
|
||||
.on_action(cx.listener(Self::select_previous))
|
||||
.on_action(cx.listener(Self::select_next))
|
||||
.on_action(cx.listener(Self::select_first))
|
||||
.on_action(cx.listener(Self::select_last))
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.on_action(cx.listener(Self::remove_selected_thread))
|
||||
.when(!self.all_entries.is_empty(), |parent| {
|
||||
parent.child(
|
||||
h_flex()
|
||||
.h(px(41.)) // Match the toolbar perfectly
|
||||
.w_full()
|
||||
.py_1()
|
||||
.px_2()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
Icon::new(IconName::MagnifyingGlass)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
.child(self.search_editor.clone()),
|
||||
)
|
||||
})
|
||||
.child({
|
||||
let view = v_flex()
|
||||
.id("list-container")
|
||||
.relative()
|
||||
.overflow_hidden()
|
||||
.flex_grow();
|
||||
|
||||
if self.all_entries.is_empty() {
|
||||
view.justify_center()
|
||||
.child(
|
||||
h_flex().w_full().justify_center().child(
|
||||
Label::new("You don't have any past threads yet.")
|
||||
.size(LabelSize::Small),
|
||||
),
|
||||
)
|
||||
} else if self.search_produced_no_matches() {
|
||||
view.justify_center().child(
|
||||
h_flex().w_full().justify_center().child(
|
||||
Label::new("No threads match your search.").size(LabelSize::Small),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
view.pr_5()
|
||||
.child(
|
||||
uniform_list(
|
||||
"thread-history",
|
||||
self.list_item_count(),
|
||||
cx.processor(|this, range: Range<usize>, window, cx| {
|
||||
this.list_items(range, window, cx)
|
||||
}),
|
||||
)
|
||||
.p_1()
|
||||
.track_scroll(self.scroll_handle.clone())
|
||||
.flex_grow(),
|
||||
)
|
||||
.when_some(self.render_scrollbar(cx), |div, scrollbar| {
|
||||
div.child(scrollbar)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct HistoryEntryElement {
|
||||
entry: HistoryEntry,
|
||||
agent_panel: WeakEntity<AgentPanel>,
|
||||
selected: bool,
|
||||
hovered: bool,
|
||||
highlight_positions: Vec<usize>,
|
||||
timestamp_format: EntryTimeFormat,
|
||||
on_hover: Box<dyn Fn(&bool, &mut Window, &mut App) + 'static>,
|
||||
}
|
||||
|
||||
impl HistoryEntryElement {
|
||||
pub fn new(entry: HistoryEntry, agent_panel: WeakEntity<AgentPanel>) -> Self {
|
||||
Self {
|
||||
entry,
|
||||
agent_panel,
|
||||
selected: false,
|
||||
hovered: false,
|
||||
highlight_positions: vec![],
|
||||
timestamp_format: EntryTimeFormat::DateAndTime,
|
||||
on_hover: Box::new(|_, _, _| {}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn hovered(mut self, hovered: bool) -> Self {
|
||||
self.hovered = hovered;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn highlight_positions(mut self, positions: Vec<usize>) -> Self {
|
||||
self.highlight_positions = positions;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_hover(mut self, on_hover: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self {
|
||||
self.on_hover = Box::new(on_hover);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn timestamp_format(mut self, format: EntryTimeFormat) -> Self {
|
||||
self.timestamp_format = format;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for HistoryEntryElement {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let (id, summary, timestamp) = match &self.entry {
|
||||
HistoryEntry::Thread(thread) => (
|
||||
thread.id.to_string(),
|
||||
thread.summary.clone(),
|
||||
thread.updated_at.timestamp(),
|
||||
),
|
||||
HistoryEntry::Context(context) => (
|
||||
context.path.to_string_lossy().to_string(),
|
||||
context.title.clone(),
|
||||
context.mtime.timestamp(),
|
||||
),
|
||||
};
|
||||
|
||||
let thread_timestamp =
|
||||
self.timestamp_format
|
||||
.format_timestamp(&self.agent_panel, timestamp, cx);
|
||||
|
||||
ListItem::new(SharedString::from(id))
|
||||
.rounded()
|
||||
.toggle_state(self.selected)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.start_slot(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(
|
||||
HighlightedLabel::new(summary, self.highlight_positions)
|
||||
.size(LabelSize::Small)
|
||||
.truncate(),
|
||||
)
|
||||
.child(
|
||||
Label::new(thread_timestamp)
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::XSmall),
|
||||
),
|
||||
)
|
||||
.on_hover(self.on_hover)
|
||||
.end_slot::<IconButton>(if self.hovered || self.selected {
|
||||
Some(
|
||||
IconButton::new("delete", IconName::Trash)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx)
|
||||
})
|
||||
.on_click({
|
||||
let agent_panel = self.agent_panel.clone();
|
||||
|
||||
let f: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static> =
|
||||
match &self.entry {
|
||||
HistoryEntry::Thread(thread) => {
|
||||
let id = thread.id.clone();
|
||||
|
||||
Box::new(move |_event, _window, cx| {
|
||||
agent_panel
|
||||
.update(cx, |this, cx| {
|
||||
this.delete_thread(&id, cx)
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
HistoryEntry::Context(context) => {
|
||||
let path = context.path.clone();
|
||||
|
||||
Box::new(move |_event, _window, cx| {
|
||||
agent_panel
|
||||
.update(cx, |this, cx| {
|
||||
this.delete_context(path.clone(), cx)
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
};
|
||||
f
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.on_click({
|
||||
let agent_panel = self.agent_panel.clone();
|
||||
|
||||
let f: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static> = match &self.entry
|
||||
{
|
||||
HistoryEntry::Thread(thread) => {
|
||||
let id = thread.id.clone();
|
||||
Box::new(move |_event, window, cx| {
|
||||
agent_panel
|
||||
.update(cx, |this, cx| {
|
||||
this.open_thread_by_id(&id, window, cx)
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
HistoryEntry::Context(context) => {
|
||||
let path = context.path.clone();
|
||||
Box::new(move |_event, window, cx| {
|
||||
agent_panel
|
||||
.update(cx, |this, cx| {
|
||||
this.open_saved_prompt_editor(path.clone(), window, cx)
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
};
|
||||
f
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum EntryTimeFormat {
|
||||
DateAndTime,
|
||||
TimeOnly,
|
||||
}
|
||||
|
||||
impl EntryTimeFormat {
|
||||
fn format_timestamp(
|
||||
&self,
|
||||
agent_panel: &WeakEntity<AgentPanel>,
|
||||
timestamp: i64,
|
||||
cx: &App,
|
||||
) -> String {
|
||||
let timestamp = OffsetDateTime::from_unix_timestamp(timestamp).unwrap();
|
||||
let timezone = agent_panel
|
||||
.read_with(cx, |this, _cx| this.local_timezone())
|
||||
.unwrap_or(UtcOffset::UTC);
|
||||
|
||||
match &self {
|
||||
EntryTimeFormat::DateAndTime => time_format::format_localized_timestamp(
|
||||
timestamp,
|
||||
OffsetDateTime::now_utc(),
|
||||
timezone,
|
||||
time_format::TimestampFormat::EnhancedAbsolute,
|
||||
),
|
||||
EntryTimeFormat::TimeOnly => time_format::format_time(timestamp),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TimeBucket> for EntryTimeFormat {
|
||||
fn from(bucket: TimeBucket) -> Self {
|
||||
match bucket {
|
||||
TimeBucket::Today => EntryTimeFormat::TimeOnly,
|
||||
TimeBucket::Yesterday => EntryTimeFormat::TimeOnly,
|
||||
TimeBucket::ThisWeek => EntryTimeFormat::DateAndTime,
|
||||
TimeBucket::PastWeek => EntryTimeFormat::DateAndTime,
|
||||
TimeBucket::All => EntryTimeFormat::DateAndTime,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
|
||||
enum TimeBucket {
|
||||
Today,
|
||||
Yesterday,
|
||||
ThisWeek,
|
||||
PastWeek,
|
||||
All,
|
||||
}
|
||||
|
||||
impl TimeBucket {
|
||||
fn from_dates(reference: NaiveDate, date: NaiveDate) -> Self {
|
||||
if date == reference {
|
||||
return TimeBucket::Today;
|
||||
}
|
||||
|
||||
if date == reference - TimeDelta::days(1) {
|
||||
return TimeBucket::Yesterday;
|
||||
}
|
||||
|
||||
let week = date.iso_week();
|
||||
|
||||
if reference.iso_week() == week {
|
||||
return TimeBucket::ThisWeek;
|
||||
}
|
||||
|
||||
let last_week = (reference - TimeDelta::days(7)).iso_week();
|
||||
|
||||
if week == last_week {
|
||||
return TimeBucket::PastWeek;
|
||||
}
|
||||
|
||||
TimeBucket::All
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for TimeBucket {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
TimeBucket::Today => write!(f, "Today"),
|
||||
TimeBucket::Yesterday => write!(f, "Yesterday"),
|
||||
TimeBucket::ThisWeek => write!(f, "This Week"),
|
||||
TimeBucket::PastWeek => write!(f, "Past Week"),
|
||||
TimeBucket::All => write!(f, "All"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use chrono::NaiveDate;
|
||||
|
||||
#[test]
|
||||
fn test_time_bucket_from_dates() {
|
||||
let today = NaiveDate::from_ymd_opt(2023, 1, 15).unwrap();
|
||||
|
||||
let date = today;
|
||||
assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::Today);
|
||||
|
||||
let date = NaiveDate::from_ymd_opt(2023, 1, 14).unwrap();
|
||||
assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::Yesterday);
|
||||
|
||||
let date = NaiveDate::from_ymd_opt(2023, 1, 13).unwrap();
|
||||
assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::ThisWeek);
|
||||
|
||||
let date = NaiveDate::from_ymd_opt(2023, 1, 11).unwrap();
|
||||
assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::ThisWeek);
|
||||
|
||||
let date = NaiveDate::from_ymd_opt(2023, 1, 8).unwrap();
|
||||
assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::PastWeek);
|
||||
|
||||
let date = NaiveDate::from_ymd_opt(2023, 1, 5).unwrap();
|
||||
assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::PastWeek);
|
||||
|
||||
// All: not in this week or last week
|
||||
let date = NaiveDate::from_ymd_opt(2023, 1, 1).unwrap();
|
||||
assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::All);
|
||||
|
||||
// Test year boundary cases
|
||||
let new_year = NaiveDate::from_ymd_opt(2023, 1, 1).unwrap();
|
||||
|
||||
let date = NaiveDate::from_ymd_opt(2022, 12, 31).unwrap();
|
||||
assert_eq!(
|
||||
TimeBucket::from_dates(new_year, date),
|
||||
TimeBucket::Yesterday
|
||||
);
|
||||
|
||||
let date = NaiveDate::from_ymd_opt(2022, 12, 28).unwrap();
|
||||
assert_eq!(TimeBucket::from_dates(new_year, date), TimeBucket::ThisWeek);
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
use agent::{Thread, ThreadEvent};
|
||||
use assistant_tool::{Tool, ToolSource};
|
||||
use collections::HashMap;
|
||||
use gpui::{App, Context, Entity, IntoElement, Render, Subscription, Window};
|
||||
use language_model::{LanguageModel, LanguageModelToolSchemaFormat};
|
||||
use std::sync::Arc;
|
||||
use ui::prelude::*;
|
||||
|
||||
pub struct IncompatibleToolsState {
|
||||
cache: HashMap<LanguageModelToolSchemaFormat, Vec<Arc<dyn Tool>>>,
|
||||
thread: Entity<Thread>,
|
||||
_thread_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl IncompatibleToolsState {
|
||||
pub fn new(thread: Entity<Thread>, cx: &mut Context<Self>) -> Self {
|
||||
let _tool_working_set_subscription = cx.subscribe(&thread, |this, _, event, _| {
|
||||
if let ThreadEvent::ProfileChanged = event {
|
||||
this.cache.clear();
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
cache: HashMap::default(),
|
||||
thread,
|
||||
_thread_subscription: _tool_working_set_subscription,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn incompatible_tools(
|
||||
&mut self,
|
||||
model: &Arc<dyn LanguageModel>,
|
||||
cx: &App,
|
||||
) -> &[Arc<dyn Tool>] {
|
||||
self.cache
|
||||
.entry(model.tool_input_format())
|
||||
.or_insert_with(|| {
|
||||
self.thread
|
||||
.read(cx)
|
||||
.profile()
|
||||
.enabled_tools(cx)
|
||||
.iter()
|
||||
.filter(|(_, tool)| tool.input_schema(model.tool_input_format()).is_err())
|
||||
.map(|(_, tool)| tool.clone())
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct IncompatibleToolsTooltip {
|
||||
pub incompatible_tools: Vec<Arc<dyn Tool>>,
|
||||
}
|
||||
|
||||
impl Render for IncompatibleToolsTooltip {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
ui::tooltip_container(window, cx, |container, _, cx| {
|
||||
container
|
||||
.w_72()
|
||||
.child(Label::new("Incompatible Tools").size(LabelSize::Small))
|
||||
.child(
|
||||
Label::new(
|
||||
"This model is incompatible with the following tools from your MCPs:",
|
||||
)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.my_1p5()
|
||||
.py_0p5()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.children(
|
||||
self.incompatible_tools
|
||||
.iter()
|
||||
.map(|tool| h_flex().gap_4().child(Label::new(tool.name()).size(LabelSize::Small)).map(|parent|
|
||||
match tool.source() {
|
||||
ToolSource::Native => parent,
|
||||
ToolSource::ContextServer { id } => parent.child(Label::new(id).size(LabelSize::Small).color(Color::Muted)),
|
||||
}
|
||||
)),
|
||||
),
|
||||
)
|
||||
.child(Label::new("What To Do Instead").size(LabelSize::Small))
|
||||
.child(
|
||||
Label::new(
|
||||
"Every other tool continues to work with this model, but to specifically use those, switch to another model.",
|
||||
)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,8 @@ mod claude_code_onboarding_modal;
|
||||
mod context_pill;
|
||||
mod end_trial_upsell;
|
||||
mod onboarding_modal;
|
||||
pub mod preview;
|
||||
mod unavailable_editing_tooltip;
|
||||
mod usage_callout;
|
||||
|
||||
pub use acp_onboarding_modal::*;
|
||||
pub use agent_notification::*;
|
||||
@@ -16,3 +16,4 @@ pub use context_pill::*;
|
||||
pub use end_trial_upsell::*;
|
||||
pub use onboarding_modal::*;
|
||||
pub use unavailable_editing_tooltip::*;
|
||||
pub use usage_callout::*;
|
||||
|
||||
@@ -13,11 +13,9 @@ use rope::Point;
|
||||
use ui::{IconButtonShape, Tooltip, prelude::*, tooltip_container};
|
||||
|
||||
use agent::context::{
|
||||
AgentContext, AgentContextHandle, ContextId, ContextKind, DirectoryContext,
|
||||
DirectoryContextHandle, FetchedUrlContext, FileContext, FileContextHandle, ImageContext,
|
||||
ImageStatus, RulesContext, RulesContextHandle, SelectionContext, SelectionContextHandle,
|
||||
SymbolContext, SymbolContextHandle, TextThreadContext, TextThreadContextHandle, ThreadContext,
|
||||
ThreadContextHandle,
|
||||
AgentContextHandle, ContextId, ContextKind, DirectoryContextHandle, FetchedUrlContext,
|
||||
FileContextHandle, ImageContext, ImageStatus, RulesContextHandle, SelectionContextHandle,
|
||||
SymbolContextHandle, TextThreadContextHandle, ThreadContextHandle,
|
||||
};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
@@ -317,33 +315,11 @@ impl AddedContext {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_attached(
|
||||
context: &AgentContext,
|
||||
model: Option<&Arc<dyn language_model::LanguageModel>>,
|
||||
cx: &App,
|
||||
) -> AddedContext {
|
||||
match context {
|
||||
AgentContext::File(context) => Self::attached_file(context, cx),
|
||||
AgentContext::Directory(context) => Self::attached_directory(context),
|
||||
AgentContext::Symbol(context) => Self::attached_symbol(context, cx),
|
||||
AgentContext::Selection(context) => Self::attached_selection(context, cx),
|
||||
AgentContext::FetchedUrl(context) => Self::fetched_url(context.clone()),
|
||||
AgentContext::Thread(context) => Self::attached_thread(context),
|
||||
AgentContext::TextThread(context) => Self::attached_text_thread(context),
|
||||
AgentContext::Rules(context) => Self::attached_rules(context),
|
||||
AgentContext::Image(context) => Self::image(context.clone(), model, cx),
|
||||
}
|
||||
}
|
||||
|
||||
fn pending_file(handle: FileContextHandle, cx: &App) -> Option<AddedContext> {
|
||||
let full_path = handle.buffer.read(cx).file()?.full_path(cx);
|
||||
Some(Self::file(handle, &full_path, cx))
|
||||
}
|
||||
|
||||
fn attached_file(context: &FileContext, cx: &App) -> AddedContext {
|
||||
Self::file(context.handle.clone(), &context.full_path, cx)
|
||||
}
|
||||
|
||||
fn file(handle: FileContextHandle, full_path: &Path, cx: &App) -> AddedContext {
|
||||
let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into();
|
||||
let (name, parent) =
|
||||
@@ -371,10 +347,6 @@ impl AddedContext {
|
||||
Some(Self::directory(handle, &full_path))
|
||||
}
|
||||
|
||||
fn attached_directory(context: &DirectoryContext) -> AddedContext {
|
||||
Self::directory(context.handle.clone(), &context.full_path)
|
||||
}
|
||||
|
||||
fn directory(handle: DirectoryContextHandle, full_path: &Path) -> AddedContext {
|
||||
let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into();
|
||||
let (name, parent) =
|
||||
@@ -411,25 +383,6 @@ impl AddedContext {
|
||||
})
|
||||
}
|
||||
|
||||
fn attached_symbol(context: &SymbolContext, cx: &App) -> AddedContext {
|
||||
let excerpt = ContextFileExcerpt::new(&context.full_path, context.line_range.clone(), cx);
|
||||
AddedContext {
|
||||
kind: ContextKind::Symbol,
|
||||
name: context.handle.symbol.clone(),
|
||||
parent: Some(excerpt.file_name_and_range.clone()),
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
status: ContextStatus::Ready,
|
||||
render_hover: {
|
||||
let text = context.text.clone();
|
||||
Some(Rc::new(move |_, cx| {
|
||||
excerpt.hover_view(text.clone(), cx).into()
|
||||
}))
|
||||
},
|
||||
handle: AgentContextHandle::Symbol(context.handle.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn pending_selection(handle: SelectionContextHandle, cx: &App) -> Option<AddedContext> {
|
||||
let excerpt = ContextFileExcerpt::new(&handle.full_path(cx)?, handle.line_range(cx), cx);
|
||||
Some(AddedContext {
|
||||
@@ -449,25 +402,6 @@ impl AddedContext {
|
||||
})
|
||||
}
|
||||
|
||||
fn attached_selection(context: &SelectionContext, cx: &App) -> AddedContext {
|
||||
let excerpt = ContextFileExcerpt::new(&context.full_path, context.line_range.clone(), cx);
|
||||
AddedContext {
|
||||
kind: ContextKind::Selection,
|
||||
name: excerpt.file_name_and_range.clone(),
|
||||
parent: excerpt.parent_name.clone(),
|
||||
tooltip: None,
|
||||
icon_path: excerpt.icon_path.clone(),
|
||||
status: ContextStatus::Ready,
|
||||
render_hover: {
|
||||
let text = context.text.clone();
|
||||
Some(Rc::new(move |_, cx| {
|
||||
excerpt.hover_view(text.clone(), cx).into()
|
||||
}))
|
||||
},
|
||||
handle: AgentContextHandle::Selection(context.handle.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn fetched_url(context: FetchedUrlContext) -> AddedContext {
|
||||
AddedContext {
|
||||
kind: ContextKind::FetchedUrl,
|
||||
@@ -506,24 +440,6 @@ impl AddedContext {
|
||||
}
|
||||
}
|
||||
|
||||
fn attached_thread(context: &ThreadContext) -> AddedContext {
|
||||
AddedContext {
|
||||
kind: ContextKind::Thread,
|
||||
name: context.title.clone(),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
status: ContextStatus::Ready,
|
||||
render_hover: {
|
||||
let text = context.text.clone();
|
||||
Some(Rc::new(move |_, cx| {
|
||||
ContextPillHover::new_text(text.clone(), cx).into()
|
||||
}))
|
||||
},
|
||||
handle: AgentContextHandle::Thread(context.handle.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn pending_text_thread(handle: TextThreadContextHandle, cx: &App) -> AddedContext {
|
||||
AddedContext {
|
||||
kind: ContextKind::TextThread,
|
||||
@@ -543,24 +459,6 @@ impl AddedContext {
|
||||
}
|
||||
}
|
||||
|
||||
fn attached_text_thread(context: &TextThreadContext) -> AddedContext {
|
||||
AddedContext {
|
||||
kind: ContextKind::TextThread,
|
||||
name: context.title.clone(),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
status: ContextStatus::Ready,
|
||||
render_hover: {
|
||||
let text = context.text.clone();
|
||||
Some(Rc::new(move |_, cx| {
|
||||
ContextPillHover::new_text(text.clone(), cx).into()
|
||||
}))
|
||||
},
|
||||
handle: AgentContextHandle::TextThread(context.handle.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn pending_rules(
|
||||
handle: RulesContextHandle,
|
||||
prompt_store: Option<&Entity<PromptStore>>,
|
||||
@@ -584,28 +482,6 @@ impl AddedContext {
|
||||
})
|
||||
}
|
||||
|
||||
fn attached_rules(context: &RulesContext) -> AddedContext {
|
||||
let title = context
|
||||
.title
|
||||
.clone()
|
||||
.unwrap_or_else(|| "Unnamed Rule".into());
|
||||
AddedContext {
|
||||
kind: ContextKind::Rules,
|
||||
name: title,
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
status: ContextStatus::Ready,
|
||||
render_hover: {
|
||||
let text = context.text.clone();
|
||||
Some(Rc::new(move |_, cx| {
|
||||
ContextPillHover::new_text(text.clone(), cx).into()
|
||||
}))
|
||||
},
|
||||
handle: AgentContextHandle::Rules(context.handle.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn image(
|
||||
context: ImageContext,
|
||||
model: Option<&Arc<dyn language_model::LanguageModel>>,
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
mod agent_preview;
|
||||
mod usage_callouts;
|
||||
|
||||
pub use agent_preview::*;
|
||||
pub use usage_callouts::*;
|
||||
@@ -1,89 +0,0 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use collections::HashMap;
|
||||
use component::ComponentId;
|
||||
use gpui::{App, Entity, WeakEntity};
|
||||
use ui::{AnyElement, Component, ComponentScope, Window};
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::ActiveThread;
|
||||
|
||||
/// Function type for creating agent component previews
|
||||
pub type PreviewFn =
|
||||
fn(WeakEntity<Workspace>, Entity<ActiveThread>, &mut Window, &mut App) -> Option<AnyElement>;
|
||||
|
||||
pub struct AgentPreviewFn(fn() -> (ComponentId, PreviewFn));
|
||||
|
||||
impl AgentPreviewFn {
|
||||
pub const fn new(f: fn() -> (ComponentId, PreviewFn)) -> Self {
|
||||
Self(f)
|
||||
}
|
||||
}
|
||||
|
||||
inventory::collect!(AgentPreviewFn);
|
||||
|
||||
/// Trait that must be implemented by components that provide agent previews.
|
||||
pub trait AgentPreview: Component + Sized {
|
||||
#[allow(unused)] // We can't know this is used due to the distributed slice
|
||||
fn scope(&self) -> ComponentScope {
|
||||
ComponentScope::Agent
|
||||
}
|
||||
|
||||
/// Static method to create a preview for this component type
|
||||
fn agent_preview(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
active_thread: Entity<ActiveThread>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<AnyElement>;
|
||||
}
|
||||
|
||||
/// Register an agent preview for the given component type
|
||||
#[macro_export]
|
||||
macro_rules! register_agent_preview {
|
||||
($type:ty) => {
|
||||
inventory::submit! {
|
||||
$crate::ui::preview::AgentPreviewFn::new(|| {
|
||||
(
|
||||
<$type as component::Component>::id(),
|
||||
<$type as $crate::ui::preview::AgentPreview>::agent_preview,
|
||||
)
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Lazy initialized registry of preview functions
|
||||
static AGENT_PREVIEW_REGISTRY: OnceLock<HashMap<ComponentId, PreviewFn>> = OnceLock::new();
|
||||
|
||||
/// Initialize the agent preview registry if needed
|
||||
fn get_or_init_registry() -> &'static HashMap<ComponentId, PreviewFn> {
|
||||
AGENT_PREVIEW_REGISTRY.get_or_init(|| {
|
||||
let mut map = HashMap::default();
|
||||
for register_fn in inventory::iter::<AgentPreviewFn>() {
|
||||
let (id, preview_fn) = (register_fn.0)();
|
||||
map.insert(id, preview_fn);
|
||||
}
|
||||
map
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a specific agent preview by component ID.
|
||||
pub fn get_agent_preview(
|
||||
id: &ComponentId,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
active_thread: Entity<ActiveThread>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<AnyElement> {
|
||||
let registry = get_or_init_registry();
|
||||
registry
|
||||
.get(id)
|
||||
.and_then(|preview_fn| preview_fn(workspace, active_thread, window, cx))
|
||||
}
|
||||
|
||||
/// Get all registered agent previews.
|
||||
pub fn all_agent_previews() -> Vec<ComponentId> {
|
||||
let registry = get_or_init_registry();
|
||||
registry.keys().cloned().collect()
|
||||
}
|
||||
@@ -29,28 +29,3 @@ pub struct JjUiFeatureFlag {}
|
||||
impl FeatureFlag for JjUiFeatureFlag {
|
||||
const NAME: &'static str = "jj-ui";
|
||||
}
|
||||
|
||||
pub struct GeminiAndNativeFeatureFlag;
|
||||
|
||||
impl FeatureFlag for GeminiAndNativeFeatureFlag {
|
||||
// This was previously called "acp".
|
||||
//
|
||||
// We renamed it because existing builds used it to enable the Claude Code
|
||||
// integration too, and we'd like to turn Gemini/Native on in new builds
|
||||
// without enabling Claude Code in old builds.
|
||||
const NAME: &'static str = "gemini-and-native";
|
||||
|
||||
fn enabled_for_all() -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ClaudeCodeFeatureFlag;
|
||||
|
||||
impl FeatureFlag for ClaudeCodeFeatureFlag {
|
||||
const NAME: &'static str = "claude-code";
|
||||
|
||||
fn enabled_for_all() -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ agent_settings.workspace = true
|
||||
anyhow.workspace = true
|
||||
askpass.workspace = true
|
||||
assets.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
assistant_tools.workspace = true
|
||||
audio.workspace = true
|
||||
auto_update.workspace = true
|
||||
@@ -77,7 +76,6 @@ gpui_tokio.workspace = true
|
||||
|
||||
http_client.workspace = true
|
||||
image_viewer.workspace = true
|
||||
indoc.workspace = true
|
||||
edit_prediction_button.workspace = true
|
||||
inspector_ui.workspace = true
|
||||
install_cli.workspace = true
|
||||
|
||||
@@ -3,10 +3,7 @@
|
||||
//! A view for exploring Zed components.
|
||||
|
||||
mod persistence;
|
||||
mod preview_support;
|
||||
|
||||
use agent::{TextThreadStore, ThreadStore};
|
||||
use agent_ui::ActiveThread;
|
||||
use client::UserStore;
|
||||
use collections::HashMap;
|
||||
use component::{ComponentId, ComponentMetadata, ComponentStatus, components};
|
||||
@@ -17,14 +14,10 @@ use gpui::{ListState, ScrollHandle, ScrollStrategy, UniformListScrollHandle};
|
||||
use languages::LanguageRegistry;
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
use persistence::COMPONENT_PREVIEW_DB;
|
||||
use preview_support::active_thread::{
|
||||
load_preview_text_thread_store, load_preview_thread_store, static_active_thread,
|
||||
};
|
||||
use project::Project;
|
||||
use std::{iter::Iterator, ops::Range, sync::Arc};
|
||||
use ui::{ButtonLike, Divider, HighlightedLabel, ListItem, ListSubHeader, Tooltip, prelude::*};
|
||||
use ui_input::SingleLineInput;
|
||||
use util::ResultExt as _;
|
||||
use workspace::{
|
||||
AppState, Item, ItemId, SerializableItem, Workspace, WorkspaceId, delete_unloaded_items,
|
||||
item::ItemEvent,
|
||||
@@ -74,7 +67,6 @@ pub fn init(app_state: Arc<AppState>, cx: &mut App) {
|
||||
|
||||
enum PreviewEntry {
|
||||
AllComponents,
|
||||
ActiveThread,
|
||||
Separator,
|
||||
Component(ComponentMetadata, Option<Vec<usize>>),
|
||||
SectionHeader(SharedString),
|
||||
@@ -97,12 +89,10 @@ enum PreviewPage {
|
||||
#[default]
|
||||
AllComponents,
|
||||
Component(ComponentId),
|
||||
ActiveThread,
|
||||
}
|
||||
|
||||
struct ComponentPreview {
|
||||
active_page: PreviewPage,
|
||||
active_thread: Option<Entity<ActiveThread>>,
|
||||
reset_key: usize,
|
||||
component_list: ListState,
|
||||
entries: Vec<PreviewEntry>,
|
||||
@@ -115,8 +105,6 @@ struct ComponentPreview {
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
nav_scroll_handle: UniformListScrollHandle,
|
||||
project: Entity<Project>,
|
||||
text_thread_store: Option<Entity<TextThreadStore>>,
|
||||
thread_store: Option<Entity<ThreadStore>>,
|
||||
user_store: Entity<UserStore>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
workspace_id: Option<WorkspaceId>,
|
||||
@@ -134,32 +122,6 @@ impl ComponentPreview {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> anyhow::Result<Self> {
|
||||
let workspace_clone = workspace.clone();
|
||||
let project_clone = project.clone();
|
||||
|
||||
cx.spawn_in(window, async move |entity, cx| {
|
||||
let thread_store_future = load_preview_thread_store(project_clone.clone(), cx);
|
||||
let text_thread_store_future =
|
||||
load_preview_text_thread_store(workspace_clone.clone(), project_clone.clone(), cx);
|
||||
|
||||
let (thread_store_result, text_thread_store_result) =
|
||||
futures::join!(thread_store_future, text_thread_store_future);
|
||||
|
||||
if let (Some(thread_store), Some(text_thread_store)) = (
|
||||
thread_store_result.log_err(),
|
||||
text_thread_store_result.log_err(),
|
||||
) {
|
||||
entity
|
||||
.update_in(cx, |this, window, cx| {
|
||||
this.thread_store = Some(thread_store.clone());
|
||||
this.text_thread_store = Some(text_thread_store.clone());
|
||||
this.create_active_thread(window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
let component_registry = Arc::new(components());
|
||||
let sorted_components = component_registry.sorted_components();
|
||||
let selected_index = selected_index.into().unwrap_or(0);
|
||||
@@ -175,7 +137,6 @@ impl ComponentPreview {
|
||||
|
||||
let mut component_preview = Self {
|
||||
active_page,
|
||||
active_thread: None,
|
||||
reset_key: 0,
|
||||
component_list,
|
||||
entries: Vec::new(),
|
||||
@@ -188,8 +149,6 @@ impl ComponentPreview {
|
||||
language_registry,
|
||||
nav_scroll_handle: UniformListScrollHandle::new(),
|
||||
project,
|
||||
text_thread_store: None,
|
||||
thread_store: None,
|
||||
user_store,
|
||||
workspace,
|
||||
workspace_id: None,
|
||||
@@ -208,43 +167,10 @@ impl ComponentPreview {
|
||||
Ok(component_preview)
|
||||
}
|
||||
|
||||
pub fn create_active_thread(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> &mut Self {
|
||||
let workspace = self.workspace.clone();
|
||||
let language_registry = self.language_registry.clone();
|
||||
let weak_handle = self.workspace.clone();
|
||||
if let Some(workspace) = workspace.upgrade() {
|
||||
let project = workspace.read(cx).project().clone();
|
||||
if let Some((thread_store, text_thread_store)) = self
|
||||
.thread_store
|
||||
.clone()
|
||||
.zip(self.text_thread_store.clone())
|
||||
{
|
||||
let active_thread = static_active_thread(
|
||||
weak_handle,
|
||||
project,
|
||||
language_registry,
|
||||
thread_store,
|
||||
text_thread_store,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
self.active_thread = Some(active_thread);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn active_page_id(&self, _cx: &App) -> ActivePageId {
|
||||
match &self.active_page {
|
||||
PreviewPage::AllComponents => ActivePageId::default(),
|
||||
PreviewPage::Component(component_id) => ActivePageId(component_id.0.to_string()),
|
||||
PreviewPage::ActiveThread => ActivePageId("active_thread".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,7 +285,6 @@ impl ComponentPreview {
|
||||
|
||||
// Always show all components first
|
||||
entries.push(PreviewEntry::AllComponents);
|
||||
entries.push(PreviewEntry::ActiveThread);
|
||||
|
||||
let mut scopes: Vec<_> = scope_groups
|
||||
.keys()
|
||||
@@ -492,19 +417,6 @@ impl ComponentPreview {
|
||||
}))
|
||||
.into_any_element()
|
||||
}
|
||||
PreviewEntry::ActiveThread => {
|
||||
let selected = self.active_page == PreviewPage::ActiveThread;
|
||||
|
||||
ListItem::new(ix)
|
||||
.child(Label::new("Active Thread"))
|
||||
.selectable(true)
|
||||
.toggle_state(selected)
|
||||
.inset(true)
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
this.set_active_page(PreviewPage::ActiveThread, cx);
|
||||
}))
|
||||
.into_any_element()
|
||||
}
|
||||
PreviewEntry::Separator => ListItem::new(ix)
|
||||
.disabled(true)
|
||||
.child(div().w_full().py_2().child(Divider::horizontal()))
|
||||
@@ -572,22 +484,7 @@ impl ComponentPreview {
|
||||
),
|
||||
);
|
||||
|
||||
// Check if the component's scope is Agent
|
||||
if scope == ComponentScope::Agent {
|
||||
if let Some(active_thread) = self.active_thread.clone() {
|
||||
if let Some(element) = agent_ui::get_agent_preview(
|
||||
&component.id(),
|
||||
self.workspace.clone(),
|
||||
active_thread,
|
||||
window,
|
||||
cx,
|
||||
) {
|
||||
preview_container = preview_container.child(element);
|
||||
} else if let Some(preview) = component.preview() {
|
||||
preview_container = preview_container.children(preview(window, cx));
|
||||
}
|
||||
}
|
||||
} else if let Some(preview) = component.preview() {
|
||||
if let Some(preview) = component.preview() {
|
||||
preview_container = preview_container.children(preview(window, cx));
|
||||
}
|
||||
|
||||
@@ -629,9 +526,6 @@ impl ComponentPreview {
|
||||
PreviewEntry::AllComponents => {
|
||||
div().w_full().h_0().into_any_element()
|
||||
}
|
||||
PreviewEntry::ActiveThread => {
|
||||
div().w_full().h_0().into_any_element()
|
||||
}
|
||||
PreviewEntry::Separator => div().w_full().h_0().into_any_element(),
|
||||
}
|
||||
}),
|
||||
@@ -655,12 +549,7 @@ impl ComponentPreview {
|
||||
v_flex()
|
||||
.id("render-component-page")
|
||||
.flex_1()
|
||||
.child(ComponentPreviewPage::new(
|
||||
component.clone(),
|
||||
self.workspace.clone(),
|
||||
self.active_thread.clone(),
|
||||
self.reset_key,
|
||||
))
|
||||
.child(ComponentPreviewPage::new(component.clone(), self.reset_key))
|
||||
.into_any_element()
|
||||
} else {
|
||||
v_flex()
|
||||
@@ -672,25 +561,6 @@ impl ComponentPreview {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_active_thread(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.id("render-active-thread")
|
||||
.size_full()
|
||||
.child(
|
||||
div()
|
||||
.mx_auto()
|
||||
.w(px(640.))
|
||||
.h_full()
|
||||
.py_8()
|
||||
.bg(cx.theme().colors().panel_background)
|
||||
.children(self.active_thread.clone())
|
||||
.when_none(&self.active_thread.clone(), |this| {
|
||||
this.child("No active thread")
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn test_status_toast(&self, cx: &mut Context<Self>) {
|
||||
if let Some(workspace) = self.workspace.upgrade() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
@@ -803,9 +673,6 @@ impl Render for ComponentPreview {
|
||||
PreviewPage::Component(id) => self
|
||||
.render_component_page(&id, window, cx)
|
||||
.into_any_element(),
|
||||
PreviewPage::ActiveThread => {
|
||||
self.render_active_thread(cx).into_any_element()
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -1009,24 +876,18 @@ impl SerializableItem for ComponentPreview {
|
||||
pub struct ComponentPreviewPage {
|
||||
// languages: Arc<LanguageRegistry>,
|
||||
component: ComponentMetadata,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
active_thread: Option<Entity<ActiveThread>>,
|
||||
reset_key: usize,
|
||||
}
|
||||
|
||||
impl ComponentPreviewPage {
|
||||
pub fn new(
|
||||
component: ComponentMetadata,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
active_thread: Option<Entity<ActiveThread>>,
|
||||
reset_key: usize,
|
||||
// languages: Arc<LanguageRegistry>
|
||||
) -> Self {
|
||||
Self {
|
||||
// languages,
|
||||
component,
|
||||
workspace,
|
||||
active_thread,
|
||||
reset_key,
|
||||
}
|
||||
}
|
||||
@@ -1100,23 +961,7 @@ impl ComponentPreviewPage {
|
||||
}
|
||||
|
||||
fn render_preview(&self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
// Try to get agent preview first if we have an active thread
|
||||
let maybe_agent_preview = if let Some(active_thread) = self.active_thread.as_ref() {
|
||||
agent_ui::get_agent_preview(
|
||||
&self.component.id(),
|
||||
self.workspace.clone(),
|
||||
active_thread.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let content = if let Some(ag_preview) = maybe_agent_preview {
|
||||
// Use agent preview if available
|
||||
ag_preview
|
||||
} else if let Some(preview) = self.component.preview() {
|
||||
let content = if let Some(preview) = self.component.preview() {
|
||||
// Fall back to component preview
|
||||
preview(window, cx).unwrap_or_else(|| {
|
||||
div()
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
pub mod active_thread;
|
||||
@@ -1,100 +0,0 @@
|
||||
use agent::{ContextStore, MessageSegment, TextThreadStore, ThreadStore};
|
||||
use agent_ui::ActiveThread;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use gpui::{AppContext, AsyncApp, Entity, Task, WeakEntity};
|
||||
use indoc::indoc;
|
||||
use languages::LanguageRegistry;
|
||||
use project::Project;
|
||||
use prompt_store::PromptBuilder;
|
||||
use std::sync::Arc;
|
||||
use ui::{App, Window};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub fn load_preview_thread_store(
|
||||
project: Entity<Project>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Task<Result<Entity<ThreadStore>>> {
|
||||
cx.update(|cx| {
|
||||
ThreadStore::load(
|
||||
project.clone(),
|
||||
cx.new(|_| ToolWorkingSet::default()),
|
||||
None,
|
||||
Arc::new(PromptBuilder::new(None).unwrap()),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.unwrap_or(Task::ready(Err(anyhow!("workspace dropped"))))
|
||||
}
|
||||
|
||||
pub fn load_preview_text_thread_store(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Task<Result<Entity<TextThreadStore>>> {
|
||||
workspace
|
||||
.update(cx, |_, cx| {
|
||||
TextThreadStore::new(
|
||||
project.clone(),
|
||||
Arc::new(PromptBuilder::new(None).unwrap()),
|
||||
Default::default(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.unwrap_or(Task::ready(Err(anyhow!("workspace dropped"))))
|
||||
}
|
||||
|
||||
pub fn static_active_thread(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
thread_store: Entity<ThreadStore>,
|
||||
text_thread_store: Entity<TextThreadStore>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<ActiveThread> {
|
||||
let context_store =
|
||||
cx.new(|_| ContextStore::new(project.downgrade(), Some(thread_store.downgrade())));
|
||||
|
||||
let thread = thread_store.update(cx, |thread_store, cx| thread_store.create_thread(cx));
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.insert_assistant_message(vec![
|
||||
MessageSegment::Text(indoc! {"
|
||||
I'll help you fix the lifetime error in your `cx.spawn` call. When working with async operations in GPUI, there are specific patterns to follow for proper lifetime management.
|
||||
|
||||
Let's look at what's happening in your code:
|
||||
|
||||
---
|
||||
|
||||
Let's check the current state of the active_thread.rs file to understand what might have changed:
|
||||
|
||||
---
|
||||
|
||||
Looking at the implementation of `load_preview_thread_store` and understanding GPUI's async patterns, here's the issue:
|
||||
|
||||
1. `load_preview_thread_store` returns a `Task<anyhow::Result<Entity<ThreadStore>>>`, which means it's already a task.
|
||||
2. When you call this function inside another `spawn` call, you're nesting tasks incorrectly.
|
||||
|
||||
Here's the correct way to implement this:
|
||||
|
||||
---
|
||||
|
||||
The problem is in how you're setting up the async closure and trying to reference variables like `window` and `language_registry` that aren't accessible in that scope.
|
||||
|
||||
Here's how to fix it:
|
||||
"}.to_string()),
|
||||
], cx);
|
||||
});
|
||||
cx.new(|cx| {
|
||||
ActiveThread::new(
|
||||
thread,
|
||||
thread_store,
|
||||
text_thread_store,
|
||||
context_store,
|
||||
language_registry,
|
||||
workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user