Detailed changes
@@ -32,9 +32,6 @@
/crates/agent_ui/ @zed-industries/ai-team
/crates/ai_onboarding/ @zed-industries/ai-team
/crates/anthropic/ @zed-industries/ai-team
-/crates/assistant_slash_command/ @zed-industries/ai-team
-/crates/assistant_slash_commands/ @zed-industries/ai-team
-/crates/assistant_text_thread/ @zed-industries/ai-team
/crates/bedrock/ @zed-industries/ai-team
/crates/cloud_llm_client/ @zed-industries/ai-team
/crates/codestral/ @zed-industries/ai-team
@@ -335,9 +335,6 @@ dependencies = [
"agent_settings",
"ai_onboarding",
"anyhow",
- "assistant_slash_command",
- "assistant_slash_commands",
- "assistant_text_thread",
"audio",
"base64 0.22.1",
"buffer_diff",
@@ -393,7 +390,6 @@ dependencies = [
"rope",
"rules_library",
"schemars",
- "search",
"semver",
"serde",
"serde_json",
@@ -783,108 +779,6 @@ dependencies = [
"rust-embed",
]
-[[package]]
-name = "assistant_slash_command"
-version = "0.1.0"
-dependencies = [
- "anyhow",
- "async-trait",
- "collections",
- "derive_more",
- "extension",
- "futures 0.3.31",
- "gpui",
- "language",
- "language_model",
- "parking_lot",
- "pretty_assertions",
- "serde",
- "serde_json",
- "ui",
- "util",
- "workspace",
-]
-
-[[package]]
-name = "assistant_slash_commands"
-version = "0.1.0"
-dependencies = [
- "anyhow",
- "assistant_slash_command",
- "chrono",
- "collections",
- "editor",
- "feature_flags",
- "fs",
- "futures 0.3.31",
- "fuzzy",
- "gpui",
- "html_to_markdown",
- "http_client",
- "language",
- "multi_buffer",
- "pretty_assertions",
- "project",
- "prompt_store",
- "rope",
- "serde",
- "serde_json",
- "settings",
- "smol",
- "text",
- "ui",
- "util",
- "workspace",
- "worktree",
- "zlog",
-]
-
-[[package]]
-name = "assistant_text_thread"
-version = "0.1.0"
-dependencies = [
- "agent_settings",
- "anyhow",
- "assistant_slash_command",
- "assistant_slash_commands",
- "chrono",
- "client",
- "clock",
- "collections",
- "context_server",
- "fs",
- "futures 0.3.31",
- "fuzzy",
- "gpui",
- "itertools 0.14.0",
- "language",
- "language_model",
- "log",
- "open_ai",
- "parking_lot",
- "paths",
- "pretty_assertions",
- "project",
- "prompt_store",
- "proto",
- "rand 0.9.2",
- "regex",
- "rpc",
- "serde",
- "serde_json",
- "settings",
- "smallvec",
- "smol",
- "telemetry",
- "text",
- "ui",
- "unindent",
- "util",
- "uuid",
- "workspace",
- "zed_env_vars",
-]
-
[[package]]
name = "async-attributes"
version = "1.1.2"
@@ -3183,8 +3077,6 @@ version = "0.44.0"
dependencies = [
"agent",
"anyhow",
- "assistant_slash_command",
- "assistant_text_thread",
"async-trait",
"async-tungstenite",
"aws-config",
@@ -9446,7 +9338,6 @@ dependencies = [
"open_ai",
"open_router",
"parking_lot",
- "proto",
"schemars",
"serde",
"serde_json",
@@ -15965,7 +15856,6 @@ dependencies = [
"agent_settings",
"agent_ui",
"anyhow",
- "assistant_text_thread",
"chrono",
"collections",
"editor",
@@ -17510,7 +17400,6 @@ name = "terminal_view"
version = "0.1.0"
dependencies = [
"anyhow",
- "assistant_slash_command",
"async-recursion",
"breadcrumbs",
"collections",
@@ -13,9 +13,6 @@ members = [
"crates/anthropic",
"crates/askpass",
"crates/assets",
- "crates/assistant_slash_command",
- "crates/assistant_slash_commands",
- "crates/assistant_text_thread",
"crates/audio",
"crates/auto_update",
"crates/auto_update_helper",
@@ -271,9 +268,6 @@ ai_onboarding = { path = "crates/ai_onboarding" }
anthropic = { path = "crates/anthropic" }
askpass = { path = "crates/askpass" }
assets = { path = "crates/assets" }
-assistant_text_thread = { path = "crates/assistant_text_thread" }
-assistant_slash_command = { path = "crates/assistant_slash_command" }
-assistant_slash_commands = { path = "crates/assistant_slash_commands" }
audio = { path = "crates/audio" }
auto_update = { path = "crates/auto_update" }
auto_update_ui = { path = "crates/auto_update_ui" }
@@ -1,7 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M7.33333 8H2" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M10.6667 5H2" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M9 11H2" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M12 7V11" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M14 9H10" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
-</svg>
@@ -148,7 +148,6 @@
"ctrl-f": "buffer_search::Deploy",
"ctrl-h": "buffer_search::DeployReplace",
"ctrl->": "agent::AddSelectionToThread",
- "ctrl-<": "assistant::InsertIntoEditor",
"ctrl-alt-e": "editor::SelectEnclosingSymbol",
"ctrl-shift-backspace": "editor::GoToPreviousChange",
"ctrl-shift-alt-backspace": "editor::GoToNextChange",
@@ -185,7 +184,7 @@
},
},
{
- "context": "Editor && jupyter && !ContextEditor",
+ "context": "Editor && jupyter",
"bindings": {
"ctrl-shift-enter": "repl::Run",
"ctrl-alt-enter": "repl::RunInPlace",
@@ -221,29 +220,10 @@
"shift-alt-z": "agent::RejectAll",
},
},
- {
- "context": "ContextEditor > Editor",
- "bindings": {
- "ctrl-enter": "assistant::Assist",
- "save": "workspace::Save",
- "ctrl-s": "workspace::Save",
- "ctrl-<": "assistant::InsertIntoEditor",
- "shift-enter": "assistant::Split",
- "ctrl-r": "assistant::CycleMessageRole",
- "enter": "assistant::ConfirmCommand",
- "alt-enter": "editor::Newline",
- "ctrl-k c": "assistant::CopyCode",
- "ctrl-g": "search::SelectNextMatch",
- "ctrl-shift-g": "search::SelectPreviousMatch",
- "ctrl-k l": "agent::OpenRulesLibrary",
- "ctrl-shift-v": "agent::PasteRaw",
- },
- },
{
"context": "AgentPanel",
"bindings": {
"ctrl-n": "agent::NewThread",
- "ctrl-alt-n": "agent::NewTextThread",
"ctrl-shift-h": "agent::OpenHistory",
"ctrl-alt-c": "agent::OpenSettings",
"ctrl-alt-p": "agent::ManageProfiles",
@@ -278,13 +258,6 @@
"ctrl-c": "markdown::CopyAsMarkdown",
},
},
- {
- "context": "AgentPanel && text_thread",
- "bindings": {
- "ctrl-n": "agent::NewTextThread",
- "ctrl-alt-t": "agent::NewThread",
- },
- },
{
"context": "AgentPanel && acp_thread",
"use_key_equivalents": true,
@@ -719,8 +692,8 @@
"context": "ThreadSwitcher",
"bindings": {
"ctrl-tab": "agents_sidebar::ToggleThreadSwitcher",
- "ctrl-shift-tab": ["agents_sidebar::ToggleThreadSwitcher", { "select_last": true }]
- }
+ "ctrl-shift-tab": ["agents_sidebar::ToggleThreadSwitcher", { "select_last": true }],
+ },
},
{
"context": "Workspace && debugger_running",
@@ -858,7 +831,7 @@
},
},
{
- "context": "!ContextEditor && !AcpThread > Editor && mode == full",
+ "context": "!AcpThread > Editor && mode == full",
"bindings": {
"alt-enter": "editor::OpenExcerpts",
"shift-enter": "editor::ExpandExcerpts",
@@ -173,7 +173,6 @@
"cmd-alt-l": ["buffer_search::Deploy", { "selection_search_enabled": true }],
"cmd-e": ["buffer_search::Deploy", { "focus": false }],
"cmd->": "agent::AddSelectionToThread",
- "cmd-<": "assistant::InsertIntoEditor",
"cmd-alt-e": "editor::SelectEnclosingSymbol",
"alt-enter": "editor::OpenSelectionsInMultibuffer",
},
@@ -220,7 +219,7 @@
},
},
{
- "context": "Editor && jupyter && !ContextEditor",
+ "context": "Editor && jupyter",
"use_key_equivalents": true,
"bindings": {
"ctrl-shift-enter": "repl::Run",
@@ -260,31 +259,11 @@
"shift-ctrl-r": "agent::OpenAgentDiff",
},
},
- {
- "context": "ContextEditor > Editor",
- "use_key_equivalents": true,
- "bindings": {
- "cmd-enter": "assistant::Assist",
- "cmd-s": "workspace::Save",
- "cmd-<": "assistant::InsertIntoEditor",
- "shift-enter": "assistant::Split",
- "ctrl-r": "assistant::CycleMessageRole",
- "enter": "assistant::ConfirmCommand",
- "alt-enter": "editor::Newline",
- "cmd-k c": "assistant::CopyCode",
- "cmd-g": "search::SelectNextMatch",
- "cmd-shift-g": "search::SelectPreviousMatch",
- "cmd-k l": "agent::OpenRulesLibrary",
- "alt-tab": "agent::CycleFavoriteModels",
- "cmd-shift-v": "agent::PasteRaw",
- },
- },
{
"context": "AgentPanel",
"use_key_equivalents": true,
"bindings": {
"cmd-n": "agent::NewThread",
- "cmd-alt-n": "agent::NewTextThread",
"cmd-shift-h": "agent::OpenHistory",
"cmd-alt-c": "agent::OpenSettings",
"cmd-alt-l": "agent::OpenRulesLibrary",
@@ -315,14 +294,6 @@
"cmd-c": "markdown::CopyAsMarkdown",
},
},
- {
- "context": "AgentPanel && text_thread",
- "use_key_equivalents": true,
- "bindings": {
- "cmd-n": "agent::NewTextThread",
- "cmd-alt-n": "agent::NewExternalAgentThread",
- },
- },
{
"context": "AgentPanel && acp_thread",
"use_key_equivalents": true,
@@ -784,8 +755,8 @@
"context": "ThreadSwitcher",
"bindings": {
"ctrl-tab": "agents_sidebar::ToggleThreadSwitcher",
- "ctrl-shift-tab": ["agents_sidebar::ToggleThreadSwitcher", { "select_last": true }]
- }
+ "ctrl-shift-tab": ["agents_sidebar::ToggleThreadSwitcher", { "select_last": true }],
+ },
},
{
"context": "Workspace && debugger_running",
@@ -918,7 +889,7 @@
},
},
{
- "context": "!ContextEditor && !AcpThread > Editor && mode == full",
+ "context": "!AcpThread > Editor && mode == full",
"use_key_equivalents": true,
"bindings": {
"alt-enter": "editor::OpenExcerpts",
@@ -143,7 +143,6 @@
"ctrl-f": "buffer_search::Deploy",
"ctrl-h": "buffer_search::DeployReplace",
"ctrl-shift-.": "agent::AddSelectionToThread",
- "ctrl-shift-,": "assistant::InsertIntoEditor",
"shift-alt-e": "editor::SelectEnclosingSymbol",
"ctrl-shift-backspace": "editor::GoToPreviousChange",
"ctrl-shift-alt-backspace": "editor::GoToNextChange",
@@ -182,7 +181,7 @@
},
},
{
- "context": "Editor && jupyter && !ContextEditor",
+ "context": "Editor && jupyter",
"use_key_equivalents": true,
"bindings": {
"ctrl-shift-enter": "repl::Run",
@@ -221,30 +220,11 @@
"shift-alt-z": "agent::RejectAll",
},
},
- {
- "context": "ContextEditor > Editor",
- "use_key_equivalents": true,
- "bindings": {
- "ctrl-i": "assistant::Assist",
- "ctrl-s": "workspace::Save",
- "ctrl-shift-,": "assistant::InsertIntoEditor",
- "shift-enter": "assistant::Split",
- "ctrl-r": "assistant::CycleMessageRole",
- "enter": "assistant::ConfirmCommand",
- "alt-enter": "editor::Newline",
- "ctrl-k c": "assistant::CopyCode",
- "ctrl-g": "search::SelectNextMatch",
- "ctrl-shift-g": "search::SelectPreviousMatch",
- "ctrl-k l": "agent::OpenRulesLibrary",
- "ctrl-shift-v": "agent::PasteRaw",
- },
- },
{
"context": "AgentPanel",
"use_key_equivalents": true,
"bindings": {
"ctrl-n": "agent::NewThread",
- "shift-alt-n": "agent::NewTextThread",
"ctrl-shift-h": "agent::OpenHistory",
"shift-alt-c": "agent::OpenSettings",
"shift-alt-l": "agent::OpenRulesLibrary",
@@ -278,14 +258,6 @@
"ctrl-c": "markdown::CopyAsMarkdown",
},
},
- {
- "context": "AgentPanel && text_thread",
- "use_key_equivalents": true,
- "bindings": {
- "ctrl-n": "agent::NewTextThread",
- "ctrl-alt-t": "agent::NewThread",
- },
- },
{
"context": "AgentPanel && acp_thread",
"use_key_equivalents": true,
@@ -721,8 +693,8 @@
"context": "ThreadSwitcher",
"bindings": {
"ctrl-tab": "agents_sidebar::ToggleThreadSwitcher",
- "ctrl-shift-tab": ["agents_sidebar::ToggleThreadSwitcher", { "select_last": true }]
- }
+ "ctrl-shift-tab": ["agents_sidebar::ToggleThreadSwitcher", { "select_last": true }],
+ },
},
{
"context": "ApplicationMenu",
@@ -858,7 +830,7 @@
},
},
{
- "context": "!ContextEditor && !AcpThread > Editor && mode == full",
+ "context": "!AcpThread > Editor && mode == full",
"use_key_equivalents": true,
"bindings": {
"alt-enter": "editor::OpenExcerpts",
@@ -20,7 +20,6 @@
"ctrl-shift-l": "agent::AddSelectionToThread", // In cursor uses "Ask" mode
"ctrl-l": "agent::AddSelectionToThread", // In cursor uses "Agent" mode
"ctrl-k": "assistant::InlineAssist",
- "ctrl-shift-k": "assistant::InsertIntoEditor",
},
},
{
@@ -34,7 +33,7 @@
},
},
{
- "context": "AgentPanel || ContextEditor || (MessageEditor > Editor)",
+ "context": "AgentPanel || (MessageEditor > Editor)",
"use_key_equivalents": true,
"bindings": {
"ctrl-i": "workspace::ToggleRightDock",
@@ -47,7 +46,6 @@
"ctrl-shift-backspace": "editor::Cancel",
"ctrl-r": "agent::NewThread",
"ctrl-shift-v": "editor::Paste",
- "ctrl-shift-k": "assistant::InsertIntoEditor",
// "escape": "agent::ToggleFocus"
///// Enable when Zed supports multiple thread tabs
// "ctrl-t": // new thread tab
@@ -20,7 +20,6 @@
"cmd-shift-l": "agent::AddSelectionToThread", // In cursor uses "Ask" mode
"cmd-l": "agent::AddSelectionToThread", // In cursor uses "Agent" mode
"cmd-k": "assistant::InlineAssist",
- "cmd-shift-k": "assistant::InsertIntoEditor",
},
},
{
@@ -35,7 +34,7 @@
},
},
{
- "context": "AgentPanel || ContextEditor || (MessageEditor > Editor)",
+ "context": "AgentPanel || (MessageEditor > Editor)",
"use_key_equivalents": true,
"bindings": {
"cmd-i": "workspace::ToggleRightDock",
@@ -48,7 +47,6 @@
"cmd-shift-backspace": "editor::Cancel",
"cmd-r": "agent::NewThread",
"cmd-shift-v": "editor::Paste",
- "cmd-shift-k": "assistant::InsertIntoEditor",
// "escape": "agent::ToggleFocus"
///// Enable when Zed supports multiple thread tabs
// "cmd-t": // new thread tab
@@ -960,8 +960,6 @@
"default_width": 640,
// Default height when the agent panel is docked to the bottom.
"default_height": 320,
- // The view to use by default (thread, or text_thread)
- "default_view": "thread",
// The default model to use when creating new threads.
"default_model": {
// The provider to use.
@@ -1614,9 +1612,6 @@
"prompt_format": "infer",
"max_output_tokens": 64,
},
- // Whether edit predictions are enabled when editing text threads in the agent panel.
- // This setting has no effect if globally disabled.
- "enabled_in_text_threads": true,
},
// Settings specific to journaling
"journal": {
@@ -32,10 +32,6 @@ pub enum MentionUri {
id: acp::SessionId,
name: String,
},
- TextThread {
- path: PathBuf,
- name: String,
- },
Rule {
id: PromptId,
name: String,
@@ -137,12 +133,6 @@ impl MentionUri {
id: acp::SessionId::new(thread_id),
name,
})
- } else if let Some(path) = path.strip_prefix("/agent/text-thread/") {
- let name = single_query_param(&url, "name")?.context("Missing thread name")?;
- Ok(Self::TextThread {
- path: path.into(),
- name,
- })
} else if let Some(rule_id) = path.strip_prefix("/agent/rule/") {
let name = single_query_param(&url, "name")?.context("Missing rule name")?;
let rule_id = UserPromptId(rule_id.parse()?);
@@ -240,7 +230,6 @@ impl MentionUri {
MentionUri::PastedImage => "Image".to_string(),
MentionUri::Symbol { name, .. } => name.clone(),
MentionUri::Thread { name, .. } => name.clone(),
- MentionUri::TextThread { name, .. } => name.clone(),
MentionUri::Rule { name, .. } => name.clone(),
MentionUri::Diagnostics { .. } => "Diagnostics".to_string(),
MentionUri::TerminalSelection { line_count } => {
@@ -312,7 +301,6 @@ impl MentionUri {
.unwrap_or_else(|| IconName::Folder.path().into()),
MentionUri::Symbol { .. } => IconName::Code.path().into(),
MentionUri::Thread { .. } => IconName::Thread.path().into(),
- MentionUri::TextThread { .. } => IconName::Thread.path().into(),
MentionUri::Rule { .. } => IconName::Reader.path().into(),
MentionUri::Diagnostics { .. } => IconName::Warning.path().into(),
MentionUri::TerminalSelection { .. } => IconName::Terminal.path().into(),
@@ -381,15 +369,6 @@ impl MentionUri {
url.query_pairs_mut().append_pair("name", name);
url
}
- MentionUri::TextThread { path, name } => {
- let mut url = Url::parse("zed:///").unwrap();
- url.set_path(&format!(
- "/agent/text-thread/{}",
- path.to_string_lossy().trim_start_matches('/')
- ));
- url.query_pairs_mut().append_pair("name", name);
- url
- }
MentionUri::Rule { name, id } => {
let mut url = Url::parse("zed:///").unwrap();
url.set_path(&format!("/agent/rule/{id}"));
@@ -295,9 +295,6 @@ impl UserMessage {
MentionUri::Thread { .. } => {
write!(&mut thread_context, "\n{}\n", content).ok();
}
- MentionUri::TextThread { .. } => {
- write!(&mut thread_context, "\n{}\n", content).ok();
- }
MentionUri::Rule { .. } => {
write!(
&mut rules_context,
@@ -563,7 +563,7 @@ mod tests {
use crate::tools::{DeletePathTool, EditFileTool, FetchTool, TerminalTool};
use agent_settings::{AgentProfileId, CompiledRegex, InvalidRegexPattern, ToolRules};
use gpui::px;
- use settings::{DefaultAgentView, DockPosition, NotifyWhenAgentWaiting};
+ use settings::{DockPosition, NotifyWhenAgentWaiting};
use std::sync::Arc;
fn test_agent_settings(tool_permissions: ToolPermissions) -> AgentSettings {
@@ -582,7 +582,6 @@ mod tests {
inline_alternatives: vec![],
favorite_models: vec![],
default_profile: AgentProfileId::default(),
- default_view: DefaultAgentView::Thread,
profiles: Default::default(),
notify_when_agent_waiting: NotifyWhenAgentWaiting::default(),
play_sound_when_agent_done: false,
@@ -12,9 +12,9 @@ use project::DisableAiSettings;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{
- DefaultAgentView, DockPosition, DockSide, LanguageModelParameters, LanguageModelSelection,
- NewThreadLocation, NotifyWhenAgentWaiting, RegisterSetting, Settings, SettingsContent,
- SettingsStore, SidebarDockPosition, SidebarSide, ThinkingBlockDisplay, ToolPermissionMode,
+ DockPosition, DockSide, LanguageModelParameters, LanguageModelSelection, NewThreadLocation,
+ NotifyWhenAgentWaiting, RegisterSetting, Settings, SettingsContent, SettingsStore,
+ SidebarDockPosition, SidebarSide, ThinkingBlockDisplay, ToolPermissionMode,
update_settings_file,
};
@@ -162,7 +162,6 @@ pub struct AgentSettings {
pub inline_alternatives: Vec<LanguageModelSelection>,
pub favorite_models: Vec<LanguageModelSelection>,
pub default_profile: AgentProfileId,
- pub default_view: DefaultAgentView,
pub profiles: IndexMap<AgentProfileId, AgentProfileSettings>,
pub notify_when_agent_waiting: NotifyWhenAgentWaiting,
@@ -611,7 +610,6 @@ impl Settings for AgentSettings {
inline_alternatives: agent.inline_alternatives.unwrap_or_default(),
favorite_models: agent.favorite_models,
default_profile: AgentProfileId(agent.default_profile.unwrap()),
- default_view: agent.default_view.unwrap(),
profiles: agent
.profiles
.unwrap()
@@ -14,7 +14,6 @@ doctest = false
[features]
test-support = [
- "assistant_text_thread/test-support",
"acp_thread/test-support",
"eval_utils",
"gpui/test-support",
@@ -36,9 +35,6 @@ agent_settings.workspace = true
ai_onboarding.workspace = true
anyhow.workspace = true
heapless.workspace = true
-assistant_text_thread.workspace = true
-assistant_slash_command.workspace = true
-assistant_slash_commands.workspace = true
audio = { workspace = true, optional = true }
base64.workspace = true
buffer_diff.workspace = true
@@ -89,7 +85,6 @@ release_channel.workspace = true
rope.workspace = true
rules_library.workspace = true
schemars.workspace = true
-search.workspace = true
serde.workspace = true
serde_json.workspace = true
serde_json_lenient.workspace = true
@@ -119,7 +114,6 @@ reqwest_client = { workspace = true, optional = true }
[dev-dependencies]
acp_thread = { workspace = true, features = ["test-support"] }
agent = { workspace = true, features = ["test-support"] }
-assistant_text_thread = { workspace = true, features = ["test-support"] }
buffer_diff = { workspace = true, features = ["test-support"] }
db = { workspace = true, features = ["test-support"] }
@@ -1,6 +1,5 @@
use std::{
- ops::Range,
- path::{Path, PathBuf},
+ path::PathBuf,
rc::Rc,
sync::{
Arc,
@@ -22,19 +21,17 @@ use settings::{LanguageModelProviderSetting, LanguageModelSelection};
use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt as _};
use zed_actions::agent::{
- ConflictContent, OpenClaudeAgentOnboardingModal, ReauthenticateAgent,
+ AddSelectionToThread, ConflictContent, OpenClaudeAgentOnboardingModal, ReauthenticateAgent,
ResolveConflictedFilesWithAgent, ResolveConflictsWithAgent, ReviewBranchDiff,
};
use crate::{
AddContextServer, AgentDiffPane, ConversationView, CopyThreadToClipboard, CycleStartThreadIn,
- Follow, InlineAssistant, LoadThreadFromClipboard, NewTextThread, NewThread,
- OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ResetTrialEndUpsell, ResetTrialUpsell,
- StartThreadIn, ToggleNavigationMenu, ToggleNewThreadMenu, ToggleOptionsMenu,
+ Follow, InlineAssistant, LoadThreadFromClipboard, NewThread, OpenActiveThreadAsMarkdown,
+ OpenAgentDiff, OpenHistory, ResetTrialEndUpsell, ResetTrialUpsell, StartThreadIn,
+ ToggleNavigationMenu, ToggleNewThreadMenu, ToggleOptionsMenu,
agent_configuration::{AgentConfiguration, AssistantConfigurationEvent},
conversation_view::{AcpThreadViewEvent, ThreadView},
- slash_command::SlashCommandCompletionProvider,
- text_thread_editor::{AgentPanelDelegate, TextThreadEditor, make_lsp_adapter_delegate},
ui::EndTrialUpsell,
};
use crate::{
@@ -45,21 +42,16 @@ use crate::{
DEFAULT_THREAD_TITLE,
ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal, HoldForDefault},
};
-use crate::{
- ExpandMessageEditor, ThreadHistoryView,
- text_thread_history::{TextThreadHistory, TextThreadHistoryEvent},
-};
+use crate::{ExpandMessageEditor, ThreadHistoryView};
use crate::{ManageProfiles, ThreadHistoryViewEvent};
use crate::{ThreadHistory, agent_connection_store::AgentConnectionStore};
use agent_settings::AgentSettings;
use ai_onboarding::AgentPanelOnboarding;
use anyhow::{Context as _, Result, anyhow};
-use assistant_slash_command::SlashCommandWorkingSet;
-use assistant_text_thread::{TextThread, TextThreadEvent, TextThreadSummary};
use client::UserStore;
use cloud_api_types::Plan;
use collections::HashMap;
-use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
+use editor::Editor;
use extension::ExtensionEvents;
use extension_host::ExtensionStore;
use fs::Fs;
@@ -69,23 +61,21 @@ use gpui::{
Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between,
};
use language::LanguageRegistry;
-use language_model::{ConfigurationError, LanguageModelRegistry};
+use language_model::LanguageModelRegistry;
use project::project_settings::ProjectSettings;
use project::{Project, ProjectPath, Worktree};
-use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
+use prompt_store::{PromptStore, UserPromptId};
use rules_library::{RulesLibrary, open_rules_library};
-use search::{BufferSearchBar, buffer_search};
use settings::{Settings, update_settings_file};
use theme_settings::ThemeSettings;
use ui::{
Button, Callout, CommonAnimationExt, ContextMenu, ContextMenuEntry, DocumentationSide,
- KeyBinding, PopoverMenu, PopoverMenuHandle, Tab, Tooltip, prelude::*, utils::WithRemSize,
+ PopoverMenu, PopoverMenuHandle, Tab, Tooltip, prelude::*, utils::WithRemSize,
};
use util::{ResultExt as _, debug_panic};
use workspace::{
CollaboratorId, DraggedSelection, DraggedTab, OpenMode, OpenResult, PathList,
- SerializedPathList, ToggleWorkspaceSidebar, ToggleZoom, ToolbarItemView, Workspace,
- WorkspaceId,
+ SerializedPathList, ToggleWorkspaceSidebar, ToggleZoom, Workspace, WorkspaceId,
dock::{DockPosition, Panel, PanelEvent},
};
use zed_actions::{
@@ -132,7 +122,7 @@ fn read_legacy_serialized_panel(kvp: &KeyValueStore) -> Option<SerializedAgentPa
#[derive(Serialize, Deserialize, Debug)]
struct SerializedAgentPanel {
- selected_agent: Option<AgentType>,
+ selected_agent: Option<Agent>,
#[serde(default)]
last_active_thread: Option<SerializedActiveThread>,
#[serde(default)]
@@ -142,7 +132,7 @@ struct SerializedAgentPanel {
#[derive(Serialize, Deserialize, Debug)]
struct SerializedActiveThread {
session_id: String,
- agent_type: AgentType,
+ agent_type: Agent,
title: Option<String>,
work_dirs: Option<SerializedPathList>,
}
@@ -185,14 +175,6 @@ pub fn init(cx: &mut App) {
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_text_thread(window, cx);
- });
- }
- })
.register_action(|workspace, action: &NewExternalAgentThread, window, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx);
@@ -419,7 +401,28 @@ pub fn init(cx: &mut App) {
panel.cycle_start_thread_in(window, cx);
});
}
- });
+ })
+ .register_action(
+ |workspace: &mut Workspace, _: &AddSelectionToThread, window, cx| {
+ 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| {
+ cx.defer_in(window, move |panel, window, cx| {
+ if let Some(conversation_view) = panel.active_conversation_view() {
+ conversation_view.update(cx, |conversation_view, cx| {
+ conversation_view.insert_selections(window, cx);
+ });
+ }
+ });
+ });
+ },
+ );
},
)
.detach();
@@ -532,76 +535,22 @@ fn build_conflicted_files_resolution_prompt(
content
}
-#[derive(Clone, Debug, PartialEq, Eq)]
-enum History {
- AgentThreads { view: Entity<ThreadHistoryView> },
- TextThreads,
-}
-
enum ActiveView {
Uninitialized,
AgentThread {
conversation_view: Entity<ConversationView>,
},
- TextThread {
- text_thread_editor: Entity<TextThreadEditor>,
- title_editor: Entity<Editor>,
- buffer_search_bar: Entity<BufferSearchBar>,
- _subscriptions: Vec<gpui::Subscription>,
- },
History {
- history: History,
+ view: Entity<ThreadHistoryView>,
},
Configuration,
}
enum WhichFontSize {
AgentFont,
- BufferFont,
None,
}
-// TODO unify this with ExternalAgent
-#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
-pub enum AgentType {
- #[default]
- NativeAgent,
- TextThread,
- Custom {
- #[serde(rename = "name")]
- id: AgentId,
- },
-}
-
-impl AgentType {
- pub fn is_native(&self) -> bool {
- matches!(self, Self::NativeAgent)
- }
-
- fn label(&self) -> SharedString {
- match self {
- Self::NativeAgent | Self::TextThread => "Zed Agent".into(),
- Self::Custom { id, .. } => id.0.clone(),
- }
- }
-
- fn icon(&self) -> Option<IconName> {
- match self {
- Self::NativeAgent | Self::TextThread => None,
- Self::Custom { .. } => Some(IconName::Sparkle),
- }
- }
-}
-
-impl From<Agent> for AgentType {
- fn from(value: Agent) -> Self {
- match value {
- Agent::Custom { id } => Self::Custom { id },
- Agent::NativeAgent => Self::NativeAgent,
- }
- }
-}
-
impl StartThreadIn {
fn label(&self) -> SharedString {
match self {
@@ -624,97 +573,9 @@ impl ActiveView {
ActiveView::Uninitialized
| ActiveView::AgentThread { .. }
| ActiveView::History { .. } => WhichFontSize::AgentFont,
- ActiveView::TextThread { .. } => WhichFontSize::BufferFont,
ActiveView::Configuration => WhichFontSize::None,
}
}
-
- pub fn text_thread(
- text_thread_editor: Entity<TextThreadEditor>,
- language_registry: Arc<LanguageRegistry>,
- window: &mut Window,
- cx: &mut App,
- ) -> Self {
- let title = text_thread_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 text_thread_editor = text_thread_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);
-
- text_thread_editor.update(cx, |text_thread_editor, cx| {
- text_thread_editor
- .text_thread()
- .update(cx, |text_thread, cx| {
- text_thread.set_custom_summary(new_summary, cx);
- })
- })
- }
- EditorEvent::Blurred => {
- if editor.read(cx).text(cx).is_empty() {
- let summary = text_thread_editor
- .read(cx)
- .text_thread()
- .read(cx)
- .summary()
- .or_default();
-
- editor.update(cx, |editor, cx| {
- editor.set_text(summary, window, cx);
- });
- }
- }
- _ => {}
- }
- }
- }),
- window.subscribe(&text_thread_editor.read(cx).text_thread().clone(), cx, {
- let editor = editor.clone();
- move |text_thread, event, window, cx| match event {
- TextThreadEvent::SummaryGenerated => {
- let summary = text_thread.read(cx).summary().or_default();
-
- editor.update(cx, |editor, cx| {
- editor.set_text(summary, window, cx);
- })
- }
- TextThreadEvent::PathChanged { .. } => {}
- _ => {}
- }
- }),
- ];
-
- 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(&text_thread_editor), window, cx)
- });
-
- Self::TextThread {
- text_thread_editor,
- title_editor: editor,
- buffer_search_bar,
- _subscriptions: subscriptions,
- }
- }
}
pub struct AgentPanel {
@@ -725,9 +586,7 @@ pub struct AgentPanel {
project: Entity<Project>,
fs: Arc<dyn Fs>,
language_registry: Arc<LanguageRegistry>,
- text_thread_history: Entity<TextThreadHistory>,
thread_store: Entity<ThreadStore>,
- text_thread_store: Entity<assistant_text_thread::TextThreadStore>,
prompt_store: Option<Entity<PromptStore>>,
connection_store: Entity<AgentConnectionStore>,
context_server_registry: Entity<ContextServerRegistry>,
@@ -747,14 +606,13 @@ pub struct AgentPanel {
zoomed: bool,
pending_serialization: Option<Task<Result<()>>>,
onboarding: Entity<AgentPanelOnboarding>,
- selected_agent_type: AgentType,
+ selected_agent: Agent,
start_thread_in: StartThreadIn,
worktree_creation_status: Option<WorktreeCreationStatus>,
_thread_view_subscription: Option<Subscription>,
_active_thread_focus_subscription: Option<Subscription>,
_worktree_creation_task: Option<Task<()>>,
show_trust_workspace_message: bool,
- last_configuration_error_telemetry: Option<String>,
on_boarding_upsell_dismissed: AtomicBool,
_active_view_observation: Option<Subscription>,
}
@@ -765,7 +623,7 @@ impl AgentPanel {
return;
};
- let selected_agent_type = self.selected_agent_type.clone();
+ let selected_agent = self.selected_agent.clone();
let start_thread_in = Some(self.start_thread_in);
let last_active_thread = self.active_agent_thread(cx).map(|thread| {
@@ -774,7 +632,7 @@ impl AgentPanel {
let work_dirs = thread.work_dirs().cloned();
SerializedActiveThread {
session_id: thread.session_id().0.to_string(),
- agent_type: self.selected_agent_type.clone(),
+ agent_type: self.selected_agent.clone(),
title: title.map(|t| t.to_string()),
work_dirs: work_dirs.map(|dirs| dirs.serialize()),
}
@@ -785,7 +643,7 @@ impl AgentPanel {
save_serialized_panel(
workspace_id,
SerializedAgentPanel {
- selected_agent: Some(selected_agent_type),
+ selected_agent: Some(selected_agent),
last_active_thread,
start_thread_in,
},
@@ -798,7 +656,6 @@ impl AgentPanel {
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));
@@ -823,19 +680,6 @@ impl AgentPanel {
})
.await;
- let slash_commands = Arc::new(SlashCommandWorkingSet::default());
- let text_thread_store = workspace
- .update(cx, |workspace, cx| {
- let project = workspace.project().clone();
- assistant_text_thread::TextThreadStore::new(
- project,
- prompt_builder,
- slash_commands,
- cx,
- )
- })?
- .await?;
-
let last_active_thread = if let Some(thread_info) = serialized_panel
.as_ref()
.and_then(|p| p.last_active_thread.as_ref())
@@ -869,12 +713,12 @@ impl AgentPanel {
let panel = workspace.update_in(cx, |workspace, window, cx| {
let panel =
- cx.new(|cx| Self::new(workspace, text_thread_store, prompt_store, window, cx));
+ cx.new(|cx| Self::new(workspace, prompt_store, window, cx));
if let Some(serialized_panel) = &serialized_panel {
panel.update(cx, |panel, cx| {
if let Some(selected_agent) = serialized_panel.selected_agent.clone() {
- panel.selected_agent_type = selected_agent;
+ panel.selected_agent = selected_agent;
}
if let Some(start_thread_in) = serialized_panel.start_thread_in {
let is_worktree_flag_enabled =
@@ -900,20 +744,18 @@ impl AgentPanel {
}
if let Some(thread_info) = last_active_thread {
- let agent_type = thread_info.agent_type.clone();
+ let agent = thread_info.agent_type.clone();
panel.update(cx, |panel, cx| {
- panel.selected_agent_type = agent_type;
- if let Some(agent) = panel.selected_agent() {
- panel.load_agent_thread(
- agent,
- thread_info.session_id.clone().into(),
- thread_info.work_dirs.as_ref().map(|dirs| PathList::deserialize(dirs)),
- thread_info.title.as_ref().map(|t| t.clone().into()),
- false,
- window,
- cx,
- );
- }
+ panel.selected_agent = agent.clone();
+ panel.load_agent_thread(
+ agent,
+ thread_info.session_id.clone().into(),
+ thread_info.work_dirs.as_ref().map(|dirs| PathList::deserialize(dirs)),
+ thread_info.title.as_ref().map(|t| t.clone().into()),
+ false,
+ window,
+ cx,
+ );
});
}
panel
@@ -925,7 +767,6 @@ impl AgentPanel {
pub(crate) fn new(
workspace: &Workspace,
- text_thread_store: Entity<assistant_text_thread::TextThreadStore>,
prompt_store: Option<Entity<PromptStore>>,
window: &mut Window,
cx: &mut Context<Self>,
@@ -942,20 +783,6 @@ impl AgentPanel {
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let thread_store = ThreadStore::global(cx);
- let text_thread_history =
- cx.new(|cx| TextThreadHistory::new(text_thread_store.clone(), window, cx));
-
- cx.subscribe_in(
- &text_thread_history,
- window,
- |this, _, event, window, cx| match event {
- TextThreadHistoryEvent::Open(thread) => {
- this.open_saved_text_thread(thread.path.clone(), window, cx)
- .detach_and_log_err(cx);
- }
- },
- )
- .detach();
let active_view = ActiveView::Uninitialized;
@@ -969,14 +796,10 @@ impl AgentPanel {
if let Some(history) = panel
.update(cx, |panel, cx| panel.history_for_selected_agent(window, cx))
{
- let view_all_label = match history {
- History::AgentThreads { .. } => "View All",
- History::TextThreads => "View All Text Threads",
- };
menu = Self::populate_recently_updated_menu_section(
menu, panel, history, cx,
);
- menu = menu.action(view_all_label, Box::new(OpenHistory));
+ menu = menu.action("View All", Box::new(OpenHistory));
}
}
@@ -1070,7 +893,6 @@ impl AgentPanel {
project: project.clone(),
fs: fs.clone(),
language_registry,
- text_thread_store,
prompt_store,
connection_store,
configuration: None,
@@ -1089,16 +911,14 @@ impl AgentPanel {
zoomed: false,
pending_serialization: None,
onboarding,
- text_thread_history,
thread_store,
- selected_agent_type: AgentType::default(),
+ selected_agent: Agent::default(),
start_thread_in: StartThreadIn::default(),
worktree_creation_status: None,
_thread_view_subscription: None,
_active_thread_focus_subscription: None,
_worktree_creation_task: None,
show_trust_workspace_message: false,
- last_configuration_error_telemetry: None,
on_boarding_upsell_dismissed: AtomicBool::new(OnboardingUpsell::dismissed(cx)),
_active_view_observation: None,
};
@@ -1240,49 +1060,6 @@ impl AgentPanel {
.detach_and_log_err(cx);
}
- fn new_text_thread(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- telemetry::event!("Agent Thread Started", agent = "zed-text");
-
- let context = self
- .text_thread_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 text_thread_editor = cx.new(|cx| {
- let mut editor = TextThreadEditor::for_text_thread(
- context,
- self.fs.clone(),
- self.workspace.clone(),
- self.project.clone(),
- lsp_adapter_delegate,
- window,
- cx,
- );
- editor.insert_default_prompt(window, cx);
- editor
- });
-
- if self.selected_agent_type != AgentType::TextThread {
- self.selected_agent_type = AgentType::TextThread;
- self.serialize(cx);
- }
-
- self.set_active_view(
- ActiveView::text_thread(
- text_thread_editor.clone(),
- self.language_registry.clone(),
- window,
- cx,
- ),
- true,
- window,
- cx,
- );
- text_thread_editor.focus_handle(cx).focus(window, cx);
- }
-
fn external_thread(
&mut self,
agent_choice: Option<crate::Agent>,
@@ -1387,13 +1164,6 @@ impl AgentPanel {
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()),
@@ -1418,15 +1188,13 @@ impl AgentPanel {
}
fn has_history_for_selected_agent(&self, cx: &App) -> bool {
- match &self.selected_agent_type {
- AgentType::TextThread | AgentType::NativeAgent => true,
- AgentType::Custom { id } => {
- let agent = Agent::Custom { id: id.clone() };
- self.connection_store
- .read(cx)
- .entry(&agent)
- .map_or(false, |entry| entry.read(cx).history().is_some())
- }
+ match &self.selected_agent {
+ Agent::NativeAgent => true,
+ Agent::Custom { .. } => self
+ .connection_store
+ .read(cx)
+ .entry(&self.selected_agent)
+ .map_or(false, |entry| entry.read(cx).history().is_some()),
}
}
@@ -1434,36 +1202,16 @@ impl AgentPanel {
&self,
window: &mut Window,
cx: &mut Context<Self>,
- ) -> Option<History> {
- match &self.selected_agent_type {
- AgentType::TextThread => Some(History::TextThreads),
- AgentType::NativeAgent => {
- let history = self
- .connection_store
- .read(cx)
- .entry(&Agent::NativeAgent)?
- .read(cx)
- .history()?
- .clone();
-
- Some(History::AgentThreads {
- view: self.create_thread_history_view(Agent::NativeAgent, history, window, cx),
- })
- }
- AgentType::Custom { id, .. } => {
- let agent = Agent::Custom { id: id.clone() };
- let history = self
- .connection_store
- .read(cx)
- .entry(&agent)?
- .read(cx)
- .history()?
- .clone();
- Some(History::AgentThreads {
- view: self.create_thread_history_view(agent, history, window, cx),
- })
- }
- }
+ ) -> Option<Entity<ThreadHistoryView>> {
+ let agent = self.selected_agent.clone();
+ let history = self
+ .connection_store
+ .read(cx)
+ .entry(&agent)?
+ .read(cx)
+ .history()?
+ .clone();
+ Some(self.create_thread_history_view(agent, history, window, cx))
}
fn create_thread_history_view(
@@ -1496,15 +1244,12 @@ impl AgentPanel {
}
fn open_history(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- let Some(history) = self.history_for_selected_agent(window, cx) else {
+ let Some(view) = self.history_for_selected_agent(window, cx) else {
return;
};
- if let ActiveView::History {
- history: active_history,
- } = &self.active_view
- {
- if active_history == &history {
+ if let ActiveView::History { view: active_view } = &self.active_view {
+ if active_view == &view {
if let Some(previous_view) = self.previous_view.take() {
self.set_active_view(previous_view, true, window, cx);
}
@@ -1512,61 +1257,10 @@ impl AgentPanel {
}
}
- self.set_active_view(ActiveView::History { history }, true, window, cx);
+ self.set_active_view(ActiveView::History { view }, true, window, cx);
cx.notify();
}
- pub(crate) fn open_saved_text_thread(
- &mut self,
- path: Arc<Path>,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) -> Task<Result<()>> {
- let text_thread_task = self
- .text_thread_store
- .update(cx, |store, cx| store.open_local(path, cx));
- cx.spawn_in(window, async move |this, cx| {
- let text_thread = text_thread_task.await?;
- this.update_in(cx, |this, window, cx| {
- this.open_text_thread(text_thread, window, cx);
- })
- })
- }
-
- pub(crate) fn open_text_thread(
- &mut self,
- text_thread: Entity<TextThread>,
- 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_text_thread(
- text_thread,
- self.fs.clone(),
- self.workspace.clone(),
- self.project.clone(),
- lsp_adapter_delegate,
- window,
- cx,
- )
- });
-
- if self.selected_agent_type != AgentType::TextThread {
- self.selected_agent_type = AgentType::TextThread;
- self.serialize(cx);
- }
-
- self.set_active_view(
- ActiveView::text_thread(editor, self.language_registry.clone(), window, cx),
- true,
- 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 { .. } => {
@@ -1650,11 +1344,6 @@ impl AgentPanel {
theme_settings::adjust_agent_buffer_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 => {}
}
}
@@ -2065,15 +1754,6 @@ impl AgentPanel {
}
}
- pub(crate) fn active_text_thread_editor(&self) -> Option<Entity<TextThreadEditor>> {
- match &self.active_view {
- ActiveView::TextThread {
- text_thread_editor, ..
- } => Some(text_thread_editor.clone()),
- _ => None,
- }
- }
-
fn set_active_view(
&mut self,
new_view: ActiveView,
@@ -2081,12 +1761,7 @@ impl AgentPanel {
window: &mut Window,
cx: &mut Context<Self>,
) {
- let was_in_agent_history = matches!(
- self.active_view,
- ActiveView::History {
- history: History::AgentThreads { .. }
- }
- );
+ let was_in_agent_history = matches!(self.active_view, ActiveView::History { .. });
let current_is_uninitialized = matches!(self.active_view, ActiveView::Uninitialized);
let current_is_history = matches!(self.active_view, ActiveView::History { .. });
let new_is_history = matches!(new_view, ActiveView::History { .. });
@@ -2144,8 +1819,8 @@ impl AgentPanel {
}
};
- if let ActiveView::History { history } = &self.active_view {
- if !was_in_agent_history && let History::AgentThreads { view } = history {
+ if let ActiveView::History { view } = &self.active_view {
+ if !was_in_agent_history {
view.update(cx, |view, cx| {
view.history()
.update(cx, |history, cx| history.refresh_full_history(cx))
@@ -2162,97 +1837,55 @@ impl AgentPanel {
fn populate_recently_updated_menu_section(
mut menu: ContextMenu,
panel: Entity<Self>,
- history: History,
+ view: Entity<ThreadHistoryView>,
cx: &mut Context<ContextMenu>,
) -> ContextMenu {
- match history {
- History::AgentThreads { view } => {
- let entries = view
- .read(cx)
- .history()
- .read(cx)
- .sessions()
- .iter()
- .take(RECENTLY_UPDATED_MENU_LIMIT)
- .cloned()
- .collect::<Vec<_>>();
-
- if entries.is_empty() {
- return menu;
- }
-
- menu = menu.header("Recently Updated");
-
- for entry in entries {
- let title = entry
- .title
- .as_ref()
- .filter(|title| !title.is_empty())
- .cloned()
- .unwrap_or_else(|| SharedString::new_static(DEFAULT_THREAD_TITLE));
-
- menu = menu.entry(title, None, {
- let panel = panel.downgrade();
- let entry = entry.clone();
- move |window, cx| {
- let entry = entry.clone();
- panel
- .update(cx, move |this, cx| {
- if let Some(agent) = this.selected_agent() {
- this.load_agent_thread(
- agent,
- entry.session_id.clone(),
- entry.work_dirs.clone(),
- entry.title.clone(),
- true,
- window,
- cx,
- );
- }
- })
- .ok();
- }
- });
- }
- }
- History::TextThreads => {
- let entries = panel
- .read(cx)
- .text_thread_store
- .read(cx)
- .ordered_text_threads()
- .take(RECENTLY_UPDATED_MENU_LIMIT)
- .cloned()
- .collect::<Vec<_>>();
+ let entries = view
+ .read(cx)
+ .history()
+ .read(cx)
+ .sessions()
+ .iter()
+ .take(RECENTLY_UPDATED_MENU_LIMIT)
+ .cloned()
+ .collect::<Vec<_>>();
- if entries.is_empty() {
- return menu;
- }
+ if entries.is_empty() {
+ return menu;
+ }
- menu = menu.header("Recent Text Threads");
+ menu = menu.header("Recently Updated");
- for entry in entries {
- let title = if entry.title.is_empty() {
- SharedString::new_static(DEFAULT_THREAD_TITLE)
- } else {
- entry.title.clone()
- };
+ for entry in entries {
+ let title = entry
+ .title
+ .as_ref()
+ .filter(|title| !title.is_empty())
+ .cloned()
+ .unwrap_or_else(|| SharedString::new_static(DEFAULT_THREAD_TITLE));
- menu = menu.entry(title, None, {
- let panel = panel.downgrade();
- let entry = entry.clone();
- move |window, cx| {
- let path = entry.path.clone();
- panel
- .update(cx, move |this, cx| {
- this.open_saved_text_thread(path.clone(), window, cx)
- .detach_and_log_err(cx);
- })
- .ok();
- }
- });
+ menu = menu.entry(title, None, {
+ let panel = panel.downgrade();
+ let entry = entry.clone();
+ move |window, cx| {
+ let entry = entry.clone();
+ panel
+ .update(cx, move |this, cx| {
+ if let Some(agent) = this.selected_agent() {
+ this.load_agent_thread(
+ agent,
+ entry.session_id.clone(),
+ entry.work_dirs.clone(),
+ entry.title.clone(),
+ true,
+ window,
+ cx,
+ );
+ }
+ })
+ .ok();
}
- }
+ });
}
menu.separator()
@@ -2347,11 +1980,7 @@ impl AgentPanel {
}
pub(crate) fn selected_agent(&self) -> Option<Agent> {
- match &self.selected_agent_type {
- AgentType::NativeAgent => Some(Agent::NativeAgent),
- AgentType::Custom { id } => Some(Agent::Custom { id: id.clone() }),
- AgentType::TextThread => None,
- }
+ Some(self.selected_agent.clone())
}
fn sync_agent_servers_from_extensions(&mut self, cx: &mut Context<Self>) {
@@ -2397,48 +2026,19 @@ impl AgentPanel {
);
}
- pub fn new_agent_thread(
- &mut self,
- agent: AgentType,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
+ pub fn new_agent_thread(&mut self, agent: Agent, window: &mut Window, cx: &mut Context<Self>) {
self.reset_start_thread_in_to_default(cx);
self.new_agent_thread_inner(agent, true, window, cx);
}
fn new_agent_thread_inner(
&mut self,
- agent: AgentType,
+ agent: Agent,
focus: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
- match agent {
- AgentType::TextThread => {
- window.dispatch_action(NewTextThread.boxed_clone(), cx);
- }
- AgentType::NativeAgent => self.external_thread(
- Some(crate::Agent::NativeAgent),
- None,
- None,
- None,
- None,
- focus,
- window,
- cx,
- ),
- AgentType::Custom { id } => self.external_thread(
- Some(crate::Agent::Custom { id }),
- None,
- None,
- None,
- None,
- focus,
- window,
- cx,
- ),
- }
+ self.external_thread(Some(agent), None, None, None, None, focus, window, cx);
}
pub fn load_agent_thread(
@@ -2512,9 +2112,8 @@ impl AgentPanel {
window: &mut Window,
cx: &mut Context<Self>,
) {
- let selected_agent = AgentType::from(ext_agent.clone());
- if self.selected_agent_type != selected_agent {
- self.selected_agent_type = selected_agent;
+ if self.selected_agent != ext_agent {
+ self.selected_agent = ext_agent.clone();
self.serialize(cx);
}
let thread_store = server
@@ -11,6 +11,7 @@ mod config_options;
mod context;
mod context_server_configuration;
pub(crate) mod conversation_view;
+mod diagnostics;
mod entry_view_state;
mod external_source_prompt;
mod favorite_models;
@@ -23,14 +24,10 @@ mod mode_selector;
mod model_selector;
mod model_selector_popover;
mod profile_selector;
-mod slash_command;
-mod slash_command_picker;
mod terminal_codegen;
mod terminal_inline_assistant;
#[cfg(any(test, feature = "test-support"))]
pub mod test_support;
-mod text_thread_editor;
-mod text_thread_history;
mod thread_history;
mod thread_history_view;
mod thread_import;
@@ -41,10 +38,9 @@ mod ui;
use std::rc::Rc;
use std::sync::Arc;
+use ::ui::IconName;
use agent_client_protocol as acp;
use agent_settings::{AgentProfileId, AgentSettings};
-use assistant_slash_command::SlashCommandRegistry;
-use client::Client;
use command_palette_hooks::CommandPaletteFilter;
use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt as _};
use fs::Fs;
@@ -65,9 +61,7 @@ use std::any::TypeId;
use workspace::Workspace;
use crate::agent_configuration::{ConfigureContextServerModal, ManageProfilesModal};
-pub use crate::agent_panel::{
- AgentPanel, AgentPanelEvent, ConcreteAssistantPanelDelegate, WorktreeCreationStatus,
-};
+pub use crate::agent_panel::{AgentPanel, AgentPanelEvent, WorktreeCreationStatus};
use crate::agent_registry_ui::AgentRegistryPage;
pub use crate::inline_assistant::InlineAssistant;
pub use agent_diff::{AgentDiffPane, AgentDiffToolbar};
@@ -76,7 +70,6 @@ pub use external_source_prompt::ExternalSourcePrompt;
pub(crate) use mode_selector::ModeSelector;
pub(crate) use model_selector::ModelSelector;
pub(crate) use model_selector_popover::ModelSelectorPopover;
-pub use text_thread_editor::{AgentPanelDelegate, TextThreadEditor};
pub(crate) use thread_history::ThreadHistory;
pub(crate) use thread_history_view::*;
pub use thread_import::{AcpThreadImportOnboarding, ThreadImportModal};
@@ -88,8 +81,6 @@ const PARALLEL_AGENT_LAYOUT_BACKFILL_KEY: &str = "parallel_agent_layout_backfill
actions!(
agent,
[
- /// Creates a new text-based conversation thread.
- NewTextThread,
/// Toggles the menu to create new agent threads.
ToggleNewThreadMenu,
/// Cycles through the options for where new threads start (current project or new worktree).
@@ -244,11 +235,13 @@ pub struct NewNativeAgentThreadFromSummary {
from_session_id: agent_client_protocol::SessionId,
}
-// TODO unify this with AgentType
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
+#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum Agent {
+ #[default]
+ #[serde(alias = "NativeAgent", alias = "TextThread")]
NativeAgent,
+ #[serde(alias = "Custom")]
Custom {
#[serde(rename = "name")]
id: AgentId,
@@ -273,6 +266,24 @@ impl Agent {
}
}
+ pub fn is_native(&self) -> bool {
+ matches!(self, Self::NativeAgent)
+ }
+
+ pub fn label(&self) -> SharedString {
+ match self {
+ Self::NativeAgent => "Zed Agent".into(),
+ Self::Custom { id, .. } => id.0.clone(),
+ }
+ }
+
+ pub fn icon(&self) -> Option<IconName> {
+ match self {
+ Self::NativeAgent => None,
+ Self::Custom { .. } => Some(IconName::Sparkle),
+ }
+ }
+
pub fn server(
&self,
fs: Arc<dyn fs::Fs>,
@@ -350,10 +361,39 @@ impl ModelUsageContext {
}
}
+pub(crate) fn humanize_token_count(count: u64) -> String {
+ match count {
+ 0..=999 => count.to_string(),
+ 1000..=9999 => {
+ let thousands = count / 1000;
+ let hundreds = (count % 1000 + 50) / 100;
+ if hundreds == 0 {
+ format!("{}k", thousands)
+ } else if hundreds == 10 {
+ format!("{}k", thousands + 1)
+ } else {
+ format!("{}.{}k", thousands, hundreds)
+ }
+ }
+ 10_000..=999_999 => format!("{}k", (count + 500) / 1000),
+ 1_000_000..=9_999_999 => {
+ let millions = count / 1_000_000;
+ let hundred_thousands = (count % 1_000_000 + 50_000) / 100_000;
+ if hundred_thousands == 0 {
+ format!("{}M", millions)
+ } else if hundred_thousands == 10 {
+ format!("{}M", millions + 1)
+ } else {
+ format!("{}.{}M", millions, hundred_thousands)
+ }
+ }
+ 10_000_000.. => format!("{}M", (count + 500_000) / 1_000_000),
+ }
+}
+
/// Initializes the `agent` crate.
pub fn init(
fs: Arc<dyn Fs>,
- client: Arc<Client>,
prompt_builder: Arc<PromptBuilder>,
language_registry: Arc<LanguageRegistry>,
is_new_install: bool,
@@ -361,20 +401,16 @@ pub fn init(
cx: &mut App,
) {
agent::ThreadStore::init_global(cx);
- assistant_text_thread::init(client, cx);
rules_library::init(cx);
if !is_eval {
// Initializing the language model from the user settings messes with the eval, so we only initialize them when
// we're not running inside of the eval.
init_language_model_settings(cx);
}
- assistant_slash_command::init(cx);
agent_panel::init(cx);
context_server_configuration::init(language_registry.clone(), fs.clone(), cx);
- TextThreadEditor::init(cx);
thread_metadata_store::init(cx);
- register_slash_commands(cx);
inline_assistant::init(fs.clone(), prompt_builder.clone(), cx);
terminal_inline_assistant::init(fs.clone(), prompt_builder, cx);
cx.observe_new(move |workspace, window, cx| {
@@ -628,34 +664,6 @@ fn update_active_language_model_from_settings(cx: &mut App) {
});
}
-fn register_slash_commands(cx: &mut App) {
- let slash_command_registry = SlashCommandRegistry::global(cx);
-
- slash_command_registry.register_command(assistant_slash_commands::FileSlashCommand, true);
- slash_command_registry.register_command(assistant_slash_commands::DeltaSlashCommand, true);
- slash_command_registry.register_command(assistant_slash_commands::OutlineSlashCommand, true);
- slash_command_registry.register_command(assistant_slash_commands::TabSlashCommand, true);
- slash_command_registry.register_command(assistant_slash_commands::PromptSlashCommand, true);
- slash_command_registry.register_command(assistant_slash_commands::SelectionCommand, true);
- slash_command_registry.register_command(assistant_slash_commands::DefaultSlashCommand, false);
- slash_command_registry.register_command(assistant_slash_commands::NowSlashCommand, false);
- slash_command_registry
- .register_command(assistant_slash_commands::DiagnosticsSlashCommand, true);
- slash_command_registry.register_command(assistant_slash_commands::FetchSlashCommand, true);
-
- cx.observe_flag::<assistant_slash_commands::StreamingExampleSlashCommandFeatureFlag, _>({
- move |is_enabled, _cx| {
- if is_enabled {
- slash_command_registry.register_command(
- assistant_slash_commands::StreamingExampleSlashCommand,
- false,
- );
- }
- }
- })
- .detach();
-}
-
#[cfg(test)]
mod tests {
use super::*;
@@ -666,9 +674,7 @@ mod tests {
use feature_flags::FeatureFlagAppExt;
use gpui::{BorrowAppContext, TestAppContext, px};
use project::DisableAiSettings;
- use settings::{
- DefaultAgentView, DockPosition, NotifyWhenAgentWaiting, Settings, SettingsStore,
- };
+ use settings::{DockPosition, NotifyWhenAgentWaiting, Settings, SettingsStore};
#[gpui::test]
fn test_agent_command_palette_visibility(cx: &mut TestAppContext) {
@@ -697,7 +703,6 @@ mod tests {
inline_alternatives: vec![],
favorite_models: vec![],
default_profile: AgentProfileId::default(),
- default_view: DefaultAgentView::Thread,
profiles: Default::default(),
notify_when_agent_waiting: NotifyWhenAgentWaiting::default(),
play_sound_when_agent_done: false,
@@ -731,10 +736,6 @@ mod tests {
!filter.is_hidden(&NewThread),
"NewThread should be visible by default"
);
- assert!(
- !filter.is_hidden(&text_thread_editor::CopyCode),
- "CopyCode should be visible when agent is enabled"
- );
});
// Disable agent
@@ -754,10 +755,6 @@ mod tests {
filter.is_hidden(&NewThread),
"NewThread should be hidden when agent is disabled"
);
- assert!(
- filter.is_hidden(&text_thread_editor::CopyCode),
- "CopyCode should be hidden when agent is disabled"
- );
});
// Test EditPredictionProvider
@@ -903,11 +900,11 @@ mod tests {
#[test]
fn test_deserialize_external_agent_variants() {
assert_eq!(
- serde_json::from_str::<Agent>(r#""native_agent""#).unwrap(),
+ serde_json::from_str::<Agent>(r#""NativeAgent""#).unwrap(),
Agent::NativeAgent,
);
assert_eq!(
- serde_json::from_str::<Agent>(r#"{"custom":{"name":"my-agent"}}"#).unwrap(),
+ serde_json::from_str::<Agent>(r#"{"Custom":{"name":"my-agent"}}"#).unwrap(),
Agent::Custom {
id: "my-agent".into(),
},
@@ -2524,22 +2524,6 @@ impl ConversationView {
}
}
- /// Inserts terminal text as a crease into the message editor.
- pub(crate) fn insert_terminal_text(
- &self,
- text: String,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- if let Some(active_thread) = self.active_thread() {
- active_thread.update(cx, |thread, cx| {
- thread.message_editor.update(cx, |editor, cx| {
- editor.insert_terminal_crease(text, window, cx);
- })
- });
- }
- }
-
fn current_model_name(&self, cx: &App) -> SharedString {
// For native agent (Zed Agent), use the specific model name (e.g., "Claude 3.5 Sonnet")
// For ACP agents, use the agent name (e.g., "Claude Agent", "Gemini CLI")
@@ -2751,7 +2735,6 @@ pub(crate) mod tests {
use action_log::ActionLog;
use agent::{AgentTool, EditFileTool, FetchTool, TerminalTool, ToolPermissionContext};
use agent_client_protocol::SessionId;
- use assistant_text_thread::TextThreadStore;
use editor::MultiBufferOffset;
use fs::FakeFs;
use gpui::{EventEmitter, TestAppContext, VisualTestContext};
@@ -3309,10 +3292,7 @@ pub(crate) mod tests {
let cx = &mut VisualTestContext::from_window(multi_workspace_handle.into(), cx);
workspace1.update_in(cx, |workspace, window, cx| {
- let text_thread_store =
- cx.new(|cx| TextThreadStore::fake(workspace.project().clone(), cx));
- let panel =
- cx.new(|cx| crate::AgentPanel::new(workspace, text_thread_store, None, window, cx));
+ let panel = cx.new(|cx| crate::AgentPanel::new(workspace, None, window, cx));
workspace.add_panel(panel, window, cx);
// Open the dock and activate the agent panel so it's visible
@@ -3419,12 +3419,10 @@ impl ThreadView {
}
};
- let used = crate::text_thread_editor::humanize_token_count(usage.used_tokens);
- let max = crate::text_thread_editor::humanize_token_count(usage.max_tokens);
- let input_tokens_label =
- crate::text_thread_editor::humanize_token_count(usage.input_tokens);
- let output_tokens_label =
- crate::text_thread_editor::humanize_token_count(usage.output_tokens);
+ let used = crate::humanize_token_count(usage.used_tokens);
+ let max = crate::humanize_token_count(usage.max_tokens);
+ let input_tokens_label = crate::humanize_token_count(usage.input_tokens);
+ let output_tokens_label = crate::humanize_token_count(usage.output_tokens);
let progress_ratio = if usage.max_tokens > 0 {
usage.used_tokens as f32 / usage.max_tokens as f32
@@ -3468,10 +3466,9 @@ impl ThreadView {
.and_then(|thread| thread.read(cx).model())
.and_then(|model| model.max_output_tokens())
.unwrap_or(0);
- let input_max_label = crate::text_thread_editor::humanize_token_count(
- usage.max_tokens.saturating_sub(max_output_tokens),
- );
- let output_max_label = crate::text_thread_editor::humanize_token_count(max_output_tokens);
+ let input_max_label =
+ crate::humanize_token_count(usage.max_tokens.saturating_sub(max_output_tokens));
+ let output_max_label = crate::humanize_token_count(max_output_tokens);
let build_tooltip = {
move |_window: &mut Window, cx: &mut App| {
@@ -4808,12 +4805,9 @@ impl ThreadView {
.last_turn_tokens
.filter(|&tokens| tokens > TOKEN_THRESHOLD)
.map(|tokens| {
- Label::new(format!(
- "{} tokens",
- crate::text_thread_editor::humanize_token_count(tokens)
- ))
- .size(LabelSize::Small)
- .color(Color::Muted)
+ Label::new(format!("{} tokens", crate::humanize_token_count(tokens)))
+ .size(LabelSize::Small)
+ .color(Color::Muted)
})
})
.flatten();
@@ -5096,7 +5090,7 @@ impl ThreadView {
self.turn_fields
.turn_tokens
.filter(|&tokens| tokens > TOKEN_THRESHOLD)
- .map(|tokens| crate::text_thread_editor::humanize_token_count(tokens))
+ .map(|tokens| crate::humanize_token_count(tokens))
})
.flatten();
@@ -8775,15 +8769,6 @@ pub(crate) fn open_link(
});
}
}
- MentionUri::TextThread { path, .. } => {
- if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
- panel.update(cx, |panel, cx| {
- panel
- .open_saved_text_thread(path.as_path().into(), window, cx)
- .detach_and_log_err(cx);
- });
- }
- }
MentionUri::Rule { id, .. } => {
let PromptId::User { uuid } = id else {
return;
@@ -0,0 +1,252 @@
+use anyhow::Result;
+use gpui::{App, AppContext as _, Entity, Task};
+use language::{Anchor, BufferSnapshot, DiagnosticEntryRef, DiagnosticSeverity, ToOffset};
+use project::{DiagnosticSummary, Project};
+use rope::Point;
+use std::{fmt::Write, ops::RangeInclusive, path::Path};
+use text::OffsetRangeExt;
+use util::ResultExt;
+use util::paths::PathMatcher;
+
+pub fn codeblock_fence_for_path(
+ path: Option<&str>,
+ row_range: Option<RangeInclusive<u32>>,
+) -> String {
+ let mut text = String::new();
+ write!(text, "```").unwrap();
+
+ if let Some(path) = path {
+ if let Some(extension) = Path::new(path).extension().and_then(|ext| ext.to_str()) {
+ write!(text, "{} ", extension).unwrap();
+ }
+
+ write!(text, "{path}").unwrap();
+ } else {
+ write!(text, "untitled").unwrap();
+ }
+
+ if let Some(row_range) = row_range {
+ write!(text, ":{}-{}", row_range.start() + 1, row_range.end() + 1).unwrap();
+ }
+
+ text.push('\n');
+ text
+}
+
+pub struct DiagnosticsOptions {
+ pub include_errors: bool,
+ pub include_warnings: bool,
+ pub path_matcher: Option<PathMatcher>,
+}
+
+/// Collects project diagnostics into a formatted string.
+///
+/// Returns `None` if no matching diagnostics were found.
+pub fn collect_diagnostics(
+ project: Entity<Project>,
+ options: DiagnosticsOptions,
+ cx: &mut App,
+) -> Task<Result<Option<String>>> {
+ let path_style = project.read(cx).path_style(cx);
+ let glob_is_exact_file_match = if let Some(path) = options
+ .path_matcher
+ .as_ref()
+ .and_then(|pm| pm.sources().next())
+ {
+ project
+ .read(cx)
+ .find_project_path(Path::new(path), cx)
+ .is_some()
+ } else {
+ false
+ };
+
+ let project_handle = project.downgrade();
+ let diagnostic_summaries: Vec<_> = project
+ .read(cx)
+ .diagnostic_summaries(false, cx)
+ .flat_map(|(path, _, summary)| {
+ let worktree = project.read(cx).worktree_for_id(path.worktree_id, cx)?;
+ let full_path = worktree.read(cx).root_name().join(&path.path);
+ Some((path, full_path, summary))
+ })
+ .collect();
+
+ cx.spawn(async move |cx| {
+ let error_source = if let Some(path_matcher) = &options.path_matcher {
+ debug_assert_eq!(path_matcher.sources().count(), 1);
+ Some(path_matcher.sources().next().unwrap_or_default())
+ } else {
+ None
+ };
+
+ let mut text = String::new();
+ if let Some(error_source) = error_source.as_ref() {
+ writeln!(text, "diagnostics: {}", error_source).unwrap();
+ } else {
+ writeln!(text, "diagnostics").unwrap();
+ }
+
+ let mut found_any_diagnostics = false;
+ let mut project_summary = DiagnosticSummary::default();
+ for (project_path, path, summary) in diagnostic_summaries {
+ if let Some(path_matcher) = &options.path_matcher
+ && !path_matcher.is_match(&path)
+ {
+ continue;
+ }
+
+ let has_errors = options.include_errors && summary.error_count > 0;
+ let has_warnings = options.include_warnings && summary.warning_count > 0;
+ if !has_errors && !has_warnings {
+ continue;
+ }
+
+ if options.include_errors {
+ project_summary.error_count += summary.error_count;
+ }
+ if options.include_warnings {
+ project_summary.warning_count += summary.warning_count;
+ }
+
+ let file_path = path.display(path_style).to_string();
+ if !glob_is_exact_file_match {
+ writeln!(&mut text, "{file_path}").unwrap();
+ }
+
+ if let Some(buffer) = project_handle
+ .update(cx, |project, cx| project.open_buffer(project_path, cx))?
+ .await
+ .log_err()
+ {
+ let snapshot = cx.read_entity(&buffer, |buffer, _| buffer.snapshot());
+ if collect_buffer_diagnostics(
+ &mut text,
+ &snapshot,
+ options.include_warnings,
+ options.include_errors,
+ ) {
+ found_any_diagnostics = true;
+ }
+ }
+ }
+
+ if !found_any_diagnostics {
+ return Ok(None);
+ }
+
+ let mut label = String::new();
+ label.push_str("Diagnostics");
+ if let Some(source) = error_source {
+ write!(label, " ({})", source).unwrap();
+ }
+
+ if project_summary.error_count > 0 || project_summary.warning_count > 0 {
+ label.push(':');
+
+ if project_summary.error_count > 0 {
+ write!(label, " {} errors", project_summary.error_count).unwrap();
+ if project_summary.warning_count > 0 {
+ label.push(',');
+ }
+ }
+
+ if project_summary.warning_count > 0 {
+ write!(label, " {} warnings", project_summary.warning_count).unwrap();
+ }
+ }
+
+ // Prepend the summary label to the output.
+ text.insert_str(0, &format!("{label}\n"));
+
+ Ok(Some(text))
+ })
+}
+
+/// Collects diagnostics from a buffer snapshot into the text output.
+///
+/// Returns `true` if any diagnostics were written.
+fn collect_buffer_diagnostics(
+ text: &mut String,
+ snapshot: &BufferSnapshot,
+ include_warnings: bool,
+ include_errors: bool,
+) -> bool {
+ let mut found_any = false;
+ for (_, group) in snapshot.diagnostic_groups(None) {
+ let entry = &group.entries[group.primary_ix];
+ if collect_diagnostic(text, entry, snapshot, include_warnings, include_errors) {
+ found_any = true;
+ }
+ }
+ found_any
+}
+
+/// Formats a single diagnostic entry as a code excerpt with the diagnostic message.
+///
+/// Returns `true` if the diagnostic was written (i.e. it matched severity filters).
+fn collect_diagnostic(
+ text: &mut String,
+ entry: &DiagnosticEntryRef<'_, Anchor>,
+ snapshot: &BufferSnapshot,
+ include_warnings: bool,
+ include_errors: bool,
+) -> bool {
+ const EXCERPT_EXPANSION_SIZE: u32 = 2;
+ const MAX_MESSAGE_LENGTH: usize = 2000;
+
+ let ty = match entry.diagnostic.severity {
+ DiagnosticSeverity::WARNING => {
+ if !include_warnings {
+ return false;
+ }
+ "warning"
+ }
+ DiagnosticSeverity::ERROR => {
+ if !include_errors {
+ return false;
+ }
+ "error"
+ }
+ _ => return false,
+ };
+
+ let range = entry.range.to_point(snapshot);
+ let diagnostic_row_number = range.start.row + 1;
+
+ let start_row = range.start.row.saturating_sub(EXCERPT_EXPANSION_SIZE);
+ let end_row = (range.end.row + EXCERPT_EXPANSION_SIZE).min(snapshot.max_point().row) + 1;
+ let excerpt_range =
+ Point::new(start_row, 0).to_offset(snapshot)..Point::new(end_row, 0).to_offset(snapshot);
+
+ text.push_str("```");
+ if let Some(language_name) = snapshot.language().map(|l| l.code_fence_block_name()) {
+ text.push_str(&language_name);
+ }
+ text.push('\n');
+
+ let mut buffer_text = String::new();
+ for chunk in snapshot.text_for_range(excerpt_range) {
+ buffer_text.push_str(chunk);
+ }
+
+ for (i, line) in buffer_text.lines().enumerate() {
+ let line_number = start_row + i as u32 + 1;
+ writeln!(text, "{}", line).unwrap();
+
+ if line_number == diagnostic_row_number {
+ text.push_str("//");
+ let marker_start = text.len();
+ write!(text, " {}: ", ty).unwrap();
+ let padding = text.len() - marker_start;
+
+ let message = util::truncate(&entry.diagnostic.message, MAX_MESSAGE_LENGTH)
+ .replace('\n', format!("\n//{:padding$}", "").as_str());
+
+ writeln!(text, "{message}").unwrap();
+ }
+ }
+
+ writeln!(text, "```").unwrap();
+ true
+}
@@ -257,12 +257,8 @@ impl InlineAssistant {
return;
}
- let Some(inline_assist_target) = Self::resolve_inline_assist_target(
- workspace,
- workspace.panel::<AgentPanel>(cx),
- window,
- cx,
- ) else {
+ let Some(inline_assist_target) = Self::resolve_inline_assist_target(workspace, window, cx)
+ else {
return;
};
@@ -1570,7 +1566,6 @@ impl InlineAssistant {
fn resolve_inline_assist_target(
workspace: &mut Workspace,
- agent_panel: Option<Entity<AgentPanel>>,
window: &mut Window,
cx: &mut App,
) -> Option<InlineAssistTarget> {
@@ -1588,20 +1583,7 @@ impl InlineAssistant {
return Some(InlineAssistTarget::Terminal(terminal_view));
}
- let text_thread_editor = agent_panel
- .and_then(|panel| panel.read(cx).active_text_thread_editor())
- .and_then(|editor| {
- let editor = &editor.read(cx).editor().clone();
- if editor.read(cx).is_focused(window) {
- Some(editor.clone())
- } else {
- None
- }
- });
-
- if let Some(text_thread_editor) = text_thread_editor {
- Some(InlineAssistTarget::Editor(text_thread_editor))
- } else if let Some(workspace_editor) = workspace
+ if let Some(workspace_editor) = workspace
.active_item(cx)
.and_then(|item| item.act_as::<Editor>(cx))
{
@@ -1,9 +1,9 @@
+use crate::diagnostics::{DiagnosticsOptions, codeblock_fence_for_path, collect_diagnostics};
use acp_thread::{MentionUri, selection_name};
use agent::{ThreadStore, outline};
use agent_client_protocol as acp;
use agent_servers::{AgentServer, AgentServerDelegate};
use anyhow::{Context as _, Result, anyhow};
-use assistant_slash_commands::{codeblock_fence_for_path, collect_diagnostics_output};
use collections::{HashMap, HashSet};
use editor::{
Anchor, Editor, EditorSnapshot, ExcerptId, FoldPlaceholder, ToOffset,
@@ -131,9 +131,6 @@ impl MentionSet {
MentionUri::Fetch { url } => self.confirm_mention_for_fetch(url, http_client, cx),
MentionUri::Directory { .. } => Task::ready(Ok(Mention::Link)),
MentionUri::Thread { id, .. } => self.confirm_mention_for_thread(id, cx),
- MentionUri::TextThread { .. } => {
- Task::ready(Err(anyhow!("Text thread mentions are no longer supported")))
- }
MentionUri::File { abs_path } => {
self.confirm_mention_for_file(abs_path, supports_images, cx)
}
@@ -276,9 +273,6 @@ impl MentionSet {
}
MentionUri::Directory { .. } => Task::ready(Ok(Mention::Link)),
MentionUri::Thread { id, .. } => self.confirm_mention_for_thread(id, cx),
- MentionUri::TextThread { .. } => {
- Task::ready(Err(anyhow!("Text thread mentions are no longer supported")))
- }
MentionUri::File { abs_path } => {
self.confirm_mention_for_file(abs_path, supports_images, cx)
}
@@ -589,9 +583,9 @@ impl MentionSet {
return Task::ready(Err(anyhow!("project not found")));
};
- let diagnostics_task = collect_diagnostics_output(
+ let diagnostics_task = collect_diagnostics(
project,
- assistant_slash_commands::Options {
+ DiagnosticsOptions {
include_errors,
include_warnings,
path_matcher: None,
@@ -599,9 +593,8 @@ impl MentionSet {
cx,
);
cx.spawn(async move |_, _| {
- let output = diagnostics_task.await?;
- let content = output
- .map(|output| output.text)
+ let content = diagnostics_task
+ .await?
.unwrap_or_else(|| "No diagnostics found.".into());
Ok(Mention::Text {
content,
@@ -1308,62 +1308,6 @@ impl MessageEditor {
}
}
- pub fn insert_terminal_crease(
- &mut self,
- text: String,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- let line_count = text.lines().count() as u32;
- let mention_uri = MentionUri::TerminalSelection { line_count };
- let mention_text = mention_uri.as_link().to_string();
-
- let (excerpt_id, text_anchor, content_len) = self.editor.update(cx, |editor, cx| {
- let buffer = editor.buffer().read(cx);
- let snapshot = buffer.snapshot(cx);
- let (excerpt_id, _, buffer_snapshot) = snapshot.as_singleton().unwrap();
- let text_anchor = editor
- .selections
- .newest_anchor()
- .start
- .text_anchor
- .bias_left(&buffer_snapshot);
-
- editor.insert(&mention_text, window, cx);
- editor.insert(" ", window, cx);
-
- (excerpt_id, text_anchor, mention_text.len())
- });
-
- let Some((crease_id, tx)) = insert_crease_for_mention(
- excerpt_id,
- text_anchor,
- content_len,
- mention_uri.name().into(),
- mention_uri.icon_path(cx),
- mention_uri.tooltip_text(),
- Some(mention_uri.clone()),
- Some(self.workspace.clone()),
- None,
- self.editor.clone(),
- window,
- cx,
- ) else {
- return;
- };
- drop(tx);
-
- let mention_task = Task::ready(Ok(Mention::Text {
- content: text,
- tracked_buffers: vec![],
- }))
- .shared();
-
- self.mention_set.update(cx, |mention_set, _| {
- mention_set.insert_mention(crease_id, mention_uri, mention_task);
- });
- }
-
pub fn insert_branch_diff_crease(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(workspace) = self.workspace.upgrade() else {
return;
@@ -1,360 +0,0 @@
-use crate::text_thread_editor::TextThreadEditor;
-use anyhow::Result;
-pub use assistant_slash_command::SlashCommand;
-use assistant_slash_command::{AfterCompletion, SlashCommandLine, SlashCommandWorkingSet};
-use editor::{CompletionProvider, Editor, ExcerptId};
-use fuzzy::{StringMatchCandidate, match_strings};
-use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity, Window};
-use language::{Anchor, Buffer, ToPoint};
-use parking_lot::Mutex;
-use project::{
- CompletionDisplayOptions, CompletionIntent, CompletionSource,
- lsp_store::CompletionDocumentation,
-};
-use rope::Point;
-use std::{
- ops::Range,
- sync::{
- Arc,
- atomic::{AtomicBool, Ordering::SeqCst},
- },
-};
-use workspace::Workspace;
-
-pub struct SlashCommandCompletionProvider {
- cancel_flag: Mutex<Arc<AtomicBool>>,
- slash_commands: Arc<SlashCommandWorkingSet>,
- editor: Option<WeakEntity<TextThreadEditor>>,
- workspace: Option<WeakEntity<Workspace>>,
-}
-
-impl SlashCommandCompletionProvider {
- pub fn new(
- slash_commands: Arc<SlashCommandWorkingSet>,
- editor: Option<WeakEntity<TextThreadEditor>>,
- workspace: Option<WeakEntity<Workspace>>,
- ) -> Self {
- Self {
- cancel_flag: Mutex::new(Arc::new(AtomicBool::new(false))),
- slash_commands,
- editor,
- workspace,
- }
- }
-
- fn complete_command_name(
- &self,
- command_name: &str,
- command_range: Range<Anchor>,
- name_range: Range<Anchor>,
- window: &mut Window,
- cx: &mut App,
- ) -> Task<Result<Vec<project::CompletionResponse>>> {
- let slash_commands = self.slash_commands.clone();
- let candidates = slash_commands
- .command_names(cx)
- .into_iter()
- .enumerate()
- .map(|(ix, def)| StringMatchCandidate::new(ix, &def))
- .collect::<Vec<_>>();
- let command_name = command_name.to_string();
- let editor = self.editor.clone();
- let workspace = self.workspace.clone();
- window.spawn(cx, async move |cx| {
- let matches = match_strings(
- &candidates,
- &command_name,
- true,
- true,
- usize::MAX,
- &Default::default(),
- cx.background_executor().clone(),
- )
- .await;
-
- cx.update(|_, cx| {
- let completions = matches
- .into_iter()
- .filter_map(|mat| {
- let command = slash_commands.command(&mat.string, cx)?;
- let mut new_text = mat.string.clone();
- let requires_argument = command.requires_argument();
- let accepts_arguments = command.accepts_arguments();
- if requires_argument || accepts_arguments {
- new_text.push(' ');
- }
-
- let confirm =
- editor
- .clone()
- .zip(workspace.clone())
- .map(|(editor, workspace)| {
- let command_name = mat.string.clone();
- let command_range = command_range.clone();
- Arc::new(
- move |intent: CompletionIntent,
- window: &mut Window,
- cx: &mut App| {
- if !requires_argument
- && (!accepts_arguments || intent.is_complete())
- {
- editor
- .update(cx, |editor, cx| {
- editor.run_command(
- command_range.clone(),
- &command_name,
- &[],
- true,
- workspace.clone(),
- window,
- cx,
- );
- })
- .ok();
- false
- } else {
- requires_argument || accepts_arguments
- }
- },
- ) as Arc<_>
- });
-
- Some(project::Completion {
- replace_range: name_range.clone(),
- documentation: Some(CompletionDocumentation::SingleLine(
- command.description().into(),
- )),
- new_text,
- label: command.label(cx),
- icon_path: None,
- match_start: None,
- snippet_deduplication_key: None,
- insert_text_mode: None,
- confirm,
- source: CompletionSource::Custom,
- })
- })
- .collect();
-
- vec![project::CompletionResponse {
- completions,
- display_options: CompletionDisplayOptions::default(),
- is_incomplete: false,
- }]
- })
- })
- }
-
- fn complete_command_argument(
- &self,
- command_name: &str,
- arguments: &[String],
- command_range: Range<Anchor>,
- argument_range: Range<Anchor>,
- last_argument_range: Range<Anchor>,
- window: &mut Window,
- cx: &mut App,
- ) -> Task<Result<Vec<project::CompletionResponse>>> {
- let new_cancel_flag = Arc::new(AtomicBool::new(false));
- let mut flag = self.cancel_flag.lock();
- flag.store(true, SeqCst);
- *flag = new_cancel_flag.clone();
- if let Some(command) = self.slash_commands.command(command_name, cx) {
- let completions = command.complete_argument(
- arguments,
- new_cancel_flag,
- self.workspace.clone(),
- window,
- cx,
- );
- let command_name: Arc<str> = command_name.into();
- let editor = self.editor.clone();
- let workspace = self.workspace.clone();
- let arguments = arguments.to_vec();
- cx.background_spawn(async move {
- let completions = completions
- .await?
- .into_iter()
- .map(|new_argument| {
- let confirm =
- editor
- .clone()
- .zip(workspace.clone())
- .map(|(editor, workspace)| {
- Arc::new({
- let mut completed_arguments = arguments.clone();
- if new_argument.replace_previous_arguments {
- completed_arguments.clear();
- } else {
- completed_arguments.pop();
- }
- completed_arguments.push(new_argument.new_text.clone());
-
- let command_range = command_range.clone();
- let command_name = command_name.clone();
- move |intent: CompletionIntent,
- window: &mut Window,
- cx: &mut App| {
- if new_argument.after_completion.run()
- || intent.is_complete()
- {
- editor
- .update(cx, |editor, cx| {
- editor.run_command(
- command_range.clone(),
- &command_name,
- &completed_arguments,
- true,
- workspace.clone(),
- window,
- cx,
- );
- })
- .ok();
- false
- } else {
- !new_argument.after_completion.run()
- }
- }
- }) as Arc<_>
- });
-
- let mut new_text = new_argument.new_text.clone();
- if new_argument.after_completion == AfterCompletion::Continue {
- new_text.push(' ');
- }
-
- project::Completion {
- replace_range: if new_argument.replace_previous_arguments {
- argument_range.clone()
- } else {
- last_argument_range.clone()
- },
- label: new_argument.label,
- icon_path: None,
- new_text,
- documentation: None,
- match_start: None,
- snippet_deduplication_key: None,
- confirm,
- insert_text_mode: None,
- source: CompletionSource::Custom,
- }
- })
- .collect();
-
- Ok(vec![project::CompletionResponse {
- completions,
- display_options: CompletionDisplayOptions::default(),
- // TODO: Could have slash commands indicate whether their completions are incomplete.
- is_incomplete: true,
- }])
- })
- } else {
- Task::ready(Ok(vec![project::CompletionResponse {
- completions: Vec::new(),
- display_options: CompletionDisplayOptions::default(),
- is_incomplete: true,
- }]))
- }
- }
-}
-
-impl CompletionProvider for SlashCommandCompletionProvider {
- fn completions(
- &self,
- _excerpt_id: ExcerptId,
- buffer: &Entity<Buffer>,
- buffer_position: Anchor,
- _: editor::CompletionContext,
- window: &mut Window,
- cx: &mut Context<Editor>,
- ) -> Task<Result<Vec<project::CompletionResponse>>> {
- let Some((name, arguments, command_range, last_argument_range)) =
- buffer.update(cx, |buffer, _cx| {
- let position = buffer_position.to_point(buffer);
- let line_start = Point::new(position.row, 0);
- let mut lines = buffer.text_for_range(line_start..position).lines();
- let line = lines.next()?;
- let call = SlashCommandLine::parse(line)?;
-
- let command_range_start = Point::new(position.row, call.name.start as u32 - 1);
- let command_range_end = Point::new(
- position.row,
- call.arguments.last().map_or(call.name.end, |arg| arg.end) as u32,
- );
- let command_range = buffer.anchor_before(command_range_start)
- ..buffer.anchor_after(command_range_end);
-
- let name = line[call.name.clone()].to_string();
- let (arguments, last_argument_range) = if let Some(argument) = call.arguments.last()
- {
- let last_arg_start =
- buffer.anchor_before(Point::new(position.row, argument.start as u32));
- let first_arg_start = call.arguments.first().expect("we have the last element");
- let first_arg_start = buffer
- .anchor_before(Point::new(position.row, first_arg_start.start as u32));
- let arguments = call
- .arguments
- .into_iter()
- .filter_map(|argument| Some(line.get(argument)?.to_string()))
- .collect::<Vec<_>>();
- let argument_range = first_arg_start..buffer_position;
- (
- Some((arguments, argument_range)),
- last_arg_start..buffer_position,
- )
- } else {
- let start =
- buffer.anchor_before(Point::new(position.row, call.name.start as u32));
- (None, start..buffer_position)
- };
-
- Some((name, arguments, command_range, last_argument_range))
- })
- else {
- return Task::ready(Ok(vec![project::CompletionResponse {
- completions: Vec::new(),
- display_options: CompletionDisplayOptions::default(),
- is_incomplete: false,
- }]));
- };
-
- if let Some((arguments, argument_range)) = arguments {
- self.complete_command_argument(
- &name,
- &arguments,
- command_range,
- argument_range,
- last_argument_range,
- window,
- cx,
- )
- } else {
- self.complete_command_name(&name, command_range, last_argument_range, window, cx)
- }
- }
-
- fn is_completion_trigger(
- &self,
- buffer: &Entity<Buffer>,
- position: language::Anchor,
- _text: &str,
- _trigger_in_words: bool,
- cx: &mut Context<Editor>,
- ) -> bool {
- let buffer = buffer.read(cx);
- let position = position.to_point(buffer);
- let line_start = Point::new(position.row, 0);
- let mut lines = buffer.text_for_range(line_start..position).lines();
- if let Some(line) = lines.next() {
- SlashCommandLine::parse(line).is_some()
- } else {
- false
- }
- }
-
- fn sort_completions(&self) -> bool {
- false
- }
-}
@@ -1,348 +0,0 @@
-use crate::text_thread_editor::TextThreadEditor;
-use assistant_slash_command::SlashCommandWorkingSet;
-use gpui::{AnyElement, AnyView, DismissEvent, SharedString, Task, WeakEntity};
-use picker::{Picker, PickerDelegate, PickerEditorPosition};
-use std::sync::Arc;
-use ui::{ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger, Tooltip, prelude::*};
-
-#[derive(IntoElement)]
-pub(super) struct SlashCommandSelector<T, TT>
-where
- T: PopoverTrigger + ButtonCommon,
- TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
-{
- working_set: Arc<SlashCommandWorkingSet>,
- active_context_editor: WeakEntity<TextThreadEditor>,
- trigger: T,
- tooltip: TT,
-}
-
-#[derive(Clone)]
-struct SlashCommandInfo {
- name: SharedString,
- description: SharedString,
- args: Option<SharedString>,
- icon: IconName,
-}
-
-#[derive(Clone)]
-enum SlashCommandEntry {
- Info(SlashCommandInfo),
- Advert {
- name: SharedString,
- renderer: fn(&mut Window, &mut App) -> AnyElement,
- on_confirm: fn(&mut Window, &mut App),
- },
-}
-
-impl AsRef<str> for SlashCommandEntry {
- fn as_ref(&self) -> &str {
- match self {
- SlashCommandEntry::Info(SlashCommandInfo { name, .. })
- | SlashCommandEntry::Advert { name, .. } => name,
- }
- }
-}
-
-pub(crate) struct SlashCommandDelegate {
- all_commands: Vec<SlashCommandEntry>,
- filtered_commands: Vec<SlashCommandEntry>,
- active_context_editor: WeakEntity<TextThreadEditor>,
- selected_index: usize,
-}
-
-impl<T, TT> SlashCommandSelector<T, TT>
-where
- T: PopoverTrigger + ButtonCommon,
- TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
-{
- pub(crate) fn new(
- working_set: Arc<SlashCommandWorkingSet>,
- active_context_editor: WeakEntity<TextThreadEditor>,
- trigger: T,
- tooltip: TT,
- ) -> Self {
- SlashCommandSelector {
- working_set,
- active_context_editor,
- trigger,
- tooltip,
- }
- }
-}
-
-impl PickerDelegate for SlashCommandDelegate {
- type ListItem = ListItem;
-
- fn match_count(&self) -> usize {
- self.filtered_commands.len()
- }
-
- fn selected_index(&self) -> usize {
- self.selected_index
- }
-
- fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) {
- self.selected_index = ix.min(self.filtered_commands.len().saturating_sub(1));
- cx.notify();
- }
-
- fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
- "Select a command...".into()
- }
-
- fn update_matches(
- &mut self,
- query: String,
- window: &mut Window,
- cx: &mut Context<Picker<Self>>,
- ) -> Task<()> {
- let all_commands = self.all_commands.clone();
- cx.spawn_in(window, async move |this, cx| {
- let filtered_commands = cx
- .background_spawn(async move {
- if query.is_empty() {
- all_commands
- } else {
- all_commands
- .into_iter()
- .filter(|model_info| {
- model_info
- .as_ref()
- .to_lowercase()
- .contains(&query.to_lowercase())
- })
- .collect()
- }
- })
- .await;
-
- this.update_in(cx, |this, window, cx| {
- this.delegate.filtered_commands = filtered_commands;
- this.delegate.set_selected_index(0, window, cx);
- cx.notify();
- })
- .ok();
- })
- }
-
- fn separators_after_indices(&self) -> Vec<usize> {
- let mut ret = vec![];
- let mut previous_is_advert = false;
-
- for (index, command) in self.filtered_commands.iter().enumerate() {
- if previous_is_advert {
- if let SlashCommandEntry::Info(_) = command {
- previous_is_advert = false;
- debug_assert_ne!(
- index, 0,
- "index cannot be zero, as we can never have a separator at 0th position"
- );
- ret.push(index - 1);
- }
- } else if let SlashCommandEntry::Advert { .. } = command {
- previous_is_advert = true;
- if index != 0 {
- ret.push(index - 1);
- }
- }
- }
- ret
- }
-
- fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
- if let Some(command) = self.filtered_commands.get(self.selected_index) {
- match command {
- SlashCommandEntry::Info(info) => {
- self.active_context_editor
- .update(cx, |text_thread_editor, cx| {
- text_thread_editor.insert_command(&info.name, window, cx)
- })
- .ok();
- }
- SlashCommandEntry::Advert { on_confirm, .. } => {
- on_confirm(window, cx);
- }
- }
- cx.emit(DismissEvent);
- }
- }
-
- fn dismissed(&mut self, _window: &mut Window, _cx: &mut Context<Picker<Self>>) {}
-
- fn editor_position(&self) -> PickerEditorPosition {
- PickerEditorPosition::End
- }
-
- fn render_match(
- &self,
- ix: usize,
- selected: bool,
- window: &mut Window,
- cx: &mut Context<Picker<Self>>,
- ) -> Option<Self::ListItem> {
- let command_info = self.filtered_commands.get(ix)?;
-
- match command_info {
- SlashCommandEntry::Info(info) => Some(
- ListItem::new(ix)
- .inset(true)
- .spacing(ListItemSpacing::Dense)
- .toggle_state(selected)
- .tooltip({
- let description = info.description.clone();
- move |_, cx| cx.new(|_| Tooltip::new(description.clone())).into()
- })
- .child(
- v_flex()
- .group(format!("command-entry-label-{ix}"))
- .w_full()
- .py_0p5()
- .min_w(px(250.))
- .max_w(px(400.))
- .child(
- h_flex()
- .gap_1p5()
- .child(
- Icon::new(info.icon)
- .size(IconSize::XSmall)
- .color(Color::Muted),
- )
- .child({
- let mut label = format!("{}", info.name);
- if let Some(args) = info.args.as_ref().filter(|_| selected)
- {
- label.push_str(args);
- }
- Label::new(label)
- .single_line()
- .size(LabelSize::Small)
- .buffer_font(cx)
- })
- .children(info.args.clone().filter(|_| !selected).map(
- |args| {
- div()
- .child(
- Label::new(args)
- .single_line()
- .size(LabelSize::Small)
- .color(Color::Muted)
- .buffer_font(cx),
- )
- .visible_on_hover(format!(
- "command-entry-label-{ix}"
- ))
- },
- )),
- )
- .child(
- Label::new(info.description.clone())
- .size(LabelSize::Small)
- .color(Color::Muted)
- .truncate(),
- ),
- ),
- ),
- SlashCommandEntry::Advert { renderer, .. } => Some(
- ListItem::new(ix)
- .inset(true)
- .spacing(ListItemSpacing::Dense)
- .toggle_state(selected)
- .child(renderer(window, cx)),
- ),
- }
- }
-}
-
-impl<T, TT> RenderOnce for SlashCommandSelector<T, TT>
-where
- T: PopoverTrigger + ButtonCommon,
- TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
-{
- fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
- let all_models = self
- .working_set
- .featured_command_names(cx)
- .into_iter()
- .filter_map(|command_name| {
- let command = self.working_set.command(&command_name, cx)?;
- let menu_text = SharedString::from(Arc::from(command.menu_text()));
- let label = command.label(cx);
- let args = label.filter_range.end.ne(&label.text.len()).then(|| {
- SharedString::from(
- label.text[label.filter_range.end..label.text.len()].to_owned(),
- )
- });
- Some(SlashCommandEntry::Info(SlashCommandInfo {
- name: command_name.into(),
- description: menu_text,
- args,
- icon: command.icon(),
- }))
- })
- .chain([SlashCommandEntry::Advert {
- name: "create-your-command".into(),
- renderer: |_, cx| {
- v_flex()
- .w_full()
- .child(
- h_flex()
- .w_full()
- .font_buffer(cx)
- .items_center()
- .justify_between()
- .child(
- h_flex()
- .items_center()
- .gap_1p5()
- .child(Icon::new(IconName::Plus).size(IconSize::XSmall))
- .child(
- Label::new("create-your-command")
- .size(LabelSize::Small)
- .buffer_font(cx),
- ),
- )
- .child(
- Icon::new(IconName::ArrowUpRight)
- .size(IconSize::Small)
- .color(Color::Muted),
- ),
- )
- .child(
- Label::new("Create your custom command")
- .size(LabelSize::Small)
- .color(Color::Muted),
- )
- .into_any_element()
- },
- on_confirm: |_, cx| cx.open_url("https://zed.dev/docs/extensions/slash-commands"),
- }])
- .collect::<Vec<_>>();
-
- let delegate = SlashCommandDelegate {
- all_commands: all_models.clone(),
- active_context_editor: self.active_context_editor.clone(),
- filtered_commands: all_models,
- selected_index: 0,
- };
-
- let picker_view = cx.new(|cx| {
- Picker::uniform_list(delegate, window, cx).max_height(Some(rems(20.).into()))
- });
-
- let handle = self
- .active_context_editor
- .read_with(cx, |this, _| this.slash_menu_handle.clone())
- .ok();
- PopoverMenu::new("model-switcher")
- .menu(move |_window, _cx| Some(picker_view.clone()))
- .trigger_with_tooltip(self.trigger, self.tooltip)
- .attach(gpui::Corner::TopLeft)
- .anchor(gpui::Corner::BottomLeft)
- .offset(gpui::Point {
- x: px(0.0),
- y: px(-2.0),
- })
- .when_some(handle, |this, handle| this.with_handle(handle))
- }
-}
@@ -1,3471 +0,0 @@
-use crate::{
- language_model_selector::{LanguageModelSelector, language_model_selector},
- mention_set::load_external_image_from_path,
- ui::ModelSelectorTooltip,
-};
-use anyhow::Result;
-use assistant_slash_command::{SlashCommand, SlashCommandOutputSection, SlashCommandWorkingSet};
-use assistant_slash_commands::{DefaultSlashCommand, FileSlashCommand, selections_creases};
-use client::{proto, zed_urls};
-use collections::{BTreeSet, HashMap, HashSet, hash_map};
-use editor::{
- Anchor, Editor, EditorEvent, MenuEditPredictionsPolicy, MultiBuffer, MultiBufferOffset,
- MultiBufferSnapshot, RowExt, ToOffset as _, ToPoint as _,
- actions::{MoveToEndOfLine, Newline, ShowCompletions},
- display_map::{
- BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata, CustomBlockId, FoldId,
- RenderBlock, ToDisplayPoint,
- },
- scroll::ScrollOffset,
-};
-use editor::{FoldPlaceholder, display_map::CreaseId};
-use fs::Fs;
-use futures::FutureExt;
-use gpui::{
- Action, Animation, AnimationExt, AnyElement, App, ClipboardEntry, ClipboardItem, Empty, Entity,
- EventEmitter, FocusHandle, Focusable, FontWeight, Global, InteractiveElement, IntoElement,
- ParentElement, Pixels, Render, RenderImage, SharedString, Size, StatefulInteractiveElement,
- Styled, Subscription, Task, WeakEntity, actions, div, img, point, prelude::*,
- pulsating_between, size,
-};
-use language::{
- BufferSnapshot, LspAdapterDelegate, ToOffset,
- language_settings::{SoftWrap, all_language_settings},
-};
-use language_model::{
- ConfigurationError, IconOrSvg, LanguageModelImage, LanguageModelRegistry, Role,
-};
-use multi_buffer::MultiBufferRow;
-use picker::{Picker, popover_menu::PickerPopoverMenu};
-use project::{Project, Worktree};
-use project::{ProjectPath, lsp_store::LocalLspAdapterDelegate};
-use rope::Point;
-use serde::{Deserialize, Serialize};
-use settings::{
- LanguageModelProviderSetting, LanguageModelSelection, Settings, SettingsStore,
- update_settings_file,
-};
-use std::{
- any::{Any, TypeId},
- cmp,
- ops::Range,
- path::{Path, PathBuf},
- rc::Rc,
- sync::Arc,
- time::Duration,
-};
-use text::SelectionGoal;
-use ui::{
- ButtonLike, CommonAnimationExt, Disclosure, ElevationIndex, KeyBinding, PopoverMenuHandle,
- TintColor, Tooltip, prelude::*,
-};
-use util::{ResultExt, maybe};
-use workspace::{
- CollaboratorId,
- searchable::{Direction, SearchToken, SearchableItemHandle},
-};
-
-use workspace::{
- Save, Toast, Workspace,
- item::{self, FollowableItem, Item},
- notifications::NotificationId,
- pane,
- searchable::{SearchEvent, SearchableItem},
-};
-use zed_actions::agent::{AddSelectionToThread, PasteRaw, ToggleModelSelector};
-
-use crate::CycleFavoriteModels;
-
-use crate::{slash_command::SlashCommandCompletionProvider, slash_command_picker};
-use assistant_text_thread::{
- CacheStatus, Content, InvokedSlashCommandId, InvokedSlashCommandStatus, Message, MessageId,
- MessageMetadata, MessageStatus, PendingSlashCommandStatus, TextThread, TextThreadEvent,
- TextThreadId, ThoughtProcessOutputSection,
-};
-
-actions!(
- assistant,
- [
- /// Sends the current message to the assistant.
- Assist,
- /// Confirms and executes the entered slash command.
- ConfirmCommand,
- /// Copies code from the assistant's response to the clipboard.
- CopyCode,
- /// Cycles between user and assistant message roles.
- CycleMessageRole,
- /// Inserts the selected text into the active editor.
- InsertIntoEditor,
- /// Splits the conversation at the current cursor position.
- Split,
- ]
-);
-
-/// Inserts files that were dragged and dropped into the assistant conversation.
-#[derive(PartialEq, Clone, Action)]
-#[action(namespace = assistant, no_json, no_register)]
-pub enum InsertDraggedFiles {
- ProjectPaths(Vec<ProjectPath>),
- ExternalFiles(Vec<PathBuf>),
-}
-
-#[derive(Copy, Clone, Debug, PartialEq)]
-struct ScrollPosition {
- offset_before_cursor: gpui::Point<ScrollOffset>,
- cursor: Anchor,
-}
-
-type MessageHeader = MessageMetadata;
-
-#[derive(Clone)]
-enum AssistError {
- PaymentRequired,
- Message(SharedString),
-}
-
-pub enum ThoughtProcessStatus {
- Pending,
- Completed,
-}
-
-pub trait AgentPanelDelegate {
- fn active_text_thread_editor(
- &self,
- workspace: &mut Workspace,
- window: &mut Window,
- cx: &mut Context<Workspace>,
- ) -> Option<Entity<TextThreadEditor>>;
-
- fn open_local_text_thread(
- &self,
- workspace: &mut Workspace,
- path: Arc<Path>,
- window: &mut Window,
- cx: &mut Context<Workspace>,
- ) -> Task<Result<()>>;
-
- fn open_remote_text_thread(
- &self,
- workspace: &mut Workspace,
- text_thread_id: TextThreadId,
- window: &mut Window,
- cx: &mut Context<Workspace>,
- ) -> Task<Result<Entity<TextThreadEditor>>>;
-
- fn quote_selection(
- &self,
- workspace: &mut Workspace,
- selection_ranges: Vec<Range<Anchor>>,
- buffer: Entity<MultiBuffer>,
- window: &mut Window,
- cx: &mut Context<Workspace>,
- );
-
- fn quote_terminal_text(
- &self,
- workspace: &mut Workspace,
- text: String,
- window: &mut Window,
- cx: &mut Context<Workspace>,
- );
-}
-
-impl dyn AgentPanelDelegate {
- /// Returns the global [`AssistantPanelDelegate`], if it exists.
- pub fn try_global(cx: &App) -> Option<Arc<Self>> {
- cx.try_global::<GlobalAssistantPanelDelegate>()
- .map(|global| global.0.clone())
- }
-
- /// Sets the global [`AssistantPanelDelegate`].
- pub fn set_global(delegate: Arc<Self>, cx: &mut App) {
- cx.set_global(GlobalAssistantPanelDelegate(delegate));
- }
-}
-
-struct GlobalAssistantPanelDelegate(Arc<dyn AgentPanelDelegate>);
-
-impl Global for GlobalAssistantPanelDelegate {}
-
-pub struct TextThreadEditor {
- text_thread: Entity<TextThread>,
- fs: Arc<dyn Fs>,
- slash_commands: Arc<SlashCommandWorkingSet>,
- workspace: WeakEntity<Workspace>,
- project: Entity<Project>,
- lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
- editor: Entity<Editor>,
- pending_thought_process: Option<(CreaseId, language::Anchor)>,
- blocks: HashMap<MessageId, (MessageHeader, CustomBlockId)>,
- image_blocks: HashSet<CustomBlockId>,
- scroll_position: Option<ScrollPosition>,
- remote_id: Option<workspace::ViewId>,
- pending_slash_command_creases: HashMap<Range<language::Anchor>, CreaseId>,
- invoked_slash_command_creases: HashMap<InvokedSlashCommandId, CreaseId>,
- _subscriptions: Vec<Subscription>,
- last_error: Option<AssistError>,
- pub(crate) slash_menu_handle:
- PopoverMenuHandle<Picker<slash_command_picker::SlashCommandDelegate>>,
- // dragged_file_worktrees is used to keep references to worktrees that were added
- // when the user drag/dropped an external file onto the context editor. Since
- // the worktree is not part of the project panel, it would be dropped as soon as
- // the file is opened. In order to keep the worktree alive for the duration of the
- // context editor, we keep a reference here.
- dragged_file_worktrees: Vec<Entity<Worktree>>,
- language_model_selector: Entity<LanguageModelSelector>,
- language_model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
-}
-
-const MAX_TAB_TITLE_LEN: usize = 16;
-
-impl TextThreadEditor {
- pub fn init(cx: &mut App) {
- workspace::FollowableViewRegistry::register::<TextThreadEditor>(cx);
-
- cx.observe_new(
- |workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
- workspace
- .register_action(TextThreadEditor::quote_selection)
- .register_action(TextThreadEditor::insert_selection)
- .register_action(TextThreadEditor::copy_code)
- .register_action(TextThreadEditor::handle_insert_dragged_files);
- },
- )
- .detach();
- }
-
- pub fn for_text_thread(
- text_thread: Entity<TextThread>,
- fs: Arc<dyn Fs>,
- workspace: WeakEntity<Workspace>,
- project: Entity<Project>,
- lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) -> Self {
- let completion_provider = SlashCommandCompletionProvider::new(
- text_thread.read(cx).slash_commands().clone(),
- Some(cx.entity().downgrade()),
- Some(workspace.clone()),
- );
-
- let editor = cx.new(|cx| {
- let mut editor =
- Editor::for_buffer(text_thread.read(cx).buffer().clone(), None, window, cx);
- editor.disable_scrollbars_and_minimap(window, cx);
- editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
- editor.set_show_line_numbers(false, cx);
- editor.set_show_git_diff_gutter(false, cx);
- editor.set_show_code_actions(false, cx);
- editor.set_show_runnables(false, cx);
- editor.set_show_breakpoints(false, cx);
- editor.set_show_wrap_guides(false, cx);
- editor.set_show_indent_guides(false, cx);
- editor.set_completion_provider(Some(Rc::new(completion_provider)));
- editor.set_menu_edit_predictions_policy(MenuEditPredictionsPolicy::Never);
- editor.set_collaboration_hub(Box::new(project.clone()));
-
- let show_edit_predictions = all_language_settings(None, cx)
- .edit_predictions
- .enabled_in_text_threads;
-
- editor.set_show_edit_predictions(Some(show_edit_predictions), window, cx);
-
- editor
- });
-
- let _subscriptions = vec![
- cx.observe(&text_thread, |_, _, cx| cx.notify()),
- cx.subscribe_in(&text_thread, window, Self::handle_text_thread_event),
- cx.subscribe_in(&editor, window, Self::handle_editor_event),
- cx.subscribe_in(&editor, window, Self::handle_editor_search_event),
- cx.observe_global_in::<SettingsStore>(window, Self::settings_changed),
- ];
-
- let slash_command_sections = text_thread
- .read(cx)
- .slash_command_output_sections()
- .to_vec();
- let thought_process_sections = text_thread
- .read(cx)
- .thought_process_output_sections()
- .to_vec();
- let slash_commands = text_thread.read(cx).slash_commands().clone();
- let focus_handle = editor.read(cx).focus_handle(cx);
-
- let mut this = Self {
- text_thread,
- slash_commands,
- editor,
- lsp_adapter_delegate,
- blocks: Default::default(),
- image_blocks: Default::default(),
- scroll_position: None,
- remote_id: None,
- pending_thought_process: None,
- fs: fs.clone(),
- workspace,
- project,
- pending_slash_command_creases: HashMap::default(),
- invoked_slash_command_creases: HashMap::default(),
- _subscriptions,
- last_error: None,
- slash_menu_handle: Default::default(),
- dragged_file_worktrees: Vec::new(),
- language_model_selector: cx.new(|cx| {
- language_model_selector(
- |cx| LanguageModelRegistry::read_global(cx).default_model(),
- {
- let fs = fs.clone();
- move |model, cx| {
- update_settings_file(fs.clone(), cx, move |settings, _| {
- let provider = model.provider_id().0.to_string();
- let model_id = model.id().0.to_string();
- settings.agent.get_or_insert_default().set_model(
- LanguageModelSelection {
- provider: LanguageModelProviderSetting(provider),
- model: model_id,
- enable_thinking: model.supports_thinking(),
- effort: model
- .default_effort_level()
- .map(|effort| effort.value.to_string()),
- },
- )
- });
- }
- },
- {
- let fs = fs.clone();
- move |model, should_be_favorite, cx| {
- crate::favorite_models::toggle_in_settings(
- model,
- should_be_favorite,
- fs.clone(),
- cx,
- );
- }
- },
- true, // Use popover styles for picker
- focus_handle,
- window,
- cx,
- )
- }),
- language_model_selector_menu_handle: PopoverMenuHandle::default(),
- };
- this.update_message_headers(cx);
- this.update_image_blocks(cx);
- this.insert_slash_command_output_sections(slash_command_sections, false, window, cx);
- this.insert_thought_process_output_sections(
- thought_process_sections
- .into_iter()
- .map(|section| (section, ThoughtProcessStatus::Completed)),
- window,
- cx,
- );
- this
- }
-
- fn settings_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- self.editor.update(cx, |editor, cx| {
- let show_edit_predictions = all_language_settings(None, cx)
- .edit_predictions
- .enabled_in_text_threads;
-
- editor.set_show_edit_predictions(Some(show_edit_predictions), window, cx);
- });
- }
-
- pub fn text_thread(&self) -> &Entity<TextThread> {
- &self.text_thread
- }
-
- pub fn editor(&self) -> &Entity<Editor> {
- &self.editor
- }
-
- pub fn insert_default_prompt(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- let command_name = DefaultSlashCommand.name();
- self.editor.update(cx, |editor, cx| {
- editor.insert(&format!("/{command_name}\n\n"), window, cx)
- });
- let command = self.text_thread.update(cx, |text_thread, cx| {
- text_thread.reparse(cx);
- text_thread.parsed_slash_commands()[0].clone()
- });
- self.run_command(
- command.source_range,
- &command.name,
- &command.arguments,
- false,
- self.workspace.clone(),
- window,
- cx,
- );
- }
-
- fn assist(&mut self, _: &Assist, window: &mut Window, cx: &mut Context<Self>) {
- if self.sending_disabled(cx) {
- return;
- }
- telemetry::event!("Agent Message Sent", agent = "zed-text");
- self.send_to_model(window, cx);
- }
-
- fn send_to_model(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- self.last_error = None;
- if let Some(user_message) = self
- .text_thread
- .update(cx, |text_thread, cx| text_thread.assist(cx))
- {
- let new_selection = {
- let cursor = user_message
- .start
- .to_offset(self.text_thread.read(cx).buffer().read(cx));
- MultiBufferOffset(cursor)..MultiBufferOffset(cursor)
- };
- self.editor.update(cx, |editor, cx| {
- editor.change_selections(Default::default(), window, cx, |selections| {
- selections.select_ranges([new_selection])
- });
- });
- // Avoid scrolling to the new cursor position so the assistant's output is stable.
- cx.defer_in(window, |this, _, _| this.scroll_position = None);
- }
-
- cx.notify();
- }
-
- fn cancel(
- &mut self,
- _: &editor::actions::Cancel,
- _window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- self.last_error = None;
-
- if self
- .text_thread
- .update(cx, |text_thread, cx| text_thread.cancel_last_assist(cx))
- {
- return;
- }
-
- cx.propagate();
- }
-
- fn cycle_message_role(
- &mut self,
- _: &CycleMessageRole,
- _window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- let cursors = self.cursors(cx);
- self.text_thread.update(cx, |text_thread, cx| {
- let messages = text_thread
- .messages_for_offsets(cursors.into_iter().map(|cursor| cursor.0), cx)
- .into_iter()
- .map(|message| message.id)
- .collect();
- text_thread.cycle_message_roles(messages, cx)
- });
- }
-
- fn cursors(&self, cx: &mut App) -> Vec<MultiBufferOffset> {
- let selections = self.editor.update(cx, |editor, cx| {
- editor
- .selections
- .all::<MultiBufferOffset>(&editor.display_snapshot(cx))
- });
- selections
- .into_iter()
- .map(|selection| selection.head())
- .collect()
- }
-
- pub fn insert_command(&mut self, name: &str, window: &mut Window, cx: &mut Context<Self>) {
- if let Some(command) = self.slash_commands.command(name, cx) {
- self.editor.update(cx, |editor, cx| {
- editor.transact(window, cx, |editor, window, cx| {
- editor.change_selections(Default::default(), window, cx, |s| s.try_cancel());
- let snapshot = editor.buffer().read(cx).snapshot(cx);
- let newest_cursor = editor
- .selections
- .newest::<Point>(&editor.display_snapshot(cx))
- .head();
- if newest_cursor.column > 0
- || snapshot
- .chars_at(newest_cursor)
- .next()
- .is_some_and(|ch| ch != '\n')
- {
- editor.move_to_end_of_line(
- &MoveToEndOfLine {
- stop_at_soft_wraps: false,
- },
- window,
- cx,
- );
- editor.newline(&Newline, window, cx);
- }
-
- editor.insert(&format!("/{name}"), window, cx);
- if command.accepts_arguments() {
- editor.insert(" ", window, cx);
- editor.show_completions(&ShowCompletions, window, cx);
- }
- });
- });
- if !command.requires_argument() {
- self.confirm_command(&ConfirmCommand, window, cx);
- }
- }
- }
-
- pub fn confirm_command(
- &mut self,
- _: &ConfirmCommand,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- if self.editor.read(cx).has_visible_completions_menu() {
- return;
- }
-
- let selections = self.editor.read(cx).selections.disjoint_anchors_arc();
- let mut commands_by_range = HashMap::default();
- let workspace = self.workspace.clone();
- self.text_thread.update(cx, |text_thread, cx| {
- text_thread.reparse(cx);
- for selection in selections.iter() {
- if let Some(command) =
- text_thread.pending_command_for_position(selection.head().text_anchor, cx)
- {
- commands_by_range
- .entry(command.source_range.clone())
- .or_insert_with(|| command.clone());
- }
- }
- });
-
- if commands_by_range.is_empty() {
- cx.propagate();
- } else {
- for command in commands_by_range.into_values() {
- self.run_command(
- command.source_range,
- &command.name,
- &command.arguments,
- true,
- workspace.clone(),
- window,
- cx,
- );
- }
- cx.stop_propagation();
- }
- }
-
- pub fn run_command(
- &mut self,
- command_range: Range<language::Anchor>,
- name: &str,
- arguments: &[String],
- ensure_trailing_newline: bool,
- workspace: WeakEntity<Workspace>,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- if let Some(command) = self.slash_commands.command(name, cx) {
- let text_thread = self.text_thread.read(cx);
- let sections = text_thread
- .slash_command_output_sections()
- .iter()
- .filter(|section| section.is_valid(text_thread.buffer().read(cx)))
- .cloned()
- .collect::<Vec<_>>();
- let snapshot = text_thread.buffer().read(cx).snapshot();
- let output = command.run(
- arguments,
- §ions,
- snapshot,
- workspace,
- self.lsp_adapter_delegate.clone(),
- window,
- cx,
- );
- self.text_thread.update(cx, |text_thread, cx| {
- text_thread.insert_command_output(
- command_range,
- name,
- output,
- ensure_trailing_newline,
- cx,
- )
- });
- }
- }
-
- fn handle_text_thread_event(
- &mut self,
- _: &Entity<TextThread>,
- event: &TextThreadEvent,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- let text_thread_editor = cx.entity().downgrade();
-
- match event {
- TextThreadEvent::MessagesEdited => {
- self.update_message_headers(cx);
- self.update_image_blocks(cx);
- self.text_thread.update(cx, |text_thread, cx| {
- text_thread.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
- });
- }
- TextThreadEvent::SummaryChanged => {
- cx.emit(EditorEvent::TitleChanged);
- self.text_thread.update(cx, |text_thread, cx| {
- text_thread.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
- });
- }
- TextThreadEvent::SummaryGenerated => {}
- TextThreadEvent::PathChanged { .. } => {}
- TextThreadEvent::StartedThoughtProcess(range) => {
- let creases = self.insert_thought_process_output_sections(
- [(
- ThoughtProcessOutputSection {
- range: range.clone(),
- },
- ThoughtProcessStatus::Pending,
- )],
- window,
- cx,
- );
- self.pending_thought_process = Some((creases[0], range.start));
- }
- TextThreadEvent::EndedThoughtProcess(end) => {
- if let Some((crease_id, start)) = self.pending_thought_process.take() {
- self.editor.update(cx, |editor, cx| {
- let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
- let start_anchor =
- multi_buffer_snapshot.as_singleton_anchor(start).unwrap();
-
- editor.display_map.update(cx, |display_map, cx| {
- display_map.unfold_intersecting(
- vec![start_anchor..start_anchor],
- true,
- cx,
- );
- });
- editor.remove_creases(vec![crease_id], cx);
- });
- self.insert_thought_process_output_sections(
- [(
- ThoughtProcessOutputSection { range: start..*end },
- ThoughtProcessStatus::Completed,
- )],
- window,
- cx,
- );
- }
- }
- TextThreadEvent::StreamedCompletion => {
- self.editor.update(cx, |editor, cx| {
- if let Some(scroll_position) = self.scroll_position {
- let snapshot = editor.snapshot(window, cx);
- let cursor_point = scroll_position.cursor.to_display_point(&snapshot);
- let scroll_top =
- cursor_point.row().as_f64() - scroll_position.offset_before_cursor.y;
- editor.set_scroll_position(
- point(scroll_position.offset_before_cursor.x, scroll_top),
- window,
- cx,
- );
- }
- });
- }
- TextThreadEvent::ParsedSlashCommandsUpdated { removed, updated } => {
- self.editor.update(cx, |editor, cx| {
- let buffer = editor.buffer().read(cx).snapshot(cx);
- let (excerpt_id, _, _) = buffer.as_singleton().unwrap();
-
- editor.remove_creases(
- removed
- .iter()
- .filter_map(|range| self.pending_slash_command_creases.remove(range)),
- cx,
- );
-
- let crease_ids = editor.insert_creases(
- updated.iter().map(|command| {
- let workspace = self.workspace.clone();
- let confirm_command = Arc::new({
- let text_thread_editor = text_thread_editor.clone();
- let command = command.clone();
- move |window: &mut Window, cx: &mut App| {
- text_thread_editor
- .update(cx, |text_thread_editor, cx| {
- text_thread_editor.run_command(
- command.source_range.clone(),
- &command.name,
- &command.arguments,
- false,
- workspace.clone(),
- window,
- cx,
- );
- })
- .ok();
- }
- });
- let placeholder = FoldPlaceholder {
- render: Arc::new(move |_, _, _| Empty.into_any()),
- ..Default::default()
- };
- let render_toggle = {
- let confirm_command = confirm_command.clone();
- let command = command.clone();
- move |row, _, _, _window: &mut Window, _cx: &mut App| {
- render_pending_slash_command_gutter_decoration(
- row,
- &command.status,
- confirm_command.clone(),
- )
- }
- };
- let render_trailer = {
- move |_row, _unfold, _window: &mut Window, _cx: &mut App| {
- Empty.into_any()
- }
- };
-
- let range = buffer
- .anchor_range_in_excerpt(excerpt_id, command.source_range.clone())
- .unwrap();
- Crease::inline(range, placeholder, render_toggle, render_trailer)
- }),
- cx,
- );
-
- self.pending_slash_command_creases.extend(
- updated
- .iter()
- .map(|command| command.source_range.clone())
- .zip(crease_ids),
- );
- })
- }
- TextThreadEvent::InvokedSlashCommandChanged { command_id } => {
- self.update_invoked_slash_command(*command_id, window, cx);
- }
- TextThreadEvent::SlashCommandOutputSectionAdded { section } => {
- self.insert_slash_command_output_sections([section.clone()], false, window, cx);
- }
- TextThreadEvent::Operation(_) => {}
- TextThreadEvent::ShowAssistError(error_message) => {
- self.last_error = Some(AssistError::Message(error_message.clone()));
- }
- TextThreadEvent::ShowPaymentRequiredError => {
- self.last_error = Some(AssistError::PaymentRequired);
- }
- }
- }
-
- fn update_invoked_slash_command(
- &mut self,
- command_id: InvokedSlashCommandId,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- if let Some(invoked_slash_command) =
- self.text_thread.read(cx).invoked_slash_command(&command_id)
- && let InvokedSlashCommandStatus::Finished = invoked_slash_command.status
- {
- let run_commands_in_ranges = invoked_slash_command.run_commands_in_ranges.clone();
- for range in run_commands_in_ranges {
- let commands = self.text_thread.update(cx, |text_thread, cx| {
- text_thread.reparse(cx);
- text_thread
- .pending_commands_for_range(range.clone(), cx)
- .to_vec()
- });
-
- for command in commands {
- self.run_command(
- command.source_range,
- &command.name,
- &command.arguments,
- false,
- self.workspace.clone(),
- window,
- cx,
- );
- }
- }
- }
-
- self.editor.update(cx, |editor, cx| {
- if let Some(invoked_slash_command) =
- self.text_thread.read(cx).invoked_slash_command(&command_id)
- {
- if let InvokedSlashCommandStatus::Finished = invoked_slash_command.status {
- let buffer = editor.buffer().read(cx).snapshot(cx);
- let (excerpt_id, _buffer_id, _buffer_snapshot) = buffer.as_singleton().unwrap();
-
- let range = buffer
- .anchor_range_in_excerpt(excerpt_id, invoked_slash_command.range.clone())
- .unwrap();
- editor.remove_folds_with_type(
- &[range],
- TypeId::of::<PendingSlashCommand>(),
- false,
- cx,
- );
-
- editor.remove_creases(
- HashSet::from_iter(self.invoked_slash_command_creases.remove(&command_id)),
- cx,
- );
- } else if let hash_map::Entry::Vacant(entry) =
- self.invoked_slash_command_creases.entry(command_id)
- {
- let buffer = editor.buffer().read(cx).snapshot(cx);
- let (excerpt_id, _buffer_id, _buffer_snapshot) = buffer.as_singleton().unwrap();
- let context = self.text_thread.downgrade();
- let range = buffer
- .anchor_range_in_excerpt(excerpt_id, invoked_slash_command.range.clone())
- .unwrap();
- let crease = Crease::inline(
- range,
- invoked_slash_command_fold_placeholder(command_id, context),
- fold_toggle("invoked-slash-command"),
- |_row, _folded, _window, _cx| Empty.into_any(),
- );
- let crease_ids = editor.insert_creases([crease.clone()], cx);
- editor.fold_creases(vec![crease], false, window, cx);
- entry.insert(crease_ids[0]);
- } else {
- cx.notify()
- }
- } else {
- editor.remove_creases(
- HashSet::from_iter(self.invoked_slash_command_creases.remove(&command_id)),
- cx,
- );
- cx.notify();
- };
- });
- }
-
- fn insert_thought_process_output_sections(
- &mut self,
- sections: impl IntoIterator<
- Item = (
- ThoughtProcessOutputSection<language::Anchor>,
- ThoughtProcessStatus,
- ),
- >,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) -> Vec<CreaseId> {
- self.editor.update(cx, |editor, cx| {
- let buffer = editor.buffer().read(cx).snapshot(cx);
- let excerpt_id = buffer.as_singleton().unwrap().0;
- let mut buffer_rows_to_fold = BTreeSet::new();
- let mut creases = Vec::new();
- for (section, status) in sections {
- let range = buffer
- .anchor_range_in_excerpt(excerpt_id, section.range)
- .unwrap();
- let buffer_row = MultiBufferRow(range.start.to_point(&buffer).row);
- buffer_rows_to_fold.insert(buffer_row);
- creases.push(
- Crease::inline(
- range,
- FoldPlaceholder {
- render: render_thought_process_fold_icon_button(
- cx.entity().downgrade(),
- status,
- ),
- merge_adjacent: false,
- ..Default::default()
- },
- render_slash_command_output_toggle,
- |_, _, _, _| Empty.into_any_element(),
- )
- .with_metadata(CreaseMetadata {
- icon_path: SharedString::from(IconName::ZedAgent.path()),
- label: "Thinking Process".into(),
- }),
- );
- }
-
- let creases = editor.insert_creases(creases, cx);
-
- for buffer_row in buffer_rows_to_fold.into_iter().rev() {
- editor.fold_at(buffer_row, window, cx);
- }
-
- creases
- })
- }
-
- fn insert_slash_command_output_sections(
- &mut self,
- sections: impl IntoIterator<Item = SlashCommandOutputSection<language::Anchor>>,
- expand_result: bool,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- self.editor.update(cx, |editor, cx| {
- let buffer = editor.buffer().read(cx).snapshot(cx);
- let excerpt_id = buffer.as_singleton().unwrap().0;
- let mut buffer_rows_to_fold = BTreeSet::new();
- let mut creases = Vec::new();
- for section in sections {
- let range = buffer
- .anchor_range_in_excerpt(excerpt_id, section.range)
- .unwrap();
- let buffer_row = MultiBufferRow(range.start.to_point(&buffer).row);
- buffer_rows_to_fold.insert(buffer_row);
- creases.push(
- Crease::inline(
- range,
- FoldPlaceholder {
- render: render_fold_icon_button(
- cx.entity().downgrade(),
- section.icon.path().into(),
- section.label.clone(),
- ),
- merge_adjacent: false,
- ..Default::default()
- },
- render_slash_command_output_toggle,
- |_, _, _, _| Empty.into_any_element(),
- )
- .with_metadata(CreaseMetadata {
- icon_path: section.icon.path().into(),
- label: section.label,
- }),
- );
- }
-
- editor.insert_creases(creases, cx);
-
- if expand_result {
- buffer_rows_to_fold.clear();
- }
- for buffer_row in buffer_rows_to_fold.into_iter().rev() {
- editor.fold_at(buffer_row, window, cx);
- }
- });
- }
-
- fn handle_editor_event(
- &mut self,
- _: &Entity<Editor>,
- event: &EditorEvent,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- match event {
- EditorEvent::ScrollPositionChanged { autoscroll, .. } => {
- let cursor_scroll_position = self.cursor_scroll_position(window, cx);
- if *autoscroll {
- self.scroll_position = cursor_scroll_position;
- } else if self.scroll_position != cursor_scroll_position {
- self.scroll_position = None;
- }
- }
- EditorEvent::SelectionsChanged { .. } => {
- self.scroll_position = self.cursor_scroll_position(window, cx);
- }
- _ => {}
- }
- cx.emit(event.clone());
- }
-
- fn handle_editor_search_event(
- &mut self,
- _: &Entity<Editor>,
- event: &SearchEvent,
- _window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- cx.emit(event.clone());
- }
-
- fn cursor_scroll_position(
- &self,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) -> Option<ScrollPosition> {
- self.editor.update(cx, |editor, cx| {
- let snapshot = editor.snapshot(window, cx);
- let cursor = editor.selections.newest_anchor().head();
- let cursor_row = cursor
- .to_display_point(&snapshot.display_snapshot)
- .row()
- .as_f64();
- let scroll_position = editor
- .scroll_manager
- .scroll_position(&snapshot.display_snapshot, cx);
-
- let scroll_bottom = scroll_position.y + editor.visible_line_count().unwrap_or(0.);
- if (scroll_position.y..scroll_bottom).contains(&cursor_row) {
- Some(ScrollPosition {
- cursor,
- offset_before_cursor: point(scroll_position.x, cursor_row - scroll_position.y),
- })
- } else {
- None
- }
- })
- }
-
- fn esc_kbd(cx: &App) -> Div {
- let colors = cx.theme().colors().clone();
-
- h_flex()
- .items_center()
- .gap_1()
- .font(
- theme_settings::ThemeSettings::get_global(cx)
- .buffer_font
- .clone(),
- )
- .text_size(TextSize::XSmall.rems(cx))
- .text_color(colors.text_muted)
- .child("Press")
- .child(
- h_flex()
- .rounded_sm()
- .px_1()
- .mr_0p5()
- .border_1()
- .border_color(colors.border_variant.alpha(0.6))
- .bg(colors.element_background.alpha(0.6))
- .child("esc"),
- )
- .child("to cancel")
- }
-
- fn update_message_headers(&mut self, cx: &mut Context<Self>) {
- self.editor.update(cx, |editor, cx| {
- let buffer = editor.buffer().read(cx).snapshot(cx);
-
- let excerpt_id = buffer.as_singleton().unwrap().0;
- let mut old_blocks = std::mem::take(&mut self.blocks);
- let mut blocks_to_remove: HashMap<_, _> = old_blocks
- .iter()
- .map(|(message_id, (_, block_id))| (*message_id, *block_id))
- .collect();
- let mut blocks_to_replace: HashMap<_, RenderBlock> = Default::default();
-
- let render_block = |message: MessageMetadata| -> RenderBlock {
- Arc::new({
- let text_thread = self.text_thread.clone();
-
- move |cx| {
- let message_id = MessageId(message.timestamp);
- let llm_loading = message.role == Role::Assistant
- && message.status == MessageStatus::Pending;
-
- let (label, spinner, note) = match message.role {
- Role::User => (
- Label::new("You").color(Color::Default).into_any_element(),
- None,
- None,
- ),
- Role::Assistant => {
- let base_label = Label::new("Agent").color(Color::Info);
- let mut spinner = None;
- let mut note = None;
- let animated_label = if llm_loading {
- base_label
- .with_animation(
- "pulsating-label",
- Animation::new(Duration::from_secs(2))
- .repeat()
- .with_easing(pulsating_between(0.4, 0.8)),
- |label, delta| label.alpha(delta),
- )
- .into_any_element()
- } else {
- base_label.into_any_element()
- };
- if llm_loading {
- spinner = Some(
- Icon::new(IconName::ArrowCircle)
- .size(IconSize::XSmall)
- .color(Color::Info)
- .with_rotate_animation(2)
- .into_any_element(),
- );
- note = Some(Self::esc_kbd(cx).into_any_element());
- }
- (animated_label, spinner, note)
- }
- Role::System => (
- Label::new("System")
- .color(Color::Warning)
- .into_any_element(),
- None,
- None,
- ),
- };
-
- let sender = h_flex()
- .items_center()
- .gap_2p5()
- .child(
- ButtonLike::new("role")
- .style(ButtonStyle::Filled)
- .child(
- h_flex()
- .items_center()
- .gap_1p5()
- .child(label)
- .children(spinner),
- )
- .tooltip(|_window, cx| {
- Tooltip::with_meta(
- "Toggle message role",
- None,
- "Available roles: You (User), Agent, System",
- cx,
- )
- })
- .on_click({
- let text_thread = text_thread.clone();
- move |_, _window, cx| {
- text_thread.update(cx, |text_thread, cx| {
- text_thread.cycle_message_roles(
- HashSet::from_iter(Some(message_id)),
- cx,
- )
- })
- }
- }),
- )
- .children(note);
-
- h_flex()
- .id(("message_header", message_id.as_u64()))
- .pl(cx.margins.gutter.full_width())
- .h_11()
- .w_full()
- .relative()
- .gap_1p5()
- .child(sender)
- .children(match &message.cache {
- Some(cache) if cache.is_final_anchor => match cache.status {
- CacheStatus::Cached => Some(
- div()
- .id("cached")
- .child(
- Icon::new(IconName::DatabaseZap)
- .size(IconSize::XSmall)
- .color(Color::Hint),
- )
- .tooltip(|_window, cx| {
- Tooltip::with_meta(
- "Context Cached",
- None,
- "Large messages cached to optimize performance",
- cx,
- )
- })
- .into_any_element(),
- ),
- CacheStatus::Pending => Some(
- div()
- .child(
- Icon::new(IconName::Ellipsis)
- .size(IconSize::XSmall)
- .color(Color::Hint),
- )
- .into_any_element(),
- ),
- },
- _ => None,
- })
- .children(match &message.status {
- MessageStatus::Error(error) => Some(
- Button::new("show-error", "Error")
- .color(Color::Error)
- .selected_label_color(Color::Error)
- .start_icon(
- Icon::new(IconName::XCircle)
- .size(IconSize::XSmall)
- .color(Color::Error),
- )
- .tooltip(Tooltip::text("View Details"))
- .on_click({
- let text_thread = text_thread.clone();
- let error = error.clone();
- move |_, _window, cx| {
- text_thread.update(cx, |_, cx| {
- cx.emit(TextThreadEvent::ShowAssistError(
- error.clone(),
- ));
- });
- }
- })
- .into_any_element(),
- ),
- MessageStatus::Canceled => Some(
- h_flex()
- .gap_1()
- .items_center()
- .child(
- Icon::new(IconName::XCircle)
- .color(Color::Disabled)
- .size(IconSize::XSmall),
- )
- .child(
- Label::new("Canceled")
- .size(LabelSize::Small)
- .color(Color::Disabled),
- )
- .into_any_element(),
- ),
- _ => None,
- })
- .into_any_element()
- }
- })
- };
- let create_block_properties = |message: &Message| BlockProperties {
- height: Some(2),
- style: BlockStyle::Sticky,
- placement: BlockPlacement::Above(
- buffer
- .anchor_in_excerpt(excerpt_id, message.anchor_range.start)
- .unwrap(),
- ),
- priority: usize::MAX,
- render: render_block(MessageMetadata::from(message)),
- };
- let mut new_blocks = vec![];
- let mut block_index_to_message = vec![];
- for message in self.text_thread.read(cx).messages(cx) {
- if blocks_to_remove.remove(&message.id).is_some() {
- // This is an old message that we might modify.
- let Some((meta, block_id)) = old_blocks.get_mut(&message.id) else {
- debug_assert!(
- false,
- "old_blocks should contain a message_id we've just removed."
- );
- continue;
- };
- // Should we modify it?
- let message_meta = MessageMetadata::from(&message);
- if meta != &message_meta {
- blocks_to_replace.insert(*block_id, render_block(message_meta.clone()));
- *meta = message_meta;
- }
- } else {
- // This is a new message.
- new_blocks.push(create_block_properties(&message));
- block_index_to_message.push((message.id, MessageMetadata::from(&message)));
- }
- }
- editor.replace_blocks(blocks_to_replace, None, cx);
- editor.remove_blocks(blocks_to_remove.into_values().collect(), None, cx);
-
- let ids = editor.insert_blocks(new_blocks, None, cx);
- old_blocks.extend(ids.into_iter().zip(block_index_to_message).map(
- |(block_id, (message_id, message_meta))| (message_id, (message_meta, block_id)),
- ));
- self.blocks = old_blocks;
- });
- }
-
- /// Returns either the selected text, or the content of the Markdown code
- /// block surrounding the cursor.
- fn get_selection_or_code_block(
- context_editor_view: &Entity<TextThreadEditor>,
- cx: &mut Context<Workspace>,
- ) -> Option<(String, bool)> {
- const CODE_FENCE_DELIMITER: &str = "```";
-
- let text_thread_editor = context_editor_view.read(cx).editor.clone();
- text_thread_editor.update(cx, |text_thread_editor, cx| {
- let display_map = text_thread_editor.display_snapshot(cx);
- if text_thread_editor
- .selections
- .newest::<Point>(&display_map)
- .is_empty()
- {
- let snapshot = text_thread_editor.buffer().read(cx).snapshot(cx);
- let (_, _, snapshot) = snapshot.as_singleton()?;
-
- let head = text_thread_editor
- .selections
- .newest::<Point>(&display_map)
- .head();
- let offset = snapshot.point_to_offset(head);
-
- let surrounding_code_block_range = find_surrounding_code_block(snapshot, offset)?;
- let mut text = snapshot
- .text_for_range(surrounding_code_block_range)
- .collect::<String>();
-
- // If there is no newline trailing the closing three-backticks, then
- // tree-sitter-md extends the range of the content node to include
- // the backticks.
- if text.ends_with(CODE_FENCE_DELIMITER) {
- text.drain((text.len() - CODE_FENCE_DELIMITER.len())..);
- }
-
- (!text.is_empty()).then_some((text, true))
- } else {
- let selection = text_thread_editor.selections.newest_adjusted(&display_map);
- let buffer = text_thread_editor.buffer().read(cx).snapshot(cx);
- let selected_text = buffer.text_for_range(selection.range()).collect::<String>();
-
- (!selected_text.is_empty()).then_some((selected_text, false))
- }
- })
- }
-
- pub fn insert_selection(
- workspace: &mut Workspace,
- _: &InsertIntoEditor,
- window: &mut Window,
- cx: &mut Context<Workspace>,
- ) {
- let Some(agent_panel_delegate) = <dyn AgentPanelDelegate>::try_global(cx) else {
- return;
- };
- let Some(context_editor_view) =
- agent_panel_delegate.active_text_thread_editor(workspace, window, cx)
- else {
- return;
- };
- let Some(active_editor_view) = workspace
- .active_item(cx)
- .and_then(|item| item.act_as::<Editor>(cx))
- else {
- return;
- };
-
- if let Some((text, _)) = Self::get_selection_or_code_block(&context_editor_view, cx) {
- active_editor_view.update(cx, |editor, cx| {
- editor.insert(&text, window, cx);
- editor.focus_handle(cx).focus(window, cx);
- })
- }
- }
-
- pub fn copy_code(
- workspace: &mut Workspace,
- _: &CopyCode,
- window: &mut Window,
- cx: &mut Context<Workspace>,
- ) {
- let result = maybe!({
- let agent_panel_delegate = <dyn AgentPanelDelegate>::try_global(cx)?;
- let context_editor_view =
- agent_panel_delegate.active_text_thread_editor(workspace, window, cx)?;
- Self::get_selection_or_code_block(&context_editor_view, cx)
- });
- let Some((text, is_code_block)) = result else {
- return;
- };
-
- cx.write_to_clipboard(ClipboardItem::new_string(text));
-
- struct CopyToClipboardToast;
- workspace.show_toast(
- Toast::new(
- NotificationId::unique::<CopyToClipboardToast>(),
- format!(
- "{} copied to clipboard.",
- if is_code_block {
- "Code block"
- } else {
- "Selection"
- }
- ),
- )
- .autohide(),
- cx,
- );
- }
-
- pub fn handle_insert_dragged_files(
- workspace: &mut Workspace,
- action: &InsertDraggedFiles,
- window: &mut Window,
- cx: &mut Context<Workspace>,
- ) {
- let Some(agent_panel_delegate) = <dyn AgentPanelDelegate>::try_global(cx) else {
- return;
- };
- let Some(context_editor_view) =
- agent_panel_delegate.active_text_thread_editor(workspace, window, cx)
- else {
- return;
- };
-
- let project = context_editor_view.read(cx).project.clone();
-
- let paths = match action {
- InsertDraggedFiles::ProjectPaths(paths) => Task::ready((paths.clone(), vec![])),
- InsertDraggedFiles::ExternalFiles(paths) => {
- let tasks = paths
- .clone()
- .into_iter()
- .map(|path| Workspace::project_path_for_path(project.clone(), &path, false, cx))
- .collect::<Vec<_>>();
-
- cx.background_spawn(async move {
- let mut paths = vec![];
- let mut 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() {
- worktrees.push(worktree);
- paths.push(project_path);
- }
- }
-
- (paths, worktrees)
- })
- }
- };
-
- context_editor_view.update(cx, |_, cx| {
- cx.spawn_in(window, async move |this, cx| {
- let (paths, dragged_file_worktrees) = paths.await;
- this.update_in(cx, |this, window, cx| {
- this.insert_dragged_files(paths, dragged_file_worktrees, window, cx);
- })
- .ok();
- })
- .detach();
- })
- }
-
- pub fn insert_dragged_files(
- &mut self,
- opened_paths: Vec<ProjectPath>,
- added_worktrees: Vec<Entity<Worktree>>,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- let mut file_slash_command_args = vec![];
- for project_path in opened_paths.into_iter() {
- let Some(worktree) = self
- .project
- .read(cx)
- .worktree_for_id(project_path.worktree_id, cx)
- else {
- continue;
- };
- let path_style = worktree.read(cx).path_style();
- let full_path = worktree
- .read(cx)
- .root_name()
- .join(&project_path.path)
- .display(path_style)
- .into_owned();
- file_slash_command_args.push(full_path);
- }
-
- let cmd_name = FileSlashCommand.name();
-
- let file_argument = file_slash_command_args.join(" ");
-
- self.editor.update(cx, |editor, cx| {
- editor.insert("\n", window, cx);
- editor.insert(&format!("/{} {}", cmd_name, file_argument), window, cx);
- });
- self.confirm_command(&ConfirmCommand, window, cx);
- self.dragged_file_worktrees.extend(added_worktrees);
- }
-
- pub fn quote_selection(
- workspace: &mut Workspace,
- _: &AddSelectionToThread,
- window: &mut Window,
- cx: &mut Context<Workspace>,
- ) {
- let Some(agent_panel_delegate) = <dyn AgentPanelDelegate>::try_global(cx) else {
- return;
- };
-
- // Get buffer info for the delegate call (even if empty, ThreadView ignores these
- // params and calls insert_selections which handles both terminal and buffer)
- if let Some((selections, buffer)) = maybe!({
- let editor = workspace
- .active_item(cx)
- .and_then(|item| item.act_as::<Editor>(cx))?;
-
- let buffer = editor.read(cx).buffer().clone();
- let snapshot = buffer.read(cx).snapshot(cx);
- let selections = editor.update(cx, |editor, cx| {
- editor
- .selections
- .all_adjusted(&editor.display_snapshot(cx))
- .into_iter()
- .map(|s| {
- let (start, end) = if s.is_empty() {
- let row = multi_buffer::MultiBufferRow(s.start.row);
- let line_start = text::Point::new(s.start.row, 0);
- let line_end = text::Point::new(s.start.row, snapshot.line_len(row));
- (line_start, line_end)
- } else {
- (s.start, s.end)
- };
- snapshot.anchor_after(start)..snapshot.anchor_before(end)
- })
- .collect::<Vec<_>>()
- });
- Some((selections, buffer))
- }) {
- agent_panel_delegate.quote_selection(workspace, selections, buffer, window, cx);
- }
- }
-
- pub fn quote_ranges(
- &mut self,
- ranges: Vec<Range<Point>>,
- snapshot: MultiBufferSnapshot,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- let creases = selections_creases(ranges, snapshot, cx);
-
- self.editor.update(cx, |editor, cx| {
- editor.insert("\n", window, cx);
- for (text, crease_title) in creases {
- let point = editor
- .selections
- .newest::<Point>(&editor.display_snapshot(cx))
- .head();
- let start_row = MultiBufferRow(point.row);
-
- editor.insert(&text, window, cx);
-
- let snapshot = editor.buffer().read(cx).snapshot(cx);
- let anchor_before = snapshot.anchor_after(point);
- let anchor_after = editor
- .selections
- .newest_anchor()
- .head()
- .bias_left(&snapshot);
-
- editor.insert("\n", window, cx);
-
- let fold_placeholder =
- quote_selection_fold_placeholder(crease_title, cx.entity().downgrade());
- let crease = Crease::inline(
- anchor_before..anchor_after,
- fold_placeholder,
- render_quote_selection_output_toggle,
- |_, _, _, _| Empty.into_any(),
- );
- editor.insert_creases(vec![crease], cx);
- editor.fold_at(start_row, window, cx);
- }
- })
- }
-
- pub fn quote_terminal_text(
- &mut self,
- text: String,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- let crease_title = "terminal".to_string();
- let formatted_text = format!("```console\n{}\n```\n", text);
-
- self.editor.update(cx, |editor, cx| {
- // Insert newline first if not at the start of a line
- let point = editor
- .selections
- .newest::<Point>(&editor.display_snapshot(cx))
- .head();
- if point.column > 0 {
- editor.insert("\n", window, cx);
- }
-
- let point = editor
- .selections
- .newest::<Point>(&editor.display_snapshot(cx))
- .head();
- let start_row = MultiBufferRow(point.row);
-
- editor.insert(&formatted_text, window, cx);
-
- let snapshot = editor.buffer().read(cx).snapshot(cx);
- let anchor_before = snapshot.anchor_after(point);
- let anchor_after = editor
- .selections
- .newest_anchor()
- .head()
- .bias_left(&snapshot);
-
- let fold_placeholder =
- quote_selection_fold_placeholder(crease_title, cx.entity().downgrade());
- let crease = Crease::inline(
- anchor_before..anchor_after,
- fold_placeholder,
- render_quote_selection_output_toggle,
- |_, _, _, _| Empty.into_any(),
- );
- editor.insert_creases(vec![crease], cx);
- editor.fold_at(start_row, window, cx);
- })
- }
-
- fn copy(&mut self, _: &editor::actions::Copy, _window: &mut Window, cx: &mut Context<Self>) {
- if self.editor.read(cx).selections.count() == 1 {
- let (copied_text, metadata, _) = self.get_clipboard_contents(cx);
- cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata(
- copied_text,
- metadata,
- ));
- cx.stop_propagation();
- return;
- }
-
- cx.propagate();
- }
-
- fn cut(&mut self, _: &editor::actions::Cut, window: &mut Window, cx: &mut Context<Self>) {
- if self.editor.read(cx).selections.count() == 1 {
- let (copied_text, metadata, selections) = self.get_clipboard_contents(cx);
-
- self.editor.update(cx, |editor, cx| {
- editor.transact(window, cx, |this, window, cx| {
- this.change_selections(Default::default(), window, cx, |s| {
- s.select(selections);
- });
- this.insert("", window, cx);
- cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata(
- copied_text,
- metadata,
- ));
- });
- });
-
- cx.stop_propagation();
- return;
- }
-
- cx.propagate();
- }
-
- fn get_clipboard_contents(
- &mut self,
- cx: &mut Context<Self>,
- ) -> (
- String,
- CopyMetadata,
- Vec<text::Selection<MultiBufferOffset>>,
- ) {
- let (mut selection, creases) = self.editor.update(cx, |editor, cx| {
- let mut selection = editor
- .selections
- .newest_adjusted(&editor.display_snapshot(cx));
- let snapshot = editor.buffer().read(cx).snapshot(cx);
-
- selection.goal = SelectionGoal::None;
-
- let selection_start = snapshot.point_to_offset(selection.start);
-
- (
- selection.map(|point| snapshot.point_to_offset(point)),
- editor.display_map.update(cx, |display_map, cx| {
- display_map
- .snapshot(cx)
- .crease_snapshot
- .creases_in_range(
- MultiBufferRow(selection.start.row)
- ..MultiBufferRow(selection.end.row + 1),
- &snapshot,
- )
- .filter_map(|crease| {
- if let Crease::Inline {
- range, metadata, ..
- } = &crease
- {
- let metadata = metadata.as_ref()?;
- let start = range
- .start
- .to_offset(&snapshot)
- .saturating_sub(selection_start);
- let end = range
- .end
- .to_offset(&snapshot)
- .saturating_sub(selection_start);
-
- let range_relative_to_selection = start..end;
- if !range_relative_to_selection.is_empty() {
- return Some(SelectedCreaseMetadata {
- range_relative_to_selection,
- crease: metadata.clone(),
- });
- }
- }
- None
- })
- .collect::<Vec<_>>()
- }),
- )
- });
-
- let text_thread = self.text_thread.read(cx);
-
- let mut text = String::new();
-
- // If selection is empty, we want to copy the entire line
- if selection.range().is_empty() {
- let snapshot = self.editor.read(cx).buffer().read(cx).snapshot(cx);
- let point = snapshot.offset_to_point(selection.range().start);
- selection.start = snapshot.point_to_offset(Point::new(point.row, 0));
- selection.end = snapshot
- .point_to_offset(cmp::min(Point::new(point.row + 1, 0), snapshot.max_point()));
- for chunk in snapshot.text_for_range(selection.range()) {
- text.push_str(chunk);
- }
- } else {
- for message in text_thread.messages(cx) {
- if message.offset_range.start >= selection.range().end.0 {
- break;
- } else if message.offset_range.end >= selection.range().start.0 {
- let range = cmp::max(message.offset_range.start, selection.range().start.0)
- ..cmp::min(message.offset_range.end, selection.range().end.0);
- if !range.is_empty() {
- for chunk in text_thread.buffer().read(cx).text_for_range(range) {
- text.push_str(chunk);
- }
- if message.offset_range.end < selection.range().end.0 {
- text.push('\n');
- }
- }
- }
- }
- }
- (text, CopyMetadata { creases }, vec![selection])
- }
-
- fn paste(
- &mut self,
- action: &editor::actions::Paste,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- let Some(workspace) = self.workspace.upgrade() else {
- return;
- };
- let editor_clipboard_selections = cx.read_from_clipboard().and_then(|item| {
- item.entries().iter().find_map(|entry| match entry {
- ClipboardEntry::String(text) => {
- text.metadata_json::<Vec<editor::ClipboardSelection>>()
- }
- _ => None,
- })
- });
-
- // Insert creases for pasted clipboard selections that:
- // 1. Contain exactly one selection
- // 2. Have an associated file path
- // 3. Span multiple lines (not single-line selections)
- // 4. Belong to a file that exists in the current project
- let should_insert_creases = util::maybe!({
- let selections = editor_clipboard_selections.as_ref()?;
- if selections.len() > 1 {
- return Some(false);
- }
- let selection = selections.first()?;
- let file_path = selection.file_path.as_ref()?;
- let line_range = selection.line_range.as_ref()?;
-
- if line_range.start() == line_range.end() {
- return Some(false);
- }
-
- Some(
- workspace
- .read(cx)
- .project()
- .read(cx)
- .project_path_for_absolute_path(file_path, cx)
- .is_some(),
- )
- })
- .unwrap_or(false);
-
- if should_insert_creases && let Some(clipboard_item) = cx.read_from_clipboard() {
- let clipboard_text = clipboard_item
- .entries()
- .iter()
- .find_map(|entry| match entry {
- ClipboardEntry::String(s) => Some(s),
- _ => None,
- });
- if let Some(clipboard_text) = clipboard_text {
- if let Some(selections) = editor_clipboard_selections {
- cx.stop_propagation();
-
- let text = clipboard_text.text();
- self.editor.update(cx, |editor, cx| {
- let mut current_offset = 0;
- let weak_editor = cx.entity().downgrade();
-
- for selection in selections {
- if let (Some(file_path), Some(line_range)) =
- (selection.file_path, selection.line_range)
- {
- let selected_text =
- &text[current_offset..current_offset + selection.len];
- let fence = assistant_slash_commands::codeblock_fence_for_path(
- file_path.to_str(),
- Some(line_range.clone()),
- );
- let formatted_text = format!("{fence}{selected_text}\n```");
-
- let insert_point = editor
- .selections
- .newest::<Point>(&editor.display_snapshot(cx))
- .head();
- let start_row = MultiBufferRow(insert_point.row);
-
- editor.insert(&formatted_text, window, cx);
-
- let snapshot = editor.buffer().read(cx).snapshot(cx);
- let anchor_before = snapshot.anchor_after(insert_point);
- let anchor_after = editor
- .selections
- .newest_anchor()
- .head()
- .bias_left(&snapshot);
-
- editor.insert("\n", window, cx);
-
- let crease_text = acp_thread::selection_name(
- Some(file_path.as_ref()),
- &line_range,
- );
-
- let fold_placeholder = quote_selection_fold_placeholder(
- crease_text,
- weak_editor.clone(),
- );
- let crease = Crease::inline(
- anchor_before..anchor_after,
- fold_placeholder,
- render_quote_selection_output_toggle,
- |_, _, _, _| Empty.into_any(),
- );
- editor.insert_creases(vec![crease], cx);
- editor.fold_at(start_row, window, cx);
-
- current_offset += selection.len;
- if !selection.is_entire_line && current_offset < text.len() {
- current_offset += 1;
- }
- }
- }
- });
- return;
- }
- }
- }
-
- cx.stop_propagation();
-
- let clipboard_item = cx.read_from_clipboard();
-
- let mut images: Vec<gpui::Image> = Vec::new();
- let mut paths: Vec<std::path::PathBuf> = Vec::new();
- let mut metadata: Option<CopyMetadata> = None;
-
- if let Some(item) = &clipboard_item {
- for entry in item.entries() {
- match entry {
- ClipboardEntry::Image(image) => images.push(image.clone()),
- ClipboardEntry::ExternalPaths(external) => {
- paths.extend(external.paths().iter().cloned());
- }
- ClipboardEntry::String(text) => {
- if metadata.is_none() {
- metadata = text.metadata_json::<CopyMetadata>();
- }
- }
- }
- }
- }
-
- let default_image_name: SharedString = "Image".into();
- for path in paths {
- let Some((image, _)) = load_external_image_from_path(&path, &default_image_name) else {
- continue;
- };
- images.push(image);
- }
-
- // Respect entry priority order β if the first entry is text, the source
- // application considers text the primary content. Discard collected images
- // so the text-paste branch runs instead.
- if clipboard_item
- .as_ref()
- .and_then(|item| item.entries().first())
- .is_some_and(|entry| matches!(entry, ClipboardEntry::String(_)))
- {
- images.clear();
- }
-
- if images.is_empty() {
- self.editor.update(cx, |editor, cx| {
- let paste_position = editor
- .selections
- .newest::<MultiBufferOffset>(&editor.display_snapshot(cx))
- .head();
- editor.paste(action, window, cx);
-
- if let Some(metadata) = metadata {
- let buffer = editor.buffer().read(cx).snapshot(cx);
-
- let mut buffer_rows_to_fold = BTreeSet::new();
- let weak_editor = cx.entity().downgrade();
- editor.insert_creases(
- metadata.creases.into_iter().map(|metadata| {
- let start = buffer.anchor_after(
- paste_position + metadata.range_relative_to_selection.start,
- );
- let end = buffer.anchor_before(
- paste_position + metadata.range_relative_to_selection.end,
- );
-
- let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
- buffer_rows_to_fold.insert(buffer_row);
- Crease::inline(
- start..end,
- FoldPlaceholder {
- render: render_fold_icon_button(
- weak_editor.clone(),
- metadata.crease.icon_path.clone(),
- metadata.crease.label.clone(),
- ),
- ..Default::default()
- },
- render_slash_command_output_toggle,
- |_, _, _, _| Empty.into_any(),
- )
- .with_metadata(metadata.crease)
- }),
- cx,
- );
- for buffer_row in buffer_rows_to_fold.into_iter().rev() {
- editor.fold_at(buffer_row, window, cx);
- }
- }
- });
- } else {
- let mut image_positions = Vec::new();
- self.editor.update(cx, |editor, cx| {
- editor.transact(window, cx, |editor, _window, cx| {
- let edits = editor
- .selections
- .all::<MultiBufferOffset>(&editor.display_snapshot(cx))
- .into_iter()
- .map(|selection| (selection.start..selection.end, "\n"));
- editor.edit(edits, cx);
-
- let snapshot = editor.buffer().read(cx).snapshot(cx);
- for selection in editor
- .selections
- .all::<MultiBufferOffset>(&editor.display_snapshot(cx))
- {
- image_positions.push(snapshot.anchor_before(selection.end));
- }
- });
- });
-
- self.text_thread.update(cx, |text_thread, cx| {
- for image in images {
- let Some(render_image) = image.to_image_data(cx.svg_renderer()).log_err()
- else {
- continue;
- };
- let image_id = image.id();
- let image_task = LanguageModelImage::from_image(Arc::new(image), cx).shared();
-
- for image_position in image_positions.iter() {
- text_thread.insert_content(
- Content::Image {
- anchor: image_position.text_anchor,
- image_id,
- image: image_task.clone(),
- render_image: render_image.clone(),
- },
- cx,
- );
- }
- }
- });
- }
- }
-
- fn paste_raw(&mut self, _: &PasteRaw, window: &mut Window, cx: &mut Context<Self>) {
- self.editor.update(cx, |editor, cx| {
- editor.paste(&editor::actions::Paste, window, cx);
- });
- }
-
- fn update_image_blocks(&mut self, cx: &mut Context<Self>) {
- self.editor.update(cx, |editor, cx| {
- let buffer = editor.buffer().read(cx).snapshot(cx);
- let excerpt_id = buffer.as_singleton().unwrap().0;
- let old_blocks = std::mem::take(&mut self.image_blocks);
- let new_blocks = self
- .text_thread
- .read(cx)
- .contents(cx)
- .map(
- |Content::Image {
- anchor,
- render_image,
- ..
- }| (anchor, render_image),
- )
- .filter_map(|(anchor, render_image)| {
- const MAX_HEIGHT_IN_LINES: u32 = 8;
- let anchor = buffer.anchor_in_excerpt(excerpt_id, anchor).unwrap();
- let image = render_image;
- anchor.is_valid(&buffer).then(|| BlockProperties {
- placement: BlockPlacement::Above(anchor),
- height: Some(MAX_HEIGHT_IN_LINES),
- style: BlockStyle::Sticky,
- render: Arc::new(move |cx| {
- let image_size = size_for_image(
- &image,
- size(
- cx.max_width - cx.margins.gutter.full_width(),
- MAX_HEIGHT_IN_LINES as f32 * cx.line_height,
- ),
- );
- h_flex()
- .pl(cx.margins.gutter.full_width())
- .child(
- img(image.clone())
- .object_fit(gpui::ObjectFit::ScaleDown)
- .w(image_size.width)
- .h(image_size.height),
- )
- .into_any_element()
- }),
- priority: 0,
- })
- })
- .collect::<Vec<_>>();
-
- editor.remove_blocks(old_blocks, None, cx);
- let ids = editor.insert_blocks(new_blocks, None, cx);
- self.image_blocks = HashSet::from_iter(ids);
- });
- }
-
- fn split(&mut self, _: &Split, _window: &mut Window, cx: &mut Context<Self>) {
- self.text_thread.update(cx, |text_thread, cx| {
- let selections = self.editor.read(cx).selections.disjoint_anchors_arc();
- for selection in selections.as_ref() {
- let buffer = self.editor.read(cx).buffer().read(cx).snapshot(cx);
- let range = selection
- .map(|endpoint| endpoint.to_offset(&buffer))
- .range();
- text_thread.split_message(range.start.0..range.end.0, cx);
- }
- });
- }
-
- fn save(&mut self, _: &Save, _window: &mut Window, cx: &mut Context<Self>) {
- self.text_thread.update(cx, |text_thread, cx| {
- text_thread.save(Some(Duration::from_millis(500)), self.fs.clone(), cx)
- });
- }
-
- pub fn title(&self, cx: &App) -> SharedString {
- self.text_thread.read(cx).summary().or_default()
- }
-
- pub fn regenerate_summary(&mut self, cx: &mut Context<Self>) {
- self.text_thread
- .update(cx, |text_thread, cx| text_thread.summarize(true, cx));
- }
-
- fn render_remaining_tokens(&self, cx: &App) -> Option<impl IntoElement + use<>> {
- let (token_count_color, token_count, max_token_count, tooltip) =
- match token_state(&self.text_thread, cx)? {
- TokenState::NoTokensLeft {
- max_token_count,
- token_count,
- } => (
- Color::Error,
- token_count,
- max_token_count,
- Some("Token Limit Reached"),
- ),
- TokenState::HasMoreTokens {
- max_token_count,
- token_count,
- over_warn_threshold,
- } => {
- let (color, tooltip) = if over_warn_threshold {
- (Color::Warning, Some("Token Limit is Close to Exhaustion"))
- } else {
- (Color::Muted, None)
- };
- (color, token_count, max_token_count, tooltip)
- }
- };
-
- Some(
- h_flex()
- .id("token-count")
- .gap_0p5()
- .child(
- Label::new(humanize_token_count(token_count))
- .size(LabelSize::Small)
- .color(token_count_color),
- )
- .child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
- .child(
- Label::new(humanize_token_count(max_token_count))
- .size(LabelSize::Small)
- .color(Color::Muted),
- )
- .when_some(tooltip, |element, tooltip| {
- element.tooltip(Tooltip::text(tooltip))
- }),
- )
- }
-
- fn render_send_button(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
- let focus_handle = self.focus_handle(cx);
-
- let (style, tooltip) = match token_state(&self.text_thread, cx) {
- Some(TokenState::NoTokensLeft { .. }) => (
- ButtonStyle::Tinted(TintColor::Error),
- Some(Tooltip::text("Token limit reached")(window, cx)),
- ),
- Some(TokenState::HasMoreTokens {
- over_warn_threshold,
- ..
- }) => {
- let (style, tooltip) = if over_warn_threshold {
- (
- ButtonStyle::Tinted(TintColor::Warning),
- Some(Tooltip::text("Token limit is close to exhaustion")(
- window, cx,
- )),
- )
- } else {
- (ButtonStyle::Filled, None)
- };
- (style, tooltip)
- }
- None => (ButtonStyle::Filled, None),
- };
-
- Button::new("send_button", "Send")
- .label_size(LabelSize::Small)
- .disabled(self.sending_disabled(cx))
- .style(style)
- .when_some(tooltip, |button, tooltip| {
- button.tooltip(move |_, _| tooltip.clone())
- })
- .layer(ElevationIndex::ModalSurface)
- .key_binding(
- KeyBinding::for_action_in(&Assist, &focus_handle, cx)
- .map(|kb| kb.size(rems_from_px(12.))),
- )
- .on_click(move |_event, window, cx| {
- focus_handle.dispatch_action(&Assist, window, cx);
- })
- }
-
- /// Whether or not we should allow messages to be sent.
- /// Will return false if the selected provided has a configuration error or
- /// if the user has not accepted the terms of service for this provider.
- fn sending_disabled(&self, cx: &mut Context<'_, TextThreadEditor>) -> bool {
- let model_registry = LanguageModelRegistry::read_global(cx);
- let Some(configuration_error) =
- model_registry.configuration_error(model_registry.default_model(), cx)
- else {
- return false;
- };
-
- match configuration_error {
- ConfigurationError::NoProvider
- | ConfigurationError::ModelNotFound
- | ConfigurationError::ProviderNotAuthenticated(_) => true,
- }
- }
-
- fn render_inject_context_menu(&self, cx: &mut Context<Self>) -> impl IntoElement {
- slash_command_picker::SlashCommandSelector::new(
- self.slash_commands.clone(),
- cx.entity().downgrade(),
- IconButton::new("trigger", IconName::Plus)
- .icon_size(IconSize::Small)
- .icon_color(Color::Muted)
- .selected_icon_color(Color::Accent)
- .selected_style(ButtonStyle::Filled),
- move |_window, cx| {
- Tooltip::with_meta("Add Context", None, "Type / to insert via keyboard", cx)
- },
- )
- }
-
- fn render_language_model_selector(
- &self,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) -> impl IntoElement {
- let active_model = LanguageModelRegistry::read_global(cx)
- .default_model()
- .map(|default| default.model);
- let model_name = match active_model {
- Some(model) => model.name().0,
- None => SharedString::from("Select Model"),
- };
-
- let active_provider = LanguageModelRegistry::read_global(cx)
- .default_model()
- .map(|default| default.provider);
-
- let provider_icon = active_provider
- .as_ref()
- .map(|p| p.icon())
- .unwrap_or(IconOrSvg::Icon(IconName::ZedAgent));
-
- let (color, icon) = if self.language_model_selector_menu_handle.is_deployed() {
- (Color::Accent, IconName::ChevronUp)
- } else {
- (Color::Muted, IconName::ChevronDown)
- };
-
- let provider_icon_element = match provider_icon {
- IconOrSvg::Svg(path) => Icon::from_external_svg(path),
- IconOrSvg::Icon(name) => Icon::new(name),
- }
- .color(color)
- .size(IconSize::XSmall);
-
- let show_cycle_row = self
- .language_model_selector
- .read(cx)
- .delegate
- .favorites_count()
- > 1;
-
- let tooltip = Tooltip::element({
- move |_, _cx| {
- ModelSelectorTooltip::new()
- .show_cycle_row(show_cycle_row)
- .into_any_element()
- }
- });
-
- PickerPopoverMenu::new(
- self.language_model_selector.clone(),
- Button::new("active-model", model_name)
- .color(color)
- .label_size(LabelSize::Small)
- .start_icon(provider_icon_element)
- .end_icon(Icon::new(icon).color(color).size(IconSize::XSmall)),
- tooltip,
- gpui::Corner::BottomRight,
- cx,
- )
- .with_handle(self.language_model_selector_menu_handle.clone())
- .render(window, cx)
- }
-
- fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
- let last_error = self.last_error.as_ref()?;
-
- Some(
- div()
- .absolute()
- .right_3()
- .bottom_12()
- .max_w_96()
- .py_2()
- .px_3()
- .elevation_2(cx)
- .occlude()
- .child(match last_error {
- AssistError::PaymentRequired => self.render_payment_required_error(cx),
- AssistError::Message(error_message) => {
- self.render_assist_error(error_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()
- .child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
- |this, _, _window, cx| {
- this.last_error = None;
- cx.open_url(&zed_urls::account_url(cx));
- cx.notify();
- },
- )))
- .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
- |this, _, _window, cx| {
- this.last_error = None;
- cx.notify();
- },
- ))),
- )
- .into_any()
- }
-
- fn render_assist_error(
- &self,
- error_message: &SharedString,
- cx: &mut Context<Self>,
- ) -> AnyElement {
- v_flex()
- .gap_0p5()
- .child(
- h_flex()
- .gap_1p5()
- .items_center()
- .child(Icon::new(IconName::XCircle).color(Color::Error))
- .child(
- Label::new("Error interacting with language model")
- .weight(FontWeight::MEDIUM),
- ),
- )
- .child(
- div()
- .id("error-message")
- .max_h_32()
- .overflow_y_scroll()
- .child(Label::new(error_message.clone())),
- )
- .child(
- h_flex()
- .justify_end()
- .mt_1()
- .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
- |this, _, _window, cx| {
- this.last_error = None;
- cx.notify();
- },
- ))),
- )
- .into_any()
- }
-}
-
-/// Returns the contents of the *outermost* fenced code block that contains the given offset.
-fn find_surrounding_code_block(snapshot: &BufferSnapshot, offset: usize) -> Option<Range<usize>> {
- const CODE_BLOCK_NODE: &str = "fenced_code_block";
- const CODE_BLOCK_CONTENT: &str = "code_fence_content";
-
- let layer = snapshot.syntax_layers().next()?;
-
- let root_node = layer.node();
- let mut cursor = root_node.walk();
-
- // Go to the first child for the given offset
- while cursor.goto_first_child_for_byte(offset).is_some() {
- // If we're at the end of the node, go to the next one.
- // Example: if you have a fenced-code-block, and you're on the start of the line
- // right after the closing ```, you want to skip the fenced-code-block and
- // go to the next sibling.
- if cursor.node().end_byte() == offset {
- cursor.goto_next_sibling();
- }
-
- if cursor.node().start_byte() > offset {
- break;
- }
-
- // We found the fenced code block.
- if cursor.node().kind() == CODE_BLOCK_NODE {
- // Now we need to find the child node that contains the code.
- cursor.goto_first_child();
- loop {
- if cursor.node().kind() == CODE_BLOCK_CONTENT {
- return Some(cursor.node().byte_range());
- }
- if !cursor.goto_next_sibling() {
- break;
- }
- }
- }
- }
-
- None
-}
-
-fn render_thought_process_fold_icon_button(
- editor: WeakEntity<Editor>,
- status: ThoughtProcessStatus,
-) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
- Arc::new(move |fold_id, fold_range, _cx| {
- let editor = editor.clone();
-
- let button = ButtonLike::new(fold_id).layer(ElevationIndex::ElevatedSurface);
- let button = match status {
- ThoughtProcessStatus::Pending => button
- .child(
- Icon::new(IconName::ToolThink)
- .size(IconSize::Small)
- .color(Color::Muted),
- )
- .child(
- Label::new("Thinkingβ¦").color(Color::Muted).with_animation(
- "pulsating-label",
- Animation::new(Duration::from_secs(2))
- .repeat()
- .with_easing(pulsating_between(0.4, 0.8)),
- |label, delta| label.alpha(delta),
- ),
- ),
- ThoughtProcessStatus::Completed => button
- .style(ButtonStyle::Filled)
- .child(Icon::new(IconName::ToolThink).size(IconSize::Small))
- .child(Label::new("Thought Process").single_line()),
- };
-
- button
- .on_click(move |_, window, cx| {
- editor
- .update(cx, |editor, cx| {
- let buffer_start = fold_range
- .start
- .to_point(&editor.buffer().read(cx).read(cx));
- let buffer_row = MultiBufferRow(buffer_start.row);
- editor.unfold_at(buffer_row, window, cx);
- })
- .ok();
- })
- .into_any_element()
- })
-}
-
-fn render_fold_icon_button(
- editor: WeakEntity<Editor>,
- icon_path: SharedString,
- label: SharedString,
-) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
- Arc::new(move |fold_id, fold_range, _cx| {
- let editor = editor.clone();
- ButtonLike::new(fold_id)
- .style(ButtonStyle::Filled)
- .layer(ElevationIndex::ElevatedSurface)
- .child(Icon::from_path(icon_path.clone()))
- .child(Label::new(label.clone()).single_line())
- .on_click(move |_, window, cx| {
- editor
- .update(cx, |editor, cx| {
- let buffer_start = fold_range
- .start
- .to_point(&editor.buffer().read(cx).read(cx));
- let buffer_row = MultiBufferRow(buffer_start.row);
- editor.unfold_at(buffer_row, window, cx);
- })
- .ok();
- })
- .into_any_element()
- })
-}
-
-type ToggleFold = Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>;
-
-fn render_slash_command_output_toggle(
- row: MultiBufferRow,
- is_folded: bool,
- fold: ToggleFold,
- _window: &mut Window,
- _cx: &mut App,
-) -> AnyElement {
- Disclosure::new(
- ("slash-command-output-fold-indicator", row.0 as u64),
- !is_folded,
- )
- .toggle_state(is_folded)
- .on_click(move |_e, window, cx| fold(!is_folded, window, cx))
- .into_any_element()
-}
-
-pub fn fold_toggle(
- name: &'static str,
-) -> impl Fn(
- MultiBufferRow,
- bool,
- Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
- &mut Window,
- &mut App,
-) -> AnyElement {
- move |row, is_folded, fold, _window, _cx| {
- Disclosure::new((name, row.0 as u64), !is_folded)
- .toggle_state(is_folded)
- .on_click(move |_e, window, cx| fold(!is_folded, window, cx))
- .into_any_element()
- }
-}
-
-fn quote_selection_fold_placeholder(title: String, editor: WeakEntity<Editor>) -> FoldPlaceholder {
- FoldPlaceholder {
- render: Arc::new({
- move |fold_id, fold_range, _cx| {
- let editor = editor.clone();
- ButtonLike::new(fold_id)
- .style(ButtonStyle::Filled)
- .layer(ElevationIndex::ElevatedSurface)
- .child(Icon::new(IconName::TextSnippet))
- .child(Label::new(title.clone()).single_line())
- .on_click(move |_, window, cx| {
- editor
- .update(cx, |editor, cx| {
- let buffer_start = fold_range
- .start
- .to_point(&editor.buffer().read(cx).read(cx));
- let buffer_row = MultiBufferRow(buffer_start.row);
- editor.unfold_at(buffer_row, window, cx);
- })
- .ok();
- })
- .into_any_element()
- }
- }),
- merge_adjacent: false,
- ..Default::default()
- }
-}
-
-fn render_quote_selection_output_toggle(
- row: MultiBufferRow,
- is_folded: bool,
- fold: ToggleFold,
- _window: &mut Window,
- _cx: &mut App,
-) -> AnyElement {
- Disclosure::new(("quote-selection-indicator", row.0 as u64), !is_folded)
- .toggle_state(is_folded)
- .on_click(move |_e, window, cx| fold(!is_folded, window, cx))
- .into_any_element()
-}
-
-fn render_pending_slash_command_gutter_decoration(
- row: MultiBufferRow,
- status: &PendingSlashCommandStatus,
- confirm_command: Arc<dyn Fn(&mut Window, &mut App)>,
-) -> AnyElement {
- let mut icon = IconButton::new(
- ("slash-command-gutter-decoration", row.0),
- ui::IconName::TriangleRight,
- )
- .on_click(move |_e, window, cx| confirm_command(window, cx))
- .icon_size(ui::IconSize::Small)
- .size(ui::ButtonSize::None);
-
- match status {
- PendingSlashCommandStatus::Idle => {
- icon = icon.icon_color(Color::Muted);
- }
- PendingSlashCommandStatus::Running { .. } => {
- icon = icon.toggle_state(true);
- }
- PendingSlashCommandStatus::Error(_) => icon = icon.icon_color(Color::Error),
- }
-
- icon.into_any_element()
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-struct CopyMetadata {
- creases: Vec<SelectedCreaseMetadata>,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-struct SelectedCreaseMetadata {
- range_relative_to_selection: Range<usize>,
- crease: CreaseMetadata,
-}
-
-impl EventEmitter<EditorEvent> for TextThreadEditor {}
-impl EventEmitter<SearchEvent> for TextThreadEditor {}
-
-impl Render for TextThreadEditor {
- fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
- let language_model_selector = self.language_model_selector_menu_handle.clone();
-
- v_flex()
- .key_context("ContextEditor")
- .capture_action(cx.listener(TextThreadEditor::cancel))
- .capture_action(cx.listener(TextThreadEditor::save))
- .capture_action(cx.listener(TextThreadEditor::copy))
- .capture_action(cx.listener(TextThreadEditor::cut))
- .capture_action(cx.listener(TextThreadEditor::paste))
- .on_action(cx.listener(TextThreadEditor::paste_raw))
- .capture_action(cx.listener(TextThreadEditor::cycle_message_role))
- .capture_action(cx.listener(TextThreadEditor::confirm_command))
- .on_action(cx.listener(TextThreadEditor::assist))
- .on_action(cx.listener(TextThreadEditor::split))
- .on_action(move |_: &ToggleModelSelector, window, cx| {
- language_model_selector.toggle(window, cx);
- })
- .on_action(cx.listener(|this, _: &CycleFavoriteModels, window, cx| {
- this.language_model_selector.update(cx, |selector, cx| {
- selector.delegate.cycle_favorite_models(window, cx);
- });
- }))
- .size_full()
- .child(
- div()
- .flex_grow()
- .bg(cx.theme().colors().editor_background)
- .child(self.editor.clone()),
- )
- .children(self.render_last_error(cx))
- .child(
- h_flex()
- .relative()
- .py_2()
- .pl_1p5()
- .pr_2()
- .w_full()
- .justify_between()
- .border_t_1()
- .border_color(cx.theme().colors().border_variant)
- .bg(cx.theme().colors().editor_background)
- .child(
- h_flex()
- .gap_0p5()
- .child(self.render_inject_context_menu(cx)),
- )
- .child(
- h_flex()
- .gap_2p5()
- .children(self.render_remaining_tokens(cx))
- .child(
- h_flex()
- .gap_1()
- .child(self.render_language_model_selector(window, cx))
- .child(self.render_send_button(window, cx)),
- ),
- ),
- )
- }
-}
-
-impl Focusable for TextThreadEditor {
- fn focus_handle(&self, cx: &App) -> FocusHandle {
- self.editor.focus_handle(cx)
- }
-}
-
-impl Item for TextThreadEditor {
- type Event = editor::EditorEvent;
-
- fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString {
- util::truncate_and_trailoff(&self.title(cx), MAX_TAB_TITLE_LEN).into()
- }
-
- fn to_item_events(event: &Self::Event, f: &mut dyn FnMut(item::ItemEvent)) {
- match event {
- EditorEvent::Edited { .. } => {
- f(item::ItemEvent::Edit);
- }
- EditorEvent::TitleChanged => {
- f(item::ItemEvent::UpdateTab);
- }
- _ => {}
- }
- }
-
- fn tab_tooltip_text(&self, cx: &App) -> Option<SharedString> {
- Some(self.title(cx).to_string().into())
- }
-
- fn as_searchable(
- &self,
- handle: &Entity<Self>,
- _: &App,
- ) -> Option<Box<dyn SearchableItemHandle>> {
- Some(Box::new(handle.clone()))
- }
-
- fn set_nav_history(
- &mut self,
- nav_history: pane::ItemNavHistory,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- self.editor.update(cx, |editor, cx| {
- Item::set_nav_history(editor, nav_history, window, cx)
- })
- }
-
- fn navigate(
- &mut self,
- data: Arc<dyn Any + Send>,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) -> bool {
- self.editor
- .update(cx, |editor, cx| Item::navigate(editor, data, window, cx))
- }
-
- fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- self.editor
- .update(cx, |editor, cx| Item::deactivated(editor, window, cx))
- }
-
- fn act_as_type<'a>(
- &'a self,
- type_id: TypeId,
- self_handle: &'a Entity<Self>,
- _: &'a App,
- ) -> Option<gpui::AnyEntity> {
- if type_id == TypeId::of::<Self>() {
- Some(self_handle.clone().into())
- } else if type_id == TypeId::of::<Editor>() {
- Some(self.editor.clone().into())
- } else {
- None
- }
- }
-
- fn include_in_nav_history() -> bool {
- false
- }
-}
-
-impl SearchableItem for TextThreadEditor {
- type Match = <Editor as SearchableItem>::Match;
-
- fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- self.editor.update(cx, |editor, cx| {
- editor.clear_matches(window, cx);
- });
- }
-
- fn update_matches(
- &mut self,
- matches: &[Self::Match],
- active_match_index: Option<usize>,
- token: SearchToken,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- self.editor.update(cx, |editor, cx| {
- editor.update_matches(matches, active_match_index, token, window, cx)
- });
- }
-
- fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {
- self.editor
- .update(cx, |editor, cx| editor.query_suggestion(window, cx))
- }
-
- fn activate_match(
- &mut self,
- index: usize,
- matches: &[Self::Match],
- token: SearchToken,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- self.editor.update(cx, |editor, cx| {
- editor.activate_match(index, matches, token, window, cx);
- });
- }
-
- fn select_matches(
- &mut self,
- matches: &[Self::Match],
- token: SearchToken,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- self.editor.update(cx, |editor, cx| {
- editor.select_matches(matches, token, window, cx)
- });
- }
-
- fn replace(
- &mut self,
- identifier: &Self::Match,
- query: &project::search::SearchQuery,
- token: SearchToken,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- self.editor.update(cx, |editor, cx| {
- editor.replace(identifier, query, token, window, cx)
- });
- }
-
- fn find_matches(
- &mut self,
- query: Arc<project::search::SearchQuery>,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) -> Task<Vec<Self::Match>> {
- self.editor
- .update(cx, |editor, cx| editor.find_matches(query, window, cx))
- }
-
- fn active_match_index(
- &mut self,
- direction: Direction,
- matches: &[Self::Match],
- token: SearchToken,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) -> Option<usize> {
- self.editor.update(cx, |editor, cx| {
- editor.active_match_index(direction, matches, token, window, cx)
- })
- }
-}
-
-impl FollowableItem for TextThreadEditor {
- fn remote_id(&self) -> Option<workspace::ViewId> {
- self.remote_id
- }
-
- fn to_state_proto(&self, window: &mut Window, cx: &mut App) -> Option<proto::view::Variant> {
- let context_id = self.text_thread.read(cx).id().to_proto();
- let editor_proto = self
- .editor
- .update(cx, |editor, cx| editor.to_state_proto(window, cx));
- Some(proto::view::Variant::ContextEditor(
- proto::view::ContextEditor {
- context_id,
- editor: if let Some(proto::view::Variant::Editor(proto)) = editor_proto {
- Some(proto)
- } else {
- None
- },
- },
- ))
- }
-
- fn from_state_proto(
- workspace: Entity<Workspace>,
- id: workspace::ViewId,
- state: &mut Option<proto::view::Variant>,
- window: &mut Window,
- cx: &mut App,
- ) -> Option<Task<Result<Entity<Self>>>> {
- let proto::view::Variant::ContextEditor(_) = state.as_ref()? else {
- return None;
- };
- let Some(proto::view::Variant::ContextEditor(state)) = state.take() else {
- unreachable!()
- };
-
- let text_thread_id = TextThreadId::from_proto(state.context_id);
- let editor_state = state.editor?;
-
- let project = workspace.read(cx).project().clone();
- let agent_panel_delegate = <dyn AgentPanelDelegate>::try_global(cx)?;
-
- let text_thread_editor_task = workspace.update(cx, |workspace, cx| {
- agent_panel_delegate.open_remote_text_thread(workspace, text_thread_id, window, cx)
- });
-
- Some(window.spawn(cx, async move |cx| {
- let text_thread_editor = text_thread_editor_task.await?;
- text_thread_editor
- .update_in(cx, |text_thread_editor, window, cx| {
- text_thread_editor.remote_id = Some(id);
- text_thread_editor.editor.update(cx, |editor, cx| {
- editor.apply_update_proto(
- &project,
- proto::update_view::Variant::Editor(proto::update_view::Editor {
- selections: editor_state.selections,
- pending_selection: editor_state.pending_selection,
- scroll_top_anchor: editor_state.scroll_top_anchor,
- scroll_x: editor_state.scroll_y,
- scroll_y: editor_state.scroll_y,
- ..Default::default()
- }),
- window,
- cx,
- )
- })
- })?
- .await?;
- Ok(text_thread_editor)
- }))
- }
-
- fn to_follow_event(event: &Self::Event) -> Option<item::FollowEvent> {
- Editor::to_follow_event(event)
- }
-
- fn add_event_to_update_proto(
- &self,
- event: &Self::Event,
- update: &mut Option<proto::update_view::Variant>,
- window: &mut Window,
- cx: &mut App,
- ) -> bool {
- self.editor.update(cx, |editor, cx| {
- editor.add_event_to_update_proto(event, update, window, cx)
- })
- }
-
- fn apply_update_proto(
- &mut self,
- project: &Entity<Project>,
- message: proto::update_view::Variant,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) -> Task<Result<()>> {
- self.editor.update(cx, |editor, cx| {
- editor.apply_update_proto(project, message, window, cx)
- })
- }
-
- fn is_project_item(&self, _window: &Window, _cx: &App) -> bool {
- true
- }
-
- fn set_leader_id(
- &mut self,
- leader_id: Option<CollaboratorId>,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- self.editor
- .update(cx, |editor, cx| editor.set_leader_id(leader_id, window, cx))
- }
-
- fn dedup(&self, existing: &Self, _window: &Window, cx: &App) -> Option<item::Dedup> {
- if existing.text_thread.read(cx).id() == self.text_thread.read(cx).id() {
- Some(item::Dedup::KeepExisting)
- } else {
- None
- }
- }
-}
-
-enum PendingSlashCommand {}
-
-fn invoked_slash_command_fold_placeholder(
- command_id: InvokedSlashCommandId,
- text_thread: WeakEntity<TextThread>,
-) -> FoldPlaceholder {
- FoldPlaceholder {
- collapsed_text: None,
- constrain_width: false,
- merge_adjacent: false,
- render: Arc::new(move |fold_id, _, cx| {
- let Some(text_thread) = text_thread.upgrade() else {
- return Empty.into_any();
- };
-
- let Some(command) = text_thread.read(cx).invoked_slash_command(&command_id) else {
- return Empty.into_any();
- };
-
- h_flex()
- .id(fold_id)
- .px_1()
- .ml_6()
- .gap_2()
- .bg(cx.theme().colors().surface_background)
- .rounded_sm()
- .child(Label::new(format!("/{}", command.name)))
- .map(|parent| match &command.status {
- InvokedSlashCommandStatus::Running(_) => {
- parent.child(Icon::new(IconName::ArrowCircle).with_rotate_animation(4))
- }
- InvokedSlashCommandStatus::Error(message) => parent.child(
- Label::new(format!("error: {message}"))
- .single_line()
- .color(Color::Error),
- ),
- InvokedSlashCommandStatus::Finished => parent,
- })
- .into_any_element()
- }),
- type_tag: Some(TypeId::of::<PendingSlashCommand>()),
- }
-}
-
-enum TokenState {
- NoTokensLeft {
- max_token_count: u64,
- token_count: u64,
- },
- HasMoreTokens {
- max_token_count: u64,
- token_count: u64,
- over_warn_threshold: bool,
- },
-}
-
-fn token_state(text_thread: &Entity<TextThread>, cx: &App) -> Option<TokenState> {
- const WARNING_TOKEN_THRESHOLD: f32 = 0.8;
-
- let model = LanguageModelRegistry::read_global(cx)
- .default_model()?
- .model;
- let token_count = text_thread.read(cx).token_count()?;
- let max_token_count = model.max_token_count();
- let token_state = if max_token_count.saturating_sub(token_count) == 0 {
- TokenState::NoTokensLeft {
- max_token_count,
- token_count,
- }
- } else {
- let over_warn_threshold =
- token_count as f32 / max_token_count as f32 >= WARNING_TOKEN_THRESHOLD;
- TokenState::HasMoreTokens {
- max_token_count,
- token_count,
- over_warn_threshold,
- }
- };
- Some(token_state)
-}
-
-fn size_for_image(data: &RenderImage, max_size: Size<Pixels>) -> Size<Pixels> {
- let image_size = data
- .size(0)
- .map(|dimension| Pixels::from(u32::from(dimension)));
- let image_ratio = image_size.width / image_size.height;
- let bounds_ratio = max_size.width / max_size.height;
-
- if image_size.width > max_size.width || image_size.height > max_size.height {
- if bounds_ratio > image_ratio {
- size(
- image_size.width * (max_size.height / image_size.height),
- max_size.height,
- )
- } else {
- size(
- max_size.width,
- image_size.height * (max_size.width / image_size.width),
- )
- }
- } else {
- size(image_size.width, image_size.height)
- }
-}
-
-pub fn humanize_token_count(count: u64) -> String {
- match count {
- 0..=999 => count.to_string(),
- 1000..=9999 => {
- let thousands = count / 1000;
- let hundreds = (count % 1000 + 50) / 100;
- if hundreds == 0 {
- format!("{}k", thousands)
- } else if hundreds == 10 {
- format!("{}k", thousands + 1)
- } else {
- format!("{}.{}k", thousands, hundreds)
- }
- }
- 1_000_000..=9_999_999 => {
- let millions = count / 1_000_000;
- let hundred_thousands = (count % 1_000_000 + 50_000) / 100_000;
- if hundred_thousands == 0 {
- format!("{}M", millions)
- } else if hundred_thousands == 10 {
- format!("{}M", millions + 1)
- } else {
- format!("{}.{}M", millions, hundred_thousands)
- }
- }
- 10_000_000.. => format!("{}M", (count + 500_000) / 1_000_000),
- _ => format!("{}k", (count + 500) / 1000),
- }
-}
-
-pub fn make_lsp_adapter_delegate(
- project: &Entity<Project>,
- cx: &mut App,
-) -> Result<Option<Arc<dyn LspAdapterDelegate>>> {
- project.update(cx, |project, cx| {
- // TODO: Find the right worktree.
- let Some(worktree) = project.worktrees(cx).next() else {
- return Ok(None::<Arc<dyn LspAdapterDelegate>>);
- };
- let http_client = project.client().http_client();
- project.lsp_store().update(cx, |_, cx| {
- Ok(Some(LocalLspAdapterDelegate::new(
- project.languages().clone(),
- project.environment(),
- cx.weak_entity(),
- &worktree,
- http_client,
- project.fs().clone(),
- cx,
- ) as Arc<dyn LspAdapterDelegate>))
- })
- })
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use editor::{MultiBufferOffset, SelectionEffects};
- use fs::FakeFs;
- use gpui::{App, TestAppContext, VisualTestContext};
- use indoc::indoc;
- use language::{Buffer, LanguageRegistry};
- use pretty_assertions::assert_eq;
- use prompt_store::PromptBuilder;
- use text::OffsetRangeExt;
- use unindent::Unindent;
- use util::path;
- use workspace::MultiWorkspace;
-
- #[gpui::test]
- async fn test_copy_paste_whole_message(cx: &mut TestAppContext) {
- let (context, text_thread_editor, mut cx) = setup_text_thread_editor_text(vec![
- (Role::User, "What is the Zed editor?"),
- (
- Role::Assistant,
- "Zed is a modern, high-performance code editor designed from the ground up for speed and collaboration.",
- ),
- (Role::User, ""),
- ],cx).await;
-
- // Select & Copy whole user message
- assert_copy_paste_text_thread_editor(
- &text_thread_editor,
- message_range(&context, 0, &mut cx),
- indoc! {"
- What is the Zed editor?
- Zed is a modern, high-performance code editor designed from the ground up for speed and collaboration.
- What is the Zed editor?
- "},
- &mut cx,
- );
-
- // Select & Copy whole assistant message
- assert_copy_paste_text_thread_editor(
- &text_thread_editor,
- message_range(&context, 1, &mut cx),
- indoc! {"
- What is the Zed editor?
- Zed is a modern, high-performance code editor designed from the ground up for speed and collaboration.
- What is the Zed editor?
- Zed is a modern, high-performance code editor designed from the ground up for speed and collaboration.
- "},
- &mut cx,
- );
- }
-
- #[gpui::test]
- async fn test_copy_paste_no_selection(cx: &mut TestAppContext) {
- let (context, text_thread_editor, mut cx) = setup_text_thread_editor_text(
- vec![
- (Role::User, "user1"),
- (Role::Assistant, "assistant1"),
- (Role::Assistant, "assistant2"),
- (Role::User, ""),
- ],
- cx,
- )
- .await;
-
- // Copy and paste first assistant message
- let message_2_range = message_range(&context, 1, &mut cx);
- assert_copy_paste_text_thread_editor(
- &text_thread_editor,
- message_2_range.start..message_2_range.start,
- indoc! {"
- user1
- assistant1
- assistant2
- assistant1
- "},
- &mut cx,
- );
-
- // Copy and cut second assistant message
- let message_3_range = message_range(&context, 2, &mut cx);
- assert_copy_paste_text_thread_editor(
- &text_thread_editor,
- message_3_range.start..message_3_range.start,
- indoc! {"
- user1
- assistant1
- assistant2
- assistant1
- assistant2
- "},
- &mut cx,
- );
- }
-
- #[gpui::test]
- fn test_find_code_blocks(cx: &mut App) {
- let markdown = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
-
- let buffer = cx.new(|cx| {
- let text = r#"
- line 0
- line 1
- ```rust
- fn main() {}
- ```
- line 5
- line 6
- line 7
- ```go
- func main() {}
- ```
- line 11
- ```
- this is plain text code block
- ```
-
- ```go
- func another() {}
- ```
- line 19
- "#
- .unindent();
- let mut buffer = Buffer::local(text, cx);
- buffer.set_language(Some(markdown.clone()), cx);
- buffer
- });
- let snapshot = buffer.read(cx).snapshot();
-
- let code_blocks = vec![
- Point::new(3, 0)..Point::new(4, 0),
- Point::new(9, 0)..Point::new(10, 0),
- Point::new(13, 0)..Point::new(14, 0),
- Point::new(17, 0)..Point::new(18, 0),
- ]
- .into_iter()
- .map(|range| snapshot.point_to_offset(range.start)..snapshot.point_to_offset(range.end))
- .collect::<Vec<_>>();
-
- let expected_results = vec![
- (0, None),
- (1, None),
- (2, Some(code_blocks[0].clone())),
- (3, Some(code_blocks[0].clone())),
- (4, Some(code_blocks[0].clone())),
- (5, None),
- (6, None),
- (7, None),
- (8, Some(code_blocks[1].clone())),
- (9, Some(code_blocks[1].clone())),
- (10, Some(code_blocks[1].clone())),
- (11, None),
- (12, Some(code_blocks[2].clone())),
- (13, Some(code_blocks[2].clone())),
- (14, Some(code_blocks[2].clone())),
- (15, None),
- (16, Some(code_blocks[3].clone())),
- (17, Some(code_blocks[3].clone())),
- (18, Some(code_blocks[3].clone())),
- (19, None),
- ];
-
- for (row, expected) in expected_results {
- let offset = snapshot.point_to_offset(Point::new(row, 0));
- let range = find_surrounding_code_block(&snapshot, offset);
- assert_eq!(range, expected, "unexpected result on row {:?}", row);
- }
- }
-
- async fn setup_text_thread_editor_text(
- messages: Vec<(Role, &str)>,
- cx: &mut TestAppContext,
- ) -> (
- Entity<TextThread>,
- Entity<TextThreadEditor>,
- VisualTestContext,
- ) {
- cx.update(init_test);
-
- let fs = FakeFs::new(cx.executor());
- let text_thread = create_text_thread_with_messages(messages, cx);
-
- let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
- let window_handle =
- cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
- let workspace = window_handle
- .read_with(cx, |mw, _| mw.workspace().clone())
- .unwrap();
- let mut cx = VisualTestContext::from_window(window_handle.into(), cx);
-
- let weak_workspace = workspace.downgrade();
- let text_thread_editor = workspace.update_in(&mut cx, |_, window, cx| {
- cx.new(|cx| {
- TextThreadEditor::for_text_thread(
- text_thread.clone(),
- fs,
- weak_workspace,
- project,
- None,
- window,
- cx,
- )
- })
- });
-
- (text_thread, text_thread_editor, cx)
- }
-
- fn message_range(
- text_thread: &Entity<TextThread>,
- message_ix: usize,
- cx: &mut TestAppContext,
- ) -> Range<MultiBufferOffset> {
- let range = text_thread.update(cx, |text_thread, cx| {
- text_thread
- .messages(cx)
- .nth(message_ix)
- .unwrap()
- .anchor_range
- .to_offset(&text_thread.buffer().read(cx).snapshot())
- });
- MultiBufferOffset(range.start)..MultiBufferOffset(range.end)
- }
-
- fn assert_copy_paste_text_thread_editor<T: editor::ToOffset>(
- text_thread_editor: &Entity<TextThreadEditor>,
- range: Range<T>,
- expected_text: &str,
- cx: &mut VisualTestContext,
- ) {
- text_thread_editor.update_in(cx, |text_thread_editor, window, cx| {
- text_thread_editor.editor.update(cx, |editor, cx| {
- editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
- s.select_ranges([range])
- });
- });
-
- text_thread_editor.copy(&Default::default(), window, cx);
-
- text_thread_editor.editor.update(cx, |editor, cx| {
- editor.move_to_end(&Default::default(), window, cx);
- });
-
- text_thread_editor.paste(&Default::default(), window, cx);
-
- text_thread_editor.editor.update(cx, |editor, cx| {
- assert_eq!(editor.text(cx), expected_text);
- });
- });
- }
-
- fn create_text_thread_with_messages(
- mut messages: Vec<(Role, &str)>,
- cx: &mut TestAppContext,
- ) -> Entity<TextThread> {
- let registry = Arc::new(LanguageRegistry::test(cx.executor()));
- let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
- cx.new(|cx| {
- let mut text_thread = TextThread::local(
- registry,
- prompt_builder.clone(),
- Arc::new(SlashCommandWorkingSet::default()),
- cx,
- );
- let mut message_1 = text_thread.messages(cx).next().unwrap();
- let (role, text) = messages.remove(0);
-
- loop {
- if role == message_1.role {
- text_thread.buffer().update(cx, |buffer, cx| {
- buffer.edit([(message_1.offset_range, text)], None, cx);
- });
- break;
- }
- let mut ids = HashSet::default();
- ids.insert(message_1.id);
- text_thread.cycle_message_roles(ids, cx);
- message_1 = text_thread.messages(cx).next().unwrap();
- }
-
- let mut last_message_id = message_1.id;
- for (role, text) in messages {
- text_thread.insert_message_after(last_message_id, role, MessageStatus::Done, cx);
- let message = text_thread.messages(cx).last().unwrap();
- last_message_id = message.id;
- text_thread.buffer().update(cx, |buffer, cx| {
- buffer.edit([(message.offset_range, text)], None, cx);
- })
- }
-
- text_thread
- })
- }
-
- fn init_test(cx: &mut App) {
- let settings_store = SettingsStore::test(cx);
- prompt_store::init(cx);
- editor::init(cx);
- LanguageModelRegistry::test(cx);
- cx.set_global(settings_store);
-
- theme_settings::init(theme::LoadThemes::JustBase, cx);
- }
-
- #[gpui::test]
- async fn test_quote_terminal_text(cx: &mut TestAppContext) {
- let (_context, text_thread_editor, mut cx) =
- setup_text_thread_editor_text(vec![(Role::User, "")], cx).await;
-
- let terminal_output = "$ ls -la\ntotal 0\ndrwxr-xr-x 2 user user 40 Jan 1 00:00 .";
-
- text_thread_editor.update_in(&mut cx, |text_thread_editor, window, cx| {
- text_thread_editor.quote_terminal_text(terminal_output.to_string(), window, cx);
-
- text_thread_editor.editor.update(cx, |editor, cx| {
- let text = editor.text(cx);
- // The text should contain the terminal output wrapped in a code block
- assert!(
- text.contains(&format!("```console\n{}\n```", terminal_output)),
- "Terminal text should be wrapped in code block. Got: {}",
- text
- );
- });
- });
- }
-}
@@ -1,736 +0,0 @@
-use crate::{RemoveHistory, RemoveSelectedThread};
-use assistant_text_thread::{SavedTextThreadMetadata, TextThreadStore};
-use chrono::{Datelike, Local, NaiveDate, TimeDelta, Utc};
-use editor::{Editor, EditorEvent};
-use fuzzy::StringMatchCandidate;
-use gpui::{
- App, Entity, EventEmitter, FocusHandle, Focusable, Task, UniformListScrollHandle, Window,
- uniform_list,
-};
-use std::{fmt::Display, ops::Range};
-use text::Bias;
-use time::{OffsetDateTime, UtcOffset};
-use ui::{
- HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Tab, Tooltip, WithScrollbar,
- prelude::*,
-};
-
-const DEFAULT_TITLE: &SharedString = &SharedString::new_static("New Thread");
-
-fn thread_title(entry: &SavedTextThreadMetadata) -> &SharedString {
- if entry.title.is_empty() {
- DEFAULT_TITLE
- } else {
- &entry.title
- }
-}
-
-pub struct TextThreadHistory {
- pub(crate) text_thread_store: Entity<TextThreadStore>,
- scroll_handle: UniformListScrollHandle,
- selected_index: usize,
- hovered_index: Option<usize>,
- search_editor: Entity<Editor>,
- search_query: SharedString,
- visible_items: Vec<ListItemType>,
- local_timezone: UtcOffset,
- confirming_delete_history: bool,
- _update_task: Task<()>,
- _subscriptions: Vec<gpui::Subscription>,
-}
-
-enum ListItemType {
- BucketSeparator(TimeBucket),
- Entry {
- entry: SavedTextThreadMetadata,
- format: EntryTimeFormat,
- },
- SearchResult {
- entry: SavedTextThreadMetadata,
- positions: Vec<usize>,
- },
-}
-
-impl ListItemType {
- fn history_entry(&self) -> Option<&SavedTextThreadMetadata> {
- match self {
- ListItemType::Entry { entry, .. } => Some(entry),
- ListItemType::SearchResult { entry, .. } => Some(entry),
- _ => None,
- }
- }
-}
-
-pub enum TextThreadHistoryEvent {
- Open(SavedTextThreadMetadata),
-}
-
-impl EventEmitter<TextThreadHistoryEvent> for TextThreadHistory {}
-
-impl TextThreadHistory {
- pub(crate) fn new(
- text_thread_store: Entity<TextThreadStore>,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) -> Self {
- let search_editor = cx.new(|cx| {
- let mut editor = Editor::single_line(window, cx);
- editor.set_placeholder_text("Search threads...", window, cx);
- editor
- });
-
- let search_editor_subscription =
- cx.subscribe(&search_editor, |this, search_editor, event, cx| {
- if let EditorEvent::BufferEdited = event {
- let query = search_editor.read(cx).text(cx);
- if this.search_query != query {
- this.search_query = query.into();
- this.update_visible_items(false, cx);
- }
- }
- });
-
- let store_subscription = cx.observe(&text_thread_store, |this, _, cx| {
- this.update_visible_items(true, cx);
- });
-
- let scroll_handle = UniformListScrollHandle::default();
-
- let mut this = Self {
- text_thread_store,
- scroll_handle,
- selected_index: 0,
- hovered_index: None,
- visible_items: Default::default(),
- search_editor,
- local_timezone: UtcOffset::from_whole_seconds(
- chrono::Local::now().offset().local_minus_utc(),
- )
- .unwrap(),
- search_query: SharedString::default(),
- confirming_delete_history: false,
- _subscriptions: vec![search_editor_subscription, store_subscription],
- _update_task: Task::ready(()),
- };
- this.update_visible_items(false, cx);
- this
- }
-
- pub fn is_empty(&self) -> bool {
- self.visible_items.is_empty()
- }
-
- fn update_visible_items(&mut self, preserve_selected_item: bool, cx: &mut Context<Self>) {
- let entries = self.text_thread_store.update(cx, |store, _| {
- store.ordered_text_threads().cloned().collect::<Vec<_>>()
- });
-
- let new_list_items = if self.search_query.is_empty() {
- self.add_list_separators(entries, cx)
- } else {
- self.filter_search_results(entries, cx)
- };
- let selected_history_entry = if preserve_selected_item {
- self.selected_history_entry().cloned()
- } else {
- None
- };
-
- self._update_task = cx.spawn(async move |this, cx| {
- let new_visible_items = new_list_items.await;
- this.update(cx, |this, cx| {
- let new_selected_index = if let Some(history_entry) = selected_history_entry {
- new_visible_items
- .iter()
- .position(|visible_entry| {
- visible_entry
- .history_entry()
- .is_some_and(|entry| entry.path == history_entry.path)
- })
- .unwrap_or(0)
- } else {
- 0
- };
-
- this.visible_items = new_visible_items;
- this.set_selected_index(new_selected_index, Bias::Right, cx);
- cx.notify();
- })
- .ok();
- });
- }
-
- fn add_list_separators(
- &self,
- entries: Vec<SavedTextThreadMetadata>,
- cx: &App,
- ) -> Task<Vec<ListItemType>> {
- cx.background_spawn(async move {
- let mut items = Vec::with_capacity(entries.len() + 1);
- let mut bucket = None;
- let today = Local::now().naive_local().date();
-
- for entry in entries.into_iter() {
- let entry_date = entry.mtime.naive_local().date();
- let entry_bucket = TimeBucket::from_dates(today, entry_date);
-
- if Some(entry_bucket) != bucket {
- bucket = Some(entry_bucket);
- items.push(ListItemType::BucketSeparator(entry_bucket));
- }
-
- items.push(ListItemType::Entry {
- entry,
- format: entry_bucket.into(),
- });
- }
- items
- })
- }
-
- fn filter_search_results(
- &self,
- entries: Vec<SavedTextThreadMetadata>,
- cx: &App,
- ) -> Task<Vec<ListItemType>> {
- let query = self.search_query.clone();
- cx.background_spawn({
- let executor = cx.background_executor().clone();
- async move {
- let mut candidates = Vec::with_capacity(entries.len());
-
- for (idx, entry) in entries.iter().enumerate() {
- candidates.push(StringMatchCandidate::new(idx, thread_title(entry)));
- }
-
- const MAX_MATCHES: usize = 100;
-
- let matches = fuzzy::match_strings(
- &candidates,
- &query,
- false,
- true,
- MAX_MATCHES,
- &Default::default(),
- executor,
- )
- .await;
-
- matches
- .into_iter()
- .map(|search_match| ListItemType::SearchResult {
- entry: entries[search_match.candidate_id].clone(),
- positions: search_match.positions,
- })
- .collect()
- }
- })
- }
-
- fn search_produced_no_matches(&self) -> bool {
- self.visible_items.is_empty() && !self.search_query.is_empty()
- }
-
- fn selected_history_entry(&self) -> Option<&SavedTextThreadMetadata> {
- self.get_history_entry(self.selected_index)
- }
-
- fn get_history_entry(&self, visible_items_ix: usize) -> Option<&SavedTextThreadMetadata> {
- self.visible_items.get(visible_items_ix)?.history_entry()
- }
-
- fn set_selected_index(&mut self, mut index: usize, bias: Bias, cx: &mut Context<Self>) {
- if self.visible_items.is_empty() {
- self.selected_index = 0;
- return;
- }
- while matches!(
- self.visible_items.get(index),
- None | Some(ListItemType::BucketSeparator(..))
- ) {
- index = match bias {
- Bias::Left => {
- if index == 0 {
- self.visible_items.len() - 1
- } else {
- index - 1
- }
- }
- Bias::Right => {
- if index == self.visible_items.len() - 1 {
- 0
- } else {
- index + 1
- }
- }
- };
- }
- self.selected_index = index;
- cx.notify();
- }
-
- fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
- if self.selected_index == self.visible_items.len() - 1 {
- self.set_selected_index(0, Bias::Right, cx);
- } else {
- self.set_selected_index(self.selected_index + 1, Bias::Right, cx);
- }
- }
-
- fn select_previous(
- &mut self,
- _: &menu::SelectPrevious,
- _window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- if self.selected_index == 0 {
- self.set_selected_index(self.visible_items.len() - 1, Bias::Left, cx);
- } else {
- self.set_selected_index(self.selected_index - 1, Bias::Left, cx);
- }
- }
-
- fn select_first(
- &mut self,
- _: &menu::SelectFirst,
- _window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- self.set_selected_index(0, Bias::Right, cx);
- }
-
- fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
- self.set_selected_index(self.visible_items.len() - 1, Bias::Left, cx);
- }
-
- fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
- self.confirm_entry(self.selected_index, cx);
- }
-
- fn confirm_entry(&mut self, ix: usize, cx: &mut Context<Self>) {
- let Some(entry) = self.get_history_entry(ix) else {
- return;
- };
- cx.emit(TextThreadHistoryEvent::Open(entry.clone()));
- }
-
- fn remove_selected_thread(
- &mut self,
- _: &RemoveSelectedThread,
- _window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- self.remove_thread(self.selected_index, cx)
- }
-
- fn remove_thread(&mut self, visible_item_ix: usize, cx: &mut Context<Self>) {
- let Some(entry) = self.get_history_entry(visible_item_ix) else {
- return;
- };
-
- let task = self
- .text_thread_store
- .update(cx, |store, cx| store.delete_local(entry.path.clone(), cx));
- task.detach_and_log_err(cx);
- }
-
- fn remove_history(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
- self.text_thread_store.update(cx, |store, cx| {
- store.delete_all_local(cx).detach_and_log_err(cx)
- });
- self.confirming_delete_history = false;
- cx.notify();
- }
-
- fn prompt_delete_history(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
- self.confirming_delete_history = true;
- cx.notify();
- }
-
- fn cancel_delete_history(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
- self.confirming_delete_history = false;
- cx.notify();
- }
-
- fn render_list_items(
- &mut self,
- range: Range<usize>,
- _window: &mut Window,
- cx: &mut Context<Self>,
- ) -> Vec<AnyElement> {
- self.visible_items
- .get(range.clone())
- .into_iter()
- .flatten()
- .enumerate()
- .map(|(ix, item)| self.render_list_item(item, range.start + ix, cx))
- .collect()
- }
-
- fn render_list_item(&self, item: &ListItemType, ix: usize, cx: &Context<Self>) -> AnyElement {
- match item {
- ListItemType::Entry { entry, format } => self
- .render_history_entry(entry, *format, ix, Vec::default(), cx)
- .into_any(),
- ListItemType::SearchResult { entry, positions } => self.render_history_entry(
- entry,
- EntryTimeFormat::DateAndTime,
- ix,
- positions.clone(),
- cx,
- ),
- ListItemType::BucketSeparator(bucket) => div()
- .px(DynamicSpacing::Base06.rems(cx))
- .pt_2()
- .pb_1()
- .child(
- Label::new(bucket.to_string())
- .size(LabelSize::XSmall)
- .color(Color::Muted),
- )
- .into_any_element(),
- }
- }
-
- fn render_history_entry(
- &self,
- entry: &SavedTextThreadMetadata,
- format: EntryTimeFormat,
- ix: usize,
- highlight_positions: Vec<usize>,
- cx: &Context<Self>,
- ) -> AnyElement {
- let selected = ix == self.selected_index;
- let hovered = Some(ix) == self.hovered_index;
- let entry_time = entry.mtime.with_timezone(&Utc);
- let timestamp = entry_time.timestamp();
-
- let display_text = match format {
- EntryTimeFormat::DateAndTime => {
- let now = Utc::now();
- let duration = now.signed_duration_since(entry_time);
- let days = duration.num_days();
-
- format!("{}d", days)
- }
- EntryTimeFormat::TimeOnly => format.format_timestamp(timestamp, self.local_timezone),
- };
-
- let title = thread_title(entry).clone();
- let full_date =
- EntryTimeFormat::DateAndTime.format_timestamp(timestamp, self.local_timezone);
-
- h_flex()
- .w_full()
- .pb_1()
- .child(
- ListItem::new(ix)
- .rounded()
- .toggle_state(selected)
- .spacing(ListItemSpacing::Sparse)
- .start_slot(
- h_flex()
- .w_full()
- .gap_2()
- .justify_between()
- .child(
- HighlightedLabel::new(thread_title(entry), highlight_positions)
- .size(LabelSize::Small)
- .truncate(),
- )
- .child(
- Label::new(display_text)
- .color(Color::Muted)
- .size(LabelSize::XSmall),
- ),
- )
- .tooltip(move |_, cx| {
- Tooltip::with_meta(title.clone(), None, full_date.clone(), cx)
- })
- .on_hover(cx.listener(move |this, is_hovered, _window, cx| {
- if *is_hovered {
- this.hovered_index = Some(ix);
- } else if this.hovered_index == Some(ix) {
- this.hovered_index = None;
- }
- cx.notify();
- }))
- .end_slot::<IconButton>(if hovered {
- Some(
- IconButton::new("delete", IconName::Trash)
- .shape(IconButtonShape::Square)
- .icon_size(IconSize::XSmall)
- .icon_color(Color::Muted)
- .tooltip(move |_window, cx| {
- Tooltip::for_action("Delete", &RemoveSelectedThread, cx)
- })
- .on_click(cx.listener(move |this, _, _window, cx| {
- this.remove_thread(ix, cx);
- cx.stop_propagation()
- })),
- )
- } else {
- None
- })
- .on_click(cx.listener(move |this, _, _window, cx| {
- this.confirm_entry(ix, cx);
- })),
- )
- .into_any_element()
- }
-}
-
-impl Render for TextThreadHistory {
- fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
- let has_no_history = !self.text_thread_store.read(cx).has_saved_text_threads();
-
- v_flex()
- .size_full()
- .key_context("ThreadHistory")
- .bg(cx.theme().colors().panel_background)
- .on_action(cx.listener(Self::select_previous))
- .on_action(cx.listener(Self::select_next))
- .on_action(cx.listener(Self::select_first))
- .on_action(cx.listener(Self::select_last))
- .on_action(cx.listener(Self::confirm))
- .on_action(cx.listener(|this, _: &RemoveSelectedThread, window, cx| {
- this.remove_selected_thread(&RemoveSelectedThread, window, cx);
- }))
- .on_action(cx.listener(|this, _: &RemoveHistory, window, cx| {
- this.remove_history(window, cx);
- }))
- .child(
- h_flex()
- .h(Tab::container_height(cx))
- .w_full()
- .py_1()
- .px_2()
- .gap_2()
- .justify_between()
- .border_b_1()
- .border_color(cx.theme().colors().border)
- .child(
- Icon::new(IconName::MagnifyingGlass)
- .color(Color::Muted)
- .size(IconSize::Small),
- )
- .child(self.search_editor.clone()),
- )
- .child({
- let view = v_flex()
- .id("list-container")
- .relative()
- .overflow_hidden()
- .flex_grow();
-
- if has_no_history {
- view.justify_center().items_center().child(
- Label::new("You don't have any past text threads yet.")
- .size(LabelSize::Small)
- .color(Color::Muted),
- )
- } else if self.search_produced_no_matches() {
- view.justify_center()
- .items_center()
- .child(Label::new("No threads match your search.").size(LabelSize::Small))
- } else {
- view.child(
- uniform_list(
- "text-thread-history",
- self.visible_items.len(),
- cx.processor(|this, range: Range<usize>, window, cx| {
- this.render_list_items(range, window, cx)
- }),
- )
- .p_1()
- .pr_4()
- .track_scroll(&self.scroll_handle)
- .flex_grow(),
- )
- .vertical_scrollbar_for(&self.scroll_handle, window, cx)
- }
- })
- .when(!has_no_history, |this| {
- this.child(
- h_flex()
- .p_2()
- .border_t_1()
- .border_color(cx.theme().colors().border_variant)
- .when(!self.confirming_delete_history, |this| {
- this.child(
- Button::new("delete_history", "Delete All History")
- .full_width()
- .style(ButtonStyle::Outlined)
- .label_size(LabelSize::Small)
- .on_click(cx.listener(|this, _, window, cx| {
- this.prompt_delete_history(window, cx);
- })),
- )
- })
- .when(self.confirming_delete_history, |this| {
- this.w_full()
- .gap_2()
- .flex_wrap()
- .justify_between()
- .child(
- h_flex()
- .flex_wrap()
- .gap_1()
- .child(
- Label::new("Delete all text threads?")
- .size(LabelSize::Small),
- )
- .child(
- Label::new("You won't be able to recover them later.")
- .size(LabelSize::Small)
- .color(Color::Muted),
- ),
- )
- .child(
- h_flex()
- .gap_1()
- .child(
- Button::new("cancel_delete", "Cancel")
- .label_size(LabelSize::Small)
- .on_click(cx.listener(|this, _, window, cx| {
- this.cancel_delete_history(window, cx);
- })),
- )
- .child(
- Button::new("confirm_delete", "Delete")
- .style(ButtonStyle::Tinted(ui::TintColor::Error))
- .color(Color::Error)
- .label_size(LabelSize::Small)
- .on_click(cx.listener(|_, _, window, cx| {
- window.dispatch_action(
- Box::new(RemoveHistory),
- cx,
- );
- })),
- ),
- )
- }),
- )
- })
- }
-}
-
-impl Focusable for TextThreadHistory {
- fn focus_handle(&self, cx: &App) -> FocusHandle {
- self.search_editor.focus_handle(cx)
- }
-}
-
-#[derive(Clone, Copy)]
-pub enum EntryTimeFormat {
- DateAndTime,
- TimeOnly,
-}
-
-impl EntryTimeFormat {
- fn format_timestamp(self, timestamp: i64, timezone: UtcOffset) -> String {
- let datetime = OffsetDateTime::from_unix_timestamp(timestamp)
- .unwrap_or_else(|_| OffsetDateTime::now_utc())
- .to_offset(timezone);
-
- match self {
- EntryTimeFormat::DateAndTime => datetime.format(&time::macros::format_description!(
- "[month repr:short] [day], [year]"
- )),
- EntryTimeFormat::TimeOnly => {
- datetime.format(&time::macros::format_description!("[hour]:[minute]"))
- }
- }
- .unwrap_or_default()
- }
-}
-
-impl From<TimeBucket> for EntryTimeFormat {
- fn from(bucket: TimeBucket) -> Self {
- match bucket {
- TimeBucket::Today => EntryTimeFormat::TimeOnly,
- TimeBucket::Yesterday => EntryTimeFormat::TimeOnly,
- TimeBucket::ThisWeek => EntryTimeFormat::DateAndTime,
- TimeBucket::PastWeek => EntryTimeFormat::DateAndTime,
- TimeBucket::All => EntryTimeFormat::DateAndTime,
- }
- }
-}
-
-#[derive(PartialEq, Eq, Clone, Copy, Debug)]
-enum TimeBucket {
- Today,
- Yesterday,
- ThisWeek,
- PastWeek,
- All,
-}
-
-impl TimeBucket {
- fn from_dates(reference: NaiveDate, date: NaiveDate) -> Self {
- if date == reference {
- return TimeBucket::Today;
- }
-
- if date == reference - TimeDelta::days(1) {
- return TimeBucket::Yesterday;
- }
-
- let week = date.iso_week();
-
- if reference.iso_week() == week {
- return TimeBucket::ThisWeek;
- }
-
- let last_week = (reference - TimeDelta::days(7)).iso_week();
-
- if week == last_week {
- return TimeBucket::PastWeek;
- }
-
- TimeBucket::All
- }
-}
-
-impl Display for TimeBucket {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- TimeBucket::Today => write!(f, "Today"),
- TimeBucket::Yesterday => write!(f, "Yesterday"),
- TimeBucket::ThisWeek => write!(f, "This Week"),
- TimeBucket::PastWeek => write!(f, "Past Week"),
- TimeBucket::All => write!(f, "All"),
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_time_bucket_from_dates() {
- let today = NaiveDate::from_ymd_opt(2023, 1, 15).unwrap();
-
- let date = today;
- assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::Today);
-
- let date = NaiveDate::from_ymd_opt(2023, 1, 14).unwrap();
- assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::Yesterday);
-
- let date = NaiveDate::from_ymd_opt(2023, 1, 13).unwrap();
- assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::ThisWeek);
-
- let date = NaiveDate::from_ymd_opt(2023, 1, 11).unwrap();
- assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::ThisWeek);
-
- let date = NaiveDate::from_ymd_opt(2023, 1, 8).unwrap();
- assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::PastWeek);
-
- let date = NaiveDate::from_ymd_opt(2023, 1, 5).unwrap();
- assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::PastWeek);
-
- let date = NaiveDate::from_ymd_opt(2023, 1, 1).unwrap();
- assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::All);
- }
-}
@@ -6,7 +6,7 @@ use gpui::{
use ui::{TintColor, Vector, VectorName, prelude::*};
use workspace::{ModalView, Workspace};
-use crate::agent_panel::{AgentPanel, AgentType};
+use crate::{Agent, agent_panel::AgentPanel};
macro_rules! acp_onboarding_event {
($name:expr) => {
@@ -38,7 +38,7 @@ impl AcpOnboardingModal {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
panel.update(cx, |panel, cx| {
panel.new_agent_thread(
- AgentType::Custom {
+ Agent::Custom {
id: GEMINI_ID.into(),
},
window,
@@ -6,7 +6,7 @@ use gpui::{
use ui::{TintColor, Vector, VectorName, prelude::*};
use workspace::{ModalView, Workspace};
-use crate::agent_panel::{AgentPanel, AgentType};
+use crate::{Agent, agent_panel::AgentPanel};
macro_rules! claude_agent_onboarding_event {
($name:expr) => {
@@ -38,7 +38,7 @@ impl ClaudeCodeOnboardingModal {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
panel.update(cx, |panel, cx| {
panel.new_agent_thread(
- AgentType::Custom {
+ Agent::Custom {
id: CLAUDE_AGENT_ID.into(),
},
window,
@@ -178,7 +178,6 @@ fn open_mention_uri(
MentionUri::Thread { id, name } => {
open_thread(workspace, id, name, window, cx);
}
- MentionUri::TextThread { .. } => {}
MentionUri::Rule { id, .. } => {
open_rule(workspace, id, window, cx);
}
@@ -1,34 +0,0 @@
-[package]
-name = "assistant_slash_command"
-version = "0.1.0"
-edition.workspace = true
-publish.workspace = true
-license = "GPL-3.0-or-later"
-
-[lints]
-workspace = true
-
-[lib]
-path = "src/assistant_slash_command.rs"
-
-[dependencies]
-anyhow.workspace = true
-async-trait.workspace = true
-collections.workspace = true
-derive_more.workspace = true
-extension.workspace = true
-futures.workspace = true
-gpui.workspace = true
-language.workspace = true
-language_model.workspace = true
-parking_lot.workspace = true
-serde.workspace = true
-serde_json.workspace = true
-ui.workspace = true
-util.workspace = true
-workspace.workspace = true
-
-[dev-dependencies]
-gpui = { workspace = true, features = ["test-support"] }
-pretty_assertions.workspace = true
-workspace = { workspace = true, features = ["test-support"] }
@@ -1 +0,0 @@
-../../LICENSE-GPL
@@ -1,617 +0,0 @@
-mod extension_slash_command;
-mod slash_command_registry;
-mod slash_command_working_set;
-
-pub use crate::extension_slash_command::*;
-pub use crate::slash_command_registry::*;
-pub use crate::slash_command_working_set::*;
-use anyhow::Result;
-use futures::StreamExt;
-use futures::stream::{self, BoxStream};
-use gpui::{App, SharedString, Task, WeakEntity, Window};
-use language::CodeLabelBuilder;
-use language::HighlightId;
-use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate, OffsetRangeExt};
-pub use language_model::Role;
-use serde::{Deserialize, Deserializer, Serialize};
-use std::{
- ops::Range,
- sync::{Arc, atomic::AtomicBool},
-};
-use ui::ActiveTheme;
-use workspace::{Workspace, ui::IconName};
-
-/// Deserializes IconName, falling back to Code for unknown variants.
-/// This handles old saved data that may contain removed or renamed icon variants.
-fn deserialize_icon_with_fallback<'de, D>(deserializer: D) -> Result<IconName, D::Error>
-where
- D: Deserializer<'de>,
-{
- Ok(String::deserialize(deserializer)
- .ok()
- .and_then(|string| serde_json::from_value(serde_json::Value::String(string)).ok())
- .unwrap_or(IconName::Code))
-}
-
-pub fn init(cx: &mut App) {
- SlashCommandRegistry::default_global(cx);
- extension_slash_command::init(cx);
-}
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub enum AfterCompletion {
- /// Run the command
- Run,
- /// Continue composing the current argument, doesn't add a space
- Compose,
- /// Continue the command composition, adds a space
- Continue,
-}
-
-impl From<bool> for AfterCompletion {
- fn from(value: bool) -> Self {
- if value {
- AfterCompletion::Run
- } else {
- AfterCompletion::Continue
- }
- }
-}
-
-impl AfterCompletion {
- pub fn run(&self) -> bool {
- match self {
- AfterCompletion::Run => true,
- AfterCompletion::Compose | AfterCompletion::Continue => false,
- }
- }
-}
-
-#[derive(Debug)]
-pub struct ArgumentCompletion {
- /// The label to display for this completion.
- pub label: CodeLabel,
- /// The new text that should be inserted into the command when this completion is accepted.
- pub new_text: String,
- /// Whether the command should be run when accepting this completion.
- pub after_completion: AfterCompletion,
- /// Whether to replace the all arguments, or whether to treat this as an independent argument.
- pub replace_previous_arguments: bool,
-}
-
-pub type SlashCommandResult = Result<BoxStream<'static, Result<SlashCommandEvent>>>;
-
-pub trait SlashCommand: 'static + Send + Sync {
- fn name(&self) -> String;
- fn icon(&self) -> IconName {
- IconName::Slash
- }
- fn label(&self, _cx: &App) -> CodeLabel {
- CodeLabel::plain(self.name(), None)
- }
- fn description(&self) -> String;
- fn menu_text(&self) -> String;
- fn complete_argument(
- self: Arc<Self>,
- arguments: &[String],
- cancel: Arc<AtomicBool>,
- workspace: Option<WeakEntity<Workspace>>,
- window: &mut Window,
- cx: &mut App,
- ) -> Task<Result<Vec<ArgumentCompletion>>>;
- fn requires_argument(&self) -> bool;
- fn accepts_arguments(&self) -> bool {
- self.requires_argument()
- }
- fn run(
- self: Arc<Self>,
- arguments: &[String],
- context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
- context_buffer: BufferSnapshot,
- workspace: WeakEntity<Workspace>,
- // TODO: We're just using the `LspAdapterDelegate` here because that is
- // what the extension API is already expecting.
- //
- // It may be that `LspAdapterDelegate` needs a more general name, or
- // perhaps another kind of delegate is needed here.
- delegate: Option<Arc<dyn LspAdapterDelegate>>,
- window: &mut Window,
- cx: &mut App,
- ) -> Task<SlashCommandResult>;
-}
-
-#[derive(Debug, PartialEq)]
-pub enum SlashCommandContent {
- Text {
- text: String,
- run_commands_in_text: bool,
- },
-}
-
-impl<'a> From<&'a str> for SlashCommandContent {
- fn from(text: &'a str) -> Self {
- Self::Text {
- text: text.into(),
- run_commands_in_text: false,
- }
- }
-}
-
-#[derive(Debug, PartialEq)]
-pub enum SlashCommandEvent {
- StartMessage {
- role: Role,
- merge_same_roles: bool,
- },
- StartSection {
- icon: IconName,
- label: SharedString,
- metadata: Option<serde_json::Value>,
- },
- Content(SlashCommandContent),
- EndSection,
-}
-
-#[derive(Debug, Default, PartialEq, Clone)]
-pub struct SlashCommandOutput {
- pub text: String,
- pub sections: Vec<SlashCommandOutputSection<usize>>,
- pub run_commands_in_text: bool,
-}
-
-impl SlashCommandOutput {
- pub fn ensure_valid_section_ranges(&mut self) {
- for section in &mut self.sections {
- section.range.start = section.range.start.min(self.text.len());
- section.range.end = section.range.end.min(self.text.len());
- while !self.text.is_char_boundary(section.range.start) {
- section.range.start -= 1;
- }
- while !self.text.is_char_boundary(section.range.end) {
- section.range.end += 1;
- }
- }
- }
-
- /// Returns this [`SlashCommandOutput`] as a stream of [`SlashCommandEvent`]s.
- pub fn into_event_stream(mut self) -> BoxStream<'static, Result<SlashCommandEvent>> {
- self.ensure_valid_section_ranges();
-
- let mut events = Vec::new();
-
- let mut section_endpoints = Vec::new();
- for section in self.sections {
- section_endpoints.push((
- section.range.start,
- SlashCommandEvent::StartSection {
- icon: section.icon,
- label: section.label,
- metadata: section.metadata,
- },
- ));
- section_endpoints.push((section.range.end, SlashCommandEvent::EndSection));
- }
- section_endpoints.sort_by_key(|(offset, _)| *offset);
-
- let mut content_offset = 0;
- for (endpoint_offset, endpoint) in section_endpoints {
- if content_offset < endpoint_offset {
- events.push(Ok(SlashCommandEvent::Content(SlashCommandContent::Text {
- text: self.text[content_offset..endpoint_offset].to_string(),
- run_commands_in_text: self.run_commands_in_text,
- })));
- content_offset = endpoint_offset;
- }
-
- events.push(Ok(endpoint));
- }
-
- if content_offset < self.text.len() {
- events.push(Ok(SlashCommandEvent::Content(SlashCommandContent::Text {
- text: self.text[content_offset..].to_string(),
- run_commands_in_text: self.run_commands_in_text,
- })));
- }
-
- stream::iter(events).boxed()
- }
-
- pub async fn from_event_stream(
- mut events: BoxStream<'static, Result<SlashCommandEvent>>,
- ) -> Result<SlashCommandOutput> {
- let mut output = SlashCommandOutput::default();
- let mut section_stack = Vec::new();
-
- while let Some(event) = events.next().await {
- match event? {
- SlashCommandEvent::StartSection {
- icon,
- label,
- metadata,
- } => {
- let start = output.text.len();
- section_stack.push(SlashCommandOutputSection {
- range: start..start,
- icon,
- label,
- metadata,
- });
- }
- SlashCommandEvent::Content(SlashCommandContent::Text {
- text,
- run_commands_in_text,
- }) => {
- output.text.push_str(&text);
- output.run_commands_in_text = run_commands_in_text;
-
- if let Some(section) = section_stack.last_mut() {
- section.range.end = output.text.len();
- }
- }
- SlashCommandEvent::EndSection => {
- if let Some(section) = section_stack.pop() {
- output.sections.push(section);
- }
- }
- SlashCommandEvent::StartMessage { .. } => {}
- }
- }
-
- while let Some(section) = section_stack.pop() {
- output.sections.push(section);
- }
-
- Ok(output)
- }
-}
-
-#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
-pub struct SlashCommandOutputSection<T> {
- pub range: Range<T>,
- #[serde(deserialize_with = "deserialize_icon_with_fallback")]
- pub icon: IconName,
- pub label: SharedString,
- pub metadata: Option<serde_json::Value>,
-}
-
-impl SlashCommandOutputSection<language::Anchor> {
- pub fn is_valid(&self, buffer: &language::TextBuffer) -> bool {
- self.range.start.is_valid(buffer) && !self.range.to_offset(buffer).is_empty()
- }
-}
-
-pub struct SlashCommandLine {
- /// The range within the line containing the command name.
- pub name: Range<usize>,
- /// Ranges within the line containing the command arguments.
- pub arguments: Vec<Range<usize>>,
-}
-
-impl SlashCommandLine {
- pub fn parse(line: &str) -> Option<Self> {
- let mut call: Option<Self> = None;
- let mut ix = 0;
- for c in line.chars() {
- let next_ix = ix + c.len_utf8();
- if let Some(call) = &mut call {
- // The command arguments start at the first non-whitespace character
- // after the command name, and continue until the end of the line.
- if let Some(argument) = call.arguments.last_mut() {
- if c.is_whitespace() {
- if (*argument).is_empty() {
- argument.start = next_ix;
- argument.end = next_ix;
- } else {
- argument.end = ix;
- call.arguments.push(next_ix..next_ix);
- }
- } else {
- argument.end = next_ix;
- }
- }
- // The command name ends at the first whitespace character.
- else if !call.name.is_empty() {
- if c.is_whitespace() {
- call.arguments = vec![next_ix..next_ix];
- } else {
- call.name.end = next_ix;
- }
- }
- // The command name must begin with a letter.
- else if c.is_alphabetic() {
- call.name.end = next_ix;
- } else {
- return None;
- }
- }
- // Commands start with a slash.
- else if c == '/' {
- call = Some(SlashCommandLine {
- name: next_ix..next_ix,
- arguments: Vec::new(),
- });
- }
- // The line can't contain anything before the slash except for whitespace.
- else if !c.is_whitespace() {
- return None;
- }
- ix = next_ix;
- }
- call
- }
-}
-
-pub fn create_label_for_command(command_name: &str, arguments: &[&str], cx: &App) -> CodeLabel {
- let mut label = CodeLabelBuilder::default();
- label.push_str(command_name, None);
- label.respan_filter_range(None);
- label.push_str(" ", None);
- label.push_str(
- &arguments.join(" "),
- cx.theme().syntax().highlight_id("comment").map(HighlightId),
- );
- label.build()
-}
-
-#[cfg(test)]
-mod tests {
- use pretty_assertions::assert_eq;
- use serde_json::json;
-
- use super::*;
-
- #[gpui::test]
- async fn test_slash_command_output_to_events_round_trip() {
- // Test basic output consisting of a single section.
- {
- let text = "Hello, world!".to_string();
- let range = 0..text.len();
- let output = SlashCommandOutput {
- text,
- sections: vec![SlashCommandOutputSection {
- range,
- icon: IconName::Code,
- label: "Section 1".into(),
- metadata: None,
- }],
- run_commands_in_text: false,
- };
-
- let events = output.clone().into_event_stream().collect::<Vec<_>>().await;
- let events = events
- .into_iter()
- .filter_map(|event| event.ok())
- .collect::<Vec<_>>();
-
- assert_eq!(
- events,
- vec![
- SlashCommandEvent::StartSection {
- icon: IconName::Code,
- label: "Section 1".into(),
- metadata: None
- },
- SlashCommandEvent::Content(SlashCommandContent::Text {
- text: "Hello, world!".into(),
- run_commands_in_text: false
- }),
- SlashCommandEvent::EndSection
- ]
- );
-
- let new_output =
- SlashCommandOutput::from_event_stream(output.clone().into_event_stream())
- .await
- .unwrap();
-
- assert_eq!(new_output, output);
- }
-
- // Test output where the sections do not comprise all of the text.
- {
- let text = "Apple\nCucumber\nBanana\n".to_string();
- let output = SlashCommandOutput {
- text,
- sections: vec![
- SlashCommandOutputSection {
- range: 0..6,
- icon: IconName::Check,
- label: "Fruit".into(),
- metadata: None,
- },
- SlashCommandOutputSection {
- range: 15..22,
- icon: IconName::Check,
- label: "Fruit".into(),
- metadata: None,
- },
- ],
- run_commands_in_text: false,
- };
-
- let events = output.clone().into_event_stream().collect::<Vec<_>>().await;
- let events = events
- .into_iter()
- .filter_map(|event| event.ok())
- .collect::<Vec<_>>();
-
- assert_eq!(
- events,
- vec![
- SlashCommandEvent::StartSection {
- icon: IconName::Check,
- label: "Fruit".into(),
- metadata: None
- },
- SlashCommandEvent::Content(SlashCommandContent::Text {
- text: "Apple\n".into(),
- run_commands_in_text: false
- }),
- SlashCommandEvent::EndSection,
- SlashCommandEvent::Content(SlashCommandContent::Text {
- text: "Cucumber\n".into(),
- run_commands_in_text: false
- }),
- SlashCommandEvent::StartSection {
- icon: IconName::Check,
- label: "Fruit".into(),
- metadata: None
- },
- SlashCommandEvent::Content(SlashCommandContent::Text {
- text: "Banana\n".into(),
- run_commands_in_text: false
- }),
- SlashCommandEvent::EndSection
- ]
- );
-
- let new_output =
- SlashCommandOutput::from_event_stream(output.clone().into_event_stream())
- .await
- .unwrap();
-
- assert_eq!(new_output, output);
- }
-
- // Test output consisting of multiple sections.
- {
- let text = "Line 1\nLine 2\nLine 3\nLine 4\n".to_string();
- let output = SlashCommandOutput {
- text,
- sections: vec![
- SlashCommandOutputSection {
- range: 0..6,
- icon: IconName::FileCode,
- label: "Section 1".into(),
- metadata: Some(json!({ "a": true })),
- },
- SlashCommandOutputSection {
- range: 7..13,
- icon: IconName::FileDoc,
- label: "Section 2".into(),
- metadata: Some(json!({ "b": true })),
- },
- SlashCommandOutputSection {
- range: 14..20,
- icon: IconName::FileGit,
- label: "Section 3".into(),
- metadata: Some(json!({ "c": true })),
- },
- SlashCommandOutputSection {
- range: 21..27,
- icon: IconName::FileToml,
- label: "Section 4".into(),
- metadata: Some(json!({ "d": true })),
- },
- ],
- run_commands_in_text: false,
- };
-
- let events = output.clone().into_event_stream().collect::<Vec<_>>().await;
- let events = events
- .into_iter()
- .filter_map(|event| event.ok())
- .collect::<Vec<_>>();
-
- assert_eq!(
- events,
- vec![
- SlashCommandEvent::StartSection {
- icon: IconName::FileCode,
- label: "Section 1".into(),
- metadata: Some(json!({ "a": true }))
- },
- SlashCommandEvent::Content(SlashCommandContent::Text {
- text: "Line 1".into(),
- run_commands_in_text: false
- }),
- SlashCommandEvent::EndSection,
- SlashCommandEvent::Content(SlashCommandContent::Text {
- text: "\n".into(),
- run_commands_in_text: false
- }),
- SlashCommandEvent::StartSection {
- icon: IconName::FileDoc,
- label: "Section 2".into(),
- metadata: Some(json!({ "b": true }))
- },
- SlashCommandEvent::Content(SlashCommandContent::Text {
- text: "Line 2".into(),
- run_commands_in_text: false
- }),
- SlashCommandEvent::EndSection,
- SlashCommandEvent::Content(SlashCommandContent::Text {
- text: "\n".into(),
- run_commands_in_text: false
- }),
- SlashCommandEvent::StartSection {
- icon: IconName::FileGit,
- label: "Section 3".into(),
- metadata: Some(json!({ "c": true }))
- },
- SlashCommandEvent::Content(SlashCommandContent::Text {
- text: "Line 3".into(),
- run_commands_in_text: false
- }),
- SlashCommandEvent::EndSection,
- SlashCommandEvent::Content(SlashCommandContent::Text {
- text: "\n".into(),
- run_commands_in_text: false
- }),
- SlashCommandEvent::StartSection {
- icon: IconName::FileToml,
- label: "Section 4".into(),
- metadata: Some(json!({ "d": true }))
- },
- SlashCommandEvent::Content(SlashCommandContent::Text {
- text: "Line 4".into(),
- run_commands_in_text: false
- }),
- SlashCommandEvent::EndSection,
- SlashCommandEvent::Content(SlashCommandContent::Text {
- text: "\n".into(),
- run_commands_in_text: false
- }),
- ]
- );
-
- let new_output =
- SlashCommandOutput::from_event_stream(output.clone().into_event_stream())
- .await
- .unwrap();
-
- assert_eq!(new_output, output);
- }
- }
-
- #[test]
- fn test_deserialize_with_valid_icon_pascal_case() {
- // Test that PascalCase icons (serde default) deserialize correctly
- let json = json!({
- "range": {
- "start": 0,
- "end": 5
- },
- "icon": "AcpRegistry",
- "label": "Test",
- "metadata": null
- });
- let section: SlashCommandOutputSection<usize> = serde_json::from_value(json).unwrap();
- assert_eq!(section.icon, IconName::AcpRegistry);
- }
- #[test]
- fn test_deserialize_with_unknown_icon() {
- // Test that unknown icon variants fall back to Code
- let json = json!({
- "range": {
- "start": 0,
- "end": 5
- },
- "icon": "removed_icon",
- "label": "Old Icon",
- "metadata": null
- });
- let section: SlashCommandOutputSection<usize> = serde_json::from_value(json).unwrap();
- assert_eq!(section.icon, IconName::Code);
- }
-}
@@ -1,171 +0,0 @@
-use anyhow::Result;
-use async_trait::async_trait;
-use extension::{Extension, ExtensionHostProxy, ExtensionSlashCommandProxy, WorktreeDelegate};
-use gpui::{App, Task, WeakEntity, Window};
-use language::{BufferSnapshot, LspAdapterDelegate};
-use std::sync::{Arc, atomic::AtomicBool};
-use ui::prelude::*;
-use util::rel_path::RelPath;
-use workspace::Workspace;
-
-use crate::{
- ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
- SlashCommandRegistry, SlashCommandResult,
-};
-
-pub fn init(cx: &mut App) {
- let proxy = ExtensionHostProxy::default_global(cx);
- proxy.register_slash_command_proxy(SlashCommandRegistryProxy {
- slash_command_registry: SlashCommandRegistry::global(cx),
- });
-}
-
-struct SlashCommandRegistryProxy {
- slash_command_registry: Arc<SlashCommandRegistry>,
-}
-
-impl ExtensionSlashCommandProxy for SlashCommandRegistryProxy {
- fn register_slash_command(
- &self,
- extension: Arc<dyn Extension>,
- command: extension::SlashCommand,
- ) {
- self.slash_command_registry
- .register_command(ExtensionSlashCommand::new(extension, command), false)
- }
-
- fn unregister_slash_command(&self, command_name: Arc<str>) {
- self.slash_command_registry
- .unregister_command_by_name(&command_name)
- }
-}
-
-/// An adapter that allows an [`LspAdapterDelegate`] to be used as a [`WorktreeDelegate`].
-struct WorktreeDelegateAdapter(Arc<dyn LspAdapterDelegate>);
-
-#[async_trait]
-impl WorktreeDelegate for WorktreeDelegateAdapter {
- fn id(&self) -> u64 {
- self.0.worktree_id().to_proto()
- }
-
- fn root_path(&self) -> String {
- self.0.worktree_root_path().to_string_lossy().into_owned()
- }
-
- async fn read_text_file(&self, path: &RelPath) -> Result<String> {
- self.0.read_text_file(path).await
- }
-
- async fn which(&self, binary_name: String) -> Option<String> {
- self.0
- .which(binary_name.as_ref())
- .await
- .map(|path| path.to_string_lossy().into_owned())
- }
-
- async fn shell_env(&self) -> Vec<(String, String)> {
- self.0.shell_env().await.into_iter().collect()
- }
-}
-
-pub struct ExtensionSlashCommand {
- extension: Arc<dyn Extension>,
- command: extension::SlashCommand,
-}
-
-impl ExtensionSlashCommand {
- pub fn new(extension: Arc<dyn Extension>, command: extension::SlashCommand) -> Self {
- Self { extension, command }
- }
-}
-
-impl SlashCommand for ExtensionSlashCommand {
- fn name(&self) -> String {
- self.command.name.clone()
- }
-
- fn description(&self) -> String {
- self.command.description.clone()
- }
-
- fn menu_text(&self) -> String {
- self.command.tooltip_text.clone()
- }
-
- fn requires_argument(&self) -> bool {
- self.command.requires_argument
- }
-
- fn complete_argument(
- self: Arc<Self>,
- arguments: &[String],
- _cancel: Arc<AtomicBool>,
- _workspace: Option<WeakEntity<Workspace>>,
- _window: &mut Window,
- cx: &mut App,
- ) -> Task<Result<Vec<ArgumentCompletion>>> {
- let command = self.command.clone();
- let arguments = arguments.to_owned();
- cx.background_spawn(async move {
- let completions = self
- .extension
- .complete_slash_command_argument(command, arguments)
- .await?;
-
- anyhow::Ok(
- completions
- .into_iter()
- .map(|completion| ArgumentCompletion {
- label: completion.label.into(),
- new_text: completion.new_text,
- replace_previous_arguments: false,
- after_completion: completion.run_command.into(),
- })
- .collect(),
- )
- })
- }
-
- fn run(
- self: Arc<Self>,
- arguments: &[String],
- _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
- _context_buffer: BufferSnapshot,
- _workspace: WeakEntity<Workspace>,
- delegate: Option<Arc<dyn LspAdapterDelegate>>,
- _window: &mut Window,
- cx: &mut App,
- ) -> Task<SlashCommandResult> {
- let command = self.command.clone();
- let arguments = arguments.to_owned();
- let output = cx.background_spawn(async move {
- let delegate =
- delegate.map(|delegate| Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _);
- let output = self
- .extension
- .run_slash_command(command, arguments, delegate)
- .await?;
-
- anyhow::Ok(output)
- });
- cx.foreground_executor().spawn(async move {
- let output = output.await?;
- Ok(SlashCommandOutput {
- text: output.text,
- sections: output
- .sections
- .into_iter()
- .map(|section| SlashCommandOutputSection {
- range: section.range,
- icon: IconName::Code,
- label: section.label.into(),
- metadata: None,
- })
- .collect(),
- run_commands_in_text: false,
- }
- .into_event_stream())
- })
- }
-}
@@ -1,90 +0,0 @@
-use std::sync::Arc;
-
-use collections::{BTreeSet, HashMap};
-use derive_more::{Deref, DerefMut};
-use gpui::Global;
-use gpui::{App, ReadGlobal};
-use parking_lot::RwLock;
-
-use crate::SlashCommand;
-
-#[derive(Default, Deref, DerefMut)]
-struct GlobalSlashCommandRegistry(Arc<SlashCommandRegistry>);
-
-impl Global for GlobalSlashCommandRegistry {}
-
-#[derive(Default)]
-struct SlashCommandRegistryState {
- commands: HashMap<Arc<str>, Arc<dyn SlashCommand>>,
- featured_commands: BTreeSet<Arc<str>>,
-}
-
-#[derive(Default)]
-pub struct SlashCommandRegistry {
- state: RwLock<SlashCommandRegistryState>,
-}
-
-impl SlashCommandRegistry {
- /// Returns the global [`SlashCommandRegistry`].
- pub fn global(cx: &App) -> Arc<Self> {
- GlobalSlashCommandRegistry::global(cx).0.clone()
- }
-
- /// Returns the global [`SlashCommandRegistry`].
- ///
- /// Inserts a default [`SlashCommandRegistry`] if one does not yet exist.
- pub fn default_global(cx: &mut App) -> Arc<Self> {
- cx.default_global::<GlobalSlashCommandRegistry>().0.clone()
- }
-
- pub fn new() -> Arc<Self> {
- Arc::new(Self {
- state: RwLock::new(SlashCommandRegistryState {
- commands: HashMap::default(),
- featured_commands: BTreeSet::default(),
- }),
- })
- }
-
- /// Registers the provided [`SlashCommand`].
- pub fn register_command(&self, command: impl SlashCommand, is_featured: bool) {
- let mut state = self.state.write();
- let command_name: Arc<str> = command.name().into();
- if is_featured {
- state.featured_commands.insert(command_name.clone());
- }
- state.commands.insert(command_name, Arc::new(command));
- }
-
- /// Unregisters the provided [`SlashCommand`].
- pub fn unregister_command(&self, command: impl SlashCommand) {
- self.unregister_command_by_name(command.name().as_str())
- }
-
- /// Unregisters the command with the given name.
- pub fn unregister_command_by_name(&self, command_name: &str) {
- let mut state = self.state.write();
- state.featured_commands.remove(command_name);
- state.commands.remove(command_name);
- }
-
- /// Returns the names of registered [`SlashCommand`]s.
- pub fn command_names(&self) -> Vec<Arc<str>> {
- self.state.read().commands.keys().cloned().collect()
- }
-
- /// Returns the names of registered, featured [`SlashCommand`]s.
- pub fn featured_command_names(&self) -> Vec<Arc<str>> {
- self.state
- .read()
- .featured_commands
- .iter()
- .cloned()
- .collect()
- }
-
- /// Returns the [`SlashCommand`] with the given name.
- pub fn command(&self, name: &str) -> Option<Arc<dyn SlashCommand>> {
- self.state.read().commands.get(name).cloned()
- }
-}
@@ -1,81 +0,0 @@
-use std::sync::Arc;
-
-use collections::HashMap;
-use gpui::App;
-use parking_lot::Mutex;
-
-use crate::{SlashCommand, SlashCommandRegistry};
-
-#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default)]
-pub struct SlashCommandId(usize);
-
-/// A working set of slash commands for use in one instance of the Assistant Panel.
-#[derive(Default)]
-pub struct SlashCommandWorkingSet {
- state: Mutex<WorkingSetState>,
-}
-
-#[derive(Default)]
-struct WorkingSetState {
- context_server_commands_by_id: HashMap<SlashCommandId, Arc<dyn SlashCommand>>,
- context_server_commands_by_name: HashMap<Arc<str>, Arc<dyn SlashCommand>>,
- next_command_id: SlashCommandId,
-}
-
-impl SlashCommandWorkingSet {
- pub fn command(&self, name: &str, cx: &App) -> Option<Arc<dyn SlashCommand>> {
- self.state
- .lock()
- .context_server_commands_by_name
- .get(name)
- .cloned()
- .or_else(|| SlashCommandRegistry::global(cx).command(name))
- }
-
- pub fn command_names(&self, cx: &App) -> Vec<Arc<str>> {
- let mut command_names = SlashCommandRegistry::global(cx).command_names();
- command_names.extend(
- self.state
- .lock()
- .context_server_commands_by_name
- .keys()
- .cloned(),
- );
-
- command_names
- }
-
- pub fn featured_command_names(&self, cx: &App) -> Vec<Arc<str>> {
- SlashCommandRegistry::global(cx).featured_command_names()
- }
-
- pub fn insert(&self, command: Arc<dyn SlashCommand>) -> SlashCommandId {
- let mut state = self.state.lock();
- let command_id = state.next_command_id;
- state.next_command_id.0 += 1;
- state
- .context_server_commands_by_id
- .insert(command_id, command.clone());
- state.slash_commands_changed();
- command_id
- }
-
- pub fn remove(&self, command_ids_to_remove: &[SlashCommandId]) {
- let mut state = self.state.lock();
- state
- .context_server_commands_by_id
- .retain(|id, _| !command_ids_to_remove.contains(id));
- state.slash_commands_changed();
- }
-}
-
-impl WorkingSetState {
- fn slash_commands_changed(&mut self) {
- self.context_server_commands_by_name.clear();
- self.context_server_commands_by_name.extend(
- self.context_server_commands_by_id
- .values()
- .map(|command| (command.name().into(), command.clone())),
- );
- }
-}
@@ -1,47 +0,0 @@
-[package]
-name = "assistant_slash_commands"
-version = "0.1.0"
-edition.workspace = true
-publish.workspace = true
-license = "GPL-3.0-or-later"
-
-[lints]
-workspace = true
-
-[lib]
-path = "src/assistant_slash_commands.rs"
-
-[dependencies]
-anyhow.workspace = true
-assistant_slash_command.workspace = true
-chrono.workspace = true
-collections.workspace = true
-editor.workspace = true
-feature_flags.workspace = true
-fs.workspace = true
-futures.workspace = true
-fuzzy.workspace = true
-gpui.workspace = true
-html_to_markdown.workspace = true
-http_client.workspace = true
-language.workspace = true
-project.workspace = true
-prompt_store.workspace = true
-rope.workspace = true
-serde.workspace = true
-serde_json.workspace = true
-smol.workspace = true
-text.workspace = true
-ui.workspace = true
-util.workspace = true
-workspace.workspace = true
-worktree.workspace = true
-
-[dev-dependencies]
-fs = { workspace = true, features = ["test-support"] }
-gpui = { workspace = true, features = ["test-support"] }
-multi_buffer = { workspace = true, features = ["test-support"] }
-pretty_assertions.workspace = true
-project = { workspace = true, features = ["test-support"] }
-settings = { workspace = true, features = ["test-support"] }
-zlog.workspace = true
@@ -1 +0,0 @@
-../../LICENSE-GPL
@@ -1,25 +0,0 @@
-mod default_command;
-mod delta_command;
-mod diagnostics_command;
-mod fetch_command;
-mod file_command;
-mod now_command;
-mod prompt_command;
-mod selection_command;
-mod streaming_example_command;
-mod symbols_command;
-mod tab_command;
-
-pub use crate::default_command::*;
-pub use crate::delta_command::*;
-pub use crate::diagnostics_command::*;
-pub use crate::fetch_command::*;
-pub use crate::file_command::*;
-pub use crate::now_command::*;
-pub use crate::prompt_command::*;
-pub use crate::selection_command::*;
-pub use crate::streaming_example_command::*;
-pub use crate::symbols_command::*;
-pub use crate::tab_command::*;
-
-use assistant_slash_command::create_label_for_command;
@@ -1,91 +0,0 @@
-use anyhow::{Result, anyhow};
-use assistant_slash_command::{
- ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
- SlashCommandResult,
-};
-use gpui::{Task, WeakEntity};
-use language::{BufferSnapshot, LspAdapterDelegate};
-use prompt_store::PromptStore;
-use std::{
- fmt::Write,
- sync::{Arc, atomic::AtomicBool},
-};
-use ui::prelude::*;
-use workspace::Workspace;
-
-pub struct DefaultSlashCommand;
-
-impl SlashCommand for DefaultSlashCommand {
- fn name(&self) -> String {
- "default".into()
- }
-
- fn description(&self) -> String {
- "Insert default prompt".into()
- }
-
- fn menu_text(&self) -> String {
- self.description()
- }
-
- fn requires_argument(&self) -> bool {
- false
- }
-
- fn complete_argument(
- self: Arc<Self>,
- _arguments: &[String],
- _cancellation_flag: Arc<AtomicBool>,
- _workspace: Option<WeakEntity<Workspace>>,
- _window: &mut Window,
- _cx: &mut App,
- ) -> Task<Result<Vec<ArgumentCompletion>>> {
- Task::ready(Err(anyhow!("this command does not require argument")))
- }
-
- fn run(
- self: Arc<Self>,
- _arguments: &[String],
- _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
- _context_buffer: BufferSnapshot,
- _workspace: WeakEntity<Workspace>,
- _delegate: Option<Arc<dyn LspAdapterDelegate>>,
- _window: &mut Window,
- cx: &mut App,
- ) -> Task<SlashCommandResult> {
- let store = PromptStore::global(cx);
- cx.spawn(async move |cx| {
- let store = store.await?;
- let prompts = store.read_with(cx, |store, _cx| store.default_prompt_metadata());
-
- let mut text = String::new();
- text.push('\n');
- for prompt in prompts {
- if let Some(title) = prompt.title {
- writeln!(text, "/prompt {}", title).unwrap();
- }
- }
- text.pop();
-
- if text.is_empty() {
- text.push('\n');
- }
-
- if !text.ends_with('\n') {
- text.push('\n');
- }
-
- Ok(SlashCommandOutput {
- sections: vec![SlashCommandOutputSection {
- range: 0..text.len(),
- icon: IconName::Library,
- label: "Default".into(),
- metadata: None,
- }],
- text,
- run_commands_in_text: true,
- }
- .into_event_stream())
- })
- }
-}
@@ -1,124 +0,0 @@
-use crate::file_command::{FileCommandMetadata, FileSlashCommand};
-use anyhow::{Result, anyhow};
-use assistant_slash_command::{
- ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
- SlashCommandResult,
-};
-use collections::HashSet;
-use futures::future;
-use gpui::{App, Task, WeakEntity, Window};
-use language::{BufferSnapshot, LspAdapterDelegate};
-use std::sync::{Arc, atomic::AtomicBool};
-use text::OffsetRangeExt;
-use ui::prelude::*;
-use workspace::Workspace;
-
-pub struct DeltaSlashCommand;
-
-impl SlashCommand for DeltaSlashCommand {
- fn name(&self) -> String {
- "delta".into()
- }
-
- fn description(&self) -> String {
- "Re-insert changed files".into()
- }
-
- fn menu_text(&self) -> String {
- self.description()
- }
-
- fn icon(&self) -> IconName {
- IconName::Diff
- }
-
- fn requires_argument(&self) -> bool {
- false
- }
-
- fn complete_argument(
- self: Arc<Self>,
- _arguments: &[String],
- _cancellation_flag: Arc<AtomicBool>,
- _workspace: Option<WeakEntity<Workspace>>,
- _window: &mut Window,
- _cx: &mut App,
- ) -> Task<Result<Vec<ArgumentCompletion>>> {
- Task::ready(Err(anyhow!("this command does not require argument")))
- }
-
- fn run(
- self: Arc<Self>,
- _arguments: &[String],
- context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
- context_buffer: BufferSnapshot,
- workspace: WeakEntity<Workspace>,
- delegate: Option<Arc<dyn LspAdapterDelegate>>,
- window: &mut Window,
- cx: &mut App,
- ) -> Task<SlashCommandResult> {
- let mut paths = HashSet::default();
- let mut file_command_old_outputs = Vec::new();
- let mut file_command_new_outputs = Vec::new();
-
- for section in context_slash_command_output_sections.iter().rev() {
- if let Some(metadata) = section
- .metadata
- .as_ref()
- .and_then(|value| serde_json::from_value::<FileCommandMetadata>(value.clone()).ok())
- && paths.insert(metadata.path.clone())
- {
- file_command_old_outputs.push(
- context_buffer
- .as_rope()
- .slice(section.range.to_offset(&context_buffer)),
- );
- file_command_new_outputs.push(Arc::new(FileSlashCommand).run(
- std::slice::from_ref(&metadata.path),
- context_slash_command_output_sections,
- context_buffer.clone(),
- workspace.clone(),
- delegate.clone(),
- window,
- cx,
- ));
- }
- }
-
- cx.background_spawn(async move {
- let mut output = SlashCommandOutput::default();
- let mut changes_detected = false;
-
- let file_command_new_outputs = future::join_all(file_command_new_outputs).await;
- for (old_text, new_output) in file_command_old_outputs
- .into_iter()
- .zip(file_command_new_outputs)
- {
- if let Ok(new_output) = new_output
- && let Ok(new_output) = SlashCommandOutput::from_event_stream(new_output).await
- && let Some(file_command_range) = new_output.sections.first()
- {
- let new_text = &new_output.text[file_command_range.range.clone()];
- if old_text.chars().ne(new_text.chars()) {
- changes_detected = true;
- output
- .sections
- .extend(new_output.sections.into_iter().map(|section| {
- SlashCommandOutputSection {
- range: output.text.len() + section.range.start
- ..output.text.len() + section.range.end,
- icon: section.icon,
- label: section.label,
- metadata: section.metadata,
- }
- }));
- output.text.push_str(&new_output.text);
- }
- }
- }
-
- anyhow::ensure!(changes_detected, "no new changes detected");
- Ok(output.into_event_stream())
- })
- }
-}
@@ -1,451 +0,0 @@
-use anyhow::{Context as _, Result, anyhow};
-use assistant_slash_command::{
- ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
- SlashCommandResult,
-};
-use fuzzy::{PathMatch, StringMatchCandidate};
-use gpui::{App, Entity, Task, WeakEntity};
-use language::{
- Anchor, BufferSnapshot, DiagnosticEntryRef, DiagnosticSeverity, LspAdapterDelegate,
- OffsetRangeExt, ToOffset,
-};
-use project::{DiagnosticSummary, PathMatchCandidateSet, Project};
-use rope::Point;
-use std::{
- fmt::Write,
- path::Path,
- sync::{Arc, atomic::AtomicBool},
-};
-use ui::prelude::*;
-use util::paths::{PathMatcher, PathStyle};
-use util::{ResultExt, rel_path::RelPath};
-use workspace::Workspace;
-
-use crate::create_label_for_command;
-
-pub struct DiagnosticsSlashCommand;
-
-impl DiagnosticsSlashCommand {
- fn search_paths(
- &self,
- query: String,
- cancellation_flag: Arc<AtomicBool>,
- workspace: &Entity<Workspace>,
- cx: &mut App,
- ) -> Task<Vec<PathMatch>> {
- if query.is_empty() {
- let workspace = workspace.read(cx);
- let entries = workspace.recent_navigation_history(Some(10), cx);
- let path_prefix: Arc<RelPath> = RelPath::empty().into();
- Task::ready(
- entries
- .into_iter()
- .map(|(entry, _)| PathMatch {
- score: 0.,
- positions: Vec::new(),
- worktree_id: entry.worktree_id.to_usize(),
- path: entry.path,
- path_prefix: path_prefix.clone(),
- is_dir: false, // Diagnostics can't be produced for directories
- distance_to_relative_ancestor: 0,
- })
- .collect(),
- )
- } else {
- let worktrees = workspace.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
- let candidate_sets = worktrees
- .into_iter()
- .map(|worktree| {
- let worktree = worktree.read(cx);
- PathMatchCandidateSet {
- snapshot: worktree.snapshot(),
- include_ignored: worktree
- .root_entry()
- .is_some_and(|entry| entry.is_ignored),
- include_root_name: true,
- candidates: project::Candidates::Entries,
- }
- })
- .collect::<Vec<_>>();
-
- let executor = cx.background_executor().clone();
- cx.foreground_executor().spawn(async move {
- fuzzy::match_path_sets(
- candidate_sets.as_slice(),
- query.as_str(),
- &None,
- false,
- 100,
- &cancellation_flag,
- executor,
- )
- .await
- })
- }
- }
-}
-
-impl SlashCommand for DiagnosticsSlashCommand {
- fn name(&self) -> String {
- "diagnostics".into()
- }
-
- fn label(&self, cx: &App) -> language::CodeLabel {
- create_label_for_command("diagnostics", &[INCLUDE_WARNINGS_ARGUMENT], cx)
- }
-
- fn description(&self) -> String {
- "Insert diagnostics".into()
- }
-
- fn icon(&self) -> IconName {
- IconName::XCircle
- }
-
- fn menu_text(&self) -> String {
- self.description()
- }
-
- fn requires_argument(&self) -> bool {
- false
- }
-
- fn accepts_arguments(&self) -> bool {
- true
- }
-
- fn complete_argument(
- self: Arc<Self>,
- arguments: &[String],
- cancellation_flag: Arc<AtomicBool>,
- workspace: Option<WeakEntity<Workspace>>,
- _: &mut Window,
- cx: &mut App,
- ) -> Task<Result<Vec<ArgumentCompletion>>> {
- let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
- return Task::ready(Err(anyhow!("workspace was dropped")));
- };
- let path_style = workspace.read(cx).project().read(cx).path_style(cx);
- let query = arguments.last().cloned().unwrap_or_default();
-
- let paths = self.search_paths(query.clone(), cancellation_flag.clone(), &workspace, cx);
- let executor = cx.background_executor().clone();
- cx.background_spawn(async move {
- let mut matches: Vec<String> = paths
- .await
- .into_iter()
- .map(|path_match| {
- path_match
- .path_prefix
- .join(&path_match.path)
- .display(path_style)
- .to_string()
- })
- .collect();
-
- matches.extend(
- fuzzy::match_strings(
- &Options::match_candidates_for_args(),
- &query,
- false,
- true,
- 10,
- &cancellation_flag,
- executor,
- )
- .await
- .into_iter()
- .map(|candidate| candidate.string),
- );
-
- Ok(matches
- .into_iter()
- .map(|completion| ArgumentCompletion {
- label: completion.clone().into(),
- new_text: completion,
- after_completion: assistant_slash_command::AfterCompletion::Run,
- replace_previous_arguments: false,
- })
- .collect())
- })
- }
-
- fn run(
- self: Arc<Self>,
- arguments: &[String],
- _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
- _context_buffer: BufferSnapshot,
- workspace: WeakEntity<Workspace>,
- _delegate: Option<Arc<dyn LspAdapterDelegate>>,
- window: &mut Window,
- cx: &mut App,
- ) -> Task<SlashCommandResult> {
- let Some(workspace) = workspace.upgrade() else {
- return Task::ready(Err(anyhow!("workspace was dropped")));
- };
-
- let project = workspace.read(cx).project();
- let path_style = project.read(cx).path_style(cx);
- let options = Options::parse(arguments, path_style);
-
- let task = collect_diagnostics_output(project.clone(), options, cx);
-
- window.spawn(cx, async move |_| {
- task.await?
- .map(|output| output.into_event_stream())
- .context("No diagnostics found")
- })
- }
-}
-
-pub struct Options {
- pub include_errors: bool,
- pub include_warnings: bool,
- pub path_matcher: Option<PathMatcher>,
-}
-
-const INCLUDE_WARNINGS_ARGUMENT: &str = "--include-warnings";
-
-impl Options {
- fn parse(arguments: &[String], path_style: PathStyle) -> Self {
- let mut include_warnings = false;
- let mut path_matcher = None;
- for arg in arguments {
- if arg == INCLUDE_WARNINGS_ARGUMENT {
- include_warnings = true;
- } else {
- path_matcher = PathMatcher::new(&[arg.to_owned()], path_style).log_err();
- }
- }
- Self {
- include_errors: true,
- include_warnings,
- path_matcher,
- }
- }
-
- fn match_candidates_for_args() -> [StringMatchCandidate; 1] {
- [StringMatchCandidate::new(0, INCLUDE_WARNINGS_ARGUMENT)]
- }
-}
-
-pub fn collect_diagnostics_output(
- project: Entity<Project>,
- options: Options,
- cx: &mut App,
-) -> Task<Result<Option<SlashCommandOutput>>> {
- let path_style = project.read(cx).path_style(cx);
- let glob_is_exact_file_match = if let Some(path) = options
- .path_matcher
- .as_ref()
- .and_then(|pm| pm.sources().next())
- {
- project
- .read(cx)
- .find_project_path(Path::new(path), cx)
- .is_some()
- } else {
- false
- };
-
- let project_handle = project.downgrade();
- let diagnostic_summaries: Vec<_> = project
- .read(cx)
- .diagnostic_summaries(false, cx)
- .flat_map(|(path, _, summary)| {
- let worktree = project.read(cx).worktree_for_id(path.worktree_id, cx)?;
- let full_path = worktree.read(cx).root_name().join(&path.path);
- Some((path, full_path, summary))
- })
- .collect();
-
- cx.spawn(async move |cx| {
- let error_source = if let Some(path_matcher) = &options.path_matcher {
- debug_assert_eq!(path_matcher.sources().count(), 1);
- Some(path_matcher.sources().next().unwrap_or_default())
- } else {
- None
- };
-
- let mut output = SlashCommandOutput::default();
-
- if let Some(error_source) = error_source.as_ref() {
- writeln!(output.text, "diagnostics: {}", error_source).unwrap();
- } else {
- writeln!(output.text, "diagnostics").unwrap();
- }
-
- let mut project_summary = DiagnosticSummary::default();
- for (project_path, path, summary) in diagnostic_summaries {
- if let Some(path_matcher) = &options.path_matcher
- && !path_matcher.is_match(&path)
- {
- continue;
- }
-
- let has_errors = options.include_errors && summary.error_count > 0;
- let has_warnings = options.include_warnings && summary.warning_count > 0;
- if !has_errors && !has_warnings {
- continue;
- }
-
- if options.include_errors {
- project_summary.error_count += summary.error_count;
- }
- if options.include_warnings {
- project_summary.warning_count += summary.warning_count;
- }
-
- let last_end = output.text.len();
- let file_path = path.display(path_style).to_string();
- if !glob_is_exact_file_match {
- writeln!(&mut output.text, "{file_path}").unwrap();
- }
-
- if let Some(buffer) = project_handle
- .update(cx, |project, cx| project.open_buffer(project_path, cx))?
- .await
- .log_err()
- {
- let snapshot = cx.read_entity(&buffer, |buffer, _| buffer.snapshot());
- collect_buffer_diagnostics(
- &mut output,
- &snapshot,
- options.include_warnings,
- options.include_errors,
- );
- }
-
- if !glob_is_exact_file_match {
- output.sections.push(SlashCommandOutputSection {
- range: last_end..output.text.len().saturating_sub(1),
- icon: IconName::File,
- label: file_path.into(),
- metadata: None,
- });
- }
- }
-
- // No diagnostics found
- if output.sections.is_empty() {
- return Ok(None);
- }
-
- let mut label = String::new();
- label.push_str("Diagnostics");
- if let Some(source) = error_source {
- write!(label, " ({})", source).unwrap();
- }
-
- if project_summary.error_count > 0 || project_summary.warning_count > 0 {
- label.push(':');
-
- if project_summary.error_count > 0 {
- write!(label, " {} errors", project_summary.error_count).unwrap();
- if project_summary.warning_count > 0 {
- label.push_str(",");
- }
- }
-
- if project_summary.warning_count > 0 {
- write!(label, " {} warnings", project_summary.warning_count).unwrap();
- }
- }
-
- output.sections.insert(
- 0,
- SlashCommandOutputSection {
- range: 0..output.text.len(),
- icon: IconName::Warning,
- label: label.into(),
- metadata: None,
- },
- );
-
- Ok(Some(output))
- })
-}
-
-pub fn collect_buffer_diagnostics(
- output: &mut SlashCommandOutput,
- snapshot: &BufferSnapshot,
- include_warnings: bool,
- include_errors: bool,
-) {
- for (_, group) in snapshot.diagnostic_groups(None) {
- let entry = &group.entries[group.primary_ix];
- collect_diagnostic(output, entry, snapshot, include_warnings, include_errors)
- }
-}
-
-fn collect_diagnostic(
- output: &mut SlashCommandOutput,
- entry: &DiagnosticEntryRef<'_, Anchor>,
- snapshot: &BufferSnapshot,
- include_warnings: bool,
- include_errors: bool,
-) {
- const EXCERPT_EXPANSION_SIZE: u32 = 2;
- const MAX_MESSAGE_LENGTH: usize = 2000;
-
- let (ty, icon) = match entry.diagnostic.severity {
- DiagnosticSeverity::WARNING => {
- if !include_warnings {
- return;
- }
- ("warning", IconName::Warning)
- }
- DiagnosticSeverity::ERROR => {
- if !include_errors {
- return;
- }
- ("error", IconName::XCircle)
- }
- _ => return,
- };
- let prev_len = output.text.len();
-
- let range = entry.range.to_point(snapshot);
- let diagnostic_row_number = range.start.row + 1;
-
- let start_row = range.start.row.saturating_sub(EXCERPT_EXPANSION_SIZE);
- let end_row = (range.end.row + EXCERPT_EXPANSION_SIZE).min(snapshot.max_point().row) + 1;
- let excerpt_range =
- Point::new(start_row, 0).to_offset(snapshot)..Point::new(end_row, 0).to_offset(snapshot);
-
- output.text.push_str("```");
- if let Some(language_name) = snapshot.language().map(|l| l.code_fence_block_name()) {
- output.text.push_str(&language_name);
- }
- output.text.push('\n');
-
- let mut buffer_text = String::new();
- for chunk in snapshot.text_for_range(excerpt_range) {
- buffer_text.push_str(chunk);
- }
-
- for (i, line) in buffer_text.lines().enumerate() {
- let line_number = start_row + i as u32 + 1;
- writeln!(output.text, "{}", line).unwrap();
-
- if line_number == diagnostic_row_number {
- output.text.push_str("//");
- let prev_len = output.text.len();
- write!(output.text, " {}: ", ty).unwrap();
- let padding = output.text.len() - prev_len;
-
- let message = util::truncate(&entry.diagnostic.message, MAX_MESSAGE_LENGTH)
- .replace('\n', format!("\n//{:padding$}", "").as_str());
-
- writeln!(output.text, "{message}").unwrap();
- }
- }
-
- writeln!(output.text, "```").unwrap();
- output.sections.push(SlashCommandOutputSection {
- range: prev_len..output.text.len().saturating_sub(1),
- icon,
- label: entry.diagnostic.message.clone().into(),
- metadata: None,
- });
-}
@@ -1,183 +0,0 @@
-use std::cell::RefCell;
-use std::rc::Rc;
-use std::sync::Arc;
-use std::sync::atomic::AtomicBool;
-
-use anyhow::{Context as _, Result, anyhow, bail};
-use assistant_slash_command::{
- ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
- SlashCommandResult,
-};
-use futures::AsyncReadExt;
-use gpui::{Task, WeakEntity};
-use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown};
-use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
-use language::{BufferSnapshot, LspAdapterDelegate};
-use ui::prelude::*;
-use workspace::Workspace;
-
-#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
-enum ContentType {
- Html,
- Plaintext,
- Json,
-}
-
-pub struct FetchSlashCommand;
-
-impl FetchSlashCommand {
- async fn build_message(http_client: Arc<HttpClientWithUrl>, url: &str) -> Result<String> {
- let mut url = url.to_owned();
- if !url.starts_with("https://") && !url.starts_with("http://") {
- url = format!("https://{url}");
- }
-
- let mut response = http_client.get(&url, AsyncBody::default(), true).await?;
-
- let mut body = Vec::new();
- response
- .body_mut()
- .read_to_end(&mut body)
- .await
- .context("error reading response body")?;
-
- if response.status().is_client_error() {
- let text = String::from_utf8_lossy(body.as_slice());
- bail!(
- "status error {}, response: {text:?}",
- response.status().as_u16()
- );
- }
-
- let Some(content_type) = response.headers().get("content-type") else {
- bail!("missing Content-Type header");
- };
- let content_type = content_type
- .to_str()
- .context("invalid Content-Type header")?;
- let content_type = if content_type.starts_with("text/html") {
- ContentType::Html
- } else if content_type.starts_with("text/plain") {
- ContentType::Plaintext
- } else if content_type.starts_with("application/json") {
- ContentType::Json
- } else {
- ContentType::Html
- };
-
- match content_type {
- ContentType::Html => {
- let mut handlers: Vec<TagHandler> = vec![
- Rc::new(RefCell::new(markdown::WebpageChromeRemover)),
- Rc::new(RefCell::new(markdown::ParagraphHandler)),
- Rc::new(RefCell::new(markdown::HeadingHandler)),
- Rc::new(RefCell::new(markdown::ListHandler)),
- Rc::new(RefCell::new(markdown::TableHandler::new())),
- Rc::new(RefCell::new(markdown::StyledTextHandler)),
- ];
- if url.contains("wikipedia.org") {
- use html_to_markdown::structure::wikipedia;
-
- handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaChromeRemover)));
- handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaInfoboxHandler)));
- handlers.push(Rc::new(
- RefCell::new(wikipedia::WikipediaCodeHandler::new()),
- ));
- } else {
- handlers.push(Rc::new(RefCell::new(markdown::CodeHandler)));
- }
-
- convert_html_to_markdown(&body[..], &mut handlers)
- }
- ContentType::Plaintext => Ok(std::str::from_utf8(&body)?.to_owned()),
- ContentType::Json => {
- let json: serde_json::Value = serde_json::from_slice(&body)?;
-
- Ok(format!(
- "```json\n{}\n```",
- serde_json::to_string_pretty(&json)?
- ))
- }
- }
- }
-}
-
-impl SlashCommand for FetchSlashCommand {
- fn name(&self) -> String {
- "fetch".into()
- }
-
- fn description(&self) -> String {
- "Insert fetched URL contents".into()
- }
-
- fn icon(&self) -> IconName {
- IconName::ToolWeb
- }
-
- fn menu_text(&self) -> String {
- self.description()
- }
-
- fn requires_argument(&self) -> bool {
- true
- }
-
- fn complete_argument(
- self: Arc<Self>,
- _arguments: &[String],
- _cancel: Arc<AtomicBool>,
- _workspace: Option<WeakEntity<Workspace>>,
- _window: &mut Window,
- _cx: &mut App,
- ) -> Task<Result<Vec<ArgumentCompletion>>> {
- Task::ready(Ok(Vec::new()))
- }
-
- fn run(
- self: Arc<Self>,
- arguments: &[String],
- _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
- _context_buffer: BufferSnapshot,
- workspace: WeakEntity<Workspace>,
- _delegate: Option<Arc<dyn LspAdapterDelegate>>,
- _: &mut Window,
- cx: &mut App,
- ) -> Task<SlashCommandResult> {
- let Some(argument) = arguments.first() else {
- return Task::ready(Err(anyhow!("missing URL")));
- };
- let Some(workspace) = workspace.upgrade() else {
- return Task::ready(Err(anyhow!("workspace was dropped")));
- };
-
- let http_client = workspace.read(cx).client().http_client();
- let url = argument.to_string();
-
- let text = cx.background_spawn({
- let url = url.clone();
- async move { Self::build_message(http_client, &url).await }
- });
-
- let url = SharedString::from(url);
- cx.foreground_executor().spawn(async move {
- let text = text.await?;
- if text.trim().is_empty() {
- bail!("no textual content found");
- }
-
- let range = 0..text.len();
- Ok(SlashCommandOutput {
- text,
- sections: vec![SlashCommandOutputSection {
- range,
- icon: IconName::ToolWeb,
- label: format!("fetch {}", url).into(),
- metadata: None,
- }],
- run_commands_in_text: false,
- }
- .into_event_stream())
- })
- }
-}
@@ -1,713 +0,0 @@
-use anyhow::{Context as _, Result, anyhow};
-use assistant_slash_command::{
- AfterCompletion, ArgumentCompletion, SlashCommand, SlashCommandContent, SlashCommandEvent,
- SlashCommandOutput, SlashCommandOutputSection, SlashCommandResult,
-};
-use futures::Stream;
-use futures::channel::mpsc;
-use fuzzy::PathMatch;
-use gpui::{App, Entity, Task, WeakEntity};
-use language::{BufferSnapshot, CodeLabelBuilder, HighlightId, LineEnding, LspAdapterDelegate};
-use project::{PathMatchCandidateSet, Project};
-use serde::{Deserialize, Serialize};
-use smol::stream::StreamExt;
-use std::{
- fmt::Write,
- ops::{Range, RangeInclusive},
- path::Path,
- sync::{Arc, atomic::AtomicBool},
-};
-use ui::prelude::*;
-use util::{ResultExt, rel_path::RelPath};
-use workspace::Workspace;
-use worktree::ChildEntriesOptions;
-
-pub struct FileSlashCommand;
-
-impl FileSlashCommand {
- fn search_paths(
- &self,
- query: String,
- cancellation_flag: Arc<AtomicBool>,
- workspace: &Entity<Workspace>,
- cx: &mut App,
- ) -> Task<Vec<PathMatch>> {
- if query.is_empty() {
- let workspace = workspace.read(cx);
- let project = workspace.project().read(cx);
- let entries = workspace.recent_navigation_history(Some(10), cx);
-
- let entries = entries
- .into_iter()
- .map(|entries| (entries.0, false))
- .chain(project.worktrees(cx).flat_map(|worktree| {
- let worktree = worktree.read(cx);
- let id = worktree.id();
- let options = ChildEntriesOptions {
- include_files: true,
- include_dirs: true,
- include_ignored: false,
- };
- let entries = worktree.child_entries_with_options(RelPath::empty(), options);
- entries.map(move |entry| {
- (
- project::ProjectPath {
- worktree_id: id,
- path: entry.path.clone(),
- },
- entry.kind.is_dir(),
- )
- })
- }))
- .collect::<Vec<_>>();
-
- let path_prefix: Arc<RelPath> = RelPath::empty().into();
- Task::ready(
- entries
- .into_iter()
- .filter_map(|(entry, is_dir)| {
- let worktree = project.worktree_for_id(entry.worktree_id, cx)?;
- let full_path = worktree.read(cx).root_name().join(&entry.path);
- Some(PathMatch {
- score: 0.,
- positions: Vec::new(),
- worktree_id: entry.worktree_id.to_usize(),
- path: full_path,
- path_prefix: path_prefix.clone(),
- distance_to_relative_ancestor: 0,
- is_dir,
- })
- })
- .collect(),
- )
- } else {
- let worktrees = workspace.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
- let candidate_sets = worktrees
- .into_iter()
- .map(|worktree| {
- let worktree = worktree.read(cx);
-
- PathMatchCandidateSet {
- snapshot: worktree.snapshot(),
- include_ignored: worktree
- .root_entry()
- .is_some_and(|entry| entry.is_ignored),
- include_root_name: true,
- candidates: project::Candidates::Entries,
- }
- })
- .collect::<Vec<_>>();
-
- let executor = cx.background_executor().clone();
- cx.foreground_executor().spawn(async move {
- fuzzy::match_path_sets(
- candidate_sets.as_slice(),
- query.as_str(),
- &None,
- false,
- 100,
- &cancellation_flag,
- executor,
- )
- .await
- })
- }
- }
-}
-
-impl SlashCommand for FileSlashCommand {
- fn name(&self) -> String {
- "file".into()
- }
-
- fn description(&self) -> String {
- "Insert file and/or directory".into()
- }
-
- fn menu_text(&self) -> String {
- self.description()
- }
-
- fn requires_argument(&self) -> bool {
- true
- }
-
- fn icon(&self) -> IconName {
- IconName::File
- }
-
- fn complete_argument(
- self: Arc<Self>,
- arguments: &[String],
- cancellation_flag: Arc<AtomicBool>,
- workspace: Option<WeakEntity<Workspace>>,
- _: &mut Window,
- cx: &mut App,
- ) -> Task<Result<Vec<ArgumentCompletion>>> {
- let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
- return Task::ready(Err(anyhow!("workspace was dropped")));
- };
-
- let path_style = workspace.read(cx).path_style(cx);
-
- let paths = self.search_paths(
- arguments.last().cloned().unwrap_or_default(),
- cancellation_flag,
- &workspace,
- cx,
- );
- let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
- cx.background_spawn(async move {
- Ok(paths
- .await
- .into_iter()
- .filter_map(|path_match| {
- let text = path_match
- .path_prefix
- .join(&path_match.path)
- .display(path_style)
- .to_string();
-
- let mut label = CodeLabelBuilder::default();
- let file_name = path_match.path.file_name()?;
- let label_text = if path_match.is_dir {
- format!("{}/ ", file_name)
- } else {
- format!("{} ", file_name)
- };
-
- label.push_str(label_text.as_str(), None);
- label.push_str(&text, comment_id);
- label.respan_filter_range(Some(file_name));
-
- Some(ArgumentCompletion {
- label: label.build(),
- new_text: text,
- after_completion: AfterCompletion::Compose,
- replace_previous_arguments: false,
- })
- })
- .collect())
- })
- }
-
- fn run(
- self: Arc<Self>,
- arguments: &[String],
- _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
- _context_buffer: BufferSnapshot,
- workspace: WeakEntity<Workspace>,
- _delegate: Option<Arc<dyn LspAdapterDelegate>>,
- _: &mut Window,
- cx: &mut App,
- ) -> Task<SlashCommandResult> {
- let Some(workspace) = workspace.upgrade() else {
- return Task::ready(Err(anyhow!("workspace was dropped")));
- };
-
- if arguments.is_empty() {
- return Task::ready(Err(anyhow!("missing path")));
- };
-
- Task::ready(Ok(collect_files(
- workspace.read(cx).project().clone(),
- arguments,
- cx,
- )
- .boxed()))
- }
-}
-
-fn collect_files(
- project: Entity<Project>,
- glob_inputs: &[String],
- cx: &mut App,
-) -> impl Stream<Item = Result<SlashCommandEvent>> + use<> {
- let Ok(matchers) = glob_inputs
- .iter()
- .map(|glob_input| {
- util::paths::PathMatcher::new(&[glob_input.to_owned()], project.read(cx).path_style(cx))
- .with_context(|| format!("invalid path {glob_input}"))
- })
- .collect::<anyhow::Result<Vec<util::paths::PathMatcher>>>()
- else {
- return futures::stream::once(async {
- anyhow::bail!("invalid path");
- })
- .boxed();
- };
-
- let project_handle = project.downgrade();
- let snapshots = project
- .read(cx)
- .worktrees(cx)
- .map(|worktree| worktree.read(cx).snapshot())
- .collect::<Vec<_>>();
-
- let (events_tx, events_rx) = mpsc::unbounded();
- cx.spawn(async move |cx| {
- for snapshot in snapshots {
- let worktree_id = snapshot.id();
- let path_style = snapshot.path_style();
- let mut directory_stack: Vec<Arc<RelPath>> = Vec::new();
- let mut folded_directory_path: Option<Arc<RelPath>> = None;
- let mut folded_directory_names: Arc<RelPath> = RelPath::empty().into();
- let mut is_top_level_directory = true;
-
- for entry in snapshot.entries(false, 0) {
- let path_including_worktree_name = snapshot.root_name().join(&entry.path);
-
- if !matchers
- .iter()
- .any(|matcher| matcher.is_match(&path_including_worktree_name))
- {
- continue;
- }
-
- while let Some(dir) = directory_stack.last() {
- if entry.path.starts_with(dir) {
- break;
- }
- directory_stack.pop().unwrap();
- events_tx.unbounded_send(Ok(SlashCommandEvent::EndSection))?;
- events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
- SlashCommandContent::Text {
- text: "\n".into(),
- run_commands_in_text: false,
- },
- )))?;
- }
-
- if let Some(folded_path) = &folded_directory_path {
- if !entry.path.starts_with(folded_path) {
- folded_directory_names = RelPath::empty().into();
- folded_directory_path = None;
- if directory_stack.is_empty() {
- is_top_level_directory = true;
- }
- }
- }
-
- let filename = entry.path.file_name().unwrap_or_default().to_string();
-
- if entry.is_dir() {
- // Auto-fold directories that contain no files
- let mut child_entries = snapshot.child_entries(&entry.path);
- if let Some(child) = child_entries.next() {
- if child_entries.next().is_none() && child.kind.is_dir() {
- if is_top_level_directory {
- is_top_level_directory = false;
- folded_directory_names =
- folded_directory_names.join(&path_including_worktree_name);
- } else {
- folded_directory_names =
- folded_directory_names.join(RelPath::unix(&filename).unwrap());
- }
- folded_directory_path = Some(entry.path.clone());
- continue;
- }
- } else {
- // Skip empty directories
- folded_directory_names = RelPath::empty().into();
- folded_directory_path = None;
- continue;
- }
-
- // Render the directory (either folded or normal)
- if folded_directory_names.is_empty() {
- let label = if is_top_level_directory {
- is_top_level_directory = false;
- path_including_worktree_name.display(path_style).to_string()
- } else {
- filename
- };
- events_tx.unbounded_send(Ok(SlashCommandEvent::StartSection {
- icon: IconName::Folder,
- label: label.clone().into(),
- metadata: None,
- }))?;
- events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
- SlashCommandContent::Text {
- text: label.to_string(),
- run_commands_in_text: false,
- },
- )))?;
- directory_stack.push(entry.path.clone());
- } else {
- let entry_name =
- folded_directory_names.join(RelPath::unix(&filename).unwrap());
- let entry_name = entry_name.display(path_style);
- events_tx.unbounded_send(Ok(SlashCommandEvent::StartSection {
- icon: IconName::Folder,
- label: entry_name.to_string().into(),
- metadata: None,
- }))?;
- events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
- SlashCommandContent::Text {
- text: entry_name.to_string(),
- run_commands_in_text: false,
- },
- )))?;
- directory_stack.push(entry.path.clone());
- folded_directory_names = RelPath::empty().into();
- folded_directory_path = None;
- }
- events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
- SlashCommandContent::Text {
- text: "\n".into(),
- run_commands_in_text: false,
- },
- )))?;
- } else if entry.is_file() {
- let Some(open_buffer_task) = project_handle
- .update(cx, |project, cx| {
- project.open_buffer((worktree_id, entry.path.clone()), cx)
- })
- .ok()
- else {
- continue;
- };
- if let Some(buffer) = open_buffer_task.await.log_err() {
- let mut output = SlashCommandOutput::default();
- let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
- append_buffer_to_output(
- &snapshot,
- Some(path_including_worktree_name.display(path_style).as_ref()),
- &mut output,
- )
- .log_err();
- let mut buffer_events = output.into_event_stream();
- while let Some(event) = buffer_events.next().await {
- events_tx.unbounded_send(event)?;
- }
- }
- }
- }
-
- while directory_stack.pop().is_some() {
- events_tx.unbounded_send(Ok(SlashCommandEvent::EndSection))?;
- }
- }
-
- anyhow::Ok(())
- })
- .detach_and_log_err(cx);
-
- events_rx.boxed()
-}
-
-pub fn codeblock_fence_for_path(
- path: Option<&str>,
- row_range: Option<RangeInclusive<u32>>,
-) -> String {
- let mut text = String::new();
- write!(text, "```").unwrap();
-
- if let Some(path) = path {
- if let Some(extension) = Path::new(path).extension().and_then(|ext| ext.to_str()) {
- write!(text, "{} ", extension).unwrap();
- }
-
- write!(text, "{path}").unwrap();
- } else {
- write!(text, "untitled").unwrap();
- }
-
- if let Some(row_range) = row_range {
- write!(text, ":{}-{}", row_range.start() + 1, row_range.end() + 1).unwrap();
- }
-
- text.push('\n');
- text
-}
-
-#[derive(Serialize, Deserialize)]
-pub struct FileCommandMetadata {
- pub path: String,
-}
-
-pub fn build_entry_output_section(
- range: Range<usize>,
- path: Option<&str>,
- is_directory: bool,
- line_range: Option<Range<u32>>,
-) -> SlashCommandOutputSection<usize> {
- let mut label = if let Some(path) = path {
- path.to_string()
- } else {
- "untitled".to_string()
- };
- if let Some(line_range) = line_range {
- write!(label, ":{}-{}", line_range.start, line_range.end).unwrap();
- }
-
- let icon = if is_directory {
- IconName::Folder
- } else {
- IconName::File
- };
-
- SlashCommandOutputSection {
- range,
- icon,
- label: label.into(),
- metadata: if is_directory {
- None
- } else {
- path.and_then(|path| {
- serde_json::to_value(FileCommandMetadata {
- path: path.to_string(),
- })
- .ok()
- })
- },
- }
-}
-
-pub fn append_buffer_to_output(
- buffer: &BufferSnapshot,
- path: Option<&str>,
- output: &mut SlashCommandOutput,
-) -> Result<()> {
- let prev_len = output.text.len();
-
- let mut content = buffer.text();
- LineEnding::normalize(&mut content);
- output.text.push_str(&codeblock_fence_for_path(path, None));
- output.text.push_str(&content);
- if !output.text.ends_with('\n') {
- output.text.push('\n');
- }
- output.text.push_str("```");
- output.text.push('\n');
-
- let section_ix = output.sections.len();
- output.sections.insert(
- section_ix,
- build_entry_output_section(prev_len..output.text.len(), path, false, None),
- );
-
- output.text.push('\n');
-
- Ok(())
-}
-
-#[cfg(test)]
-mod test {
- use assistant_slash_command::SlashCommandOutput;
- use fs::FakeFs;
- use gpui::TestAppContext;
- use pretty_assertions::assert_eq;
- use project::Project;
- use serde_json::json;
- use settings::SettingsStore;
- use smol::stream::StreamExt;
- use util::path;
-
- use super::collect_files;
-
- pub fn init_test(cx: &mut gpui::TestAppContext) {
- zlog::init_test();
-
- cx.update(|cx| {
- let settings_store = SettingsStore::test(cx);
- cx.set_global(settings_store);
- // release_channel::init(SemanticVersion::default(), cx);
- });
- }
-
- #[gpui::test]
- async fn test_file_exact_matching(cx: &mut TestAppContext) {
- init_test(cx);
- let fs = FakeFs::new(cx.executor());
-
- fs.insert_tree(
- path!("/root"),
- json!({
- "dir": {
- "subdir": {
- "file_0": "0"
- },
- "file_1": "1",
- "file_2": "2",
- "file_3": "3",
- },
- "dir.rs": "4"
- }),
- )
- .await;
-
- let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
-
- let result_1 =
- cx.update(|cx| collect_files(project.clone(), &["root/dir".to_string()], cx));
- let result_1 = SlashCommandOutput::from_event_stream(result_1.boxed())
- .await
- .unwrap();
-
- assert!(result_1.text.starts_with(path!("root/dir")));
- // 4 files + 2 directories
- assert_eq!(result_1.sections.len(), 6);
-
- let result_2 =
- cx.update(|cx| collect_files(project.clone(), &["root/dir/".to_string()], cx));
- let result_2 = SlashCommandOutput::from_event_stream(result_2.boxed())
- .await
- .unwrap();
-
- assert_eq!(result_1, result_2);
-
- let result =
- cx.update(|cx| collect_files(project.clone(), &["root/dir*".to_string()], cx).boxed());
- let result = SlashCommandOutput::from_event_stream(result).await.unwrap();
-
- assert!(result.text.starts_with(path!("root/dir")));
- // 5 files + 2 directories
- assert_eq!(result.sections.len(), 7);
-
- // Ensure that the project lasts until after the last await
- drop(project);
- }
-
- #[gpui::test]
- async fn test_file_sub_directory_rendering(cx: &mut TestAppContext) {
- init_test(cx);
- let fs = FakeFs::new(cx.executor());
-
- fs.insert_tree(
- path!("/zed"),
- json!({
- "assets": {
- "dir1": {
- ".gitkeep": ""
- },
- "dir2": {
- ".gitkeep": ""
- },
- "themes": {
- "ayu": {
- "LICENSE": "1",
- },
- "andromeda": {
- "LICENSE": "2",
- },
- "summercamp": {
- "LICENSE": "3",
- },
- },
- },
- }),
- )
- .await;
-
- let project = Project::test(fs, [path!("/zed").as_ref()], cx).await;
-
- let result =
- cx.update(|cx| collect_files(project.clone(), &["zed/assets/themes".to_string()], cx));
- let result = SlashCommandOutput::from_event_stream(result.boxed())
- .await
- .unwrap();
-
- // Sanity check
- assert!(result.text.starts_with(path!("zed/assets/themes\n")));
- assert_eq!(result.sections.len(), 7);
-
- // Ensure that full file paths are included in the real output
- assert!(
- result
- .text
- .contains(path!("zed/assets/themes/andromeda/LICENSE"))
- );
- assert!(result.text.contains(path!("zed/assets/themes/ayu/LICENSE")));
- assert!(
- result
- .text
- .contains(path!("zed/assets/themes/summercamp/LICENSE"))
- );
-
- assert_eq!(result.sections[5].label, "summercamp");
-
- // Ensure that things are in descending order, with properly relativized paths
- assert_eq!(
- result.sections[0].label,
- path!("zed/assets/themes/andromeda/LICENSE")
- );
- assert_eq!(result.sections[1].label, "andromeda");
- assert_eq!(
- result.sections[2].label,
- path!("zed/assets/themes/ayu/LICENSE")
- );
- assert_eq!(result.sections[3].label, "ayu");
- assert_eq!(
- result.sections[4].label,
- path!("zed/assets/themes/summercamp/LICENSE")
- );
-
- // Ensure that the project lasts until after the last await
- drop(project);
- }
-
- #[gpui::test]
- async fn test_file_deep_sub_directory_rendering(cx: &mut TestAppContext) {
- init_test(cx);
- let fs = FakeFs::new(cx.executor());
-
- fs.insert_tree(
- path!("/zed"),
- json!({
- "assets": {
- "themes": {
- "LICENSE": "1",
- "summercamp": {
- "LICENSE": "1",
- "subdir": {
- "LICENSE": "1",
- "subsubdir": {
- "LICENSE": "3",
- }
- }
- },
- },
- },
- }),
- )
- .await;
-
- let project = Project::test(fs, [path!("/zed").as_ref()], cx).await;
-
- let result =
- cx.update(|cx| collect_files(project.clone(), &["zed/assets/themes".to_string()], cx));
- let result = SlashCommandOutput::from_event_stream(result.boxed())
- .await
- .unwrap();
-
- assert!(result.text.starts_with(path!("zed/assets/themes\n")));
- assert_eq!(result.sections[0].label, path!("zed/assets/themes/LICENSE"));
- assert_eq!(
- result.sections[1].label,
- path!("zed/assets/themes/summercamp/LICENSE")
- );
- assert_eq!(
- result.sections[2].label,
- path!("zed/assets/themes/summercamp/subdir/LICENSE")
- );
- assert_eq!(
- result.sections[3].label,
- path!("zed/assets/themes/summercamp/subdir/subsubdir/LICENSE")
- );
- assert_eq!(result.sections[4].label, "subsubdir");
- assert_eq!(result.sections[5].label, "subdir");
- assert_eq!(result.sections[6].label, "summercamp");
- assert_eq!(result.sections[7].label, path!("zed/assets/themes"));
-
- assert_eq!(
- result.text,
- path!(
- "zed/assets/themes\n```zed/assets/themes/LICENSE\n1\n```\n\nsummercamp\n```zed/assets/themes/summercamp/LICENSE\n1\n```\n\nsubdir\n```zed/assets/themes/summercamp/subdir/LICENSE\n1\n```\n\nsubsubdir\n```zed/assets/themes/summercamp/subdir/subsubdir/LICENSE\n3\n```\n\n"
- )
- );
-
- // Ensure that the project lasts until after the last await
- drop(project);
- }
-}
@@ -1,71 +0,0 @@
-use std::sync::Arc;
-use std::sync::atomic::AtomicBool;
-
-use anyhow::Result;
-use assistant_slash_command::{
- ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
- SlashCommandResult,
-};
-use chrono::Local;
-use gpui::{Task, WeakEntity};
-use language::{BufferSnapshot, LspAdapterDelegate};
-use ui::prelude::*;
-use workspace::Workspace;
-
-pub struct NowSlashCommand;
-
-impl SlashCommand for NowSlashCommand {
- fn name(&self) -> String {
- "now".into()
- }
-
- fn description(&self) -> String {
- "Insert current date and time".into()
- }
-
- fn menu_text(&self) -> String {
- self.description()
- }
-
- fn requires_argument(&self) -> bool {
- false
- }
-
- fn complete_argument(
- self: Arc<Self>,
- _arguments: &[String],
- _cancel: Arc<AtomicBool>,
- _workspace: Option<WeakEntity<Workspace>>,
- _window: &mut Window,
- _cx: &mut App,
- ) -> Task<Result<Vec<ArgumentCompletion>>> {
- Task::ready(Ok(Vec::new()))
- }
-
- fn run(
- self: Arc<Self>,
- _arguments: &[String],
- _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
- _context_buffer: BufferSnapshot,
- _workspace: WeakEntity<Workspace>,
- _delegate: Option<Arc<dyn LspAdapterDelegate>>,
- _window: &mut Window,
- _cx: &mut App,
- ) -> Task<SlashCommandResult> {
- let now = Local::now();
- let text = format!("Today is {now}.", now = now.to_rfc2822());
- let range = 0..text.len();
-
- Task::ready(Ok(SlashCommandOutput {
- text,
- sections: vec![SlashCommandOutputSection {
- range,
- icon: IconName::CountdownTimer,
- label: now.to_rfc2822().into(),
- metadata: None,
- }],
- run_commands_in_text: false,
- }
- .into_event_stream()))
- }
-}
@@ -1,123 +0,0 @@
-use anyhow::{Context as _, Result, anyhow};
-use assistant_slash_command::{
- ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
- SlashCommandResult,
-};
-use gpui::{Task, WeakEntity};
-use language::{BufferSnapshot, LspAdapterDelegate};
-use prompt_store::{PromptMetadata, PromptStore};
-use std::sync::{Arc, atomic::AtomicBool};
-use ui::prelude::*;
-use workspace::Workspace;
-
-pub struct PromptSlashCommand;
-
-impl SlashCommand for PromptSlashCommand {
- fn name(&self) -> String {
- "prompt".into()
- }
-
- fn description(&self) -> String {
- "Insert prompt from library".into()
- }
-
- fn icon(&self) -> IconName {
- IconName::Library
- }
-
- fn menu_text(&self) -> String {
- self.description()
- }
-
- fn requires_argument(&self) -> bool {
- true
- }
-
- fn complete_argument(
- self: Arc<Self>,
- arguments: &[String],
- _cancellation_flag: Arc<AtomicBool>,
- _workspace: Option<WeakEntity<Workspace>>,
- _: &mut Window,
- cx: &mut App,
- ) -> Task<Result<Vec<ArgumentCompletion>>> {
- let store = PromptStore::global(cx);
- let query = arguments.to_owned().join(" ");
- cx.spawn(async move |cx| {
- let cancellation_flag = Arc::new(AtomicBool::default());
- let prompts: Vec<PromptMetadata> = store
- .await?
- .read_with(cx, |store, cx| store.search(query, cancellation_flag, cx))
- .await;
- Ok(prompts
- .into_iter()
- .filter_map(|prompt| {
- let prompt_title = prompt.title?.to_string();
- Some(ArgumentCompletion {
- label: prompt_title.clone().into(),
- new_text: prompt_title,
- after_completion: true.into(),
- replace_previous_arguments: true,
- })
- })
- .collect())
- })
- }
-
- fn run(
- self: Arc<Self>,
- arguments: &[String],
- _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
- _context_buffer: BufferSnapshot,
- _workspace: WeakEntity<Workspace>,
- _delegate: Option<Arc<dyn LspAdapterDelegate>>,
- _: &mut Window,
- cx: &mut App,
- ) -> Task<SlashCommandResult> {
- let title = arguments.to_owned().join(" ");
- if title.trim().is_empty() {
- return Task::ready(Err(anyhow!("missing prompt name")));
- };
-
- let store = PromptStore::global(cx);
- let title = SharedString::from(title);
- let prompt = cx.spawn({
- let title = title.clone();
- async move |cx| {
- let store = store.await?;
- let body = store
- .read_with(cx, |store, cx| {
- let prompt_id = store
- .id_for_title(&title)
- .with_context(|| format!("no prompt found with title {:?}", title))?;
- anyhow::Ok(store.load(prompt_id, cx))
- })?
- .await?;
- anyhow::Ok(body)
- }
- });
- cx.foreground_executor().spawn(async move {
- let mut prompt = prompt.await?;
-
- if prompt.starts_with('/') {
- // Prevent an edge case where the inserted prompt starts with a slash command (that leads to funky rendering).
- prompt.insert(0, '\n');
- }
- if prompt.is_empty() {
- prompt.push('\n');
- }
- let range = 0..prompt.len();
- Ok(SlashCommandOutput {
- text: prompt,
- sections: vec![SlashCommandOutputSection {
- range,
- icon: IconName::Library,
- label: title,
- metadata: None,
- }],
- run_commands_in_text: true,
- }
- .into_event_stream())
- })
- }
-}
@@ -1,357 +0,0 @@
-use anyhow::{Result, anyhow};
-use assistant_slash_command::{
- ArgumentCompletion, SlashCommand, SlashCommandContent, SlashCommandEvent,
- SlashCommandOutputSection, SlashCommandResult,
-};
-use editor::{BufferOffset, Editor, MultiBufferSnapshot};
-use futures::StreamExt;
-use gpui::{App, SharedString, Task, WeakEntity, Window};
-use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate};
-
-use rope::Point;
-use std::ops::Range;
-use std::sync::Arc;
-use std::sync::atomic::AtomicBool;
-use ui::IconName;
-use workspace::Workspace;
-
-use crate::file_command::codeblock_fence_for_path;
-
-pub struct SelectionCommand;
-
-impl SlashCommand for SelectionCommand {
- fn name(&self) -> String {
- "selection".into()
- }
-
- fn label(&self, _cx: &App) -> CodeLabel {
- CodeLabel::plain(self.name(), None)
- }
-
- fn description(&self) -> String {
- "Insert editor selection".into()
- }
-
- fn icon(&self) -> IconName {
- IconName::Quote
- }
-
- fn menu_text(&self) -> String {
- self.description()
- }
-
- fn requires_argument(&self) -> bool {
- false
- }
-
- fn accepts_arguments(&self) -> bool {
- true
- }
-
- fn complete_argument(
- self: Arc<Self>,
- _arguments: &[String],
- _cancel: Arc<AtomicBool>,
- _workspace: Option<WeakEntity<Workspace>>,
- _window: &mut Window,
- _cx: &mut App,
- ) -> Task<Result<Vec<ArgumentCompletion>>> {
- Task::ready(Err(anyhow!("this command does not require argument")))
- }
-
- fn run(
- self: Arc<Self>,
- _arguments: &[String],
- _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
- _context_buffer: BufferSnapshot,
- workspace: WeakEntity<Workspace>,
- _delegate: Option<Arc<dyn LspAdapterDelegate>>,
- _window: &mut Window,
- cx: &mut App,
- ) -> Task<SlashCommandResult> {
- let mut events = vec![];
-
- let Some(creases) = workspace
- .update(cx, |workspace, cx| {
- let editor = workspace
- .active_item(cx)
- .and_then(|item| item.act_as::<Editor>(cx))?;
-
- editor.update(cx, |editor, cx| {
- let selection_ranges = editor
- .selections
- .all_adjusted(&editor.display_snapshot(cx))
- .iter()
- .map(|selection| selection.range())
- .collect::<Vec<_>>();
- let snapshot = editor.buffer().read(cx).snapshot(cx);
- Some(selections_creases(selection_ranges, snapshot, cx))
- })
- })
- .unwrap_or_else(|e| {
- events.push(Err(e));
- None
- })
- else {
- return Task::ready(Err(anyhow!("no active selection")));
- };
-
- for (text, title) in creases {
- events.push(Ok(SlashCommandEvent::StartSection {
- icon: IconName::TextSnippet,
- label: SharedString::from(title),
- metadata: None,
- }));
- events.push(Ok(SlashCommandEvent::Content(SlashCommandContent::Text {
- text,
- run_commands_in_text: false,
- })));
- events.push(Ok(SlashCommandEvent::EndSection));
- events.push(Ok(SlashCommandEvent::Content(SlashCommandContent::Text {
- text: "\n".to_string(),
- run_commands_in_text: false,
- })));
- }
-
- let result = futures::stream::iter(events).boxed();
-
- Task::ready(Ok(result))
- }
-}
-
-pub fn selections_creases(
- selection_ranges: Vec<Range<Point>>,
- snapshot: MultiBufferSnapshot,
- cx: &App,
-) -> Vec<(String, String)> {
- let mut creases = Vec::new();
- for range in selection_ranges {
- let buffer_ranges = snapshot.range_to_buffer_ranges(range.clone());
-
- if buffer_ranges.is_empty() {
- creases.extend(crease_for_range(range, &snapshot, cx));
- continue;
- }
-
- for (buffer_snapshot, buffer_range, _excerpt_id) in buffer_ranges {
- creases.extend(crease_for_buffer_range(buffer_snapshot, buffer_range, cx));
- }
- }
- creases
-}
-
-/// Creates a crease for a range within a specific buffer (excerpt).
-/// This is used when we know the exact buffer and range within it.
-fn crease_for_buffer_range(
- buffer: &BufferSnapshot,
- Range { start, end }: Range<BufferOffset>,
- cx: &App,
-) -> Option<(String, String)> {
- let selected_text: String = buffer.text_for_range(start.0..end.0).collect();
-
- if selected_text.is_empty() {
- return None;
- }
-
- let start_point = buffer.offset_to_point(start.0);
- let end_point = buffer.offset_to_point(end.0);
- let start_buffer_row = start_point.row;
- let end_buffer_row = end_point.row;
-
- let language = buffer.language_at(start.0);
- let language_name_arc = language.map(|l| l.code_fence_block_name());
- let language_name = language_name_arc.as_deref().unwrap_or_default();
-
- let filename = buffer
- .file()
- .map(|file| file.full_path(cx).to_string_lossy().into_owned());
-
- let text = if language_name == "markdown" {
- selected_text
- .lines()
- .map(|line| format!("> {}", line))
- .collect::<Vec<_>>()
- .join("\n")
- } else {
- let start_symbols = buffer.symbols_containing(start, None);
- let end_symbols = buffer.symbols_containing(end, None);
-
- let outline_text = if !start_symbols.is_empty() && !end_symbols.is_empty() {
- Some(
- start_symbols
- .into_iter()
- .zip(end_symbols)
- .take_while(|(a, b)| a == b)
- .map(|(a, _)| a.text)
- .collect::<Vec<_>>()
- .join(" > "),
- )
- } else {
- None
- };
-
- let line_comment_prefix =
- language.and_then(|l| l.default_scope().line_comment_prefixes().first().cloned());
-
- let fence =
- codeblock_fence_for_path(filename.as_deref(), Some(start_buffer_row..=end_buffer_row));
-
- if let Some((line_comment_prefix, outline_text)) = line_comment_prefix.zip(outline_text) {
- let breadcrumb = format!("{line_comment_prefix}Excerpt from: {outline_text}\n");
- format!("{fence}{breadcrumb}{selected_text}\n```")
- } else {
- format!("{fence}{selected_text}\n```")
- }
- };
-
- let crease_title = if let Some(path) = filename {
- let start_line = start_buffer_row + 1;
- let end_line = end_buffer_row + 1;
- if start_line == end_line {
- format!("{path}, Line {start_line}")
- } else {
- format!("{path}, Lines {start_line} to {end_line}")
- }
- } else {
- "Quoted selection".to_string()
- };
-
- Some((text, crease_title))
-}
-
-/// Fallback function to create a crease from a multibuffer range when we can't split by excerpt.
-fn crease_for_range(
- range: Range<Point>,
- snapshot: &MultiBufferSnapshot,
- cx: &App,
-) -> Option<(String, String)> {
- let selected_text = snapshot.text_for_range(range.clone()).collect::<String>();
- if selected_text.is_empty() {
- return None;
- }
-
- // Get actual file line numbers (not multibuffer row numbers)
- let start_buffer_row = snapshot
- .point_to_buffer_point(range.start)
- .map(|(_, point, _)| point.row)
- .unwrap_or(range.start.row);
- let end_buffer_row = snapshot
- .point_to_buffer_point(range.end)
- .map(|(_, point, _)| point.row)
- .unwrap_or(range.end.row);
-
- let start_language = snapshot.language_at(range.start);
- let end_language = snapshot.language_at(range.end);
- let language_name = if start_language == end_language {
- start_language.map(|language| language.code_fence_block_name())
- } else {
- None
- };
- let language_name = language_name.as_deref().unwrap_or("");
-
- let filename = snapshot
- .file_at(range.start)
- .map(|file| file.full_path(cx).to_string_lossy().into_owned());
-
- let text = if language_name == "markdown" {
- selected_text
- .lines()
- .map(|line| format!("> {}", line))
- .collect::<Vec<_>>()
- .join("\n")
- } else {
- let start_symbols = snapshot
- .symbols_containing(range.start, None)
- .map(|(_, symbols)| symbols);
- let end_symbols = snapshot
- .symbols_containing(range.end, None)
- .map(|(_, symbols)| symbols);
-
- let outline_text =
- if let Some((start_symbols, end_symbols)) = start_symbols.zip(end_symbols) {
- Some(
- start_symbols
- .into_iter()
- .zip(end_symbols)
- .take_while(|(a, b)| a == b)
- .map(|(a, _)| a.text)
- .collect::<Vec<_>>()
- .join(" > "),
- )
- } else {
- None
- };
-
- let line_comment_prefix =
- start_language.and_then(|l| l.default_scope().line_comment_prefixes().first().cloned());
-
- let fence =
- codeblock_fence_for_path(filename.as_deref(), Some(start_buffer_row..=end_buffer_row));
-
- if let Some((line_comment_prefix, outline_text)) = line_comment_prefix.zip(outline_text) {
- let breadcrumb = format!("{line_comment_prefix}Excerpt from: {outline_text}\n");
- format!("{fence}{breadcrumb}{selected_text}\n```")
- } else {
- format!("{fence}{selected_text}\n```")
- }
- };
-
- let crease_title = if let Some(path) = filename {
- let start_line = start_buffer_row + 1;
- let end_line = end_buffer_row + 1;
- if start_line == end_line {
- format!("{path}, Line {start_line}")
- } else {
- format!("{path}, Lines {start_line} to {end_line}")
- }
- } else {
- "Quoted selection".to_string()
- };
-
- Some((text, crease_title))
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use gpui::TestAppContext;
- use multi_buffer::MultiBuffer;
-
- #[gpui::test]
- fn test_selections_creases_single_excerpt(cx: &mut TestAppContext) {
- let buffer = cx.update(|cx| {
- MultiBuffer::build_multi(
- [("a\nb\nc\n", vec![Point::new(0, 0)..Point::new(3, 0)])],
- cx,
- )
- });
- let creases = cx.update(|cx| {
- let snapshot = buffer.read(cx).snapshot(cx);
- selections_creases(vec![Point::new(0, 0)..Point::new(2, 1)], snapshot, cx)
- });
- assert_eq!(creases.len(), 1);
- assert_eq!(creases[0].0, "```untitled:1-3\na\nb\nc\n```");
- assert_eq!(creases[0].1, "Quoted selection");
- }
-
- #[gpui::test]
- fn test_selections_creases_spans_multiple_excerpts(cx: &mut TestAppContext) {
- let buffer = cx.update(|cx| {
- MultiBuffer::build_multi(
- [
- ("aaa\nbbb\n", vec![Point::new(0, 0)..Point::new(2, 0)]),
- ("111\n222\n", vec![Point::new(0, 0)..Point::new(2, 0)]),
- ],
- cx,
- )
- });
- let creases = cx.update(|cx| {
- let snapshot = buffer.read(cx).snapshot(cx);
- let end = snapshot.offset_to_point(snapshot.len());
- selections_creases(vec![Point::new(0, 0)..end], snapshot, cx)
- });
- assert_eq!(creases.len(), 2);
- assert!(creases[0].0.contains("aaa") && !creases[0].0.contains("111"));
- assert!(creases[1].0.contains("111") && !creases[1].0.contains("aaa"));
- }
-}
@@ -1,118 +0,0 @@
-use std::sync::Arc;
-use std::sync::atomic::AtomicBool;
-use std::time::Duration;
-
-use anyhow::Result;
-use assistant_slash_command::{
- ArgumentCompletion, SlashCommand, SlashCommandContent, SlashCommandEvent,
- SlashCommandOutputSection, SlashCommandResult,
-};
-use feature_flags::FeatureFlag;
-use futures::channel::mpsc;
-use gpui::{Task, WeakEntity};
-use language::{BufferSnapshot, LspAdapterDelegate};
-use smol::stream::StreamExt;
-use ui::prelude::*;
-use workspace::Workspace;
-
-pub struct StreamingExampleSlashCommandFeatureFlag;
-
-impl FeatureFlag for StreamingExampleSlashCommandFeatureFlag {
- const NAME: &'static str = "streaming-example-slash-command";
-}
-
-pub struct StreamingExampleSlashCommand;
-
-impl SlashCommand for StreamingExampleSlashCommand {
- fn name(&self) -> String {
- "streaming-example".into()
- }
-
- fn description(&self) -> String {
- "An example slash command that showcases streaming.".into()
- }
-
- fn menu_text(&self) -> String {
- self.description()
- }
-
- fn requires_argument(&self) -> bool {
- false
- }
-
- fn complete_argument(
- self: Arc<Self>,
- _arguments: &[String],
- _cancel: Arc<AtomicBool>,
- _workspace: Option<WeakEntity<Workspace>>,
- _window: &mut Window,
- _cx: &mut App,
- ) -> Task<Result<Vec<ArgumentCompletion>>> {
- Task::ready(Ok(Vec::new()))
- }
-
- fn run(
- self: Arc<Self>,
- _arguments: &[String],
- _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
- _context_buffer: BufferSnapshot,
- _workspace: WeakEntity<Workspace>,
- _delegate: Option<Arc<dyn LspAdapterDelegate>>,
- _: &mut Window,
- cx: &mut App,
- ) -> Task<SlashCommandResult> {
- let (events_tx, events_rx) = mpsc::unbounded();
- let executor = cx.background_executor().clone();
- cx.background_spawn(async move {
- events_tx.unbounded_send(Ok(SlashCommandEvent::StartSection {
- icon: IconName::FileRust,
- label: "Section 1".into(),
- metadata: None,
- }))?;
- events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
- SlashCommandContent::Text {
- text: "Hello".into(),
- run_commands_in_text: false,
- },
- )))?;
- events_tx.unbounded_send(Ok(SlashCommandEvent::EndSection))?;
-
- executor.timer(Duration::from_secs(1)).await;
-
- events_tx.unbounded_send(Ok(SlashCommandEvent::StartSection {
- icon: IconName::FileRust,
- label: "Section 2".into(),
- metadata: None,
- }))?;
- events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
- SlashCommandContent::Text {
- text: "World".into(),
- run_commands_in_text: false,
- },
- )))?;
- events_tx.unbounded_send(Ok(SlashCommandEvent::EndSection))?;
-
- for n in 1..=10 {
- executor.timer(Duration::from_secs(1)).await;
-
- events_tx.unbounded_send(Ok(SlashCommandEvent::StartSection {
- icon: IconName::StarFilled,
- label: format!("Section {n}").into(),
- metadata: None,
- }))?;
- events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
- SlashCommandContent::Text {
- text: "lorem ipsum ".repeat(n).trim().into(),
- run_commands_in_text: false,
- },
- )))?;
- events_tx.unbounded_send(Ok(SlashCommandEvent::EndSection))?;
- }
-
- anyhow::Ok(())
- })
- .detach_and_log_err(cx);
-
- Task::ready(Ok(events_rx.boxed()))
- }
-}
@@ -1,99 +0,0 @@
-use anyhow::{Result, anyhow};
-use assistant_slash_command::{
- ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
- SlashCommandResult,
-};
-use editor::Editor;
-use gpui::{AppContext as _, Task, WeakEntity};
-use language::{BufferSnapshot, LspAdapterDelegate};
-use std::sync::Arc;
-use std::sync::atomic::AtomicBool;
-use ui::{App, IconName, SharedString, Window};
-use workspace::Workspace;
-
-pub struct OutlineSlashCommand;
-
-impl SlashCommand for OutlineSlashCommand {
- fn name(&self) -> String {
- "symbols".into()
- }
-
- fn description(&self) -> String {
- "Insert symbols for active tab".into()
- }
-
- fn icon(&self) -> IconName {
- IconName::ListTree
- }
-
- fn menu_text(&self) -> String {
- self.description()
- }
-
- fn complete_argument(
- self: Arc<Self>,
- _arguments: &[String],
- _cancel: Arc<AtomicBool>,
- _workspace: Option<WeakEntity<Workspace>>,
- _window: &mut Window,
- _cx: &mut App,
- ) -> Task<Result<Vec<ArgumentCompletion>>> {
- Task::ready(Err(anyhow!("this command does not require argument")))
- }
-
- fn requires_argument(&self) -> bool {
- false
- }
-
- fn run(
- self: Arc<Self>,
- _arguments: &[String],
- _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
- _context_buffer: BufferSnapshot,
- workspace: WeakEntity<Workspace>,
- _delegate: Option<Arc<dyn LspAdapterDelegate>>,
- _: &mut Window,
- cx: &mut App,
- ) -> Task<SlashCommandResult> {
- let output = workspace.update(cx, |workspace, cx| {
- let Some(active_item) = workspace.active_item(cx) else {
- return Task::ready(Err(anyhow!("no active tab")));
- };
- let Some(buffer) = active_item
- .downcast::<Editor>()
- .and_then(|editor| editor.read(cx).buffer().read(cx).as_singleton())
- else {
- return Task::ready(Err(anyhow!("active tab is not an editor")));
- };
-
- let snapshot = buffer.read(cx).snapshot();
- let path = snapshot.resolve_file_path(true, cx);
-
- cx.background_spawn(async move {
- let outline = snapshot.outline(None);
-
- let path = path.as_deref().unwrap_or("untitled");
- let mut outline_text = format!("Symbols for {path}:\n");
- for item in &outline.path_candidates {
- outline_text.push_str("- ");
- outline_text.push_str(&item.string);
- outline_text.push('\n');
- }
-
- Ok(SlashCommandOutput {
- sections: vec![SlashCommandOutputSection {
- range: 0..outline_text.len(),
- icon: IconName::ListTree,
- label: SharedString::new(path),
- metadata: None,
- }],
- text: outline_text,
- run_commands_in_text: false,
- }
- .into_event_stream())
- })
- });
-
- output.unwrap_or_else(|error| Task::ready(Err(error)))
- }
-}
@@ -1,317 +0,0 @@
-use anyhow::{Context as _, Result};
-use assistant_slash_command::{
- ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
- SlashCommandResult,
-};
-use collections::{HashMap, HashSet};
-use editor::Editor;
-use futures::future::join_all;
-use gpui::{Task, WeakEntity};
-use language::{BufferSnapshot, CodeLabel, CodeLabelBuilder, HighlightId, LspAdapterDelegate};
-use std::sync::{Arc, atomic::AtomicBool};
-use ui::{ActiveTheme, App, Window, prelude::*};
-use util::{ResultExt, paths::PathStyle};
-use workspace::Workspace;
-
-use crate::file_command::append_buffer_to_output;
-
-pub struct TabSlashCommand;
-
-const ALL_TABS_COMPLETION_ITEM: &str = "all";
-
-impl SlashCommand for TabSlashCommand {
- fn name(&self) -> String {
- "tab".into()
- }
-
- fn description(&self) -> String {
- "Insert open tabs (active tab by default)".to_owned()
- }
-
- fn icon(&self) -> IconName {
- IconName::FileTree
- }
-
- fn menu_text(&self) -> String {
- self.description()
- }
-
- fn requires_argument(&self) -> bool {
- false
- }
-
- fn accepts_arguments(&self) -> bool {
- true
- }
-
- fn complete_argument(
- self: Arc<Self>,
- arguments: &[String],
- cancel: Arc<AtomicBool>,
- workspace: Option<WeakEntity<Workspace>>,
- window: &mut Window,
- cx: &mut App,
- ) -> Task<Result<Vec<ArgumentCompletion>>> {
- let mut has_all_tabs_completion_item = false;
- let argument_set = arguments
- .iter()
- .filter(|argument| {
- if has_all_tabs_completion_item || ALL_TABS_COMPLETION_ITEM == argument.as_str() {
- has_all_tabs_completion_item = true;
- false
- } else {
- true
- }
- })
- .cloned()
- .collect::<HashSet<_>>();
- if has_all_tabs_completion_item {
- return Task::ready(Ok(Vec::new()));
- }
-
- let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
- return Task::ready(Err(anyhow::anyhow!("no workspace")));
- };
-
- let active_item_path = workspace.update(cx, |workspace, cx| {
- let snapshot = active_item_buffer(workspace, cx).ok()?;
- snapshot.resolve_file_path(true, cx)
- });
- let path_style = workspace.read(cx).path_style(cx);
-
- let current_query = arguments.last().cloned().unwrap_or_default();
- let tab_items_search = tab_items_for_queries(
- Some(workspace.downgrade()),
- &[current_query],
- cancel,
- false,
- window,
- cx,
- );
-
- let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
- window.spawn(cx, async move |_| {
- let tab_items = tab_items_search.await?;
- let run_command = tab_items.len() == 1;
- let tab_completion_items = tab_items.into_iter().filter_map(|(path, ..)| {
- let path = path?;
- if argument_set.contains(&path) {
- return None;
- }
- if active_item_path.as_ref() == Some(&path) {
- return None;
- }
- let label = create_tab_completion_label(&path, path_style, comment_id);
- Some(ArgumentCompletion {
- label,
- new_text: path,
- replace_previous_arguments: false,
- after_completion: run_command.into(),
- })
- });
-
- let active_item_completion = active_item_path
- .as_deref()
- .map(|active_item_path| {
- let path_string = active_item_path.to_string();
- let label =
- create_tab_completion_label(active_item_path, path_style, comment_id);
- ArgumentCompletion {
- label,
- new_text: path_string,
- replace_previous_arguments: false,
- after_completion: run_command.into(),
- }
- })
- .filter(|completion| !argument_set.contains(&completion.new_text));
-
- Ok(active_item_completion
- .into_iter()
- .chain(Some(ArgumentCompletion {
- label: ALL_TABS_COMPLETION_ITEM.into(),
- new_text: ALL_TABS_COMPLETION_ITEM.to_owned(),
- replace_previous_arguments: false,
- after_completion: true.into(),
- }))
- .chain(tab_completion_items)
- .collect())
- })
- }
-
- fn run(
- self: Arc<Self>,
- arguments: &[String],
- _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
- _context_buffer: BufferSnapshot,
- workspace: WeakEntity<Workspace>,
- _delegate: Option<Arc<dyn LspAdapterDelegate>>,
- window: &mut Window,
- cx: &mut App,
- ) -> Task<SlashCommandResult> {
- let tab_items_search = tab_items_for_queries(
- Some(workspace),
- arguments,
- Arc::new(AtomicBool::new(false)),
- true,
- window,
- cx,
- );
-
- cx.background_spawn(async move {
- let mut output = SlashCommandOutput::default();
- for (full_path, buffer, _) in tab_items_search.await? {
- append_buffer_to_output(&buffer, full_path.as_deref(), &mut output).log_err();
- }
- Ok(output.into_event_stream())
- })
- }
-}
-
-fn tab_items_for_queries(
- workspace: Option<WeakEntity<Workspace>>,
- queries: &[String],
- cancel: Arc<AtomicBool>,
- strict_match: bool,
- window: &mut Window,
- cx: &mut App,
-) -> Task<anyhow::Result<Vec<(Option<String>, BufferSnapshot, usize)>>> {
- let empty_query = queries.is_empty() || queries.iter().all(|query| query.trim().is_empty());
- let queries = queries.to_owned();
- window.spawn(cx, async move |cx| {
- let mut open_buffers =
- workspace
- .context("no workspace")?
- .update(cx, |workspace, cx| {
- if strict_match && empty_query {
- let snapshot = active_item_buffer(workspace, cx)?;
- let full_path = snapshot.resolve_file_path(true, cx);
- return anyhow::Ok(vec![(full_path, snapshot, 0)]);
- }
-
- let mut timestamps_by_entity_id = HashMap::default();
- let mut visited_buffers = HashSet::default();
- let mut open_buffers = Vec::new();
-
- for pane in workspace.panes() {
- let pane = pane.read(cx);
- for entry in pane.activation_history() {
- timestamps_by_entity_id.insert(entry.entity_id, entry.timestamp);
- }
- }
-
- for editor in workspace.items_of_type::<Editor>(cx) {
- if let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton()
- && let Some(timestamp) =
- timestamps_by_entity_id.get(&editor.entity_id())
- && visited_buffers.insert(buffer.read(cx).remote_id())
- {
- let snapshot = buffer.read(cx).snapshot();
- let full_path = snapshot.resolve_file_path(true, cx);
- open_buffers.push((full_path, snapshot, *timestamp));
- }
- }
-
- Ok(open_buffers)
- })??;
-
- let background_executor = cx.background_executor().clone();
- cx.background_spawn(async move {
- open_buffers.sort_by_key(|(_, _, timestamp)| *timestamp);
- if empty_query
- || queries
- .iter()
- .any(|query| query == ALL_TABS_COMPLETION_ITEM)
- {
- return Ok(open_buffers);
- }
-
- let matched_items = if strict_match {
- let match_candidates = open_buffers
- .iter()
- .enumerate()
- .filter_map(|(id, (full_path, ..))| Some((id, full_path.clone()?)))
- .fold(HashMap::default(), |mut candidates, (id, path_string)| {
- candidates
- .entry(path_string)
- .or_insert_with(Vec::new)
- .push(id);
- candidates
- });
-
- queries
- .iter()
- .filter_map(|query| match_candidates.get(query))
- .flatten()
- .copied()
- .filter_map(|id| open_buffers.get(id))
- .cloned()
- .collect()
- } else {
- let match_candidates = open_buffers
- .iter()
- .enumerate()
- .filter_map(|(id, (full_path, ..))| {
- Some(fuzzy::StringMatchCandidate::new(id, full_path.as_ref()?))
- })
- .collect::<Vec<_>>();
- let mut processed_matches = HashSet::default();
- let file_queries = queries.iter().map(|query| {
- fuzzy::match_strings(
- &match_candidates,
- query,
- true,
- true,
- usize::MAX,
- &cancel,
- background_executor.clone(),
- )
- });
-
- join_all(file_queries)
- .await
- .into_iter()
- .flatten()
- .filter(|string_match| processed_matches.insert(string_match.candidate_id))
- .filter_map(|string_match| open_buffers.get(string_match.candidate_id))
- .cloned()
- .collect()
- };
- Ok(matched_items)
- })
- .await
- })
-}
-
-fn active_item_buffer(
- workspace: &mut Workspace,
- cx: &mut Context<Workspace>,
-) -> anyhow::Result<BufferSnapshot> {
- let active_editor = workspace
- .active_item(cx)
- .context("no active item")?
- .downcast::<Editor>()
- .context("active item is not an editor")?;
- let snapshot = active_editor
- .read(cx)
- .buffer()
- .read(cx)
- .as_singleton()
- .context("active editor is not a singleton buffer")?
- .read(cx)
- .snapshot();
- Ok(snapshot)
-}
-
-fn create_tab_completion_label(
- path: &str,
- path_style: PathStyle,
- comment_id: Option<HighlightId>,
-) -> CodeLabel {
- let (parent_path, file_name) = path_style.split(path);
- let mut label = CodeLabelBuilder::default();
- label.push_str(file_name, None);
- label.push_str(" ", None);
- label.push_str(parent_path.unwrap_or_default(), comment_id);
- label.respan_filter_range(Some(file_name));
- label.build()
-}
@@ -1,62 +0,0 @@
-[package]
-name = "assistant_text_thread"
-version = "0.1.0"
-edition.workspace = true
-publish.workspace = true
-license = "GPL-3.0-or-later"
-
-[lints]
-workspace = true
-
-[lib]
-path = "src/assistant_text_thread.rs"
-
-[features]
-test-support = []
-
-[dependencies]
-agent_settings.workspace = true
-anyhow.workspace = true
-assistant_slash_command.workspace = true
-chrono.workspace = true
-client.workspace = true
-clock.workspace = true
-collections.workspace = true
-context_server.workspace = true
-fs.workspace = true
-futures.workspace = true
-fuzzy.workspace = true
-gpui.workspace = true
-itertools.workspace = true
-language.workspace = true
-language_model.workspace = true
-log.workspace = true
-open_ai.workspace = true
-parking_lot.workspace = true
-paths.workspace = true
-project.workspace = true
-prompt_store.workspace = true
-proto.workspace = true
-regex.workspace = true
-rpc.workspace = true
-serde.workspace = true
-serde_json.workspace = true
-settings.workspace = true
-smallvec.workspace = true
-smol.workspace = true
-telemetry.workspace = true
-text.workspace = true
-ui.workspace = true
-util.workspace = true
-uuid.workspace = true
-workspace.workspace = true
-zed_env_vars.workspace = true
-
-[dev-dependencies]
-assistant_slash_commands.workspace = true
-
-language_model = { workspace = true, features = ["test-support"] }
-pretty_assertions.workspace = true
-rand.workspace = true
-unindent.workspace = true
-workspace = { workspace = true, features = ["test-support"] }
@@ -1 +0,0 @@
-../../LICENSE-GPL
@@ -1,16 +0,0 @@
-#[cfg(test)]
-mod assistant_text_thread_tests;
-mod context_server_command;
-mod text_thread;
-mod text_thread_store;
-
-pub use crate::text_thread::*;
-pub use crate::text_thread_store::*;
-
-use client::Client;
-use gpui::App;
-use std::sync::Arc;
-
-pub fn init(client: Arc<Client>, _: &mut App) {
- text_thread_store::init(&client.into());
-}
@@ -1,1444 +0,0 @@
-use crate::{
- CacheStatus, InvokedSlashCommandId, MessageCacheMetadata, MessageId, MessageStatus, TextThread,
- TextThreadEvent, TextThreadId, TextThreadOperation, TextThreadSummary,
-};
-use anyhow::Result;
-use assistant_slash_command::{
- ArgumentCompletion, SlashCommand, SlashCommandContent, SlashCommandEvent, SlashCommandOutput,
- SlashCommandOutputSection, SlashCommandRegistry, SlashCommandResult, SlashCommandWorkingSet,
-};
-use assistant_slash_commands::FileSlashCommand;
-use collections::{HashMap, HashSet};
-use fs::FakeFs;
-use futures::{
- channel::mpsc,
- stream::{self, StreamExt},
-};
-use gpui::{App, Entity, SharedString, Task, TestAppContext, WeakEntity, prelude::*};
-use language::{Buffer, BufferSnapshot, LanguageRegistry, LspAdapterDelegate};
-use language_model::{
- ConfiguredModel, LanguageModelCacheConfiguration, LanguageModelRegistry, Role,
- fake_provider::{FakeLanguageModel, FakeLanguageModelProvider},
-};
-use parking_lot::Mutex;
-use pretty_assertions::assert_eq;
-use prompt_store::PromptBuilder;
-use rand::prelude::*;
-use serde_json::json;
-use settings::SettingsStore;
-use std::{
- cell::RefCell,
- env,
- ops::Range,
- path::Path,
- rc::Rc,
- sync::{Arc, atomic::AtomicBool},
-};
-use text::{ReplicaId, ToOffset, network::Network};
-use ui::{IconName, Window};
-use unindent::Unindent;
-use util::RandomCharIter;
-use workspace::Workspace;
-
-#[gpui::test]
-fn test_inserting_and_removing_messages(cx: &mut App) {
- init_test(cx);
-
- let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
- let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
- let text_thread = cx.new(|cx| {
- TextThread::local(
- registry,
- prompt_builder.clone(),
- Arc::new(SlashCommandWorkingSet::default()),
- cx,
- )
- });
- let buffer = text_thread.read(cx).buffer().clone();
-
- let message_1 = text_thread.read(cx).message_anchors[0].clone();
- assert_eq!(
- messages(&text_thread, cx),
- vec![(message_1.id, Role::User, 0..0)]
- );
-
- let message_2 = text_thread.update(cx, |context, cx| {
- context
- .insert_message_after(message_1.id, Role::Assistant, MessageStatus::Done, cx)
- .unwrap()
- });
- assert_eq!(
- messages(&text_thread, cx),
- vec![
- (message_1.id, Role::User, 0..1),
- (message_2.id, Role::Assistant, 1..1)
- ]
- );
-
- buffer.update(cx, |buffer, cx| {
- buffer.edit([(0..0, "1"), (1..1, "2")], None, cx)
- });
- assert_eq!(
- messages(&text_thread, cx),
- vec![
- (message_1.id, Role::User, 0..2),
- (message_2.id, Role::Assistant, 2..3)
- ]
- );
-
- let message_3 = text_thread.update(cx, |context, cx| {
- context
- .insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
- .unwrap()
- });
- assert_eq!(
- messages(&text_thread, cx),
- vec![
- (message_1.id, Role::User, 0..2),
- (message_2.id, Role::Assistant, 2..4),
- (message_3.id, Role::User, 4..4)
- ]
- );
-
- let message_4 = text_thread.update(cx, |context, cx| {
- context
- .insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
- .unwrap()
- });
- assert_eq!(
- messages(&text_thread, cx),
- vec![
- (message_1.id, Role::User, 0..2),
- (message_2.id, Role::Assistant, 2..4),
- (message_4.id, Role::User, 4..5),
- (message_3.id, Role::User, 5..5),
- ]
- );
-
- buffer.update(cx, |buffer, cx| {
- buffer.edit([(4..4, "C"), (5..5, "D")], None, cx)
- });
- assert_eq!(
- messages(&text_thread, cx),
- vec![
- (message_1.id, Role::User, 0..2),
- (message_2.id, Role::Assistant, 2..4),
- (message_4.id, Role::User, 4..6),
- (message_3.id, Role::User, 6..7),
- ]
- );
-
- // Deleting across message boundaries merges the messages.
- buffer.update(cx, |buffer, cx| buffer.edit([(1..4, "")], None, cx));
- assert_eq!(
- messages(&text_thread, cx),
- vec![
- (message_1.id, Role::User, 0..3),
- (message_3.id, Role::User, 3..4),
- ]
- );
-
- // Undoing the deletion should also undo the merge.
- buffer.update(cx, |buffer, cx| buffer.undo(cx));
- assert_eq!(
- messages(&text_thread, cx),
- vec![
- (message_1.id, Role::User, 0..2),
- (message_2.id, Role::Assistant, 2..4),
- (message_4.id, Role::User, 4..6),
- (message_3.id, Role::User, 6..7),
- ]
- );
-
- // Redoing the deletion should also redo the merge.
- buffer.update(cx, |buffer, cx| buffer.redo(cx));
- assert_eq!(
- messages(&text_thread, cx),
- vec![
- (message_1.id, Role::User, 0..3),
- (message_3.id, Role::User, 3..4),
- ]
- );
-
- // Ensure we can still insert after a merged message.
- let message_5 = text_thread.update(cx, |context, cx| {
- context
- .insert_message_after(message_1.id, Role::System, MessageStatus::Done, cx)
- .unwrap()
- });
- assert_eq!(
- messages(&text_thread, cx),
- vec![
- (message_1.id, Role::User, 0..3),
- (message_5.id, Role::System, 3..4),
- (message_3.id, Role::User, 4..5)
- ]
- );
-}
-
-#[gpui::test]
-fn test_message_splitting(cx: &mut App) {
- init_test(cx);
-
- let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
-
- let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
- let text_thread = cx.new(|cx| {
- TextThread::local(
- registry.clone(),
- prompt_builder.clone(),
- Arc::new(SlashCommandWorkingSet::default()),
- cx,
- )
- });
- let buffer = text_thread.read(cx).buffer().clone();
-
- let message_1 = text_thread.read(cx).message_anchors[0].clone();
- assert_eq!(
- messages(&text_thread, cx),
- vec![(message_1.id, Role::User, 0..0)]
- );
-
- buffer.update(cx, |buffer, cx| {
- buffer.edit([(0..0, "aaa\nbbb\nccc\nddd\n")], None, cx)
- });
-
- let (_, message_2) =
- text_thread.update(cx, |text_thread, cx| text_thread.split_message(3..3, cx));
- let message_2 = message_2.unwrap();
-
- // We recycle newlines in the middle of a split message
- assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc\nddd\n");
- assert_eq!(
- messages(&text_thread, cx),
- vec![
- (message_1.id, Role::User, 0..4),
- (message_2.id, Role::User, 4..16),
- ]
- );
-
- let (_, message_3) =
- text_thread.update(cx, |text_thread, cx| text_thread.split_message(3..3, cx));
- let message_3 = message_3.unwrap();
-
- // We don't recycle newlines at the end of a split message
- assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\nccc\nddd\n");
- assert_eq!(
- messages(&text_thread, cx),
- vec![
- (message_1.id, Role::User, 0..4),
- (message_3.id, Role::User, 4..5),
- (message_2.id, Role::User, 5..17),
- ]
- );
-
- let (_, message_4) =
- text_thread.update(cx, |text_thread, cx| text_thread.split_message(9..9, cx));
- let message_4 = message_4.unwrap();
- assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\nccc\nddd\n");
- assert_eq!(
- messages(&text_thread, cx),
- vec![
- (message_1.id, Role::User, 0..4),
- (message_3.id, Role::User, 4..5),
- (message_2.id, Role::User, 5..9),
- (message_4.id, Role::User, 9..17),
- ]
- );
-
- let (_, message_5) =
- text_thread.update(cx, |text_thread, cx| text_thread.split_message(9..9, cx));
- let message_5 = message_5.unwrap();
- assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\nddd\n");
- assert_eq!(
- messages(&text_thread, cx),
- vec![
- (message_1.id, Role::User, 0..4),
- (message_3.id, Role::User, 4..5),
- (message_2.id, Role::User, 5..9),
- (message_4.id, Role::User, 9..10),
- (message_5.id, Role::User, 10..18),
- ]
- );
-
- let (message_6, message_7) =
- text_thread.update(cx, |text_thread, cx| text_thread.split_message(14..16, cx));
- let message_6 = message_6.unwrap();
- let message_7 = message_7.unwrap();
- assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\ndd\nd\n");
- assert_eq!(
- messages(&text_thread, cx),
- vec![
- (message_1.id, Role::User, 0..4),
- (message_3.id, Role::User, 4..5),
- (message_2.id, Role::User, 5..9),
- (message_4.id, Role::User, 9..10),
- (message_5.id, Role::User, 10..14),
- (message_6.id, Role::User, 14..17),
- (message_7.id, Role::User, 17..19),
- ]
- );
-}
-
-#[gpui::test]
-fn test_messages_for_offsets(cx: &mut App) {
- init_test(cx);
-
- let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
- let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
- let text_thread = cx.new(|cx| {
- TextThread::local(
- registry,
- prompt_builder.clone(),
- Arc::new(SlashCommandWorkingSet::default()),
- cx,
- )
- });
- let buffer = text_thread.read(cx).buffer().clone();
-
- let message_1 = text_thread.read(cx).message_anchors[0].clone();
- assert_eq!(
- messages(&text_thread, cx),
- vec![(message_1.id, Role::User, 0..0)]
- );
-
- buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "aaa")], None, cx));
- let message_2 = text_thread
- .update(cx, |text_thread, cx| {
- text_thread.insert_message_after(message_1.id, Role::User, MessageStatus::Done, cx)
- })
- .unwrap();
- buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "bbb")], None, cx));
-
- let message_3 = text_thread
- .update(cx, |text_thread, cx| {
- text_thread.insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
- })
- .unwrap();
- buffer.update(cx, |buffer, cx| buffer.edit([(8..8, "ccc")], None, cx));
-
- assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc");
- assert_eq!(
- messages(&text_thread, cx),
- vec![
- (message_1.id, Role::User, 0..4),
- (message_2.id, Role::User, 4..8),
- (message_3.id, Role::User, 8..11)
- ]
- );
-
- assert_eq!(
- message_ids_for_offsets(&text_thread, &[0, 4, 9], cx),
- [message_1.id, message_2.id, message_3.id]
- );
- assert_eq!(
- message_ids_for_offsets(&text_thread, &[0, 1, 11], cx),
- [message_1.id, message_3.id]
- );
-
- let message_4 = text_thread
- .update(cx, |text_thread, cx| {
- text_thread.insert_message_after(message_3.id, Role::User, MessageStatus::Done, cx)
- })
- .unwrap();
- assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc\n");
- assert_eq!(
- messages(&text_thread, cx),
- vec![
- (message_1.id, Role::User, 0..4),
- (message_2.id, Role::User, 4..8),
- (message_3.id, Role::User, 8..12),
- (message_4.id, Role::User, 12..12)
- ]
- );
- assert_eq!(
- message_ids_for_offsets(&text_thread, &[0, 4, 8, 12], cx),
- [message_1.id, message_2.id, message_3.id, message_4.id]
- );
-
- fn message_ids_for_offsets(
- context: &Entity<TextThread>,
- offsets: &[usize],
- cx: &App,
- ) -> Vec<MessageId> {
- context
- .read(cx)
- .messages_for_offsets(offsets.iter().copied(), cx)
- .into_iter()
- .map(|message| message.id)
- .collect()
- }
-}
-
-#[gpui::test]
-async fn test_slash_commands(cx: &mut TestAppContext) {
- cx.update(init_test);
-
- let fs = FakeFs::new(cx.background_executor.clone());
-
- fs.insert_tree(
- "/test",
- json!({
- "src": {
- "lib.rs": "fn one() -> usize { 1 }",
- "main.rs": "
- use crate::one;
- fn main() { one(); }
- ".unindent(),
- }
- }),
- )
- .await;
-
- let slash_command_registry = cx.update(SlashCommandRegistry::default_global);
- slash_command_registry.register_command(FileSlashCommand, false);
-
- let registry = Arc::new(LanguageRegistry::test(cx.executor()));
- let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
- let text_thread = cx.new(|cx| {
- TextThread::local(
- registry.clone(),
- prompt_builder.clone(),
- Arc::new(SlashCommandWorkingSet::default()),
- cx,
- )
- });
-
- #[derive(Default)]
- struct ContextRanges {
- parsed_commands: HashSet<Range<language::Anchor>>,
- command_outputs: HashMap<InvokedSlashCommandId, Range<language::Anchor>>,
- output_sections: HashSet<Range<language::Anchor>>,
- }
-
- let context_ranges = Rc::new(RefCell::new(ContextRanges::default()));
- text_thread.update(cx, |_, cx| {
- cx.subscribe(&text_thread, {
- let context_ranges = context_ranges.clone();
- move |text_thread, _, event, _| {
- let mut context_ranges = context_ranges.borrow_mut();
- match event {
- TextThreadEvent::InvokedSlashCommandChanged { command_id } => {
- let command = text_thread.invoked_slash_command(command_id).unwrap();
- context_ranges
- .command_outputs
- .insert(*command_id, command.range.clone());
- }
- TextThreadEvent::ParsedSlashCommandsUpdated { removed, updated } => {
- for range in removed {
- context_ranges.parsed_commands.remove(range);
- }
- for command in updated {
- context_ranges
- .parsed_commands
- .insert(command.source_range.clone());
- }
- }
- TextThreadEvent::SlashCommandOutputSectionAdded { section } => {
- context_ranges.output_sections.insert(section.range.clone());
- }
- _ => {}
- }
- }
- })
- .detach();
- });
-
- let buffer = text_thread.read_with(cx, |text_thread, _| text_thread.buffer().clone());
-
- // Insert a slash command
- buffer.update(cx, |buffer, cx| {
- buffer.edit([(0..0, "/file src/lib.rs")], None, cx);
- });
- assert_text_and_context_ranges(
- &buffer,
- &context_ranges,
- &"
- Β«/file src/lib.rsΒ»"
- .unindent(),
- cx,
- );
-
- // Edit the argument of the slash command.
- buffer.update(cx, |buffer, cx| {
- let edit_offset = buffer.text().find("lib.rs").unwrap();
- buffer.edit([(edit_offset..edit_offset + "lib".len(), "main")], None, cx);
- });
- assert_text_and_context_ranges(
- &buffer,
- &context_ranges,
- &"
- Β«/file src/main.rsΒ»"
- .unindent(),
- cx,
- );
-
- // Edit the name of the slash command, using one that doesn't exist.
- buffer.update(cx, |buffer, cx| {
- let edit_offset = buffer.text().find("/file").unwrap();
- buffer.edit(
- [(edit_offset..edit_offset + "/file".len(), "/unknown")],
- None,
- cx,
- );
- });
- assert_text_and_context_ranges(
- &buffer,
- &context_ranges,
- &"
- /unknown src/main.rs"
- .unindent(),
- cx,
- );
-
- // Undoing the insertion of an non-existent slash command resorts the previous one.
- buffer.update(cx, |buffer, cx| buffer.undo(cx));
- assert_text_and_context_ranges(
- &buffer,
- &context_ranges,
- &"
- Β«/file src/main.rsΒ»"
- .unindent(),
- cx,
- );
-
- let (command_output_tx, command_output_rx) = mpsc::unbounded();
- text_thread.update(cx, |text_thread, cx| {
- let command_source_range = text_thread.parsed_slash_commands[0].source_range.clone();
- text_thread.insert_command_output(
- command_source_range,
- "file",
- Task::ready(Ok(command_output_rx.boxed())),
- true,
- cx,
- );
- });
- assert_text_and_context_ranges(
- &buffer,
- &context_ranges,
- &"
- β¦Β«/file src/main.rsΒ»
- β¦β§
- "
- .unindent(),
- cx,
- );
-
- command_output_tx
- .unbounded_send(Ok(SlashCommandEvent::StartSection {
- icon: IconName::ZedAgent,
- label: "src/main.rs".into(),
- metadata: None,
- }))
- .unwrap();
- command_output_tx
- .unbounded_send(Ok(SlashCommandEvent::Content("src/main.rs".into())))
- .unwrap();
- cx.run_until_parked();
- assert_text_and_context_ranges(
- &buffer,
- &context_ranges,
- &"
- β¦Β«/file src/main.rsΒ»
- src/main.rsβ¦β§
- "
- .unindent(),
- cx,
- );
-
- command_output_tx
- .unbounded_send(Ok(SlashCommandEvent::Content("\nfn main() {}".into())))
- .unwrap();
- cx.run_until_parked();
- assert_text_and_context_ranges(
- &buffer,
- &context_ranges,
- &"
- β¦Β«/file src/main.rsΒ»
- src/main.rs
- fn main() {}β¦β§
- "
- .unindent(),
- cx,
- );
-
- command_output_tx
- .unbounded_send(Ok(SlashCommandEvent::EndSection))
- .unwrap();
- cx.run_until_parked();
- assert_text_and_context_ranges(
- &buffer,
- &context_ranges,
- &"
- β¦Β«/file src/main.rsΒ»
- βͺsrc/main.rs
- fn main() {}β«β¦β§
- "
- .unindent(),
- cx,
- );
-
- drop(command_output_tx);
- cx.run_until_parked();
- assert_text_and_context_ranges(
- &buffer,
- &context_ranges,
- &"
- β¦βͺsrc/main.rs
- fn main() {}β«β§
- "
- .unindent(),
- cx,
- );
-
- #[track_caller]
- fn assert_text_and_context_ranges(
- buffer: &Entity<Buffer>,
- ranges: &RefCell<ContextRanges>,
- expected_marked_text: &str,
- cx: &mut TestAppContext,
- ) {
- let mut actual_marked_text = String::new();
- buffer.update(cx, |buffer, _| {
- struct Endpoint {
- offset: usize,
- marker: char,
- }
-
- let ranges = ranges.borrow();
- let mut endpoints = Vec::new();
- for range in ranges.command_outputs.values() {
- endpoints.push(Endpoint {
- offset: range.start.to_offset(buffer),
- marker: 'β¦',
- });
- }
- for range in ranges.parsed_commands.iter() {
- endpoints.push(Endpoint {
- offset: range.start.to_offset(buffer),
- marker: 'Β«',
- });
- }
- for range in ranges.output_sections.iter() {
- endpoints.push(Endpoint {
- offset: range.start.to_offset(buffer),
- marker: 'βͺ',
- });
- }
-
- for range in ranges.output_sections.iter() {
- endpoints.push(Endpoint {
- offset: range.end.to_offset(buffer),
- marker: 'β«',
- });
- }
- for range in ranges.parsed_commands.iter() {
- endpoints.push(Endpoint {
- offset: range.end.to_offset(buffer),
- marker: 'Β»',
- });
- }
- for range in ranges.command_outputs.values() {
- endpoints.push(Endpoint {
- offset: range.end.to_offset(buffer),
- marker: 'β§',
- });
- }
-
- endpoints.sort_by_key(|endpoint| endpoint.offset);
- let mut offset = 0;
- for endpoint in endpoints {
- actual_marked_text.extend(buffer.text_for_range(offset..endpoint.offset));
- actual_marked_text.push(endpoint.marker);
- offset = endpoint.offset;
- }
- actual_marked_text.extend(buffer.text_for_range(offset..buffer.len()));
- });
-
- assert_eq!(actual_marked_text, expected_marked_text);
- }
-}
-
-#[gpui::test]
-async fn test_serialization(cx: &mut TestAppContext) {
- cx.update(init_test);
-
- let registry = Arc::new(LanguageRegistry::test(cx.executor()));
- let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
- let text_thread = cx.new(|cx| {
- TextThread::local(
- registry.clone(),
- prompt_builder.clone(),
- Arc::new(SlashCommandWorkingSet::default()),
- cx,
- )
- });
- let buffer = text_thread.read_with(cx, |text_thread, _| text_thread.buffer().clone());
- let message_0 = text_thread.read_with(cx, |text_thread, _| text_thread.message_anchors[0].id);
- let message_1 = text_thread.update(cx, |text_thread, cx| {
- text_thread
- .insert_message_after(message_0, Role::Assistant, MessageStatus::Done, cx)
- .unwrap()
- });
- let message_2 = text_thread.update(cx, |text_thread, cx| {
- text_thread
- .insert_message_after(message_1.id, Role::System, MessageStatus::Done, cx)
- .unwrap()
- });
- buffer.update(cx, |buffer, cx| {
- buffer.edit([(0..0, "a"), (1..1, "b\nc")], None, cx);
- buffer.finalize_last_transaction();
- });
- let _message_3 = text_thread.update(cx, |text_thread, cx| {
- text_thread
- .insert_message_after(message_2.id, Role::System, MessageStatus::Done, cx)
- .unwrap()
- });
- buffer.update(cx, |buffer, cx| buffer.undo(cx));
- assert_eq!(buffer.read_with(cx, |buffer, _| buffer.text()), "a\nb\nc\n");
- assert_eq!(
- cx.read(|cx| messages(&text_thread, cx)),
- [
- (message_0, Role::User, 0..2),
- (message_1.id, Role::Assistant, 2..6),
- (message_2.id, Role::System, 6..6),
- ]
- );
-
- let serialized_context = text_thread.read_with(cx, |text_thread, cx| text_thread.serialize(cx));
- let deserialized_context = cx.new(|cx| {
- TextThread::deserialize(
- serialized_context,
- Path::new("").into(),
- registry.clone(),
- prompt_builder.clone(),
- Arc::new(SlashCommandWorkingSet::default()),
- cx,
- )
- });
- let deserialized_buffer =
- deserialized_context.read_with(cx, |text_thread, _| text_thread.buffer().clone());
- assert_eq!(
- deserialized_buffer.read_with(cx, |buffer, _| buffer.text()),
- "a\nb\nc\n"
- );
- assert_eq!(
- cx.read(|cx| messages(&deserialized_context, cx)),
- [
- (message_0, Role::User, 0..2),
- (message_1.id, Role::Assistant, 2..6),
- (message_2.id, Role::System, 6..6),
- ]
- );
-}
-
-#[gpui::test(iterations = 25)]
-async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: StdRng) {
- cx.update(init_test);
-
- let min_peers = env::var("MIN_PEERS")
- .map(|i| i.parse().expect("invalid `MIN_PEERS` variable"))
- .unwrap_or(2);
- let max_peers = env::var("MAX_PEERS")
- .map(|i| i.parse().expect("invalid `MAX_PEERS` variable"))
- .unwrap_or(5);
- let operations = env::var("OPERATIONS")
- .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
- .unwrap_or(50);
-
- let slash_commands = cx.update(SlashCommandRegistry::default_global);
- slash_commands.register_command(FakeSlashCommand("cmd-1".into()), false);
- slash_commands.register_command(FakeSlashCommand("cmd-2".into()), false);
- slash_commands.register_command(FakeSlashCommand("cmd-3".into()), false);
-
- let registry = Arc::new(LanguageRegistry::test(cx.background_executor.clone()));
- let network = Arc::new(Mutex::new(Network::new(rng.clone())));
- let mut text_threads = Vec::new();
-
- let num_peers = rng.random_range(min_peers..=max_peers);
- let context_id = TextThreadId::new();
- let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
- for i in 0..num_peers {
- let context = cx.new(|cx| {
- TextThread::new(
- context_id.clone(),
- ReplicaId::new(i as u16),
- language::Capability::ReadWrite,
- registry.clone(),
- prompt_builder.clone(),
- Arc::new(SlashCommandWorkingSet::default()),
- cx,
- )
- });
-
- cx.update(|cx| {
- cx.subscribe(&context, {
- let network = network.clone();
- move |_, event, _| {
- if let TextThreadEvent::Operation(op) = event {
- network
- .lock()
- .broadcast(ReplicaId::new(i as u16), vec![op.to_proto()]);
- }
- }
- })
- .detach();
- });
-
- text_threads.push(context);
- network.lock().add_peer(ReplicaId::new(i as u16));
- }
-
- let mut mutation_count = operations;
-
- while mutation_count > 0
- || !network.lock().is_idle()
- || network.lock().contains_disconnected_peers()
- {
- let context_index = rng.random_range(0..text_threads.len());
- let text_thread = &text_threads[context_index];
-
- match rng.random_range(0..100) {
- 0..=29 if mutation_count > 0 => {
- log::info!("Context {}: edit buffer", context_index);
- text_thread.update(cx, |text_thread, cx| {
- text_thread
- .buffer()
- .update(cx, |buffer, cx| buffer.randomly_edit(&mut rng, 1, cx));
- });
- mutation_count -= 1;
- }
- 30..=44 if mutation_count > 0 => {
- text_thread.update(cx, |text_thread, cx| {
- let range = text_thread.buffer().read(cx).random_byte_range(0, &mut rng);
- log::info!("Context {}: split message at {:?}", context_index, range);
- text_thread.split_message(range, cx);
- });
- mutation_count -= 1;
- }
- 45..=59 if mutation_count > 0 => {
- text_thread.update(cx, |text_thread, cx| {
- if let Some(message) = text_thread.messages(cx).choose(&mut rng) {
- let role = *[Role::User, Role::Assistant, Role::System]
- .choose(&mut rng)
- .unwrap();
- log::info!(
- "Context {}: insert message after {:?} with {:?}",
- context_index,
- message.id,
- role
- );
- text_thread.insert_message_after(message.id, role, MessageStatus::Done, cx);
- }
- });
- mutation_count -= 1;
- }
- 60..=74 if mutation_count > 0 => {
- text_thread.update(cx, |text_thread, cx| {
- let command_text = "/".to_string()
- + slash_commands
- .command_names()
- .choose(&mut rng)
- .unwrap()
- .clone()
- .as_ref();
-
- let command_range = text_thread.buffer().update(cx, |buffer, cx| {
- let offset = buffer.random_byte_range(0, &mut rng).start;
- buffer.edit(
- [(offset..offset, format!("\n{}\n", command_text))],
- None,
- cx,
- );
- offset + 1..offset + 1 + command_text.len()
- });
-
- let output_text = RandomCharIter::new(&mut rng)
- .filter(|c| *c != '\r')
- .take(10)
- .collect::<String>();
-
- let mut events = vec![Ok(SlashCommandEvent::StartMessage {
- role: Role::User,
- merge_same_roles: true,
- })];
-
- let num_sections = rng.random_range(0..=3);
- let mut section_start = 0;
- for _ in 0..num_sections {
- let section_end = output_text.floor_char_boundary(
- rng.random_range(section_start..=output_text.len()),
- );
- events.push(Ok(SlashCommandEvent::StartSection {
- icon: IconName::ZedAgent,
- label: "section".into(),
- metadata: None,
- }));
- events.push(Ok(SlashCommandEvent::Content(SlashCommandContent::Text {
- text: output_text[section_start..section_end].to_string(),
- run_commands_in_text: false,
- })));
- events.push(Ok(SlashCommandEvent::EndSection));
- section_start = section_end;
- }
-
- if section_start < output_text.len() {
- events.push(Ok(SlashCommandEvent::Content(SlashCommandContent::Text {
- text: output_text[section_start..].to_string(),
- run_commands_in_text: false,
- })));
- }
-
- log::info!(
- "Context {}: insert slash command output at {:?} with {:?} events",
- context_index,
- command_range,
- events.len()
- );
-
- let command_range = text_thread
- .buffer()
- .read(cx)
- .anchor_after(command_range.start)
- ..text_thread
- .buffer()
- .read(cx)
- .anchor_after(command_range.end);
- text_thread.insert_command_output(
- command_range,
- "/command",
- Task::ready(Ok(stream::iter(events).boxed())),
- true,
- cx,
- );
- });
- cx.run_until_parked();
- mutation_count -= 1;
- }
- 75..=84 if mutation_count > 0 => {
- text_thread.update(cx, |text_thread, cx| {
- if let Some(message) = text_thread.messages(cx).choose(&mut rng) {
- let new_status = match rng.random_range(0..3) {
- 0 => MessageStatus::Done,
- 1 => MessageStatus::Pending,
- _ => MessageStatus::Error(SharedString::from("Random error")),
- };
- log::info!(
- "Context {}: update message {:?} status to {:?}",
- context_index,
- message.id,
- new_status
- );
- text_thread.update_metadata(message.id, cx, |metadata| {
- metadata.status = new_status;
- });
- }
- });
- mutation_count -= 1;
- }
- _ => {
- let replica_id = ReplicaId::new(context_index as u16);
- if network.lock().is_disconnected(replica_id) {
- network.lock().reconnect_peer(replica_id, ReplicaId::new(0));
-
- let (ops_to_send, ops_to_receive) = cx.read(|cx| {
- let host_context = &text_threads[0].read(cx);
- let guest_context = text_thread.read(cx);
- (
- guest_context.serialize_ops(&host_context.version(cx), cx),
- host_context.serialize_ops(&guest_context.version(cx), cx),
- )
- });
- let ops_to_send = ops_to_send.await;
- let ops_to_receive = ops_to_receive
- .await
- .into_iter()
- .map(TextThreadOperation::from_proto)
- .collect::<Result<Vec<_>>>()
- .unwrap();
- log::info!(
- "Context {}: reconnecting. Sent {} operations, received {} operations",
- context_index,
- ops_to_send.len(),
- ops_to_receive.len()
- );
-
- network.lock().broadcast(replica_id, ops_to_send);
- text_thread.update(cx, |text_thread, cx| {
- text_thread.apply_ops(ops_to_receive, cx)
- });
- } else if rng.random_bool(0.1) && replica_id != ReplicaId::new(0) {
- log::info!("Context {}: disconnecting", context_index);
- network.lock().disconnect_peer(replica_id);
- } else if network.lock().has_unreceived(replica_id) {
- log::info!("Context {}: applying operations", context_index);
- let ops = network.lock().receive(replica_id);
- let ops = ops
- .into_iter()
- .map(TextThreadOperation::from_proto)
- .collect::<Result<Vec<_>>>()
- .unwrap();
- text_thread.update(cx, |text_thread, cx| text_thread.apply_ops(ops, cx));
- }
- }
- }
- }
-
- cx.read(|cx| {
- let first_context = text_threads[0].read(cx);
- for text_thread in &text_threads[1..] {
- let text_thread = text_thread.read(cx);
- assert!(text_thread.pending_ops.is_empty(), "pending ops: {:?}", text_thread.pending_ops);
- assert_eq!(
- text_thread.buffer().read(cx).text(),
- first_context.buffer().read(cx).text(),
- "Context {:?} text != Context 0 text",
- text_thread.buffer().read(cx).replica_id()
- );
- assert_eq!(
- text_thread.message_anchors,
- first_context.message_anchors,
- "Context {:?} messages != Context 0 messages",
- text_thread.buffer().read(cx).replica_id()
- );
- assert_eq!(
- text_thread.messages_metadata,
- first_context.messages_metadata,
- "Context {:?} message metadata != Context 0 message metadata",
- text_thread.buffer().read(cx).replica_id()
- );
- assert_eq!(
- text_thread.slash_command_output_sections,
- first_context.slash_command_output_sections,
- "Context {:?} slash command output sections != Context 0 slash command output sections",
- text_thread.buffer().read(cx).replica_id()
- );
- }
- });
-}
-
-#[gpui::test]
-fn test_mark_cache_anchors(cx: &mut App) {
- init_test(cx);
-
- let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
- let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
- let text_thread = cx.new(|cx| {
- TextThread::local(
- registry,
- prompt_builder.clone(),
- Arc::new(SlashCommandWorkingSet::default()),
- cx,
- )
- });
- let buffer = text_thread.read(cx).buffer().clone();
-
- // Create a test cache configuration
- let cache_configuration = &Some(LanguageModelCacheConfiguration {
- max_cache_anchors: 3,
- should_speculate: true,
- min_total_token: 10,
- });
-
- let message_1 = text_thread.read(cx).message_anchors[0].clone();
-
- text_thread.update(cx, |text_thread, cx| {
- text_thread.mark_cache_anchors(cache_configuration, false, cx)
- });
-
- assert_eq!(
- messages_cache(&text_thread, cx)
- .iter()
- .filter(|(_, cache)| cache.as_ref().is_some_and(|cache| cache.is_anchor))
- .count(),
- 0,
- "Empty messages should not have any cache anchors."
- );
-
- buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "aaa")], None, cx));
- let message_2 = text_thread
- .update(cx, |text_thread, cx| {
- text_thread.insert_message_after(message_1.id, Role::User, MessageStatus::Pending, cx)
- })
- .unwrap();
-
- buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "bbbbbbb")], None, cx));
- let message_3 = text_thread
- .update(cx, |text_thread, cx| {
- text_thread.insert_message_after(message_2.id, Role::User, MessageStatus::Pending, cx)
- })
- .unwrap();
- buffer.update(cx, |buffer, cx| buffer.edit([(12..12, "cccccc")], None, cx));
-
- text_thread.update(cx, |text_thread, cx| {
- text_thread.mark_cache_anchors(cache_configuration, false, cx)
- });
- assert_eq!(buffer.read(cx).text(), "aaa\nbbbbbbb\ncccccc");
- assert_eq!(
- messages_cache(&text_thread, cx)
- .iter()
- .filter(|(_, cache)| cache.as_ref().is_some_and(|cache| cache.is_anchor))
- .count(),
- 0,
- "Messages should not be marked for cache before going over the token minimum."
- );
- text_thread.update(cx, |text_thread, _| {
- text_thread.token_count = Some(20);
- });
-
- text_thread.update(cx, |text_thread, cx| {
- text_thread.mark_cache_anchors(cache_configuration, true, cx)
- });
- assert_eq!(
- messages_cache(&text_thread, cx)
- .iter()
- .map(|(_, cache)| cache.as_ref().is_some_and(|cache| cache.is_anchor))
- .collect::<Vec<bool>>(),
- vec![true, true, false],
- "Last message should not be an anchor on speculative request."
- );
-
- text_thread
- .update(cx, |text_thread, cx| {
- text_thread.insert_message_after(
- message_3.id,
- Role::Assistant,
- MessageStatus::Pending,
- cx,
- )
- })
- .unwrap();
-
- text_thread.update(cx, |text_thread, cx| {
- text_thread.mark_cache_anchors(cache_configuration, false, cx)
- });
- assert_eq!(
- messages_cache(&text_thread, cx)
- .iter()
- .map(|(_, cache)| cache.as_ref().is_some_and(|cache| cache.is_anchor))
- .collect::<Vec<bool>>(),
- vec![false, true, true, false],
- "Most recent message should also be cached if not a speculative request."
- );
- text_thread.update(cx, |text_thread, cx| {
- text_thread.update_cache_status_for_completion(cx)
- });
- assert_eq!(
- messages_cache(&text_thread, cx)
- .iter()
- .map(|(_, cache)| cache
- .as_ref()
- .map_or(None, |cache| Some(cache.status.clone())))
- .collect::<Vec<Option<CacheStatus>>>(),
- vec![
- Some(CacheStatus::Cached),
- Some(CacheStatus::Cached),
- Some(CacheStatus::Cached),
- None
- ],
- "All user messages prior to anchor should be marked as cached."
- );
-
- buffer.update(cx, |buffer, cx| buffer.edit([(14..14, "d")], None, cx));
- text_thread.update(cx, |text_thread, cx| {
- text_thread.mark_cache_anchors(cache_configuration, false, cx)
- });
- assert_eq!(
- messages_cache(&text_thread, cx)
- .iter()
- .map(|(_, cache)| cache
- .as_ref()
- .map_or(None, |cache| Some(cache.status.clone())))
- .collect::<Vec<Option<CacheStatus>>>(),
- vec![
- Some(CacheStatus::Cached),
- Some(CacheStatus::Cached),
- Some(CacheStatus::Pending),
- None
- ],
- "Modifying a message should invalidate it's cache but leave previous messages."
- );
- buffer.update(cx, |buffer, cx| buffer.edit([(2..2, "e")], None, cx));
- text_thread.update(cx, |text_thread, cx| {
- text_thread.mark_cache_anchors(cache_configuration, false, cx)
- });
- assert_eq!(
- messages_cache(&text_thread, cx)
- .iter()
- .map(|(_, cache)| cache
- .as_ref()
- .map_or(None, |cache| Some(cache.status.clone())))
- .collect::<Vec<Option<CacheStatus>>>(),
- vec![
- Some(CacheStatus::Pending),
- Some(CacheStatus::Pending),
- Some(CacheStatus::Pending),
- None
- ],
- "Modifying a message should invalidate all future messages."
- );
-}
-
-#[gpui::test]
-async fn test_summarization(cx: &mut TestAppContext) {
- let (text_thread, fake_model) = setup_context_editor_with_fake_model(cx);
-
- // Initial state should be pending
- text_thread.read_with(cx, |text_thread, _| {
- assert!(matches!(text_thread.summary(), TextThreadSummary::Pending));
- assert_eq!(
- text_thread.summary().or_default(),
- TextThreadSummary::DEFAULT
- );
- });
-
- let message_1 = text_thread.read_with(cx, |text_thread, _cx| {
- text_thread.message_anchors[0].clone()
- });
- text_thread.update(cx, |context, cx| {
- context
- .insert_message_after(message_1.id, Role::Assistant, MessageStatus::Done, cx)
- .unwrap();
- });
-
- // Send a message
- text_thread.update(cx, |text_thread, cx| {
- text_thread.assist(cx);
- });
-
- simulate_successful_response(&fake_model, cx);
-
- // Should start generating summary when there are >= 2 messages
- text_thread.read_with(cx, |text_thread, _| {
- assert!(!text_thread.summary().content().unwrap().done);
- });
-
- cx.run_until_parked();
- fake_model.send_last_completion_stream_text_chunk("Brief");
- fake_model.send_last_completion_stream_text_chunk(" Introduction");
- fake_model.end_last_completion_stream();
- cx.run_until_parked();
-
- // Summary should be set
- text_thread.read_with(cx, |text_thread, _| {
- assert_eq!(text_thread.summary().or_default(), "Brief Introduction");
- });
-
- // We should be able to manually set a summary
- text_thread.update(cx, |text_thread, cx| {
- text_thread.set_custom_summary("Brief Intro".into(), cx);
- });
-
- text_thread.read_with(cx, |text_thread, _| {
- assert_eq!(text_thread.summary().or_default(), "Brief Intro");
- });
-}
-
-#[gpui::test]
-async fn test_thread_summary_error_set_manually(cx: &mut TestAppContext) {
- let (text_thread, fake_model) = setup_context_editor_with_fake_model(cx);
-
- test_summarize_error(&fake_model, &text_thread, cx);
-
- // Now we should be able to set a summary
- text_thread.update(cx, |text_thread, cx| {
- text_thread.set_custom_summary("Brief Intro".into(), cx);
- });
-
- text_thread.read_with(cx, |text_thread, _| {
- assert_eq!(text_thread.summary().or_default(), "Brief Intro");
- });
-}
-
-#[gpui::test]
-async fn test_thread_summary_error_retry(cx: &mut TestAppContext) {
- let (text_thread, fake_model) = setup_context_editor_with_fake_model(cx);
-
- test_summarize_error(&fake_model, &text_thread, cx);
-
- // Sending another message should not trigger another summarize request
- text_thread.update(cx, |text_thread, cx| {
- text_thread.assist(cx);
- });
-
- simulate_successful_response(&fake_model, cx);
-
- text_thread.read_with(cx, |text_thread, _| {
- // State is still Error, not Generating
- assert!(matches!(text_thread.summary(), TextThreadSummary::Error));
- });
-
- // But the summarize request can be invoked manually
- text_thread.update(cx, |text_thread, cx| {
- text_thread.summarize(true, cx);
- });
-
- text_thread.read_with(cx, |text_thread, _| {
- assert!(!text_thread.summary().content().unwrap().done);
- });
-
- cx.run_until_parked();
- fake_model.send_last_completion_stream_text_chunk("A successful summary");
- fake_model.end_last_completion_stream();
- cx.run_until_parked();
-
- text_thread.read_with(cx, |text_thread, _| {
- assert_eq!(text_thread.summary().or_default(), "A successful summary");
- });
-}
-
-fn test_summarize_error(
- model: &Arc<FakeLanguageModel>,
- text_thread: &Entity<TextThread>,
- cx: &mut TestAppContext,
-) {
- let message_1 = text_thread.read_with(cx, |text_thread, _cx| {
- text_thread.message_anchors[0].clone()
- });
- text_thread.update(cx, |text_thread, cx| {
- text_thread
- .insert_message_after(message_1.id, Role::Assistant, MessageStatus::Done, cx)
- .unwrap();
- });
-
- // Send a message
- text_thread.update(cx, |text_thread, cx| {
- text_thread.assist(cx);
- });
-
- simulate_successful_response(model, cx);
-
- text_thread.read_with(cx, |text_thread, _| {
- assert!(!text_thread.summary().content().unwrap().done);
- });
-
- // Simulate summary request ending
- cx.run_until_parked();
- model.end_last_completion_stream();
- cx.run_until_parked();
-
- // State is set to Error and default message
- text_thread.read_with(cx, |text_thread, _| {
- assert_eq!(*text_thread.summary(), TextThreadSummary::Error);
- assert_eq!(
- text_thread.summary().or_default(),
- TextThreadSummary::DEFAULT
- );
- });
-}
-
-fn setup_context_editor_with_fake_model(
- cx: &mut TestAppContext,
-) -> (Entity<TextThread>, Arc<FakeLanguageModel>) {
- let registry = Arc::new(LanguageRegistry::test(cx.executor()));
-
- let fake_provider = Arc::new(FakeLanguageModelProvider::default());
- let fake_model = Arc::new(fake_provider.test_model());
-
- cx.update(|cx| {
- init_test(cx);
- LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
- let configured_model = ConfiguredModel {
- provider: fake_provider.clone(),
- model: fake_model.clone(),
- };
- registry.set_default_model(Some(configured_model.clone()), cx);
- registry.set_thread_summary_model(Some(configured_model), cx);
- })
- });
-
- let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
- let context = cx.new(|cx| {
- TextThread::local(
- registry,
- prompt_builder.clone(),
- Arc::new(SlashCommandWorkingSet::default()),
- cx,
- )
- });
-
- (context, fake_model)
-}
-
-fn simulate_successful_response(fake_model: &FakeLanguageModel, cx: &mut TestAppContext) {
- cx.run_until_parked();
- fake_model.send_last_completion_stream_text_chunk("Assistant response");
- fake_model.end_last_completion_stream();
- cx.run_until_parked();
-}
-
-fn messages(context: &Entity<TextThread>, cx: &App) -> Vec<(MessageId, Role, Range<usize>)> {
- context
- .read(cx)
- .messages(cx)
- .map(|message| (message.id, message.role, message.offset_range))
- .collect()
-}
-
-fn messages_cache(
- context: &Entity<TextThread>,
- cx: &App,
-) -> Vec<(MessageId, Option<MessageCacheMetadata>)> {
- context
- .read(cx)
- .messages(cx)
- .map(|message| (message.id, message.cache))
- .collect()
-}
-
-fn init_test(cx: &mut App) {
- let settings_store = SettingsStore::test(cx);
- prompt_store::init(cx);
- LanguageModelRegistry::test(cx);
- cx.set_global(settings_store);
-}
-
-#[derive(Clone)]
-struct FakeSlashCommand(String);
-
-impl SlashCommand for FakeSlashCommand {
- fn name(&self) -> String {
- self.0.clone()
- }
-
- fn description(&self) -> String {
- format!("Fake slash command: {}", self.0)
- }
-
- fn menu_text(&self) -> String {
- format!("Run fake command: {}", self.0)
- }
-
- fn complete_argument(
- self: Arc<Self>,
- _arguments: &[String],
- _cancel: Arc<AtomicBool>,
- _workspace: Option<WeakEntity<Workspace>>,
- _window: &mut Window,
- _cx: &mut App,
- ) -> Task<Result<Vec<ArgumentCompletion>>> {
- Task::ready(Ok(vec![]))
- }
-
- fn requires_argument(&self) -> bool {
- false
- }
-
- fn run(
- self: Arc<Self>,
- _arguments: &[String],
- _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
- _context_buffer: BufferSnapshot,
- _workspace: WeakEntity<Workspace>,
- _delegate: Option<Arc<dyn LspAdapterDelegate>>,
- _window: &mut Window,
- _cx: &mut App,
- ) -> Task<SlashCommandResult> {
- Task::ready(Ok(SlashCommandOutput {
- text: format!("Executed fake command: {}", self.0),
- sections: vec![],
- run_commands_in_text: false,
- }
- .into_event_stream()))
- }
-}
@@ -1,251 +0,0 @@
-use anyhow::{Context as _, Result, anyhow};
-use assistant_slash_command::{
- AfterCompletion, ArgumentCompletion, SlashCommand, SlashCommandOutput,
- SlashCommandOutputSection, SlashCommandResult,
-};
-use collections::HashMap;
-use context_server::{ContextServerId, types::Prompt};
-use gpui::{App, Entity, Task, WeakEntity, Window};
-use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate};
-use project::context_server_store::ContextServerStore;
-use std::sync::Arc;
-use std::sync::atomic::AtomicBool;
-use text::LineEnding;
-use ui::{IconName, SharedString};
-use workspace::Workspace;
-
-use assistant_slash_command::create_label_for_command;
-
-pub struct ContextServerSlashCommand {
- store: Entity<ContextServerStore>,
- server_id: ContextServerId,
- prompt: Prompt,
-}
-
-impl ContextServerSlashCommand {
- pub fn new(store: Entity<ContextServerStore>, id: ContextServerId, prompt: Prompt) -> Self {
- Self {
- server_id: id,
- prompt,
- store,
- }
- }
-}
-
-impl SlashCommand for ContextServerSlashCommand {
- fn name(&self) -> String {
- self.prompt.name.clone()
- }
-
- fn label(&self, cx: &App) -> language::CodeLabel {
- let mut parts = vec![self.prompt.name.as_str()];
- if let Some(args) = &self.prompt.arguments
- && let Some(arg) = args.first()
- {
- parts.push(arg.name.as_str());
- }
- create_label_for_command(parts[0], &parts[1..], cx)
- }
-
- fn description(&self) -> String {
- match &self.prompt.description {
- Some(desc) => desc.clone(),
- None => format!("Run '{}' from {}", self.prompt.name, self.server_id),
- }
- }
-
- fn menu_text(&self) -> String {
- match &self.prompt.description {
- Some(desc) => desc.clone(),
- None => format!("Run '{}' from {}", self.prompt.name, self.server_id),
- }
- }
-
- fn requires_argument(&self) -> bool {
- self.prompt
- .arguments
- .as_ref()
- .is_some_and(|args| args.iter().any(|arg| arg.required == Some(true)))
- }
-
- fn complete_argument(
- self: Arc<Self>,
- arguments: &[String],
- _cancel: Arc<AtomicBool>,
- _workspace: Option<WeakEntity<Workspace>>,
- _window: &mut Window,
- cx: &mut App,
- ) -> Task<Result<Vec<ArgumentCompletion>>> {
- let Ok((arg_name, arg_value)) = completion_argument(&self.prompt, arguments) else {
- return Task::ready(Err(anyhow!("Failed to complete argument")));
- };
-
- let server_id = self.server_id.clone();
- let prompt_name = self.prompt.name.clone();
-
- if let Some(server) = self.store.read(cx).get_running_server(&server_id) {
- cx.foreground_executor().spawn(async move {
- let protocol = server.client().context("Context server not initialized")?;
-
- let response = protocol
- .request::<context_server::types::requests::CompletionComplete>(
- context_server::types::CompletionCompleteParams {
- reference: context_server::types::CompletionReference::Prompt(
- context_server::types::PromptReference {
- ty: context_server::types::PromptReferenceType::Prompt,
- name: prompt_name,
- },
- ),
- argument: context_server::types::CompletionArgument {
- name: arg_name,
- value: arg_value,
- },
- meta: None,
- },
- )
- .await?;
-
- let completions = response
- .completion
- .values
- .into_iter()
- .map(|value| ArgumentCompletion {
- label: CodeLabel::plain(value.clone(), None),
- new_text: value,
- after_completion: AfterCompletion::Continue,
- replace_previous_arguments: false,
- })
- .collect();
- Ok(completions)
- })
- } else {
- Task::ready(Err(anyhow!("Context server not found")))
- }
- }
-
- fn run(
- self: Arc<Self>,
- arguments: &[String],
- _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
- _context_buffer: BufferSnapshot,
- _workspace: WeakEntity<Workspace>,
- _delegate: Option<Arc<dyn LspAdapterDelegate>>,
- _window: &mut Window,
- cx: &mut App,
- ) -> Task<SlashCommandResult> {
- let server_id = self.server_id.clone();
- let prompt_name = self.prompt.name.clone();
-
- let prompt_args = match prompt_arguments(&self.prompt, arguments) {
- Ok(args) => args,
- Err(e) => return Task::ready(Err(e)),
- };
-
- let store = self.store.read(cx);
- if let Some(server) = store.get_running_server(&server_id) {
- cx.foreground_executor().spawn(async move {
- let protocol = server.client().context("Context server not initialized")?;
- let response = protocol
- .request::<context_server::types::requests::PromptsGet>(
- context_server::types::PromptsGetParams {
- name: prompt_name.clone(),
- arguments: Some(prompt_args),
- meta: None,
- },
- )
- .await?;
-
- anyhow::ensure!(
- response
- .messages
- .iter()
- .all(|msg| matches!(msg.role, context_server::types::Role::User)),
- "Prompt contains non-user roles, which is not supported"
- );
-
- // Extract text from user messages into a single prompt string
- let mut prompt = response
- .messages
- .into_iter()
- .filter_map(|msg| match msg.content {
- context_server::types::MessageContent::Text { text, .. } => Some(text),
- _ => None,
- })
- .collect::<Vec<String>>()
- .join("\n\n");
-
- // We must normalize the line endings here, since servers might return CR characters.
- LineEnding::normalize(&mut prompt);
-
- Ok(SlashCommandOutput {
- sections: vec![SlashCommandOutputSection {
- range: 0..(prompt.len()),
- icon: IconName::ZedAssistant,
- label: SharedString::from(
- response
- .description
- .unwrap_or(format!("Result from {}", prompt_name)),
- ),
- metadata: None,
- }],
- text: prompt,
- run_commands_in_text: false,
- }
- .into_event_stream())
- })
- } else {
- Task::ready(Err(anyhow!("Context server not found")))
- }
- }
-}
-
-fn completion_argument(prompt: &Prompt, arguments: &[String]) -> Result<(String, String)> {
- anyhow::ensure!(!arguments.is_empty(), "No arguments given");
-
- match &prompt.arguments {
- Some(args) if args.len() == 1 => {
- let arg_name = args[0].name.clone();
- let arg_value = arguments.join(" ");
- Ok((arg_name, arg_value))
- }
- Some(_) => anyhow::bail!("Prompt must have exactly one argument"),
- None => anyhow::bail!("Prompt has no arguments"),
- }
-}
-
-fn prompt_arguments(prompt: &Prompt, arguments: &[String]) -> Result<HashMap<String, String>> {
- match &prompt.arguments {
- Some(args) if args.len() > 1 => {
- anyhow::bail!("Prompt has more than one argument, which is not supported");
- }
- Some(args) if args.len() == 1 => {
- if !arguments.is_empty() {
- let mut map = HashMap::default();
- map.insert(args[0].name.clone(), arguments.join(" "));
- Ok(map)
- } else if arguments.is_empty() && args[0].required == Some(false) {
- Ok(HashMap::default())
- } else {
- anyhow::bail!("Prompt expects argument but none given");
- }
- }
- Some(_) | None => {
- anyhow::ensure!(
- arguments.is_empty(),
- "Prompt expects no arguments but some were given"
- );
- Ok(HashMap::default())
- }
- }
-}
-
-/// MCP servers can return prompts with multiple arguments. Since we only
-/// support one argument, we ignore all others. This is the necessary predicate
-/// for this.
-pub fn acceptable_prompt(prompt: &Prompt) -> bool {
- match &prompt.arguments {
- None => true,
- Some(args) if args.len() <= 1 => true,
- _ => false,
- }
-}
@@ -1,3286 +0,0 @@
-use agent_settings::{AgentSettings, SUMMARIZE_THREAD_PROMPT};
-use anyhow::{Context as _, Result, bail};
-use assistant_slash_command::{
- SlashCommandContent, SlashCommandEvent, SlashCommandLine, SlashCommandOutputSection,
- SlashCommandResult, SlashCommandWorkingSet,
-};
-use client::{self, proto};
-use clock::ReplicaId;
-use collections::{HashMap, HashSet};
-use fs::{Fs, RenameOptions};
-
-use futures::{FutureExt, StreamExt, future::Shared};
-use gpui::{
- App, AppContext as _, Context, Entity, EventEmitter, RenderImage, SharedString, Subscription,
- Task,
-};
-use itertools::Itertools as _;
-use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, Point, ToOffset};
-use language_model::{
- AnthropicCompletionType, AnthropicEventData, AnthropicEventType, CompletionIntent,
- LanguageModel, LanguageModelCacheConfiguration, LanguageModelCompletionEvent,
- LanguageModelImage, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
- LanguageModelToolUseId, MessageContent, PaymentRequiredError, Role, StopReason,
- report_anthropic_event,
-};
-use open_ai::Model as OpenAiModel;
-use paths::text_threads_dir;
-use prompt_store::PromptBuilder;
-use serde::{Deserialize, Serialize};
-use smallvec::SmallVec;
-use std::{
- cmp::{Ordering, max},
- fmt::{Debug, Write as _},
- iter, mem,
- ops::Range,
- path::Path,
- sync::Arc,
- time::{Duration, Instant},
-};
-
-use text::{BufferSnapshot, ToPoint};
-use ui::IconName;
-use util::{ResultExt, TryFutureExt, post_inc};
-use uuid::Uuid;
-
-#[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
-pub struct TextThreadId(String);
-
-impl TextThreadId {
- pub fn new() -> Self {
- Self(Uuid::new_v4().to_string())
- }
-
- pub fn from_proto(id: String) -> Self {
- Self(id)
- }
-
- pub fn to_proto(&self) -> String {
- self.0.clone()
- }
-}
-
-#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
-pub struct MessageId(pub clock::Lamport);
-
-impl MessageId {
- pub fn as_u64(self) -> u64 {
- self.0.as_u64()
- }
-}
-
-#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
-pub enum MessageStatus {
- Pending,
- Done,
- Error(SharedString),
- Canceled,
-}
-
-impl MessageStatus {
- pub fn from_proto(status: proto::ContextMessageStatus) -> MessageStatus {
- match status.variant {
- Some(proto::context_message_status::Variant::Pending(_)) => MessageStatus::Pending,
- Some(proto::context_message_status::Variant::Done(_)) => MessageStatus::Done,
- Some(proto::context_message_status::Variant::Error(error)) => {
- MessageStatus::Error(error.message.into())
- }
- Some(proto::context_message_status::Variant::Canceled(_)) => MessageStatus::Canceled,
- None => MessageStatus::Pending,
- }
- }
-
- pub fn to_proto(&self) -> proto::ContextMessageStatus {
- match self {
- MessageStatus::Pending => proto::ContextMessageStatus {
- variant: Some(proto::context_message_status::Variant::Pending(
- proto::context_message_status::Pending {},
- )),
- },
- MessageStatus::Done => proto::ContextMessageStatus {
- variant: Some(proto::context_message_status::Variant::Done(
- proto::context_message_status::Done {},
- )),
- },
- MessageStatus::Error(message) => proto::ContextMessageStatus {
- variant: Some(proto::context_message_status::Variant::Error(
- proto::context_message_status::Error {
- message: message.to_string(),
- },
- )),
- },
- MessageStatus::Canceled => proto::ContextMessageStatus {
- variant: Some(proto::context_message_status::Variant::Canceled(
- proto::context_message_status::Canceled {},
- )),
- },
- }
- }
-}
-
-#[derive(Clone, Debug)]
-pub enum TextThreadOperation {
- InsertMessage {
- anchor: MessageAnchor,
- metadata: MessageMetadata,
- version: clock::Global,
- },
- UpdateMessage {
- message_id: MessageId,
- metadata: MessageMetadata,
- version: clock::Global,
- },
- UpdateSummary {
- summary: TextThreadSummaryContent,
- version: clock::Global,
- },
- SlashCommandStarted {
- id: InvokedSlashCommandId,
- output_range: Range<language::Anchor>,
- name: String,
- version: clock::Global,
- },
- SlashCommandFinished {
- id: InvokedSlashCommandId,
- timestamp: clock::Lamport,
- error_message: Option<String>,
- version: clock::Global,
- },
- SlashCommandOutputSectionAdded {
- timestamp: clock::Lamport,
- section: SlashCommandOutputSection<language::Anchor>,
- version: clock::Global,
- },
- ThoughtProcessOutputSectionAdded {
- timestamp: clock::Lamport,
- section: ThoughtProcessOutputSection<language::Anchor>,
- version: clock::Global,
- },
- BufferOperation(language::Operation),
-}
-
-impl TextThreadOperation {
- pub fn from_proto(op: proto::ContextOperation) -> Result<Self> {
- match op.variant.context("invalid variant")? {
- proto::context_operation::Variant::InsertMessage(insert) => {
- let message = insert.message.context("invalid message")?;
- let id = MessageId(language::proto::deserialize_timestamp(
- message.id.context("invalid id")?,
- ));
- Ok(Self::InsertMessage {
- anchor: MessageAnchor {
- id,
- start: language::proto::deserialize_anchor(
- message.start.context("invalid anchor")?,
- )
- .context("invalid anchor")?,
- },
- metadata: MessageMetadata {
- role: Role::from_proto(message.role),
- status: MessageStatus::from_proto(
- message.status.context("invalid status")?,
- ),
- timestamp: id.0,
- cache: None,
- },
- version: language::proto::deserialize_version(&insert.version),
- })
- }
- proto::context_operation::Variant::UpdateMessage(update) => Ok(Self::UpdateMessage {
- message_id: MessageId(language::proto::deserialize_timestamp(
- update.message_id.context("invalid message id")?,
- )),
- metadata: MessageMetadata {
- role: Role::from_proto(update.role),
- status: MessageStatus::from_proto(update.status.context("invalid status")?),
- timestamp: language::proto::deserialize_timestamp(
- update.timestamp.context("invalid timestamp")?,
- ),
- cache: None,
- },
- version: language::proto::deserialize_version(&update.version),
- }),
- proto::context_operation::Variant::UpdateSummary(update) => Ok(Self::UpdateSummary {
- summary: TextThreadSummaryContent {
- text: update.summary,
- done: update.done,
- timestamp: language::proto::deserialize_timestamp(
- update.timestamp.context("invalid timestamp")?,
- ),
- },
- version: language::proto::deserialize_version(&update.version),
- }),
- proto::context_operation::Variant::SlashCommandStarted(message) => {
- Ok(Self::SlashCommandStarted {
- id: InvokedSlashCommandId(language::proto::deserialize_timestamp(
- message.id.context("invalid id")?,
- )),
- output_range: language::proto::deserialize_anchor_range(
- message.output_range.context("invalid range")?,
- )?,
- name: message.name,
- version: language::proto::deserialize_version(&message.version),
- })
- }
- proto::context_operation::Variant::SlashCommandOutputSectionAdded(message) => {
- let section = message.section.context("missing section")?;
- Ok(Self::SlashCommandOutputSectionAdded {
- timestamp: language::proto::deserialize_timestamp(
- message.timestamp.context("missing timestamp")?,
- ),
- section: SlashCommandOutputSection {
- range: language::proto::deserialize_anchor_range(
- section.range.context("invalid range")?,
- )?,
- icon: section.icon_name.parse()?,
- label: section.label.into(),
- metadata: section
- .metadata
- .and_then(|metadata| serde_json::from_str(&metadata).log_err()),
- },
- version: language::proto::deserialize_version(&message.version),
- })
- }
- proto::context_operation::Variant::SlashCommandCompleted(message) => {
- Ok(Self::SlashCommandFinished {
- id: InvokedSlashCommandId(language::proto::deserialize_timestamp(
- message.id.context("invalid id")?,
- )),
- timestamp: language::proto::deserialize_timestamp(
- message.timestamp.context("missing timestamp")?,
- ),
- error_message: message.error_message,
- version: language::proto::deserialize_version(&message.version),
- })
- }
- proto::context_operation::Variant::ThoughtProcessOutputSectionAdded(message) => {
- let section = message.section.context("missing section")?;
- Ok(Self::ThoughtProcessOutputSectionAdded {
- timestamp: language::proto::deserialize_timestamp(
- message.timestamp.context("missing timestamp")?,
- ),
- section: ThoughtProcessOutputSection {
- range: language::proto::deserialize_anchor_range(
- section.range.context("invalid range")?,
- )?,
- },
- version: language::proto::deserialize_version(&message.version),
- })
- }
- proto::context_operation::Variant::BufferOperation(op) => Ok(Self::BufferOperation(
- language::proto::deserialize_operation(
- op.operation.context("invalid buffer operation")?,
- )?,
- )),
- }
- }
-
- pub fn to_proto(&self) -> proto::ContextOperation {
- match self {
- Self::InsertMessage {
- anchor,
- metadata,
- version,
- } => proto::ContextOperation {
- variant: Some(proto::context_operation::Variant::InsertMessage(
- proto::context_operation::InsertMessage {
- message: Some(proto::ContextMessage {
- id: Some(language::proto::serialize_timestamp(anchor.id.0)),
- start: Some(language::proto::serialize_anchor(&anchor.start)),
- role: metadata.role.to_proto() as i32,
- status: Some(metadata.status.to_proto()),
- }),
- version: language::proto::serialize_version(version),
- },
- )),
- },
- Self::UpdateMessage {
- message_id,
- metadata,
- version,
- } => proto::ContextOperation {
- variant: Some(proto::context_operation::Variant::UpdateMessage(
- proto::context_operation::UpdateMessage {
- message_id: Some(language::proto::serialize_timestamp(message_id.0)),
- role: metadata.role.to_proto() as i32,
- status: Some(metadata.status.to_proto()),
- timestamp: Some(language::proto::serialize_timestamp(metadata.timestamp)),
- version: language::proto::serialize_version(version),
- },
- )),
- },
- Self::UpdateSummary { summary, version } => proto::ContextOperation {
- variant: Some(proto::context_operation::Variant::UpdateSummary(
- proto::context_operation::UpdateSummary {
- summary: summary.text.clone(),
- done: summary.done,
- timestamp: Some(language::proto::serialize_timestamp(summary.timestamp)),
- version: language::proto::serialize_version(version),
- },
- )),
- },
- Self::SlashCommandStarted {
- id,
- output_range,
- name,
- version,
- } => proto::ContextOperation {
- variant: Some(proto::context_operation::Variant::SlashCommandStarted(
- proto::context_operation::SlashCommandStarted {
- id: Some(language::proto::serialize_timestamp(id.0)),
- output_range: Some(language::proto::serialize_anchor_range(
- output_range.clone(),
- )),
- name: name.clone(),
- version: language::proto::serialize_version(version),
- },
- )),
- },
- Self::SlashCommandOutputSectionAdded {
- timestamp,
- section,
- version,
- } => proto::ContextOperation {
- variant: Some(
- proto::context_operation::Variant::SlashCommandOutputSectionAdded(
- proto::context_operation::SlashCommandOutputSectionAdded {
- timestamp: Some(language::proto::serialize_timestamp(*timestamp)),
- section: Some({
- let icon_name: &'static str = section.icon.into();
- proto::SlashCommandOutputSection {
- range: Some(language::proto::serialize_anchor_range(
- section.range.clone(),
- )),
- icon_name: icon_name.to_string(),
- label: section.label.to_string(),
- metadata: section.metadata.as_ref().and_then(|metadata| {
- serde_json::to_string(metadata).log_err()
- }),
- }
- }),
- version: language::proto::serialize_version(version),
- },
- ),
- ),
- },
- Self::SlashCommandFinished {
- id,
- timestamp,
- error_message,
- version,
- } => proto::ContextOperation {
- variant: Some(proto::context_operation::Variant::SlashCommandCompleted(
- proto::context_operation::SlashCommandCompleted {
- id: Some(language::proto::serialize_timestamp(id.0)),
- timestamp: Some(language::proto::serialize_timestamp(*timestamp)),
- error_message: error_message.clone(),
- version: language::proto::serialize_version(version),
- },
- )),
- },
- Self::ThoughtProcessOutputSectionAdded {
- timestamp,
- section,
- version,
- } => proto::ContextOperation {
- variant: Some(
- proto::context_operation::Variant::ThoughtProcessOutputSectionAdded(
- proto::context_operation::ThoughtProcessOutputSectionAdded {
- timestamp: Some(language::proto::serialize_timestamp(*timestamp)),
- section: Some({
- proto::ThoughtProcessOutputSection {
- range: Some(language::proto::serialize_anchor_range(
- section.range.clone(),
- )),
- }
- }),
- version: language::proto::serialize_version(version),
- },
- ),
- ),
- },
- Self::BufferOperation(operation) => proto::ContextOperation {
- variant: Some(proto::context_operation::Variant::BufferOperation(
- proto::context_operation::BufferOperation {
- operation: Some(language::proto::serialize_operation(operation)),
- },
- )),
- },
- }
- }
-
- fn timestamp(&self) -> clock::Lamport {
- match self {
- Self::InsertMessage { anchor, .. } => anchor.id.0,
- Self::UpdateMessage { metadata, .. } => metadata.timestamp,
- Self::UpdateSummary { summary, .. } => summary.timestamp,
- Self::SlashCommandStarted { id, .. } => id.0,
- Self::SlashCommandOutputSectionAdded { timestamp, .. }
- | Self::SlashCommandFinished { timestamp, .. }
- | Self::ThoughtProcessOutputSectionAdded { timestamp, .. } => *timestamp,
- Self::BufferOperation(_) => {
- panic!("reading the timestamp of a buffer operation is not supported")
- }
- }
- }
-
- /// Returns the current version of the context operation.
- pub fn version(&self) -> &clock::Global {
- match self {
- Self::InsertMessage { version, .. }
- | Self::UpdateMessage { version, .. }
- | Self::UpdateSummary { version, .. }
- | Self::SlashCommandStarted { version, .. }
- | Self::SlashCommandOutputSectionAdded { version, .. }
- | Self::SlashCommandFinished { version, .. }
- | Self::ThoughtProcessOutputSectionAdded { version, .. } => version,
- Self::BufferOperation(_) => {
- panic!("reading the version of a buffer operation is not supported")
- }
- }
- }
-}
-
-#[derive(Debug, Clone)]
-pub enum TextThreadEvent {
- ShowAssistError(SharedString),
- ShowPaymentRequiredError,
- MessagesEdited,
- SummaryChanged,
- SummaryGenerated,
- PathChanged {
- old_path: Option<Arc<Path>>,
- new_path: Arc<Path>,
- },
- StreamedCompletion,
- StartedThoughtProcess(Range<language::Anchor>),
- EndedThoughtProcess(language::Anchor),
- InvokedSlashCommandChanged {
- command_id: InvokedSlashCommandId,
- },
- ParsedSlashCommandsUpdated {
- removed: Vec<Range<language::Anchor>>,
- updated: Vec<ParsedSlashCommand>,
- },
- SlashCommandOutputSectionAdded {
- section: SlashCommandOutputSection<language::Anchor>,
- },
- Operation(TextThreadOperation),
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub enum TextThreadSummary {
- Pending,
- Content(TextThreadSummaryContent),
- Error,
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct TextThreadSummaryContent {
- pub text: String,
- pub done: bool,
- pub timestamp: clock::Lamport,
-}
-
-impl TextThreadSummary {
- pub const DEFAULT: &str = "New Text Thread";
-
- pub fn or_default(&self) -> SharedString {
- self.unwrap_or(Self::DEFAULT)
- }
-
- pub fn unwrap_or(&self, message: impl Into<SharedString>) -> SharedString {
- self.content()
- .map_or_else(|| message.into(), |content| content.text.clone().into())
- }
-
- pub fn content(&self) -> Option<&TextThreadSummaryContent> {
- match self {
- TextThreadSummary::Content(content) => Some(content),
- TextThreadSummary::Pending | TextThreadSummary::Error => None,
- }
- }
-
- fn content_as_mut(&mut self) -> Option<&mut TextThreadSummaryContent> {
- match self {
- TextThreadSummary::Content(content) => Some(content),
- TextThreadSummary::Pending | TextThreadSummary::Error => None,
- }
- }
-
- fn content_or_set_empty(&mut self) -> &mut TextThreadSummaryContent {
- match self {
- TextThreadSummary::Content(content) => content,
- TextThreadSummary::Pending | TextThreadSummary::Error => {
- let content = TextThreadSummaryContent {
- text: "".to_string(),
- done: false,
- timestamp: clock::Lamport::MIN,
- };
- *self = TextThreadSummary::Content(content);
- self.content_as_mut().unwrap()
- }
- }
- }
-
- pub fn is_pending(&self) -> bool {
- matches!(self, TextThreadSummary::Pending)
- }
-
- fn timestamp(&self) -> Option<clock::Lamport> {
- match self {
- TextThreadSummary::Content(content) => Some(content.timestamp),
- TextThreadSummary::Pending | TextThreadSummary::Error => None,
- }
- }
-}
-
-impl PartialOrd for TextThreadSummary {
- fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
- self.timestamp().partial_cmp(&other.timestamp())
- }
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct MessageAnchor {
- pub id: MessageId,
- pub start: language::Anchor,
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub enum CacheStatus {
- Pending,
- Cached,
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct MessageCacheMetadata {
- pub is_anchor: bool,
- pub is_final_anchor: bool,
- pub status: CacheStatus,
- pub cached_at: clock::Global,
-}
-
-#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
-pub struct MessageMetadata {
- pub role: Role,
- pub status: MessageStatus,
- pub timestamp: clock::Lamport,
- #[serde(skip)]
- pub cache: Option<MessageCacheMetadata>,
-}
-
-impl From<&Message> for MessageMetadata {
- fn from(message: &Message) -> Self {
- Self {
- role: message.role,
- status: message.status.clone(),
- timestamp: message.id.0,
- cache: message.cache.clone(),
- }
- }
-}
-
-impl MessageMetadata {
- pub fn is_cache_valid(&self, buffer: &BufferSnapshot, range: &Range<usize>) -> bool {
- match &self.cache {
- Some(MessageCacheMetadata { cached_at, .. }) => !buffer.has_edits_since_in_range(
- cached_at,
- Range {
- start: buffer.anchor_at(range.start, Bias::Right),
- end: buffer.anchor_at(range.end, Bias::Left),
- },
- ),
- _ => false,
- }
- }
-}
-
-#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
-pub struct ThoughtProcessOutputSection<T> {
- pub range: Range<T>,
-}
-
-impl ThoughtProcessOutputSection<language::Anchor> {
- pub fn is_valid(&self, buffer: &language::TextBuffer) -> bool {
- self.range.start.is_valid(buffer) && !self.range.to_offset(buffer).is_empty()
- }
-}
-
-#[derive(Clone, Debug)]
-pub struct Message {
- pub offset_range: Range<usize>,
- pub index_range: Range<usize>,
- pub anchor_range: Range<language::Anchor>,
- pub id: MessageId,
- pub role: Role,
- pub status: MessageStatus,
- pub cache: Option<MessageCacheMetadata>,
-}
-
-#[derive(Debug, Clone)]
-pub enum Content {
- Image {
- anchor: language::Anchor,
- image_id: u64,
- render_image: Arc<RenderImage>,
- image: Shared<Task<Option<LanguageModelImage>>>,
- },
-}
-
-impl Content {
- fn range(&self) -> Range<language::Anchor> {
- match self {
- Self::Image { anchor, .. } => *anchor..*anchor,
- }
- }
-
- fn cmp(&self, other: &Self, buffer: &BufferSnapshot) -> Ordering {
- let self_range = self.range();
- let other_range = other.range();
- if self_range.end.cmp(&other_range.start, buffer).is_lt() {
- Ordering::Less
- } else if self_range.start.cmp(&other_range.end, buffer).is_gt() {
- Ordering::Greater
- } else {
- Ordering::Equal
- }
- }
-}
-
-struct PendingCompletion {
- id: usize,
- assistant_message_id: MessageId,
- _task: Task<()>,
-}
-
-#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
-pub struct InvokedSlashCommandId(clock::Lamport);
-
-pub struct TextThread {
- id: TextThreadId,
- timestamp: clock::Lamport,
- version: clock::Global,
- pub(crate) pending_ops: Vec<TextThreadOperation>,
- operations: Vec<TextThreadOperation>,
- buffer: Entity<Buffer>,
- pub(crate) parsed_slash_commands: Vec<ParsedSlashCommand>,
- invoked_slash_commands: HashMap<InvokedSlashCommandId, InvokedSlashCommand>,
- edits_since_last_parse: language::Subscription<usize>,
- slash_commands: Arc<SlashCommandWorkingSet>,
- pub(crate) slash_command_output_sections: Vec<SlashCommandOutputSection<language::Anchor>>,
- thought_process_output_sections: Vec<ThoughtProcessOutputSection<language::Anchor>>,
- pub(crate) message_anchors: Vec<MessageAnchor>,
- contents: Vec<Content>,
- pub(crate) messages_metadata: HashMap<MessageId, MessageMetadata>,
- summary: TextThreadSummary,
- summary_task: Task<Option<()>>,
- completion_count: usize,
- pending_completions: Vec<PendingCompletion>,
- pub(crate) token_count: Option<u64>,
- pending_token_count: Task<Option<()>>,
- pending_save: Task<Result<()>>,
- pending_cache_warming_task: Task<Option<()>>,
- path: Option<Arc<Path>>,
- _subscriptions: Vec<Subscription>,
- language_registry: Arc<LanguageRegistry>,
- prompt_builder: Arc<PromptBuilder>,
-}
-
-trait ContextAnnotation {
- fn range(&self) -> &Range<language::Anchor>;
-}
-
-impl ContextAnnotation for ParsedSlashCommand {
- fn range(&self) -> &Range<language::Anchor> {
- &self.source_range
- }
-}
-
-impl EventEmitter<TextThreadEvent> for TextThread {}
-
-impl TextThread {
- pub fn local(
- language_registry: Arc<LanguageRegistry>,
- prompt_builder: Arc<PromptBuilder>,
- slash_commands: Arc<SlashCommandWorkingSet>,
- cx: &mut Context<Self>,
- ) -> Self {
- Self::new(
- TextThreadId::new(),
- ReplicaId::default(),
- language::Capability::ReadWrite,
- language_registry,
- prompt_builder,
- slash_commands,
- cx,
- )
- }
-
- pub fn new(
- id: TextThreadId,
- replica_id: ReplicaId,
- capability: language::Capability,
- language_registry: Arc<LanguageRegistry>,
- prompt_builder: Arc<PromptBuilder>,
- slash_commands: Arc<SlashCommandWorkingSet>,
- cx: &mut Context<Self>,
- ) -> Self {
- let buffer = cx.new(|_cx| {
- let buffer = Buffer::remote(
- language::BufferId::new(1).unwrap(),
- replica_id,
- capability,
- "",
- );
- buffer.set_language_registry(language_registry.clone());
- buffer
- });
- let edits_since_last_slash_command_parse =
- buffer.update(cx, |buffer, _| buffer.subscribe());
- let mut this = Self {
- id,
- timestamp: clock::Lamport::new(replica_id),
- version: clock::Global::new(),
- pending_ops: Vec::new(),
- operations: Vec::new(),
- message_anchors: Default::default(),
- contents: Default::default(),
- messages_metadata: Default::default(),
- parsed_slash_commands: Vec::new(),
- invoked_slash_commands: HashMap::default(),
- slash_command_output_sections: Vec::new(),
- thought_process_output_sections: Vec::new(),
- edits_since_last_parse: edits_since_last_slash_command_parse,
- summary: TextThreadSummary::Pending,
- summary_task: Task::ready(None),
- completion_count: Default::default(),
- pending_completions: Default::default(),
- token_count: None,
- pending_token_count: Task::ready(None),
- pending_cache_warming_task: Task::ready(None),
- _subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)],
- pending_save: Task::ready(Ok(())),
- path: None,
- buffer,
- language_registry,
- slash_commands,
- prompt_builder,
- };
-
- let first_message_id = MessageId(clock::Lamport {
- replica_id: ReplicaId::LOCAL,
- value: 0,
- });
- let message = MessageAnchor {
- id: first_message_id,
- start: language::Anchor::min_for_buffer(this.buffer.read(cx).remote_id()),
- };
- this.messages_metadata.insert(
- first_message_id,
- MessageMetadata {
- role: Role::User,
- status: MessageStatus::Done,
- timestamp: first_message_id.0,
- cache: None,
- },
- );
- this.message_anchors.push(message);
-
- this.set_language(cx);
- this.count_remaining_tokens(cx);
- this
- }
-
- pub(crate) fn serialize(&self, cx: &App) -> SavedTextThread {
- let buffer = self.buffer.read(cx);
- SavedTextThread {
- id: Some(self.id.clone()),
- zed: "context".into(),
- version: SavedTextThread::VERSION.into(),
- text: buffer.text(),
- messages: self
- .messages(cx)
- .map(|message| SavedMessage {
- id: message.id,
- start: message.offset_range.start,
- metadata: self.messages_metadata[&message.id].clone(),
- })
- .collect(),
- summary: self
- .summary
- .content()
- .map(|summary| summary.text.clone())
- .unwrap_or_default(),
- slash_command_output_sections: self
- .slash_command_output_sections
- .iter()
- .filter_map(|section| {
- if section.is_valid(buffer) {
- let range = section.range.to_offset(buffer);
- Some(assistant_slash_command::SlashCommandOutputSection {
- range,
- icon: section.icon,
- label: section.label.clone(),
- metadata: section.metadata.clone(),
- })
- } else {
- None
- }
- })
- .collect(),
- thought_process_output_sections: self
- .thought_process_output_sections
- .iter()
- .filter_map(|section| {
- if section.is_valid(buffer) {
- let range = section.range.to_offset(buffer);
- Some(ThoughtProcessOutputSection { range })
- } else {
- None
- }
- })
- .collect(),
- }
- }
-
- pub fn deserialize(
- saved_context: SavedTextThread,
- path: Arc<Path>,
- language_registry: Arc<LanguageRegistry>,
- prompt_builder: Arc<PromptBuilder>,
- slash_commands: Arc<SlashCommandWorkingSet>,
- cx: &mut Context<Self>,
- ) -> Self {
- let id = saved_context.id.clone().unwrap_or_else(TextThreadId::new);
- let mut this = Self::new(
- id,
- ReplicaId::default(),
- language::Capability::ReadWrite,
- language_registry,
- prompt_builder,
- slash_commands,
- cx,
- );
- this.path = Some(path);
- this.buffer.update(cx, |buffer, cx| {
- buffer.set_text(saved_context.text.as_str(), cx)
- });
- let operations = saved_context.into_ops(&this.buffer, cx);
- this.apply_ops(operations, cx);
- this
- }
-
- pub fn id(&self) -> &TextThreadId {
- &self.id
- }
-
- pub fn replica_id(&self) -> ReplicaId {
- self.timestamp.replica_id
- }
-
- pub fn version(&self, cx: &App) -> TextThreadVersion {
- TextThreadVersion {
- text_thread: self.version.clone(),
- buffer: self.buffer.read(cx).version(),
- }
- }
-
- pub fn slash_commands(&self) -> &Arc<SlashCommandWorkingSet> {
- &self.slash_commands
- }
-
- pub fn set_capability(&mut self, capability: language::Capability, cx: &mut Context<Self>) {
- self.buffer
- .update(cx, |buffer, cx| buffer.set_capability(capability, cx));
- }
-
- fn next_timestamp(&mut self) -> clock::Lamport {
- let timestamp = self.timestamp.tick();
- self.version.observe(timestamp);
- timestamp
- }
-
- pub fn serialize_ops(
- &self,
- since: &TextThreadVersion,
- cx: &App,
- ) -> Task<Vec<proto::ContextOperation>> {
- let buffer_ops = self
- .buffer
- .read(cx)
- .serialize_ops(Some(since.buffer.clone()), cx);
-
- let mut context_ops = self
- .operations
- .iter()
- .filter(|op| !since.text_thread.observed(op.timestamp()))
- .cloned()
- .collect::<Vec<_>>();
- context_ops.extend(self.pending_ops.iter().cloned());
-
- cx.background_spawn(async move {
- let buffer_ops = buffer_ops.await;
- context_ops.sort_unstable_by_key(|op| op.timestamp());
- buffer_ops
- .into_iter()
- .map(|op| proto::ContextOperation {
- variant: Some(proto::context_operation::Variant::BufferOperation(
- proto::context_operation::BufferOperation {
- operation: Some(op),
- },
- )),
- })
- .chain(context_ops.into_iter().map(|op| op.to_proto()))
- .collect()
- })
- }
-
- pub fn apply_ops(
- &mut self,
- ops: impl IntoIterator<Item = TextThreadOperation>,
- cx: &mut Context<Self>,
- ) {
- let mut buffer_ops = Vec::new();
- for op in ops {
- match op {
- TextThreadOperation::BufferOperation(buffer_op) => buffer_ops.push(buffer_op),
- op @ _ => self.pending_ops.push(op),
- }
- }
- self.buffer
- .update(cx, |buffer, cx| buffer.apply_ops(buffer_ops, cx));
- self.flush_ops(cx);
- }
-
- fn flush_ops(&mut self, cx: &mut Context<TextThread>) {
- let mut changed_messages = HashSet::default();
- let mut summary_generated = false;
-
- self.pending_ops.sort_unstable_by_key(|op| op.timestamp());
- for op in mem::take(&mut self.pending_ops) {
- if !self.can_apply_op(&op, cx) {
- self.pending_ops.push(op);
- continue;
- }
-
- let timestamp = op.timestamp();
- match op.clone() {
- TextThreadOperation::InsertMessage {
- anchor, metadata, ..
- } => {
- if self.messages_metadata.contains_key(&anchor.id) {
- // We already applied this operation.
- } else {
- changed_messages.insert(anchor.id);
- self.insert_message(anchor, metadata, cx);
- }
- }
- TextThreadOperation::UpdateMessage {
- message_id,
- metadata: new_metadata,
- ..
- } => {
- let metadata = self.messages_metadata.get_mut(&message_id).unwrap();
- if new_metadata.timestamp > metadata.timestamp {
- *metadata = new_metadata;
- changed_messages.insert(message_id);
- }
- }
- TextThreadOperation::UpdateSummary {
- summary: new_summary,
- ..
- } => {
- if self
- .summary
- .timestamp()
- .is_none_or(|current_timestamp| new_summary.timestamp > current_timestamp)
- {
- self.summary = TextThreadSummary::Content(new_summary);
- summary_generated = true;
- }
- }
- TextThreadOperation::SlashCommandStarted {
- id,
- output_range,
- name,
- ..
- } => {
- self.invoked_slash_commands.insert(
- id,
- InvokedSlashCommand {
- name: name.into(),
- range: output_range,
- run_commands_in_ranges: Vec::new(),
- status: InvokedSlashCommandStatus::Running(Task::ready(())),
- transaction: None,
- timestamp: id.0,
- },
- );
- cx.emit(TextThreadEvent::InvokedSlashCommandChanged { command_id: id });
- }
- TextThreadOperation::SlashCommandOutputSectionAdded { section, .. } => {
- let buffer = self.buffer.read(cx);
- if let Err(ix) = self
- .slash_command_output_sections
- .binary_search_by(|probe| probe.range.cmp(§ion.range, buffer))
- {
- self.slash_command_output_sections
- .insert(ix, section.clone());
- cx.emit(TextThreadEvent::SlashCommandOutputSectionAdded { section });
- }
- }
- TextThreadOperation::ThoughtProcessOutputSectionAdded { section, .. } => {
- let buffer = self.buffer.read(cx);
- if let Err(ix) = self
- .thought_process_output_sections
- .binary_search_by(|probe| probe.range.cmp(§ion.range, buffer))
- {
- self.thought_process_output_sections
- .insert(ix, section.clone());
- }
- }
- TextThreadOperation::SlashCommandFinished {
- id,
- error_message,
- timestamp,
- ..
- } => {
- if let Some(slash_command) = self.invoked_slash_commands.get_mut(&id)
- && timestamp > slash_command.timestamp
- {
- slash_command.timestamp = timestamp;
- match error_message {
- Some(message) => {
- slash_command.status =
- InvokedSlashCommandStatus::Error(message.into());
- }
- None => {
- slash_command.status = InvokedSlashCommandStatus::Finished;
- }
- }
- cx.emit(TextThreadEvent::InvokedSlashCommandChanged { command_id: id });
- }
- }
- TextThreadOperation::BufferOperation(_) => unreachable!(),
- }
-
- self.version.observe(timestamp);
- self.timestamp.observe(timestamp);
- self.operations.push(op);
- }
-
- if !changed_messages.is_empty() {
- self.message_roles_updated(changed_messages, cx);
- cx.emit(TextThreadEvent::MessagesEdited);
- cx.notify();
- }
-
- if summary_generated {
- cx.emit(TextThreadEvent::SummaryChanged);
- cx.emit(TextThreadEvent::SummaryGenerated);
- cx.notify();
- }
- }
-
- fn can_apply_op(&self, op: &TextThreadOperation, cx: &App) -> bool {
- if !self.version.observed_all(op.version()) {
- return false;
- }
-
- match op {
- TextThreadOperation::InsertMessage { anchor, .. } => self
- .buffer
- .read(cx)
- .version
- .observed(anchor.start.timestamp()),
- TextThreadOperation::UpdateMessage { message_id, .. } => {
- self.messages_metadata.contains_key(message_id)
- }
- TextThreadOperation::UpdateSummary { .. } => true,
- TextThreadOperation::SlashCommandStarted { output_range, .. } => {
- self.has_received_operations_for_anchor_range(output_range.clone(), cx)
- }
- TextThreadOperation::SlashCommandOutputSectionAdded { section, .. } => {
- self.has_received_operations_for_anchor_range(section.range.clone(), cx)
- }
- TextThreadOperation::ThoughtProcessOutputSectionAdded { section, .. } => {
- self.has_received_operations_for_anchor_range(section.range.clone(), cx)
- }
- TextThreadOperation::SlashCommandFinished { .. } => true,
- TextThreadOperation::BufferOperation(_) => {
- panic!("buffer operations should always be applied")
- }
- }
- }
-
- fn has_received_operations_for_anchor_range(
- &self,
- range: Range<text::Anchor>,
- cx: &App,
- ) -> bool {
- let version = &self.buffer.read(cx).version;
- let observed_start = range.start.is_min()
- || range.start.is_max()
- || version.observed(range.start.timestamp());
- let observed_end =
- range.end.is_min() || range.end.is_max() || version.observed(range.end.timestamp());
- observed_start && observed_end
- }
-
- fn push_op(&mut self, op: TextThreadOperation, cx: &mut Context<Self>) {
- self.operations.push(op.clone());
- cx.emit(TextThreadEvent::Operation(op));
- }
-
- pub fn buffer(&self) -> &Entity<Buffer> {
- &self.buffer
- }
-
- pub fn language_registry(&self) -> Arc<LanguageRegistry> {
- self.language_registry.clone()
- }
-
- pub fn prompt_builder(&self) -> Arc<PromptBuilder> {
- self.prompt_builder.clone()
- }
-
- pub fn path(&self) -> Option<&Arc<Path>> {
- self.path.as_ref()
- }
-
- pub fn summary(&self) -> &TextThreadSummary {
- &self.summary
- }
-
- pub fn parsed_slash_commands(&self) -> &[ParsedSlashCommand] {
- &self.parsed_slash_commands
- }
-
- pub fn invoked_slash_command(
- &self,
- command_id: &InvokedSlashCommandId,
- ) -> Option<&InvokedSlashCommand> {
- self.invoked_slash_commands.get(command_id)
- }
-
- pub fn slash_command_output_sections(&self) -> &[SlashCommandOutputSection<language::Anchor>] {
- &self.slash_command_output_sections
- }
-
- pub fn thought_process_output_sections(
- &self,
- ) -> &[ThoughtProcessOutputSection<language::Anchor>] {
- &self.thought_process_output_sections
- }
-
- pub fn contains_files(&self, cx: &App) -> bool {
- // Mimics assistant_slash_commands::FileCommandMetadata.
- #[derive(Serialize, Deserialize)]
- pub struct FileCommandMetadata {
- pub path: String,
- }
- let buffer = self.buffer.read(cx);
- self.slash_command_output_sections.iter().any(|section| {
- section.is_valid(buffer)
- && section
- .metadata
- .as_ref()
- .and_then(|metadata| {
- serde_json::from_value::<FileCommandMetadata>(metadata.clone()).ok()
- })
- .is_some()
- })
- }
-
- fn set_language(&mut self, cx: &mut Context<Self>) {
- let markdown = self.language_registry.language_for_name("Markdown");
- cx.spawn(async move |this, cx| {
- let markdown = markdown.await?;
- this.update(cx, |this, cx| {
- this.buffer
- .update(cx, |buffer, cx| buffer.set_language(Some(markdown), cx));
- })
- })
- .detach_and_log_err(cx);
- }
-
- fn handle_buffer_event(
- &mut self,
- _: Entity<Buffer>,
- event: &language::BufferEvent,
- cx: &mut Context<Self>,
- ) {
- match event {
- language::BufferEvent::Operation {
- operation,
- is_local: true,
- } => cx.emit(TextThreadEvent::Operation(
- TextThreadOperation::BufferOperation(operation.clone()),
- )),
- language::BufferEvent::Edited { .. } => {
- self.count_remaining_tokens(cx);
- self.reparse(cx);
- cx.emit(TextThreadEvent::MessagesEdited);
- }
- _ => {}
- }
- }
-
- pub fn token_count(&self) -> Option<u64> {
- self.token_count
- }
-
- pub(crate) fn count_remaining_tokens(&mut self, cx: &mut Context<Self>) {
- // Assume it will be a Chat request, even though that takes fewer tokens (and risks going over the limit),
- // because otherwise you see in the UI that your empty message has a bunch of tokens already used.
- let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
- return;
- };
- let request = self.to_completion_request(Some(&model.model), cx);
- let debounce = self.token_count.is_some();
- self.pending_token_count = cx.spawn(async move |this, cx| {
- async move {
- if debounce {
- cx.background_executor()
- .timer(Duration::from_millis(200))
- .await;
- }
-
- let token_count = cx
- .update(|cx| model.model.count_tokens(request, cx))
- .await?;
- this.update(cx, |this, cx| {
- this.token_count = Some(token_count);
- this.start_cache_warming(&model.model, cx);
- cx.notify()
- })
- }
- .log_err()
- .await
- });
- }
-
- pub fn mark_cache_anchors(
- &mut self,
- cache_configuration: &Option<LanguageModelCacheConfiguration>,
- speculative: bool,
- cx: &mut Context<Self>,
- ) -> bool {
- let cache_configuration =
- cache_configuration
- .as_ref()
- .unwrap_or(&LanguageModelCacheConfiguration {
- max_cache_anchors: 0,
- should_speculate: false,
- min_total_token: 0,
- });
-
- let messages: Vec<Message> = self.messages(cx).collect();
-
- let mut sorted_messages = messages.clone();
- if speculative {
- // Avoid caching the last message if this is a speculative cache fetch as
- // it's likely to change.
- sorted_messages.pop();
- }
- sorted_messages.retain(|m| m.role == Role::User);
- sorted_messages.sort_by(|a, b| b.offset_range.len().cmp(&a.offset_range.len()));
-
- let cache_anchors = if self.token_count.unwrap_or(0) < cache_configuration.min_total_token {
- // If we have't hit the minimum threshold to enable caching, don't cache anything.
- 0
- } else {
- // Save 1 anchor for the inline assistant to use.
- max(cache_configuration.max_cache_anchors, 1) - 1
- };
- sorted_messages.truncate(cache_anchors);
-
- let anchors: HashSet<MessageId> = sorted_messages
- .into_iter()
- .map(|message| message.id)
- .collect();
-
- let buffer = self.buffer.read(cx).snapshot();
- let invalidated_caches: HashSet<MessageId> = messages
- .iter()
- .scan(false, |encountered_invalid, message| {
- let message_id = message.id;
- let is_invalid = self
- .messages_metadata
- .get(&message_id)
- .is_none_or(|metadata| {
- !metadata.is_cache_valid(&buffer, &message.offset_range)
- || *encountered_invalid
- });
- *encountered_invalid |= is_invalid;
- Some(if is_invalid { Some(message_id) } else { None })
- })
- .flatten()
- .collect();
-
- let last_anchor = messages.iter().rev().find_map(|message| {
- if anchors.contains(&message.id) {
- Some(message.id)
- } else {
- None
- }
- });
-
- let mut new_anchor_needs_caching = false;
- let current_version = &buffer.version;
- // If we have no anchors, mark all messages as not being cached.
- let mut hit_last_anchor = last_anchor.is_none();
-
- for message in messages.iter() {
- if hit_last_anchor {
- self.update_metadata(message.id, cx, |metadata| metadata.cache = None);
- continue;
- }
-
- if let Some(last_anchor) = last_anchor
- && message.id == last_anchor
- {
- hit_last_anchor = true;
- }
-
- new_anchor_needs_caching = new_anchor_needs_caching
- || (invalidated_caches.contains(&message.id) && anchors.contains(&message.id));
-
- self.update_metadata(message.id, cx, |metadata| {
- let cache_status = if invalidated_caches.contains(&message.id) {
- CacheStatus::Pending
- } else {
- metadata
- .cache
- .as_ref()
- .map_or(CacheStatus::Pending, |cm| cm.status.clone())
- };
- metadata.cache = Some(MessageCacheMetadata {
- is_anchor: anchors.contains(&message.id),
- is_final_anchor: hit_last_anchor,
- status: cache_status,
- cached_at: current_version.clone(),
- });
- });
- }
- new_anchor_needs_caching
- }
-
- fn start_cache_warming(&mut self, model: &Arc<dyn LanguageModel>, cx: &mut Context<Self>) {
- let cache_configuration = model.cache_configuration();
-
- if !self.mark_cache_anchors(&cache_configuration, true, cx) {
- return;
- }
- if !self.pending_completions.is_empty() {
- return;
- }
- if let Some(cache_configuration) = cache_configuration
- && !cache_configuration.should_speculate
- {
- return;
- }
-
- let request = {
- let mut req = self.to_completion_request(Some(model), cx);
- // Skip the last message because it's likely to change and
- // therefore would be a waste to cache.
- req.messages.pop();
- req.messages.push(LanguageModelRequestMessage {
- role: Role::User,
- content: vec!["Respond only with OK, nothing else.".into()],
- cache: false,
- reasoning_details: None,
- });
- req
- };
-
- let model = Arc::clone(model);
- self.pending_cache_warming_task = cx.spawn(async move |this, cx| {
- async move {
- match model.stream_completion(request, cx).await {
- Ok(mut stream) => {
- stream.next().await;
- log::info!("Cache warming completed successfully");
- }
- Err(e) => {
- log::warn!("Cache warming failed: {}", e);
- }
- };
- this.update(cx, |this, cx| {
- this.update_cache_status_for_completion(cx);
- })
- .ok();
- anyhow::Ok(())
- }
- .log_err()
- .await
- });
- }
-
- pub fn update_cache_status_for_completion(&mut self, cx: &mut Context<Self>) {
- let cached_message_ids: Vec<MessageId> = self
- .messages_metadata
- .iter()
- .filter_map(|(message_id, metadata)| {
- metadata.cache.as_ref().and_then(|cache| {
- if cache.status == CacheStatus::Pending {
- Some(*message_id)
- } else {
- None
- }
- })
- })
- .collect();
-
- for message_id in cached_message_ids {
- self.update_metadata(message_id, cx, |metadata| {
- if let Some(cache) = &mut metadata.cache {
- cache.status = CacheStatus::Cached;
- }
- });
- }
- cx.notify();
- }
-
- pub fn reparse(&mut self, cx: &mut Context<Self>) {
- let buffer = self.buffer.read(cx).text_snapshot();
- let mut row_ranges = self
- .edits_since_last_parse
- .consume()
- .into_iter()
- .map(|edit| {
- let start_row = buffer.offset_to_point(edit.new.start).row;
- let end_row = buffer.offset_to_point(edit.new.end).row + 1;
- start_row..end_row
- })
- .peekable();
-
- let mut removed_parsed_slash_command_ranges = Vec::new();
- let mut updated_parsed_slash_commands = Vec::new();
- while let Some(mut row_range) = row_ranges.next() {
- while let Some(next_row_range) = row_ranges.peek() {
- if row_range.end >= next_row_range.start {
- row_range.end = next_row_range.end;
- row_ranges.next();
- } else {
- break;
- }
- }
-
- let start = buffer.anchor_before(Point::new(row_range.start, 0));
- let end = buffer.anchor_after(Point::new(
- row_range.end - 1,
- buffer.line_len(row_range.end - 1),
- ));
-
- self.reparse_slash_commands_in_range(
- start..end,
- &buffer,
- &mut updated_parsed_slash_commands,
- &mut removed_parsed_slash_command_ranges,
- cx,
- );
- self.invalidate_pending_slash_commands(&buffer, cx);
- }
-
- if !updated_parsed_slash_commands.is_empty()
- || !removed_parsed_slash_command_ranges.is_empty()
- {
- cx.emit(TextThreadEvent::ParsedSlashCommandsUpdated {
- removed: removed_parsed_slash_command_ranges,
- updated: updated_parsed_slash_commands,
- });
- }
- }
-
- fn reparse_slash_commands_in_range(
- &mut self,
- range: Range<text::Anchor>,
- buffer: &BufferSnapshot,
- updated: &mut Vec<ParsedSlashCommand>,
- removed: &mut Vec<Range<text::Anchor>>,
- cx: &App,
- ) {
- let old_range = self.pending_command_indices_for_range(range.clone(), cx);
-
- let mut new_commands = Vec::new();
- let mut lines = buffer.text_for_range(range).lines();
- let mut offset = lines.offset();
- while let Some(line) = lines.next() {
- if let Some(command_line) = SlashCommandLine::parse(line) {
- let name = &line[command_line.name.clone()];
- let arguments = command_line
- .arguments
- .iter()
- .filter_map(|argument_range| {
- if argument_range.is_empty() {
- None
- } else {
- line.get(argument_range.clone())
- }
- })
- .map(ToOwned::to_owned)
- .collect::<SmallVec<_>>();
- if let Some(command) = self.slash_commands.command(name, cx)
- && (!command.requires_argument() || !arguments.is_empty())
- {
- let start_ix = offset + command_line.name.start - 1;
- let end_ix = offset
- + command_line
- .arguments
- .last()
- .map_or(command_line.name.end, |argument| argument.end);
- let source_range = buffer.anchor_after(start_ix)..buffer.anchor_after(end_ix);
- let pending_command = ParsedSlashCommand {
- name: name.to_string(),
- arguments,
- source_range,
- status: PendingSlashCommandStatus::Idle,
- };
- updated.push(pending_command.clone());
- new_commands.push(pending_command);
- }
- }
-
- offset = lines.offset();
- }
-
- let removed_commands = self.parsed_slash_commands.splice(old_range, new_commands);
- removed.extend(removed_commands.map(|command| command.source_range));
- }
-
- fn invalidate_pending_slash_commands(
- &mut self,
- buffer: &BufferSnapshot,
- cx: &mut Context<Self>,
- ) {
- let mut invalidated_command_ids = Vec::new();
- for (&command_id, command) in self.invoked_slash_commands.iter_mut() {
- if !matches!(command.status, InvokedSlashCommandStatus::Finished)
- && (!command.range.start.is_valid(buffer) || !command.range.end.is_valid(buffer))
- {
- command.status = InvokedSlashCommandStatus::Finished;
- cx.emit(TextThreadEvent::InvokedSlashCommandChanged { command_id });
- invalidated_command_ids.push(command_id);
- }
- }
-
- for command_id in invalidated_command_ids {
- let version = self.version.clone();
- let timestamp = self.next_timestamp();
- self.push_op(
- TextThreadOperation::SlashCommandFinished {
- id: command_id,
- timestamp,
- error_message: None,
- version: version.clone(),
- },
- cx,
- );
- }
- }
-
- pub fn pending_command_for_position(
- &mut self,
- position: language::Anchor,
- cx: &mut Context<Self>,
- ) -> Option<&mut ParsedSlashCommand> {
- let buffer = self.buffer.read(cx);
- match self
- .parsed_slash_commands
- .binary_search_by(|probe| probe.source_range.end.cmp(&position, buffer))
- {
- Ok(ix) => Some(&mut self.parsed_slash_commands[ix]),
- Err(ix) => {
- let cmd = self.parsed_slash_commands.get_mut(ix)?;
- if position.cmp(&cmd.source_range.start, buffer).is_ge()
- && position.cmp(&cmd.source_range.end, buffer).is_le()
- {
- Some(cmd)
- } else {
- None
- }
- }
- }
- }
-
- pub fn pending_commands_for_range(
- &self,
- range: Range<language::Anchor>,
- cx: &App,
- ) -> &[ParsedSlashCommand] {
- let range = self.pending_command_indices_for_range(range, cx);
- &self.parsed_slash_commands[range]
- }
-
- fn pending_command_indices_for_range(
- &self,
- range: Range<language::Anchor>,
- cx: &App,
- ) -> Range<usize> {
- self.indices_intersecting_buffer_range(&self.parsed_slash_commands, range, cx)
- }
-
- fn indices_intersecting_buffer_range<T: ContextAnnotation>(
- &self,
- all_annotations: &[T],
- range: Range<language::Anchor>,
- cx: &App,
- ) -> Range<usize> {
- let buffer = self.buffer.read(cx);
- let start_ix = match all_annotations
- .binary_search_by(|probe| probe.range().end.cmp(&range.start, buffer))
- {
- Ok(ix) | Err(ix) => ix,
- };
- let end_ix = match all_annotations
- .binary_search_by(|probe| probe.range().start.cmp(&range.end, buffer))
- {
- Ok(ix) => ix + 1,
- Err(ix) => ix,
- };
- start_ix..end_ix
- }
-
- pub fn insert_command_output(
- &mut self,
- command_source_range: Range<language::Anchor>,
- name: &str,
- output: Task<SlashCommandResult>,
- ensure_trailing_newline: bool,
- cx: &mut Context<Self>,
- ) {
- let version = self.version.clone();
- let command_id = InvokedSlashCommandId(self.next_timestamp());
-
- const PENDING_OUTPUT_END_MARKER: &str = "β¦";
-
- let (command_range, command_source_range, insert_position, first_transaction) =
- self.buffer.update(cx, |buffer, cx| {
- let command_source_range = command_source_range.to_offset(buffer);
- let mut insertion = format!("\n{PENDING_OUTPUT_END_MARKER}");
- if ensure_trailing_newline {
- insertion.push('\n');
- }
-
- buffer.finalize_last_transaction();
- buffer.start_transaction();
- buffer.edit(
- [(
- command_source_range.end..command_source_range.end,
- insertion,
- )],
- None,
- cx,
- );
- let first_transaction = buffer.end_transaction(cx).unwrap();
- buffer.finalize_last_transaction();
-
- let insert_position = buffer.anchor_after(command_source_range.end + 1);
- let command_range = buffer.anchor_after(command_source_range.start)
- ..buffer.anchor_before(
- command_source_range.end + 1 + PENDING_OUTPUT_END_MARKER.len(),
- );
- let command_source_range = buffer.anchor_before(command_source_range.start)
- ..buffer.anchor_before(command_source_range.end + 1);
- (
- command_range,
- command_source_range,
- insert_position,
- first_transaction,
- )
- });
- self.reparse(cx);
-
- let insert_output_task = cx.spawn(async move |this, cx| {
- let run_command = async {
- let mut stream = output.await?;
-
- struct PendingSection {
- start: language::Anchor,
- icon: IconName,
- label: SharedString,
- metadata: Option<serde_json::Value>,
- }
-
- let mut pending_section_stack: Vec<PendingSection> = Vec::new();
- let mut last_role: Option<Role> = None;
- let mut last_section_range = None;
-
- while let Some(event) = stream.next().await {
- let event = event?;
- this.update(cx, |this, cx| {
- this.buffer.update(cx, |buffer, _cx| {
- buffer.finalize_last_transaction();
- buffer.start_transaction()
- });
-
- match event {
- SlashCommandEvent::StartMessage {
- role,
- merge_same_roles,
- } => {
- if !merge_same_roles && Some(role) != last_role {
- let buffer = this.buffer.read(cx);
- let offset = insert_position.to_offset(buffer);
- this.insert_message_at_offset(
- offset,
- role,
- MessageStatus::Pending,
- cx,
- );
- }
-
- last_role = Some(role);
- }
- SlashCommandEvent::StartSection {
- icon,
- label,
- metadata,
- } => {
- this.buffer.update(cx, |buffer, cx| {
- let insert_point = insert_position.to_point(buffer);
- if insert_point.column > 0 {
- buffer.edit([(insert_point..insert_point, "\n")], None, cx);
- }
-
- pending_section_stack.push(PendingSection {
- start: buffer.anchor_before(insert_position),
- icon,
- label,
- metadata,
- });
- });
- }
- SlashCommandEvent::Content(SlashCommandContent::Text {
- text,
- run_commands_in_text,
- }) => {
- let start = this.buffer.read(cx).anchor_before(insert_position);
-
- this.buffer.update(cx, |buffer, cx| {
- buffer.edit(
- [(insert_position..insert_position, text)],
- None,
- cx,
- )
- });
-
- let end = this.buffer.read(cx).anchor_before(insert_position);
- if run_commands_in_text
- && let Some(invoked_slash_command) =
- this.invoked_slash_commands.get_mut(&command_id)
- {
- invoked_slash_command
- .run_commands_in_ranges
- .push(start..end);
- }
- }
- SlashCommandEvent::EndSection => {
- if let Some(pending_section) = pending_section_stack.pop() {
- let offset_range = (pending_section.start..insert_position)
- .to_offset(this.buffer.read(cx));
- if !offset_range.is_empty() {
- let range = this.buffer.update(cx, |buffer, _cx| {
- buffer.anchor_after(offset_range.start)
- ..buffer.anchor_before(offset_range.end)
- });
- this.insert_slash_command_output_section(
- SlashCommandOutputSection {
- range: range.clone(),
- icon: pending_section.icon,
- label: pending_section.label,
- metadata: pending_section.metadata,
- },
- cx,
- );
- last_section_range = Some(range);
- }
- }
- }
- }
-
- this.buffer.update(cx, |buffer, cx| {
- if let Some(event_transaction) = buffer.end_transaction(cx) {
- buffer.merge_transactions(event_transaction, first_transaction);
- }
- });
- })?;
- }
-
- this.update(cx, |this, cx| {
- this.buffer.update(cx, |buffer, cx| {
- buffer.finalize_last_transaction();
- buffer.start_transaction();
-
- let mut deletions = vec![(command_source_range.to_offset(buffer), "")];
- let insert_position = insert_position.to_offset(buffer);
- let command_range_end = command_range.end.to_offset(buffer);
-
- if buffer.contains_str_at(insert_position, PENDING_OUTPUT_END_MARKER) {
- deletions.push((
- insert_position..insert_position + PENDING_OUTPUT_END_MARKER.len(),
- "",
- ));
- }
-
- if ensure_trailing_newline
- && buffer
- .chars_at(command_range_end)
- .next()
- .is_some_and(|c| c == '\n')
- {
- if let Some((prev_char, '\n')) =
- buffer.reversed_chars_at(insert_position).next_tuple()
- && last_section_range.is_none_or(|last_section_range| {
- !last_section_range
- .to_offset(buffer)
- .contains(&(insert_position - prev_char.len_utf8()))
- })
- {
- deletions.push((command_range_end..command_range_end + 1, ""));
- }
- }
-
- buffer.edit(deletions, None, cx);
-
- if let Some(deletion_transaction) = buffer.end_transaction(cx) {
- buffer.merge_transactions(deletion_transaction, first_transaction);
- }
- });
- })?;
-
- debug_assert!(pending_section_stack.is_empty());
-
- anyhow::Ok(())
- };
-
- let command_result = run_command.await;
-
- this.update(cx, |this, cx| {
- let version = this.version.clone();
- let timestamp = this.next_timestamp();
- let Some(invoked_slash_command) = this.invoked_slash_commands.get_mut(&command_id)
- else {
- return;
- };
- let mut error_message = None;
- match command_result {
- Ok(()) => {
- invoked_slash_command.status = InvokedSlashCommandStatus::Finished;
- }
- Err(error) => {
- let message = error.to_string();
- invoked_slash_command.status =
- InvokedSlashCommandStatus::Error(message.clone().into());
- error_message = Some(message);
- }
- }
-
- cx.emit(TextThreadEvent::InvokedSlashCommandChanged { command_id });
- this.push_op(
- TextThreadOperation::SlashCommandFinished {
- id: command_id,
- timestamp,
- error_message,
- version,
- },
- cx,
- );
- })
- .ok();
- });
-
- self.invoked_slash_commands.insert(
- command_id,
- InvokedSlashCommand {
- name: name.to_string().into(),
- range: command_range.clone(),
- run_commands_in_ranges: Vec::new(),
- status: InvokedSlashCommandStatus::Running(insert_output_task),
- transaction: Some(first_transaction),
- timestamp: command_id.0,
- },
- );
- cx.emit(TextThreadEvent::InvokedSlashCommandChanged { command_id });
- self.push_op(
- TextThreadOperation::SlashCommandStarted {
- id: command_id,
- output_range: command_range,
- name: name.to_string(),
- version,
- },
- cx,
- );
- }
-
- fn insert_slash_command_output_section(
- &mut self,
- section: SlashCommandOutputSection<language::Anchor>,
- cx: &mut Context<Self>,
- ) {
- let buffer = self.buffer.read(cx);
- let insertion_ix = match self
- .slash_command_output_sections
- .binary_search_by(|probe| probe.range.cmp(§ion.range, buffer))
- {
- Ok(ix) | Err(ix) => ix,
- };
- self.slash_command_output_sections
- .insert(insertion_ix, section.clone());
- cx.emit(TextThreadEvent::SlashCommandOutputSectionAdded {
- section: section.clone(),
- });
- let version = self.version.clone();
- let timestamp = self.next_timestamp();
- self.push_op(
- TextThreadOperation::SlashCommandOutputSectionAdded {
- timestamp,
- section,
- version,
- },
- cx,
- );
- }
-
- fn insert_thought_process_output_section(
- &mut self,
- section: ThoughtProcessOutputSection<language::Anchor>,
- cx: &mut Context<Self>,
- ) {
- let buffer = self.buffer.read(cx);
- let insertion_ix = match self
- .thought_process_output_sections
- .binary_search_by(|probe| probe.range.cmp(§ion.range, buffer))
- {
- Ok(ix) | Err(ix) => ix,
- };
- self.thought_process_output_sections
- .insert(insertion_ix, section.clone());
- // cx.emit(ContextEvent::ThoughtProcessOutputSectionAdded {
- // section: section.clone(),
- // });
- let version = self.version.clone();
- let timestamp = self.next_timestamp();
- self.push_op(
- TextThreadOperation::ThoughtProcessOutputSectionAdded {
- timestamp,
- section,
- version,
- },
- cx,
- );
- }
-
- pub fn completion_provider_changed(&mut self, cx: &mut Context<Self>) {
- self.count_remaining_tokens(cx);
- }
-
- fn get_last_valid_message_id(&self, cx: &Context<Self>) -> Option<MessageId> {
- self.message_anchors.iter().rev().find_map(|message| {
- message
- .start
- .is_valid(self.buffer.read(cx))
- .then_some(message.id)
- })
- }
-
- pub fn assist(&mut self, cx: &mut Context<Self>) -> Option<MessageAnchor> {
- let model_registry = LanguageModelRegistry::read_global(cx);
- let model = model_registry.default_model()?;
- let last_message_id = self.get_last_valid_message_id(cx)?;
-
- if !model.provider.is_authenticated(cx) {
- log::info!("completion provider has no credentials");
- return None;
- }
-
- let model = model.model;
-
- // Compute which messages to cache, including the last one.
- self.mark_cache_anchors(&model.cache_configuration(), false, cx);
-
- let request = self.to_completion_request(Some(&model), cx);
-
- let assistant_message = self
- .insert_message_after(last_message_id, Role::Assistant, MessageStatus::Pending, cx)
- .unwrap();
-
- // Queue up the user's next reply.
- let user_message = self
- .insert_message_after(assistant_message.id, Role::User, MessageStatus::Done, cx)
- .unwrap();
-
- let pending_completion_id = post_inc(&mut self.completion_count);
-
- let task = cx.spawn({
- async move |this, cx| {
- let stream = model.stream_completion(request, cx);
- let assistant_message_id = assistant_message.id;
- let mut response_latency = None;
- let stream_completion = async {
- let request_start = Instant::now();
- let mut events = stream.await?;
- let mut stop_reason = StopReason::EndTurn;
- let mut thought_process_stack = Vec::new();
-
- const THOUGHT_PROCESS_START_MARKER: &str = "<think>\n";
- const THOUGHT_PROCESS_END_MARKER: &str = "\n</think>";
-
- while let Some(event) = events.next().await {
- if response_latency.is_none() {
- response_latency = Some(request_start.elapsed());
- }
- let event = event?;
-
- let mut context_event = None;
- let mut thought_process_output_section = None;
-
- this.update(cx, |this, cx| {
- let message_ix = this
- .message_anchors
- .iter()
- .position(|message| message.id == assistant_message_id)?;
- this.buffer.update(cx, |buffer, cx| {
- let message_old_end_offset = this.message_anchors[message_ix + 1..]
- .iter()
- .find(|message| message.start.is_valid(buffer))
- .map_or(buffer.len(), |message| {
- message.start.to_offset(buffer).saturating_sub(1)
- });
-
- match event {
- LanguageModelCompletionEvent::Started |
- LanguageModelCompletionEvent::Queued {..} => {}
- LanguageModelCompletionEvent::StartMessage { .. } => {}
- LanguageModelCompletionEvent::ReasoningDetails(_) => {
- // ReasoningDetails are metadata (signatures, encrypted data, format info)
- // used for request/response validation, not UI content.
- // The displayable thinking text is already handled by the Thinking event.
- }
- LanguageModelCompletionEvent::Stop(reason) => {
- stop_reason = reason;
- }
- LanguageModelCompletionEvent::Thinking { text: chunk, .. } => {
- if thought_process_stack.is_empty() {
- let start =
- buffer.anchor_before(message_old_end_offset);
- thought_process_stack.push(start);
- let chunk =
- format!("{THOUGHT_PROCESS_START_MARKER}{chunk}{THOUGHT_PROCESS_END_MARKER}");
- let chunk_len = chunk.len();
- buffer.edit(
- [(
- message_old_end_offset..message_old_end_offset,
- chunk,
- )],
- None,
- cx,
- );
- let end = buffer
- .anchor_before(message_old_end_offset + chunk_len);
- context_event = Some(
- TextThreadEvent::StartedThoughtProcess(start..end),
- );
- } else {
- // This ensures that all the thinking chunks are inserted inside the thinking tag
- let insertion_position =
- message_old_end_offset - THOUGHT_PROCESS_END_MARKER.len();
- buffer.edit(
- [(insertion_position..insertion_position, chunk)],
- None,
- cx,
- );
- }
- }
- LanguageModelCompletionEvent::RedactedThinking { .. } => {},
- LanguageModelCompletionEvent::Text(mut chunk) => {
- if let Some(start) = thought_process_stack.pop() {
- let end = buffer.anchor_before(message_old_end_offset);
- context_event =
- Some(TextThreadEvent::EndedThoughtProcess(end));
- thought_process_output_section =
- Some(ThoughtProcessOutputSection {
- range: start..end,
- });
- chunk.insert_str(0, "\n\n");
- }
-
- buffer.edit(
- [(
- message_old_end_offset..message_old_end_offset,
- chunk,
- )],
- None,
- cx,
- );
- }
- LanguageModelCompletionEvent::ToolUse(_) |
- LanguageModelCompletionEvent::ToolUseJsonParseError { .. } |
- LanguageModelCompletionEvent::UsageUpdate(_) => {}
- }
- });
-
- if let Some(section) = thought_process_output_section.take() {
- this.insert_thought_process_output_section(section, cx);
- }
- if let Some(context_event) = context_event.take() {
- cx.emit(context_event);
- }
-
- cx.emit(TextThreadEvent::StreamedCompletion);
-
- Some(())
- })?;
- smol::future::yield_now().await;
- }
- this.update(cx, |this, cx| {
- this.pending_completions
- .retain(|completion| completion.id != pending_completion_id);
- this.summarize(false, cx);
- this.update_cache_status_for_completion(cx);
- })?;
-
- anyhow::Ok(stop_reason)
- };
-
- let result = stream_completion.await;
-
- this.update(cx, |this, cx| {
- let error_message = if let Some(error) = result.as_ref().err() {
- if error.is::<PaymentRequiredError>() {
- cx.emit(TextThreadEvent::ShowPaymentRequiredError);
- this.update_metadata(assistant_message_id, cx, |metadata| {
- metadata.status = MessageStatus::Canceled;
- });
- Some(error.to_string())
- } else {
- let error_message = error
- .chain()
- .map(|err| err.to_string())
- .collect::<Vec<_>>()
- .join("\n");
- cx.emit(TextThreadEvent::ShowAssistError(SharedString::from(
- error_message.clone(),
- )));
- this.update_metadata(assistant_message_id, cx, |metadata| {
- metadata.status =
- MessageStatus::Error(SharedString::from(error_message.clone()));
- });
- Some(error_message)
- }
- } else {
- this.update_metadata(assistant_message_id, cx, |metadata| {
- metadata.status = MessageStatus::Done;
- });
- None
- };
-
- let language_name = this
- .buffer
- .read(cx)
- .language()
- .map(|language| language.name());
-
- telemetry::event!(
- "Assistant Responded",
- conversation_id = this.id.0.clone(),
- kind = "panel",
- phase = "response",
- model = model.telemetry_id(),
- model_provider = model.provider_id().to_string(),
- response_latency,
- error_message,
- language_name = language_name.as_ref().map(|name| name.to_proto()),
- );
-
- report_anthropic_event(&model, AnthropicEventData {
- completion_type: AnthropicCompletionType::Panel,
- event: AnthropicEventType::Response,
- language_name: language_name.map(|name| name.to_proto()),
- message_id: None,
- }, cx);
-
- if let Ok(stop_reason) = result {
- match stop_reason {
- StopReason::ToolUse => {}
- StopReason::EndTurn => {}
- StopReason::MaxTokens => {}
- StopReason::Refusal => {}
- }
- }
- })
- .ok();
- }
- });
-
- self.pending_completions.push(PendingCompletion {
- id: pending_completion_id,
- assistant_message_id: assistant_message.id,
- _task: task,
- });
-
- Some(user_message)
- }
-
- pub fn to_xml(&self, cx: &App) -> String {
- let mut output = String::new();
- let buffer = self.buffer.read(cx);
- for message in self.messages(cx) {
- if message.status != MessageStatus::Done {
- continue;
- }
-
- writeln!(&mut output, "<{}>", message.role).unwrap();
- for chunk in buffer.text_for_range(message.offset_range) {
- output.push_str(chunk);
- }
- if !output.ends_with('\n') {
- output.push('\n');
- }
- writeln!(&mut output, "</{}>", message.role).unwrap();
- }
- output
- }
-
- pub fn to_completion_request(
- &self,
- model: Option<&Arc<dyn LanguageModel>>,
- cx: &App,
- ) -> LanguageModelRequest {
- let buffer = self.buffer.read(cx);
-
- let mut contents = self.contents(cx).peekable();
-
- fn collect_text_content(buffer: &Buffer, range: Range<usize>) -> Option<String> {
- let text: String = buffer.text_for_range(range).collect();
- if text.trim().is_empty() {
- None
- } else {
- Some(text)
- }
- }
-
- let mut completion_request = LanguageModelRequest {
- thread_id: None,
- prompt_id: None,
- intent: Some(CompletionIntent::UserPrompt),
- messages: Vec::new(),
- tools: Vec::new(),
- tool_choice: None,
- stop: Vec::new(),
- temperature: model.and_then(|model| AgentSettings::temperature_for_model(model, cx)),
- thinking_allowed: true,
- thinking_effort: None,
- speed: None,
- };
- for message in self.messages(cx) {
- if message.status != MessageStatus::Done {
- continue;
- }
-
- let mut offset = message.offset_range.start;
- let mut request_message = LanguageModelRequestMessage {
- role: message.role,
- content: Vec::new(),
- cache: message.cache.as_ref().is_some_and(|cache| cache.is_anchor),
- reasoning_details: None,
- };
-
- while let Some(content) = contents.peek() {
- if content
- .range()
- .end
- .cmp(&message.anchor_range.end, buffer)
- .is_lt()
- {
- let content = contents.next().unwrap();
- let range = content.range().to_offset(buffer);
- request_message.content.extend(
- collect_text_content(buffer, offset..range.start).map(MessageContent::Text),
- );
-
- match content {
- Content::Image { image, .. } => {
- if let Some(image) = image.clone().now_or_never().flatten() {
- request_message
- .content
- .push(language_model::MessageContent::Image(image));
- }
- }
- }
-
- offset = range.end;
- } else {
- break;
- }
- }
-
- request_message.content.extend(
- collect_text_content(buffer, offset..message.offset_range.end)
- .map(MessageContent::Text),
- );
-
- if !request_message.contents_empty() {
- completion_request.messages.push(request_message);
- }
- }
-
- completion_request
- }
-
- pub fn cancel_last_assist(&mut self, cx: &mut Context<Self>) -> bool {
- if let Some(pending_completion) = self.pending_completions.pop() {
- self.update_metadata(pending_completion.assistant_message_id, cx, |metadata| {
- if metadata.status == MessageStatus::Pending {
- metadata.status = MessageStatus::Canceled;
- }
- });
- true
- } else {
- false
- }
- }
-
- pub fn cycle_message_roles(&mut self, ids: HashSet<MessageId>, cx: &mut Context<Self>) {
- for id in &ids {
- if let Some(metadata) = self.messages_metadata.get(id) {
- let role = metadata.role.cycle();
- self.update_metadata(*id, cx, |metadata| metadata.role = role);
- }
- }
-
- self.message_roles_updated(ids, cx);
- }
-
- fn message_roles_updated(&mut self, ids: HashSet<MessageId>, cx: &mut Context<Self>) {
- let mut ranges = Vec::new();
- for message in self.messages(cx) {
- if ids.contains(&message.id) {
- ranges.push(message.anchor_range.clone());
- }
- }
- }
-
- pub fn update_metadata(
- &mut self,
- id: MessageId,
- cx: &mut Context<Self>,
- f: impl FnOnce(&mut MessageMetadata),
- ) {
- let version = self.version.clone();
- let timestamp = self.next_timestamp();
- if let Some(metadata) = self.messages_metadata.get_mut(&id) {
- f(metadata);
- metadata.timestamp = timestamp;
- let operation = TextThreadOperation::UpdateMessage {
- message_id: id,
- metadata: metadata.clone(),
- version,
- };
- self.push_op(operation, cx);
- cx.emit(TextThreadEvent::MessagesEdited);
- cx.notify();
- }
- }
-
- pub fn insert_message_after(
- &mut self,
- message_id: MessageId,
- role: Role,
- status: MessageStatus,
- cx: &mut Context<Self>,
- ) -> Option<MessageAnchor> {
- if let Some(prev_message_ix) = self
- .message_anchors
- .iter()
- .position(|message| message.id == message_id)
- {
- // Find the next valid message after the one we were given.
- let mut next_message_ix = prev_message_ix + 1;
- while let Some(next_message) = self.message_anchors.get(next_message_ix) {
- if next_message.start.is_valid(self.buffer.read(cx)) {
- break;
- }
- next_message_ix += 1;
- }
-
- let buffer = self.buffer.read(cx);
- let offset = self
- .message_anchors
- .get(next_message_ix)
- .map_or(buffer.len(), |message| {
- buffer.clip_offset(message.start.to_previous_offset(buffer), Bias::Left)
- });
- Some(self.insert_message_at_offset(offset, role, status, cx))
- } else {
- None
- }
- }
-
- fn insert_message_at_offset(
- &mut self,
- offset: usize,
- role: Role,
- status: MessageStatus,
- cx: &mut Context<Self>,
- ) -> MessageAnchor {
- let start = self.buffer.update(cx, |buffer, cx| {
- buffer.edit([(offset..offset, "\n")], None, cx);
- buffer.anchor_before(offset + 1)
- });
-
- let version = self.version.clone();
- let anchor = MessageAnchor {
- id: MessageId(self.next_timestamp()),
- start,
- };
- let metadata = MessageMetadata {
- role,
- status,
- timestamp: anchor.id.0,
- cache: None,
- };
- self.insert_message(anchor.clone(), metadata.clone(), cx);
- self.push_op(
- TextThreadOperation::InsertMessage {
- anchor: anchor.clone(),
- metadata,
- version,
- },
- cx,
- );
- anchor
- }
-
- pub fn insert_content(&mut self, content: Content, cx: &mut Context<Self>) {
- let buffer = self.buffer.read(cx);
- let insertion_ix = match self
- .contents
- .binary_search_by(|probe| probe.cmp(&content, buffer))
- {
- Ok(ix) => {
- self.contents.remove(ix);
- ix
- }
- Err(ix) => ix,
- };
- self.contents.insert(insertion_ix, content);
- cx.emit(TextThreadEvent::MessagesEdited);
- }
-
- pub fn contents<'a>(&'a self, cx: &'a App) -> impl 'a + Iterator<Item = Content> {
- let buffer = self.buffer.read(cx);
- self.contents
- .iter()
- .filter(|content| {
- let range = content.range();
- range.start.is_valid(buffer) && range.end.is_valid(buffer)
- })
- .cloned()
- }
-
- pub fn split_message(
- &mut self,
- range: Range<usize>,
- cx: &mut Context<Self>,
- ) -> (Option<MessageAnchor>, Option<MessageAnchor>) {
- let start_message = self.message_for_offset(range.start, cx);
- let end_message = self.message_for_offset(range.end, cx);
- if let Some((start_message, end_message)) = start_message.zip(end_message) {
- // Prevent splitting when range spans multiple messages.
- if start_message.id != end_message.id {
- return (None, None);
- }
-
- let message = start_message;
- let at_end = range.end >= message.offset_range.end.saturating_sub(1);
- let role_after = if range.start == range.end || at_end {
- Role::User
- } else {
- message.role
- };
- let role = message.role;
- let mut edited_buffer = false;
-
- let mut suffix_start = None;
-
- // TODO: why did this start panicking?
- if range.start > message.offset_range.start
- && range.end < message.offset_range.end.saturating_sub(1)
- {
- if self.buffer.read(cx).chars_at(range.end).next() == Some('\n') {
- suffix_start = Some(range.end + 1);
- } else if self.buffer.read(cx).reversed_chars_at(range.end).next() == Some('\n') {
- suffix_start = Some(range.end);
- }
- }
-
- let version = self.version.clone();
- let suffix = if let Some(suffix_start) = suffix_start {
- MessageAnchor {
- id: MessageId(self.next_timestamp()),
- start: self.buffer.read(cx).anchor_before(suffix_start),
- }
- } else {
- self.buffer.update(cx, |buffer, cx| {
- buffer.edit([(range.end..range.end, "\n")], None, cx);
- });
- edited_buffer = true;
- MessageAnchor {
- id: MessageId(self.next_timestamp()),
- start: self.buffer.read(cx).anchor_before(range.end + 1),
- }
- };
-
- let suffix_metadata = MessageMetadata {
- role: role_after,
- status: MessageStatus::Done,
- timestamp: suffix.id.0,
- cache: None,
- };
- self.insert_message(suffix.clone(), suffix_metadata.clone(), cx);
- self.push_op(
- TextThreadOperation::InsertMessage {
- anchor: suffix.clone(),
- metadata: suffix_metadata,
- version,
- },
- cx,
- );
-
- let new_messages =
- if range.start == range.end || range.start == message.offset_range.start {
- (None, Some(suffix))
- } else {
- let mut prefix_end = None;
- if range.start > message.offset_range.start
- && range.end < message.offset_range.end - 1
- {
- if self.buffer.read(cx).chars_at(range.start).next() == Some('\n') {
- prefix_end = Some(range.start + 1);
- } else if self.buffer.read(cx).reversed_chars_at(range.start).next()
- == Some('\n')
- {
- prefix_end = Some(range.start);
- }
- }
-
- let version = self.version.clone();
- let selection = if let Some(prefix_end) = prefix_end {
- MessageAnchor {
- id: MessageId(self.next_timestamp()),
- start: self.buffer.read(cx).anchor_before(prefix_end),
- }
- } else {
- self.buffer.update(cx, |buffer, cx| {
- buffer.edit([(range.start..range.start, "\n")], None, cx)
- });
- edited_buffer = true;
- MessageAnchor {
- id: MessageId(self.next_timestamp()),
- start: self.buffer.read(cx).anchor_before(range.end + 1),
- }
- };
-
- let selection_metadata = MessageMetadata {
- role,
- status: MessageStatus::Done,
- timestamp: selection.id.0,
- cache: None,
- };
- self.insert_message(selection.clone(), selection_metadata.clone(), cx);
- self.push_op(
- TextThreadOperation::InsertMessage {
- anchor: selection.clone(),
- metadata: selection_metadata,
- version,
- },
- cx,
- );
-
- (Some(selection), Some(suffix))
- };
-
- if !edited_buffer {
- cx.emit(TextThreadEvent::MessagesEdited);
- }
- new_messages
- } else {
- (None, None)
- }
- }
-
- fn insert_message(
- &mut self,
- new_anchor: MessageAnchor,
- new_metadata: MessageMetadata,
- cx: &mut Context<Self>,
- ) {
- cx.emit(TextThreadEvent::MessagesEdited);
-
- self.messages_metadata.insert(new_anchor.id, new_metadata);
-
- let buffer = self.buffer.read(cx);
- let insertion_ix = self
- .message_anchors
- .iter()
- .position(|anchor| {
- let comparison = new_anchor.start.cmp(&anchor.start, buffer);
- comparison.is_lt() || (comparison.is_eq() && new_anchor.id > anchor.id)
- })
- .unwrap_or(self.message_anchors.len());
- self.message_anchors.insert(insertion_ix, new_anchor);
- }
-
- pub fn summarize(&mut self, mut replace_old: bool, cx: &mut Context<Self>) {
- let Some(model) = LanguageModelRegistry::read_global(cx).thread_summary_model() else {
- return;
- };
-
- if replace_old || (self.message_anchors.len() >= 2 && self.summary.is_pending()) {
- if !model.provider.is_authenticated(cx) {
- return;
- }
-
- let mut request = self.to_completion_request(Some(&model.model), cx);
- request.messages.push(LanguageModelRequestMessage {
- role: Role::User,
- content: vec![SUMMARIZE_THREAD_PROMPT.into()],
- cache: false,
- reasoning_details: None,
- });
-
- // If there is no summary, it is set with `done: false` so that "Loading Summaryβ¦" can
- // be displayed.
- match self.summary {
- TextThreadSummary::Pending | TextThreadSummary::Error => {
- self.summary = TextThreadSummary::Content(TextThreadSummaryContent {
- text: "".to_string(),
- done: false,
- timestamp: clock::Lamport::MIN,
- });
- replace_old = true;
- }
- TextThreadSummary::Content(_) => {}
- }
-
- self.summary_task = cx.spawn(async move |this, cx| {
- let result = async {
- let stream = model.model.stream_completion_text(request, cx);
- let mut messages = stream.await?;
-
- let mut replaced = !replace_old;
- while let Some(message) = messages.stream.next().await {
- let text = message?;
- let mut lines = text.lines();
- this.update(cx, |this, cx| {
- let version = this.version.clone();
- let timestamp = this.next_timestamp();
- let summary = this.summary.content_or_set_empty();
- if !replaced && replace_old {
- summary.text.clear();
- replaced = true;
- }
- summary.text.extend(lines.next());
- summary.timestamp = timestamp;
- let operation = TextThreadOperation::UpdateSummary {
- summary: summary.clone(),
- version,
- };
- this.push_op(operation, cx);
- cx.emit(TextThreadEvent::SummaryChanged);
- cx.emit(TextThreadEvent::SummaryGenerated);
- })?;
-
- // Stop if the LLM generated multiple lines.
- if lines.next().is_some() {
- break;
- }
- }
-
- this.read_with(cx, |this, _cx| {
- if let Some(summary) = this.summary.content()
- && summary.text.is_empty()
- {
- bail!("Model generated an empty summary");
- }
- Ok(())
- })??;
-
- this.update(cx, |this, cx| {
- let version = this.version.clone();
- let timestamp = this.next_timestamp();
- if let Some(summary) = this.summary.content_as_mut() {
- summary.done = true;
- summary.timestamp = timestamp;
- let operation = TextThreadOperation::UpdateSummary {
- summary: summary.clone(),
- version,
- };
- this.push_op(operation, cx);
- cx.emit(TextThreadEvent::SummaryChanged);
- cx.emit(TextThreadEvent::SummaryGenerated);
- }
- })?;
-
- anyhow::Ok(())
- }
- .await;
-
- if let Err(err) = result {
- this.update(cx, |this, cx| {
- this.summary = TextThreadSummary::Error;
- cx.emit(TextThreadEvent::SummaryChanged);
- })
- .log_err();
- log::error!("Error generating context summary: {}", err);
- }
-
- Some(())
- });
- }
- }
-
- fn message_for_offset(&self, offset: usize, cx: &App) -> Option<Message> {
- self.messages_for_offsets([offset], cx).pop()
- }
-
- pub fn messages_for_offsets(
- &self,
- offsets: impl IntoIterator<Item = usize>,
- cx: &App,
- ) -> Vec<Message> {
- let mut result = Vec::new();
-
- let mut messages = self.messages(cx).peekable();
- let mut offsets = offsets.into_iter().peekable();
- let mut current_message = messages.next();
- while let Some(offset) = offsets.next() {
- // Locate the message that contains the offset.
- while current_message.as_ref().is_some_and(|message| {
- !message.offset_range.contains(&offset) && messages.peek().is_some()
- }) {
- current_message = messages.next();
- }
- let Some(message) = current_message.as_ref() else {
- break;
- };
-
- // Skip offsets that are in the same message.
- while offsets.peek().is_some_and(|offset| {
- message.offset_range.contains(offset) || messages.peek().is_none()
- }) {
- offsets.next();
- }
-
- result.push(message.clone());
- }
- result
- }
-
- fn messages_from_anchors<'a>(
- &'a self,
- message_anchors: impl Iterator<Item = &'a MessageAnchor> + 'a,
- cx: &'a App,
- ) -> impl 'a + Iterator<Item = Message> {
- let buffer = self.buffer.read(cx);
-
- Self::messages_from_iters(buffer, &self.messages_metadata, message_anchors.enumerate())
- }
-
- pub fn messages<'a>(&'a self, cx: &'a App) -> impl 'a + Iterator<Item = Message> {
- self.messages_from_anchors(self.message_anchors.iter(), cx)
- }
-
- pub fn messages_from_iters<'a>(
- buffer: &'a Buffer,
- metadata: &'a HashMap<MessageId, MessageMetadata>,
- messages: impl Iterator<Item = (usize, &'a MessageAnchor)> + 'a,
- ) -> impl 'a + Iterator<Item = Message> {
- let mut messages = messages.peekable();
-
- iter::from_fn(move || {
- if let Some((start_ix, message_anchor)) = messages.next() {
- let metadata = metadata.get(&message_anchor.id)?;
-
- let message_start = message_anchor.start.to_offset(buffer);
- let mut message_end = None;
- let mut end_ix = start_ix;
- while let Some((_, next_message)) = messages.peek() {
- if next_message.start.is_valid(buffer) {
- message_end = Some(next_message.start);
- break;
- } else {
- end_ix += 1;
- messages.next();
- }
- }
- let message_end_anchor =
- message_end.unwrap_or(language::Anchor::max_for_buffer(buffer.remote_id()));
- let message_end = message_end_anchor.to_offset(buffer);
-
- return Some(Message {
- index_range: start_ix..end_ix,
- offset_range: message_start..message_end,
- anchor_range: message_anchor.start..message_end_anchor,
- id: message_anchor.id,
- role: metadata.role,
- status: metadata.status.clone(),
- cache: metadata.cache.clone(),
- });
- }
- None
- })
- }
-
- pub fn save(
- &mut self,
- debounce: Option<Duration>,
- fs: Arc<dyn Fs>,
- cx: &mut Context<TextThread>,
- ) {
- if self.replica_id() != ReplicaId::default() {
- // Prevent saving a remote context for now.
- return;
- }
-
- self.pending_save = cx.spawn(async move |this, cx| {
- if let Some(debounce) = debounce {
- cx.background_executor().timer(debounce).await;
- }
-
- let (old_path, summary) = this.read_with(cx, |this, _| {
- let path = this.path.clone();
- let summary = if let Some(summary) = this.summary.content() {
- if summary.done {
- Some(summary.text.clone())
- } else {
- None
- }
- } else {
- None
- };
- (path, summary)
- })?;
-
- if let Some(summary) = summary {
- let context = this.read_with(cx, |this, cx| this.serialize(cx))?;
- let mut discriminant = 1;
- let mut new_path;
- loop {
- new_path = text_threads_dir().join(&format!(
- "{} - {}.zed.json",
- summary.trim(),
- discriminant
- ));
- if fs.is_file(&new_path).await {
- discriminant += 1;
- } else {
- break;
- }
- }
-
- fs.create_dir(text_threads_dir().as_ref()).await?;
-
- // rename before write ensures that only one file exists
- if let Some(old_path) = old_path.as_ref()
- && new_path.as_path() != old_path.as_ref()
- {
- fs.rename(
- old_path,
- &new_path,
- RenameOptions {
- overwrite: true,
- ignore_if_exists: true,
- create_parents: false,
- },
- )
- .await?;
- }
-
- // update path before write in case it fails
- this.update(cx, {
- let new_path: Arc<Path> = new_path.clone().into();
- move |this, cx| {
- this.path = Some(new_path.clone());
- cx.emit(TextThreadEvent::PathChanged { old_path, new_path });
- }
- })
- .ok();
-
- fs.atomic_write(new_path, serde_json::to_string(&context).unwrap())
- .await?;
- }
-
- Ok(())
- });
- }
-
- pub fn set_custom_summary(&mut self, custom_summary: String, cx: &mut Context<Self>) {
- let timestamp = self.next_timestamp();
- let summary = self.summary.content_or_set_empty();
- summary.timestamp = timestamp;
- summary.done = true;
- summary.text = custom_summary;
- cx.emit(TextThreadEvent::SummaryChanged);
- }
-}
-
-#[derive(Debug, Default)]
-pub struct TextThreadVersion {
- text_thread: clock::Global,
- buffer: clock::Global,
-}
-
-impl TextThreadVersion {
- pub fn from_proto(proto: &proto::ContextVersion) -> Self {
- Self {
- text_thread: language::proto::deserialize_version(&proto.context_version),
- buffer: language::proto::deserialize_version(&proto.buffer_version),
- }
- }
-
- pub fn to_proto(&self, context_id: TextThreadId) -> proto::ContextVersion {
- proto::ContextVersion {
- context_id: context_id.to_proto(),
- context_version: language::proto::serialize_version(&self.text_thread),
- buffer_version: language::proto::serialize_version(&self.buffer),
- }
- }
-}
-
-#[derive(Debug, Clone)]
-pub struct ParsedSlashCommand {
- pub name: String,
- pub arguments: SmallVec<[String; 3]>,
- pub status: PendingSlashCommandStatus,
- pub source_range: Range<language::Anchor>,
-}
-
-#[derive(Debug)]
-pub struct InvokedSlashCommand {
- pub name: SharedString,
- pub range: Range<language::Anchor>,
- pub run_commands_in_ranges: Vec<Range<language::Anchor>>,
- pub status: InvokedSlashCommandStatus,
- pub transaction: Option<language::TransactionId>,
- timestamp: clock::Lamport,
-}
-
-#[derive(Debug)]
-pub enum InvokedSlashCommandStatus {
- Running(Task<()>),
- Error(SharedString),
- Finished,
-}
-
-#[derive(Debug, Clone)]
-pub enum PendingSlashCommandStatus {
- Idle,
- Running { _task: Shared<Task<()>> },
- Error(String),
-}
-
-#[derive(Debug, Clone)]
-pub struct PendingToolUse {
- pub id: LanguageModelToolUseId,
- pub name: String,
- pub input: serde_json::Value,
- pub status: PendingToolUseStatus,
- pub source_range: Range<language::Anchor>,
-}
-
-#[derive(Debug, Clone)]
-pub enum PendingToolUseStatus {
- Idle,
- Running { _task: Shared<Task<()>> },
- Error(String),
-}
-
-impl PendingToolUseStatus {
- pub fn is_idle(&self) -> bool {
- matches!(self, PendingToolUseStatus::Idle)
- }
-}
-
-#[derive(Serialize, Deserialize)]
-pub struct SavedMessage {
- pub id: MessageId,
- pub start: usize,
- pub metadata: MessageMetadata,
-}
-
-#[derive(Serialize, Deserialize)]
-pub struct SavedTextThread {
- pub id: Option<TextThreadId>,
- pub zed: String,
- pub version: String,
- pub text: String,
- pub messages: Vec<SavedMessage>,
- pub summary: String,
- pub slash_command_output_sections:
- Vec<assistant_slash_command::SlashCommandOutputSection<usize>>,
- #[serde(default)]
- pub thought_process_output_sections: Vec<ThoughtProcessOutputSection<usize>>,
-}
-
-impl SavedTextThread {
- pub const VERSION: &'static str = "0.4.0";
-
- pub fn from_json(json: &str) -> Result<Self> {
- let saved_context_json = serde_json::from_str::<serde_json::Value>(json)?;
- match saved_context_json
- .get("version")
- .context("version not found")?
- {
- serde_json::Value::String(version) => match version.as_str() {
- SavedTextThread::VERSION => Ok(serde_json::from_value::<SavedTextThread>(
- saved_context_json,
- )?),
- SavedContextV0_3_0::VERSION => {
- let saved_context =
- serde_json::from_value::<SavedContextV0_3_0>(saved_context_json)?;
- Ok(saved_context.upgrade())
- }
- SavedContextV0_2_0::VERSION => {
- let saved_context =
- serde_json::from_value::<SavedContextV0_2_0>(saved_context_json)?;
- Ok(saved_context.upgrade())
- }
- SavedContextV0_1_0::VERSION => {
- let saved_context =
- serde_json::from_value::<SavedContextV0_1_0>(saved_context_json)?;
- Ok(saved_context.upgrade())
- }
- _ => anyhow::bail!("unrecognized saved context version: {version:?}"),
- },
- _ => anyhow::bail!("version not found on saved context"),
- }
- }
-
- fn into_ops(
- self,
- buffer: &Entity<Buffer>,
- cx: &mut Context<TextThread>,
- ) -> Vec<TextThreadOperation> {
- let mut operations = Vec::new();
- let mut version = clock::Global::new();
- let mut next_timestamp = clock::Lamport::new(ReplicaId::default());
-
- let mut first_message_metadata = None;
- for message in self.messages {
- if message.id == MessageId(clock::Lamport::MIN) {
- first_message_metadata = Some(message.metadata);
- } else {
- operations.push(TextThreadOperation::InsertMessage {
- anchor: MessageAnchor {
- id: message.id,
- start: buffer.read(cx).anchor_before(message.start),
- },
- metadata: MessageMetadata {
- role: message.metadata.role,
- status: message.metadata.status,
- timestamp: message.metadata.timestamp,
- cache: None,
- },
- version: version.clone(),
- });
- version.observe(message.id.0);
- next_timestamp.observe(message.id.0);
- }
- }
-
- if let Some(metadata) = first_message_metadata {
- let timestamp = next_timestamp.tick();
- operations.push(TextThreadOperation::UpdateMessage {
- message_id: MessageId(clock::Lamport::MIN),
- metadata: MessageMetadata {
- role: metadata.role,
- status: metadata.status,
- timestamp,
- cache: None,
- },
- version: version.clone(),
- });
- version.observe(timestamp);
- }
-
- let buffer = buffer.read(cx);
- for section in self.slash_command_output_sections {
- let timestamp = next_timestamp.tick();
- operations.push(TextThreadOperation::SlashCommandOutputSectionAdded {
- timestamp,
- section: SlashCommandOutputSection {
- range: buffer.anchor_after(section.range.start)
- ..buffer.anchor_before(section.range.end),
- icon: section.icon,
- label: section.label,
- metadata: section.metadata,
- },
- version: version.clone(),
- });
-
- version.observe(timestamp);
- }
-
- for section in self.thought_process_output_sections {
- let timestamp = next_timestamp.tick();
- operations.push(TextThreadOperation::ThoughtProcessOutputSectionAdded {
- timestamp,
- section: ThoughtProcessOutputSection {
- range: buffer.anchor_after(section.range.start)
- ..buffer.anchor_before(section.range.end),
- },
- version: version.clone(),
- });
-
- version.observe(timestamp);
- }
-
- let timestamp = next_timestamp.tick();
- operations.push(TextThreadOperation::UpdateSummary {
- summary: TextThreadSummaryContent {
- text: self.summary,
- done: true,
- timestamp,
- },
- version: version.clone(),
- });
- version.observe(timestamp);
-
- operations
- }
-}
-
-#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
-struct SavedMessageIdPreV0_4_0(usize);
-
-#[derive(Serialize, Deserialize)]
-struct SavedMessagePreV0_4_0 {
- id: SavedMessageIdPreV0_4_0,
- start: usize,
-}
-
-#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
-struct SavedMessageMetadataPreV0_4_0 {
- role: Role,
- status: MessageStatus,
-}
-
-#[derive(Serialize, Deserialize)]
-struct SavedContextV0_3_0 {
- id: Option<TextThreadId>,
- zed: String,
- version: String,
- text: String,
- messages: Vec<SavedMessagePreV0_4_0>,
- message_metadata: HashMap<SavedMessageIdPreV0_4_0, SavedMessageMetadataPreV0_4_0>,
- summary: String,
- slash_command_output_sections: Vec<assistant_slash_command::SlashCommandOutputSection<usize>>,
-}
-
-impl SavedContextV0_3_0 {
- const VERSION: &'static str = "0.3.0";
-
- fn upgrade(self) -> SavedTextThread {
- SavedTextThread {
- id: self.id,
- zed: self.zed,
- version: SavedTextThread::VERSION.into(),
- text: self.text,
- messages: self
- .messages
- .into_iter()
- .filter_map(|message| {
- let metadata = self.message_metadata.get(&message.id)?;
- let timestamp = clock::Lamport {
- replica_id: ReplicaId::default(),
- value: message.id.0 as u32,
- };
- Some(SavedMessage {
- id: MessageId(timestamp),
- start: message.start,
- metadata: MessageMetadata {
- role: metadata.role,
- status: metadata.status.clone(),
- timestamp,
- cache: None,
- },
- })
- })
- .collect(),
- summary: self.summary,
- slash_command_output_sections: self.slash_command_output_sections,
- thought_process_output_sections: Vec::new(),
- }
- }
-}
-
-#[derive(Serialize, Deserialize)]
-struct SavedContextV0_2_0 {
- id: Option<TextThreadId>,
- zed: String,
- version: String,
- text: String,
- messages: Vec<SavedMessagePreV0_4_0>,
- message_metadata: HashMap<SavedMessageIdPreV0_4_0, SavedMessageMetadataPreV0_4_0>,
- summary: String,
-}
-
-impl SavedContextV0_2_0 {
- const VERSION: &'static str = "0.2.0";
-
- fn upgrade(self) -> SavedTextThread {
- SavedContextV0_3_0 {
- id: self.id,
- zed: self.zed,
- version: SavedContextV0_3_0::VERSION.to_string(),
- text: self.text,
- messages: self.messages,
- message_metadata: self.message_metadata,
- summary: self.summary,
- slash_command_output_sections: Vec::new(),
- }
- .upgrade()
- }
-}
-
-#[derive(Serialize, Deserialize)]
-struct SavedContextV0_1_0 {
- id: Option<TextThreadId>,
- zed: String,
- version: String,
- text: String,
- messages: Vec<SavedMessagePreV0_4_0>,
- message_metadata: HashMap<SavedMessageIdPreV0_4_0, SavedMessageMetadataPreV0_4_0>,
- summary: String,
- api_url: Option<String>,
- model: OpenAiModel,
-}
-
-impl SavedContextV0_1_0 {
- const VERSION: &'static str = "0.1.0";
-
- fn upgrade(self) -> SavedTextThread {
- SavedContextV0_2_0 {
- id: self.id,
- zed: self.zed,
- version: SavedContextV0_2_0::VERSION.to_string(),
- text: self.text,
- messages: self.messages,
- message_metadata: self.message_metadata,
- summary: self.summary,
- }
- .upgrade()
- }
-}
-
-#[derive(Debug, Clone)]
-pub struct SavedTextThreadMetadata {
- pub title: SharedString,
- pub path: Arc<Path>,
- pub mtime: chrono::DateTime<chrono::Local>,
-}
@@ -1,1089 +0,0 @@
-use crate::{
- SavedTextThread, SavedTextThreadMetadata, TextThread, TextThreadEvent, TextThreadId,
- TextThreadOperation, TextThreadVersion, context_server_command,
-};
-use anyhow::{Context as _, Result};
-use assistant_slash_command::{SlashCommandId, SlashCommandWorkingSet};
-use client::{Client, TypedEnvelope, proto};
-use clock::ReplicaId;
-use collections::HashMap;
-use context_server::ContextServerId;
-use fs::{Fs, RemoveOptions};
-use futures::StreamExt;
-use fuzzy::StringMatchCandidate;
-use gpui::{App, AppContext as _, AsyncApp, Context, Entity, Task, WeakEntity};
-use itertools::Itertools;
-use language::LanguageRegistry;
-use paths::text_threads_dir;
-use project::{
- Project,
- context_server_store::{ContextServerStatus, ContextServerStore},
-};
-use prompt_store::PromptBuilder;
-use regex::Regex;
-use rpc::AnyProtoClient;
-use std::sync::LazyLock;
-use std::{cmp::Reverse, ffi::OsStr, mem, path::Path, sync::Arc, time::Duration};
-use util::{ResultExt, TryFutureExt};
-use zed_env_vars::ZED_STATELESS;
-
-pub(crate) fn init(client: &AnyProtoClient) {
- client.add_entity_message_handler(TextThreadStore::handle_advertise_contexts);
- client.add_entity_request_handler(TextThreadStore::handle_open_context);
- client.add_entity_request_handler(TextThreadStore::handle_create_context);
- client.add_entity_message_handler(TextThreadStore::handle_update_context);
- client.add_entity_request_handler(TextThreadStore::handle_synchronize_contexts);
-}
-
-#[derive(Clone)]
-pub struct RemoteTextThreadMetadata {
- pub id: TextThreadId,
- pub summary: Option<String>,
-}
-
-pub struct TextThreadStore {
- text_threads: Vec<TextThreadHandle>,
- text_threads_metadata: Vec<SavedTextThreadMetadata>,
- context_server_slash_command_ids: HashMap<ContextServerId, Vec<SlashCommandId>>,
- host_text_threads: Vec<RemoteTextThreadMetadata>,
- fs: Arc<dyn Fs>,
- languages: Arc<LanguageRegistry>,
- slash_commands: Arc<SlashCommandWorkingSet>,
- _watch_updates: Task<Option<()>>,
- client: Arc<Client>,
- project: WeakEntity<Project>,
- project_is_shared: bool,
- client_subscription: Option<client::Subscription>,
- _project_subscriptions: Vec<gpui::Subscription>,
- prompt_builder: Arc<PromptBuilder>,
-}
-
-enum TextThreadHandle {
- Weak(WeakEntity<TextThread>),
- Strong(Entity<TextThread>),
-}
-
-impl TextThreadHandle {
- fn upgrade(&self) -> Option<Entity<TextThread>> {
- match self {
- TextThreadHandle::Weak(weak) => weak.upgrade(),
- TextThreadHandle::Strong(strong) => Some(strong.clone()),
- }
- }
-
- fn downgrade(&self) -> WeakEntity<TextThread> {
- match self {
- TextThreadHandle::Weak(weak) => weak.clone(),
- TextThreadHandle::Strong(strong) => strong.downgrade(),
- }
- }
-}
-
-impl TextThreadStore {
- pub fn new(
- project: Entity<Project>,
- prompt_builder: Arc<PromptBuilder>,
- slash_commands: Arc<SlashCommandWorkingSet>,
- cx: &mut App,
- ) -> Task<Result<Entity<Self>>> {
- let fs = project.read(cx).fs().clone();
- let languages = project.read(cx).languages().clone();
- cx.spawn(async move |cx| {
- const CONTEXT_WATCH_DURATION: Duration = Duration::from_millis(100);
- let (mut events, _) = fs.watch(text_threads_dir(), CONTEXT_WATCH_DURATION).await;
-
- let this = cx.new(|cx: &mut Context<Self>| {
- let mut this = Self {
- text_threads: Vec::new(),
- text_threads_metadata: Vec::new(),
- context_server_slash_command_ids: HashMap::default(),
- host_text_threads: Vec::new(),
- fs,
- languages,
- slash_commands,
- _watch_updates: cx.spawn(async move |this, cx| {
- async move {
- while events.next().await.is_some() {
- this.update(cx, |this, cx| this.reload(cx))?.await.log_err();
- }
- anyhow::Ok(())
- }
- .log_err()
- .await
- }),
- client_subscription: None,
- _project_subscriptions: vec![
- cx.subscribe(&project, Self::handle_project_event),
- ],
- project_is_shared: false,
- client: project.read(cx).client(),
- project: project.downgrade(),
- prompt_builder,
- };
- this.handle_project_shared(cx);
- this.synchronize_contexts(cx);
- this.register_context_server_handlers(cx);
- this.reload(cx).detach_and_log_err(cx);
- this
- });
-
- Ok(this)
- })
- }
-
- #[cfg(any(test, feature = "test-support"))]
- pub fn fake(project: Entity<Project>, cx: &mut Context<Self>) -> Self {
- Self {
- text_threads: Default::default(),
- text_threads_metadata: Default::default(),
- context_server_slash_command_ids: Default::default(),
- host_text_threads: Default::default(),
- fs: project.read(cx).fs().clone(),
- languages: project.read(cx).languages().clone(),
- slash_commands: Arc::default(),
- _watch_updates: Task::ready(None),
- client: project.read(cx).client(),
- project: project.downgrade(),
- project_is_shared: false,
- client_subscription: None,
- _project_subscriptions: Default::default(),
- prompt_builder: Arc::new(PromptBuilder::new(None).unwrap()),
- }
- }
-
- async fn handle_advertise_contexts(
- this: Entity<Self>,
- envelope: TypedEnvelope<proto::AdvertiseContexts>,
- mut cx: AsyncApp,
- ) -> Result<()> {
- this.update(&mut cx, |this, cx| {
- this.host_text_threads = envelope
- .payload
- .contexts
- .into_iter()
- .map(|text_thread| RemoteTextThreadMetadata {
- id: TextThreadId::from_proto(text_thread.context_id),
- summary: text_thread.summary,
- })
- .collect();
- cx.notify();
- });
- Ok(())
- }
-
- async fn handle_open_context(
- this: Entity<Self>,
- envelope: TypedEnvelope<proto::OpenContext>,
- mut cx: AsyncApp,
- ) -> Result<proto::OpenContextResponse> {
- let context_id = TextThreadId::from_proto(envelope.payload.context_id);
- let operations = this.update(&mut cx, |this, cx| {
- let project = this.project.upgrade().context("project not found")?;
-
- anyhow::ensure!(
- !project.read(cx).is_via_collab(),
- "only the host contexts can be opened"
- );
-
- let text_thread = this
- .loaded_text_thread_for_id(&context_id, cx)
- .context("context not found")?;
- anyhow::ensure!(
- text_thread.read(cx).replica_id() == ReplicaId::default(),
- "context must be opened via the host"
- );
-
- anyhow::Ok(
- text_thread
- .read(cx)
- .serialize_ops(&TextThreadVersion::default(), cx),
- )
- })?;
- let operations = operations.await;
- Ok(proto::OpenContextResponse {
- context: Some(proto::Context { operations }),
- })
- }
-
- async fn handle_create_context(
- this: Entity<Self>,
- _: TypedEnvelope<proto::CreateContext>,
- mut cx: AsyncApp,
- ) -> Result<proto::CreateContextResponse> {
- let (context_id, operations) = this.update(&mut cx, |this, cx| {
- let project = this.project.upgrade().context("project not found")?;
- anyhow::ensure!(
- !project.read(cx).is_via_collab(),
- "can only create contexts as the host"
- );
-
- let text_thread = this.create(cx);
- let context_id = text_thread.read(cx).id().clone();
-
- anyhow::Ok((
- context_id,
- text_thread
- .read(cx)
- .serialize_ops(&TextThreadVersion::default(), cx),
- ))
- })?;
- let operations = operations.await;
- Ok(proto::CreateContextResponse {
- context_id: context_id.to_proto(),
- context: Some(proto::Context { operations }),
- })
- }
-
- async fn handle_update_context(
- this: Entity<Self>,
- envelope: TypedEnvelope<proto::UpdateContext>,
- mut cx: AsyncApp,
- ) -> Result<()> {
- this.update(&mut cx, |this, cx| {
- let context_id = TextThreadId::from_proto(envelope.payload.context_id);
- if let Some(text_thread) = this.loaded_text_thread_for_id(&context_id, cx) {
- let operation_proto = envelope.payload.operation.context("invalid operation")?;
- let operation = TextThreadOperation::from_proto(operation_proto)?;
- text_thread.update(cx, |text_thread, cx| text_thread.apply_ops([operation], cx));
- }
- Ok(())
- })
- }
-
- async fn handle_synchronize_contexts(
- this: Entity<Self>,
- envelope: TypedEnvelope<proto::SynchronizeContexts>,
- mut cx: AsyncApp,
- ) -> Result<proto::SynchronizeContextsResponse> {
- this.update(&mut cx, |this, cx| {
- let project = this.project.upgrade().context("project not found")?;
- anyhow::ensure!(
- !project.read(cx).is_via_collab(),
- "only the host can synchronize contexts"
- );
-
- let mut local_versions = Vec::new();
- for remote_version_proto in envelope.payload.contexts {
- let remote_version = TextThreadVersion::from_proto(&remote_version_proto);
- let context_id = TextThreadId::from_proto(remote_version_proto.context_id);
- if let Some(text_thread) = this.loaded_text_thread_for_id(&context_id, cx) {
- let text_thread = text_thread.read(cx);
- let operations = text_thread.serialize_ops(&remote_version, cx);
- local_versions.push(text_thread.version(cx).to_proto(context_id.clone()));
- let client = this.client.clone();
- let project_id = envelope.payload.project_id;
- cx.background_spawn(async move {
- let operations = operations.await;
- for operation in operations {
- client.send(proto::UpdateContext {
- project_id,
- context_id: context_id.to_proto(),
- operation: Some(operation),
- })?;
- }
- anyhow::Ok(())
- })
- .detach_and_log_err(cx);
- }
- }
-
- this.advertise_contexts(cx);
-
- anyhow::Ok(proto::SynchronizeContextsResponse {
- contexts: local_versions,
- })
- })
- }
-
- fn handle_project_shared(&mut self, cx: &mut Context<Self>) {
- let Some(project) = self.project.upgrade() else {
- return;
- };
-
- let is_shared = project.read(cx).is_shared();
- let was_shared = mem::replace(&mut self.project_is_shared, is_shared);
- if is_shared == was_shared {
- return;
- }
-
- if is_shared {
- self.text_threads.retain_mut(|text_thread| {
- if let Some(strong_context) = text_thread.upgrade() {
- *text_thread = TextThreadHandle::Strong(strong_context);
- true
- } else {
- false
- }
- });
- let remote_id = project.read(cx).remote_id().unwrap();
- self.client_subscription = self
- .client
- .subscribe_to_entity(remote_id)
- .log_err()
- .map(|subscription| subscription.set_entity(&cx.entity(), &cx.to_async()));
- self.advertise_contexts(cx);
- } else {
- self.client_subscription = None;
- }
- }
-
- fn handle_project_event(
- &mut self,
- _project: Entity<Project>,
- event: &project::Event,
- cx: &mut Context<Self>,
- ) {
- match event {
- project::Event::RemoteIdChanged(_) => {
- self.handle_project_shared(cx);
- }
- project::Event::Reshared => {
- self.advertise_contexts(cx);
- }
- project::Event::HostReshared | project::Event::Rejoined => {
- self.synchronize_contexts(cx);
- }
- project::Event::DisconnectedFromHost => {
- self.text_threads.retain_mut(|text_thread| {
- if let Some(strong_context) = text_thread.upgrade() {
- *text_thread = TextThreadHandle::Weak(text_thread.downgrade());
- strong_context.update(cx, |text_thread, cx| {
- if text_thread.replica_id() != ReplicaId::default() {
- text_thread.set_capability(language::Capability::ReadOnly, cx);
- }
- });
- true
- } else {
- false
- }
- });
- self.host_text_threads.clear();
- cx.notify();
- }
- _ => {}
- }
- }
-
- /// Returns saved threads ordered by `mtime` descending (newest first).
- pub fn ordered_text_threads(&self) -> impl Iterator<Item = &SavedTextThreadMetadata> {
- self.text_threads_metadata
- .iter()
- .sorted_by(|a, b| b.mtime.cmp(&a.mtime))
- }
-
- pub fn has_saved_text_threads(&self) -> bool {
- !self.text_threads_metadata.is_empty()
- }
-
- pub fn host_text_threads(&self) -> impl Iterator<Item = &RemoteTextThreadMetadata> {
- self.host_text_threads.iter()
- }
-
- pub fn create(&mut self, cx: &mut Context<Self>) -> Entity<TextThread> {
- let context = cx.new(|cx| {
- TextThread::local(
- self.languages.clone(),
- self.prompt_builder.clone(),
- self.slash_commands.clone(),
- cx,
- )
- });
- self.register_text_thread(&context, cx);
- context
- }
-
- pub fn create_remote(&mut self, cx: &mut Context<Self>) -> Task<Result<Entity<TextThread>>> {
- let Some(project) = self.project.upgrade() else {
- return Task::ready(Err(anyhow::anyhow!("project was dropped")));
- };
- let project = project.read(cx);
- let Some(project_id) = project.remote_id() else {
- return Task::ready(Err(anyhow::anyhow!("project was not remote")));
- };
-
- let replica_id = project.replica_id();
- let capability = project.capability();
- let language_registry = self.languages.clone();
-
- let prompt_builder = self.prompt_builder.clone();
- let slash_commands = self.slash_commands.clone();
- let request = self.client.request(proto::CreateContext { project_id });
- cx.spawn(async move |this, cx| {
- let response = request.await?;
- let context_id = TextThreadId::from_proto(response.context_id);
- let context_proto = response.context.context("invalid context")?;
- let text_thread = cx.new(|cx| {
- TextThread::new(
- context_id.clone(),
- replica_id,
- capability,
- language_registry,
- prompt_builder,
- slash_commands,
- cx,
- )
- });
- let operations = cx
- .background_spawn(async move {
- context_proto
- .operations
- .into_iter()
- .map(TextThreadOperation::from_proto)
- .collect::<Result<Vec<_>>>()
- })
- .await?;
- text_thread.update(cx, |context, cx| context.apply_ops(operations, cx));
- this.update(cx, |this, cx| {
- if let Some(existing_context) = this.loaded_text_thread_for_id(&context_id, cx) {
- existing_context
- } else {
- this.register_text_thread(&text_thread, cx);
- this.synchronize_contexts(cx);
- text_thread
- }
- })
- })
- }
-
- pub fn open_local(
- &mut self,
- path: Arc<Path>,
- cx: &Context<Self>,
- ) -> Task<Result<Entity<TextThread>>> {
- if let Some(existing_context) = self.loaded_text_thread_for_path(&path, cx) {
- return Task::ready(Ok(existing_context));
- }
-
- let fs = self.fs.clone();
- let languages = self.languages.clone();
- let load = cx.background_spawn({
- let path = path.clone();
- async move {
- let saved_context = fs.load(&path).await?;
- SavedTextThread::from_json(&saved_context)
- }
- });
- let prompt_builder = self.prompt_builder.clone();
- let slash_commands = self.slash_commands.clone();
-
- cx.spawn(async move |this, cx| {
- let saved_context = load.await?;
- let context = cx.new(|cx| {
- TextThread::deserialize(
- saved_context,
- path.clone(),
- languages,
- prompt_builder,
- slash_commands,
- cx,
- )
- });
- this.update(cx, |this, cx| {
- if let Some(existing_context) = this.loaded_text_thread_for_path(&path, cx) {
- existing_context
- } else {
- this.register_text_thread(&context, cx);
- context
- }
- })
- })
- }
-
- pub fn delete_local(&mut self, path: Arc<Path>, cx: &mut Context<Self>) -> Task<Result<()>> {
- let fs = self.fs.clone();
-
- cx.spawn(async move |this, cx| {
- fs.remove_file(
- &path,
- RemoveOptions {
- recursive: false,
- ignore_if_not_exists: true,
- },
- )
- .await?;
-
- this.update(cx, |this, cx| {
- this.text_threads.retain(|text_thread| {
- text_thread
- .upgrade()
- .and_then(|text_thread| text_thread.read(cx).path())
- != Some(&path)
- });
- this.text_threads_metadata
- .retain(|text_thread| text_thread.path.as_ref() != path.as_ref());
- })?;
-
- Ok(())
- })
- }
-
- pub fn delete_all_local(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
- let fs = self.fs.clone();
- let paths = self
- .text_threads_metadata
- .iter()
- .map(|metadata| metadata.path.clone())
- .collect::<Vec<_>>();
-
- cx.spawn(async move |this, cx| {
- for path in paths {
- fs.remove_file(
- &path,
- RemoveOptions {
- recursive: false,
- ignore_if_not_exists: true,
- },
- )
- .await?;
- }
-
- this.update(cx, |this, cx| {
- this.text_threads.clear();
- this.text_threads_metadata.clear();
- cx.notify();
- })?;
-
- Ok(())
- })
- }
-
- fn loaded_text_thread_for_path(&self, path: &Path, cx: &App) -> Option<Entity<TextThread>> {
- self.text_threads.iter().find_map(|text_thread| {
- let text_thread = text_thread.upgrade()?;
- if text_thread.read(cx).path().map(Arc::as_ref) == Some(path) {
- Some(text_thread)
- } else {
- None
- }
- })
- }
-
- pub fn loaded_text_thread_for_id(
- &self,
- id: &TextThreadId,
- cx: &App,
- ) -> Option<Entity<TextThread>> {
- self.text_threads.iter().find_map(|text_thread| {
- let text_thread = text_thread.upgrade()?;
- if text_thread.read(cx).id() == id {
- Some(text_thread)
- } else {
- None
- }
- })
- }
-
- pub fn open_remote(
- &mut self,
- text_thread_id: TextThreadId,
- cx: &mut Context<Self>,
- ) -> Task<Result<Entity<TextThread>>> {
- let Some(project) = self.project.upgrade() else {
- return Task::ready(Err(anyhow::anyhow!("project was dropped")));
- };
- let project = project.read(cx);
- let Some(project_id) = project.remote_id() else {
- return Task::ready(Err(anyhow::anyhow!("project was not remote")));
- };
-
- if let Some(context) = self.loaded_text_thread_for_id(&text_thread_id, cx) {
- return Task::ready(Ok(context));
- }
-
- let replica_id = project.replica_id();
- let capability = project.capability();
- let language_registry = self.languages.clone();
- let request = self.client.request(proto::OpenContext {
- project_id,
- context_id: text_thread_id.to_proto(),
- });
- let prompt_builder = self.prompt_builder.clone();
- let slash_commands = self.slash_commands.clone();
- cx.spawn(async move |this, cx| {
- let response = request.await?;
- let context_proto = response.context.context("invalid context")?;
- let text_thread = cx.new(|cx| {
- TextThread::new(
- text_thread_id.clone(),
- replica_id,
- capability,
- language_registry,
- prompt_builder,
- slash_commands,
- cx,
- )
- });
- let operations = cx
- .background_spawn(async move {
- context_proto
- .operations
- .into_iter()
- .map(TextThreadOperation::from_proto)
- .collect::<Result<Vec<_>>>()
- })
- .await?;
- text_thread.update(cx, |context, cx| context.apply_ops(operations, cx));
- this.update(cx, |this, cx| {
- if let Some(existing_context) = this.loaded_text_thread_for_id(&text_thread_id, cx)
- {
- existing_context
- } else {
- this.register_text_thread(&text_thread, cx);
- this.synchronize_contexts(cx);
- text_thread
- }
- })
- })
- }
-
- fn register_text_thread(&mut self, text_thread: &Entity<TextThread>, cx: &mut Context<Self>) {
- let handle = if self.project_is_shared {
- TextThreadHandle::Strong(text_thread.clone())
- } else {
- TextThreadHandle::Weak(text_thread.downgrade())
- };
- self.text_threads.push(handle);
- self.advertise_contexts(cx);
- cx.subscribe(text_thread, Self::handle_context_event)
- .detach();
- }
-
- fn handle_context_event(
- &mut self,
- text_thread: Entity<TextThread>,
- event: &TextThreadEvent,
- cx: &mut Context<Self>,
- ) {
- let Some(project) = self.project.upgrade() else {
- return;
- };
- let Some(project_id) = project.read(cx).remote_id() else {
- return;
- };
-
- match event {
- TextThreadEvent::SummaryChanged => {
- self.advertise_contexts(cx);
- }
- TextThreadEvent::PathChanged { old_path, new_path } => {
- if let Some(old_path) = old_path.as_ref() {
- for metadata in &mut self.text_threads_metadata {
- if &metadata.path == old_path {
- metadata.path = new_path.clone();
- break;
- }
- }
- }
- }
- TextThreadEvent::Operation(operation) => {
- let context_id = text_thread.read(cx).id().to_proto();
- let operation = operation.to_proto();
- self.client
- .send(proto::UpdateContext {
- project_id,
- context_id,
- operation: Some(operation),
- })
- .log_err();
- }
- _ => {}
- }
- }
-
- fn advertise_contexts(&self, cx: &App) {
- let Some(project) = self.project.upgrade() else {
- return;
- };
- let Some(project_id) = project.read(cx).remote_id() else {
- return;
- };
- // For now, only the host can advertise their open contexts.
- if project.read(cx).is_via_collab() {
- return;
- }
-
- let contexts = self
- .text_threads
- .iter()
- .rev()
- .filter_map(|text_thread| {
- let text_thread = text_thread.upgrade()?.read(cx);
- if text_thread.replica_id() == ReplicaId::default() {
- Some(proto::ContextMetadata {
- context_id: text_thread.id().to_proto(),
- summary: text_thread
- .summary()
- .content()
- .map(|summary| summary.text.clone()),
- })
- } else {
- None
- }
- })
- .collect();
- self.client
- .send(proto::AdvertiseContexts {
- project_id,
- contexts,
- })
- .ok();
- }
-
- fn synchronize_contexts(&mut self, cx: &mut Context<Self>) {
- let Some(project) = self.project.upgrade() else {
- return;
- };
- let Some(project_id) = project.read(cx).remote_id() else {
- return;
- };
-
- let text_threads = self
- .text_threads
- .iter()
- .filter_map(|text_thread| {
- let text_thread = text_thread.upgrade()?.read(cx);
- if text_thread.replica_id() != ReplicaId::default() {
- Some(text_thread.version(cx).to_proto(text_thread.id().clone()))
- } else {
- None
- }
- })
- .collect();
-
- let client = self.client.clone();
- let request = self.client.request(proto::SynchronizeContexts {
- project_id,
- contexts: text_threads,
- });
- cx.spawn(async move |this, cx| {
- let response = request.await?;
-
- let mut text_thread_ids = Vec::new();
- let mut operations = Vec::new();
- this.read_with(cx, |this, cx| {
- for context_version_proto in response.contexts {
- let text_thread_version = TextThreadVersion::from_proto(&context_version_proto);
- let text_thread_id = TextThreadId::from_proto(context_version_proto.context_id);
- if let Some(text_thread) = this.loaded_text_thread_for_id(&text_thread_id, cx) {
- text_thread_ids.push(text_thread_id);
- operations
- .push(text_thread.read(cx).serialize_ops(&text_thread_version, cx));
- }
- }
- })?;
-
- let operations = futures::future::join_all(operations).await;
- for (context_id, operations) in text_thread_ids.into_iter().zip(operations) {
- for operation in operations {
- client.send(proto::UpdateContext {
- project_id,
- context_id: context_id.to_proto(),
- operation: Some(operation),
- })?;
- }
- }
-
- anyhow::Ok(())
- })
- .detach_and_log_err(cx);
- }
-
- pub fn search(&self, query: String, cx: &App) -> Task<Vec<SavedTextThreadMetadata>> {
- let metadata = self.text_threads_metadata.clone();
- let executor = cx.background_executor().clone();
- cx.background_spawn(async move {
- if query.is_empty() {
- metadata
- } else {
- let candidates = metadata
- .iter()
- .enumerate()
- .map(|(id, metadata)| StringMatchCandidate::new(id, &metadata.title))
- .collect::<Vec<_>>();
- let matches = fuzzy::match_strings(
- &candidates,
- &query,
- false,
- true,
- 100,
- &Default::default(),
- executor,
- )
- .await;
-
- matches
- .into_iter()
- .map(|mat| metadata[mat.candidate_id].clone())
- .collect()
- }
- })
- }
-
- fn reload(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
- let fs = self.fs.clone();
- cx.spawn(async move |this, cx| {
- if *ZED_STATELESS {
- return Ok(());
- }
- fs.create_dir(text_threads_dir()).await?;
-
- let mut paths = fs.read_dir(text_threads_dir()).await?;
- let mut contexts = Vec::<SavedTextThreadMetadata>::new();
- while let Some(path) = paths.next().await {
- let path = path?;
- if path.extension() != Some(OsStr::new("json")) {
- continue;
- }
-
- static ASSISTANT_CONTEXT_REGEX: LazyLock<Regex> =
- LazyLock::new(|| Regex::new(r" - \d+.zed.json$").unwrap());
-
- let metadata = fs.metadata(&path).await?;
- if let Some((file_name, metadata)) = path
- .file_name()
- .and_then(|name| name.to_str())
- .zip(metadata)
- {
- // This is used to filter out contexts saved by the new assistant.
- if !ASSISTANT_CONTEXT_REGEX.is_match(file_name) {
- continue;
- }
-
- if let Some(title) = ASSISTANT_CONTEXT_REGEX
- .replace(file_name, "")
- .lines()
- .next()
- {
- contexts.push(SavedTextThreadMetadata {
- title: title.to_string().into(),
- path: path.into(),
- mtime: metadata.mtime.timestamp_for_user().into(),
- });
- }
- }
- }
- contexts.sort_unstable_by_key(|text_thread| Reverse(text_thread.mtime));
-
- this.update(cx, |this, cx| {
- this.text_threads_metadata = contexts;
- cx.notify();
- })
- })
- }
-
- fn register_context_server_handlers(&self, cx: &mut Context<Self>) {
- let Some(project) = self.project.upgrade() else {
- return;
- };
- let context_server_store = project.read(cx).context_server_store();
- cx.subscribe(&context_server_store, Self::handle_context_server_event)
- .detach();
-
- // Check for any servers that were already running before the handler was registered
- for server in context_server_store.read(cx).running_servers() {
- self.load_context_server_slash_commands(server.id(), context_server_store.clone(), cx);
- }
- }
-
- fn handle_context_server_event(
- &mut self,
- context_server_store: Entity<ContextServerStore>,
- event: &project::context_server_store::ServerStatusChangedEvent,
- cx: &mut Context<Self>,
- ) {
- let project::context_server_store::ServerStatusChangedEvent { server_id, status } = event;
-
- match status {
- ContextServerStatus::Running => {
- self.load_context_server_slash_commands(
- server_id.clone(),
- context_server_store,
- cx,
- );
- }
- ContextServerStatus::Stopped
- | ContextServerStatus::Error(_)
- | ContextServerStatus::AuthRequired => {
- if let Some(slash_command_ids) =
- self.context_server_slash_command_ids.remove(server_id)
- {
- self.slash_commands.remove(&slash_command_ids);
- }
- }
- ContextServerStatus::Starting | ContextServerStatus::Authenticating => {}
- }
- }
-
- fn load_context_server_slash_commands(
- &self,
- server_id: ContextServerId,
- context_server_store: Entity<ContextServerStore>,
- cx: &mut Context<Self>,
- ) {
- let Some(server) = context_server_store.read(cx).get_running_server(&server_id) else {
- return;
- };
- let slash_command_working_set = self.slash_commands.clone();
- cx.spawn(async move |this, cx| {
- let Some(protocol) = server.client() else {
- return;
- };
-
- if protocol.capable(context_server::protocol::ServerCapability::Prompts)
- && let Some(response) = protocol
- .request::<context_server::types::requests::PromptsList>(())
- .await
- .log_err()
- {
- let slash_command_ids = response
- .prompts
- .into_iter()
- .filter(context_server_command::acceptable_prompt)
- .map(|prompt| {
- log::info!("registering context server command: {:?}", prompt.name);
- slash_command_working_set.insert(Arc::new(
- context_server_command::ContextServerSlashCommand::new(
- context_server_store.clone(),
- server.id(),
- prompt,
- ),
- ))
- })
- .collect::<Vec<_>>();
-
- this.update(cx, |this, _cx| {
- this.context_server_slash_command_ids
- .insert(server_id.clone(), slash_command_ids);
- })
- .log_err();
- }
- })
- .detach();
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use fs::FakeFs;
- use language_model::LanguageModelRegistry;
- use project::Project;
- use serde_json::json;
- use settings::SettingsStore;
- use std::path::{Path, PathBuf};
- use std::sync::Arc;
-
- fn init_test(cx: &mut gpui::TestAppContext) {
- cx.update(|cx| {
- let settings_store = SettingsStore::test(cx);
- prompt_store::init(cx);
- LanguageModelRegistry::test(cx);
- cx.set_global(settings_store);
- });
- }
-
- #[gpui::test]
- async fn ordered_text_threads_sort_by_mtime(cx: &mut gpui::TestAppContext) {
- init_test(cx);
-
- let fs = FakeFs::new(cx.background_executor.clone());
- fs.insert_tree("/root", json!({})).await;
-
- let project = Project::test(fs, [Path::new("/root")], cx).await;
- let store = cx.new(|cx| TextThreadStore::fake(project, cx));
-
- let now = chrono::Local::now();
- let older = SavedTextThreadMetadata {
- title: "older".into(),
- path: Arc::from(PathBuf::from("/root/older.zed.json")),
- mtime: now - chrono::TimeDelta::days(1),
- };
- let middle = SavedTextThreadMetadata {
- title: "middle".into(),
- path: Arc::from(PathBuf::from("/root/middle.zed.json")),
- mtime: now - chrono::TimeDelta::hours(1),
- };
- let newer = SavedTextThreadMetadata {
- title: "newer".into(),
- path: Arc::from(PathBuf::from("/root/newer.zed.json")),
- mtime: now,
- };
-
- store.update(cx, |store, _| {
- store.text_threads_metadata = vec![middle, older, newer];
- });
-
- let ordered = store.read_with(cx, |store, _| {
- store
- .ordered_text_threads()
- .map(|entry| entry.title.to_string())
- .collect::<Vec<_>>()
- });
-
- assert_eq!(ordered, vec!["newer", "middle", "older"]);
- }
-
- #[gpui::test]
- async fn has_saved_text_threads_reflects_metadata(cx: &mut gpui::TestAppContext) {
- init_test(cx);
-
- let fs = FakeFs::new(cx.background_executor.clone());
- fs.insert_tree("/root", json!({})).await;
-
- let project = Project::test(fs, [Path::new("/root")], cx).await;
- let store = cx.new(|cx| TextThreadStore::fake(project, cx));
-
- assert!(!store.read_with(cx, |store, _| store.has_saved_text_threads()));
-
- store.update(cx, |store, _| {
- store.text_threads_metadata = vec![SavedTextThreadMetadata {
- title: "thread".into(),
- path: Arc::from(PathBuf::from("/root/thread.zed.json")),
- mtime: chrono::Local::now(),
- }];
- });
-
- assert!(store.read_with(cx, |store, _| store.has_saved_text_threads()));
- }
-
- #[gpui::test]
- async fn delete_all_local_clears_metadata_and_files(cx: &mut gpui::TestAppContext) {
- init_test(cx);
-
- let fs = FakeFs::new(cx.background_executor.clone());
- fs.insert_tree("/root", json!({})).await;
-
- let thread_a = PathBuf::from("/root/thread-a.zed.json");
- let thread_b = PathBuf::from("/root/thread-b.zed.json");
- fs.touch_path(&thread_a).await;
- fs.touch_path(&thread_b).await;
-
- let project = Project::test(fs.clone(), [Path::new("/root")], cx).await;
- let store = cx.new(|cx| TextThreadStore::fake(project, cx));
-
- let now = chrono::Local::now();
- store.update(cx, |store, cx| {
- store.create(cx);
- store.text_threads_metadata = vec![
- SavedTextThreadMetadata {
- title: "thread-a".into(),
- path: Arc::from(thread_a.clone()),
- mtime: now,
- },
- SavedTextThreadMetadata {
- title: "thread-b".into(),
- path: Arc::from(thread_b.clone()),
- mtime: now - chrono::TimeDelta::seconds(1),
- },
- ];
- });
-
- let task = store.update(cx, |store, cx| store.delete_all_local(cx));
- task.await.unwrap();
-
- assert!(!store.read_with(cx, |store, _| store.has_saved_text_threads()));
- assert_eq!(store.read_with(cx, |store, _| store.text_threads.len()), 0);
- assert!(fs.metadata(&thread_a).await.unwrap().is_none());
- assert!(fs.metadata(&thread_b).await.unwrap().is_none());
- }
-}
@@ -75,11 +75,6 @@ uuid.workspace = true
[dev-dependencies]
agent = { workspace = true, features = ["test-support"] }
-
-
-
-assistant_text_thread.workspace = true
-assistant_slash_command.workspace = true
async-trait.workspace = true
buffer_diff.workspace = true
@@ -410,9 +410,6 @@ impl Server {
.add_message_handler(update_followers)
.add_message_handler(acknowledge_channel_message)
.add_message_handler(acknowledge_buffer_version)
- .add_request_handler(forward_mutating_project_request::<proto::OpenContext>)
- .add_request_handler(forward_mutating_project_request::<proto::CreateContext>)
- .add_request_handler(forward_mutating_project_request::<proto::SynchronizeContexts>)
.add_request_handler(forward_mutating_project_request::<proto::Stage>)
.add_request_handler(forward_mutating_project_request::<proto::Unstage>)
.add_request_handler(forward_mutating_project_request::<proto::Stash>)
@@ -442,8 +439,6 @@ impl Server {
.add_request_handler(disallow_guest_request::<proto::GitRemoveWorktree>)
.add_request_handler(disallow_guest_request::<proto::GitRenameWorktree>)
.add_request_handler(forward_mutating_project_request::<proto::CheckForPushedCommits>)
- .add_message_handler(broadcast_project_message_from_host::<proto::AdvertiseContexts>)
- .add_message_handler(update_context)
.add_request_handler(forward_mutating_project_request::<proto::ToggleLspLogs>)
.add_message_handler(broadcast_project_message_from_host::<proto::LanguageServerLog>)
.add_request_handler(share_agent_thread)
@@ -2372,48 +2367,6 @@ async fn update_buffer(
Ok(())
}
-async fn update_context(message: proto::UpdateContext, session: MessageContext) -> Result<()> {
- let project_id = ProjectId::from_proto(message.project_id);
-
- let operation = message.operation.as_ref().context("invalid operation")?;
- let capability = match operation.variant.as_ref() {
- Some(proto::context_operation::Variant::BufferOperation(buffer_op)) => {
- if let Some(buffer_op) = buffer_op.operation.as_ref() {
- match buffer_op.variant {
- None | Some(proto::operation::Variant::UpdateSelections(_)) => {
- Capability::ReadOnly
- }
- _ => Capability::ReadWrite,
- }
- } else {
- Capability::ReadWrite
- }
- }
- Some(_) => Capability::ReadWrite,
- None => Capability::ReadOnly,
- };
-
- let guard = session
- .db()
- .await
- .connections_for_buffer_update(project_id, session.connection_id, capability)
- .await?;
-
- let (host, guests) = &*guard;
-
- broadcast(
- Some(session.connection_id),
- guests.iter().chain([host]).copied(),
- |connection_id| {
- session
- .peer
- .forward_send(session.connection_id, connection_id, message.clone())
- },
- );
-
- Ok(())
-}
-
async fn forward_project_search_chunk(
message: proto::FindSearchCandidatesChunk,
response: Response<proto::FindSearchCandidatesChunk>,
@@ -3,8 +3,6 @@ use crate::{
room_participants,
};
use anyhow::{Result, anyhow};
-use assistant_slash_command::SlashCommandWorkingSet;
-use assistant_text_thread::TextThreadStore;
use buffer_diff::{DiffHunkSecondaryStatus, DiffHunkStatus, assert_hunks};
use call::{ActiveCall, Room, room};
use client::{RECEIVE_TIMEOUT, User};
@@ -34,7 +32,6 @@ use project::{
lsp_store::{FormatTrigger, LspFormatTarget, SymbolLocation},
search::{SearchQuery, SearchResult},
};
-use prompt_store::PromptBuilder;
use rand::prelude::*;
use serde_json::json;
use settings::{LanguageServerFormatterSpecifier, PrettierSettingsContent, SettingsStore};
@@ -7095,141 +7092,6 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
});
}
-#[gpui::test(iterations = 10)]
-async fn test_context_collaboration_with_reconnect(
- executor: BackgroundExecutor,
- cx_a: &mut TestAppContext,
- cx_b: &mut TestAppContext,
-) {
- let mut server = TestServer::start(executor.clone()).await;
- let client_a = server.create_client(cx_a, "user_a").await;
- let client_b = server.create_client(cx_b, "user_b").await;
- server
- .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
- .await;
- let active_call_a = cx_a.read(ActiveCall::global);
-
- client_a.fs().insert_tree("/a", Default::default()).await;
- let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
- let project_id = active_call_a
- .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
- .await
- .unwrap();
- let project_b = client_b.join_remote_project(project_id, cx_b).await;
-
- // Client A sees that a guest has joined.
- executor.run_until_parked();
-
- project_a.read_with(cx_a, |project, _| {
- assert_eq!(project.collaborators().len(), 1);
- });
- project_b.read_with(cx_b, |project, _| {
- assert_eq!(project.collaborators().len(), 1);
- });
-
- let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
- let text_thread_store_a = cx_a
- .update(|cx| {
- TextThreadStore::new(
- project_a.clone(),
- prompt_builder.clone(),
- Arc::new(SlashCommandWorkingSet::default()),
- cx,
- )
- })
- .await
- .unwrap();
- let text_thread_store_b = cx_b
- .update(|cx| {
- TextThreadStore::new(
- project_b.clone(),
- prompt_builder.clone(),
- Arc::new(SlashCommandWorkingSet::default()),
- cx,
- )
- })
- .await
- .unwrap();
-
- // Client A creates a new chats.
- let text_thread_a = text_thread_store_a.update(cx_a, |store, cx| store.create(cx));
- executor.run_until_parked();
-
- // Client B retrieves host's contexts and joins one.
- let text_thread_b = text_thread_store_b
- .update(cx_b, |store, cx| {
- let host_text_threads = store.host_text_threads().collect::<Vec<_>>();
- assert_eq!(host_text_threads.len(), 1);
- store.open_remote(host_text_threads[0].id.clone(), cx)
- })
- .await
- .unwrap();
-
- // Host and guest make changes
- text_thread_a.update(cx_a, |text_thread, cx| {
- text_thread.buffer().update(cx, |buffer, cx| {
- buffer.edit([(0..0, "Host change\n")], None, cx)
- })
- });
- text_thread_b.update(cx_b, |text_thread, cx| {
- text_thread.buffer().update(cx, |buffer, cx| {
- buffer.edit([(0..0, "Guest change\n")], None, cx)
- })
- });
- executor.run_until_parked();
- assert_eq!(
- text_thread_a.read_with(cx_a, |text_thread, cx| text_thread.buffer().read(cx).text()),
- "Guest change\nHost change\n"
- );
- assert_eq!(
- text_thread_b.read_with(cx_b, |text_thread, cx| text_thread.buffer().read(cx).text()),
- "Guest change\nHost change\n"
- );
-
- // Disconnect client A and make some changes while disconnected.
- server.disconnect_client(client_a.peer_id().unwrap());
- server.forbid_connections();
- text_thread_a.update(cx_a, |text_thread, cx| {
- text_thread.buffer().update(cx, |buffer, cx| {
- buffer.edit([(0..0, "Host offline change\n")], None, cx)
- })
- });
- text_thread_b.update(cx_b, |text_thread, cx| {
- text_thread.buffer().update(cx, |buffer, cx| {
- buffer.edit([(0..0, "Guest offline change\n")], None, cx)
- })
- });
- executor.run_until_parked();
- assert_eq!(
- text_thread_a.read_with(cx_a, |text_thread, cx| text_thread.buffer().read(cx).text()),
- "Host offline change\nGuest change\nHost change\n"
- );
- assert_eq!(
- text_thread_b.read_with(cx_b, |text_thread, cx| text_thread.buffer().read(cx).text()),
- "Guest offline change\nGuest change\nHost change\n"
- );
-
- // Allow client A to reconnect and verify that contexts converge.
- server.allow_connections();
- executor.advance_clock(RECEIVE_TIMEOUT);
- assert_eq!(
- text_thread_a.read_with(cx_a, |text_thread, cx| text_thread.buffer().read(cx).text()),
- "Guest offline change\nHost offline change\nGuest change\nHost change\n"
- );
- assert_eq!(
- text_thread_b.read_with(cx_b, |text_thread, cx| text_thread.buffer().read(cx).text()),
- "Guest offline change\nHost offline change\nGuest change\nHost change\n"
- );
-
- // Client A disconnects without being able to reconnect. Context B becomes readonly.
- server.forbid_connections();
- server.disconnect_client(client_a.peer_id().unwrap());
- executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
- text_thread_b.read_with(cx_b, |text_thread, cx| {
- assert!(text_thread.buffer().read(cx).read_only());
- });
-}
-
#[gpui::test]
async fn test_remote_git_branches(
executor: BackgroundExecutor,
@@ -356,7 +356,6 @@ impl TestServer {
settings::KeymapFile::load_asset_allow_partial_failure(os_keymap, cx).unwrap(),
);
language_model::LanguageModelRegistry::test(cx);
- assistant_text_thread::init(client.clone(), cx);
});
client
@@ -543,9 +543,9 @@ fn template_and_validate_json_snippets(book: &mut Book, errors: &mut HashSet<Pre
/// This will return the action name unmodified.
///
/// ```
-/// let action_as_str = "assistant::Assist";
+/// let action_as_str = "workspace::Save";
/// let action_name = name_for_action(action_as_str);
-/// assert_eq!(action_name, "assistant::Assist");
+/// assert_eq!(action_name, "workspace::Save");
/// ```
///
/// This will return the action name with any trailing options removed.
@@ -118,7 +118,6 @@ pub fn init(cx: &mut App) -> Arc<AgentCliAppState> {
let prompt_builder = PromptBuilder::load(fs.clone(), stdout_is_a_pty, cx);
agent_ui::init(
fs.clone(),
- client.clone(),
prompt_builder,
languages.clone(),
true,
@@ -8,7 +8,7 @@ use language::{BinaryStatus, LanguageMatcher, LanguageName, LoadedLanguage};
use lsp::LanguageServerName;
use parking_lot::RwLock;
-use crate::{Extension, SlashCommand};
+use crate::Extension;
#[derive(Default)]
struct GlobalExtensionHostProxy(Arc<ExtensionHostProxy>);
@@ -29,7 +29,6 @@ pub struct ExtensionHostProxy {
language_proxy: RwLock<Option<Arc<dyn ExtensionLanguageProxy>>>,
language_server_proxy: RwLock<Option<Arc<dyn ExtensionLanguageServerProxy>>>,
snippet_proxy: RwLock<Option<Arc<dyn ExtensionSnippetProxy>>>,
- slash_command_proxy: RwLock<Option<Arc<dyn ExtensionSlashCommandProxy>>>,
context_server_proxy: RwLock<Option<Arc<dyn ExtensionContextServerProxy>>>,
debug_adapter_provider_proxy: RwLock<Option<Arc<dyn ExtensionDebugAdapterProviderProxy>>>,
language_model_provider_proxy: RwLock<Option<Arc<dyn ExtensionLanguageModelProviderProxy>>>,
@@ -55,7 +54,6 @@ impl ExtensionHostProxy {
language_proxy: RwLock::default(),
language_server_proxy: RwLock::default(),
snippet_proxy: RwLock::default(),
- slash_command_proxy: RwLock::default(),
context_server_proxy: RwLock::default(),
debug_adapter_provider_proxy: RwLock::default(),
language_model_provider_proxy: RwLock::default(),
@@ -82,10 +80,6 @@ impl ExtensionHostProxy {
self.snippet_proxy.write().replace(Arc::new(proxy));
}
- pub fn register_slash_command_proxy(&self, proxy: impl ExtensionSlashCommandProxy) {
- self.slash_command_proxy.write().replace(Arc::new(proxy));
- }
-
pub fn register_context_server_proxy(&self, proxy: impl ExtensionContextServerProxy) {
self.context_server_proxy.write().replace(Arc::new(proxy));
}
@@ -356,30 +350,6 @@ impl ExtensionSnippetProxy for ExtensionHostProxy {
}
}
-pub trait ExtensionSlashCommandProxy: Send + Sync + 'static {
- fn register_slash_command(&self, extension: Arc<dyn Extension>, command: SlashCommand);
-
- fn unregister_slash_command(&self, command_name: Arc<str>);
-}
-
-impl ExtensionSlashCommandProxy for ExtensionHostProxy {
- fn register_slash_command(&self, extension: Arc<dyn Extension>, command: SlashCommand) {
- let Some(proxy) = self.slash_command_proxy.read().clone() else {
- return;
- };
-
- proxy.register_slash_command(extension, command)
- }
-
- fn unregister_slash_command(&self, command_name: Arc<str>) {
- let Some(proxy) = self.slash_command_proxy.read().clone() else {
- return;
- };
-
- proxy.unregister_slash_command(command_name)
- }
-}
-
pub trait ExtensionContextServerProxy: Send + Sync + 'static {
fn register_context_server(
&self,
@@ -17,8 +17,7 @@ use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder};
use extension::{
ExtensionContextServerProxy, ExtensionDebugAdapterProviderProxy, ExtensionEvents,
ExtensionGrammarProxy, ExtensionHostProxy, ExtensionLanguageProxy,
- ExtensionLanguageServerProxy, ExtensionSlashCommandProxy, ExtensionSnippetProxy,
- ExtensionThemeProxy,
+ ExtensionLanguageServerProxy, ExtensionSnippetProxy, ExtensionThemeProxy,
};
use fs::{Fs, RemoveOptions};
use futures::future::join_all;
@@ -1209,9 +1208,6 @@ impl ExtensionStore {
for locator in extension.manifest.debug_locators.keys() {
self.proxy.unregister_debug_locator(locator.clone());
}
- for command_name in extension.manifest.slash_commands.keys() {
- self.proxy.unregister_slash_command(command_name.clone());
- }
}
self.wasm_extensions
@@ -1430,21 +1426,6 @@ impl ExtensionStore {
}
}
- for (slash_command_name, slash_command) in &manifest.slash_commands {
- this.proxy.register_slash_command(
- extension.clone(),
- extension::SlashCommand {
- name: slash_command_name.to_string(),
- description: slash_command.description.to_string(),
- // We don't currently expose this as a configurable option, as it currently drives
- // the `menu_text` on the `SlashCommand` trait, which is not used for slash commands
- // defined in extensions, as they are not able to be added to the menu.
- tooltip_text: String::new(),
- requires_argument: slash_command.requires_argument,
- },
- );
- }
-
for id in manifest.context_servers.keys() {
this.proxy
.register_context_server(extension.clone(), id.clone(), cx);
@@ -69,10 +69,6 @@ pub fn init(cx: &mut App) {
ExtensionProvides::ContextServers
}
ExtensionCategoryFilter::AgentServers => ExtensionProvides::AgentServers,
- ExtensionCategoryFilter::SlashCommands => ExtensionProvides::SlashCommands,
- ExtensionCategoryFilter::IndexedDocsProviders => {
- ExtensionProvides::IndexedDocsProviders
- }
ExtensionCategoryFilter::Snippets => ExtensionProvides::Snippets,
ExtensionCategoryFilter::DebugAdapters => ExtensionProvides::DebugAdapters,
});
@@ -236,7 +236,6 @@ pub enum IconName {
Terminal,
TerminalAlt,
TextSnippet,
- TextThread,
ThinkingMode,
ThinkingModeOff,
Thread,
@@ -472,9 +472,6 @@ pub struct EditPredictionSettings {
/// Settings specific to Ollama.
pub ollama: Option<OpenAiCompatibleEditPredictionSettings>,
pub open_ai_compatible_api: Option<OpenAiCompatibleEditPredictionSettings>,
- /// Whether edit predictions are enabled in the assistant panel.
- /// This setting has no effect if globally disabled.
- pub enabled_in_text_threads: bool,
pub examples_dir: Option<Arc<Path>>,
}
@@ -820,8 +817,6 @@ impl settings::Settings for AllLanguageSettings {
prompt_format: openai_compatible_settings.prompt_format.unwrap(),
});
- let enabled_in_text_threads = edit_predictions.enabled_in_text_threads.unwrap();
-
let mut file_types: FxHashMap<Arc<str>, (GlobSet, Vec<String>)> = FxHashMap::default();
for (language, patterns) in all_languages.file_types.iter().flatten() {
@@ -859,7 +854,6 @@ impl settings::Settings for AllLanguageSettings {
codestral: codestral_settings,
ollama: ollama_settings,
open_ai_compatible_api: openai_compatible_settings,
- enabled_in_text_threads,
examples_dir: edit_predictions.examples_dir,
},
defaults: default_language_settings,
@@ -34,7 +34,6 @@ log.workspace = true
open_ai = { workspace = true, features = ["schemars"] }
open_router.workspace = true
parking_lot.workspace = true
-proto.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
@@ -857,16 +857,6 @@ pub enum ConfigurationViewTargetAgent {
Other(SharedString),
}
-#[derive(PartialEq, Eq)]
-pub enum LanguageModelProviderTosView {
- /// When there are some past interactions in the Agent Panel.
- ThreadEmptyState,
- /// When there are no past interactions in the Agent Panel.
- ThreadFreshStart,
- TextThreadPopup,
- Configuration,
-}
-
pub trait LanguageModelProviderState: 'static {
type ObservableEntity;
@@ -10,23 +10,6 @@ pub enum Role {
}
impl Role {
- pub fn from_proto(role: i32) -> Role {
- match proto::LanguageModelRole::from_i32(role) {
- Some(proto::LanguageModelRole::LanguageModelUser) => Role::User,
- Some(proto::LanguageModelRole::LanguageModelAssistant) => Role::Assistant,
- Some(proto::LanguageModelRole::LanguageModelSystem) => Role::System,
- None => Role::User,
- }
- }
-
- pub fn to_proto(self) -> proto::LanguageModelRole {
- match self {
- Role::User => proto::LanguageModelRole::LanguageModelUser,
- Role::Assistant => proto::LanguageModelRole::LanguageModelAssistant,
- Role::System => proto::LanguageModelRole::LanguageModelSystem,
- }
- }
-
pub fn cycle(self) -> Role {
match self {
Role::User => Role::Assistant,
@@ -316,3 +316,9 @@ pub(crate) mod m_2026_03_23 {
pub(crate) use keymap::KEYMAP_PATTERNS;
}
+
+pub(crate) mod m_2026_03_31 {
+ mod settings;
+
+ pub(crate) use settings::remove_text_thread_settings;
+}
@@ -0,0 +1,29 @@
+use anyhow::Result;
+use serde_json::Value;
+
+use crate::migrations::migrate_settings;
+
+pub fn remove_text_thread_settings(value: &mut Value) -> Result<()> {
+ migrate_settings(value, &mut migrate_one)
+}
+
+fn migrate_one(obj: &mut serde_json::Map<String, Value>) -> Result<()> {
+ // Remove `agent.default_view`
+ if let Some(agent) = obj.get_mut("agent") {
+ if let Some(agent_obj) = agent.as_object_mut() {
+ agent_obj.remove("default_view");
+ }
+ }
+
+ // Remove `edit_predictions.enabled_in_text_threads`
+ if let Some(edit_predictions) = obj.get_mut("edit_predictions") {
+ if let Some(edit_predictions_obj) = edit_predictions.as_object_mut() {
+ edit_predictions_obj.remove("enabled_in_text_threads");
+ }
+ }
+
+ // Remove top-level `slash_commands`
+ obj.remove("slash_commands");
+
+ Ok(())
+}
@@ -247,6 +247,7 @@ pub fn migrate_settings(text: &str) -> Result<Option<String>> {
migrations::m_2026_03_16::SETTINGS_PATTERNS,
&SETTINGS_QUERY_2026_03_16,
),
+ MigrationType::Json(migrations::m_2026_03_31::remove_text_thread_settings),
];
run_migrations(text, migrations)
}
@@ -940,8 +941,7 @@ mod tests {
"foo": "bar"
},
"edit_predictions": {
- "enabled_in_text_threads": false,
- }
+ }
}"#,
),
);
@@ -4480,4 +4480,109 @@ mod tests {
),
);
}
+
+ #[test]
+ fn test_remove_text_thread_settings() {
+ assert_migrate_with_migrations(
+ &[MigrationType::Json(
+ migrations::m_2026_03_31::remove_text_thread_settings,
+ )],
+ r#"{
+ "agent": {
+ "default_model": {
+ "provider": "anthropic",
+ "model": "claude-sonnet"
+ },
+ "default_view": "text_thread"
+ },
+ "edit_predictions": {
+ "mode": "eager",
+ "enabled_in_text_threads": true
+ },
+ "slash_commands": {
+ "cargo_workspace": {
+ "enabled": true
+ }
+ }
+}"#,
+ Some(
+ r#"{
+ "agent": {
+ "default_model": {
+ "provider": "anthropic",
+ "model": "claude-sonnet"
+ }
+ },
+ "edit_predictions": {
+ "mode": "eager"
+ }
+}"#,
+ ),
+ );
+ }
+
+ #[test]
+ fn test_remove_text_thread_settings_only_default_view() {
+ assert_migrate_with_migrations(
+ &[MigrationType::Json(
+ migrations::m_2026_03_31::remove_text_thread_settings,
+ )],
+ r#"{
+ "agent": {
+ "default_model": "claude-sonnet",
+ "default_view": "thread"
+ }
+}"#,
+ Some(
+ r#"{
+ "agent": {
+ "default_model": "claude-sonnet"
+ }
+}"#,
+ ),
+ );
+ }
+
+ #[test]
+ fn test_remove_text_thread_settings_only_slash_commands() {
+ assert_migrate_with_migrations(
+ &[MigrationType::Json(
+ migrations::m_2026_03_31::remove_text_thread_settings,
+ )],
+ r#"{
+ "slash_commands": {
+ "cargo_workspace": {
+ "enabled": true
+ }
+ },
+ "vim_mode": true
+}"#,
+ Some(
+ r#"{
+ "vim_mode": true
+}"#,
+ ),
+ );
+ }
+
+ #[test]
+ fn test_remove_text_thread_settings_none_present() {
+ assert_migrate_with_migrations(
+ &[MigrationType::Json(
+ migrations::m_2026_03_31::remove_text_thread_settings,
+ )],
+ r#"{
+ "agent": {
+ "default_model": {
+ "provider": "anthropic",
+ "model": "claude-sonnet"
+ }
+ },
+ "edit_predictions": {
+ "mode": "eager"
+ }
+}"#,
+ None,
+ );
+ }
}
@@ -310,30 +310,6 @@ pub fn snippets_dir() -> &'static PathBuf {
SNIPPETS_DIR.get_or_init(|| config_dir().join("snippets"))
}
-// Returns old path to contexts directory.
-// Fallback
-fn text_threads_dir_fallback() -> &'static PathBuf {
- static CONTEXTS_DIR: OnceLock<PathBuf> = OnceLock::new();
- CONTEXTS_DIR.get_or_init(|| {
- if cfg!(target_os = "macos") {
- config_dir().join("conversations")
- } else {
- data_dir().join("conversations")
- }
- })
-}
-/// Returns the path to the contexts directory.
-///
-/// This is where the saved contexts from the Assistant are stored.
-pub fn text_threads_dir() -> &'static PathBuf {
- let fallback_dir = text_threads_dir_fallback();
- if fallback_dir.exists() {
- return fallback_dir;
- }
- static CONTEXTS_DIR: OnceLock<PathBuf> = OnceLock::new();
- CONTEXTS_DIR.get_or_init(|| state_dir().join("conversations"))
-}
-
/// Returns the path to the contexts directory.
///
/// This is where the prompts for use with the Assistant are stored.
@@ -1,171 +1,8 @@
syntax = "proto3";
package zed.messages;
-import "buffer.proto";
import "task.proto";
-message Context {
- repeated ContextOperation operations = 1;
-}
-
-message ContextMetadata {
- string context_id = 1;
- optional string summary = 2;
-}
-
-message ContextMessageStatus {
- oneof variant {
- Done done = 1;
- Pending pending = 2;
- Error error = 3;
- Canceled canceled = 4;
- }
-
- message Done {}
-
- message Pending {}
-
- message Error {
- string message = 1;
- }
-
- message Canceled {}
-}
-
-message ContextMessage {
- LamportTimestamp id = 1;
- Anchor start = 2;
- LanguageModelRole role = 3;
- ContextMessageStatus status = 4;
-}
-
-message SlashCommandOutputSection {
- AnchorRange range = 1;
- string icon_name = 2;
- string label = 3;
- optional string metadata = 4;
-}
-
-message ThoughtProcessOutputSection {
- AnchorRange range = 1;
-}
-
-message ContextOperation {
- oneof variant {
- InsertMessage insert_message = 1;
- UpdateMessage update_message = 2;
- UpdateSummary update_summary = 3;
- BufferOperation buffer_operation = 5;
- SlashCommandStarted slash_command_started = 6;
- SlashCommandOutputSectionAdded slash_command_output_section_added = 7;
- SlashCommandCompleted slash_command_completed = 8;
- ThoughtProcessOutputSectionAdded thought_process_output_section_added = 9;
- }
-
- reserved 4;
-
- message InsertMessage {
- ContextMessage message = 1;
- repeated VectorClockEntry version = 2;
- }
-
- message UpdateMessage {
- LamportTimestamp message_id = 1;
- LanguageModelRole role = 2;
- ContextMessageStatus status = 3;
- LamportTimestamp timestamp = 4;
- repeated VectorClockEntry version = 5;
- }
-
- message UpdateSummary {
- string summary = 1;
- bool done = 2;
- LamportTimestamp timestamp = 3;
- repeated VectorClockEntry version = 4;
- }
-
- message SlashCommandStarted {
- LamportTimestamp id = 1;
- AnchorRange output_range = 2;
- string name = 3;
- repeated VectorClockEntry version = 4;
- }
-
- message SlashCommandOutputSectionAdded {
- LamportTimestamp timestamp = 1;
- SlashCommandOutputSection section = 2;
- repeated VectorClockEntry version = 3;
- }
-
- message SlashCommandCompleted {
- LamportTimestamp id = 1;
- LamportTimestamp timestamp = 3;
- optional string error_message = 4;
- repeated VectorClockEntry version = 5;
- }
-
- message ThoughtProcessOutputSectionAdded {
- LamportTimestamp timestamp = 1;
- ThoughtProcessOutputSection section = 2;
- repeated VectorClockEntry version = 3;
- }
-
- message BufferOperation {
- Operation operation = 1;
- }
-}
-
-message AdvertiseContexts {
- uint64 project_id = 1;
- repeated ContextMetadata contexts = 2;
-}
-
-message OpenContext {
- uint64 project_id = 1;
- string context_id = 2;
-}
-
-message OpenContextResponse {
- Context context = 1;
-}
-
-message CreateContext {
- uint64 project_id = 1;
-}
-
-message CreateContextResponse {
- string context_id = 1;
- Context context = 2;
-}
-
-message UpdateContext {
- uint64 project_id = 1;
- string context_id = 2;
- ContextOperation operation = 3;
-}
-
-message ContextVersion {
- string context_id = 1;
- repeated VectorClockEntry context_version = 2;
- repeated VectorClockEntry buffer_version = 3;
-}
-
-message SynchronizeContexts {
- uint64 project_id = 1;
- repeated ContextVersion contexts = 2;
-}
-
-message SynchronizeContextsResponse {
- repeated ContextVersion contexts = 1;
-}
-
-enum LanguageModelRole {
- LanguageModelUser = 0;
- LanguageModelAssistant = 1;
- LanguageModelSystem = 2;
- reserved 3;
-}
-
message GetAgentServerCommand {
uint64 project_id = 1;
string name = 2;
@@ -370,9 +370,10 @@ message View {
oneof variant {
Editor editor = 3;
ChannelView channel_view = 4;
- ContextEditor context_editor = 5;
}
+ reserved 5;
+
message Editor {
bool singleton = 1;
optional string title = 2;
@@ -390,11 +391,6 @@ message View {
uint64 channel_id = 1;
Editor editor = 2;
}
-
- message ContextEditor {
- string context_id = 1;
- Editor editor = 2;
- }
}
message ExcerptInsertion {
@@ -223,15 +223,6 @@ message Envelope {
LinkedEditingRange linked_editing_range = 209;
LinkedEditingRangeResponse linked_editing_range_response = 210;
- AdvertiseContexts advertise_contexts = 211;
- OpenContext open_context = 212;
- OpenContextResponse open_context_response = 213;
- CreateContext create_context = 232;
- CreateContextResponse create_context_response = 233;
- UpdateContext update_context = 214;
- SynchronizeContexts synchronize_contexts = 215;
- SynchronizeContextsResponse synchronize_contexts_response = 216;
-
GetSignatureHelp get_signature_help = 217;
GetSignatureHelpResponse get_signature_help_response = 218;
@@ -502,6 +493,7 @@ message Envelope {
reserved 332 to 333;
reserved 394 to 396;
reserved 429 to 430;
+ reserved 211 to 216, 232 to 233;
}
message Hello {
@@ -32,7 +32,6 @@ messages!(
(AddProjectCollaborator, Foreground),
(AddWorktree, Foreground),
(AddWorktreeResponse, Foreground),
- (AdvertiseContexts, Foreground),
(AllocateWorktreeId, Foreground),
(AllocateWorktreeIdResponse, Foreground),
(ApplyCodeAction, Background),
@@ -58,8 +57,6 @@ messages!(
(CreateFileForPeer, Foreground),
(CreateChannel, Foreground),
(CreateChannelResponse, Foreground),
- (CreateContext, Foreground),
- (CreateContextResponse, Foreground),
(CreateProjectEntry, Foreground),
(CreateRoom, Foreground),
(CreateRoomResponse, Foreground),
@@ -191,8 +188,6 @@ messages!(
(OpenBufferResponse, Background),
(OpenImageResponse, Background),
(OpenCommitMessageBuffer, Background),
- (OpenContext, Foreground),
- (OpenContextResponse, Foreground),
(OpenNewBuffer, Foreground),
(OpenServerSettings, Foreground),
(PerformRename, Background),
@@ -258,8 +253,6 @@ messages!(
(ToggleBreakpoint, Foreground),
(SynchronizeBuffers, Foreground),
(SynchronizeBuffersResponse, Foreground),
- (SynchronizeContexts, Foreground),
- (SynchronizeContextsResponse, Foreground),
(TaskContext, Background),
(TaskContextForLocation, Background),
(Test, Foreground),
@@ -278,7 +271,6 @@ messages!(
(UpdateChannelMessage, Foreground),
(UpdateChannels, Foreground),
(UpdateContacts, Foreground),
- (UpdateContext, Foreground),
(UpdateDiagnosticSummary, Foreground),
(UpdateDiffBases, Foreground),
(UpdateFollowers, Foreground),
@@ -496,9 +488,6 @@ request_messages!(
(LspQueryResponse, Ack),
(RestartLanguageServers, Ack),
(StopLanguageServers, Ack),
- (OpenContext, OpenContextResponse),
- (CreateContext, CreateContextResponse),
- (SynchronizeContexts, SynchronizeContextsResponse),
(LspExtSwitchSourceHeader, LspExtSwitchSourceHeaderResponse),
(LspExtGoToParentModule, LspExtGoToParentModuleResponse),
(LspExtCancelFlycheck, Ack),
@@ -684,11 +673,6 @@ entity_messages!(
LspExtExpandMacro,
LspExtOpenDocs,
LspExtRunnables,
- AdvertiseContexts,
- OpenContext,
- CreateContext,
- UpdateContext,
- SynchronizeContexts,
LspExtSwitchSourceHeader,
LspExtGoToParentModule,
LspExtCancelFlycheck,
@@ -1,6 +1,6 @@
use anyhow::Result;
use collections::{HashMap, HashSet};
-use editor::{CompletionProvider, SelectionEffects};
+use editor::SelectionEffects;
use editor::{CurrentLineHighlight, Editor, EditorElement, EditorEvent, EditorStyle, actions::Tab};
use gpui::{
App, Bounds, DEFAULT_ADDITIONAL_WINDOW_SIZE, Entity, EventEmitter, Focusable, PromptLevel,
@@ -16,7 +16,6 @@ use platform_title_bar::PlatformTitleBar;
use release_channel::ReleaseChannel;
use rope::Rope;
use settings::{ActionSequence, Settings};
-use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::time::Duration;
@@ -76,7 +75,6 @@ pub trait InlineAssistDelegate {
pub fn open_rules_library(
language_registry: Arc<LanguageRegistry>,
inline_assist_delegate: Box<dyn InlineAssistDelegate>,
- make_completion_provider: Rc<dyn Fn() -> Rc<dyn CompletionProvider>>,
prompt_to_select: Option<PromptId>,
cx: &mut App,
) -> Task<Result<WindowHandle<RulesLibrary>>> {
@@ -141,7 +139,6 @@ pub fn open_rules_library(
store,
language_registry,
inline_assist_delegate,
- make_completion_provider,
prompt_to_select,
window,
cx,
@@ -162,7 +159,6 @@ pub struct RulesLibrary {
picker: Entity<Picker<RulePickerDelegate>>,
pending_load: Task<()>,
inline_assist_delegate: Box<dyn InlineAssistDelegate>,
- make_completion_provider: Rc<dyn Fn() -> Rc<dyn CompletionProvider>>,
_subscriptions: Vec<Subscription>,
}
@@ -471,7 +467,6 @@ impl RulesLibrary {
store: Entity<PromptStore>,
language_registry: Arc<LanguageRegistry>,
inline_assist_delegate: Box<dyn InlineAssistDelegate>,
- make_completion_provider: Rc<dyn Fn() -> Rc<dyn CompletionProvider>>,
rule_to_select: Option<PromptId>,
window: &mut Window,
cx: &mut Context<Self>,
@@ -514,7 +509,6 @@ impl RulesLibrary {
active_rule_id: None,
pending_load: Task::ready(()),
inline_assist_delegate,
- make_completion_provider,
_subscriptions: vec![cx.subscribe_in(&picker, window, Self::handle_picker_event)],
picker,
}
@@ -721,7 +715,6 @@ impl RulesLibrary {
} else if let Some(rule_metadata) = self.store.read(cx).metadata(prompt_id) {
let language_registry = self.language_registry.clone();
let rule = self.store.read(cx).load(prompt_id, cx);
- let make_completion_provider = self.make_completion_provider.clone();
self.pending_load = cx.spawn_in(window, async move |this, cx| {
let rule = rule.await;
let markdown = language_registry.language_for_name("Markdown").await;
@@ -756,7 +749,6 @@ impl RulesLibrary {
editor.set_show_indent_guides(false, cx);
editor.set_use_modal_editing(true);
editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
- editor.set_completion_provider(Some(make_completion_provider()));
if focus {
window.focus(&editor.focus_handle(cx), cx);
}
@@ -509,7 +509,6 @@ impl VsCodeSettings {
context_servers: self.context_servers(),
context_server_timeout: None,
load_direnv: None,
- slash_commands: None,
git_hosting_providers: None,
disable_ai: None,
}
@@ -146,10 +146,6 @@ pub struct AgentSettingsContent {
///
/// Default: write
pub default_profile: Option<Arc<str>>,
- /// Which view type to show by default in the agent panel.
- ///
- /// Default: "thread"
- pub default_view: Option<DefaultAgentView>,
/// Where new threads should start by default.
///
/// Default: "local_project"
@@ -327,14 +323,6 @@ pub struct ContextServerPresetContent {
pub tools: IndexMap<Arc<str>, bool>,
}
-#[derive(Copy, Clone, Default, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
-#[serde(rename_all = "snake_case")]
-pub enum DefaultAgentView {
- #[default]
- Thread,
- TextThread,
-}
-
#[derive(
Copy,
Clone,
@@ -180,9 +180,6 @@ pub struct EditPredictionSettingsContent {
pub ollama: Option<OllamaEditPredictionSettingsContent>,
/// Settings specific to using custom OpenAI-compatible servers for edit prediction.
pub open_ai_compatible_api: Option<CustomEditPredictionProviderSettingsContent>,
- /// Whether edit predictions are enabled in the assistant prompt editor.
- /// This has no effect if globally disabled.
- pub enabled_in_text_threads: Option<bool>,
/// The directory where manually captured edit prediction examples are stored.
pub examples_dir: Option<Arc<Path>>,
}
@@ -14,7 +14,7 @@ use util::serde::default_true;
use crate::{
AllLanguageSettingsContent, DelayMs, ExtendingVec, ParseStatus, ProjectTerminalSettingsContent,
- RootUserSettings, SaturatingBool, SlashCommandSettings, fallible_options,
+ RootUserSettings, SaturatingBool, fallible_options,
};
#[with_fallible_options]
@@ -78,9 +78,6 @@ pub struct ProjectSettingsContent {
/// Configuration for how direnv configuration should be loaded
pub load_direnv: Option<DirenvSettings>,
- /// Settings for slash commands.
- pub slash_commands: Option<SlashCommandSettings>,
-
/// The list of custom Git hosting providers.
pub git_hosting_providers: Option<ExtendingVec<GitHostingProviderConfig>>,
@@ -483,22 +483,6 @@ pub enum DockPosition {
Right,
}
-/// Settings for slash commands.
-#[with_fallible_options]
-#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, MergeFrom, PartialEq, Eq)]
-pub struct SlashCommandSettings {
- /// Settings for the `/cargo-workspace` slash command.
- pub cargo_workspace: Option<CargoWorkspaceCommandSettings>,
-}
-
-/// Settings for the `/cargo-workspace` slash command.
-#[with_fallible_options]
-#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, MergeFrom, PartialEq, Eq)]
-pub struct CargoWorkspaceCommandSettings {
- /// Whether `/cargo-workspace` is enabled.
- pub enabled: Option<bool>,
-}
-
/// Configuration of voice calls in Zed.
#[with_fallible_options]
#[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug)]
@@ -7470,61 +7470,33 @@ fn ai_page(cx: &App) -> SettingsPage {
]
}
- fn edit_prediction_display_sub_section() -> [SettingsPageItem; 2] {
- [
- SettingsPageItem::SettingItem(SettingItem {
- title: "Display Mode",
- description: "When to show edit predictions previews in buffer. The eager mode displays them inline, while the subtle mode displays them only when holding a modifier key.",
- field: Box::new(SettingField {
- json_path: Some("edit_prediction.display_mode"),
- pick: |settings_content| {
- settings_content
- .project
- .all_languages
- .edit_predictions
- .as_ref()?
- .mode
- .as_ref()
- },
- write: |settings_content, value| {
- settings_content
- .project
- .all_languages
- .edit_predictions
- .get_or_insert_default()
- .mode = value;
- },
- }),
- metadata: None,
- files: USER,
- }),
- SettingsPageItem::SettingItem(SettingItem {
- title: "Display In Text Threads",
- description: "Whether edit predictions are enabled when editing text threads in the agent panel.",
- field: Box::new(SettingField {
- json_path: Some("edit_prediction.in_text_threads"),
- pick: |settings_content| {
- settings_content
- .project
- .all_languages
- .edit_predictions
- .as_ref()?
- .enabled_in_text_threads
- .as_ref()
- },
- write: |settings_content, value| {
- settings_content
- .project
- .all_languages
- .edit_predictions
- .get_or_insert_default()
- .enabled_in_text_threads = value;
- },
- }),
- metadata: None,
- files: USER,
+ fn edit_prediction_display_sub_section() -> [SettingsPageItem; 1] {
+ [SettingsPageItem::SettingItem(SettingItem {
+ title: "Display Mode",
+ description: "When to show edit predictions previews in buffer. The eager mode displays them inline, while the subtle mode displays them only when holding a modifier key.",
+ field: Box::new(SettingField {
+ json_path: Some("edit_prediction.display_mode"),
+ pick: |settings_content| {
+ settings_content
+ .project
+ .all_languages
+ .edit_predictions
+ .as_ref()?
+ .mode
+ .as_ref()
+ },
+ write: |settings_content, value| {
+ settings_content
+ .project
+ .all_languages
+ .edit_predictions
+ .get_or_insert_default()
+ .mode = value;
+ },
}),
- ]
+ metadata: None,
+ files: USER,
+ })]
}
SettingsPage {
@@ -49,7 +49,6 @@ zed_actions.workspace = true
acp_thread = { workspace = true, features = ["test-support"] }
agent = { workspace = true, features = ["test-support"] }
agent_ui = { workspace = true, features = ["test-support"] }
-assistant_text_thread = { workspace = true, features = ["test-support"] }
editor.workspace = true
language_model = { workspace = true, features = ["test-support"] }
pretty_assertions.workspace = true
@@ -5,7 +5,6 @@ use agent_ui::{
test_support::{active_session_id, open_thread_with_connection, send_message},
thread_metadata_store::ThreadMetadata,
};
-use assistant_text_thread::TextThreadStore;
use chrono::DateTime;
use feature_flags::FeatureFlagAppExt as _;
use fs::FakeFs;
@@ -1185,12 +1184,10 @@ async fn init_test_project_with_agent_panel(
fn add_agent_panel(
workspace: &Entity<Workspace>,
- project: &Entity<project::Project>,
cx: &mut gpui::VisualTestContext,
) -> Entity<AgentPanel> {
workspace.update_in(cx, |workspace, window, cx| {
- let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
- let panel = cx.new(|cx| AgentPanel::test_new(workspace, text_thread_store, window, cx));
+ let panel = cx.new(|cx| AgentPanel::test_new(workspace, window, cx));
workspace.add_panel(panel.clone(), window, cx);
panel
})
@@ -1198,12 +1195,11 @@ fn add_agent_panel(
fn setup_sidebar_with_agent_panel(
multi_workspace: &Entity<MultiWorkspace>,
- project: &Entity<project::Project>,
cx: &mut gpui::VisualTestContext,
) -> (Entity<Sidebar>, Entity<AgentPanel>) {
let sidebar = setup_sidebar(multi_workspace, cx);
let workspace = multi_workspace.read_with(cx, |mw, _cx| mw.workspace().clone());
- let panel = add_agent_panel(&workspace, project, cx);
+ let panel = add_agent_panel(&workspace, cx);
(sidebar, panel)
}
@@ -1212,7 +1208,7 @@ async fn test_parallel_threads_shown_with_live_status(cx: &mut TestAppContext) {
let project = init_test_project_with_agent_panel("/my-project", cx).await;
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
- let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, &project, cx);
+ let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, cx);
let path_list = PathList::new(&[std::path::PathBuf::from("/my-project")]);
@@ -1258,7 +1254,7 @@ async fn test_background_thread_completion_triggers_notification(cx: &mut TestAp
let project_a = init_test_project_with_agent_panel("/project-a", cx).await;
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
- let (sidebar, panel_a) = setup_sidebar_with_agent_panel(&multi_workspace, &project_a, cx);
+ let (sidebar, panel_a) = setup_sidebar_with_agent_panel(&multi_workspace, cx);
let path_list_a = PathList::new(&[std::path::PathBuf::from("/project-a")]);
@@ -1924,7 +1920,7 @@ async fn test_thread_title_update_propagates_to_sidebar(cx: &mut TestAppContext)
let project = init_test_project_with_agent_panel("/my-project", cx).await;
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
- let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, &project, cx);
+ let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, cx);
let path_list = PathList::new(&[std::path::PathBuf::from("/my-project")]);
@@ -1972,7 +1968,7 @@ async fn test_focused_thread_tracks_user_intent(cx: &mut TestAppContext) {
let project_a = init_test_project_with_agent_panel("/project-a", cx).await;
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
- let (sidebar, panel_a) = setup_sidebar_with_agent_panel(&multi_workspace, &project_a, cx);
+ let (sidebar, panel_a) = setup_sidebar_with_agent_panel(&multi_workspace, cx);
let path_list_a = PathList::new(&[std::path::PathBuf::from("/project-a")]);
@@ -1995,7 +1991,7 @@ async fn test_focused_thread_tracks_user_intent(cx: &mut TestAppContext) {
let workspace_b = multi_workspace.update_in(cx, |mw, window, cx| {
mw.test_add_workspace(project_b.clone(), window, cx)
});
- let panel_b = add_agent_panel(&workspace_b, &project_b, cx);
+ let panel_b = add_agent_panel(&workspace_b, cx);
cx.run_until_parked();
let workspace_a = multi_workspace.read_with(cx, |mw, _cx| mw.workspaces()[0].clone());
@@ -2199,7 +2195,7 @@ async fn test_new_thread_button_works_after_adding_folder(cx: &mut TestAppContex
let fs = cx.update(|cx| <dyn fs::Fs>::global(cx));
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
- let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, &project, cx);
+ let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, cx);
let path_list_a = PathList::new(&[std::path::PathBuf::from("/project-a")]);
@@ -2290,7 +2286,7 @@ async fn test_cmd_n_shows_new_thread_entry(cx: &mut TestAppContext) {
let project = init_test_project_with_agent_panel("/my-project", cx).await;
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
- let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, &project, cx);
+ let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, cx);
let path_list = PathList::new(&[std::path::PathBuf::from("/my-project")]);
@@ -2341,7 +2337,7 @@ async fn test_draft_with_server_session_shows_as_draft(cx: &mut TestAppContext)
let project = init_test_project_with_agent_panel("/my-project", cx).await;
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
- let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, &project, cx);
+ let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, cx);
let path_list = PathList::new(&[std::path::PathBuf::from("/my-project")]);
@@ -2455,7 +2451,7 @@ async fn test_cmd_n_shows_new_thread_entry_in_absorbed_worktree(cx: &mut TestApp
mw.test_add_workspace(worktree_project.clone(), window, cx)
});
- let worktree_panel = add_agent_panel(&worktree_workspace, &worktree_project, cx);
+ let worktree_panel = add_agent_panel(&worktree_workspace, cx);
// Switch to the worktree workspace.
multi_workspace.update_in(cx, |mw, window, cx| {
@@ -3128,7 +3124,7 @@ async fn test_absorbed_worktree_running_thread_shows_live_status(cx: &mut TestAp
// Add an agent panel to the worktree workspace so we can run a
// thread inside it.
- let worktree_panel = add_agent_panel(&worktree_workspace, &worktree_project, cx);
+ let worktree_panel = add_agent_panel(&worktree_workspace, cx);
// Switch back to the main workspace before setting up the sidebar.
multi_workspace.update_in(cx, |mw, window, cx| {
@@ -3240,7 +3236,7 @@ async fn test_absorbed_worktree_completion_triggers_notification(cx: &mut TestAp
mw.test_add_workspace(worktree_project.clone(), window, cx)
});
- let worktree_panel = add_agent_panel(&worktree_workspace, &worktree_project, cx);
+ let worktree_panel = add_agent_panel(&worktree_workspace, cx);
multi_workspace.update_in(cx, |mw, window, cx| {
let workspace = mw.workspaces()[0].clone();
@@ -3998,7 +3994,7 @@ async fn test_activate_archived_thread_reuses_workspace_in_another_window_with_t
let cx_b = &mut gpui::VisualTestContext::from_window(multi_workspace_b.into(), cx);
let sidebar_b = setup_sidebar(&multi_workspace_b_entity, cx_b);
let workspace_b = multi_workspace_b_entity.read_with(cx_b, |mw, _| mw.workspace().clone());
- let _panel_b = add_agent_panel(&workspace_b, &project_b, cx_b);
+ let _panel_b = add_agent_panel(&workspace_b, cx_b);
let session_id = acp::SessionId::new(Arc::from("archived-cross-window-with-sidebar"));
@@ -4204,8 +4200,8 @@ async fn test_archive_thread_uses_next_threads_own_workspace(cx: &mut TestAppCon
let sidebar = setup_sidebar(&multi_workspace, cx);
let main_workspace = multi_workspace.read_with(cx, |mw, _| mw.workspaces()[0].clone());
- let main_panel = add_agent_panel(&main_workspace, &main_project, cx);
- let _worktree_panel = add_agent_panel(&worktree_workspace, &worktree_project, cx);
+ let main_panel = add_agent_panel(&main_workspace, cx);
+ let _worktree_panel = add_agent_panel(&worktree_workspace, cx);
// Open Thread 2 in the main panel and keep it running.
let connection = StubAgentConnection::new();
@@ -4395,7 +4391,7 @@ async fn test_thread_switcher_ordering(cx: &mut TestAppContext) {
let project = init_test_project_with_agent_panel("/my-project", cx).await;
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
- let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, &project, cx);
+ let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, cx);
let path_list = PathList::new(&[std::path::PathBuf::from("/my-project")]);
@@ -5070,7 +5066,7 @@ mod property_test {
let workspace = multi_workspace.update_in(cx, |mw, window, cx| {
mw.test_add_workspace(project.clone(), window, cx)
});
- add_agent_panel(&workspace, &project, cx);
+ add_agent_panel(&workspace, cx);
let new_index = state.workspace_paths.len();
state.workspace_paths.push(path);
state.main_repo_indices.push(new_index);
@@ -5087,7 +5083,7 @@ mod property_test {
let workspace = multi_workspace.update_in(cx, |mw, window, cx| {
mw.test_add_workspace(project.clone(), window, cx)
});
- add_agent_panel(&workspace, &project, cx);
+ add_agent_panel(&workspace, cx);
state.workspace_paths.push(worktree.path);
}
Operation::RemoveWorkspace { index } => {
@@ -5375,7 +5371,7 @@ mod property_test {
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
- let (sidebar, _panel) = setup_sidebar_with_agent_panel(&multi_workspace, &project, cx);
+ let (sidebar, _panel) = setup_sidebar_with_agent_panel(&multi_workspace, cx);
let mut state = TestState::new(fs, "/my-project".to_string());
let mut executed: Vec<String> = Vec::new();
@@ -18,7 +18,6 @@ doctest = false
[dependencies]
anyhow.workspace = true
async-recursion.workspace = true
-assistant_slash_command.workspace = true
breadcrumbs.workspace = true
collections.workspace = true
db.workspace = true
@@ -1,129 +0,0 @@
-use std::sync::Arc;
-use std::sync::atomic::AtomicBool;
-
-use crate::{TerminalView, terminal_panel::TerminalPanel};
-use anyhow::Result;
-use assistant_slash_command::{
- ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
- SlashCommandResult,
-};
-use gpui::{App, Entity, Task, WeakEntity};
-use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate};
-use ui::prelude::*;
-use workspace::{Workspace, dock::Panel};
-
-use assistant_slash_command::create_label_for_command;
-
-pub struct TerminalSlashCommand;
-
-const LINE_COUNT_ARG: &str = "--line-count";
-
-const DEFAULT_CONTEXT_LINES: usize = 50;
-
-impl SlashCommand for TerminalSlashCommand {
- fn name(&self) -> String {
- "terminal".into()
- }
-
- fn label(&self, cx: &App) -> CodeLabel {
- create_label_for_command("terminal", &[LINE_COUNT_ARG], cx)
- }
-
- fn description(&self) -> String {
- "Insert terminal output".into()
- }
-
- fn icon(&self) -> IconName {
- IconName::Terminal
- }
-
- fn menu_text(&self) -> String {
- self.description()
- }
-
- fn requires_argument(&self) -> bool {
- false
- }
-
- fn accepts_arguments(&self) -> bool {
- true
- }
-
- fn complete_argument(
- self: Arc<Self>,
- _arguments: &[String],
- _cancel: Arc<AtomicBool>,
- _workspace: Option<WeakEntity<Workspace>>,
- _window: &mut Window,
- _cx: &mut App,
- ) -> Task<Result<Vec<ArgumentCompletion>>> {
- Task::ready(Ok(Vec::new()))
- }
-
- fn run(
- self: Arc<Self>,
- arguments: &[String],
- _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
- _context_buffer: BufferSnapshot,
- workspace: WeakEntity<Workspace>,
- _delegate: Option<Arc<dyn LspAdapterDelegate>>,
- _: &mut Window,
- cx: &mut App,
- ) -> Task<SlashCommandResult> {
- let Some(workspace) = workspace.upgrade() else {
- return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));
- };
-
- let Some(active_terminal) = resolve_active_terminal(&workspace, cx) else {
- return Task::ready(Err(anyhow::anyhow!("no active terminal")));
- };
-
- let line_count = arguments
- .get(0)
- .and_then(|s| s.parse::<usize>().ok())
- .unwrap_or(DEFAULT_CONTEXT_LINES);
-
- let lines = active_terminal
- .read(cx)
- .entity()
- .read(cx)
- .last_n_non_empty_lines(line_count);
-
- let mut text = String::new();
- text.push_str("Terminal output:\n");
- text.push_str(&lines.join("\n"));
- let range = 0..text.len();
-
- Task::ready(Ok(SlashCommandOutput {
- text,
- sections: vec![SlashCommandOutputSection {
- range,
- icon: IconName::Terminal,
- label: "Terminal".into(),
- metadata: None,
- }],
- run_commands_in_text: false,
- }
- .into_event_stream()))
- }
-}
-
-fn resolve_active_terminal(
- workspace: &Entity<Workspace>,
- cx: &mut App,
-) -> Option<Entity<TerminalView>> {
- if let Some(terminal_view) = workspace
- .read(cx)
- .active_item(cx)
- .and_then(|item| item.act_as::<TerminalView>(cx))
- {
- return Some(terminal_view);
- }
-
- let terminal_panel = workspace.read(cx).panel::<TerminalPanel>(cx)?;
- terminal_panel.read(cx).pane().and_then(|pane| {
- pane.read(cx)
- .active_item()
- .and_then(|t| t.downcast::<TerminalView>())
- })
-}
@@ -3,9 +3,7 @@ pub mod terminal_element;
pub mod terminal_panel;
mod terminal_path_like_target;
pub mod terminal_scrollbar;
-mod terminal_slash_command;
-use assistant_slash_command::SlashCommandRegistry;
use editor::{
Editor, EditorSettings, actions::SelectAll, blink_manager::BlinkManager,
ui_scrollbar_settings_from_raw,
@@ -47,7 +45,6 @@ use terminal_element::TerminalElement;
use terminal_panel::TerminalPanel;
use terminal_path_like_target::{hover_path_like_target, open_path_like_target};
use terminal_scrollbar::TerminalScrollHandle;
-use terminal_slash_command::TerminalSlashCommand;
use ui::{
ContextMenu, Divider, ScrollAxes, Scrollbars, Tooltip, WithScrollbar,
prelude::*,
@@ -101,7 +98,6 @@ actions!(
pub struct RenameTerminal;
pub fn init(cx: &mut App) {
- assistant_slash_command::init(cx);
terminal_panel::init(cx);
register_serializable_item::<TerminalView>(cx);
@@ -110,7 +106,6 @@ pub fn init(cx: &mut App) {
workspace.register_action(TerminalView::deploy);
})
.detach();
- SlashCommandRegistry::global(cx).register_command(TerminalSlashCommand, true);
}
pub struct BlockProperties {
@@ -682,8 +682,7 @@ fn main() {
);
agent_ui::init(
app_state.fs.clone(),
- app_state.client.clone(),
- prompt_builder.clone(),
+ prompt_builder,
app_state.languages.clone(),
is_new_install,
false,
@@ -819,7 +818,7 @@ fn main() {
let menus = app_menus(cx);
cx.set_menus(menus);
- initialize_workspace(app_state.clone(), prompt_builder, cx);
+ initialize_workspace(app_state.clone(), cx);
cx.activate(true);
@@ -211,7 +211,6 @@ fn run_visual_tests(project_path: PathBuf, update_baseline: bool) -> Result<()>
);
agent_ui::init(
app_state.fs.clone(),
- app_state.client.clone(),
prompt_builder,
app_state.languages.clone(),
true,
@@ -2134,16 +2133,10 @@ fn run_agent_thread_view_test(
})
.context("Failed to get workspace handle")?;
- let prompt_builder =
- cx.update(|cx| prompt_store::PromptBuilder::load(app_state.fs.clone(), false, cx));
cx.background_executor.allow_parking();
let panel = cx
.foreground_executor
- .block_test(AgentPanel::load(
- weak_workspace,
- prompt_builder,
- async_window_cx,
- ))
+ .block_test(AgentPanel::load(weak_workspace, async_window_cx))
.context("Failed to load AgentPanel")?;
cx.background_executor.forbid_parking();
@@ -3296,52 +3289,40 @@ edition = "2021"
})
.context("Failed to get workspace handle for agent panel")?;
- let prompt_builder =
- cx.update(|cx| prompt_store::PromptBuilder::load(app_state.fs.clone(), false, cx));
-
// Register an observer so that workspaces created by the worktree creation
// flow get AgentPanel and ProjectPanel loaded automatically. Without this,
// `workspace.panel::<AgentPanel>(cx)` returns None in the new workspace and
// the creation flow's `focus_panel::<AgentPanel>` call is a no-op.
- let _workspace_observer = cx.update({
- let prompt_builder = prompt_builder.clone();
- |cx| {
- cx.observe_new(move |workspace: &mut Workspace, window, cx| {
- let Some(window) = window else { return };
- let prompt_builder = prompt_builder.clone();
- let panels_task = cx.spawn_in(window, async move |workspace_handle, cx| {
- let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
- let agent_panel =
- AgentPanel::load(workspace_handle.clone(), prompt_builder, cx.clone());
- if let Ok(panel) = project_panel.await {
- workspace_handle
- .update_in(cx, |workspace, window, cx| {
- workspace.add_panel(panel, window, cx);
- })
- .log_err();
- }
- if let Ok(panel) = agent_panel.await {
- workspace_handle
- .update_in(cx, |workspace, window, cx| {
- workspace.add_panel(panel, window, cx);
- })
- .log_err();
- }
- anyhow::Ok(())
- });
- workspace.set_panels_task(panels_task);
- })
- }
+ let _workspace_observer = cx.update(|cx| {
+ cx.observe_new(move |workspace: &mut Workspace, window, cx| {
+ let Some(window) = window else { return };
+ let panels_task = cx.spawn_in(window, async move |workspace_handle, cx| {
+ let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
+ let agent_panel = AgentPanel::load(workspace_handle.clone(), cx.clone());
+ if let Ok(panel) = project_panel.await {
+ workspace_handle
+ .update_in(cx, |workspace, window, cx| {
+ workspace.add_panel(panel, window, cx);
+ })
+ .log_err();
+ }
+ if let Ok(panel) = agent_panel.await {
+ workspace_handle
+ .update_in(cx, |workspace, window, cx| {
+ workspace.add_panel(panel, window, cx);
+ })
+ .log_err();
+ }
+ anyhow::Ok(())
+ });
+ workspace.set_panels_task(panels_task);
+ })
});
cx.background_executor.allow_parking();
let panel = cx
.foreground_executor
- .block_test(AgentPanel::load(
- weak_workspace,
- prompt_builder,
- async_window_cx,
- ))
+ .block_test(AgentPanel::load(weak_workspace, async_window_cx))
.context("Failed to load AgentPanel")?;
cx.background_executor.forbid_parking();
@@ -13,7 +13,7 @@ pub mod visual_tests;
#[cfg(target_os = "windows")]
pub(crate) mod windows_only_instance;
-use agent_ui::{AgentDiffToolbar, AgentPanelDelegate};
+use agent_ui::AgentDiffToolbar;
use anyhow::Context as _;
pub use app_menus::*;
use assets::Assets;
@@ -56,7 +56,6 @@ use paths::{
};
use project::{DirectoryLister, DisableAiSettings, ProjectItem};
use project_panel::ProjectPanel;
-use prompt_store::PromptBuilder;
use quick_action_bar::QuickActionBar;
use recent_projects::open_remote_project;
use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
@@ -355,11 +354,7 @@ pub fn build_window_options(display_uuid: Option<Uuid>, cx: &mut App) -> WindowO
}
}
-pub fn initialize_workspace(
- app_state: Arc<AppState>,
- prompt_builder: Arc<PromptBuilder>,
- cx: &mut App,
-) {
+pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut App) {
let mut _on_close_subscription = bind_on_window_closed(cx);
cx.observe_global::<SettingsStore>(move |cx| {
// A 1.92 regression causes unused-assignment to trigger on this variable.
@@ -524,7 +519,7 @@ pub fn initialize_workspace(
status_bar.add_right_item(image_info, window, cx);
});
- let panels_task = initialize_panels(prompt_builder.clone(), window, cx);
+ let panels_task = initialize_panels(window, cx);
workspace.set_panels_task(panels_task);
register_actions(app_state.clone(), workspace, window, cx);
@@ -647,11 +642,7 @@ fn show_software_emulation_warning_if_needed(
}
}
-fn initialize_panels(
- prompt_builder: Arc<PromptBuilder>,
- window: &mut Window,
- cx: &mut Context<Workspace>,
-) -> Task<anyhow::Result<()>> {
+fn initialize_panels(window: &mut Window, cx: &mut Context<Workspace>) -> Task<anyhow::Result<()>> {
cx.spawn_in(window, async move |workspace_handle, cx| {
let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
let outline_panel = OutlinePanel::load(workspace_handle.clone(), cx.clone());
@@ -688,7 +679,7 @@ fn initialize_panels(
add_panel_when_ready(channels_panel, workspace_handle.clone(), cx.clone()),
add_panel_when_ready(notification_panel, workspace_handle.clone(), cx.clone()),
add_panel_when_ready(debug_panel, workspace_handle.clone(), cx.clone()),
- initialize_agent_panel(workspace_handle, prompt_builder, cx.clone()).map(|r| r.log_err()),
+ initialize_agent_panel(workspace_handle, cx.clone()).map(|r| r.log_err()),
);
anyhow::Ok(())
@@ -733,24 +724,20 @@ fn setup_or_teardown_ai_panel<P: Panel>(
async fn initialize_agent_panel(
workspace_handle: WeakEntity<Workspace>,
- prompt_builder: Arc<PromptBuilder>,
mut cx: AsyncWindowContext,
) -> anyhow::Result<()> {
workspace_handle
.update_in(&mut cx, |workspace, window, cx| {
- let prompt_builder = prompt_builder.clone();
setup_or_teardown_ai_panel(workspace, window, cx, move |workspace, cx| {
- agent_ui::AgentPanel::load(workspace, prompt_builder, cx)
+ agent_ui::AgentPanel::load(workspace, cx)
})
})?
.await?;
workspace_handle.update_in(&mut cx, |workspace, window, cx| {
- let prompt_builder = prompt_builder.clone();
cx.observe_global_in::<SettingsStore>(window, move |workspace, window, cx| {
- let prompt_builder = prompt_builder.clone();
setup_or_teardown_ai_panel(workspace, window, cx, move |workspace, cx| {
- agent_ui::AgentPanel::load(workspace, prompt_builder, cx)
+ agent_ui::AgentPanel::load(workspace, cx)
})
.detach_and_log_err(cx);
})
@@ -763,11 +750,6 @@ async fn initialize_agent_panel(
//
// Once we ship `assistant2` we can push this back down into `agent::agent_panel::init`.
if !cfg!(test) {
- <dyn AgentPanelDelegate>::set_global(
- Arc::new(agent_ui::ConcreteAssistantPanelDelegate),
- cx,
- );
-
workspace
.register_action(agent_ui::AgentPanel::toggle_focus)
.register_action(agent_ui::AgentPanel::toggle)
@@ -2269,6 +2251,7 @@ mod tests {
use languages::{markdown_lang, rust_lang};
use pretty_assertions::{assert_eq, assert_ne};
use project::{Project, ProjectPath};
+ use prompt_store::PromptBuilder;
use semver::Version;
use serde_json::json;
use settings::{SaturatingBool, SettingsStore, watch_config_file};
@@ -5045,8 +5028,7 @@ mod tests {
);
agent_ui::init(
app_state.fs.clone(),
- app_state.client.clone(),
- prompt_builder.clone(),
+ prompt_builder,
app_state.languages.clone(),
true,
false,
@@ -5061,7 +5043,7 @@ mod tests {
);
project::debugger::dap_store::DapStore::init(&app_state.client.clone().into(), cx);
debugger_ui::init(cx);
- initialize_workspace(app_state.clone(), prompt_builder, cx);
+ initialize_workspace(app_state.clone(), cx);
search::init(cx);
cx.set_global(workspace::PaneSearchBarCallbacks {
setup_search_bar: |languages, toolbar, window, cx| {
@@ -85,8 +85,6 @@ pub enum ExtensionCategoryFilter {
LanguageServers,
ContextServers,
AgentServers,
- SlashCommands,
- IndexedDocsProviders,
Snippets,
DebugAdapters,
}
@@ -528,14 +526,6 @@ pub mod assistant {
]
);
- actions!(
- assistant,
- [
- /// Shows the assistant configuration panel.
- ShowConfiguration
- ]
- );
-
/// Opens the rules library for managing agent rules and prompts.
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
#[action(namespace = agent, deprecated_aliases = ["assistant::OpenRulesLibrary", "assistant::DeployPromptLibrary"])]
@@ -17,7 +17,6 @@
- [External Agents](./ai/external-agents.md)
- [Inline Assistant](./ai/inline-assistant.md)
- [Edit Prediction](./ai/edit-prediction.md)
-- [Text Threads](./ai/text-threads.md)
- [Rules](./ai/rules.md)
- [Model Context Protocol](./ai/mcp.md)
- [Configuration](./ai/configuration.md)
@@ -163,7 +162,6 @@
- [Theme Extensions](./extensions/themes.md)
- [Icon Theme Extensions](./extensions/icon-themes.md)
- [Snippets Extensions](./extensions/snippets.md)
-- [Slash Command Extensions](./extensions/slash-commands.md)
- [Agent Server Extensions](./extensions/agent-servers.md)
- [MCP Server Extensions](./extensions/mcp-extensions.md)
@@ -34,7 +34,7 @@ The sections below cover what you can do from here.
By default, the Agent Panel uses Zed's first-party agent.
-To choose another agent, go to the plus button in the top-right of the Agent Panel and pick either one of the [external agents](./external-agents.md) installed out of the box or a new [Text Thread](./text-threads.md).
+To choose another agent, go to the plus button in the top-right of the Agent Panel and pick one of the [external agents](./external-agents.md) installed out of the box.
### Editing Messages {#editing-messages}
@@ -222,15 +222,6 @@ All [Zed's hosted models](./models.md) support tool calling out-of-the-box.
Similarly to the built-in tools, some models may not support all tools included in a given MCP Server.
Zed's UI will inform you about this via a warning icon that appears close to the model selector.
-## Text Threads {#text-threads}
-
-["Text Threads"](./text-threads.md) present your conversation with the LLM in a different formatβas raw text.
-With text threads, you have full control over the conversation data.
-You can remove and edit responses from the LLM, swap roles, and include more context earlier in the conversation.
-
-Text threads are Zed's original assistant panel format, preserved for users who want direct control over conversation data.
-Autonomous code editing (where the agent writes to files) is only available in the default thread format, not text threads.
-
## Errors and Debugging {#errors-and-debugging}
If you hit an error or unusual LLM behavior, open the thread as Markdown with `agent: open thread as markdown` and attach it to your GitHub issue.
@@ -140,19 +140,6 @@ Specify a custom temperature for a provider and/or model:
Note that some of these settings are also surfaced in the Agent Panel's settings UI, which you can access either via the `agent: open settings` action or by the dropdown menu on the top-right corner of the panel.
-### Default View
-
-Use the `default_view` setting to change the default view of the Agent Panel.
-You can choose between `thread` (the default) and `text_thread`:
-
-```json [settings]
-{
- "agent": {
- "default_view": "text_thread"
- }
-}
-```
-
### Font Size
Use the `agent_ui_font_size` setting to change the font size of rendered agent responses in the panel.
@@ -12,7 +12,6 @@ AI features in Zed include:
- [Agent Panel](./agent-panel.md)
- [Edit Predictions](./edit-prediction.md)
- [Inline Assist](./inline-assistant.md)
-- [Text Threads](./text-threads.md)
- Auto Git Commit Message Generation
By default, Zed does not store your prompts or code context. This data is sent to your selected AI provider (e.g., Anthropic, OpenAI, Google, or xAI) to generate responses, then discarded. Zed will not use your data to evaluate or improve AI features unless you explicitly share it (see [AI Feedback with Ratings](#ai-feedback-with-ratings)) or you opt in to edit prediction training data collection (see [Edit Predictions](#edit-predictions)).
@@ -7,7 +7,7 @@ description: Transform code inline with AI in Zed. Send selections to any LLM fo
## Usage Overview
-Use {#kb assistant::InlineAssist} to open the Inline Assistant in editors, text threads, the rules library, channel notes, and the terminal panel.
+Use {#kb assistant::InlineAssist} to open the Inline Assistant in editors, the rules library, channel notes, and the terminal panel.
The Inline Assistant sends your current selection (or line) to a language model and replaces it with the response.
@@ -5,7 +5,7 @@ description: Bring your own API keys to Zed. Set up Anthropic, OpenAI, Google AI
# LLM Providers
-To use AI in Zed, you need to have at least one large language model provider set up. Once configured, providers are available in the [Agent Panel](./agent-panel.md), [Inline Assistant](./inline-assistant.md), and [Text Threads](./text-threads.md).
+To use AI in Zed, you need to have at least one large language model provider set up. Once configured, providers are available in the [Agent Panel](./agent-panel.md) and [Inline Assistant](./inline-assistant.md).
You can do that by either subscribing to [one of Zed's plans](./plans-and-usage.md), or by using API keys you already have for the supported providers. For general AI setup, see [Configuration](./configuration.md).
@@ -96,7 +96,7 @@ A context window is the maximum span of text and code an LLM can consider at onc
> Context window limits for hosted Gemini 3.1 Pro/3 Pro/Flash may increase in future releases.
-Each Agent thread and text thread in Zed maintains its own context window.
+Each Agent thread in Zed maintains its own context window.
The more prompts, attached files, and responses included in a session, the larger the context window grows.
Start a new thread for each distinct task to keep context focused.
@@ -30,10 +30,6 @@ The [Inline Assistant](./inline-assistant.md) works differently: select code or
The default provider is Zeta, Zed's open-source model trained on open data. You can also use GitHub Copilot, or Codestral.
-## Text threads
-
-[Text Threads](./text-threads.md) are conversations with models inside any buffer. They work like a regular editor with your keybindings, multiple cursors, and standard editing features. Content is organized into message blocks with roles (You, Assistant, System).
-
## Getting started
- [Configuration](./configuration.md): Connect to Anthropic, OpenAI, Ollama, Google AI, or other LLM providers.
@@ -74,11 +74,4 @@ The new rules system replaces the Prompt Library except in a few specific cases,
### Slash Commands in Rules
Previously, it was possible to use slash commands (now @-mentions) in custom prompts (now rules).
-There is currently no support for using @-mentions in rules files, however, slash commands are supported in rules files when used with text threads.
-See the documentation for using [slash commands in rules](./text-threads.md#slash-commands-in-rules) for more information.
-
-### Prompt templates
-
-Zed maintains backwards compatibility with its original template system, which allows you to customize prompts used throughout the application, including the inline assistant.
-While the Rules Library is now the primary way to manage prompts, you can still use these legacy templates to override default prompts.
-For more details, see the [Rules Templates](./text-threads.md#rule-templates) section under [Text Threads](./text-threads.md).
+There is currently no support for using @-mentions in rules files.
@@ -1,261 +1,13 @@
---
-title: AI Chat in Your Editor - Zed Text Threads
-description: Chat with LLMs directly in your editor with Zed's text threads. Full control over context, message roles, and slash commands.
+title: Text Threads (Removed)
+description: Text threads have been removed from Zed. Use the Agent Panel for all AI conversations.
+redirect_to: ./agent-panel.md
---
# Text Threads
-Text threads in the [Agent Panel](./agent-panel.md) work like a regular editor.
-You can use custom keybindings, multiple cursors, and all the standard editing features while chatting.
+Text threads have been removed from Zed.
-## Text Threads vs. Threads
+All AI conversations now happen through the [Agent Panel](./agent-panel.md), which supports agentic workflows including tool calls, file editing, terminal access, and [external agents](./external-agents.md).
-Text Threads were Zed's original AI interface.
-In May 2025, Zed introduced the current [Agent Panel](./agent-panel.md), designed for agentic workflows.
-
-The key difference: text threads don't support tool calls and many other more modern agentic features.
-They can't autonomously read files, write code, or run commands on your behalf.
-Text Threads are for simpler conversational interactions where you send text and receive text responses back.
-
-Therefore, [MCP servers](./mcp.md) and [external agents](./external-agents.md) are also not available in Text Threads.
-
-## Usage Overview
-
-Text threads organize content into message blocks with roles:
-
-- `You`
-- `Assistant`
-- `System`
-
-To begin, type your message in a `You` block.
-As you type, the remaining token count for the selected model updates automatically.
-
-To add context from an editor, highlight text and run `agent: add selection to thread` ({#kb agent::AddSelectionToThread}).
-If the selection is code, Zed will wrap it in a fenced code block.
-
-To submit a message, use {#kb assistant::Assist} (`assistant: assist`).
-In text threads, {#kb editor::Newline} inserts a new line instead of submitting, which preserves standard editor behavior.
-
-After you submit a message, the response is streamed below in an `Assistant` message block.
-You can cancel the stream at any point with <kbd>escape</kbd>, or start a new conversation at any time via <kbd>cmd-n|ctrl-n</kbd>.
-
-Text threads support straightforward conversations, but you can also go back and edit earlier messagesβincluding previous LLM responsesβto change direction, refine context, or correct mistakes without starting a new thread or spending tokens on follow-up corrections.
-If you want to remove a message block entirely, place your cursor at the beginning of the block and use the `delete` key.
-
-A typical workflow might involve making edits and adjustments throughout the context to refine your inquiry or provide additional information.
-Here's an example:
-
-1. Write text in a `You` block.
-2. Submit the message with {#kb assistant::Assist}.
-3. Receive an `Assistant` response that doesn't meet your expectations.
-4. Cancel the response with <kbd>escape</kbd>.
-5. Erase the content of the `Assistant` message block and remove the block entirely.
-6. Add additional context to your original message.
-7. Submit the message with {#kb assistant::Assist}.
-
-You can also cycle the role of a message block by clicking on the role, which is useful when you receive a response in an `Assistant` block that you want to edit and send back up as a `You` block.
-
-## Commands Overview {#commands}
-
-Type `/` at the beginning of a line to see available slash commands:
-
-- `/default`: Inserts the default rule
-- `/diagnostics`: Injects errors reported by the project's language server
-- `/fetch`: Fetches the content of a webpage and inserts it
-- `/file`: Inserts a single file or a directory of files
-- `/now`: Inserts the current date and time
-- `/prompt`: Adds a custom-configured prompt to the context ([see Rules Library](./rules.md#rules-library))
-- `/symbols`: Inserts the current tab's active symbols
-- `/tab`: Inserts the content of the active tab or all open tabs
-- `/terminal`: Inserts a select number of lines of output from the terminal
-- `/selection`: Inserts the selected text
-
-> **Note:** Remember, commands are only evaluated when the text thread is created or when the command is inserted, so a command like `/now` won't continuously update, or `/file` commands won't keep their contents up to date.
-
-### `/default`
-
-Read more about `/default` in the [Rules: Editing the Default Rules](./rules.md#default-rules) section.
-
-Usage: `/default`
-
-### `/diagnostics`
-
-Injects errors reported by the project's language server into the context.
-
-Usage: `/diagnostics [--include-warnings] [path]`
-
-- `--include-warnings`: Optional flag to include warnings in addition to errors.
-- `path`: Optional path to limit diagnostics to a specific file or directory.
-
-### `/file`
-
-Inserts the content of a file or directory into the context. Supports glob patterns.
-
-Usage: `/file <path>`
-
-Examples:
-
-- `/file src/index.js` - Inserts the content of `src/index.js` into the context.
-- `/file src/*.js` - Inserts the content of all `.js` files in the `src` directory.
-- `/file src` - Inserts the content of all files in the `src` directory.
-
-### `/now`
-
-Inserts the current date and time. Useful for informing the model about its knowledge cutoff relative to now.
-
-Usage: `/now`
-
-### `/prompt`
-
-Inserts a rule from the Rules Library into the context. Rules can nest other rules.
-
-Usage: `/prompt <prompt_name>`
-
-Related: `/default`
-
-### `/symbols`
-
-Inserts the active symbols (functions, classes, etc.) from the current tab, providing a structural overview of the file.
-
-Usage: `/symbols`
-
-### `/tab`
-
-Inserts the content of the active tab or all open tabs.
-
-Usage: `/tab [tab_name|all]`
-
-- `tab_name`: Optional name of a specific tab to insert.
-- `all`: Insert content from all open tabs.
-
-Examples:
-
-- `/tab` - Inserts the content of the active tab.
-- `/tab "index.js"` - Inserts the content of the tab named "index.js".
-- `/tab all` - Inserts the content of all open tabs.
-
-### `/terminal`
-
-Inserts recent terminal output (default: 50 lines).
-
-Usage: `/terminal [<number>]`
-
-- `<number>`: Optional parameter to specify the number of lines to insert (default is 50).
-
-### `/selection`
-
-Inserts the currently selected text. Equivalent to `agent: add selection to thread` ({#kb agent::AddSelectionToThread}).
-
-Usage: `/selection`
-
-## Commands in the Rules Library {#slash-commands-in-rules}
-
-[Commands](#commands) can be used in rules, in the Rules Library (previously known as Prompt Library), to insert dynamic content or perform actions.
-For example, if you want to create a rule where it is important for the model to know the date, you can use the `/now` command to insert the current date.
-
-<div class="warning">
-
-Slash commands in rules **only** work when they are used in text threads. Using them in non-text threads is not supported.
-
-</div>
-
-> **Note:** Slash commands in rules **must** be on their own line.
-
-See the [list of commands](#commands) above for more information on commands, and what slash commands are available.
-
-### Example
-
-```plaintext
-You are an expert Rust engineer. The user has asked you to review their project and answer some questions.
-
-Here is some information about their project:
-
-/file Cargo.toml
-```
-
-In the above example, the `/file` command is used to insert the contents of the `Cargo.toml` file (or all `Cargo.toml` files present in the project) into the rule.
-
-## Nesting Rules
-
-Similar to adding rules to the default rules, you can nest rules within other rules with the `/prompt` command (only supported in Text Threads currently).
-
-You might want to nest rules to:
-
-- Create templates on the fly
-- Break collections like docs or references into smaller, mix-and-matchable parts
-- Create variants of a similar rule (e.g., `Async Rust - Tokio` vs. `Async Rust - Async-std`)
-
-### Example
-
-```plaintext
-Title: Zed-Flavored Rust
-
-## About Zed
-
-/prompt Zed: Zed (a rule about what Zed is)
-
-## Rust - Zed Style
-
-/prompt Rust: Async - Async-std (zed doesn't use tokio)
-/prompt Rust: Zed-style Crates (we have some unique conventions)
-/prompt Rust - Workspace deps (bias towards reusing deps from the workspace)
-```
-
-_The text in parentheses above are comments and are not part of the rule._
-
-> **Note:** You can technically nest a rule within itself, but we don't recommend doing so.
-
-By using nested rules, you can create modular and reusable rule components that can be combined in various ways to suit different scenarios.
-
-> **Note:** When using slash commands to bring in additional context, the injected content can be edited directly inline in the text threadβedits here will not propagate to the saved rules.
-
-## Extensibility
-
-Additional slash commands can be provided by extensions.
-
-See [Extension: Slash Commands](../extensions/slash-commands.md) to learn how to create your own.
-
-## Advanced Concepts
-
-### Rule Templates {#rule-templates}
-
-Zed uses rule templates to power internal assistant features, like the terminal assistant, or the content rules used in the inline assistant.
-
-Zed has the following internal rule templates:
-
-- `content_prompt.hbs`: Used for generating content in the editor.
-- `terminal_assistant_prompt.hbs`: Used for the terminal assistant feature.
-
-At this point it is unknown if we will expand templates further to be user-creatable.
-
-### Overriding Templates
-
-> **Note:** It is not recommended to override templates unless you know what you are doing. Editing templates will break your assistant if done incorrectly.
-
-Zed allows you to override the default rules used for various assistant features by placing custom Handlebars (.hbs) templates in your `~/.config/zed/prompt_overrides` directory.
-
-The following templates can be overridden:
-
-1. [`content_prompt.hbs`](https://github.com/zed-industries/zed/tree/main/assets/prompts/content_prompt.hbs): Used for generating content in the editor.
-
-2. [`terminal_assistant_prompt.hbs`](https://github.com/zed-industries/zed/tree/main/assets/prompts/terminal_assistant_prompt.hbs): Used for the terminal assistant feature.
-
-> **Note:** Be sure you want to override these, as you'll miss out on iteration on our built-in features.
-> This should be primarily used when developing Zed.
-
-You can customize these templates to better suit your needs while maintaining the core structure and variables used by Zed.
-Zed will automatically reload your prompt overrides when they change on disk.
-
-Consult Zed's [assets/prompts](https://github.com/zed-industries/zed/tree/main/assets/prompts) directory for current versions you can play with.
-
-### History {#history}
-
-After you submit your first message in a text thread, a name for your context is generated by the language model, and the context is automatically saved to your file system in
-
-- `~/.config/zed/conversations` (macOS)
-- `~/.local/share/zed/conversations` (Linux)
-- `%LocalAppData%\Zed\conversations` (Windows)
-
-You can access and load previous contexts by clicking on the history button in the top-left corner of the agent panel.
-
-
+See the [Agent Panel documentation](./agent-panel.md) to get started.
@@ -15,6 +15,5 @@ Zed lets you add new functionality using user-defined extensions.
- [Developing Themes](./extensions/themes.md)
- [Developing Icon Themes](./extensions/icon-themes.md)
- [Developing Snippets](./extensions/snippets.md)
- - [Developing Slash Commands](./extensions/slash-commands.md)
- [Developing Agent Servers](./extensions/agent-servers.md)
- [Developing MCP Servers](./extensions/mcp-extensions.md)
@@ -1,11 +1,11 @@
---
title: Developing Extensions
-description: "Create Zed extensions: languages, themes, debuggers, slash commands, and more."
+description: "Create Zed extensions: languages, themes, debuggers, and more."
---
# Developing Extensions {#developing-extensions}
-Zed extensions are Git repositories containing an `extension.toml` manifest. They can provide languages, themes, debuggers, snippets, slash commands, and MCP servers.
+Zed extensions are Git repositories containing an `extension.toml` manifest. They can provide languages, themes, debuggers, snippets, and MCP servers.
## Extension Features {#extension-features}
@@ -16,7 +16,6 @@ Extensions can provide:
- [Themes](./themes.md)
- [Icon Themes](./icon-themes.md)
- [Snippets](./snippets.md)
-- [Slash Commands](./slash-commands.md)
- [MCP Servers](./mcp-extensions.md)
## Developing an Extension Locally
@@ -1,143 +1,11 @@
---
-title: Slash Commands
-description: "Slash Commands for Zed extensions."
+title: Slash Commands (Removed)
+description: Extension slash commands have been removed from Zed.
+redirect_to: ./mcp-extensions.md
---
# Slash Commands
-Extensions may provide slash commands for use in the Assistant.
+Extension-provided slash commands have been removed from Zed.
-## Example extension
-
-To see a working example of an extension that provides slash commands, check out the [`slash-commands-example` extension](https://github.com/zed-industries/zed/tree/main/extensions/slash-commands-example).
-
-This extension can be [installed as a dev extension](./developing-extensions.md#developing-an-extension-locally) if you want to try it out for yourself.
-
-## Defining slash commands
-
-A given extension may provide one or more slash commands. Each slash command must be registered in the `extension.toml`.
-
-For example, here is an extension that provides two slash commands: `/echo` and `/pick-one`:
-
-```toml
-[slash_commands.echo]
-description = "echoes the provided input"
-requires_argument = true
-
-[slash_commands.pick-one]
-description = "pick one of three options"
-requires_argument = true
-```
-
-Each slash command may define the following properties:
-
-- `description`: A description of the slash command that will be shown when completing available commands.
-- `requires_argument`: Indicates whether a slash command requires at least one argument to run.
-
-## Implementing slash command behavior
-
-To implement behavior for your slash commands, implement `run_slash_command` for your extension.
-
-This method accepts the slash command that will be run, the list of arguments passed to it, and an optional `Worktree`.
-
-This method returns `SlashCommandOutput`, which contains the textual output of the command in the `text` field. The output may also define `SlashCommandOutputSection`s that contain ranges into the output. These sections are then rendered as creases in the Assistant's context editor.
-
-Your extension should `match` on the command name (without the leading `/`) and then execute behavior accordingly:
-
-```rs
-impl zed::Extension for MyExtension {
- fn run_slash_command(
- &self,
- command: SlashCommand,
- args: Vec<String>,
- _worktree: Option<&Worktree>,
- ) -> Result<SlashCommandOutput, String> {
- match command.name.as_str() {
- "echo" => {
- if args.is_empty() {
- return Err("nothing to echo".to_string());
- }
-
- let text = args.join(" ");
-
- Ok(SlashCommandOutput {
- sections: vec![SlashCommandOutputSection {
- range: (0..text.len()).into(),
- label: "Echo".to_string(),
- }],
- text,
- })
- }
- "pick-one" => {
- let Some(selection) = args.first() else {
- return Err("no option selected".to_string());
- };
-
- match selection.as_str() {
- "option-1" | "option-2" | "option-3" => {}
- invalid_option => {
- return Err(format!("{invalid_option} is not a valid option"));
- }
- }
-
- let text = format!("You chose {selection}.");
-
- Ok(SlashCommandOutput {
- sections: vec![SlashCommandOutputSection {
- range: (0..text.len()).into(),
- label: format!("Pick One: {selection}"),
- }],
- text,
- })
- }
- command => Err(format!("unknown slash command: \"{command}\"")),
- }
- }
-}
-```
-
-## Auto-completing slash command arguments
-
-For slash commands that have arguments, you may also choose to implement `complete_slash_command_argument` to provide completions for your slash commands.
-
-This method accepts the slash command that will be run and the list of arguments passed to it. It returns a list of `SlashCommandArgumentCompletion`s that will be shown in the completion menu.
-
-A `SlashCommandArgumentCompletion` consists of the following properties:
-
-- `label`: The label that will be shown in the completion menu.
-- `new_text`: The text that will be inserted when the completion is accepted.
-- `run_command`: Whether the slash command will be run when the completion is accepted.
-
-Once again, your extension should `match` on the command name (without the leading `/`) and return the desired argument completions:
-
-```rs
-impl zed::Extension for MyExtension {
- fn complete_slash_command_argument(
- &self,
- command: SlashCommand,
- _args: Vec<String>,
- ) -> Result<Vec<SlashCommandArgumentCompletion>, String> {
- match command.name.as_str() {
- "echo" => Ok(vec![]),
- "pick-one" => Ok(vec![
- SlashCommandArgumentCompletion {
- label: "Option One".to_string(),
- new_text: "option-1".to_string(),
- run_command: true,
- },
- SlashCommandArgumentCompletion {
- label: "Option Two".to_string(),
- new_text: "option-2".to_string(),
- run_command: true,
- },
- SlashCommandArgumentCompletion {
- label: "Option Three".to_string(),
- new_text: "option-3".to_string(),
- run_command: true,
- },
- ]),
- command => Err(format!("unknown slash command: \"{command}\"")),
- }
- }
-}
-```
+To extend the Agent Panel with custom tools and context, use [MCP Servers](./mcp-extensions.md) instead.
@@ -261,7 +261,6 @@ Zed's extension catalog is smaller and more focused:
- Language support and syntax highlighting
- Themes
-- Slash commands for AI
- Context servers
Several features that require plugins in other editors are built into Zed:
@@ -319,7 +319,6 @@ Zed's extension catalog is smaller and more focused:
- Language support and syntax highlighting
- Themes
-- Slash commands for AI
- Context servers
Several features that require plugins in PyCharm are built into Zed:
@@ -315,7 +315,6 @@ Zed's extension catalog is smaller and more focused:
- Language support and syntax highlighting
- Themes
-- Slash commands for AI
- Context servers
Several features that might require plugins in other editors are built into Zed:
@@ -311,7 +311,6 @@ Zed's extension catalog is smaller and more focused:
- Language support and syntax highlighting
- Themes
-- Slash commands for AI
- Context servers
Several features that require plugins in WebStorm are built into Zed:
@@ -392,8 +392,7 @@ TBD: Centered layout related settings
```json [settings]
"edit_predictions": {
- "mode": "eager", // Automatically show (eager) or hold-alt (subtle)
- "enabled_in_text_threads": true // Show/hide predictions in agent text threads
+ "mode": "eager" // Automatically show (eager) or hold-alt (subtle)
},
"show_edit_predictions": true // Show/hide predictions in editor
```