
- **Fix a bunch of low hanging style lints like unnecessary-return** - **Fix single worktree violation** - **And the rest** Release Notes: - N/A
3874 lines
153 KiB
Rust
3874 lines
153 KiB
Rust
use std::ops::{Not, Range};
|
|
use std::path::Path;
|
|
use std::rc::Rc;
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
|
|
use agent2::{DbThreadMetadata, HistoryEntry};
|
|
use db::kvp::{Dismissable, KEY_VALUE_STORE};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::acp::{AcpThreadHistory, ThreadHistoryEvent};
|
|
use crate::agent_diff::AgentDiffThread;
|
|
use crate::{
|
|
AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode,
|
|
DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread,
|
|
NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ResetTrialEndUpsell,
|
|
ResetTrialUpsell, ToggleBurnMode, ToggleContextPicker, ToggleNavigationMenu,
|
|
ToggleNewThreadMenu, ToggleOptionsMenu,
|
|
acp::AcpThreadView,
|
|
active_thread::{self, ActiveThread, ActiveThreadEvent},
|
|
agent_configuration::{AgentConfiguration, AssistantConfigurationEvent},
|
|
agent_diff::AgentDiff,
|
|
message_editor::{MessageEditor, MessageEditorEvent},
|
|
slash_command::SlashCommandCompletionProvider,
|
|
text_thread_editor::{
|
|
AgentPanelDelegate, TextThreadEditor, humanize_token_count, make_lsp_adapter_delegate,
|
|
render_remaining_tokens,
|
|
},
|
|
thread_history::{HistoryEntryElement, ThreadHistory},
|
|
ui::{AgentOnboardingModal, EndTrialUpsell},
|
|
};
|
|
use crate::{ExternalAgent, NewExternalAgentThread};
|
|
use agent::{
|
|
Thread, ThreadError, ThreadEvent, ThreadId, ThreadSummary, TokenUsageRatio,
|
|
context_store::ContextStore,
|
|
history_store::{HistoryEntryId, HistoryStore},
|
|
thread_store::{TextThreadStore, ThreadStore},
|
|
};
|
|
use agent_settings::{AgentDockPosition, AgentSettings, CompletionMode, DefaultView};
|
|
use ai_onboarding::AgentPanelOnboarding;
|
|
use anyhow::{Result, anyhow};
|
|
use assistant_context::{AssistantContext, ContextEvent, ContextSummary};
|
|
use assistant_slash_command::SlashCommandWorkingSet;
|
|
use assistant_tool::ToolWorkingSet;
|
|
use client::{UserStore, zed_urls};
|
|
use cloud_llm_client::{CompletionIntent, Plan, UsageLimit};
|
|
use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
|
|
use feature_flags::{self, AcpFeatureFlag, ClaudeCodeFeatureFlag, FeatureFlagAppExt};
|
|
use fs::Fs;
|
|
use gpui::{
|
|
Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem,
|
|
Corner, DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, KeyContext,
|
|
Pixels, Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between,
|
|
};
|
|
use language::LanguageRegistry;
|
|
use language_model::{
|
|
ConfigurationError, ConfiguredModel, LanguageModelProviderTosView, LanguageModelRegistry,
|
|
};
|
|
use project::{DisableAiSettings, Project, ProjectPath, Worktree};
|
|
use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
|
|
use rules_library::{RulesLibrary, open_rules_library};
|
|
use search::{BufferSearchBar, buffer_search};
|
|
use settings::{Settings, update_settings_file};
|
|
use theme::ThemeSettings;
|
|
use time::UtcOffset;
|
|
use ui::utils::WithRemSize;
|
|
use ui::{
|
|
Banner, Callout, ContextMenu, ContextMenuEntry, Divider, ElevationIndex, KeyBinding,
|
|
PopoverMenu, PopoverMenuHandle, ProgressBar, Tab, Tooltip, prelude::*,
|
|
};
|
|
use util::ResultExt as _;
|
|
use workspace::{
|
|
CollaboratorId, DraggedSelection, DraggedTab, ToggleZoom, ToolbarItemView, Workspace,
|
|
dock::{DockPosition, Panel, PanelEvent},
|
|
};
|
|
use zed_actions::{
|
|
DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize,
|
|
agent::{OpenOnboardingModal, OpenSettings, ResetOnboarding, ToggleModelSelector},
|
|
assistant::{OpenRulesLibrary, ToggleFocus},
|
|
};
|
|
|
|
const AGENT_PANEL_KEY: &str = "agent_panel";
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
struct SerializedAgentPanel {
|
|
width: Option<Pixels>,
|
|
selected_agent: Option<AgentType>,
|
|
}
|
|
|
|
pub fn init(cx: &mut App) {
|
|
cx.observe_new(
|
|
|workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
|
|
workspace
|
|
.register_action(|workspace, action: &NewThread, window, cx| {
|
|
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
|
panel.update(cx, |panel, cx| panel.new_thread(action, window, cx));
|
|
workspace.focus_panel::<AgentPanel>(window, cx);
|
|
}
|
|
})
|
|
.register_action(|workspace, _: &OpenHistory, window, cx| {
|
|
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
|
workspace.focus_panel::<AgentPanel>(window, cx);
|
|
panel.update(cx, |panel, cx| panel.open_history(window, cx));
|
|
}
|
|
})
|
|
.register_action(|workspace, _: &OpenSettings, window, cx| {
|
|
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
|
workspace.focus_panel::<AgentPanel>(window, cx);
|
|
panel.update(cx, |panel, cx| panel.open_configuration(window, cx));
|
|
}
|
|
})
|
|
.register_action(|workspace, _: &NewTextThread, window, cx| {
|
|
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
|
workspace.focus_panel::<AgentPanel>(window, cx);
|
|
panel.update(cx, |panel, cx| panel.new_prompt_editor(window, cx));
|
|
}
|
|
})
|
|
.register_action(|workspace, action: &NewExternalAgentThread, window, cx| {
|
|
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
|
workspace.focus_panel::<AgentPanel>(window, cx);
|
|
panel.update(cx, |panel, cx| {
|
|
panel.external_thread(action.agent, None, window, cx)
|
|
});
|
|
}
|
|
})
|
|
.register_action(|workspace, action: &OpenRulesLibrary, window, cx| {
|
|
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
|
workspace.focus_panel::<AgentPanel>(window, cx);
|
|
panel.update(cx, |panel, cx| {
|
|
panel.deploy_rules_library(action, window, cx)
|
|
});
|
|
}
|
|
})
|
|
.register_action(|workspace, _: &OpenAgentDiff, window, cx| {
|
|
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
|
workspace.focus_panel::<AgentPanel>(window, cx);
|
|
match &panel.read(cx).active_view {
|
|
ActiveView::Thread { thread, .. } => {
|
|
let thread = thread.read(cx).thread().clone();
|
|
AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx);
|
|
}
|
|
ActiveView::ExternalAgentThread { .. }
|
|
| ActiveView::TextThread { .. }
|
|
| ActiveView::History
|
|
| ActiveView::Configuration => {}
|
|
}
|
|
}
|
|
})
|
|
.register_action(|workspace, _: &Follow, window, cx| {
|
|
workspace.follow(CollaboratorId::Agent, window, cx);
|
|
})
|
|
.register_action(|workspace, _: &ExpandMessageEditor, window, cx| {
|
|
let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
|
|
return;
|
|
};
|
|
workspace.focus_panel::<AgentPanel>(window, cx);
|
|
panel.update(cx, |panel, cx| {
|
|
if let Some(message_editor) = panel.active_message_editor() {
|
|
message_editor.update(cx, |editor, cx| {
|
|
editor.expand_message_editor(&ExpandMessageEditor, window, cx);
|
|
});
|
|
}
|
|
});
|
|
})
|
|
.register_action(|workspace, _: &ToggleNavigationMenu, window, cx| {
|
|
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
|
workspace.focus_panel::<AgentPanel>(window, cx);
|
|
panel.update(cx, |panel, cx| {
|
|
panel.toggle_navigation_menu(&ToggleNavigationMenu, window, cx);
|
|
});
|
|
}
|
|
})
|
|
.register_action(|workspace, _: &ToggleOptionsMenu, window, cx| {
|
|
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
|
workspace.focus_panel::<AgentPanel>(window, cx);
|
|
panel.update(cx, |panel, cx| {
|
|
panel.toggle_options_menu(&ToggleOptionsMenu, window, cx);
|
|
});
|
|
}
|
|
})
|
|
.register_action(|workspace, _: &ToggleNewThreadMenu, window, cx| {
|
|
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
|
workspace.focus_panel::<AgentPanel>(window, cx);
|
|
panel.update(cx, |panel, cx| {
|
|
panel.toggle_new_thread_menu(&ToggleNewThreadMenu, window, cx);
|
|
});
|
|
}
|
|
})
|
|
.register_action(|workspace, _: &OpenOnboardingModal, window, cx| {
|
|
AgentOnboardingModal::toggle(workspace, window, cx)
|
|
})
|
|
.register_action(|_workspace, _: &ResetOnboarding, window, cx| {
|
|
window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx);
|
|
window.refresh();
|
|
})
|
|
.register_action(|_workspace, _: &ResetTrialUpsell, _window, cx| {
|
|
OnboardingUpsell::set_dismissed(false, cx);
|
|
})
|
|
.register_action(|_workspace, _: &ResetTrialEndUpsell, _window, cx| {
|
|
TrialEndUpsell::set_dismissed(false, cx);
|
|
});
|
|
},
|
|
)
|
|
.detach();
|
|
}
|
|
|
|
enum ActiveView {
|
|
Thread {
|
|
thread: Entity<ActiveThread>,
|
|
change_title_editor: Entity<Editor>,
|
|
message_editor: Entity<MessageEditor>,
|
|
_subscriptions: Vec<gpui::Subscription>,
|
|
},
|
|
ExternalAgentThread {
|
|
thread_view: Entity<AcpThreadView>,
|
|
},
|
|
TextThread {
|
|
context_editor: Entity<TextThreadEditor>,
|
|
title_editor: Entity<Editor>,
|
|
buffer_search_bar: Entity<BufferSearchBar>,
|
|
_subscriptions: Vec<gpui::Subscription>,
|
|
},
|
|
History,
|
|
Configuration,
|
|
}
|
|
|
|
enum WhichFontSize {
|
|
AgentFont,
|
|
BufferFont,
|
|
None,
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum AgentType {
|
|
#[default]
|
|
Zed,
|
|
TextThread,
|
|
Gemini,
|
|
ClaudeCode,
|
|
NativeAgent,
|
|
}
|
|
|
|
impl AgentType {
|
|
fn label(self) -> impl Into<SharedString> {
|
|
match self {
|
|
Self::Zed | Self::TextThread => "Zed Agent",
|
|
Self::NativeAgent => "Agent 2",
|
|
Self::Gemini => "Google Gemini",
|
|
Self::ClaudeCode => "Claude Code",
|
|
}
|
|
}
|
|
|
|
fn icon(self) -> IconName {
|
|
match self {
|
|
Self::Zed | Self::TextThread => IconName::AiZed,
|
|
Self::NativeAgent => IconName::ZedAssistant,
|
|
Self::Gemini => IconName::AiGemini,
|
|
Self::ClaudeCode => IconName::AiClaude,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ActiveView {
|
|
pub fn which_font_size_used(&self) -> WhichFontSize {
|
|
match self {
|
|
ActiveView::Thread { .. }
|
|
| ActiveView::ExternalAgentThread { .. }
|
|
| ActiveView::History => WhichFontSize::AgentFont,
|
|
ActiveView::TextThread { .. } => WhichFontSize::BufferFont,
|
|
ActiveView::Configuration => WhichFontSize::None,
|
|
}
|
|
}
|
|
|
|
pub fn thread(
|
|
active_thread: Entity<ActiveThread>,
|
|
message_editor: Entity<MessageEditor>,
|
|
window: &mut Window,
|
|
cx: &mut Context<AgentPanel>,
|
|
) -> Self {
|
|
let summary = active_thread.read(cx).summary(cx).or_default();
|
|
|
|
let editor = cx.new(|cx| {
|
|
let mut editor = Editor::single_line(window, cx);
|
|
editor.set_text(summary.clone(), window, cx);
|
|
editor
|
|
});
|
|
|
|
let subscriptions = vec![
|
|
cx.subscribe(&message_editor, |this, _, event, cx| match event {
|
|
MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
|
|
cx.notify();
|
|
}
|
|
MessageEditorEvent::ScrollThreadToBottom => match &this.active_view {
|
|
ActiveView::Thread { thread, .. } => {
|
|
thread.update(cx, |thread, cx| {
|
|
thread.scroll_to_bottom(cx);
|
|
});
|
|
}
|
|
ActiveView::ExternalAgentThread { .. } => {}
|
|
ActiveView::TextThread { .. }
|
|
| ActiveView::History
|
|
| ActiveView::Configuration => {}
|
|
},
|
|
}),
|
|
window.subscribe(&editor, cx, {
|
|
{
|
|
let thread = active_thread.clone();
|
|
move |editor, event, window, cx| match event {
|
|
EditorEvent::BufferEdited => {
|
|
let new_summary = editor.read(cx).text(cx);
|
|
|
|
thread.update(cx, |thread, cx| {
|
|
thread.thread().update(cx, |thread, cx| {
|
|
thread.set_summary(new_summary, cx);
|
|
});
|
|
})
|
|
}
|
|
EditorEvent::Blurred => {
|
|
if editor.read(cx).text(cx).is_empty() {
|
|
let summary = thread.read(cx).summary(cx).or_default();
|
|
|
|
editor.update(cx, |editor, cx| {
|
|
editor.set_text(summary, window, cx);
|
|
});
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}),
|
|
cx.subscribe(&active_thread, |_, _, event, cx| match &event {
|
|
ActiveThreadEvent::EditingMessageTokenCountChanged => {
|
|
cx.notify();
|
|
}
|
|
}),
|
|
cx.subscribe_in(&active_thread.read(cx).thread().clone(), window, {
|
|
let editor = editor.clone();
|
|
move |_, thread, event, window, cx| match event {
|
|
ThreadEvent::SummaryGenerated => {
|
|
let summary = thread.read(cx).summary().or_default();
|
|
|
|
editor.update(cx, |editor, cx| {
|
|
editor.set_text(summary, window, cx);
|
|
})
|
|
}
|
|
ThreadEvent::MessageAdded(_) => {
|
|
cx.notify();
|
|
}
|
|
_ => {}
|
|
}
|
|
}),
|
|
];
|
|
|
|
Self::Thread {
|
|
change_title_editor: editor,
|
|
thread: active_thread,
|
|
message_editor: message_editor,
|
|
_subscriptions: subscriptions,
|
|
}
|
|
}
|
|
|
|
pub fn prompt_editor(
|
|
context_editor: Entity<TextThreadEditor>,
|
|
history_store: Entity<HistoryStore>,
|
|
acp_history_store: Entity<agent2::HistoryStore>,
|
|
language_registry: Arc<LanguageRegistry>,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) -> Self {
|
|
let title = context_editor.read(cx).title(cx).to_string();
|
|
|
|
let editor = cx.new(|cx| {
|
|
let mut editor = Editor::single_line(window, cx);
|
|
editor.set_text(title, window, cx);
|
|
editor
|
|
});
|
|
|
|
// This is a workaround for `editor.set_text` emitting a `BufferEdited` event, which would
|
|
// cause a custom summary to be set. The presence of this custom summary would cause
|
|
// summarization to not happen.
|
|
let mut suppress_first_edit = true;
|
|
|
|
let subscriptions = vec![
|
|
window.subscribe(&editor, cx, {
|
|
{
|
|
let context_editor = context_editor.clone();
|
|
move |editor, event, window, cx| match event {
|
|
EditorEvent::BufferEdited => {
|
|
if suppress_first_edit {
|
|
suppress_first_edit = false;
|
|
return;
|
|
}
|
|
let new_summary = editor.read(cx).text(cx);
|
|
|
|
context_editor.update(cx, |context_editor, cx| {
|
|
context_editor
|
|
.context()
|
|
.update(cx, |assistant_context, cx| {
|
|
assistant_context.set_custom_summary(new_summary, cx);
|
|
})
|
|
})
|
|
}
|
|
EditorEvent::Blurred => {
|
|
if editor.read(cx).text(cx).is_empty() {
|
|
let summary = context_editor
|
|
.read(cx)
|
|
.context()
|
|
.read(cx)
|
|
.summary()
|
|
.or_default();
|
|
|
|
editor.update(cx, |editor, cx| {
|
|
editor.set_text(summary, window, cx);
|
|
});
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}),
|
|
window.subscribe(&context_editor.read(cx).context().clone(), cx, {
|
|
let editor = editor.clone();
|
|
move |assistant_context, event, window, cx| match event {
|
|
ContextEvent::SummaryGenerated => {
|
|
let summary = assistant_context.read(cx).summary().or_default();
|
|
|
|
editor.update(cx, |editor, cx| {
|
|
editor.set_text(summary, window, cx);
|
|
})
|
|
}
|
|
ContextEvent::PathChanged { old_path, new_path } => {
|
|
history_store.update(cx, |history_store, cx| {
|
|
if let Some(old_path) = old_path {
|
|
history_store
|
|
.replace_recently_opened_text_thread(old_path, new_path, cx);
|
|
} else {
|
|
history_store.push_recently_opened_entry(
|
|
HistoryEntryId::Context(new_path.clone()),
|
|
cx,
|
|
);
|
|
}
|
|
});
|
|
|
|
acp_history_store.update(cx, |history_store, cx| {
|
|
if let Some(old_path) = old_path {
|
|
history_store
|
|
.replace_recently_opened_text_thread(old_path, new_path, cx);
|
|
} else {
|
|
history_store.push_recently_opened_entry(
|
|
agent2::HistoryEntryId::TextThread(new_path.clone()),
|
|
cx,
|
|
);
|
|
}
|
|
});
|
|
}
|
|
_ => {}
|
|
}
|
|
}),
|
|
];
|
|
|
|
let buffer_search_bar =
|
|
cx.new(|cx| BufferSearchBar::new(Some(language_registry), window, cx));
|
|
buffer_search_bar.update(cx, |buffer_search_bar, cx| {
|
|
buffer_search_bar.set_active_pane_item(Some(&context_editor), window, cx)
|
|
});
|
|
|
|
Self::TextThread {
|
|
context_editor,
|
|
title_editor: editor,
|
|
buffer_search_bar,
|
|
_subscriptions: subscriptions,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct AgentPanel {
|
|
workspace: WeakEntity<Workspace>,
|
|
user_store: Entity<UserStore>,
|
|
project: Entity<Project>,
|
|
fs: Arc<dyn Fs>,
|
|
language_registry: Arc<LanguageRegistry>,
|
|
thread_store: Entity<ThreadStore>,
|
|
acp_history: Entity<AcpThreadHistory>,
|
|
acp_history_store: Entity<agent2::HistoryStore>,
|
|
_default_model_subscription: Subscription,
|
|
context_store: Entity<TextThreadStore>,
|
|
prompt_store: Option<Entity<PromptStore>>,
|
|
inline_assist_context_store: Entity<ContextStore>,
|
|
configuration: Option<Entity<AgentConfiguration>>,
|
|
configuration_subscription: Option<Subscription>,
|
|
local_timezone: UtcOffset,
|
|
active_view: ActiveView,
|
|
previous_view: Option<ActiveView>,
|
|
history_store: Entity<HistoryStore>,
|
|
history: Entity<ThreadHistory>,
|
|
hovered_recent_history_item: Option<usize>,
|
|
new_thread_menu_handle: PopoverMenuHandle<ContextMenu>,
|
|
agent_panel_menu_handle: PopoverMenuHandle<ContextMenu>,
|
|
assistant_navigation_menu_handle: PopoverMenuHandle<ContextMenu>,
|
|
assistant_navigation_menu: Option<Entity<ContextMenu>>,
|
|
width: Option<Pixels>,
|
|
height: Option<Pixels>,
|
|
zoomed: bool,
|
|
pending_serialization: Option<Task<Result<()>>>,
|
|
onboarding: Entity<AgentPanelOnboarding>,
|
|
selected_agent: AgentType,
|
|
}
|
|
|
|
impl AgentPanel {
|
|
fn serialize(&mut self, cx: &mut Context<Self>) {
|
|
let width = self.width;
|
|
let selected_agent = self.selected_agent;
|
|
self.pending_serialization = Some(cx.background_spawn(async move {
|
|
KEY_VALUE_STORE
|
|
.write_kvp(
|
|
AGENT_PANEL_KEY.into(),
|
|
serde_json::to_string(&SerializedAgentPanel {
|
|
width,
|
|
selected_agent: Some(selected_agent),
|
|
})?,
|
|
)
|
|
.await?;
|
|
anyhow::Ok(())
|
|
}));
|
|
}
|
|
pub fn load(
|
|
workspace: WeakEntity<Workspace>,
|
|
prompt_builder: Arc<PromptBuilder>,
|
|
mut cx: AsyncWindowContext,
|
|
) -> Task<Result<Entity<Self>>> {
|
|
let prompt_store = cx.update(|_window, cx| PromptStore::global(cx));
|
|
cx.spawn(async move |cx| {
|
|
let prompt_store = match prompt_store {
|
|
Ok(prompt_store) => prompt_store.await.ok(),
|
|
Err(_) => None,
|
|
};
|
|
let tools = cx.new(|_| ToolWorkingSet::default())?;
|
|
let thread_store = workspace
|
|
.update(cx, |workspace, cx| {
|
|
let project = workspace.project().clone();
|
|
ThreadStore::load(
|
|
project,
|
|
tools.clone(),
|
|
prompt_store.clone(),
|
|
prompt_builder.clone(),
|
|
cx,
|
|
)
|
|
})?
|
|
.await?;
|
|
|
|
let slash_commands = Arc::new(SlashCommandWorkingSet::default());
|
|
let context_store = workspace
|
|
.update(cx, |workspace, cx| {
|
|
let project = workspace.project().clone();
|
|
assistant_context::ContextStore::new(
|
|
project,
|
|
prompt_builder.clone(),
|
|
slash_commands,
|
|
cx,
|
|
)
|
|
})?
|
|
.await?;
|
|
|
|
let serialized_panel = if let Some(panel) = cx
|
|
.background_spawn(async move { KEY_VALUE_STORE.read_kvp(AGENT_PANEL_KEY) })
|
|
.await
|
|
.log_err()
|
|
.flatten()
|
|
{
|
|
Some(serde_json::from_str::<SerializedAgentPanel>(&panel)?)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
|
let panel = cx.new(|cx| {
|
|
Self::new(
|
|
workspace,
|
|
thread_store,
|
|
context_store,
|
|
prompt_store,
|
|
window,
|
|
cx,
|
|
)
|
|
});
|
|
if let Some(serialized_panel) = serialized_panel {
|
|
panel.update(cx, |panel, cx| {
|
|
panel.width = serialized_panel.width.map(|w| w.round());
|
|
if let Some(selected_agent) = serialized_panel.selected_agent {
|
|
panel.selected_agent = selected_agent;
|
|
panel.new_agent_thread(selected_agent, window, cx);
|
|
}
|
|
cx.notify();
|
|
});
|
|
}
|
|
panel
|
|
})?;
|
|
|
|
Ok(panel)
|
|
})
|
|
}
|
|
|
|
fn new(
|
|
workspace: &Workspace,
|
|
thread_store: Entity<ThreadStore>,
|
|
context_store: Entity<TextThreadStore>,
|
|
prompt_store: Option<Entity<PromptStore>>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> Self {
|
|
let thread = thread_store.update(cx, |this, cx| this.create_thread(cx));
|
|
let fs = workspace.app_state().fs.clone();
|
|
let user_store = workspace.app_state().user_store.clone();
|
|
let project = workspace.project();
|
|
let language_registry = project.read(cx).languages().clone();
|
|
let client = workspace.client().clone();
|
|
let workspace = workspace.weak_handle();
|
|
let weak_self = cx.entity().downgrade();
|
|
|
|
let message_editor_context_store =
|
|
cx.new(|_cx| ContextStore::new(project.downgrade(), Some(thread_store.downgrade())));
|
|
let inline_assist_context_store =
|
|
cx.new(|_cx| ContextStore::new(project.downgrade(), Some(thread_store.downgrade())));
|
|
|
|
let thread_id = thread.read(cx).id().clone();
|
|
|
|
let history_store = cx.new(|cx| {
|
|
HistoryStore::new(
|
|
thread_store.clone(),
|
|
context_store.clone(),
|
|
[HistoryEntryId::Thread(thread_id)],
|
|
cx,
|
|
)
|
|
});
|
|
|
|
let message_editor = cx.new(|cx| {
|
|
MessageEditor::new(
|
|
fs.clone(),
|
|
workspace.clone(),
|
|
message_editor_context_store.clone(),
|
|
prompt_store.clone(),
|
|
thread_store.downgrade(),
|
|
context_store.downgrade(),
|
|
Some(history_store.downgrade()),
|
|
thread.clone(),
|
|
window,
|
|
cx,
|
|
)
|
|
});
|
|
|
|
let acp_history_store =
|
|
cx.new(|cx| agent2::HistoryStore::new(context_store.clone(), [], cx));
|
|
let acp_history = cx.new(|cx| AcpThreadHistory::new(acp_history_store.clone(), window, cx));
|
|
cx.subscribe_in(
|
|
&acp_history,
|
|
window,
|
|
|this, _, event, window, cx| match event {
|
|
ThreadHistoryEvent::Open(HistoryEntry::AcpThread(thread)) => {
|
|
this.external_thread(
|
|
Some(crate::ExternalAgent::NativeAgent),
|
|
Some(thread.clone()),
|
|
window,
|
|
cx,
|
|
);
|
|
}
|
|
ThreadHistoryEvent::Open(HistoryEntry::TextThread(thread)) => {
|
|
this.open_saved_prompt_editor(thread.path.clone(), window, cx)
|
|
.detach_and_log_err(cx);
|
|
}
|
|
},
|
|
)
|
|
.detach();
|
|
|
|
cx.observe(&history_store, |_, _, cx| cx.notify()).detach();
|
|
|
|
let active_thread = cx.new(|cx| {
|
|
ActiveThread::new(
|
|
thread.clone(),
|
|
thread_store.clone(),
|
|
context_store.clone(),
|
|
message_editor_context_store.clone(),
|
|
language_registry.clone(),
|
|
workspace.clone(),
|
|
window,
|
|
cx,
|
|
)
|
|
});
|
|
|
|
let panel_type = AgentSettings::get_global(cx).default_view;
|
|
let active_view = match panel_type {
|
|
DefaultView::Thread => ActiveView::thread(active_thread, message_editor, window, cx),
|
|
DefaultView::TextThread => {
|
|
let context =
|
|
context_store.update(cx, |context_store, cx| context_store.create(cx));
|
|
let lsp_adapter_delegate = make_lsp_adapter_delegate(&project.clone(), cx).unwrap();
|
|
let context_editor = cx.new(|cx| {
|
|
let mut editor = TextThreadEditor::for_context(
|
|
context,
|
|
fs.clone(),
|
|
workspace.clone(),
|
|
project.clone(),
|
|
lsp_adapter_delegate,
|
|
window,
|
|
cx,
|
|
);
|
|
editor.insert_default_prompt(window, cx);
|
|
editor
|
|
});
|
|
ActiveView::prompt_editor(
|
|
context_editor,
|
|
history_store.clone(),
|
|
acp_history_store.clone(),
|
|
language_registry.clone(),
|
|
window,
|
|
cx,
|
|
)
|
|
}
|
|
};
|
|
|
|
AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx);
|
|
|
|
let weak_panel = weak_self.clone();
|
|
|
|
window.defer(cx, move |window, cx| {
|
|
let panel = weak_panel.clone();
|
|
let assistant_navigation_menu =
|
|
ContextMenu::build_persistent(window, cx, move |mut menu, _window, cx| {
|
|
if let Some(panel) = panel.upgrade() {
|
|
if cx.has_flag::<AcpFeatureFlag>() {
|
|
menu = Self::populate_recently_opened_menu_section_new(menu, panel, cx);
|
|
} else {
|
|
menu = Self::populate_recently_opened_menu_section_old(menu, panel, cx);
|
|
}
|
|
}
|
|
menu.action("View All", Box::new(OpenHistory))
|
|
.end_slot_action(DeleteRecentlyOpenThread.boxed_clone())
|
|
.fixed_width(px(320.).into())
|
|
.keep_open_on_confirm(false)
|
|
.key_context("NavigationMenu")
|
|
});
|
|
weak_panel
|
|
.update(cx, |panel, cx| {
|
|
cx.subscribe_in(
|
|
&assistant_navigation_menu,
|
|
window,
|
|
|_, menu, _: &DismissEvent, window, cx| {
|
|
menu.update(cx, |menu, _| {
|
|
menu.clear_selected();
|
|
});
|
|
cx.focus_self(window);
|
|
},
|
|
)
|
|
.detach();
|
|
panel.assistant_navigation_menu = Some(assistant_navigation_menu);
|
|
})
|
|
.ok();
|
|
});
|
|
|
|
let _default_model_subscription = cx.subscribe(
|
|
&LanguageModelRegistry::global(cx),
|
|
|this, _, event: &language_model::Event, cx| match event {
|
|
language_model::Event::DefaultModelChanged => match &this.active_view {
|
|
ActiveView::Thread { thread, .. } => {
|
|
thread
|
|
.read(cx)
|
|
.thread()
|
|
.clone()
|
|
.update(cx, |thread, cx| thread.get_or_init_configured_model(cx));
|
|
}
|
|
ActiveView::ExternalAgentThread { .. }
|
|
| ActiveView::TextThread { .. }
|
|
| ActiveView::History
|
|
| ActiveView::Configuration => {}
|
|
},
|
|
_ => {}
|
|
},
|
|
);
|
|
|
|
let onboarding = cx.new(|cx| {
|
|
AgentPanelOnboarding::new(
|
|
user_store.clone(),
|
|
client,
|
|
|_window, cx| {
|
|
OnboardingUpsell::set_dismissed(true, cx);
|
|
},
|
|
cx,
|
|
)
|
|
});
|
|
|
|
Self {
|
|
active_view,
|
|
workspace,
|
|
user_store,
|
|
project: project.clone(),
|
|
fs: fs.clone(),
|
|
language_registry,
|
|
thread_store: thread_store.clone(),
|
|
_default_model_subscription,
|
|
context_store,
|
|
prompt_store,
|
|
configuration: None,
|
|
configuration_subscription: None,
|
|
local_timezone: UtcOffset::from_whole_seconds(
|
|
chrono::Local::now().offset().local_minus_utc(),
|
|
)
|
|
.unwrap(),
|
|
inline_assist_context_store,
|
|
previous_view: None,
|
|
history_store: history_store.clone(),
|
|
history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, window, cx)),
|
|
hovered_recent_history_item: None,
|
|
new_thread_menu_handle: PopoverMenuHandle::default(),
|
|
agent_panel_menu_handle: PopoverMenuHandle::default(),
|
|
assistant_navigation_menu_handle: PopoverMenuHandle::default(),
|
|
assistant_navigation_menu: None,
|
|
width: None,
|
|
height: None,
|
|
zoomed: false,
|
|
pending_serialization: None,
|
|
onboarding,
|
|
acp_history,
|
|
acp_history_store,
|
|
selected_agent: AgentType::default(),
|
|
}
|
|
}
|
|
|
|
pub fn toggle_focus(
|
|
workspace: &mut Workspace,
|
|
_: &ToggleFocus,
|
|
window: &mut Window,
|
|
cx: &mut Context<Workspace>,
|
|
) {
|
|
if workspace
|
|
.panel::<Self>(cx)
|
|
.is_some_and(|panel| panel.read(cx).enabled(cx))
|
|
&& !DisableAiSettings::get_global(cx).disable_ai
|
|
{
|
|
workspace.toggle_panel_focus::<Self>(window, cx);
|
|
}
|
|
}
|
|
|
|
pub(crate) fn local_timezone(&self) -> UtcOffset {
|
|
self.local_timezone
|
|
}
|
|
|
|
pub(crate) fn prompt_store(&self) -> &Option<Entity<PromptStore>> {
|
|
&self.prompt_store
|
|
}
|
|
|
|
pub(crate) fn inline_assist_context_store(&self) -> &Entity<ContextStore> {
|
|
&self.inline_assist_context_store
|
|
}
|
|
|
|
pub(crate) fn thread_store(&self) -> &Entity<ThreadStore> {
|
|
&self.thread_store
|
|
}
|
|
|
|
pub(crate) fn text_thread_store(&self) -> &Entity<TextThreadStore> {
|
|
&self.context_store
|
|
}
|
|
|
|
fn cancel(&mut self, _: &editor::actions::Cancel, window: &mut Window, cx: &mut Context<Self>) {
|
|
match &self.active_view {
|
|
ActiveView::Thread { thread, .. } => {
|
|
thread.update(cx, |thread, cx| thread.cancel_last_completion(window, cx));
|
|
}
|
|
ActiveView::ExternalAgentThread { .. }
|
|
| ActiveView::TextThread { .. }
|
|
| ActiveView::History
|
|
| ActiveView::Configuration => {}
|
|
}
|
|
}
|
|
|
|
fn active_message_editor(&self) -> Option<&Entity<MessageEditor>> {
|
|
match &self.active_view {
|
|
ActiveView::Thread { message_editor, .. } => Some(message_editor),
|
|
ActiveView::ExternalAgentThread { .. }
|
|
| ActiveView::TextThread { .. }
|
|
| ActiveView::History
|
|
| ActiveView::Configuration => None,
|
|
}
|
|
}
|
|
|
|
fn new_thread(&mut self, action: &NewThread, window: &mut Window, cx: &mut Context<Self>) {
|
|
// Preserve chat box text when using creating new thread
|
|
let preserved_text = self
|
|
.active_message_editor()
|
|
.map(|editor| editor.read(cx).get_text(cx).trim().to_string());
|
|
|
|
let thread = self
|
|
.thread_store
|
|
.update(cx, |this, cx| this.create_thread(cx));
|
|
|
|
let context_store = cx.new(|_cx| {
|
|
ContextStore::new(
|
|
self.project.downgrade(),
|
|
Some(self.thread_store.downgrade()),
|
|
)
|
|
});
|
|
|
|
if let Some(other_thread_id) = action.from_thread_id.clone() {
|
|
let other_thread_task = self.thread_store.update(cx, |this, cx| {
|
|
this.open_thread(&other_thread_id, window, cx)
|
|
});
|
|
|
|
cx.spawn({
|
|
let context_store = context_store.clone();
|
|
|
|
async move |_panel, cx| {
|
|
let other_thread = other_thread_task.await?;
|
|
|
|
context_store.update(cx, |this, cx| {
|
|
this.add_thread(other_thread, false, cx);
|
|
})?;
|
|
anyhow::Ok(())
|
|
}
|
|
})
|
|
.detach_and_log_err(cx);
|
|
}
|
|
|
|
let active_thread = cx.new(|cx| {
|
|
ActiveThread::new(
|
|
thread.clone(),
|
|
self.thread_store.clone(),
|
|
self.context_store.clone(),
|
|
context_store.clone(),
|
|
self.language_registry.clone(),
|
|
self.workspace.clone(),
|
|
window,
|
|
cx,
|
|
)
|
|
});
|
|
|
|
let message_editor = cx.new(|cx| {
|
|
MessageEditor::new(
|
|
self.fs.clone(),
|
|
self.workspace.clone(),
|
|
context_store.clone(),
|
|
self.prompt_store.clone(),
|
|
self.thread_store.downgrade(),
|
|
self.context_store.downgrade(),
|
|
Some(self.history_store.downgrade()),
|
|
thread.clone(),
|
|
window,
|
|
cx,
|
|
)
|
|
});
|
|
|
|
if let Some(text) = preserved_text {
|
|
message_editor.update(cx, |editor, cx| {
|
|
editor.set_text(text, window, cx);
|
|
});
|
|
}
|
|
|
|
message_editor.focus_handle(cx).focus(window);
|
|
|
|
let thread_view = ActiveView::thread(active_thread.clone(), message_editor, window, cx);
|
|
self.set_active_view(thread_view, window, cx);
|
|
|
|
AgentDiff::set_active_thread(&self.workspace, thread.clone(), window, cx);
|
|
}
|
|
|
|
fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
let context = self
|
|
.context_store
|
|
.update(cx, |context_store, cx| context_store.create(cx));
|
|
let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx)
|
|
.log_err()
|
|
.flatten();
|
|
|
|
let context_editor = cx.new(|cx| {
|
|
let mut editor = TextThreadEditor::for_context(
|
|
context,
|
|
self.fs.clone(),
|
|
self.workspace.clone(),
|
|
self.project.clone(),
|
|
lsp_adapter_delegate,
|
|
window,
|
|
cx,
|
|
);
|
|
editor.insert_default_prompt(window, cx);
|
|
editor
|
|
});
|
|
|
|
self.set_active_view(
|
|
ActiveView::prompt_editor(
|
|
context_editor.clone(),
|
|
self.history_store.clone(),
|
|
self.acp_history_store.clone(),
|
|
self.language_registry.clone(),
|
|
window,
|
|
cx,
|
|
),
|
|
window,
|
|
cx,
|
|
);
|
|
context_editor.focus_handle(cx).focus(window);
|
|
}
|
|
|
|
fn external_thread(
|
|
&mut self,
|
|
agent_choice: Option<crate::ExternalAgent>,
|
|
resume_thread: Option<DbThreadMetadata>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let workspace = self.workspace.clone();
|
|
let project = self.project.clone();
|
|
let fs = self.fs.clone();
|
|
|
|
const LAST_USED_EXTERNAL_AGENT_KEY: &str = "agent_panel__last_used_external_agent";
|
|
|
|
#[derive(Default, Serialize, Deserialize)]
|
|
struct LastUsedExternalAgent {
|
|
agent: crate::ExternalAgent,
|
|
}
|
|
|
|
let thread_store = self.thread_store.clone();
|
|
let text_thread_store = self.context_store.clone();
|
|
let history = self.acp_history_store.clone();
|
|
|
|
cx.spawn_in(window, async move |this, cx| {
|
|
let ext_agent = match agent_choice {
|
|
Some(agent) => {
|
|
cx.background_spawn(async move {
|
|
if let Some(serialized) =
|
|
serde_json::to_string(&LastUsedExternalAgent { agent }).log_err()
|
|
{
|
|
KEY_VALUE_STORE
|
|
.write_kvp(LAST_USED_EXTERNAL_AGENT_KEY.to_string(), serialized)
|
|
.await
|
|
.log_err();
|
|
}
|
|
})
|
|
.detach();
|
|
|
|
agent
|
|
}
|
|
None => {
|
|
cx.background_spawn(async move {
|
|
KEY_VALUE_STORE.read_kvp(LAST_USED_EXTERNAL_AGENT_KEY)
|
|
})
|
|
.await
|
|
.log_err()
|
|
.flatten()
|
|
.and_then(|value| {
|
|
serde_json::from_str::<LastUsedExternalAgent>(&value).log_err()
|
|
})
|
|
.unwrap_or_default()
|
|
.agent
|
|
}
|
|
};
|
|
|
|
let server = ext_agent.server(fs, history);
|
|
|
|
this.update_in(cx, |this, window, cx| {
|
|
match ext_agent {
|
|
crate::ExternalAgent::Gemini | crate::ExternalAgent::NativeAgent => {
|
|
if !cx.has_flag::<AcpFeatureFlag>() {
|
|
return;
|
|
}
|
|
}
|
|
crate::ExternalAgent::ClaudeCode => {
|
|
if !cx.has_flag::<ClaudeCodeFeatureFlag>() {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
let thread_view = cx.new(|cx| {
|
|
crate::acp::AcpThreadView::new(
|
|
server,
|
|
resume_thread,
|
|
workspace.clone(),
|
|
project,
|
|
thread_store.clone(),
|
|
text_thread_store.clone(),
|
|
window,
|
|
cx,
|
|
)
|
|
});
|
|
|
|
this.set_active_view(ActiveView::ExternalAgentThread { thread_view }, window, cx);
|
|
})
|
|
})
|
|
.detach_and_log_err(cx);
|
|
}
|
|
|
|
fn deploy_rules_library(
|
|
&mut self,
|
|
action: &OpenRulesLibrary,
|
|
_window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
open_rules_library(
|
|
self.language_registry.clone(),
|
|
Box::new(PromptLibraryInlineAssist::new(self.workspace.clone())),
|
|
Rc::new(|| {
|
|
Rc::new(SlashCommandCompletionProvider::new(
|
|
Arc::new(SlashCommandWorkingSet::default()),
|
|
None,
|
|
None,
|
|
))
|
|
}),
|
|
action
|
|
.prompt_to_select
|
|
.map(|uuid| UserPromptId(uuid).into()),
|
|
cx,
|
|
)
|
|
.detach_and_log_err(cx);
|
|
}
|
|
|
|
fn open_history(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
if matches!(self.active_view, ActiveView::History) {
|
|
if let Some(previous_view) = self.previous_view.take() {
|
|
self.set_active_view(previous_view, window, cx);
|
|
}
|
|
} else {
|
|
self.thread_store
|
|
.update(cx, |thread_store, cx| thread_store.reload(cx))
|
|
.detach_and_log_err(cx);
|
|
self.set_active_view(ActiveView::History, window, cx);
|
|
}
|
|
cx.notify();
|
|
}
|
|
|
|
pub(crate) fn open_saved_prompt_editor(
|
|
&mut self,
|
|
path: Arc<Path>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> Task<Result<()>> {
|
|
let context = self
|
|
.context_store
|
|
.update(cx, |store, cx| store.open_local_context(path, cx));
|
|
cx.spawn_in(window, async move |this, cx| {
|
|
let context = context.await?;
|
|
this.update_in(cx, |this, window, cx| {
|
|
this.open_prompt_editor(context, window, cx);
|
|
})
|
|
})
|
|
}
|
|
|
|
pub(crate) fn open_prompt_editor(
|
|
&mut self,
|
|
context: Entity<AssistantContext>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project.clone(), cx)
|
|
.log_err()
|
|
.flatten();
|
|
let editor = cx.new(|cx| {
|
|
TextThreadEditor::for_context(
|
|
context,
|
|
self.fs.clone(),
|
|
self.workspace.clone(),
|
|
self.project.clone(),
|
|
lsp_adapter_delegate,
|
|
window,
|
|
cx,
|
|
)
|
|
});
|
|
self.set_active_view(
|
|
ActiveView::prompt_editor(
|
|
editor.clone(),
|
|
self.history_store.clone(),
|
|
self.acp_history_store.clone(),
|
|
self.language_registry.clone(),
|
|
window,
|
|
cx,
|
|
),
|
|
window,
|
|
cx,
|
|
);
|
|
}
|
|
|
|
pub(crate) fn open_thread_by_id(
|
|
&mut self,
|
|
thread_id: &ThreadId,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> Task<Result<()>> {
|
|
let open_thread_task = self
|
|
.thread_store
|
|
.update(cx, |this, cx| this.open_thread(thread_id, window, cx));
|
|
cx.spawn_in(window, async move |this, cx| {
|
|
let thread = open_thread_task.await?;
|
|
this.update_in(cx, |this, window, cx| {
|
|
this.open_thread(thread, window, cx);
|
|
anyhow::Ok(())
|
|
})??;
|
|
Ok(())
|
|
})
|
|
}
|
|
|
|
pub(crate) fn open_thread(
|
|
&mut self,
|
|
thread: Entity<Thread>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let context_store = cx.new(|_cx| {
|
|
ContextStore::new(
|
|
self.project.downgrade(),
|
|
Some(self.thread_store.downgrade()),
|
|
)
|
|
});
|
|
|
|
let active_thread = cx.new(|cx| {
|
|
ActiveThread::new(
|
|
thread.clone(),
|
|
self.thread_store.clone(),
|
|
self.context_store.clone(),
|
|
context_store.clone(),
|
|
self.language_registry.clone(),
|
|
self.workspace.clone(),
|
|
window,
|
|
cx,
|
|
)
|
|
});
|
|
|
|
let message_editor = cx.new(|cx| {
|
|
MessageEditor::new(
|
|
self.fs.clone(),
|
|
self.workspace.clone(),
|
|
context_store,
|
|
self.prompt_store.clone(),
|
|
self.thread_store.downgrade(),
|
|
self.context_store.downgrade(),
|
|
Some(self.history_store.downgrade()),
|
|
thread.clone(),
|
|
window,
|
|
cx,
|
|
)
|
|
});
|
|
message_editor.focus_handle(cx).focus(window);
|
|
|
|
let thread_view = ActiveView::thread(active_thread.clone(), message_editor, window, cx);
|
|
self.set_active_view(thread_view, window, cx);
|
|
AgentDiff::set_active_thread(&self.workspace, thread.clone(), window, cx);
|
|
}
|
|
|
|
pub fn go_back(&mut self, _: &workspace::GoBack, window: &mut Window, cx: &mut Context<Self>) {
|
|
match self.active_view {
|
|
ActiveView::Configuration | ActiveView::History => {
|
|
if let Some(previous_view) = self.previous_view.take() {
|
|
self.active_view = previous_view;
|
|
|
|
match &self.active_view {
|
|
ActiveView::Thread { message_editor, .. } => {
|
|
message_editor.focus_handle(cx).focus(window);
|
|
}
|
|
ActiveView::ExternalAgentThread { thread_view } => {
|
|
thread_view.focus_handle(cx).focus(window);
|
|
}
|
|
ActiveView::TextThread { context_editor, .. } => {
|
|
context_editor.focus_handle(cx).focus(window);
|
|
}
|
|
ActiveView::History | ActiveView::Configuration => {}
|
|
}
|
|
}
|
|
cx.notify();
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
pub fn toggle_navigation_menu(
|
|
&mut self,
|
|
_: &ToggleNavigationMenu,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.assistant_navigation_menu_handle.toggle(window, cx);
|
|
}
|
|
|
|
pub fn toggle_options_menu(
|
|
&mut self,
|
|
_: &ToggleOptionsMenu,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.agent_panel_menu_handle.toggle(window, cx);
|
|
}
|
|
|
|
pub fn toggle_new_thread_menu(
|
|
&mut self,
|
|
_: &ToggleNewThreadMenu,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.new_thread_menu_handle.toggle(window, cx);
|
|
}
|
|
|
|
pub fn increase_font_size(
|
|
&mut self,
|
|
action: &IncreaseBufferFontSize,
|
|
_: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.handle_font_size_action(action.persist, px(1.0), cx);
|
|
}
|
|
|
|
pub fn decrease_font_size(
|
|
&mut self,
|
|
action: &DecreaseBufferFontSize,
|
|
_: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.handle_font_size_action(action.persist, px(-1.0), cx);
|
|
}
|
|
|
|
fn handle_font_size_action(&mut self, persist: bool, delta: Pixels, cx: &mut Context<Self>) {
|
|
match self.active_view.which_font_size_used() {
|
|
WhichFontSize::AgentFont => {
|
|
if persist {
|
|
update_settings_file::<ThemeSettings>(
|
|
self.fs.clone(),
|
|
cx,
|
|
move |settings, cx| {
|
|
let agent_font_size =
|
|
ThemeSettings::get_global(cx).agent_font_size(cx) + delta;
|
|
let _ = settings
|
|
.agent_font_size
|
|
.insert(Some(theme::clamp_font_size(agent_font_size).into()));
|
|
},
|
|
);
|
|
} else {
|
|
theme::adjust_agent_font_size(cx, |size| size + delta);
|
|
}
|
|
}
|
|
WhichFontSize::BufferFont => {
|
|
// Prompt editor uses the buffer font size, so allow the action to propagate to the
|
|
// default handler that changes that font size.
|
|
cx.propagate();
|
|
}
|
|
WhichFontSize::None => {}
|
|
}
|
|
}
|
|
|
|
pub fn reset_font_size(
|
|
&mut self,
|
|
action: &ResetBufferFontSize,
|
|
_: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
if action.persist {
|
|
update_settings_file::<ThemeSettings>(self.fs.clone(), cx, move |settings, _| {
|
|
settings.agent_font_size = None;
|
|
});
|
|
} else {
|
|
theme::reset_agent_font_size(cx);
|
|
}
|
|
}
|
|
|
|
pub fn toggle_zoom(&mut self, _: &ToggleZoom, window: &mut Window, cx: &mut Context<Self>) {
|
|
if self.zoomed {
|
|
cx.emit(PanelEvent::ZoomOut);
|
|
} else {
|
|
if !self.focus_handle(cx).contains_focused(window, cx) {
|
|
cx.focus_self(window);
|
|
}
|
|
cx.emit(PanelEvent::ZoomIn);
|
|
}
|
|
}
|
|
|
|
pub fn open_agent_diff(
|
|
&mut self,
|
|
_: &OpenAgentDiff,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
match &self.active_view {
|
|
ActiveView::Thread { thread, .. } => {
|
|
let thread = thread.read(cx).thread().clone();
|
|
self.workspace
|
|
.update(cx, |workspace, cx| {
|
|
AgentDiffPane::deploy_in_workspace(
|
|
AgentDiffThread::Native(thread),
|
|
workspace,
|
|
window,
|
|
cx,
|
|
)
|
|
})
|
|
.log_err();
|
|
}
|
|
ActiveView::ExternalAgentThread { .. }
|
|
| ActiveView::TextThread { .. }
|
|
| ActiveView::History
|
|
| ActiveView::Configuration => {}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
let context_server_store = self.project.read(cx).context_server_store();
|
|
let tools = self.thread_store.read(cx).tools();
|
|
let fs = self.fs.clone();
|
|
|
|
self.set_active_view(ActiveView::Configuration, window, cx);
|
|
self.configuration = Some(cx.new(|cx| {
|
|
AgentConfiguration::new(
|
|
fs,
|
|
context_server_store,
|
|
tools,
|
|
self.language_registry.clone(),
|
|
self.workspace.clone(),
|
|
window,
|
|
cx,
|
|
)
|
|
}));
|
|
|
|
if let Some(configuration) = self.configuration.as_ref() {
|
|
self.configuration_subscription = Some(cx.subscribe_in(
|
|
configuration,
|
|
window,
|
|
Self::handle_agent_configuration_event,
|
|
));
|
|
|
|
configuration.focus_handle(cx).focus(window);
|
|
}
|
|
}
|
|
|
|
pub(crate) fn open_active_thread_as_markdown(
|
|
&mut self,
|
|
_: &OpenActiveThreadAsMarkdown,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let Some(workspace) = self.workspace.upgrade() else {
|
|
return;
|
|
};
|
|
|
|
match &self.active_view {
|
|
ActiveView::Thread { thread, .. } => {
|
|
active_thread::open_active_thread_as_markdown(
|
|
thread.read(cx).thread().clone(),
|
|
workspace,
|
|
window,
|
|
cx,
|
|
)
|
|
.detach_and_log_err(cx);
|
|
}
|
|
ActiveView::ExternalAgentThread { thread_view } => {
|
|
thread_view
|
|
.update(cx, |thread_view, cx| {
|
|
thread_view.open_thread_as_markdown(workspace, window, cx)
|
|
})
|
|
.detach_and_log_err(cx);
|
|
}
|
|
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {}
|
|
}
|
|
}
|
|
|
|
fn handle_agent_configuration_event(
|
|
&mut self,
|
|
_entity: &Entity<AgentConfiguration>,
|
|
event: &AssistantConfigurationEvent,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
match event {
|
|
AssistantConfigurationEvent::NewThread(provider) => {
|
|
if LanguageModelRegistry::read_global(cx)
|
|
.default_model()
|
|
.is_none_or(|model| model.provider.id() != provider.id())
|
|
&& let Some(model) = provider.default_model(cx)
|
|
{
|
|
update_settings_file::<AgentSettings>(
|
|
self.fs.clone(),
|
|
cx,
|
|
move |settings, _| settings.set_model(model),
|
|
);
|
|
}
|
|
|
|
self.new_thread(&NewThread::default(), window, cx);
|
|
if let Some((thread, model)) =
|
|
self.active_thread(cx).zip(provider.default_model(cx))
|
|
{
|
|
thread.update(cx, |thread, cx| {
|
|
thread.set_configured_model(
|
|
Some(ConfiguredModel {
|
|
provider: provider.clone(),
|
|
model,
|
|
}),
|
|
cx,
|
|
);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn active_thread(&self, cx: &App) -> Option<Entity<Thread>> {
|
|
match &self.active_view {
|
|
ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn delete_thread(
|
|
&mut self,
|
|
thread_id: &ThreadId,
|
|
cx: &mut Context<Self>,
|
|
) -> Task<Result<()>> {
|
|
self.thread_store
|
|
.update(cx, |this, cx| this.delete_thread(thread_id, cx))
|
|
}
|
|
|
|
fn continue_conversation(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
let ActiveView::Thread { thread, .. } = &self.active_view else {
|
|
return;
|
|
};
|
|
|
|
let thread_state = thread.read(cx).thread().read(cx);
|
|
if !thread_state.tool_use_limit_reached() {
|
|
return;
|
|
}
|
|
|
|
let model = thread_state.configured_model().map(|cm| cm.model.clone());
|
|
if let Some(model) = model {
|
|
thread.update(cx, |active_thread, cx| {
|
|
active_thread.thread().update(cx, |thread, cx| {
|
|
thread.insert_invisible_continue_message(cx);
|
|
thread.advance_prompt_id();
|
|
thread.send_to_model(
|
|
model,
|
|
CompletionIntent::UserPrompt,
|
|
Some(window.window_handle()),
|
|
cx,
|
|
);
|
|
});
|
|
});
|
|
} else {
|
|
log::warn!("No configured model available for continuation");
|
|
}
|
|
}
|
|
|
|
fn toggle_burn_mode(
|
|
&mut self,
|
|
_: &ToggleBurnMode,
|
|
_window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let ActiveView::Thread { thread, .. } = &self.active_view else {
|
|
return;
|
|
};
|
|
|
|
thread.update(cx, |active_thread, cx| {
|
|
active_thread.thread().update(cx, |thread, _cx| {
|
|
let current_mode = thread.completion_mode();
|
|
|
|
thread.set_completion_mode(match current_mode {
|
|
CompletionMode::Burn => CompletionMode::Normal,
|
|
CompletionMode::Normal => CompletionMode::Burn,
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
pub(crate) fn active_context_editor(&self) -> Option<Entity<TextThreadEditor>> {
|
|
match &self.active_view {
|
|
ActiveView::TextThread { context_editor, .. } => Some(context_editor.clone()),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn delete_context(
|
|
&mut self,
|
|
path: Arc<Path>,
|
|
cx: &mut Context<Self>,
|
|
) -> Task<Result<()>> {
|
|
self.context_store
|
|
.update(cx, |this, cx| this.delete_local_context(path, cx))
|
|
}
|
|
|
|
fn set_active_view(
|
|
&mut self,
|
|
new_view: ActiveView,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let current_is_history = matches!(self.active_view, ActiveView::History);
|
|
let new_is_history = matches!(new_view, ActiveView::History);
|
|
|
|
let current_is_config = matches!(self.active_view, ActiveView::Configuration);
|
|
let new_is_config = matches!(new_view, ActiveView::Configuration);
|
|
|
|
let current_is_special = current_is_history || current_is_config;
|
|
let new_is_special = new_is_history || new_is_config;
|
|
|
|
match &self.active_view {
|
|
ActiveView::Thread { thread, .. } => {
|
|
let thread = thread.read(cx);
|
|
if thread.is_empty() {
|
|
let id = thread.thread().read(cx).id().clone();
|
|
self.history_store.update(cx, |store, cx| {
|
|
store.remove_recently_opened_thread(id, cx);
|
|
});
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
match &new_view {
|
|
ActiveView::Thread { thread, .. } => self.history_store.update(cx, |store, cx| {
|
|
let id = thread.read(cx).thread().read(cx).id().clone();
|
|
store.push_recently_opened_entry(HistoryEntryId::Thread(id), cx);
|
|
}),
|
|
ActiveView::TextThread { context_editor, .. } => {
|
|
self.history_store.update(cx, |store, cx| {
|
|
if let Some(path) = context_editor.read(cx).context().read(cx).path() {
|
|
store.push_recently_opened_entry(HistoryEntryId::Context(path.clone()), cx)
|
|
}
|
|
})
|
|
}
|
|
ActiveView::ExternalAgentThread { .. } => {}
|
|
ActiveView::History | ActiveView::Configuration => {}
|
|
}
|
|
|
|
if current_is_special && !new_is_special {
|
|
self.active_view = new_view;
|
|
} else if !current_is_special && new_is_special {
|
|
self.previous_view = Some(std::mem::replace(&mut self.active_view, new_view));
|
|
} else {
|
|
if !new_is_special {
|
|
self.previous_view = None;
|
|
}
|
|
self.active_view = new_view;
|
|
}
|
|
|
|
self.focus_handle(cx).focus(window);
|
|
}
|
|
|
|
fn populate_recently_opened_menu_section_old(
|
|
mut menu: ContextMenu,
|
|
panel: Entity<Self>,
|
|
cx: &mut Context<ContextMenu>,
|
|
) -> ContextMenu {
|
|
let entries = panel
|
|
.read(cx)
|
|
.history_store
|
|
.read(cx)
|
|
.recently_opened_entries(cx);
|
|
|
|
if entries.is_empty() {
|
|
return menu;
|
|
}
|
|
|
|
menu = menu.header("Recently Opened");
|
|
|
|
for entry in entries {
|
|
let title = entry.title().clone();
|
|
let id = entry.id();
|
|
|
|
menu = menu.entry_with_end_slot_on_hover(
|
|
title,
|
|
None,
|
|
{
|
|
let panel = panel.downgrade();
|
|
let id = id.clone();
|
|
move |window, cx| {
|
|
let id = id.clone();
|
|
panel
|
|
.update(cx, move |this, cx| match id {
|
|
HistoryEntryId::Thread(id) => this
|
|
.open_thread_by_id(&id, window, cx)
|
|
.detach_and_log_err(cx),
|
|
HistoryEntryId::Context(path) => this
|
|
.open_saved_prompt_editor(path.clone(), window, cx)
|
|
.detach_and_log_err(cx),
|
|
})
|
|
.ok();
|
|
}
|
|
},
|
|
IconName::Close,
|
|
"Close Entry".into(),
|
|
{
|
|
let panel = panel.downgrade();
|
|
let id = id.clone();
|
|
move |_window, cx| {
|
|
panel
|
|
.update(cx, |this, cx| {
|
|
this.history_store.update(cx, |history_store, cx| {
|
|
history_store.remove_recently_opened_entry(&id, cx);
|
|
});
|
|
})
|
|
.ok();
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
menu = menu.separator();
|
|
|
|
menu
|
|
}
|
|
|
|
fn populate_recently_opened_menu_section_new(
|
|
mut menu: ContextMenu,
|
|
panel: Entity<Self>,
|
|
cx: &mut Context<ContextMenu>,
|
|
) -> ContextMenu {
|
|
let entries = panel
|
|
.read(cx)
|
|
.acp_history_store
|
|
.read(cx)
|
|
.recently_opened_entries(cx);
|
|
|
|
if entries.is_empty() {
|
|
return menu;
|
|
}
|
|
|
|
menu = menu.header("Recently Opened");
|
|
|
|
for entry in entries {
|
|
let title = entry.title().clone();
|
|
|
|
menu = menu.entry_with_end_slot_on_hover(
|
|
title,
|
|
None,
|
|
{
|
|
let panel = panel.downgrade();
|
|
let entry = entry.clone();
|
|
move |window, cx| {
|
|
let entry = entry.clone();
|
|
panel
|
|
.update(cx, move |this, cx| match &entry {
|
|
agent2::HistoryEntry::AcpThread(entry) => this.external_thread(
|
|
Some(ExternalAgent::NativeAgent),
|
|
Some(entry.clone()),
|
|
window,
|
|
cx,
|
|
),
|
|
agent2::HistoryEntry::TextThread(entry) => this
|
|
.open_saved_prompt_editor(entry.path.clone(), window, cx)
|
|
.detach_and_log_err(cx),
|
|
})
|
|
.ok();
|
|
}
|
|
},
|
|
IconName::Close,
|
|
"Close Entry".into(),
|
|
{
|
|
let panel = panel.downgrade();
|
|
let id = entry.id();
|
|
move |_window, cx| {
|
|
panel
|
|
.update(cx, |this, cx| {
|
|
this.acp_history_store.update(cx, |history_store, cx| {
|
|
history_store.remove_recently_opened_entry(&id, cx);
|
|
});
|
|
})
|
|
.ok();
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
menu = menu.separator();
|
|
|
|
menu
|
|
}
|
|
|
|
pub fn set_selected_agent(
|
|
&mut self,
|
|
agent: AgentType,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
if self.selected_agent != agent {
|
|
self.selected_agent = agent;
|
|
self.serialize(cx);
|
|
}
|
|
self.new_agent_thread(agent, window, cx);
|
|
}
|
|
|
|
pub fn selected_agent(&self) -> AgentType {
|
|
self.selected_agent
|
|
}
|
|
|
|
pub fn new_agent_thread(
|
|
&mut self,
|
|
agent: AgentType,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
match agent {
|
|
AgentType::Zed => {
|
|
window.dispatch_action(
|
|
NewThread {
|
|
from_thread_id: None,
|
|
}
|
|
.boxed_clone(),
|
|
cx,
|
|
);
|
|
}
|
|
AgentType::TextThread => {
|
|
window.dispatch_action(NewTextThread.boxed_clone(), cx);
|
|
}
|
|
AgentType::NativeAgent => {
|
|
self.external_thread(Some(crate::ExternalAgent::NativeAgent), None, window, cx)
|
|
}
|
|
AgentType::Gemini => {
|
|
self.external_thread(Some(crate::ExternalAgent::Gemini), None, window, cx)
|
|
}
|
|
AgentType::ClaudeCode => {
|
|
self.external_thread(Some(crate::ExternalAgent::ClaudeCode), None, window, cx)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Focusable for AgentPanel {
|
|
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
|
match &self.active_view {
|
|
ActiveView::Thread { message_editor, .. } => message_editor.focus_handle(cx),
|
|
ActiveView::ExternalAgentThread { thread_view, .. } => thread_view.focus_handle(cx),
|
|
ActiveView::History => {
|
|
if cx.has_flag::<feature_flags::AcpFeatureFlag>() {
|
|
self.acp_history.focus_handle(cx)
|
|
} else {
|
|
self.history.focus_handle(cx)
|
|
}
|
|
}
|
|
ActiveView::TextThread { context_editor, .. } => context_editor.focus_handle(cx),
|
|
ActiveView::Configuration => {
|
|
if let Some(configuration) = self.configuration.as_ref() {
|
|
configuration.focus_handle(cx)
|
|
} else {
|
|
cx.focus_handle()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn agent_panel_dock_position(cx: &App) -> DockPosition {
|
|
match AgentSettings::get_global(cx).dock {
|
|
AgentDockPosition::Left => DockPosition::Left,
|
|
AgentDockPosition::Bottom => DockPosition::Bottom,
|
|
AgentDockPosition::Right => DockPosition::Right,
|
|
}
|
|
}
|
|
|
|
impl EventEmitter<PanelEvent> for AgentPanel {}
|
|
|
|
impl Panel for AgentPanel {
|
|
fn persistent_name() -> &'static str {
|
|
"AgentPanel"
|
|
}
|
|
|
|
fn position(&self, _window: &Window, cx: &App) -> DockPosition {
|
|
agent_panel_dock_position(cx)
|
|
}
|
|
|
|
fn position_is_valid(&self, position: DockPosition) -> bool {
|
|
position != DockPosition::Bottom
|
|
}
|
|
|
|
fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
|
|
settings::update_settings_file::<AgentSettings>(self.fs.clone(), cx, move |settings, _| {
|
|
let dock = match position {
|
|
DockPosition::Left => AgentDockPosition::Left,
|
|
DockPosition::Bottom => AgentDockPosition::Bottom,
|
|
DockPosition::Right => AgentDockPosition::Right,
|
|
};
|
|
settings.set_dock(dock);
|
|
});
|
|
}
|
|
|
|
fn size(&self, window: &Window, cx: &App) -> Pixels {
|
|
let settings = AgentSettings::get_global(cx);
|
|
match self.position(window, cx) {
|
|
DockPosition::Left | DockPosition::Right => {
|
|
self.width.unwrap_or(settings.default_width)
|
|
}
|
|
DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
|
|
}
|
|
}
|
|
|
|
fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
|
|
match self.position(window, cx) {
|
|
DockPosition::Left | DockPosition::Right => self.width = size,
|
|
DockPosition::Bottom => self.height = size,
|
|
}
|
|
self.serialize(cx);
|
|
cx.notify();
|
|
}
|
|
|
|
fn set_active(&mut self, _active: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
|
|
|
|
fn remote_id() -> Option<proto::PanelId> {
|
|
Some(proto::PanelId::AssistantPanel)
|
|
}
|
|
|
|
fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
|
|
(self.enabled(cx) && AgentSettings::get_global(cx).button).then_some(IconName::ZedAssistant)
|
|
}
|
|
|
|
fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
|
|
Some("Agent Panel")
|
|
}
|
|
|
|
fn toggle_action(&self) -> Box<dyn Action> {
|
|
Box::new(ToggleFocus)
|
|
}
|
|
|
|
fn activation_priority(&self) -> u32 {
|
|
3
|
|
}
|
|
|
|
fn enabled(&self, cx: &App) -> bool {
|
|
DisableAiSettings::get_global(cx).disable_ai.not() && AgentSettings::get_global(cx).enabled
|
|
}
|
|
|
|
fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
|
|
self.zoomed
|
|
}
|
|
|
|
fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, cx: &mut Context<Self>) {
|
|
self.zoomed = zoomed;
|
|
cx.notify();
|
|
}
|
|
}
|
|
|
|
impl AgentPanel {
|
|
fn render_title_view(&self, _window: &mut Window, cx: &Context<Self>) -> AnyElement {
|
|
const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…";
|
|
|
|
let content = match &self.active_view {
|
|
ActiveView::Thread {
|
|
thread: active_thread,
|
|
change_title_editor,
|
|
..
|
|
} => {
|
|
let state = {
|
|
let active_thread = active_thread.read(cx);
|
|
if active_thread.is_empty() {
|
|
&ThreadSummary::Pending
|
|
} else {
|
|
active_thread.summary(cx)
|
|
}
|
|
};
|
|
|
|
match state {
|
|
ThreadSummary::Pending => Label::new(ThreadSummary::DEFAULT.clone())
|
|
.truncate()
|
|
.into_any_element(),
|
|
ThreadSummary::Generating => Label::new(LOADING_SUMMARY_PLACEHOLDER)
|
|
.truncate()
|
|
.into_any_element(),
|
|
ThreadSummary::Ready(_) => div()
|
|
.w_full()
|
|
.child(change_title_editor.clone())
|
|
.into_any_element(),
|
|
ThreadSummary::Error => h_flex()
|
|
.w_full()
|
|
.child(change_title_editor.clone())
|
|
.child(
|
|
IconButton::new("retry-summary-generation", IconName::RotateCcw)
|
|
.icon_size(IconSize::Small)
|
|
.on_click({
|
|
let active_thread = active_thread.clone();
|
|
move |_, _window, cx| {
|
|
active_thread.update(cx, |thread, cx| {
|
|
thread.regenerate_summary(cx);
|
|
});
|
|
}
|
|
})
|
|
.tooltip(move |_window, cx| {
|
|
cx.new(|_| {
|
|
Tooltip::new("Failed to generate title")
|
|
.meta("Click to try again")
|
|
})
|
|
.into()
|
|
}),
|
|
)
|
|
.into_any_element(),
|
|
}
|
|
}
|
|
ActiveView::ExternalAgentThread { thread_view } => {
|
|
Label::new(thread_view.read(cx).title(cx))
|
|
.truncate()
|
|
.into_any_element()
|
|
}
|
|
ActiveView::TextThread {
|
|
title_editor,
|
|
context_editor,
|
|
..
|
|
} => {
|
|
let summary = context_editor.read(cx).context().read(cx).summary();
|
|
|
|
match summary {
|
|
ContextSummary::Pending => Label::new(ContextSummary::DEFAULT)
|
|
.truncate()
|
|
.into_any_element(),
|
|
ContextSummary::Content(summary) => {
|
|
if summary.done {
|
|
div()
|
|
.w_full()
|
|
.child(title_editor.clone())
|
|
.into_any_element()
|
|
} else {
|
|
Label::new(LOADING_SUMMARY_PLACEHOLDER)
|
|
.truncate()
|
|
.into_any_element()
|
|
}
|
|
}
|
|
ContextSummary::Error => h_flex()
|
|
.w_full()
|
|
.child(title_editor.clone())
|
|
.child(
|
|
IconButton::new("retry-summary-generation", IconName::RotateCcw)
|
|
.icon_size(IconSize::Small)
|
|
.on_click({
|
|
let context_editor = context_editor.clone();
|
|
move |_, _window, cx| {
|
|
context_editor.update(cx, |context_editor, cx| {
|
|
context_editor.regenerate_summary(cx);
|
|
});
|
|
}
|
|
})
|
|
.tooltip(move |_window, cx| {
|
|
cx.new(|_| {
|
|
Tooltip::new("Failed to generate title")
|
|
.meta("Click to try again")
|
|
})
|
|
.into()
|
|
}),
|
|
)
|
|
.into_any_element(),
|
|
}
|
|
}
|
|
ActiveView::History => Label::new("History").truncate().into_any_element(),
|
|
ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(),
|
|
};
|
|
|
|
h_flex()
|
|
.key_context("TitleEditor")
|
|
.id("TitleEditor")
|
|
.flex_grow()
|
|
.w_full()
|
|
.max_w_full()
|
|
.overflow_x_scroll()
|
|
.child(content)
|
|
.into_any()
|
|
}
|
|
|
|
fn render_panel_options_menu(
|
|
&self,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> impl IntoElement {
|
|
let user_store = self.user_store.read(cx);
|
|
let usage = user_store.model_request_usage();
|
|
let account_url = zed_urls::account_url(cx);
|
|
|
|
let focus_handle = self.focus_handle(cx);
|
|
|
|
let full_screen_label = if self.is_zoomed(window, cx) {
|
|
"Disable Full Screen"
|
|
} else {
|
|
"Enable Full Screen"
|
|
};
|
|
|
|
PopoverMenu::new("agent-options-menu")
|
|
.trigger_with_tooltip(
|
|
IconButton::new("agent-options-menu", IconName::Ellipsis)
|
|
.icon_size(IconSize::Small),
|
|
{
|
|
let focus_handle = focus_handle.clone();
|
|
move |window, cx| {
|
|
Tooltip::for_action_in(
|
|
"Toggle Agent Menu",
|
|
&ToggleOptionsMenu,
|
|
&focus_handle,
|
|
window,
|
|
cx,
|
|
)
|
|
}
|
|
},
|
|
)
|
|
.anchor(Corner::TopRight)
|
|
.with_handle(self.agent_panel_menu_handle.clone())
|
|
.menu({
|
|
let focus_handle = focus_handle.clone();
|
|
move |window, cx| {
|
|
Some(ContextMenu::build(window, cx, |mut menu, _window, _| {
|
|
menu = menu.context(focus_handle.clone());
|
|
if let Some(usage) = usage {
|
|
menu = menu
|
|
.header_with_link("Prompt Usage", "Manage", account_url.clone())
|
|
.custom_entry(
|
|
move |_window, cx| {
|
|
let used_percentage = match usage.limit {
|
|
UsageLimit::Limited(limit) => {
|
|
Some((usage.amount as f32 / limit as f32) * 100.)
|
|
}
|
|
UsageLimit::Unlimited => None,
|
|
};
|
|
|
|
h_flex()
|
|
.flex_1()
|
|
.gap_1p5()
|
|
.children(used_percentage.map(|percent| {
|
|
ProgressBar::new("usage", percent, 100., cx)
|
|
}))
|
|
.child(
|
|
Label::new(match usage.limit {
|
|
UsageLimit::Limited(limit) => {
|
|
format!("{} / {limit}", usage.amount)
|
|
}
|
|
UsageLimit::Unlimited => {
|
|
format!("{} / ∞", usage.amount)
|
|
}
|
|
})
|
|
.size(LabelSize::Small)
|
|
.color(Color::Muted),
|
|
)
|
|
.into_any_element()
|
|
},
|
|
move |_, cx| cx.open_url(&zed_urls::account_url(cx)),
|
|
)
|
|
.separator()
|
|
}
|
|
|
|
menu = menu
|
|
.header("MCP Servers")
|
|
.action(
|
|
"View Server Extensions",
|
|
Box::new(zed_actions::Extensions {
|
|
category_filter: Some(
|
|
zed_actions::ExtensionCategoryFilter::ContextServers,
|
|
),
|
|
id: None,
|
|
}),
|
|
)
|
|
.action("Add Custom Server…", Box::new(AddContextServer))
|
|
.separator();
|
|
|
|
menu = menu
|
|
.action("Rules…", Box::new(OpenRulesLibrary::default()))
|
|
.action("Settings", Box::new(OpenSettings))
|
|
.separator()
|
|
.action(full_screen_label, Box::new(ToggleZoom));
|
|
menu
|
|
}))
|
|
}
|
|
})
|
|
}
|
|
|
|
fn render_recent_entries_menu(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let focus_handle = self.focus_handle(cx);
|
|
|
|
PopoverMenu::new("agent-nav-menu")
|
|
.trigger_with_tooltip(
|
|
IconButton::new("agent-nav-menu", IconName::MenuAlt).icon_size(IconSize::Small),
|
|
{
|
|
let focus_handle = focus_handle.clone();
|
|
move |window, cx| {
|
|
Tooltip::for_action_in(
|
|
"Toggle Recent Threads",
|
|
&ToggleNavigationMenu,
|
|
&focus_handle,
|
|
window,
|
|
cx,
|
|
)
|
|
}
|
|
},
|
|
)
|
|
.anchor(Corner::TopLeft)
|
|
.with_handle(self.assistant_navigation_menu_handle.clone())
|
|
.menu({
|
|
let menu = self.assistant_navigation_menu.clone();
|
|
move |window, cx| {
|
|
if let Some(menu) = menu.as_ref() {
|
|
menu.update(cx, |_, cx| {
|
|
cx.defer_in(window, |menu, window, cx| {
|
|
menu.rebuild(window, cx);
|
|
});
|
|
})
|
|
}
|
|
menu.clone()
|
|
}
|
|
})
|
|
}
|
|
|
|
fn render_toolbar_back_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let focus_handle = self.focus_handle(cx);
|
|
|
|
IconButton::new("go-back", IconName::ArrowLeft)
|
|
.icon_size(IconSize::Small)
|
|
.on_click(cx.listener(|this, _, window, cx| {
|
|
this.go_back(&workspace::GoBack, window, cx);
|
|
}))
|
|
.tooltip({
|
|
let focus_handle = focus_handle.clone();
|
|
|
|
move |window, cx| {
|
|
Tooltip::for_action_in("Go Back", &workspace::GoBack, &focus_handle, window, cx)
|
|
}
|
|
})
|
|
}
|
|
|
|
fn render_toolbar_old(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let focus_handle = self.focus_handle(cx);
|
|
|
|
let active_thread = match &self.active_view {
|
|
ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()),
|
|
ActiveView::ExternalAgentThread { .. }
|
|
| ActiveView::TextThread { .. }
|
|
| ActiveView::History
|
|
| ActiveView::Configuration => None,
|
|
};
|
|
|
|
let new_thread_menu = PopoverMenu::new("new_thread_menu")
|
|
.trigger_with_tooltip(
|
|
IconButton::new("new_thread_menu_btn", IconName::Plus).icon_size(IconSize::Small),
|
|
Tooltip::text("New Thread…"),
|
|
)
|
|
.anchor(Corner::TopRight)
|
|
.with_handle(self.new_thread_menu_handle.clone())
|
|
.menu({
|
|
let focus_handle = focus_handle.clone();
|
|
move |window, cx| {
|
|
let active_thread = active_thread.clone();
|
|
Some(ContextMenu::build(window, cx, |mut menu, _window, cx| {
|
|
menu = menu
|
|
.context(focus_handle.clone())
|
|
.when_some(active_thread, |this, active_thread| {
|
|
let thread = active_thread.read(cx);
|
|
|
|
if !thread.is_empty() {
|
|
let thread_id = thread.id().clone();
|
|
this.item(
|
|
ContextMenuEntry::new("New From Summary")
|
|
.icon(IconName::ThreadFromSummary)
|
|
.icon_color(Color::Muted)
|
|
.handler(move |window, cx| {
|
|
window.dispatch_action(
|
|
Box::new(NewThread {
|
|
from_thread_id: Some(thread_id.clone()),
|
|
}),
|
|
cx,
|
|
);
|
|
}),
|
|
)
|
|
} else {
|
|
this
|
|
}
|
|
})
|
|
.item(
|
|
ContextMenuEntry::new("New Thread")
|
|
.icon(IconName::Thread)
|
|
.icon_color(Color::Muted)
|
|
.action(NewThread::default().boxed_clone())
|
|
.handler(move |window, cx| {
|
|
window.dispatch_action(
|
|
NewThread::default().boxed_clone(),
|
|
cx,
|
|
);
|
|
}),
|
|
)
|
|
.item(
|
|
ContextMenuEntry::new("New Text Thread")
|
|
.icon(IconName::TextThread)
|
|
.icon_color(Color::Muted)
|
|
.action(NewTextThread.boxed_clone())
|
|
.handler(move |window, cx| {
|
|
window.dispatch_action(NewTextThread.boxed_clone(), cx);
|
|
}),
|
|
);
|
|
menu
|
|
}))
|
|
}
|
|
});
|
|
|
|
h_flex()
|
|
.id("assistant-toolbar")
|
|
.h(Tab::container_height(cx))
|
|
.max_w_full()
|
|
.flex_none()
|
|
.justify_between()
|
|
.gap_2()
|
|
.bg(cx.theme().colors().tab_bar_background)
|
|
.border_b_1()
|
|
.border_color(cx.theme().colors().border)
|
|
.child(
|
|
h_flex()
|
|
.size_full()
|
|
.pl_1()
|
|
.gap_1()
|
|
.child(match &self.active_view {
|
|
ActiveView::History | ActiveView::Configuration => div()
|
|
.pl(DynamicSpacing::Base04.rems(cx))
|
|
.child(self.render_toolbar_back_button(cx))
|
|
.into_any_element(),
|
|
_ => self.render_recent_entries_menu(cx).into_any_element(),
|
|
})
|
|
.child(self.render_title_view(window, cx)),
|
|
)
|
|
.child(
|
|
h_flex()
|
|
.h_full()
|
|
.gap_2()
|
|
.children(self.render_token_count(cx))
|
|
.child(
|
|
h_flex()
|
|
.h_full()
|
|
.gap(DynamicSpacing::Base02.rems(cx))
|
|
.px(DynamicSpacing::Base08.rems(cx))
|
|
.border_l_1()
|
|
.border_color(cx.theme().colors().border)
|
|
.child(new_thread_menu)
|
|
.child(self.render_panel_options_menu(window, cx)),
|
|
),
|
|
)
|
|
}
|
|
|
|
fn render_toolbar_new(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let focus_handle = self.focus_handle(cx);
|
|
|
|
let active_thread = match &self.active_view {
|
|
ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()),
|
|
ActiveView::ExternalAgentThread { .. }
|
|
| ActiveView::TextThread { .. }
|
|
| ActiveView::History
|
|
| ActiveView::Configuration => None,
|
|
};
|
|
|
|
let new_thread_menu = PopoverMenu::new("new_thread_menu")
|
|
.trigger_with_tooltip(
|
|
IconButton::new("new_thread_menu_btn", IconName::Plus).icon_size(IconSize::Small),
|
|
{
|
|
let focus_handle = focus_handle.clone();
|
|
move |window, cx| {
|
|
Tooltip::for_action_in(
|
|
"New…",
|
|
&ToggleNewThreadMenu,
|
|
&focus_handle,
|
|
window,
|
|
cx,
|
|
)
|
|
}
|
|
},
|
|
)
|
|
.anchor(Corner::TopLeft)
|
|
.with_handle(self.new_thread_menu_handle.clone())
|
|
.menu({
|
|
let focus_handle = focus_handle.clone();
|
|
let workspace = self.workspace.clone();
|
|
|
|
move |window, cx| {
|
|
let active_thread = active_thread.clone();
|
|
Some(ContextMenu::build(window, cx, |mut menu, _window, cx| {
|
|
menu = menu
|
|
.context(focus_handle.clone())
|
|
.header("Zed Agent")
|
|
.when_some(active_thread, |this, active_thread| {
|
|
let thread = active_thread.read(cx);
|
|
|
|
if !thread.is_empty() {
|
|
let thread_id = thread.id().clone();
|
|
this.item(
|
|
ContextMenuEntry::new("New From Summary")
|
|
.icon(IconName::ThreadFromSummary)
|
|
.icon_color(Color::Muted)
|
|
.handler(move |window, cx| {
|
|
window.dispatch_action(
|
|
Box::new(NewThread {
|
|
from_thread_id: Some(thread_id.clone()),
|
|
}),
|
|
cx,
|
|
);
|
|
}),
|
|
)
|
|
} else {
|
|
this
|
|
}
|
|
})
|
|
.item(
|
|
ContextMenuEntry::new("New Thread")
|
|
.icon(IconName::Thread)
|
|
.icon_color(Color::Muted)
|
|
.action(NewThread::default().boxed_clone())
|
|
.handler({
|
|
let workspace = workspace.clone();
|
|
move |window, cx| {
|
|
if let Some(workspace) = workspace.upgrade() {
|
|
workspace.update(cx, |workspace, cx| {
|
|
if let Some(panel) =
|
|
workspace.panel::<AgentPanel>(cx)
|
|
{
|
|
panel.update(cx, |panel, cx| {
|
|
panel.set_selected_agent(
|
|
AgentType::Zed,
|
|
window,
|
|
cx,
|
|
);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}),
|
|
)
|
|
.item(
|
|
ContextMenuEntry::new("New Text Thread")
|
|
.icon(IconName::TextThread)
|
|
.icon_color(Color::Muted)
|
|
.action(NewTextThread.boxed_clone())
|
|
.handler({
|
|
let workspace = workspace.clone();
|
|
move |window, cx| {
|
|
if let Some(workspace) = workspace.upgrade() {
|
|
workspace.update(cx, |workspace, cx| {
|
|
if let Some(panel) =
|
|
workspace.panel::<AgentPanel>(cx)
|
|
{
|
|
panel.update(cx, |panel, cx| {
|
|
panel.set_selected_agent(
|
|
AgentType::TextThread,
|
|
window,
|
|
cx,
|
|
);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}),
|
|
)
|
|
.item(
|
|
ContextMenuEntry::new("New Native Agent Thread")
|
|
.icon(IconName::ZedAssistant)
|
|
.icon_color(Color::Muted)
|
|
.handler({
|
|
let workspace = workspace.clone();
|
|
move |window, cx| {
|
|
if let Some(workspace) = workspace.upgrade() {
|
|
workspace.update(cx, |workspace, cx| {
|
|
if let Some(panel) =
|
|
workspace.panel::<AgentPanel>(cx)
|
|
{
|
|
panel.update(cx, |panel, cx| {
|
|
panel.set_selected_agent(
|
|
AgentType::NativeAgent,
|
|
window,
|
|
cx,
|
|
);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}),
|
|
)
|
|
.separator()
|
|
.header("External Agents")
|
|
.when(cx.has_flag::<AcpFeatureFlag>(), |menu| {
|
|
menu.item(
|
|
ContextMenuEntry::new("New Gemini Thread")
|
|
.icon(IconName::AiGemini)
|
|
.icon_color(Color::Muted)
|
|
.handler({
|
|
let workspace = workspace.clone();
|
|
move |window, cx| {
|
|
if let Some(workspace) = workspace.upgrade() {
|
|
workspace.update(cx, |workspace, cx| {
|
|
if let Some(panel) =
|
|
workspace.panel::<AgentPanel>(cx)
|
|
{
|
|
panel.update(cx, |panel, cx| {
|
|
panel.set_selected_agent(
|
|
AgentType::Gemini,
|
|
window,
|
|
cx,
|
|
);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}),
|
|
)
|
|
})
|
|
.when(cx.has_flag::<ClaudeCodeFeatureFlag>(), |menu| {
|
|
menu.item(
|
|
ContextMenuEntry::new("New Claude Code Thread")
|
|
.icon(IconName::AiClaude)
|
|
.icon_color(Color::Muted)
|
|
.handler({
|
|
let workspace = workspace.clone();
|
|
move |window, cx| {
|
|
if let Some(workspace) = workspace.upgrade() {
|
|
workspace.update(cx, |workspace, cx| {
|
|
if let Some(panel) =
|
|
workspace.panel::<AgentPanel>(cx)
|
|
{
|
|
panel.update(cx, |panel, cx| {
|
|
panel.set_selected_agent(
|
|
AgentType::ClaudeCode,
|
|
window,
|
|
cx,
|
|
);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}),
|
|
)
|
|
});
|
|
menu
|
|
}))
|
|
}
|
|
});
|
|
|
|
let selected_agent_label = self.selected_agent.label().into();
|
|
let selected_agent = div()
|
|
.id("selected_agent_icon")
|
|
.px(DynamicSpacing::Base02.rems(cx))
|
|
.child(Icon::new(self.selected_agent.icon()).color(Color::Muted))
|
|
.tooltip(move |window, cx| {
|
|
Tooltip::with_meta(
|
|
selected_agent_label.clone(),
|
|
None,
|
|
"Selected Agent",
|
|
window,
|
|
cx,
|
|
)
|
|
})
|
|
.into_any_element();
|
|
|
|
h_flex()
|
|
.id("agent-panel-toolbar")
|
|
.h(Tab::container_height(cx))
|
|
.max_w_full()
|
|
.flex_none()
|
|
.justify_between()
|
|
.gap_2()
|
|
.bg(cx.theme().colors().tab_bar_background)
|
|
.border_b_1()
|
|
.border_color(cx.theme().colors().border)
|
|
.child(
|
|
h_flex()
|
|
.size_full()
|
|
.gap(DynamicSpacing::Base04.rems(cx))
|
|
.pl(DynamicSpacing::Base04.rems(cx))
|
|
.child(match &self.active_view {
|
|
ActiveView::History | ActiveView::Configuration => {
|
|
self.render_toolbar_back_button(cx).into_any_element()
|
|
}
|
|
_ => h_flex()
|
|
.gap_1()
|
|
.child(self.render_recent_entries_menu(cx))
|
|
.child(Divider::vertical())
|
|
.child(selected_agent)
|
|
.into_any_element(),
|
|
})
|
|
.child(self.render_title_view(window, cx)),
|
|
)
|
|
.child(
|
|
h_flex()
|
|
.h_full()
|
|
.gap_2()
|
|
.children(self.render_token_count(cx))
|
|
.child(
|
|
h_flex()
|
|
.h_full()
|
|
.gap(DynamicSpacing::Base02.rems(cx))
|
|
.pl(DynamicSpacing::Base04.rems(cx))
|
|
.pr(DynamicSpacing::Base06.rems(cx))
|
|
.border_l_1()
|
|
.border_color(cx.theme().colors().border)
|
|
.child(new_thread_menu)
|
|
.child(self.render_panel_options_menu(window, cx)),
|
|
),
|
|
)
|
|
}
|
|
|
|
fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
if cx.has_flag::<feature_flags::AcpFeatureFlag>()
|
|
|| cx.has_flag::<feature_flags::ClaudeCodeFeatureFlag>()
|
|
{
|
|
self.render_toolbar_new(window, cx).into_any_element()
|
|
} else {
|
|
self.render_toolbar_old(window, cx).into_any_element()
|
|
}
|
|
}
|
|
|
|
fn render_token_count(&self, cx: &App) -> Option<AnyElement> {
|
|
match &self.active_view {
|
|
ActiveView::Thread {
|
|
thread,
|
|
message_editor,
|
|
..
|
|
} => {
|
|
let active_thread = thread.read(cx);
|
|
let message_editor = message_editor.read(cx);
|
|
|
|
let editor_empty = message_editor.is_editor_fully_empty(cx);
|
|
|
|
if active_thread.is_empty() && editor_empty {
|
|
return None;
|
|
}
|
|
|
|
let thread = active_thread.thread().read(cx);
|
|
let is_generating = thread.is_generating();
|
|
let conversation_token_usage = thread.total_token_usage()?;
|
|
|
|
let (total_token_usage, is_estimating) =
|
|
if let Some((editing_message_id, unsent_tokens)) =
|
|
active_thread.editing_message_id()
|
|
{
|
|
let combined = thread
|
|
.token_usage_up_to_message(editing_message_id)
|
|
.add(unsent_tokens);
|
|
|
|
(combined, unsent_tokens > 0)
|
|
} else {
|
|
let unsent_tokens =
|
|
message_editor.last_estimated_token_count().unwrap_or(0);
|
|
let combined = conversation_token_usage.add(unsent_tokens);
|
|
|
|
(combined, unsent_tokens > 0)
|
|
};
|
|
|
|
let is_waiting_to_update_token_count =
|
|
message_editor.is_waiting_to_update_token_count();
|
|
|
|
if total_token_usage.total == 0 {
|
|
return None;
|
|
}
|
|
|
|
let token_color = match total_token_usage.ratio() {
|
|
TokenUsageRatio::Normal if is_estimating => Color::Default,
|
|
TokenUsageRatio::Normal => Color::Muted,
|
|
TokenUsageRatio::Warning => Color::Warning,
|
|
TokenUsageRatio::Exceeded => Color::Error,
|
|
};
|
|
|
|
let token_count = h_flex()
|
|
.id("token-count")
|
|
.flex_shrink_0()
|
|
.gap_0p5()
|
|
.when(!is_generating && is_estimating, |parent| {
|
|
parent
|
|
.child(
|
|
h_flex()
|
|
.mr_1()
|
|
.size_2p5()
|
|
.justify_center()
|
|
.rounded_full()
|
|
.bg(cx.theme().colors().text.opacity(0.1))
|
|
.child(
|
|
div().size_1().rounded_full().bg(cx.theme().colors().text),
|
|
),
|
|
)
|
|
.tooltip(move |window, cx| {
|
|
Tooltip::with_meta(
|
|
"Estimated New Token Count",
|
|
None,
|
|
format!(
|
|
"Current Conversation Tokens: {}",
|
|
humanize_token_count(conversation_token_usage.total)
|
|
),
|
|
window,
|
|
cx,
|
|
)
|
|
})
|
|
})
|
|
.child(
|
|
Label::new(humanize_token_count(total_token_usage.total))
|
|
.size(LabelSize::Small)
|
|
.color(token_color)
|
|
.map(|label| {
|
|
if is_generating || is_waiting_to_update_token_count {
|
|
label
|
|
.with_animation(
|
|
"used-tokens-label",
|
|
Animation::new(Duration::from_secs(2))
|
|
.repeat()
|
|
.with_easing(pulsating_between(0.6, 1.)),
|
|
|label, delta| label.alpha(delta),
|
|
)
|
|
.into_any()
|
|
} else {
|
|
label.into_any_element()
|
|
}
|
|
}),
|
|
)
|
|
.child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
|
|
.child(
|
|
Label::new(humanize_token_count(total_token_usage.max))
|
|
.size(LabelSize::Small)
|
|
.color(Color::Muted),
|
|
)
|
|
.into_any();
|
|
|
|
Some(token_count)
|
|
}
|
|
ActiveView::TextThread { context_editor, .. } => {
|
|
let element = render_remaining_tokens(context_editor, cx)?;
|
|
|
|
Some(element.into_any_element())
|
|
}
|
|
ActiveView::ExternalAgentThread { .. }
|
|
| ActiveView::History
|
|
| ActiveView::Configuration => None,
|
|
}
|
|
}
|
|
|
|
fn should_render_trial_end_upsell(&self, cx: &mut Context<Self>) -> bool {
|
|
if TrialEndUpsell::dismissed() {
|
|
return false;
|
|
}
|
|
|
|
match &self.active_view {
|
|
ActiveView::Thread { thread, .. } => {
|
|
if thread
|
|
.read(cx)
|
|
.thread()
|
|
.read(cx)
|
|
.configured_model()
|
|
.is_some_and(|model| {
|
|
model.provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
|
|
})
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
ActiveView::TextThread { .. } => {
|
|
if LanguageModelRegistry::global(cx)
|
|
.read(cx)
|
|
.default_model()
|
|
.is_some_and(|model| {
|
|
model.provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
|
|
})
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
ActiveView::ExternalAgentThread { .. }
|
|
| ActiveView::History
|
|
| ActiveView::Configuration => return false,
|
|
}
|
|
|
|
let plan = self.user_store.read(cx).plan();
|
|
let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
|
|
|
|
matches!(plan, Some(Plan::ZedFree)) && has_previous_trial
|
|
}
|
|
|
|
fn should_render_onboarding(&self, cx: &mut Context<Self>) -> bool {
|
|
if OnboardingUpsell::dismissed() {
|
|
return false;
|
|
}
|
|
|
|
match &self.active_view {
|
|
ActiveView::History | ActiveView::Configuration => false,
|
|
ActiveView::ExternalAgentThread { thread_view, .. }
|
|
if thread_view.read(cx).as_native_thread(cx).is_none() =>
|
|
{
|
|
false
|
|
}
|
|
_ => {
|
|
let history_is_empty = self
|
|
.history_store
|
|
.update(cx, |store, cx| store.recent_entries(1, cx).is_empty());
|
|
|
|
let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx)
|
|
.providers()
|
|
.iter()
|
|
.any(|provider| {
|
|
provider.is_authenticated(cx)
|
|
&& provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
|
|
});
|
|
|
|
history_is_empty || !has_configured_non_zed_providers
|
|
}
|
|
}
|
|
}
|
|
|
|
fn render_onboarding(
|
|
&self,
|
|
_window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> Option<impl IntoElement> {
|
|
if !self.should_render_onboarding(cx) {
|
|
return None;
|
|
}
|
|
|
|
let thread_view = matches!(&self.active_view, ActiveView::Thread { .. });
|
|
let text_thread_view = matches!(&self.active_view, ActiveView::TextThread { .. });
|
|
|
|
Some(
|
|
div()
|
|
.when(thread_view, |this| {
|
|
this.size_full().bg(cx.theme().colors().panel_background)
|
|
})
|
|
.when(text_thread_view, |this| {
|
|
this.bg(cx.theme().colors().editor_background)
|
|
})
|
|
.child(self.onboarding.clone()),
|
|
)
|
|
}
|
|
|
|
fn render_backdrop(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
|
div()
|
|
.size_full()
|
|
.absolute()
|
|
.inset_0()
|
|
.bg(cx.theme().colors().panel_background)
|
|
.opacity(0.8)
|
|
.block_mouse_except_scroll()
|
|
}
|
|
|
|
fn render_trial_end_upsell(
|
|
&self,
|
|
_window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> Option<impl IntoElement> {
|
|
if !self.should_render_trial_end_upsell(cx) {
|
|
return None;
|
|
}
|
|
|
|
Some(
|
|
v_flex()
|
|
.absolute()
|
|
.inset_0()
|
|
.size_full()
|
|
.bg(cx.theme().colors().panel_background)
|
|
.opacity(0.85)
|
|
.block_mouse_except_scroll()
|
|
.child(EndTrialUpsell::new(Arc::new({
|
|
let this = cx.entity();
|
|
move |_, cx| {
|
|
this.update(cx, |_this, cx| {
|
|
TrialEndUpsell::set_dismissed(true, cx);
|
|
cx.notify();
|
|
});
|
|
}
|
|
}))),
|
|
)
|
|
}
|
|
|
|
fn render_empty_state_section_header(
|
|
&self,
|
|
label: impl Into<SharedString>,
|
|
action_slot: Option<AnyElement>,
|
|
cx: &mut Context<Self>,
|
|
) -> impl IntoElement {
|
|
div().pl_1().pr_1p5().child(
|
|
h_flex()
|
|
.mt_2()
|
|
.pl_1p5()
|
|
.pb_1()
|
|
.w_full()
|
|
.justify_between()
|
|
.border_b_1()
|
|
.border_color(cx.theme().colors().border_variant)
|
|
.child(
|
|
Label::new(label.into())
|
|
.size(LabelSize::Small)
|
|
.color(Color::Muted),
|
|
)
|
|
.children(action_slot),
|
|
)
|
|
}
|
|
|
|
fn render_thread_empty_state(
|
|
&self,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> impl IntoElement {
|
|
let recent_history = self
|
|
.history_store
|
|
.update(cx, |this, cx| this.recent_entries(6, cx));
|
|
|
|
let model_registry = LanguageModelRegistry::read_global(cx);
|
|
|
|
let configuration_error =
|
|
model_registry.configuration_error(model_registry.default_model(), cx);
|
|
|
|
let no_error = configuration_error.is_none();
|
|
let focus_handle = self.focus_handle(cx);
|
|
|
|
v_flex()
|
|
.size_full()
|
|
.bg(cx.theme().colors().panel_background)
|
|
.when(recent_history.is_empty(), |this| {
|
|
this.child(
|
|
v_flex()
|
|
.size_full()
|
|
.mx_auto()
|
|
.justify_center()
|
|
.items_center()
|
|
.gap_1()
|
|
.child(h_flex().child(Headline::new("Welcome to the Agent Panel")))
|
|
.when(no_error, |parent| {
|
|
parent
|
|
.child(h_flex().child(
|
|
Label::new("Ask and build anything.").color(Color::Muted),
|
|
))
|
|
.child(
|
|
v_flex()
|
|
.mt_2()
|
|
.gap_1()
|
|
.max_w_48()
|
|
.child(
|
|
Button::new("context", "Add Context")
|
|
.label_size(LabelSize::Small)
|
|
.icon(IconName::FileCode)
|
|
.icon_position(IconPosition::Start)
|
|
.icon_size(IconSize::Small)
|
|
.icon_color(Color::Muted)
|
|
.full_width()
|
|
.key_binding(KeyBinding::for_action_in(
|
|
&ToggleContextPicker,
|
|
&focus_handle,
|
|
window,
|
|
cx,
|
|
))
|
|
.on_click(|_event, window, cx| {
|
|
window.dispatch_action(
|
|
ToggleContextPicker.boxed_clone(),
|
|
cx,
|
|
)
|
|
}),
|
|
)
|
|
.child(
|
|
Button::new("mode", "Switch Model")
|
|
.label_size(LabelSize::Small)
|
|
.icon(IconName::DatabaseZap)
|
|
.icon_position(IconPosition::Start)
|
|
.icon_size(IconSize::Small)
|
|
.icon_color(Color::Muted)
|
|
.full_width()
|
|
.key_binding(KeyBinding::for_action_in(
|
|
&ToggleModelSelector,
|
|
&focus_handle,
|
|
window,
|
|
cx,
|
|
))
|
|
.on_click(|_event, window, cx| {
|
|
window.dispatch_action(
|
|
ToggleModelSelector.boxed_clone(),
|
|
cx,
|
|
)
|
|
}),
|
|
)
|
|
.child(
|
|
Button::new("settings", "View Settings")
|
|
.label_size(LabelSize::Small)
|
|
.icon(IconName::Settings)
|
|
.icon_position(IconPosition::Start)
|
|
.icon_size(IconSize::Small)
|
|
.icon_color(Color::Muted)
|
|
.full_width()
|
|
.key_binding(KeyBinding::for_action_in(
|
|
&OpenSettings,
|
|
&focus_handle,
|
|
window,
|
|
cx,
|
|
))
|
|
.on_click(|_event, window, cx| {
|
|
window.dispatch_action(
|
|
OpenSettings.boxed_clone(),
|
|
cx,
|
|
)
|
|
}),
|
|
),
|
|
)
|
|
}),
|
|
)
|
|
})
|
|
.when(!recent_history.is_empty(), |parent| {
|
|
parent
|
|
.overflow_hidden()
|
|
.justify_end()
|
|
.gap_1()
|
|
.child(
|
|
self.render_empty_state_section_header(
|
|
"Recent",
|
|
Some(
|
|
Button::new("view-history", "View All")
|
|
.style(ButtonStyle::Subtle)
|
|
.label_size(LabelSize::Small)
|
|
.key_binding(
|
|
KeyBinding::for_action_in(
|
|
&OpenHistory,
|
|
&self.focus_handle(cx),
|
|
window,
|
|
cx,
|
|
)
|
|
.map(|kb| kb.size(rems_from_px(12.))),
|
|
)
|
|
.on_click(move |_event, window, cx| {
|
|
window.dispatch_action(OpenHistory.boxed_clone(), cx);
|
|
})
|
|
.into_any_element(),
|
|
),
|
|
cx,
|
|
),
|
|
)
|
|
.child(
|
|
v_flex().p_1().pr_1p5().gap_1().children(
|
|
recent_history
|
|
.into_iter()
|
|
.enumerate()
|
|
.map(|(index, entry)| {
|
|
// TODO: Add keyboard navigation.
|
|
let is_hovered =
|
|
self.hovered_recent_history_item == Some(index);
|
|
HistoryEntryElement::new(entry.clone(), cx.entity().downgrade())
|
|
.hovered(is_hovered)
|
|
.on_hover(cx.listener(
|
|
move |this, is_hovered, _window, cx| {
|
|
if *is_hovered {
|
|
this.hovered_recent_history_item = Some(index);
|
|
} else if this.hovered_recent_history_item
|
|
== Some(index)
|
|
{
|
|
this.hovered_recent_history_item = None;
|
|
}
|
|
cx.notify();
|
|
},
|
|
))
|
|
.into_any_element()
|
|
}),
|
|
),
|
|
)
|
|
})
|
|
.when_some(configuration_error.as_ref(), |this, err| {
|
|
this.child(self.render_configuration_error(false, err, &focus_handle, window, cx))
|
|
})
|
|
}
|
|
|
|
fn render_configuration_error(
|
|
&self,
|
|
border_bottom: bool,
|
|
configuration_error: &ConfigurationError,
|
|
focus_handle: &FocusHandle,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) -> impl IntoElement {
|
|
let zed_provider_configured = AgentSettings::get_global(cx)
|
|
.default_model
|
|
.as_ref()
|
|
.is_some_and(|selection| selection.provider.0.as_str() == "zed.dev");
|
|
|
|
let callout = if zed_provider_configured {
|
|
Callout::new()
|
|
.icon(IconName::Warning)
|
|
.severity(Severity::Warning)
|
|
.when(border_bottom, |this| {
|
|
this.border_position(ui::BorderPosition::Bottom)
|
|
})
|
|
.title("Sign in to continue using Zed as your LLM provider.")
|
|
.actions_slot(
|
|
Button::new("sign_in", "Sign In")
|
|
.style(ButtonStyle::Tinted(ui::TintColor::Warning))
|
|
.label_size(LabelSize::Small)
|
|
.on_click({
|
|
let workspace = self.workspace.clone();
|
|
move |_, _, cx| {
|
|
let Ok(client) =
|
|
workspace.update(cx, |workspace, _| workspace.client().clone())
|
|
else {
|
|
return;
|
|
};
|
|
|
|
cx.spawn(async move |cx| {
|
|
client.sign_in_with_optional_connect(true, cx).await
|
|
})
|
|
.detach_and_log_err(cx);
|
|
}
|
|
}),
|
|
)
|
|
} else {
|
|
Callout::new()
|
|
.icon(IconName::Warning)
|
|
.severity(Severity::Warning)
|
|
.when(border_bottom, |this| {
|
|
this.border_position(ui::BorderPosition::Bottom)
|
|
})
|
|
.title(configuration_error.to_string())
|
|
.actions_slot(
|
|
Button::new("settings", "Configure")
|
|
.style(ButtonStyle::Tinted(ui::TintColor::Warning))
|
|
.label_size(LabelSize::Small)
|
|
.key_binding(
|
|
KeyBinding::for_action_in(&OpenSettings, focus_handle, window, cx)
|
|
.map(|kb| kb.size(rems_from_px(12.))),
|
|
)
|
|
.on_click(|_event, window, cx| {
|
|
window.dispatch_action(OpenSettings.boxed_clone(), cx)
|
|
}),
|
|
)
|
|
};
|
|
|
|
match configuration_error {
|
|
ConfigurationError::ModelNotFound
|
|
| ConfigurationError::ProviderNotAuthenticated(_)
|
|
| ConfigurationError::NoProvider => callout.into_any_element(),
|
|
ConfigurationError::ProviderPendingTermsAcceptance(provider) => {
|
|
Banner::new()
|
|
.severity(Severity::Warning)
|
|
.child(h_flex().w_full().children(
|
|
provider.render_accept_terms(
|
|
LanguageModelProviderTosView::ThreadEmptyState,
|
|
cx,
|
|
),
|
|
))
|
|
.into_any_element()
|
|
}
|
|
}
|
|
}
|
|
|
|
fn render_tool_use_limit_reached(
|
|
&self,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> Option<AnyElement> {
|
|
let active_thread = match &self.active_view {
|
|
ActiveView::Thread { thread, .. } => thread,
|
|
ActiveView::ExternalAgentThread { .. } => {
|
|
return None;
|
|
}
|
|
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {
|
|
return None;
|
|
}
|
|
};
|
|
|
|
let thread = active_thread.read(cx).thread().read(cx);
|
|
|
|
let tool_use_limit_reached = thread.tool_use_limit_reached();
|
|
if !tool_use_limit_reached {
|
|
return None;
|
|
}
|
|
|
|
let model = thread.configured_model()?.model;
|
|
|
|
let focus_handle = self.focus_handle(cx);
|
|
|
|
let banner = Banner::new()
|
|
.severity(Severity::Info)
|
|
.child(Label::new("Consecutive tool use limit reached.").size(LabelSize::Small))
|
|
.action_slot(
|
|
h_flex()
|
|
.gap_1()
|
|
.child(
|
|
Button::new("continue-conversation", "Continue")
|
|
.layer(ElevationIndex::ModalSurface)
|
|
.label_size(LabelSize::Small)
|
|
.key_binding(
|
|
KeyBinding::for_action_in(
|
|
&ContinueThread,
|
|
&focus_handle,
|
|
window,
|
|
cx,
|
|
)
|
|
.map(|kb| kb.size(rems_from_px(10.))),
|
|
)
|
|
.on_click(cx.listener(|this, _, window, cx| {
|
|
this.continue_conversation(window, cx);
|
|
})),
|
|
)
|
|
.when(model.supports_burn_mode(), |this| {
|
|
this.child(
|
|
Button::new("continue-burn-mode", "Continue with Burn Mode")
|
|
.style(ButtonStyle::Filled)
|
|
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
|
.layer(ElevationIndex::ModalSurface)
|
|
.label_size(LabelSize::Small)
|
|
.key_binding(
|
|
KeyBinding::for_action_in(
|
|
&ContinueWithBurnMode,
|
|
&focus_handle,
|
|
window,
|
|
cx,
|
|
)
|
|
.map(|kb| kb.size(rems_from_px(10.))),
|
|
)
|
|
.tooltip(Tooltip::text("Enable Burn Mode for unlimited tool use."))
|
|
.on_click({
|
|
let active_thread = active_thread.clone();
|
|
cx.listener(move |this, _, window, cx| {
|
|
active_thread.update(cx, |active_thread, cx| {
|
|
active_thread.thread().update(cx, |thread, _cx| {
|
|
thread.set_completion_mode(CompletionMode::Burn);
|
|
});
|
|
});
|
|
this.continue_conversation(window, cx);
|
|
})
|
|
}),
|
|
)
|
|
}),
|
|
);
|
|
|
|
Some(div().px_2().pb_2().child(banner).into_any_element())
|
|
}
|
|
|
|
fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
|
|
let message = message.into();
|
|
|
|
IconButton::new("copy", IconName::Copy)
|
|
.icon_size(IconSize::Small)
|
|
.icon_color(Color::Muted)
|
|
.tooltip(Tooltip::text("Copy Error Message"))
|
|
.on_click(move |_, _, cx| {
|
|
cx.write_to_clipboard(ClipboardItem::new_string(message.clone()))
|
|
})
|
|
}
|
|
|
|
fn dismiss_error_button(
|
|
&self,
|
|
thread: &Entity<ActiveThread>,
|
|
cx: &mut Context<Self>,
|
|
) -> impl IntoElement {
|
|
IconButton::new("dismiss", IconName::Close)
|
|
.icon_size(IconSize::Small)
|
|
.icon_color(Color::Muted)
|
|
.tooltip(Tooltip::text("Dismiss Error"))
|
|
.on_click(cx.listener({
|
|
let thread = thread.clone();
|
|
move |_, _, _, cx| {
|
|
thread.update(cx, |this, _cx| {
|
|
this.clear_last_error();
|
|
});
|
|
|
|
cx.notify();
|
|
}
|
|
}))
|
|
}
|
|
|
|
fn upgrade_button(
|
|
&self,
|
|
thread: &Entity<ActiveThread>,
|
|
cx: &mut Context<Self>,
|
|
) -> impl IntoElement {
|
|
Button::new("upgrade", "Upgrade")
|
|
.label_size(LabelSize::Small)
|
|
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
|
.on_click(cx.listener({
|
|
let thread = thread.clone();
|
|
move |_, _, _, cx| {
|
|
thread.update(cx, |this, _cx| {
|
|
this.clear_last_error();
|
|
});
|
|
|
|
cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx));
|
|
cx.notify();
|
|
}
|
|
}))
|
|
}
|
|
|
|
fn render_payment_required_error(
|
|
&self,
|
|
thread: &Entity<ActiveThread>,
|
|
cx: &mut Context<Self>,
|
|
) -> AnyElement {
|
|
const ERROR_MESSAGE: &str =
|
|
"You reached your free usage limit. Upgrade to Zed Pro for more prompts.";
|
|
|
|
Callout::new()
|
|
.severity(Severity::Error)
|
|
.icon(IconName::XCircle)
|
|
.title("Free Usage Exceeded")
|
|
.description(ERROR_MESSAGE)
|
|
.actions_slot(
|
|
h_flex()
|
|
.gap_0p5()
|
|
.child(self.upgrade_button(thread, cx))
|
|
.child(self.create_copy_button(ERROR_MESSAGE)),
|
|
)
|
|
.dismiss_action(self.dismiss_error_button(thread, cx))
|
|
.into_any_element()
|
|
}
|
|
|
|
fn render_model_request_limit_reached_error(
|
|
&self,
|
|
plan: Plan,
|
|
thread: &Entity<ActiveThread>,
|
|
cx: &mut Context<Self>,
|
|
) -> AnyElement {
|
|
let error_message = match plan {
|
|
Plan::ZedPro => "Upgrade to usage-based billing for more prompts.",
|
|
Plan::ZedProTrial | Plan::ZedFree => "Upgrade to Zed Pro for more prompts.",
|
|
};
|
|
|
|
Callout::new()
|
|
.severity(Severity::Error)
|
|
.title("Model Prompt Limit Reached")
|
|
.description(error_message)
|
|
.actions_slot(
|
|
h_flex()
|
|
.gap_0p5()
|
|
.child(self.upgrade_button(thread, cx))
|
|
.child(self.create_copy_button(error_message)),
|
|
)
|
|
.dismiss_action(self.dismiss_error_button(thread, cx))
|
|
.into_any_element()
|
|
}
|
|
|
|
fn render_retry_button(&self, thread: &Entity<ActiveThread>) -> AnyElement {
|
|
Button::new("retry", "Retry")
|
|
.icon(IconName::RotateCw)
|
|
.icon_position(IconPosition::Start)
|
|
.icon_size(IconSize::Small)
|
|
.label_size(LabelSize::Small)
|
|
.on_click({
|
|
let thread = thread.clone();
|
|
move |_, window, cx| {
|
|
thread.update(cx, |thread, cx| {
|
|
thread.clear_last_error();
|
|
thread.thread().update(cx, |thread, cx| {
|
|
thread.retry_last_completion(Some(window.window_handle()), cx);
|
|
});
|
|
});
|
|
}
|
|
})
|
|
.into_any_element()
|
|
}
|
|
|
|
fn render_error_message(
|
|
&self,
|
|
header: SharedString,
|
|
message: SharedString,
|
|
thread: &Entity<ActiveThread>,
|
|
cx: &mut Context<Self>,
|
|
) -> AnyElement {
|
|
let message_with_header = format!("{}\n{}", header, message);
|
|
|
|
Callout::new()
|
|
.severity(Severity::Error)
|
|
.icon(IconName::XCircle)
|
|
.title(header)
|
|
.description(message.clone())
|
|
.actions_slot(
|
|
h_flex()
|
|
.gap_0p5()
|
|
.child(self.render_retry_button(thread))
|
|
.child(self.create_copy_button(message_with_header)),
|
|
)
|
|
.dismiss_action(self.dismiss_error_button(thread, cx))
|
|
.into_any_element()
|
|
}
|
|
|
|
fn render_retryable_error(
|
|
&self,
|
|
message: SharedString,
|
|
can_enable_burn_mode: bool,
|
|
thread: &Entity<ActiveThread>,
|
|
) -> AnyElement {
|
|
Callout::new()
|
|
.severity(Severity::Error)
|
|
.title("Error")
|
|
.description(message.clone())
|
|
.actions_slot(
|
|
h_flex()
|
|
.gap_0p5()
|
|
.when(can_enable_burn_mode, |this| {
|
|
this.child(
|
|
Button::new("enable_burn_retry", "Enable Burn Mode and Retry")
|
|
.icon(IconName::ZedBurnMode)
|
|
.icon_position(IconPosition::Start)
|
|
.icon_size(IconSize::Small)
|
|
.label_size(LabelSize::Small)
|
|
.on_click({
|
|
let thread = thread.clone();
|
|
move |_, window, cx| {
|
|
thread.update(cx, |thread, cx| {
|
|
thread.clear_last_error();
|
|
thread.thread().update(cx, |thread, cx| {
|
|
thread.enable_burn_mode_and_retry(
|
|
Some(window.window_handle()),
|
|
cx,
|
|
);
|
|
});
|
|
});
|
|
}
|
|
}),
|
|
)
|
|
})
|
|
.child(self.render_retry_button(thread)),
|
|
)
|
|
.into_any_element()
|
|
}
|
|
|
|
fn render_prompt_editor(
|
|
&self,
|
|
context_editor: &Entity<TextThreadEditor>,
|
|
buffer_search_bar: &Entity<BufferSearchBar>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> Div {
|
|
let mut registrar = buffer_search::DivRegistrar::new(
|
|
|this, _, _cx| match &this.active_view {
|
|
ActiveView::TextThread {
|
|
buffer_search_bar, ..
|
|
} => Some(buffer_search_bar.clone()),
|
|
_ => None,
|
|
},
|
|
cx,
|
|
);
|
|
BufferSearchBar::register(&mut registrar);
|
|
registrar
|
|
.into_div()
|
|
.size_full()
|
|
.relative()
|
|
.map(|parent| {
|
|
buffer_search_bar.update(cx, |buffer_search_bar, cx| {
|
|
if buffer_search_bar.is_dismissed() {
|
|
return parent;
|
|
}
|
|
parent.child(
|
|
div()
|
|
.p(DynamicSpacing::Base08.rems(cx))
|
|
.border_b_1()
|
|
.border_color(cx.theme().colors().border_variant)
|
|
.bg(cx.theme().colors().editor_background)
|
|
.child(buffer_search_bar.render(window, cx)),
|
|
)
|
|
})
|
|
})
|
|
.child(context_editor.clone())
|
|
.child(self.render_drag_target(cx))
|
|
}
|
|
|
|
fn render_drag_target(&self, cx: &Context<Self>) -> Div {
|
|
let is_local = self.project.read(cx).is_local();
|
|
div()
|
|
.invisible()
|
|
.absolute()
|
|
.top_0()
|
|
.right_0()
|
|
.bottom_0()
|
|
.left_0()
|
|
.bg(cx.theme().colors().drop_target_background)
|
|
.drag_over::<DraggedTab>(|this, _, _, _| this.visible())
|
|
.drag_over::<DraggedSelection>(|this, _, _, _| this.visible())
|
|
.when(is_local, |this| {
|
|
this.drag_over::<ExternalPaths>(|this, _, _, _| this.visible())
|
|
})
|
|
.on_drop(cx.listener(move |this, tab: &DraggedTab, window, cx| {
|
|
let item = tab.pane.read(cx).item_for_index(tab.ix);
|
|
let project_paths = item
|
|
.and_then(|item| item.project_path(cx))
|
|
.into_iter()
|
|
.collect::<Vec<_>>();
|
|
this.handle_drop(project_paths, vec![], window, cx);
|
|
}))
|
|
.on_drop(
|
|
cx.listener(move |this, selection: &DraggedSelection, window, cx| {
|
|
let project_paths = selection
|
|
.items()
|
|
.filter_map(|item| this.project.read(cx).path_for_entry(item.entry_id, cx))
|
|
.collect::<Vec<_>>();
|
|
this.handle_drop(project_paths, vec![], window, cx);
|
|
}),
|
|
)
|
|
.on_drop(cx.listener(move |this, paths: &ExternalPaths, window, cx| {
|
|
let tasks = paths
|
|
.paths()
|
|
.into_iter()
|
|
.map(|path| {
|
|
Workspace::project_path_for_path(this.project.clone(), path, false, cx)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
cx.spawn_in(window, async move |this, cx| {
|
|
let mut paths = vec![];
|
|
let mut added_worktrees = vec![];
|
|
let opened_paths = futures::future::join_all(tasks).await;
|
|
for entry in opened_paths {
|
|
if let Some((worktree, project_path)) = entry.log_err() {
|
|
added_worktrees.push(worktree);
|
|
paths.push(project_path);
|
|
}
|
|
}
|
|
this.update_in(cx, |this, window, cx| {
|
|
this.handle_drop(paths, added_worktrees, window, cx);
|
|
})
|
|
.ok();
|
|
})
|
|
.detach();
|
|
}))
|
|
}
|
|
|
|
fn handle_drop(
|
|
&mut self,
|
|
paths: Vec<ProjectPath>,
|
|
added_worktrees: Vec<Entity<Worktree>>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
match &self.active_view {
|
|
ActiveView::Thread { thread, .. } => {
|
|
let context_store = thread.read(cx).context_store().clone();
|
|
context_store.update(cx, move |context_store, cx| {
|
|
let mut tasks = Vec::new();
|
|
for project_path in &paths {
|
|
tasks.push(context_store.add_file_from_path(
|
|
project_path.clone(),
|
|
false,
|
|
cx,
|
|
));
|
|
}
|
|
cx.background_spawn(async move {
|
|
futures::future::join_all(tasks).await;
|
|
// Need to hold onto the worktrees until they have already been used when
|
|
// opening the buffers.
|
|
drop(added_worktrees);
|
|
})
|
|
.detach();
|
|
});
|
|
}
|
|
ActiveView::ExternalAgentThread { thread_view } => {
|
|
thread_view.update(cx, |thread_view, cx| {
|
|
thread_view.insert_dragged_files(paths, added_worktrees, window, cx);
|
|
});
|
|
}
|
|
ActiveView::TextThread { context_editor, .. } => {
|
|
context_editor.update(cx, |context_editor, cx| {
|
|
TextThreadEditor::insert_dragged_files(
|
|
context_editor,
|
|
paths,
|
|
added_worktrees,
|
|
window,
|
|
cx,
|
|
);
|
|
});
|
|
}
|
|
ActiveView::History | ActiveView::Configuration => {}
|
|
}
|
|
}
|
|
|
|
fn key_context(&self) -> KeyContext {
|
|
let mut key_context = KeyContext::new_with_defaults();
|
|
key_context.add("AgentPanel");
|
|
match &self.active_view {
|
|
ActiveView::ExternalAgentThread { .. } => key_context.add("external_agent_thread"),
|
|
ActiveView::TextThread { .. } => key_context.add("prompt_editor"),
|
|
ActiveView::Thread { .. } | ActiveView::History | ActiveView::Configuration => {}
|
|
}
|
|
key_context
|
|
}
|
|
}
|
|
|
|
impl Render for AgentPanel {
|
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
// WARNING: Changes to this element hierarchy can have
|
|
// non-obvious implications to the layout of children.
|
|
//
|
|
// If you need to change it, please confirm:
|
|
// - The message editor expands (cmd-option-esc) correctly
|
|
// - When expanded, the buttons at the bottom of the panel are displayed correctly
|
|
// - Font size works as expected and can be changed with cmd-+/cmd-
|
|
// - Scrolling in all views works as expected
|
|
// - Files can be dropped into the panel
|
|
let content = v_flex()
|
|
.relative()
|
|
.size_full()
|
|
.justify_between()
|
|
.key_context(self.key_context())
|
|
.on_action(cx.listener(Self::cancel))
|
|
.on_action(cx.listener(|this, action: &NewThread, window, cx| {
|
|
this.new_thread(action, window, cx);
|
|
}))
|
|
.on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
|
|
this.open_history(window, cx);
|
|
}))
|
|
.on_action(cx.listener(|this, _: &OpenSettings, window, cx| {
|
|
this.open_configuration(window, cx);
|
|
}))
|
|
.on_action(cx.listener(Self::open_active_thread_as_markdown))
|
|
.on_action(cx.listener(Self::deploy_rules_library))
|
|
.on_action(cx.listener(Self::open_agent_diff))
|
|
.on_action(cx.listener(Self::go_back))
|
|
.on_action(cx.listener(Self::toggle_navigation_menu))
|
|
.on_action(cx.listener(Self::toggle_options_menu))
|
|
.on_action(cx.listener(Self::increase_font_size))
|
|
.on_action(cx.listener(Self::decrease_font_size))
|
|
.on_action(cx.listener(Self::reset_font_size))
|
|
.on_action(cx.listener(Self::toggle_zoom))
|
|
.on_action(cx.listener(|this, _: &ContinueThread, window, cx| {
|
|
this.continue_conversation(window, cx);
|
|
}))
|
|
.on_action(cx.listener(|this, _: &ContinueWithBurnMode, window, cx| {
|
|
match &this.active_view {
|
|
ActiveView::Thread { thread, .. } => {
|
|
thread.update(cx, |active_thread, cx| {
|
|
active_thread.thread().update(cx, |thread, _cx| {
|
|
thread.set_completion_mode(CompletionMode::Burn);
|
|
});
|
|
});
|
|
this.continue_conversation(window, cx);
|
|
}
|
|
ActiveView::ExternalAgentThread { .. } => {}
|
|
ActiveView::TextThread { .. }
|
|
| ActiveView::History
|
|
| ActiveView::Configuration => {}
|
|
}
|
|
}))
|
|
.on_action(cx.listener(Self::toggle_burn_mode))
|
|
.child(self.render_toolbar(window, cx))
|
|
.children(self.render_onboarding(window, cx))
|
|
.map(|parent| match &self.active_view {
|
|
ActiveView::Thread {
|
|
thread,
|
|
message_editor,
|
|
..
|
|
} => parent
|
|
.child(
|
|
if thread.read(cx).is_empty() && !self.should_render_onboarding(cx) {
|
|
self.render_thread_empty_state(window, cx)
|
|
.into_any_element()
|
|
} else {
|
|
thread.clone().into_any_element()
|
|
},
|
|
)
|
|
.children(self.render_tool_use_limit_reached(window, cx))
|
|
.when_some(thread.read(cx).last_error(), |this, last_error| {
|
|
this.child(
|
|
div()
|
|
.child(match last_error {
|
|
ThreadError::PaymentRequired => {
|
|
self.render_payment_required_error(thread, cx)
|
|
}
|
|
ThreadError::ModelRequestLimitReached { plan } => self
|
|
.render_model_request_limit_reached_error(plan, thread, cx),
|
|
ThreadError::Message { header, message } => {
|
|
self.render_error_message(header, message, thread, cx)
|
|
}
|
|
ThreadError::RetryableError {
|
|
message,
|
|
can_enable_burn_mode,
|
|
} => self.render_retryable_error(
|
|
message,
|
|
can_enable_burn_mode,
|
|
thread,
|
|
),
|
|
})
|
|
.into_any(),
|
|
)
|
|
})
|
|
.child(h_flex().relative().child(message_editor.clone()).when(
|
|
!LanguageModelRegistry::read_global(cx).has_authenticated_provider(cx),
|
|
|this| this.child(self.render_backdrop(cx)),
|
|
))
|
|
.child(self.render_drag_target(cx)),
|
|
ActiveView::ExternalAgentThread { thread_view, .. } => parent
|
|
.child(thread_view.clone())
|
|
.child(self.render_drag_target(cx)),
|
|
ActiveView::History => {
|
|
if cx.has_flag::<feature_flags::AcpFeatureFlag>() {
|
|
parent.child(self.acp_history.clone())
|
|
} else {
|
|
parent.child(self.history.clone())
|
|
}
|
|
}
|
|
ActiveView::TextThread {
|
|
context_editor,
|
|
buffer_search_bar,
|
|
..
|
|
} => {
|
|
let model_registry = LanguageModelRegistry::read_global(cx);
|
|
let configuration_error =
|
|
model_registry.configuration_error(model_registry.default_model(), cx);
|
|
parent
|
|
.map(|this| {
|
|
if !self.should_render_onboarding(cx)
|
|
&& let Some(err) = configuration_error.as_ref()
|
|
{
|
|
this.child(self.render_configuration_error(
|
|
true,
|
|
err,
|
|
&self.focus_handle(cx),
|
|
window,
|
|
cx,
|
|
))
|
|
} else {
|
|
this
|
|
}
|
|
})
|
|
.child(self.render_prompt_editor(
|
|
context_editor,
|
|
buffer_search_bar,
|
|
window,
|
|
cx,
|
|
))
|
|
}
|
|
ActiveView::Configuration => parent.children(self.configuration.clone()),
|
|
})
|
|
.children(self.render_trial_end_upsell(window, cx));
|
|
|
|
match self.active_view.which_font_size_used() {
|
|
WhichFontSize::AgentFont => {
|
|
WithRemSize::new(ThemeSettings::get_global(cx).agent_font_size(cx))
|
|
.size_full()
|
|
.child(content)
|
|
.into_any()
|
|
}
|
|
_ => content.into_any(),
|
|
}
|
|
}
|
|
}
|
|
|
|
struct PromptLibraryInlineAssist {
|
|
workspace: WeakEntity<Workspace>,
|
|
}
|
|
|
|
impl PromptLibraryInlineAssist {
|
|
pub fn new(workspace: WeakEntity<Workspace>) -> Self {
|
|
Self { workspace }
|
|
}
|
|
}
|
|
|
|
impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
|
|
fn assist(
|
|
&self,
|
|
prompt_editor: &Entity<Editor>,
|
|
initial_prompt: Option<String>,
|
|
window: &mut Window,
|
|
cx: &mut Context<RulesLibrary>,
|
|
) {
|
|
InlineAssistant::update_global(cx, |assistant, cx| {
|
|
let Some(project) = self
|
|
.workspace
|
|
.upgrade()
|
|
.map(|workspace| workspace.read(cx).project().downgrade())
|
|
else {
|
|
return;
|
|
};
|
|
let prompt_store = None;
|
|
let thread_store = None;
|
|
let text_thread_store = None;
|
|
let context_store = cx.new(|_| ContextStore::new(project.clone(), None));
|
|
assistant.assist(
|
|
prompt_editor,
|
|
self.workspace.clone(),
|
|
context_store,
|
|
project,
|
|
prompt_store,
|
|
thread_store,
|
|
text_thread_store,
|
|
initial_prompt,
|
|
window,
|
|
cx,
|
|
)
|
|
})
|
|
}
|
|
|
|
fn focus_agent_panel(
|
|
&self,
|
|
workspace: &mut Workspace,
|
|
window: &mut Window,
|
|
cx: &mut Context<Workspace>,
|
|
) -> bool {
|
|
workspace.focus_panel::<AgentPanel>(window, cx).is_some()
|
|
}
|
|
}
|
|
|
|
pub struct ConcreteAssistantPanelDelegate;
|
|
|
|
impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
|
|
fn active_context_editor(
|
|
&self,
|
|
workspace: &mut Workspace,
|
|
_window: &mut Window,
|
|
cx: &mut Context<Workspace>,
|
|
) -> Option<Entity<TextThreadEditor>> {
|
|
let panel = workspace.panel::<AgentPanel>(cx)?;
|
|
panel.read(cx).active_context_editor()
|
|
}
|
|
|
|
fn open_saved_context(
|
|
&self,
|
|
workspace: &mut Workspace,
|
|
path: Arc<Path>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Workspace>,
|
|
) -> Task<Result<()>> {
|
|
let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
|
|
return Task::ready(Err(anyhow!("Agent panel not found")));
|
|
};
|
|
|
|
panel.update(cx, |panel, cx| {
|
|
panel.open_saved_prompt_editor(path, window, cx)
|
|
})
|
|
}
|
|
|
|
fn open_remote_context(
|
|
&self,
|
|
_workspace: &mut Workspace,
|
|
_context_id: assistant_context::ContextId,
|
|
_window: &mut Window,
|
|
_cx: &mut Context<Workspace>,
|
|
) -> Task<Result<Entity<TextThreadEditor>>> {
|
|
Task::ready(Err(anyhow!("opening remote context not implemented")))
|
|
}
|
|
|
|
fn quote_selection(
|
|
&self,
|
|
workspace: &mut Workspace,
|
|
selection_ranges: Vec<Range<Anchor>>,
|
|
buffer: Entity<MultiBuffer>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Workspace>,
|
|
) {
|
|
let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
|
|
return;
|
|
};
|
|
|
|
if !panel.focus_handle(cx).contains_focused(window, cx) {
|
|
workspace.toggle_panel_focus::<AgentPanel>(window, cx);
|
|
}
|
|
|
|
panel.update(cx, |_, cx| {
|
|
// Wait to create a new context until the workspace is no longer
|
|
// being updated.
|
|
cx.defer_in(window, move |panel, window, cx| {
|
|
if let Some(message_editor) = panel.active_message_editor() {
|
|
message_editor.update(cx, |message_editor, cx| {
|
|
message_editor.context_store().update(cx, |store, cx| {
|
|
let buffer = buffer.read(cx);
|
|
let selection_ranges = selection_ranges
|
|
.into_iter()
|
|
.flat_map(|range| {
|
|
let (start_buffer, start) =
|
|
buffer.text_anchor_for_position(range.start, cx)?;
|
|
let (end_buffer, end) =
|
|
buffer.text_anchor_for_position(range.end, cx)?;
|
|
if start_buffer != end_buffer {
|
|
return None;
|
|
}
|
|
Some((start_buffer, start..end))
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
for (buffer, range) in selection_ranges {
|
|
store.add_selection(buffer, range, cx);
|
|
}
|
|
})
|
|
})
|
|
} else if let Some(context_editor) = panel.active_context_editor() {
|
|
let snapshot = buffer.read(cx).snapshot(cx);
|
|
let selection_ranges = selection_ranges
|
|
.into_iter()
|
|
.map(|range| range.to_point(&snapshot))
|
|
.collect::<Vec<_>>();
|
|
|
|
context_editor.update(cx, |context_editor, cx| {
|
|
context_editor.quote_ranges(selection_ranges, snapshot, window, cx)
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
struct OnboardingUpsell;
|
|
|
|
impl Dismissable for OnboardingUpsell {
|
|
const KEY: &'static str = "dismissed-trial-upsell";
|
|
}
|
|
|
|
struct TrialEndUpsell;
|
|
|
|
impl Dismissable for TrialEndUpsell {
|
|
const KEY: &'static str = "dismissed-trial-end-upsell";
|
|
}
|