
This PR adds a new `intent` field to completion requests to assist in categorizing them correctly. Release Notes: - N/A --------- Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
3312 lines
130 KiB
Rust
3312 lines
130 KiB
Rust
use std::ops::Range;
|
|
use std::path::Path;
|
|
use std::rc::Rc;
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
|
|
use db::kvp::{Dismissable, KEY_VALUE_STORE};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use agent_settings::{AgentDockPosition, AgentSettings, CompletionMode, DefaultView};
|
|
use anyhow::{Result, anyhow};
|
|
use assistant_context_editor::{
|
|
AgentPanelDelegate, AssistantContext, ConfigurationError, ContextEditor, ContextEvent,
|
|
ContextSummary, SlashCommandCompletionProvider, humanize_token_count,
|
|
make_lsp_adapter_delegate, render_remaining_tokens,
|
|
};
|
|
use assistant_slash_command::SlashCommandWorkingSet;
|
|
use assistant_tool::ToolWorkingSet;
|
|
|
|
use assistant_context_editor::language_model_selector::ToggleModelSelector;
|
|
use client::{UserStore, zed_urls};
|
|
use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
|
|
use fs::Fs;
|
|
use gpui::{
|
|
Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem,
|
|
Corner, DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, FontWeight,
|
|
KeyContext, Pixels, Subscription, Task, UpdateGlobal, WeakEntity, linear_color_stop,
|
|
linear_gradient, prelude::*, pulsating_between,
|
|
};
|
|
use language::LanguageRegistry;
|
|
use language_model::{
|
|
LanguageModelProviderTosView, LanguageModelRegistry, RequestUsage, ZED_CLOUD_PROVIDER_ID,
|
|
};
|
|
use project::{Project, ProjectPath, Worktree};
|
|
use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
|
|
use proto::Plan;
|
|
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, CheckboxWithLabel, ContextMenu, ElevationIndex, KeyBinding, PopoverMenu,
|
|
PopoverMenuHandle, ProgressBar, Tab, Tooltip, Vector, VectorName, prelude::*,
|
|
};
|
|
use util::{ResultExt as _, maybe};
|
|
use workspace::dock::{DockPosition, Panel, PanelEvent};
|
|
use workspace::{
|
|
CollaboratorId, DraggedSelection, DraggedTab, ToggleZoom, ToolbarItemView, Workspace,
|
|
};
|
|
use zed_actions::agent::{OpenConfiguration, OpenOnboardingModal, ResetOnboarding};
|
|
use zed_actions::assistant::{OpenRulesLibrary, ToggleFocus};
|
|
use zed_actions::{DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize};
|
|
use zed_llm_client::{CompletionIntent, UsageLimit};
|
|
|
|
use crate::active_thread::{self, ActiveThread, ActiveThreadEvent};
|
|
use crate::agent_configuration::{AgentConfiguration, AssistantConfigurationEvent};
|
|
use crate::agent_diff::AgentDiff;
|
|
use crate::history_store::{HistoryStore, RecentEntry};
|
|
use crate::message_editor::{MessageEditor, MessageEditorEvent};
|
|
use crate::thread::{Thread, ThreadError, ThreadId, ThreadSummary, TokenUsageRatio};
|
|
use crate::thread_history::{HistoryEntryElement, ThreadHistory};
|
|
use crate::thread_store::ThreadStore;
|
|
use crate::ui::AgentOnboardingModal;
|
|
use crate::{
|
|
AddContextServer, AgentDiffPane, ContextStore, ContinueThread, ContinueWithBurnMode,
|
|
DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread,
|
|
NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ResetTrialEndUpsell,
|
|
ResetTrialUpsell, TextThreadStore, ThreadEvent, ToggleBurnMode, ToggleContextPicker,
|
|
ToggleNavigationMenu, ToggleOptionsMenu,
|
|
};
|
|
|
|
const AGENT_PANEL_KEY: &str = "agent_panel";
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
struct SerializedAgentPanel {
|
|
width: Option<Pixels>,
|
|
}
|
|
|
|
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, _: &OpenConfiguration, 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: &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);
|
|
let thread = panel.read(cx).thread.read(cx).thread().clone();
|
|
AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx);
|
|
}
|
|
})
|
|
.register_action(|workspace, _: &Follow, window, cx| {
|
|
workspace.follow(CollaboratorId::Agent, window, cx);
|
|
})
|
|
.register_action(|workspace, _: &ExpandMessageEditor, window, cx| {
|
|
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
|
workspace.focus_panel::<AgentPanel>(window, cx);
|
|
panel.update(cx, |panel, cx| {
|
|
panel.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, _: &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| {
|
|
Upsell::set_dismissed(false, cx);
|
|
})
|
|
.register_action(|_workspace, _: &ResetTrialEndUpsell, _window, cx| {
|
|
TrialEndUpsell::set_dismissed(false, cx);
|
|
});
|
|
},
|
|
)
|
|
.detach();
|
|
}
|
|
|
|
enum ActiveView {
|
|
Thread {
|
|
change_title_editor: Entity<Editor>,
|
|
thread: WeakEntity<Thread>,
|
|
_subscriptions: Vec<gpui::Subscription>,
|
|
},
|
|
TextThread {
|
|
context_editor: Entity<ContextEditor>,
|
|
title_editor: Entity<Editor>,
|
|
buffer_search_bar: Entity<BufferSearchBar>,
|
|
_subscriptions: Vec<gpui::Subscription>,
|
|
},
|
|
History,
|
|
Configuration,
|
|
}
|
|
|
|
enum WhichFontSize {
|
|
AgentFont,
|
|
BufferFont,
|
|
None,
|
|
}
|
|
|
|
impl ActiveView {
|
|
pub fn which_font_size_used(&self) -> WhichFontSize {
|
|
match self {
|
|
ActiveView::Thread { .. } | ActiveView::History => WhichFontSize::AgentFont,
|
|
ActiveView::TextThread { .. } => WhichFontSize::BufferFont,
|
|
ActiveView::Configuration => WhichFontSize::None,
|
|
}
|
|
}
|
|
|
|
pub fn thread(thread: Entity<Thread>, window: &mut Window, cx: &mut App) -> Self {
|
|
let summary = thread.read(cx).summary().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![
|
|
window.subscribe(&editor, cx, {
|
|
{
|
|
let thread = 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.set_summary(new_summary, cx);
|
|
})
|
|
}
|
|
EditorEvent::Blurred => {
|
|
if editor.read(cx).text(cx).is_empty() {
|
|
let summary = thread.read(cx).summary().or_default();
|
|
|
|
editor.update(cx, |editor, cx| {
|
|
editor.set_text(summary, window, cx);
|
|
});
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}),
|
|
window.subscribe(&thread, cx, {
|
|
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);
|
|
})
|
|
}
|
|
_ => {}
|
|
}
|
|
}),
|
|
];
|
|
|
|
Self::Thread {
|
|
change_title_editor: editor,
|
|
thread: thread.downgrade(),
|
|
_subscriptions: subscriptions,
|
|
}
|
|
}
|
|
|
|
pub fn prompt_editor(
|
|
context_editor: Entity<ContextEditor>,
|
|
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);
|
|
})
|
|
}
|
|
_ => {}
|
|
}
|
|
}),
|
|
];
|
|
|
|
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>,
|
|
thread: Entity<ActiveThread>,
|
|
message_editor: Entity<MessageEditor>,
|
|
_active_thread_subscriptions: Vec<Subscription>,
|
|
_default_model_subscription: Subscription,
|
|
context_store: Entity<TextThreadStore>,
|
|
prompt_store: Option<Entity<PromptStore>>,
|
|
inline_assist_context_store: Entity<crate::context_store::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>,
|
|
assistant_dropdown_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<()>>>,
|
|
hide_upsell: bool,
|
|
}
|
|
|
|
impl AgentPanel {
|
|
fn serialize(&mut self, cx: &mut Context<Self>) {
|
|
let width = self.width;
|
|
self.pending_serialization = Some(cx.background_spawn(async move {
|
|
KEY_VALUE_STORE
|
|
.write_kvp(
|
|
AGENT_PANEL_KEY.into(),
|
|
serde_json::to_string(&SerializedAgentPanel { width })?,
|
|
)
|
|
.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_editor::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());
|
|
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 workspace = workspace.weak_handle();
|
|
let weak_self = cx.entity().downgrade();
|
|
|
|
let message_editor_context_store = cx.new(|_cx| {
|
|
crate::context_store::ContextStore::new(
|
|
project.downgrade(),
|
|
Some(thread_store.downgrade()),
|
|
)
|
|
});
|
|
let inline_assist_context_store = cx.new(|_cx| {
|
|
crate::context_store::ContextStore::new(
|
|
project.downgrade(),
|
|
Some(thread_store.downgrade()),
|
|
)
|
|
});
|
|
|
|
let message_editor = cx.new(|cx| {
|
|
MessageEditor::new(
|
|
fs.clone(),
|
|
workspace.clone(),
|
|
user_store.clone(),
|
|
message_editor_context_store.clone(),
|
|
prompt_store.clone(),
|
|
thread_store.downgrade(),
|
|
context_store.downgrade(),
|
|
thread.clone(),
|
|
window,
|
|
cx,
|
|
)
|
|
});
|
|
|
|
let message_editor_subscription =
|
|
cx.subscribe(&message_editor, |_, _, event, cx| match event {
|
|
MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
|
|
cx.notify();
|
|
}
|
|
});
|
|
|
|
let thread_id = thread.read(cx).id().clone();
|
|
let history_store = cx.new(|cx| {
|
|
HistoryStore::new(
|
|
thread_store.clone(),
|
|
context_store.clone(),
|
|
[RecentEntry::Thread(thread_id, thread.clone())],
|
|
window,
|
|
cx,
|
|
)
|
|
});
|
|
|
|
cx.observe(&history_store, |_, _, cx| cx.notify()).detach();
|
|
|
|
let panel_type = AgentSettings::get_global(cx).default_view;
|
|
let active_view = match panel_type {
|
|
DefaultView::Thread => ActiveView::thread(thread.clone(), 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 = ContextEditor::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, language_registry.clone(), window, cx)
|
|
}
|
|
};
|
|
|
|
let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
|
|
if let ThreadEvent::MessageAdded(_) = &event {
|
|
// needed to leave empty state
|
|
cx.notify();
|
|
}
|
|
});
|
|
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,
|
|
)
|
|
});
|
|
AgentDiff::set_active_thread(&workspace, &thread, window, cx);
|
|
|
|
let active_thread_subscription =
|
|
cx.subscribe(&active_thread, |_, _, event, cx| match &event {
|
|
ActiveThreadEvent::EditingMessageTokenCountChanged => {
|
|
cx.notify();
|
|
}
|
|
});
|
|
|
|
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| {
|
|
let recently_opened = panel
|
|
.update(cx, |this, cx| {
|
|
this.history_store.update(cx, |history_store, cx| {
|
|
history_store.recently_opened_entries(cx)
|
|
})
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
if !recently_opened.is_empty() {
|
|
menu = menu.header("Recently Opened");
|
|
|
|
for entry in recently_opened.iter() {
|
|
if let RecentEntry::Context(context) = entry {
|
|
if context.read(cx).path().is_none() {
|
|
log::error!(
|
|
"bug: text thread in recent history list was never saved"
|
|
);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
let summary = entry.summary(cx);
|
|
|
|
menu = menu.entry_with_end_slot_on_hover(
|
|
summary,
|
|
None,
|
|
{
|
|
let panel = panel.clone();
|
|
let entry = entry.clone();
|
|
move |window, cx| {
|
|
panel
|
|
.update(cx, {
|
|
let entry = entry.clone();
|
|
move |this, cx| match entry {
|
|
RecentEntry::Thread(_, thread) => {
|
|
this.open_thread(thread, window, cx)
|
|
}
|
|
RecentEntry::Context(context) => {
|
|
let Some(path) = context.read(cx).path()
|
|
else {
|
|
return;
|
|
};
|
|
this.open_saved_prompt_editor(
|
|
path.clone(),
|
|
window,
|
|
cx,
|
|
)
|
|
.detach_and_log_err(cx)
|
|
}
|
|
}
|
|
})
|
|
.ok();
|
|
}
|
|
},
|
|
IconName::Close,
|
|
"Close Entry".into(),
|
|
{
|
|
let panel = panel.clone();
|
|
let entry = entry.clone();
|
|
move |_window, cx| {
|
|
panel
|
|
.update(cx, |this, cx| {
|
|
this.history_store.update(
|
|
cx,
|
|
|history_store, cx| {
|
|
history_store.remove_recently_opened_entry(
|
|
&entry, cx,
|
|
);
|
|
},
|
|
);
|
|
})
|
|
.ok();
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
menu = menu.separator();
|
|
}
|
|
|
|
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 => {
|
|
this.thread
|
|
.read(cx)
|
|
.thread()
|
|
.clone()
|
|
.update(cx, |thread, cx| thread.get_or_init_configured_model(cx));
|
|
}
|
|
_ => {}
|
|
},
|
|
);
|
|
|
|
Self {
|
|
active_view,
|
|
workspace,
|
|
user_store,
|
|
project: project.clone(),
|
|
fs: fs.clone(),
|
|
language_registry,
|
|
thread_store: thread_store.clone(),
|
|
thread: active_thread,
|
|
message_editor,
|
|
_active_thread_subscriptions: vec![
|
|
thread_subscription,
|
|
active_thread_subscription,
|
|
message_editor_subscription,
|
|
],
|
|
_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,
|
|
assistant_dropdown_menu_handle: PopoverMenuHandle::default(),
|
|
assistant_navigation_menu_handle: PopoverMenuHandle::default(),
|
|
assistant_navigation_menu: None,
|
|
width: None,
|
|
height: None,
|
|
zoomed: false,
|
|
pending_serialization: None,
|
|
hide_upsell: false,
|
|
}
|
|
}
|
|
|
|
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))
|
|
{
|
|
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<crate::context_store::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>) {
|
|
self.thread
|
|
.update(cx, |thread, cx| thread.cancel_last_completion(window, cx));
|
|
}
|
|
|
|
fn new_thread(&mut self, action: &NewThread, window: &mut Window, cx: &mut Context<Self>) {
|
|
let thread = self
|
|
.thread_store
|
|
.update(cx, |this, cx| this.create_thread(cx));
|
|
|
|
let thread_view = ActiveView::thread(thread.clone(), window, cx);
|
|
self.set_active_view(thread_view, window, cx);
|
|
|
|
let context_store = cx.new(|_cx| {
|
|
crate::context_store::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 thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
|
|
if let ThreadEvent::MessageAdded(_) = &event {
|
|
// needed to leave empty state
|
|
cx.notify();
|
|
}
|
|
});
|
|
|
|
self.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,
|
|
)
|
|
});
|
|
AgentDiff::set_active_thread(&self.workspace, &thread, window, cx);
|
|
|
|
let active_thread_subscription =
|
|
cx.subscribe(&self.thread, |_, _, event, cx| match &event {
|
|
ActiveThreadEvent::EditingMessageTokenCountChanged => {
|
|
cx.notify();
|
|
}
|
|
});
|
|
|
|
self.message_editor = cx.new(|cx| {
|
|
MessageEditor::new(
|
|
self.fs.clone(),
|
|
self.workspace.clone(),
|
|
self.user_store.clone(),
|
|
context_store,
|
|
self.prompt_store.clone(),
|
|
self.thread_store.downgrade(),
|
|
self.context_store.downgrade(),
|
|
thread,
|
|
window,
|
|
cx,
|
|
)
|
|
});
|
|
self.message_editor.focus_handle(cx).focus(window);
|
|
|
|
let message_editor_subscription =
|
|
cx.subscribe(&self.message_editor, |_, _, event, cx| match event {
|
|
MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
|
|
cx.notify();
|
|
}
|
|
});
|
|
|
|
self._active_thread_subscriptions = vec![
|
|
thread_subscription,
|
|
active_thread_subscription,
|
|
message_editor_subscription,
|
|
];
|
|
}
|
|
|
|
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 = ContextEditor::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.language_registry.clone(),
|
|
window,
|
|
cx,
|
|
),
|
|
window,
|
|
cx,
|
|
);
|
|
context_editor.focus_handle(cx).focus(window);
|
|
}
|
|
|
|
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| {
|
|
ContextEditor::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.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 thread_view = ActiveView::thread(thread.clone(), window, cx);
|
|
self.set_active_view(thread_view, window, cx);
|
|
let context_store = cx.new(|_cx| {
|
|
crate::context_store::ContextStore::new(
|
|
self.project.downgrade(),
|
|
Some(self.thread_store.downgrade()),
|
|
)
|
|
});
|
|
let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
|
|
if let ThreadEvent::MessageAdded(_) = &event {
|
|
// needed to leave empty state
|
|
cx.notify();
|
|
}
|
|
});
|
|
|
|
self.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,
|
|
)
|
|
});
|
|
AgentDiff::set_active_thread(&self.workspace, &thread, window, cx);
|
|
|
|
let active_thread_subscription =
|
|
cx.subscribe(&self.thread, |_, _, event, cx| match &event {
|
|
ActiveThreadEvent::EditingMessageTokenCountChanged => {
|
|
cx.notify();
|
|
}
|
|
});
|
|
|
|
self.message_editor = cx.new(|cx| {
|
|
MessageEditor::new(
|
|
self.fs.clone(),
|
|
self.workspace.clone(),
|
|
self.user_store.clone(),
|
|
context_store,
|
|
self.prompt_store.clone(),
|
|
self.thread_store.downgrade(),
|
|
self.context_store.downgrade(),
|
|
thread,
|
|
window,
|
|
cx,
|
|
)
|
|
});
|
|
self.message_editor.focus_handle(cx).focus(window);
|
|
|
|
let message_editor_subscription =
|
|
cx.subscribe(&self.message_editor, |_, _, event, cx| match event {
|
|
MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
|
|
cx.notify();
|
|
}
|
|
});
|
|
|
|
self._active_thread_subscriptions = vec![
|
|
thread_subscription,
|
|
active_thread_subscription,
|
|
message_editor_subscription,
|
|
];
|
|
}
|
|
|
|
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 { .. } => {
|
|
self.message_editor.focus_handle(cx).focus(window);
|
|
}
|
|
ActiveView::TextThread { context_editor, .. } => {
|
|
context_editor.focus_handle(cx).focus(window);
|
|
}
|
|
_ => {}
|
|
}
|
|
} else {
|
|
self.active_view =
|
|
ActiveView::thread(self.thread.read(cx).thread().clone(), window, cx);
|
|
self.message_editor.focus_handle(cx).focus(window);
|
|
}
|
|
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.assistant_dropdown_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(theme::clamp_font_size(agent_font_size).0);
|
|
},
|
|
);
|
|
} 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>,
|
|
) {
|
|
let thread = self.thread.read(cx).thread().clone();
|
|
self.workspace
|
|
.update(cx, |workspace, cx| {
|
|
AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx)
|
|
})
|
|
.log_err();
|
|
}
|
|
|
|
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, 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;
|
|
};
|
|
|
|
let Some(thread) = self.active_thread() else {
|
|
return;
|
|
};
|
|
|
|
active_thread::open_active_thread_as_markdown(thread, workspace, window, cx)
|
|
.detach_and_log_err(cx);
|
|
}
|
|
|
|
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()
|
|
.map_or(true, |model| model.provider.id() != provider.id())
|
|
{
|
|
if 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);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn active_thread(&self) -> Option<Entity<Thread>> {
|
|
match &self.active_view {
|
|
ActiveView::Thread { thread, .. } => thread.upgrade(),
|
|
_ => 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))
|
|
}
|
|
|
|
pub(crate) fn has_active_thread(&self) -> bool {
|
|
matches!(self.active_view, ActiveView::Thread { .. })
|
|
}
|
|
|
|
fn continue_conversation(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
let thread_state = self.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 {
|
|
self.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>,
|
|
) {
|
|
self.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<ContextEditor>> {
|
|
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, .. } => {
|
|
if let Some(thread) = thread.upgrade() {
|
|
if thread.read(cx).is_empty() {
|
|
let id = thread.read(cx).id().clone();
|
|
self.history_store.update(cx, |store, cx| {
|
|
store.remove_recently_opened_thread(id, cx);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
ActiveView::TextThread { context_editor, .. } => {
|
|
let context = context_editor.read(cx).context();
|
|
// When switching away from an unsaved text thread, delete its entry.
|
|
if context.read(cx).path().is_none() {
|
|
let context = context.clone();
|
|
self.history_store.update(cx, |store, cx| {
|
|
store.remove_recently_opened_entry(&RecentEntry::Context(context), cx);
|
|
});
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
match &new_view {
|
|
ActiveView::Thread { thread, .. } => self.history_store.update(cx, |store, cx| {
|
|
if let Some(thread) = thread.upgrade() {
|
|
let id = thread.read(cx).id().clone();
|
|
store.push_recently_opened_entry(RecentEntry::Thread(id, thread), cx);
|
|
}
|
|
}),
|
|
ActiveView::TextThread { context_editor, .. } => {
|
|
self.history_store.update(cx, |store, cx| {
|
|
let context = context_editor.read(cx).context().clone();
|
|
store.push_recently_opened_entry(RecentEntry::Context(context), cx)
|
|
})
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
impl Focusable for AgentPanel {
|
|
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
|
match &self.active_view {
|
|
ActiveView::Thread { .. } => self.message_editor.focus_handle(cx),
|
|
ActiveView::History => 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 {
|
|
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 {
|
|
change_title_editor,
|
|
..
|
|
} => {
|
|
let active_thread = self.thread.read(cx);
|
|
let state = 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(
|
|
ui::IconButton::new("retry-summary-generation", IconName::RotateCcw)
|
|
.on_click({
|
|
let active_thread = self.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::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(
|
|
ui::IconButton::new("retry-summary-generation", IconName::RotateCcw)
|
|
.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_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let active_thread = self.thread.read(cx);
|
|
let user_store = self.user_store.read(cx);
|
|
let thread = active_thread.thread().read(cx);
|
|
let thread_id = thread.id().clone();
|
|
let is_empty = active_thread.is_empty();
|
|
let editor_empty = self.message_editor.read(cx).is_editor_fully_empty(cx);
|
|
let last_usage = active_thread.thread().read(cx).last_usage().or_else(|| {
|
|
maybe!({
|
|
let amount = user_store.model_request_usage_amount()?;
|
|
let limit = user_store.model_request_usage_limit()?.variant?;
|
|
|
|
Some(RequestUsage {
|
|
amount: amount as i32,
|
|
limit: match limit {
|
|
proto::usage_limit::Variant::Limited(limited) => {
|
|
zed_llm_client::UsageLimit::Limited(limited.limit as i32)
|
|
}
|
|
proto::usage_limit::Variant::Unlimited(_) => {
|
|
zed_llm_client::UsageLimit::Unlimited
|
|
}
|
|
},
|
|
})
|
|
})
|
|
});
|
|
|
|
let account_url = zed_urls::account_url(cx);
|
|
|
|
let show_token_count = match &self.active_view {
|
|
ActiveView::Thread { .. } => !is_empty || !editor_empty,
|
|
ActiveView::TextThread { .. } => true,
|
|
_ => false,
|
|
};
|
|
|
|
let focus_handle = self.focus_handle(cx);
|
|
|
|
let go_back_button = div().child(
|
|
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,
|
|
)
|
|
}
|
|
}),
|
|
);
|
|
|
|
let recent_entries_menu = div().child(
|
|
PopoverMenu::new("agent-nav-menu")
|
|
.trigger_with_tooltip(
|
|
IconButton::new("agent-nav-menu", IconName::MenuAlt)
|
|
.icon_size(IconSize::Small)
|
|
.style(ui::ButtonStyle::Subtle),
|
|
{
|
|
let focus_handle = focus_handle.clone();
|
|
move |window, cx| {
|
|
Tooltip::for_action_in(
|
|
"Toggle Panel Menu",
|
|
&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()
|
|
}
|
|
}),
|
|
);
|
|
|
|
let zoom_in_label = if self.is_zoomed(window, cx) {
|
|
"Zoom Out"
|
|
} else {
|
|
"Zoom In"
|
|
};
|
|
|
|
let agent_extra_menu = 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.assistant_dropdown_menu_handle.clone())
|
|
.menu(move |window, cx| {
|
|
Some(ContextMenu::build(window, cx, |mut menu, _window, _cx| {
|
|
menu = menu
|
|
.action("New Thread", NewThread::default().boxed_clone())
|
|
.action("New Text Thread", NewTextThread.boxed_clone())
|
|
.when(!is_empty, |menu| {
|
|
menu.action(
|
|
"New From Summary",
|
|
Box::new(NewThread {
|
|
from_thread_id: Some(thread_id.clone()),
|
|
}),
|
|
)
|
|
})
|
|
.separator();
|
|
|
|
menu = menu
|
|
.header("MCP Servers")
|
|
.action(
|
|
"View Server Extensions",
|
|
Box::new(zed_actions::Extensions {
|
|
category_filter: Some(
|
|
zed_actions::ExtensionCategoryFilter::ContextServers,
|
|
),
|
|
}),
|
|
)
|
|
.action("Add Custom Server…", Box::new(AddContextServer))
|
|
.separator();
|
|
|
|
if let Some(usage) = last_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
|
|
.action("Rules…", Box::new(OpenRulesLibrary::default()))
|
|
.action("Settings", Box::new(OpenConfiguration))
|
|
.action(zoom_in_label, Box::new(ToggleZoom));
|
|
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 => go_back_button,
|
|
_ => recent_entries_menu,
|
|
})
|
|
.child(self.render_title_view(window, cx)),
|
|
)
|
|
.child(
|
|
h_flex()
|
|
.h_full()
|
|
.gap_2()
|
|
.when(show_token_count, |parent| {
|
|
parent.children(self.render_token_count(&thread, 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(
|
|
IconButton::new("new", IconName::Plus)
|
|
.icon_size(IconSize::Small)
|
|
.style(ButtonStyle::Subtle)
|
|
.tooltip(move |window, cx| {
|
|
Tooltip::for_action_in(
|
|
"New Thread",
|
|
&NewThread::default(),
|
|
&focus_handle,
|
|
window,
|
|
cx,
|
|
)
|
|
})
|
|
.on_click(move |_event, window, cx| {
|
|
window.dispatch_action(
|
|
NewThread::default().boxed_clone(),
|
|
cx,
|
|
);
|
|
}),
|
|
)
|
|
.child(agent_extra_menu),
|
|
),
|
|
)
|
|
}
|
|
|
|
fn render_token_count(&self, thread: &Thread, cx: &App) -> Option<AnyElement> {
|
|
let is_generating = thread.is_generating();
|
|
let message_editor = self.message_editor.read(cx);
|
|
|
|
let conversation_token_usage = thread.total_token_usage()?;
|
|
|
|
let (total_token_usage, is_estimating) = if let Some((editing_message_id, unsent_tokens)) =
|
|
self.thread.read(cx).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();
|
|
|
|
match &self.active_view {
|
|
ActiveView::Thread { .. } => {
|
|
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())
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn should_render_trial_end_upsell(&self, cx: &mut Context<Self>) -> bool {
|
|
if TrialEndUpsell::dismissed() {
|
|
return false;
|
|
}
|
|
|
|
let plan = self.user_store.read(cx).current_plan();
|
|
let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
|
|
|
|
matches!(plan, Some(Plan::Free)) && has_previous_trial
|
|
}
|
|
|
|
fn should_render_upsell(&self, cx: &mut Context<Self>) -> bool {
|
|
if !matches!(self.active_view, ActiveView::Thread { .. }) {
|
|
return false;
|
|
}
|
|
|
|
if self.hide_upsell || Upsell::dismissed() {
|
|
return false;
|
|
}
|
|
|
|
let is_using_zed_provider = self
|
|
.thread
|
|
.read(cx)
|
|
.thread()
|
|
.read(cx)
|
|
.configured_model()
|
|
.map_or(false, |model| {
|
|
model.provider.id().0 == ZED_CLOUD_PROVIDER_ID
|
|
});
|
|
if !is_using_zed_provider {
|
|
return false;
|
|
}
|
|
|
|
let plan = self.user_store.read(cx).current_plan();
|
|
if matches!(plan, Some(Plan::ZedPro | Plan::ZedProTrial)) {
|
|
return false;
|
|
}
|
|
|
|
let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
|
|
if has_previous_trial {
|
|
return false;
|
|
}
|
|
|
|
true
|
|
}
|
|
|
|
fn render_upsell(
|
|
&self,
|
|
_window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> Option<impl IntoElement> {
|
|
if !self.should_render_upsell(cx) {
|
|
return None;
|
|
}
|
|
|
|
if self.user_store.read(cx).account_too_young() {
|
|
Some(self.render_young_account_upsell(cx).into_any_element())
|
|
} else {
|
|
Some(self.render_trial_upsell(cx).into_any_element())
|
|
}
|
|
}
|
|
|
|
fn render_young_account_upsell(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let checkbox = CheckboxWithLabel::new(
|
|
"dont-show-again",
|
|
Label::new("Don't show again").color(Color::Muted),
|
|
ToggleState::Unselected,
|
|
move |toggle_state, _window, cx| {
|
|
let toggle_state_bool = toggle_state.selected();
|
|
|
|
Upsell::set_dismissed(toggle_state_bool, cx);
|
|
},
|
|
);
|
|
|
|
let contents = div()
|
|
.size_full()
|
|
.gap_2()
|
|
.flex()
|
|
.flex_col()
|
|
.child(Headline::new("Build better with Zed Pro").size(HeadlineSize::Small))
|
|
.child(
|
|
Label::new("Your GitHub account was created less than 30 days ago, so we can't offer you a free trial.")
|
|
.size(LabelSize::Small),
|
|
)
|
|
.child(
|
|
Label::new(
|
|
"Use your own API keys, upgrade to Zed Pro or send an email to billing-support@zed.dev.",
|
|
)
|
|
.color(Color::Muted),
|
|
)
|
|
.child(
|
|
h_flex()
|
|
.w_full()
|
|
.px_neg_1()
|
|
.justify_between()
|
|
.items_center()
|
|
.child(h_flex().items_center().gap_1().child(checkbox))
|
|
.child(
|
|
h_flex()
|
|
.gap_2()
|
|
.child(
|
|
Button::new("dismiss-button", "Not Now")
|
|
.style(ButtonStyle::Transparent)
|
|
.color(Color::Muted)
|
|
.on_click({
|
|
let agent_panel = cx.entity();
|
|
move |_, _, cx| {
|
|
agent_panel.update(cx, |this, cx| {
|
|
this.hide_upsell = true;
|
|
cx.notify();
|
|
});
|
|
}
|
|
}),
|
|
)
|
|
.child(
|
|
Button::new("cta-button", "Upgrade to Zed Pro")
|
|
.style(ButtonStyle::Transparent)
|
|
.on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))),
|
|
),
|
|
),
|
|
);
|
|
|
|
self.render_upsell_container(cx, contents)
|
|
}
|
|
|
|
fn render_trial_upsell(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let checkbox = CheckboxWithLabel::new(
|
|
"dont-show-again",
|
|
Label::new("Don't show again").color(Color::Muted),
|
|
ToggleState::Unselected,
|
|
move |toggle_state, _window, cx| {
|
|
let toggle_state_bool = toggle_state.selected();
|
|
|
|
Upsell::set_dismissed(toggle_state_bool, cx);
|
|
},
|
|
);
|
|
|
|
let contents = div()
|
|
.size_full()
|
|
.gap_2()
|
|
.flex()
|
|
.flex_col()
|
|
.child(Headline::new("Build better with Zed Pro").size(HeadlineSize::Small))
|
|
.child(
|
|
Label::new("Try Zed Pro for free for 14 days - no credit card required.")
|
|
.size(LabelSize::Small),
|
|
)
|
|
.child(
|
|
Label::new(
|
|
"Use your own API keys or enable usage-based billing once you hit the cap.",
|
|
)
|
|
.color(Color::Muted),
|
|
)
|
|
.child(
|
|
h_flex()
|
|
.w_full()
|
|
.px_neg_1()
|
|
.justify_between()
|
|
.items_center()
|
|
.child(h_flex().items_center().gap_1().child(checkbox))
|
|
.child(
|
|
h_flex()
|
|
.gap_2()
|
|
.child(
|
|
Button::new("dismiss-button", "Not Now")
|
|
.style(ButtonStyle::Transparent)
|
|
.color(Color::Muted)
|
|
.on_click({
|
|
let agent_panel = cx.entity();
|
|
move |_, _, cx| {
|
|
agent_panel.update(cx, |this, cx| {
|
|
this.hide_upsell = true;
|
|
cx.notify();
|
|
});
|
|
}
|
|
}),
|
|
)
|
|
.child(
|
|
Button::new("cta-button", "Start Trial")
|
|
.style(ButtonStyle::Transparent)
|
|
.on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))),
|
|
),
|
|
),
|
|
);
|
|
|
|
self.render_upsell_container(cx, contents)
|
|
}
|
|
|
|
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(
|
|
self.render_upsell_container(
|
|
cx,
|
|
div()
|
|
.size_full()
|
|
.gap_2()
|
|
.flex()
|
|
.flex_col()
|
|
.child(
|
|
Headline::new("Your Zed Pro trial has expired.").size(HeadlineSize::Small),
|
|
)
|
|
.child(
|
|
Label::new("You've been automatically reset to the free plan.")
|
|
.size(LabelSize::Small),
|
|
)
|
|
.child(
|
|
h_flex()
|
|
.w_full()
|
|
.px_neg_1()
|
|
.justify_between()
|
|
.items_center()
|
|
.child(div())
|
|
.child(
|
|
h_flex()
|
|
.gap_2()
|
|
.child(
|
|
Button::new("dismiss-button", "Stay on Free")
|
|
.style(ButtonStyle::Transparent)
|
|
.color(Color::Muted)
|
|
.on_click({
|
|
let agent_panel = cx.entity();
|
|
move |_, _, cx| {
|
|
agent_panel.update(cx, |_this, cx| {
|
|
TrialEndUpsell::set_dismissed(true, cx);
|
|
cx.notify();
|
|
});
|
|
}
|
|
}),
|
|
)
|
|
.child(
|
|
Button::new("cta-button", "Upgrade to Zed Pro")
|
|
.style(ButtonStyle::Transparent)
|
|
.on_click(|_, _, cx| {
|
|
cx.open_url(&zed_urls::account_url(cx))
|
|
}),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
}
|
|
|
|
fn render_upsell_container(&self, cx: &mut Context<Self>, content: Div) -> Div {
|
|
div().p_2().child(
|
|
v_flex()
|
|
.w_full()
|
|
.elevation_2(cx)
|
|
.rounded(px(8.))
|
|
.bg(cx.theme().colors().background.alpha(0.5))
|
|
.p(px(3.))
|
|
.child(
|
|
div()
|
|
.gap_2()
|
|
.flex()
|
|
.flex_col()
|
|
.size_full()
|
|
.border_1()
|
|
.rounded(px(5.))
|
|
.border_color(cx.theme().colors().text.alpha(0.1))
|
|
.overflow_hidden()
|
|
.relative()
|
|
.bg(cx.theme().colors().panel_background)
|
|
.px_4()
|
|
.py_3()
|
|
.child(
|
|
div()
|
|
.absolute()
|
|
.top_0()
|
|
.right(px(-1.0))
|
|
.w(px(441.))
|
|
.h(px(167.))
|
|
.child(
|
|
Vector::new(
|
|
VectorName::Grid,
|
|
rems_from_px(441.),
|
|
rems_from_px(167.),
|
|
)
|
|
.color(ui::Color::Custom(cx.theme().colors().text.alpha(0.1))),
|
|
),
|
|
)
|
|
.child(
|
|
div()
|
|
.absolute()
|
|
.top(px(-8.0))
|
|
.right_0()
|
|
.w(px(400.))
|
|
.h(px(92.))
|
|
.child(
|
|
Vector::new(
|
|
VectorName::AiGrid,
|
|
rems_from_px(400.),
|
|
rems_from_px(92.),
|
|
)
|
|
.color(ui::Color::Custom(cx.theme().colors().text.alpha(0.32))),
|
|
),
|
|
)
|
|
// .child(
|
|
// div()
|
|
// .absolute()
|
|
// .top_0()
|
|
// .right(px(360.))
|
|
// .size(px(401.))
|
|
// .overflow_hidden()
|
|
// .bg(cx.theme().colors().panel_background)
|
|
// )
|
|
.child(
|
|
div()
|
|
.absolute()
|
|
.top_0()
|
|
.right_0()
|
|
.w(px(660.))
|
|
.h(px(401.))
|
|
.overflow_hidden()
|
|
.bg(linear_gradient(
|
|
75.,
|
|
linear_color_stop(
|
|
cx.theme().colors().panel_background.alpha(0.01),
|
|
1.0,
|
|
),
|
|
linear_color_stop(cx.theme().colors().panel_background, 0.45),
|
|
)),
|
|
)
|
|
.child(content),
|
|
),
|
|
)
|
|
}
|
|
|
|
fn render_active_thread_or_empty_state(
|
|
&self,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> AnyElement {
|
|
if self.thread.read(cx).is_empty() {
|
|
return self
|
|
.render_thread_empty_state(window, cx)
|
|
.into_any_element();
|
|
}
|
|
|
|
self.thread.clone().into_any_element()
|
|
}
|
|
|
|
fn configuration_error(&self, cx: &App) -> Option<ConfigurationError> {
|
|
let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
|
|
return Some(ConfigurationError::NoProvider);
|
|
};
|
|
|
|
if !model.provider.is_authenticated(cx) {
|
|
return Some(ConfigurationError::ProviderNotAuthenticated);
|
|
}
|
|
|
|
if model.provider.must_accept_terms(cx) {
|
|
return Some(ConfigurationError::ProviderPendingTermsAcceptance(
|
|
model.provider,
|
|
));
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
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 configuration_error = self.configuration_error(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| {
|
|
let configuration_error_ref = &configuration_error;
|
|
this.child(
|
|
v_flex()
|
|
.size_full()
|
|
.max_w_80()
|
|
.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)
|
|
.mb_2p5(),
|
|
),
|
|
)
|
|
.child(
|
|
Button::new("new-thread", "Start New Thread")
|
|
.icon(IconName::Plus)
|
|
.icon_position(IconPosition::Start)
|
|
.icon_size(IconSize::Small)
|
|
.icon_color(Color::Muted)
|
|
.full_width()
|
|
.key_binding(KeyBinding::for_action_in(
|
|
&NewThread::default(),
|
|
&focus_handle,
|
|
window,
|
|
cx,
|
|
))
|
|
.on_click(|_event, window, cx| {
|
|
window.dispatch_action(NewThread::default().boxed_clone(), cx)
|
|
}),
|
|
)
|
|
.child(
|
|
Button::new("context", "Add Context")
|
|
.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")
|
|
.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")
|
|
.icon(IconName::Settings)
|
|
.icon_position(IconPosition::Start)
|
|
.icon_size(IconSize::Small)
|
|
.icon_color(Color::Muted)
|
|
.full_width()
|
|
.key_binding(KeyBinding::for_action_in(
|
|
&OpenConfiguration,
|
|
&focus_handle,
|
|
window,
|
|
cx,
|
|
))
|
|
.on_click(|_event, window, cx| {
|
|
window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
|
|
}),
|
|
)
|
|
})
|
|
.map(|parent| {
|
|
match configuration_error_ref {
|
|
Some(ConfigurationError::ProviderNotAuthenticated)
|
|
| Some(ConfigurationError::NoProvider) => {
|
|
parent
|
|
.child(
|
|
h_flex().child(
|
|
Label::new("To start using the agent, configure at least one LLM provider.")
|
|
.color(Color::Muted)
|
|
.mb_2p5()
|
|
)
|
|
)
|
|
.child(
|
|
Button::new("settings", "Configure a Provider")
|
|
.icon(IconName::Settings)
|
|
.icon_position(IconPosition::Start)
|
|
.icon_size(IconSize::Small)
|
|
.icon_color(Color::Muted)
|
|
.full_width()
|
|
.key_binding(KeyBinding::for_action_in(
|
|
&OpenConfiguration,
|
|
&focus_handle,
|
|
window,
|
|
cx,
|
|
))
|
|
.on_click(|_event, window, cx| {
|
|
window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
|
|
}),
|
|
)
|
|
}
|
|
Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
|
|
parent.children(
|
|
provider.render_accept_terms(
|
|
LanguageModelProviderTosView::ThreadFreshStart,
|
|
cx,
|
|
),
|
|
)
|
|
}
|
|
None => parent,
|
|
}
|
|
})
|
|
)
|
|
})
|
|
.when(!recent_history.is_empty(), |parent| {
|
|
let focus_handle = focus_handle.clone();
|
|
let configuration_error_ref = &configuration_error;
|
|
|
|
parent
|
|
.overflow_hidden()
|
|
.p_1p5()
|
|
.justify_end()
|
|
.gap_1()
|
|
.child(
|
|
h_flex()
|
|
.pl_1p5()
|
|
.pb_1()
|
|
.w_full()
|
|
.justify_between()
|
|
.border_b_1()
|
|
.border_color(cx.theme().colors().border_variant)
|
|
.child(
|
|
Label::new("Recent")
|
|
.size(LabelSize::Small)
|
|
.color(Color::Muted),
|
|
)
|
|
.child(
|
|
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);
|
|
}),
|
|
),
|
|
)
|
|
.child(
|
|
v_flex()
|
|
.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()
|
|
}),
|
|
)
|
|
)
|
|
.map(|parent| {
|
|
match configuration_error_ref {
|
|
Some(ConfigurationError::ProviderNotAuthenticated)
|
|
| Some(ConfigurationError::NoProvider) => {
|
|
parent
|
|
.child(
|
|
Banner::new()
|
|
.severity(ui::Severity::Warning)
|
|
.child(
|
|
Label::new(
|
|
"Configure at least one LLM provider to start using the panel.",
|
|
)
|
|
.size(LabelSize::Small),
|
|
)
|
|
.action_slot(
|
|
Button::new("settings", "Configure Provider")
|
|
.style(ButtonStyle::Tinted(ui::TintColor::Warning))
|
|
.label_size(LabelSize::Small)
|
|
.key_binding(
|
|
KeyBinding::for_action_in(
|
|
&OpenConfiguration,
|
|
&focus_handle,
|
|
window,
|
|
cx,
|
|
)
|
|
.map(|kb| kb.size(rems_from_px(12.))),
|
|
)
|
|
.on_click(|_event, window, cx| {
|
|
window.dispatch_action(
|
|
OpenConfiguration.boxed_clone(),
|
|
cx,
|
|
)
|
|
}),
|
|
),
|
|
)
|
|
}
|
|
Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
|
|
parent
|
|
.child(
|
|
Banner::new()
|
|
.severity(ui::Severity::Warning)
|
|
.child(
|
|
h_flex()
|
|
.w_full()
|
|
.children(
|
|
provider.render_accept_terms(
|
|
LanguageModelProviderTosView::ThreadtEmptyState,
|
|
cx,
|
|
),
|
|
),
|
|
),
|
|
)
|
|
}
|
|
None => parent,
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
fn render_tool_use_limit_reached(
|
|
&self,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> Option<AnyElement> {
|
|
let tool_use_limit_reached = self
|
|
.thread
|
|
.read(cx)
|
|
.thread()
|
|
.read(cx)
|
|
.tool_use_limit_reached();
|
|
if !tool_use_limit_reached {
|
|
return None;
|
|
}
|
|
|
|
let model = self
|
|
.thread
|
|
.read(cx)
|
|
.thread()
|
|
.read(cx)
|
|
.configured_model()?
|
|
.model;
|
|
|
|
let focus_handle = self.focus_handle(cx);
|
|
|
|
let banner = Banner::new()
|
|
.severity(ui::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_max_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(cx.listener(|this, _, window, cx| {
|
|
this.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 render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
|
let last_error = self.thread.read(cx).last_error()?;
|
|
|
|
Some(
|
|
div()
|
|
.absolute()
|
|
.right_3()
|
|
.bottom_12()
|
|
.max_w_96()
|
|
.py_2()
|
|
.px_3()
|
|
.elevation_2(cx)
|
|
.occlude()
|
|
.child(match last_error {
|
|
ThreadError::PaymentRequired => self.render_payment_required_error(cx),
|
|
ThreadError::ModelRequestLimitReached { plan } => {
|
|
self.render_model_request_limit_reached_error(plan, cx)
|
|
}
|
|
ThreadError::Message { header, message } => {
|
|
self.render_error_message(header, message, cx)
|
|
}
|
|
})
|
|
.into_any(),
|
|
)
|
|
}
|
|
|
|
fn render_payment_required_error(&self, cx: &mut Context<Self>) -> AnyElement {
|
|
const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used.";
|
|
|
|
v_flex()
|
|
.gap_0p5()
|
|
.child(
|
|
h_flex()
|
|
.gap_1p5()
|
|
.items_center()
|
|
.child(Icon::new(IconName::XCircle).color(Color::Error))
|
|
.child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
|
|
)
|
|
.child(
|
|
div()
|
|
.id("error-message")
|
|
.max_h_24()
|
|
.overflow_y_scroll()
|
|
.child(Label::new(ERROR_MESSAGE)),
|
|
)
|
|
.child(
|
|
h_flex()
|
|
.justify_end()
|
|
.mt_1()
|
|
.gap_1()
|
|
.child(self.create_copy_button(ERROR_MESSAGE))
|
|
.child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
|
|
|this, _, _, cx| {
|
|
this.thread.update(cx, |this, _cx| {
|
|
this.clear_last_error();
|
|
});
|
|
|
|
cx.open_url(&zed_urls::account_url(cx));
|
|
cx.notify();
|
|
},
|
|
)))
|
|
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|
|
|this, _, _, cx| {
|
|
this.thread.update(cx, |this, _cx| {
|
|
this.clear_last_error();
|
|
});
|
|
|
|
cx.notify();
|
|
},
|
|
))),
|
|
)
|
|
.into_any()
|
|
}
|
|
|
|
fn render_model_request_limit_reached_error(
|
|
&self,
|
|
plan: Plan,
|
|
cx: &mut Context<Self>,
|
|
) -> AnyElement {
|
|
let error_message = match plan {
|
|
Plan::ZedPro => {
|
|
"Model request limit reached. Upgrade to usage-based billing for more requests."
|
|
}
|
|
Plan::ZedProTrial => {
|
|
"Model request limit reached. Upgrade to Zed Pro for more requests."
|
|
}
|
|
Plan::Free => "Model request limit reached. Upgrade to Zed Pro for more requests.",
|
|
};
|
|
let call_to_action = match plan {
|
|
Plan::ZedPro => "Upgrade to usage-based billing",
|
|
Plan::ZedProTrial => "Upgrade to Zed Pro",
|
|
Plan::Free => "Upgrade to Zed Pro",
|
|
};
|
|
|
|
v_flex()
|
|
.gap_0p5()
|
|
.child(
|
|
h_flex()
|
|
.gap_1p5()
|
|
.items_center()
|
|
.child(Icon::new(IconName::XCircle).color(Color::Error))
|
|
.child(Label::new("Model Request Limit Reached").weight(FontWeight::MEDIUM)),
|
|
)
|
|
.child(
|
|
div()
|
|
.id("error-message")
|
|
.max_h_24()
|
|
.overflow_y_scroll()
|
|
.child(Label::new(error_message)),
|
|
)
|
|
.child(
|
|
h_flex()
|
|
.justify_end()
|
|
.mt_1()
|
|
.gap_1()
|
|
.child(self.create_copy_button(error_message))
|
|
.child(
|
|
Button::new("subscribe", call_to_action).on_click(cx.listener(
|
|
|this, _, _, cx| {
|
|
this.thread.update(cx, |this, _cx| {
|
|
this.clear_last_error();
|
|
});
|
|
|
|
cx.open_url(&zed_urls::account_url(cx));
|
|
cx.notify();
|
|
},
|
|
)),
|
|
)
|
|
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|
|
|this, _, _, cx| {
|
|
this.thread.update(cx, |this, _cx| {
|
|
this.clear_last_error();
|
|
});
|
|
|
|
cx.notify();
|
|
},
|
|
))),
|
|
)
|
|
.into_any()
|
|
}
|
|
|
|
fn render_error_message(
|
|
&self,
|
|
header: SharedString,
|
|
message: SharedString,
|
|
cx: &mut Context<Self>,
|
|
) -> AnyElement {
|
|
let message_with_header = format!("{}\n{}", header, message);
|
|
v_flex()
|
|
.gap_0p5()
|
|
.child(
|
|
h_flex()
|
|
.gap_1p5()
|
|
.items_center()
|
|
.child(Icon::new(IconName::XCircle).color(Color::Error))
|
|
.child(Label::new(header).weight(FontWeight::MEDIUM)),
|
|
)
|
|
.child(
|
|
div()
|
|
.id("error-message")
|
|
.max_h_32()
|
|
.overflow_y_scroll()
|
|
.child(Label::new(message.clone())),
|
|
)
|
|
.child(
|
|
h_flex()
|
|
.justify_end()
|
|
.mt_1()
|
|
.gap_1()
|
|
.child(self.create_copy_button(message_with_header))
|
|
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|
|
|this, _, _, cx| {
|
|
this.thread.update(cx, |this, _cx| {
|
|
this.clear_last_error();
|
|
});
|
|
|
|
cx.notify();
|
|
},
|
|
))),
|
|
)
|
|
.into_any()
|
|
}
|
|
|
|
fn render_prompt_editor(
|
|
&self,
|
|
context_editor: &Entity<ContextEditor>,
|
|
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 { .. } => {
|
|
let context_store = self.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::TextThread { context_editor, .. } => {
|
|
context_editor.update(cx, |context_editor, cx| {
|
|
ContextEditor::insert_dragged_files(
|
|
context_editor,
|
|
paths,
|
|
added_worktrees,
|
|
window,
|
|
cx,
|
|
);
|
|
});
|
|
}
|
|
ActiveView::History | ActiveView::Configuration => {}
|
|
}
|
|
}
|
|
|
|
fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
|
|
let message = message.into();
|
|
IconButton::new("copy", IconName::Copy)
|
|
.on_click(move |_, _, cx| {
|
|
cx.write_to_clipboard(ClipboardItem::new_string(message.clone()))
|
|
})
|
|
.tooltip(Tooltip::text("Copy Error Message"))
|
|
}
|
|
|
|
fn key_context(&self) -> KeyContext {
|
|
let mut key_context = KeyContext::new_with_defaults();
|
|
key_context.add("AgentPanel");
|
|
if matches!(self.active_view, ActiveView::TextThread { .. }) {
|
|
key_context.add("prompt_editor");
|
|
}
|
|
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()
|
|
.key_context(self.key_context())
|
|
.justify_between()
|
|
.size_full()
|
|
.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, _: &OpenConfiguration, 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| {
|
|
this.thread.update(cx, |active_thread, cx| {
|
|
active_thread.thread().update(cx, |thread, _cx| {
|
|
thread.set_completion_mode(CompletionMode::Burn);
|
|
});
|
|
});
|
|
this.continue_conversation(window, cx);
|
|
}))
|
|
.on_action(cx.listener(Self::toggle_burn_mode))
|
|
.child(self.render_toolbar(window, cx))
|
|
.children(self.render_upsell(window, cx))
|
|
.children(self.render_trial_end_upsell(window, cx))
|
|
.map(|parent| match &self.active_view {
|
|
ActiveView::Thread { .. } => parent
|
|
.relative()
|
|
.child(self.render_active_thread_or_empty_state(window, cx))
|
|
.children(self.render_tool_use_limit_reached(window, cx))
|
|
.child(h_flex().child(self.message_editor.clone()))
|
|
.children(self.render_last_error(cx))
|
|
.child(self.render_drag_target(cx)),
|
|
ActiveView::History => parent.child(self.history.clone()),
|
|
ActiveView::TextThread {
|
|
context_editor,
|
|
buffer_search_bar,
|
|
..
|
|
} => parent.child(self.render_prompt_editor(
|
|
context_editor,
|
|
buffer_search_bar,
|
|
window,
|
|
cx,
|
|
)),
|
|
ActiveView::Configuration => parent.children(self.configuration.clone()),
|
|
});
|
|
|
|
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<ContextEditor>> {
|
|
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_editor::ContextId,
|
|
_window: &mut Window,
|
|
_cx: &mut Context<Workspace>,
|
|
) -> Task<Result<Entity<ContextEditor>>> {
|
|
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 panel.has_active_thread() {
|
|
panel.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 Upsell;
|
|
|
|
impl Dismissable for Upsell {
|
|
const KEY: &'static str = "dismissed-trial-upsell";
|
|
}
|
|
|
|
struct TrialEndUpsell;
|
|
|
|
impl Dismissable for TrialEndUpsell {
|
|
const KEY: &'static str = "dismissed-trial-end-upsell";
|
|
}
|