From 76c6004b27e15531785ce83919dbd7ae96f05ec5 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Tue, 31 Mar 2026 17:55:05 +0200 Subject: [PATCH] Remove text thread and slash command crates (#52757) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🫡 Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Release Notes: - Removed legacy Text Threads feature to help streamline the new agentic workflows in Zed. Thanks to all of you who were enthusiastic Text Thread users over the years ❤️! --------- Co-authored-by: Bennet Bo Fenner --- .github/CODEOWNERS.hold | 3 - Cargo.lock | 111 - Cargo.toml | 6 - assets/icons/text_thread.svg | 7 - assets/keymaps/default-linux.json | 35 +- assets/keymaps/default-macos.json | 37 +- assets/keymaps/default-windows.json | 36 +- assets/keymaps/linux/cursor.json | 4 +- assets/keymaps/macos/cursor.json | 4 +- assets/settings/default.json | 5 - crates/acp_thread/src/mention.rs | 21 - crates/agent/src/thread.rs | 3 - crates/agent/src/tool_permissions.rs | 3 +- crates/agent_settings/src/agent_settings.rs | 8 +- crates/agent_ui/Cargo.toml | 6 - crates/agent_ui/src/agent_panel.rs | 1368 ++----- crates/agent_ui/src/agent_ui.rs | 119 +- crates/agent_ui/src/conversation_view.rs | 22 +- .../src/conversation_view/thread_view.rs | 37 +- crates/agent_ui/src/diagnostics.rs | 252 ++ crates/agent_ui/src/inline_assistant.rs | 24 +- crates/agent_ui/src/mention_set.rs | 17 +- crates/agent_ui/src/message_editor.rs | 56 - crates/agent_ui/src/slash_command.rs | 360 -- crates/agent_ui/src/slash_command_picker.rs | 348 -- crates/agent_ui/src/text_thread_editor.rs | 3471 ----------------- crates/agent_ui/src/text_thread_history.rs | 736 ---- .../agent_ui/src/ui/acp_onboarding_modal.rs | 4 +- .../src/ui/claude_agent_onboarding_modal.rs | 4 +- crates/agent_ui/src/ui/mention_crease.rs | 1 - crates/assistant_slash_command/Cargo.toml | 34 - crates/assistant_slash_command/LICENSE-GPL | 1 - .../src/assistant_slash_command.rs | 617 --- .../src/extension_slash_command.rs | 171 - .../src/slash_command_registry.rs | 90 - .../src/slash_command_working_set.rs | 81 - crates/assistant_slash_commands/Cargo.toml | 47 - crates/assistant_slash_commands/LICENSE-GPL | 1 - .../src/assistant_slash_commands.rs | 25 - .../src/default_command.rs | 91 - .../src/delta_command.rs | 124 - .../src/diagnostics_command.rs | 451 --- .../src/fetch_command.rs | 183 - .../src/file_command.rs | 713 ---- .../src/now_command.rs | 71 - .../src/prompt_command.rs | 123 - .../src/selection_command.rs | 357 -- .../src/streaming_example_command.rs | 118 - .../src/symbols_command.rs | 99 - .../src/tab_command.rs | 317 -- crates/assistant_text_thread/Cargo.toml | 62 - crates/assistant_text_thread/LICENSE-GPL | 1 - .../src/assistant_text_thread.rs | 16 - .../src/assistant_text_thread_tests.rs | 1444 ------- .../src/context_server_command.rs | 251 -- .../assistant_text_thread/src/text_thread.rs | 3286 ---------------- .../src/text_thread_store.rs | 1089 ------ crates/collab/Cargo.toml | 5 - crates/collab/src/rpc.rs | 47 - .../tests/integration/integration_tests.rs | 138 - .../collab/tests/integration/test_server.rs | 1 - crates/docs_preprocessor/src/main.rs | 4 +- crates/eval_cli/src/headless.rs | 1 - crates/extension/src/extension_host_proxy.rs | 32 +- crates/extension_host/src/extension_host.rs | 21 +- crates/extensions_ui/src/extensions_ui.rs | 4 - crates/icons/src/icons.rs | 1 - crates/language/src/language_settings.rs | 6 - crates/language_model/Cargo.toml | 1 - crates/language_model/src/language_model.rs | 10 - crates/language_model/src/role.rs | 17 - crates/migrator/src/migrations.rs | 6 + .../src/migrations/m_2026_03_31/settings.rs | 29 + crates/migrator/src/migrator.rs | 109 +- crates/paths/src/paths.rs | 24 - crates/proto/proto/ai.proto | 163 - crates/proto/proto/call.proto | 8 +- crates/proto/proto/zed.proto | 10 +- crates/proto/src/proto.rs | 16 - crates/rules_library/src/rules_library.rs | 10 +- crates/settings/src/vscode_import.rs | 1 - crates/settings_content/src/agent.rs | 12 - crates/settings_content/src/language.rs | 3 - crates/settings_content/src/project.rs | 5 +- .../settings_content/src/settings_content.rs | 16 - crates/settings_ui/src/page_data.rs | 80 +- crates/sidebar/Cargo.toml | 1 - crates/sidebar/src/sidebar_tests.rs | 44 +- crates/terminal_view/Cargo.toml | 1 - .../src/terminal_slash_command.rs | 129 - crates/terminal_view/src/terminal_view.rs | 5 - crates/zed/src/main.rs | 5 +- crates/zed/src/visual_test_runner.rs | 71 +- crates/zed/src/zed.rs | 38 +- crates/zed_actions/src/lib.rs | 10 - docs/src/SUMMARY.md | 2 - docs/src/ai/agent-panel.md | 11 +- docs/src/ai/agent-settings.md | 13 - docs/src/ai/ai-improvement.md | 1 - docs/src/ai/inline-assistant.md | 2 +- docs/src/ai/llm-providers.md | 2 +- docs/src/ai/models.md | 2 +- docs/src/ai/overview.md | 4 - docs/src/ai/rules.md | 9 +- docs/src/ai/text-threads.md | 260 +- docs/src/extensions.md | 1 - docs/src/extensions/developing-extensions.md | 5 +- docs/src/extensions/slash-commands.md | 142 +- docs/src/migrate/intellij.md | 1 - docs/src/migrate/pycharm.md | 1 - docs/src/migrate/rustrover.md | 1 - docs/src/migrate/webstorm.md | 1 - docs/src/visual-customization.md | 3 +- 113 files changed, 834 insertions(+), 17682 deletions(-) delete mode 100644 assets/icons/text_thread.svg create mode 100644 crates/agent_ui/src/diagnostics.rs delete mode 100644 crates/agent_ui/src/slash_command.rs delete mode 100644 crates/agent_ui/src/slash_command_picker.rs delete mode 100644 crates/agent_ui/src/text_thread_editor.rs delete mode 100644 crates/agent_ui/src/text_thread_history.rs delete mode 100644 crates/assistant_slash_command/Cargo.toml delete mode 120000 crates/assistant_slash_command/LICENSE-GPL delete mode 100644 crates/assistant_slash_command/src/assistant_slash_command.rs delete mode 100644 crates/assistant_slash_command/src/extension_slash_command.rs delete mode 100644 crates/assistant_slash_command/src/slash_command_registry.rs delete mode 100644 crates/assistant_slash_command/src/slash_command_working_set.rs delete mode 100644 crates/assistant_slash_commands/Cargo.toml delete mode 120000 crates/assistant_slash_commands/LICENSE-GPL delete mode 100644 crates/assistant_slash_commands/src/assistant_slash_commands.rs delete mode 100644 crates/assistant_slash_commands/src/default_command.rs delete mode 100644 crates/assistant_slash_commands/src/delta_command.rs delete mode 100644 crates/assistant_slash_commands/src/diagnostics_command.rs delete mode 100644 crates/assistant_slash_commands/src/fetch_command.rs delete mode 100644 crates/assistant_slash_commands/src/file_command.rs delete mode 100644 crates/assistant_slash_commands/src/now_command.rs delete mode 100644 crates/assistant_slash_commands/src/prompt_command.rs delete mode 100644 crates/assistant_slash_commands/src/selection_command.rs delete mode 100644 crates/assistant_slash_commands/src/streaming_example_command.rs delete mode 100644 crates/assistant_slash_commands/src/symbols_command.rs delete mode 100644 crates/assistant_slash_commands/src/tab_command.rs delete mode 100644 crates/assistant_text_thread/Cargo.toml delete mode 120000 crates/assistant_text_thread/LICENSE-GPL delete mode 100644 crates/assistant_text_thread/src/assistant_text_thread.rs delete mode 100644 crates/assistant_text_thread/src/assistant_text_thread_tests.rs delete mode 100644 crates/assistant_text_thread/src/context_server_command.rs delete mode 100644 crates/assistant_text_thread/src/text_thread.rs delete mode 100644 crates/assistant_text_thread/src/text_thread_store.rs create mode 100644 crates/migrator/src/migrations/m_2026_03_31/settings.rs delete mode 100644 crates/terminal_view/src/terminal_slash_command.rs diff --git a/.github/CODEOWNERS.hold b/.github/CODEOWNERS.hold index 3b7cbc644768f82646591619e49c4b6a0d6de200..073a543b881052f3c2e0c2a9a1054261af40dba5 100644 --- a/.github/CODEOWNERS.hold +++ b/.github/CODEOWNERS.hold @@ -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 diff --git a/Cargo.lock b/Cargo.lock index 81d82dcd46c85293f67a927405251bbc87df4967..b8e4c549314fa30614cdf5afb0f62af1b4922fdb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index f3056b87fbdcc1ccc380b9fc0059df8a94b0c1f3..ebe14d332ad477a58a44305145c51f6b18ea72dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" } diff --git a/assets/icons/text_thread.svg b/assets/icons/text_thread.svg deleted file mode 100644 index aa078c72a2f35d2b82e90f2be64d23fcda3418a5..0000000000000000000000000000000000000000 --- a/assets/icons/text_thread.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 84b814e27a702232f5c54a6745b31f42935ca7a5..523a961d6964e2c6e08d03b75a3e1eb1890fc586 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -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", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 1ffe011a25b74c6e7fdb30282f86b0667fc54793..9ca71aa9be3a99b1b52ab8490a6fe841956ecf50 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -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", diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json index 8ceec7e7f494c352b65a7a1f5aa1ad608eb5ff96..1883d0df0b3ff44ad8dceefb997198cb203a9b8d 100644 --- a/assets/keymaps/default-windows.json +++ b/assets/keymaps/default-windows.json @@ -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", diff --git a/assets/keymaps/linux/cursor.json b/assets/keymaps/linux/cursor.json index e1eeade9db16d178fb2ce0ec4b2ec03f0ac2c221..8d5f7b5a76cb09a6c1be2638019f9cd6cf9942de 100644 --- a/assets/keymaps/linux/cursor.json +++ b/assets/keymaps/linux/cursor.json @@ -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 diff --git a/assets/keymaps/macos/cursor.json b/assets/keymaps/macos/cursor.json index 2824575a445ad0c870a59cb516441dc6f1421f31..f7cab89fb6118777ea07268cdeef2cf440c7b077 100644 --- a/assets/keymaps/macos/cursor.json +++ b/assets/keymaps/macos/cursor.json @@ -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 diff --git a/assets/settings/default.json b/assets/settings/default.json index d3defb428c68120e89c6bc6cc82488f03a06b320..57bad245474b9469a0a9b9d5674c692059f039af 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -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": { diff --git a/crates/acp_thread/src/mention.rs b/crates/acp_thread/src/mention.rs index 43dfe7610e34a0399a27a1d28858b938acfc2e0f..753838d3b98ed60dc02c3d9383c28fe4f848a29e 100644 --- a/crates/acp_thread/src/mention.rs +++ b/crates/acp_thread/src/mention.rs @@ -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}")); diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 627fb37b4d2559e5cda573d849fd0df306c1cc7d..b61df1b8af84d312d7f186fb85e5a1d04ab59dfd 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -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, diff --git a/crates/agent/src/tool_permissions.rs b/crates/agent/src/tool_permissions.rs index 73b3ff842ab6961b22815c902ce9ae79e60cd2e3..e74b6e4c5ce34383ad7ea702f1ba3a0cfd028455 100644 --- a/crates/agent/src/tool_permissions.rs +++ b/crates/agent/src/tool_permissions.rs @@ -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, diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index 7f51bd8ea5b9b8864663fbf9dc95beedb643d480..2ef65fe33641cdeca1a77642251523275511e81f 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/crates/agent_settings/src/agent_settings.rs @@ -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, pub favorite_models: Vec, pub default_profile: AgentProfileId, - pub default_view: DefaultAgentView, pub profiles: IndexMap, 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() diff --git a/crates/agent_ui/Cargo.toml b/crates/agent_ui/Cargo.toml index 6c045d4dd2114834605da278aad111fab174d4c6..e505a124b6898953db9751ddfc8ab98cb7f496f0 100644 --- a/crates/agent_ui/Cargo.toml +++ b/crates/agent_ui/Cargo.toml @@ -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"] } diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 17eb0e966fe51580a7d756fa6f5524ecaa96a640..e6ef267a95110e745534010bae32b1b1fd6c0f0c 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -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, + selected_agent: Option, #[serde(default)] last_active_thread: Option, #[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, work_dirs: Option, } @@ -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::(cx) { - workspace.focus_panel::(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::(cx) { workspace.focus_panel::(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::(cx) else { + return; + }; + + if !panel.focus_handle(cx).contains_focused(window, cx) { + workspace.toggle_panel_focus::(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 }, - TextThreads, -} - enum ActiveView { Uninitialized, AgentThread { conversation_view: Entity, }, - TextThread { - text_thread_editor: Entity, - title_editor: Entity, - buffer_search_bar: Entity, - _subscriptions: Vec, - }, History { - history: History, + view: Entity, }, 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 { - match self { - Self::NativeAgent | Self::TextThread => None, - Self::Custom { .. } => Some(IconName::Sparkle), - } - } -} - -impl From 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, - language_registry: Arc, - 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, fs: Arc, language_registry: Arc, - text_thread_history: Entity, thread_store: Entity, - text_thread_store: Entity, prompt_store: Option>, connection_store: Entity, context_server_registry: Entity, @@ -747,14 +606,13 @@ pub struct AgentPanel { zoomed: bool, pending_serialization: Option>>, onboarding: Entity, - selected_agent_type: AgentType, + selected_agent: Agent, start_thread_in: StartThreadIn, worktree_creation_status: Option, _thread_view_subscription: Option, _active_thread_focus_subscription: Option, _worktree_creation_task: Option>, show_trust_workspace_message: bool, - last_configuration_error_telemetry: Option, on_boarding_upsell_dismissed: AtomicBool, _active_view_observation: Option, } @@ -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, - prompt_builder: Arc, mut cx: AsyncWindowContext, ) -> Task>> { 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, prompt_store: Option>, window: &mut Window, cx: &mut Context, @@ -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) { - 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, @@ -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, - ) -> Option { - 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> { + 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) { - 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, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - 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, - window: &mut Window, - cx: &mut Context, - ) { - 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) { 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> { - 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, ) { - 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, - history: History, + view: Entity, cx: &mut Context, ) -> ContextMenu { - match history { - History::AgentThreads { view } => { - let entries = view - .read(cx) - .history() - .read(cx) - .sessions() - .iter() - .take(RECENTLY_UPDATED_MENU_LIMIT) - .cloned() - .collect::>(); - - 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::>(); + let entries = view + .read(cx) + .history() + .read(cx) + .sessions() + .iter() + .take(RECENTLY_UPDATED_MENU_LIMIT) + .cloned() + .collect::>(); - 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 { - 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) { @@ -2397,48 +2026,19 @@ impl AgentPanel { ); } - pub fn new_agent_thread( - &mut self, - agent: AgentType, - window: &mut Window, - cx: &mut Context, - ) { + pub fn new_agent_thread(&mut self, agent: Agent, window: &mut Window, cx: &mut Context) { 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, ) { - 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, ) { - 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 @@ -2775,8 +2374,8 @@ impl AgentPanel { ) { self.worktree_creation_status = Some(WorktreeCreationStatus::Error(message)); if matches!(self.active_view, ActiveView::Uninitialized) { - let selected_agent_type = self.selected_agent_type.clone(); - self.new_agent_thread(selected_agent_type, window, cx); + let selected_agent = self.selected_agent.clone(); + self.new_agent_thread(selected_agent, window, cx); } cx.notify(); } @@ -3136,13 +2735,7 @@ impl Focusable for AgentPanel { ActiveView::AgentThread { conversation_view, .. } => conversation_view.focus_handle(cx), - ActiveView::History { history: kind } => match kind { - History::AgentThreads { view } => view.read(cx).focus_handle(cx), - History::TextThreads => self.text_thread_history.focus_handle(cx), - }, - ActiveView::TextThread { - text_thread_editor, .. - } => text_thread_editor.focus_handle(cx), + ActiveView::History { view } => view.read(cx).focus_handle(cx), ActiveView::Configuration => { if let Some(configuration) = self.configuration.as_ref() { configuration.focus_handle(cx) @@ -3227,8 +2820,8 @@ impl Panel for AgentPanel { Some(WorktreeCreationStatus::Creating) ) { - let selected_agent_type = self.selected_agent_type.clone(); - self.new_agent_thread_inner(selected_agent_type, false, window, cx); + let selected_agent = self.selected_agent.clone(); + self.new_agent_thread_inner(selected_agent, false, window, cx); } } @@ -3272,8 +2865,6 @@ impl Panel for AgentPanel { impl AgentPanel { fn render_title_view(&self, _window: &mut Window, cx: &Context) -> AnyElement { - const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…"; - let content = match &self.active_view { ActiveView::AgentThread { conversation_view } => { let server_view_ref = conversation_view.read(cx); @@ -3327,70 +2918,7 @@ impl AgentPanel { .into_any_element() } } - ActiveView::TextThread { - title_editor, - text_thread_editor, - .. - } => { - let summary = text_thread_editor.read(cx).text_thread().read(cx).summary(); - - match summary { - TextThreadSummary::Pending => Label::new(TextThreadSummary::DEFAULT) - .color(Color::Muted) - .truncate() - .into_any_element(), - TextThreadSummary::Content(summary) => { - if summary.done { - div() - .w_full() - .child(title_editor.clone()) - .into_any_element() - } else { - Label::new(LOADING_SUMMARY_PLACEHOLDER) - .truncate() - .color(Color::Muted) - .with_animation( - "generating_title", - Animation::new(Duration::from_secs(2)) - .repeat() - .with_easing(pulsating_between(0.4, 0.8)), - |label, delta| label.alpha(delta), - ) - .into_any_element() - } - } - TextThreadSummary::Error => h_flex() - .w_full() - .child(title_editor.clone()) - .child( - IconButton::new("retry-summary-generation", IconName::RotateCcw) - .icon_size(IconSize::Small) - .on_click({ - let text_thread_editor = text_thread_editor.clone(); - move |_, _window, cx| { - text_thread_editor.update(cx, |text_thread_editor, cx| { - text_thread_editor.regenerate_summary(cx); - }); - } - }) - .tooltip(move |_window, cx| { - cx.new(|_| { - Tooltip::new("Failed to generate title") - .meta("Click to try again") - }) - .into() - }), - ) - .into_any_element(), - } - } - ActiveView::History { history: kind } => { - let title = match kind { - History::AgentThreads { .. } => "History", - History::TextThreads => "Text Thread History", - }; - Label::new(title).truncate().into_any_element() - } + ActiveView::History { .. } => Label::new("History").truncate().into_any_element(), ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(), ActiveView::Uninitialized => Label::new("Agent").truncate().into_any_element(), }; @@ -3416,15 +2944,6 @@ impl AgentPanel { }); } - fn handle_regenerate_text_thread_title( - text_thread_editor: Entity, - cx: &mut App, - ) { - text_thread_editor.update(cx, |text_thread_editor, cx| { - text_thread_editor.regenerate_summary(cx); - }); - } - fn render_panel_options_menu( &self, window: &mut Window, @@ -3438,24 +2957,6 @@ impl AgentPanel { "Enable Full Screen" }; - let text_thread_view = match &self.active_view { - ActiveView::TextThread { - text_thread_editor, .. - } => Some(text_thread_editor.clone()), - _ => None, - }; - let text_thread_with_messages = match &self.active_view { - ActiveView::TextThread { - text_thread_editor, .. - } => text_thread_editor - .read(cx) - .text_thread() - .read(cx) - .messages(cx) - .any(|message| message.role == language_model::Role::Assistant), - _ => false, - }; - let conversation_view = match &self.active_view { ActiveView::AgentThread { conversation_view } => Some(conversation_view.clone()), _ => None, @@ -3496,23 +2997,9 @@ impl AgentPanel { Some(ContextMenu::build(window, cx, |mut menu, _window, _| { menu = menu.context(focus_handle.clone()); - if thread_with_messages | text_thread_with_messages { + if thread_with_messages { menu = menu.header("Current Thread"); - if let Some(text_thread_view) = text_thread_view.as_ref() { - menu = menu - .entry("Regenerate Thread Title", None, { - let text_thread_view = text_thread_view.clone(); - move |_, cx| { - Self::handle_regenerate_text_thread_title( - text_thread_view.clone(), - cx, - ); - } - }) - .separator(); - } - if let Some(conversation_view) = conversation_view.as_ref() { menu = menu .entry("Regenerate Thread Title", None, { @@ -3764,33 +3251,32 @@ impl AgentPanel { let focus_handle = self.focus_handle(cx); let (selected_agent_custom_icon, selected_agent_label) = - if let AgentType::Custom { id, .. } = &self.selected_agent_type { + if let Agent::Custom { id, .. } = &self.selected_agent { let store = agent_server_store.read(cx); let icon = store.agent_icon(&id); let label = store .agent_display_name(&id) - .unwrap_or_else(|| self.selected_agent_type.label()); + .unwrap_or_else(|| self.selected_agent.label()); (icon, label) } else { - (None, self.selected_agent_type.label()) + (None, self.selected_agent.label()) }; let active_thread = match &self.active_view { ActiveView::AgentThread { conversation_view } => { conversation_view.read(cx).as_native_thread(cx) } - ActiveView::Uninitialized - | ActiveView::TextThread { .. } - | ActiveView::History { .. } - | ActiveView::Configuration => None, + ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => { + None + } }; let new_thread_menu_builder: Rc< dyn Fn(&mut Window, &mut App) -> Option>, > = { - let selected_agent = self.selected_agent_type.clone(); - let is_agent_selected = move |agent_type: AgentType| selected_agent == agent_type; + let selected_agent = self.selected_agent.clone(); + let is_agent_selected = move |agent: Agent| selected_agent == agent; let workspace = self.workspace.clone(); let is_via_collab = workspace @@ -3832,15 +3318,9 @@ impl AgentPanel { }) .item( ContextMenuEntry::new("Zed Agent") - .when( - is_agent_selected(AgentType::NativeAgent) - | is_agent_selected(AgentType::TextThread), - |this| { - this.action(Box::new(NewExternalAgentThread { - agent: None, - })) - }, - ) + .when(is_agent_selected(Agent::NativeAgent), |this| { + this.action(Box::new(NewExternalAgentThread { agent: None })) + }) .icon(IconName::ZedAgent) .icon_color(Color::Muted) .handler({ @@ -3853,33 +3333,7 @@ impl AgentPanel { { panel.update(cx, |panel, cx| { panel.new_agent_thread( - AgentType::NativeAgent, - window, - cx, - ); - }); - } - }); - } - } - }), - ) - .item( - ContextMenuEntry::new("Text Thread") - .action(NewTextThread.boxed_clone()) - .icon(IconName::TextThread) - .icon_color(Color::Muted) - .handler({ - let workspace = workspace.clone(); - move |window, cx| { - if let Some(workspace) = workspace.upgrade() { - workspace.update(cx, |workspace, cx| { - if let Some(panel) = - workspace.panel::(cx) - { - panel.update(cx, |panel, cx| { - panel.new_agent_thread( - AgentType::TextThread, + Agent::NativeAgent, window, cx, ); @@ -3942,7 +3396,7 @@ impl AgentPanel { entry = entry .when( - is_agent_selected(AgentType::Custom { + is_agent_selected(Agent::Custom { id: item.id.clone(), }), |this| { @@ -3964,7 +3418,7 @@ impl AgentPanel { { panel.update(cx, |panel, cx| { panel.new_agent_thread( - AgentType::Custom { + Agent::Custom { id: agent_id.clone(), }, window, @@ -4005,7 +3459,7 @@ impl AgentPanel { let has_custom_icon = selected_agent_custom_icon.is_some(); let selected_agent_custom_icon_for_button = selected_agent_custom_icon.clone(); - let selected_agent_builtin_icon = self.selected_agent_type.icon(); + let selected_agent_builtin_icon = self.selected_agent.icon(); let selected_agent_label_for_tooltip = selected_agent_label.clone(); let selected_agent = div() @@ -4015,7 +3469,7 @@ impl AgentPanel { .child(Icon::from_external_svg(icon_path).color(Color::Muted)) }) .when(!has_custom_icon, |this| { - this.when_some(self.selected_agent_type.icon(), |this, icon| { + this.when_some(selected_agent_builtin_icon, |this, icon| { this.px_1().child(Icon::new(icon).color(Color::Muted)) }) }) @@ -4051,12 +3505,9 @@ impl AgentPanel { ActiveView::History { .. } | ActiveView::Configuration ); - let is_text_thread = matches!(&self.active_view, ActiveView::TextThread { .. }); - let is_full_screen = self.is_zoomed(window, cx); - let use_v2_empty_toolbar = - has_v2_flag && is_empty_state && !is_in_history_or_config && !is_text_thread; + let use_v2_empty_toolbar = has_v2_flag && is_empty_state && !is_in_history_or_config; let base_container = h_flex() .id("agent-panel-toolbar") @@ -4270,7 +3721,7 @@ impl AgentPanel { } match &self.active_view { - ActiveView::TextThread { .. } => { + ActiveView::AgentThread { .. } => { if LanguageModelRegistry::global(cx) .read(cx) .default_model() @@ -4281,10 +3732,9 @@ impl AgentPanel { return false; } } - ActiveView::Uninitialized - | ActiveView::AgentThread { .. } - | ActiveView::History { .. } - | ActiveView::Configuration => return false, + ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => { + return false; + } } let plan = self.user_store.read(cx).plan(); @@ -4334,10 +3784,6 @@ impl AgentPanel { .is_none_or(|h| h.read(cx).is_empty()); history_is_empty || !has_configured_non_zed_providers } - ActiveView::TextThread { .. } => { - let history_is_empty = self.text_thread_history.read(cx).is_empty(); - history_is_empty || !has_configured_non_zed_providers - } } } @@ -4350,15 +3796,7 @@ impl AgentPanel { return None; } - let text_thread_view = matches!(&self.active_view, ActiveView::TextThread { .. }); - - Some( - div() - .when(text_thread_view, |this| { - this.bg(cx.theme().colors().editor_background) - }) - .child(self.onboarding.clone()), - ) + Some(div().child(self.onboarding.clone())) } fn render_trial_end_upsell( @@ -4390,142 +3828,6 @@ impl AgentPanel { ) } - fn emit_configuration_error_telemetry_if_needed( - &mut self, - configuration_error: Option<&ConfigurationError>, - ) { - let error_kind = configuration_error.map(|err| match err { - ConfigurationError::NoProvider => "no_provider", - ConfigurationError::ModelNotFound => "model_not_found", - ConfigurationError::ProviderNotAuthenticated(_) => "provider_not_authenticated", - }); - - let error_kind_string = error_kind.map(String::from); - - if self.last_configuration_error_telemetry == error_kind_string { - return; - } - - self.last_configuration_error_telemetry = error_kind_string; - - if let Some(kind) = error_kind { - let message = configuration_error - .map(|err| err.to_string()) - .unwrap_or_default(); - - telemetry::event!("Agent Panel Error Shown", kind = kind, message = message,); - } - } - - fn render_configuration_error( - &self, - border_bottom: bool, - configuration_error: &ConfigurationError, - focus_handle: &FocusHandle, - cx: &mut App, - ) -> impl IntoElement { - let zed_provider_configured = AgentSettings::get_global(cx) - .default_model - .as_ref() - .is_some_and(|selection| selection.provider.0.as_str() == "zed.dev"); - - let callout = if zed_provider_configured { - Callout::new() - .icon(IconName::Warning) - .severity(Severity::Warning) - .when(border_bottom, |this| { - this.border_position(ui::BorderPosition::Bottom) - }) - .title("Sign in to continue using Zed as your LLM provider.") - .actions_slot( - Button::new("sign_in", "Sign In") - .style(ButtonStyle::Tinted(ui::TintColor::Warning)) - .label_size(LabelSize::Small) - .on_click({ - let workspace = self.workspace.clone(); - move |_, _, cx| { - let Ok(client) = - workspace.update(cx, |workspace, _| workspace.client().clone()) - else { - return; - }; - - cx.spawn(async move |cx| { - client.sign_in_with_optional_connect(true, cx).await - }) - .detach_and_log_err(cx); - } - }), - ) - } else { - Callout::new() - .icon(IconName::Warning) - .severity(Severity::Warning) - .when(border_bottom, |this| { - this.border_position(ui::BorderPosition::Bottom) - }) - .title(configuration_error.to_string()) - .actions_slot( - Button::new("settings", "Configure") - .style(ButtonStyle::Tinted(ui::TintColor::Warning)) - .label_size(LabelSize::Small) - .key_binding( - KeyBinding::for_action_in(&OpenSettings, focus_handle, cx) - .map(|kb| kb.size(rems_from_px(12.))), - ) - .on_click(|_event, window, cx| { - window.dispatch_action(OpenSettings.boxed_clone(), cx) - }), - ) - }; - - match configuration_error { - ConfigurationError::ModelNotFound - | ConfigurationError::ProviderNotAuthenticated(_) - | ConfigurationError::NoProvider => callout.into_any_element(), - } - } - - fn render_text_thread( - &self, - text_thread_editor: &Entity, - buffer_search_bar: &Entity, - window: &mut Window, - cx: &mut Context, - ) -> Div { - let mut registrar = buffer_search::DivRegistrar::new( - |this, _, _cx| match &this.active_view { - ActiveView::TextThread { - buffer_search_bar, .. - } => Some(buffer_search_bar.clone()), - _ => None, - }, - cx, - ); - BufferSearchBar::register(&mut registrar); - registrar - .into_div() - .size_full() - .relative() - .map(|parent| { - buffer_search_bar.update(cx, |buffer_search_bar, cx| { - if buffer_search_bar.is_dismissed() { - return parent; - } - parent.child( - div() - .p(DynamicSpacing::Base08.rems(cx)) - .border_b_1() - .border_color(cx.theme().colors().border_variant) - .bg(cx.theme().colors().editor_background) - .child(buffer_search_bar.render(window, cx)), - ) - }) - }) - .child(text_thread_editor.clone()) - .child(self.render_drag_target(cx)) - } - fn render_drag_target(&self, cx: &Context) -> Div { let is_local = self.project.read(cx).is_local(); div() @@ -4598,19 +3900,6 @@ impl AgentPanel { conversation_view.insert_dragged_files(paths, added_worktrees, window, cx); }); } - ActiveView::TextThread { - text_thread_editor, .. - } => { - text_thread_editor.update(cx, |text_thread_editor, cx| { - TextThreadEditor::insert_dragged_files( - text_thread_editor, - paths, - added_worktrees, - window, - cx, - ); - }); - } ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {} } } @@ -4652,7 +3941,6 @@ impl AgentPanel { key_context.add("AgentPanel"); match &self.active_view { ActiveView::AgentThread { .. } => key_context.add("acp_thread"), - ActiveView::TextThread { .. } => key_context.add("text_thread"), ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {} } key_context @@ -4703,59 +3991,15 @@ impl Render for AgentPanel { .child(self.render_toolbar(window, cx)) .children(self.render_workspace_trust_message(cx)) .children(self.render_onboarding(window, cx)) - .map(|parent| { - // Emit configuration error telemetry before entering the match to avoid borrow conflicts - if matches!(&self.active_view, ActiveView::TextThread { .. }) { - let model_registry = LanguageModelRegistry::read_global(cx); - let configuration_error = - model_registry.configuration_error(model_registry.default_model(), cx); - self.emit_configuration_error_telemetry_if_needed(configuration_error.as_ref()); - } - - match &self.active_view { - ActiveView::Uninitialized => parent, - ActiveView::AgentThread { - conversation_view, .. - } => parent - .child(conversation_view.clone()) - .child(self.render_drag_target(cx)), - ActiveView::History { history: kind } => match kind { - History::AgentThreads { view } => parent.child(view.clone()), - History::TextThreads => parent.child(self.text_thread_history.clone()), - }, - ActiveView::TextThread { - text_thread_editor, - buffer_search_bar, - .. - } => { - let model_registry = LanguageModelRegistry::read_global(cx); - let configuration_error = - model_registry.configuration_error(model_registry.default_model(), cx); - - parent - .map(|this| { - if !self.should_render_onboarding(cx) - && let Some(err) = configuration_error.as_ref() - { - this.child(self.render_configuration_error( - true, - err, - &self.focus_handle(cx), - cx, - )) - } else { - this - } - }) - .child(self.render_text_thread( - text_thread_editor, - buffer_search_bar, - window, - cx, - )) - } - ActiveView::Configuration => parent.children(self.configuration.clone()), - } + .map(|parent| match &self.active_view { + ActiveView::Uninitialized => parent, + ActiveView::AgentThread { + conversation_view, .. + } => parent + .child(conversation_view.clone()) + .child(self.render_drag_target(cx)), + ActiveView::History { view } => parent.child(view.clone()), + ActiveView::Configuration => parent.children(self.configuration.clone()), }) .children(self.render_worktree_creation_status(cx)) .children(self.render_trial_end_upsell(window, cx)); @@ -4831,117 +4075,6 @@ impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist { } } -pub struct ConcreteAssistantPanelDelegate; - -impl AgentPanelDelegate for ConcreteAssistantPanelDelegate { - fn active_text_thread_editor( - &self, - workspace: &mut Workspace, - _window: &mut Window, - cx: &mut Context, - ) -> Option> { - let panel = workspace.panel::(cx)?; - panel.read(cx).active_text_thread_editor() - } - - fn open_local_text_thread( - &self, - workspace: &mut Workspace, - path: Arc, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - let Some(panel) = workspace.panel::(cx) else { - return Task::ready(Err(anyhow!("Agent panel not found"))); - }; - - panel.update(cx, |panel, cx| { - panel.open_saved_text_thread(path, window, cx) - }) - } - - fn open_remote_text_thread( - &self, - _workspace: &mut Workspace, - _text_thread_id: assistant_text_thread::TextThreadId, - _window: &mut Window, - _cx: &mut Context, - ) -> Task>> { - Task::ready(Err(anyhow!("opening remote context not implemented"))) - } - - fn quote_selection( - &self, - workspace: &mut Workspace, - selection_ranges: Vec>, - buffer: Entity, - window: &mut Window, - cx: &mut Context, - ) { - let Some(panel) = workspace.panel::(cx) else { - return; - }; - - if !panel.focus_handle(cx).contains_focused(window, cx) { - workspace.toggle_panel_focus::(window, cx); - } - - panel.update(cx, |_, cx| { - // Wait to create a new context until the workspace is no longer - // being updated. - cx.defer_in(window, move |panel, window, cx| { - if let Some(conversation_view) = panel.active_conversation_view() { - conversation_view.update(cx, |conversation_view, cx| { - conversation_view.insert_selections(window, cx); - }); - } else if let Some(text_thread_editor) = panel.active_text_thread_editor() { - let snapshot = buffer.read(cx).snapshot(cx); - let selection_ranges = selection_ranges - .into_iter() - .map(|range| range.to_point(&snapshot)) - .collect::>(); - - text_thread_editor.update(cx, |text_thread_editor, cx| { - text_thread_editor.quote_ranges(selection_ranges, snapshot, window, cx) - }); - } - }); - }); - } - - fn quote_terminal_text( - &self, - workspace: &mut Workspace, - text: String, - window: &mut Window, - cx: &mut Context, - ) { - let Some(panel) = workspace.panel::(cx) else { - return; - }; - - if !panel.focus_handle(cx).contains_focused(window, cx) { - workspace.toggle_panel_focus::(window, cx); - } - - panel.update(cx, |_, cx| { - // Wait to create a new context until the workspace is no longer - // being updated. - cx.defer_in(window, move |panel, window, cx| { - if let Some(conversation_view) = panel.active_conversation_view() { - conversation_view.update(cx, |conversation_view, cx| { - conversation_view.insert_terminal_text(text, window, cx); - }); - } else if let Some(text_thread_editor) = panel.active_text_thread_editor() { - text_thread_editor.update(cx, |text_thread_editor, cx| { - text_thread_editor.quote_terminal_text(text, window, cx) - }); - } - }); - }); - } -} - struct OnboardingUpsell; impl Dismissable for OnboardingUpsell { @@ -4957,13 +4090,8 @@ impl Dismissable for TrialEndUpsell { /// Test-only helper methods #[cfg(any(test, feature = "test-support"))] impl AgentPanel { - pub fn test_new( - workspace: &Workspace, - text_thread_store: Entity, - window: &mut Window, - cx: &mut Context, - ) -> Self { - Self::new(workspace, text_thread_store, None, window, cx) + pub fn test_new(workspace: &Workspace, window: &mut Window, cx: &mut Context) -> Self { + Self::new(workspace, None, window, cx) } /// Opens an external thread using an arbitrary AgentServer. @@ -5063,12 +4191,12 @@ mod tests { }; use acp_thread::{StubAgentConnection, ThreadStatus}; use agent_servers::CODEX_ID; - use assistant_text_thread::TextThreadStore; use feature_flags::FeatureFlagAppExt; use fs::FakeFs; use gpui::{TestAppContext, VisualTestContext}; use project::Project; use serde_json::json; + use std::path::Path; use std::time::Instant; use workspace::MultiWorkspace; @@ -5112,8 +4240,7 @@ mod tests { // --- Set up workspace A: with an active thread --- let panel_a = workspace_a.update_in(cx, |workspace, window, cx| { - let text_thread_store = cx.new(|cx| TextThreadStore::fake(project_a.clone(), cx)); - cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx)) + cx.new(|cx| AgentPanel::new(workspace, None, window, cx)) }); panel_a.update_in(cx, |panel, window, cx| { @@ -5133,16 +4260,15 @@ mod tests { ); }); - let agent_type_a = panel_a.read_with(cx, |panel, _cx| panel.selected_agent_type.clone()); + let agent_type_a = panel_a.read_with(cx, |panel, _cx| panel.selected_agent.clone()); // --- Set up workspace B: ClaudeCode, no active thread --- let panel_b = workspace_b.update_in(cx, |workspace, window, cx| { - let text_thread_store = cx.new(|cx| TextThreadStore::fake(project_b.clone(), cx)); - cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx)) + cx.new(|cx| AgentPanel::new(workspace, None, window, cx)) }); panel_b.update(cx, |panel, _cx| { - panel.selected_agent_type = AgentType::Custom { + panel.selected_agent = Agent::Custom { id: "claude-acp".into(), }; }); @@ -5153,16 +4279,14 @@ mod tests { cx.run_until_parked(); // --- Load fresh panels for each workspace and verify independent state --- - let prompt_builder = Arc::new(prompt_store::PromptBuilder::new(None).unwrap()); - let async_cx = cx.update(|window, cx| window.to_async(cx)); - let loaded_a = AgentPanel::load(workspace_a.downgrade(), prompt_builder.clone(), async_cx) + let loaded_a = AgentPanel::load(workspace_a.downgrade(), async_cx) .await .expect("panel A load should succeed"); cx.run_until_parked(); let async_cx = cx.update(|window, cx| window.to_async(cx)); - let loaded_b = AgentPanel::load(workspace_b.downgrade(), prompt_builder.clone(), async_cx) + let loaded_b = AgentPanel::load(workspace_b.downgrade(), async_cx) .await .expect("panel B load should succeed"); cx.run_until_parked(); @@ -5170,7 +4294,7 @@ mod tests { // Workspace A should restore its thread and agent type loaded_a.read_with(cx, |panel, _cx| { assert_eq!( - panel.selected_agent_type, agent_type_a, + panel.selected_agent, agent_type_a, "workspace A agent type should be restored" ); assert!( @@ -5182,8 +4306,8 @@ mod tests { // Workspace B should restore its own agent type, with no thread loaded_b.read_with(cx, |panel, _cx| { assert_eq!( - panel.selected_agent_type, - AgentType::Custom { + panel.selected_agent, + Agent::Custom { id: "claude-acp".into() }, "workspace B agent type should be restored" @@ -5195,53 +4319,6 @@ mod tests { }); } - // Simple regression test - #[gpui::test] - async fn test_new_text_thread_action_handler(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - - cx.update(|cx| { - cx.update_flags(true, vec!["agent-v2".to_string()]); - agent::ThreadStore::init_global(cx); - language_model::LanguageModelRegistry::test(cx); - let slash_command_registry = - assistant_slash_command::SlashCommandRegistry::default_global(cx); - slash_command_registry - .register_command(assistant_slash_commands::DefaultSlashCommand, false); - ::set_global(fs.clone(), cx); - }); - - let project = Project::test(fs.clone(), [], cx).await; - - let multi_workspace = - cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); - - let workspace_a = multi_workspace - .read_with(cx, |multi_workspace, _cx| { - multi_workspace.workspace().clone() - }) - .unwrap(); - - let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx); - - workspace_a.update_in(cx, |workspace, window, cx| { - let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); - let panel = - cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx)); - workspace.add_panel(panel, window, cx); - }); - - cx.run_until_parked(); - - workspace_a.update_in(cx, |_, window, cx| { - window.dispatch_action(NewTextThread.boxed_clone(), cx); - }); - - cx.run_until_parked(); - } - /// Extracts the text from a Text content block, panicking if it's not Text. fn expect_text_block(block: &acp::ContentBlock) -> &str { match block { @@ -5578,8 +4655,7 @@ mod tests { let mut cx = VisualTestContext::from_window(multi_workspace.into(), cx); let panel = workspace.update_in(&mut cx, |workspace, window, cx| { - let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); - cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx)) + cx.new(|cx| AgentPanel::new(workspace, None, window, cx)) }); (panel, cx) @@ -5923,9 +4999,7 @@ mod tests { cx.run_until_parked(); let panel = 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::new(workspace, text_thread_store, None, window, cx)); + let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx)); workspace.add_panel(panel.clone(), window, cx); panel }); @@ -6033,9 +5107,7 @@ mod tests { cx.run_until_parked(); let panel = 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::new(workspace, text_thread_store, None, window, cx)); + let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx)); workspace.add_panel(panel.clone(), window, cx); panel }); @@ -6064,12 +5136,10 @@ mod tests { cx.run_until_parked(); // Load a fresh panel from the serialized data. - let prompt_builder = Arc::new(prompt_store::PromptBuilder::new(None).unwrap()); let async_cx = cx.update(|window, cx| window.to_async(cx)); - let loaded_panel = - AgentPanel::load(workspace.downgrade(), prompt_builder.clone(), async_cx) - .await - .expect("panel load should succeed"); + let loaded_panel = AgentPanel::load(workspace.downgrade(), async_cx) + .await + .expect("panel load should succeed"); cx.run_until_parked(); loaded_panel.read_with(cx, |panel, _cx| { @@ -6118,9 +5188,7 @@ mod tests { let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx); let panel = 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::new(workspace, text_thread_store, None, window, cx)); + let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx)); workspace.add_panel(panel.clone(), window, cx); panel }); @@ -6163,21 +5231,49 @@ mod tests { } #[test] - fn test_deserialize_agent_type_variants() { + fn test_deserialize_agent_variants() { + // PascalCase (legacy AgentType format, persisted in panel state) + assert_eq!( + serde_json::from_str::(r#""NativeAgent""#).unwrap(), + Agent::NativeAgent, + ); + assert_eq!( + serde_json::from_str::(r#"{"Custom":{"name":"my-agent"}}"#).unwrap(), + Agent::Custom { + id: "my-agent".into(), + }, + ); + + // Legacy TextThread variant deserializes to NativeAgent assert_eq!( - serde_json::from_str::(r#""NativeAgent""#).unwrap(), - AgentType::NativeAgent, + serde_json::from_str::(r#""TextThread""#).unwrap(), + Agent::NativeAgent, ); + + // snake_case (canonical format) assert_eq!( - serde_json::from_str::(r#""TextThread""#).unwrap(), - AgentType::TextThread, + serde_json::from_str::(r#""native_agent""#).unwrap(), + Agent::NativeAgent, ); assert_eq!( - serde_json::from_str::(r#"{"Custom":{"name":"my-agent"}}"#).unwrap(), - AgentType::Custom { + serde_json::from_str::(r#"{"custom":{"name":"my-agent"}}"#).unwrap(), + Agent::Custom { id: "my-agent".into(), }, ); + + // Serialization uses snake_case + assert_eq!( + serde_json::to_string(&Agent::NativeAgent).unwrap(), + r#""native_agent""#, + ); + assert_eq!( + serde_json::to_string(&Agent::Custom { + id: "my-agent".into() + }) + .unwrap(), + r#"{"custom":{"name":"my-agent"}}"#, + ); } #[gpui::test] @@ -6229,12 +5325,7 @@ mod tests { window: Option<&mut Window>, cx: &mut Context| { if let Some(window) = window { - let project = workspace.project().clone(); - let text_thread_store = - cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); - let panel = cx.new(|cx| { - AgentPanel::new(workspace, text_thread_store, None, window, cx) - }); + let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx)); workspace.add_panel(panel, window, cx); } }, @@ -6248,9 +5339,7 @@ mod tests { cx.run_until_parked(); let panel = 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::new(workspace, text_thread_store, None, window, cx)); + let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx)); workspace.add_panel(panel.clone(), window, cx); panel }); @@ -6270,9 +5359,9 @@ mod tests { // Set the selected agent to Codex (a custom agent) and start_thread_in // to NewWorktree. We do this AFTER opening the thread because - // open_external_thread_with_server overrides selected_agent_type. + // open_external_thread_with_server overrides selected_agent. panel.update_in(cx, |panel, window, cx| { - panel.selected_agent_type = AgentType::Custom { + panel.selected_agent = Agent::Custom { id: CODEX_ID.into(), }; panel.set_start_thread_in(&StartThreadIn::NewWorktree, window, cx); @@ -6281,8 +5370,8 @@ mod tests { // Verify the panel has the Codex agent selected. panel.read_with(cx, |panel, _cx| { assert_eq!( - panel.selected_agent_type, - AgentType::Custom { + panel.selected_agent, + Agent::Custom { id: CODEX_ID.into() }, ); @@ -6321,13 +5410,13 @@ mod tests { .panel::(cx) .expect("new workspace should have an AgentPanel"); - new_panel.read(cx).selected_agent_type.clone() + new_panel.read(cx).selected_agent.clone() }) .unwrap(); assert_eq!( found_codex, - AgentType::Custom { + Agent::Custom { id: CODEX_ID.into() }, "the new worktree workspace should use the same agent (Codex) that was selected in the original panel", @@ -6359,8 +5448,7 @@ mod tests { let mut cx = VisualTestContext::from_window(multi_workspace.into(), cx); let panel = workspace.update_in(&mut cx, |workspace, window, cx| { - let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); - cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx)) + cx.new(|cx| AgentPanel::new(workspace, None, window, cx)) }); // Open thread A and send a message. With empty next_prompt_updates it diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 02050a0e11d45dbb0c7c5cfeea3a200409a266ad..98715056ccec43fb91cc4dc9307cf41d84719fc0 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -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 { + match self { + Self::NativeAgent => None, + Self::Custom { .. } => Some(IconName::Sparkle), + } + } + pub fn server( &self, fs: Arc, @@ -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, - client: Arc, prompt_builder: Arc, language_registry: Arc, 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::({ - 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::(r#""native_agent""#).unwrap(), + serde_json::from_str::(r#""NativeAgent""#).unwrap(), Agent::NativeAgent, ); assert_eq!( - serde_json::from_str::(r#"{"custom":{"name":"my-agent"}}"#).unwrap(), + serde_json::from_str::(r#"{"Custom":{"name":"my-agent"}}"#).unwrap(), Agent::Custom { id: "my-agent".into(), }, diff --git a/crates/agent_ui/src/conversation_view.rs b/crates/agent_ui/src/conversation_view.rs index bdc30d49a85a4a76f18f4f9c65b2e50ebbc13d85..c13b6d29f92c273b36b948c90f5f5c6f1b659970 100644 --- a/crates/agent_ui/src/conversation_view.rs +++ b/crates/agent_ui/src/conversation_view.rs @@ -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, - ) { - 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 diff --git a/crates/agent_ui/src/conversation_view/thread_view.rs b/crates/agent_ui/src/conversation_view/thread_view.rs index afd508e47366d3217df6e5f1eb4cd0128c479895..63aa8b8529655a26b99ba74062f8d0a6a4812c5f 100644 --- a/crates/agent_ui/src/conversation_view/thread_view.rs +++ b/crates/agent_ui/src/conversation_view/thread_view.rs @@ -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::(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; diff --git a/crates/agent_ui/src/diagnostics.rs b/crates/agent_ui/src/diagnostics.rs new file mode 100644 index 0000000000000000000000000000000000000000..5a2423cae65aad960e73a0f50b11d7eb87323c90 --- /dev/null +++ b/crates/agent_ui/src/diagnostics.rs @@ -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>, +) -> 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, +} + +/// Collects project diagnostics into a formatted string. +/// +/// Returns `None` if no matching diagnostics were found. +pub fn collect_diagnostics( + project: Entity, + options: DiagnosticsOptions, + cx: &mut App, +) -> Task>> { + 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 +} diff --git a/crates/agent_ui/src/inline_assistant.rs b/crates/agent_ui/src/inline_assistant.rs index 9a2b95519b5977cb9937d2a37c8ef8c133e57976..3b98e496d4732deaf54be9b4e14da380285f467f 100644 --- a/crates/agent_ui/src/inline_assistant.rs +++ b/crates/agent_ui/src/inline_assistant.rs @@ -257,12 +257,8 @@ impl InlineAssistant { return; } - let Some(inline_assist_target) = Self::resolve_inline_assist_target( - workspace, - workspace.panel::(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>, window: &mut Window, cx: &mut App, ) -> Option { @@ -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::(cx)) { diff --git a/crates/agent_ui/src/mention_set.rs b/crates/agent_ui/src/mention_set.rs index b8e16de99f13d9eb6925e5618ccca81c742f8d12..2559edc566d4467eaaab180e0a16f4af5fae7ab9 100644 --- a/crates/agent_ui/src/mention_set.rs +++ b/crates/agent_ui/src/mention_set.rs @@ -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, diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index 44a816f894f791f8b9f3b4753deef7028fae20ab..df36f38899c9abea165d0ff5a01834a2bb84c82f 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -1308,62 +1308,6 @@ impl MessageEditor { } } - pub fn insert_terminal_crease( - &mut self, - text: String, - window: &mut Window, - cx: &mut Context, - ) { - 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) { let Some(workspace) = self.workspace.upgrade() else { return; diff --git a/crates/agent_ui/src/slash_command.rs b/crates/agent_ui/src/slash_command.rs deleted file mode 100644 index e328ef6725e5e789bd402667da91417ad69a372d..0000000000000000000000000000000000000000 --- a/crates/agent_ui/src/slash_command.rs +++ /dev/null @@ -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>, - slash_commands: Arc, - editor: Option>, - workspace: Option>, -} - -impl SlashCommandCompletionProvider { - pub fn new( - slash_commands: Arc, - editor: Option>, - workspace: Option>, - ) -> 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, - name_range: Range, - window: &mut Window, - cx: &mut App, - ) -> Task>> { - 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::>(); - 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, - argument_range: Range, - last_argument_range: Range, - window: &mut Window, - cx: &mut App, - ) -> Task>> { - 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 = 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_position: Anchor, - _: editor::CompletionContext, - window: &mut Window, - cx: &mut Context, - ) -> Task>> { - 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::>(); - 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, - position: language::Anchor, - _text: &str, - _trigger_in_words: bool, - cx: &mut Context, - ) -> 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 - } -} diff --git a/crates/agent_ui/src/slash_command_picker.rs b/crates/agent_ui/src/slash_command_picker.rs deleted file mode 100644 index 0c3cf37599887fe8e97dcdc67bb0bd7e28a744a7..0000000000000000000000000000000000000000 --- a/crates/agent_ui/src/slash_command_picker.rs +++ /dev/null @@ -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 -where - T: PopoverTrigger + ButtonCommon, - TT: Fn(&mut Window, &mut App) -> AnyView + 'static, -{ - working_set: Arc, - active_context_editor: WeakEntity, - trigger: T, - tooltip: TT, -} - -#[derive(Clone)] -struct SlashCommandInfo { - name: SharedString, - description: SharedString, - args: Option, - 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 for SlashCommandEntry { - fn as_ref(&self) -> &str { - match self { - SlashCommandEntry::Info(SlashCommandInfo { name, .. }) - | SlashCommandEntry::Advert { name, .. } => name, - } - } -} - -pub(crate) struct SlashCommandDelegate { - all_commands: Vec, - filtered_commands: Vec, - active_context_editor: WeakEntity, - selected_index: usize, -} - -impl SlashCommandSelector -where - T: PopoverTrigger + ButtonCommon, - TT: Fn(&mut Window, &mut App) -> AnyView + 'static, -{ - pub(crate) fn new( - working_set: Arc, - active_context_editor: WeakEntity, - 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>) { - 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 { - "Select a command...".into() - } - - fn update_matches( - &mut self, - query: String, - window: &mut Window, - cx: &mut Context>, - ) -> 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 { - 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>) { - 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>) {} - - fn editor_position(&self) -> PickerEditorPosition { - PickerEditorPosition::End - } - - fn render_match( - &self, - ix: usize, - selected: bool, - window: &mut Window, - cx: &mut Context>, - ) -> Option { - 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 RenderOnce for SlashCommandSelector -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::>(); - - 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)) - } -} diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs deleted file mode 100644 index 180a31edde29b7ef78ee263a437458abd5affafc..0000000000000000000000000000000000000000 --- a/crates/agent_ui/src/text_thread_editor.rs +++ /dev/null @@ -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), - ExternalFiles(Vec), -} - -#[derive(Copy, Clone, Debug, PartialEq)] -struct ScrollPosition { - offset_before_cursor: gpui::Point, - 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, - ) -> Option>; - - fn open_local_text_thread( - &self, - workspace: &mut Workspace, - path: Arc, - window: &mut Window, - cx: &mut Context, - ) -> Task>; - - fn open_remote_text_thread( - &self, - workspace: &mut Workspace, - text_thread_id: TextThreadId, - window: &mut Window, - cx: &mut Context, - ) -> Task>>; - - fn quote_selection( - &self, - workspace: &mut Workspace, - selection_ranges: Vec>, - buffer: Entity, - window: &mut Window, - cx: &mut Context, - ); - - fn quote_terminal_text( - &self, - workspace: &mut Workspace, - text: String, - window: &mut Window, - cx: &mut Context, - ); -} - -impl dyn AgentPanelDelegate { - /// Returns the global [`AssistantPanelDelegate`], if it exists. - pub fn try_global(cx: &App) -> Option> { - cx.try_global::() - .map(|global| global.0.clone()) - } - - /// Sets the global [`AssistantPanelDelegate`]. - pub fn set_global(delegate: Arc, cx: &mut App) { - cx.set_global(GlobalAssistantPanelDelegate(delegate)); - } -} - -struct GlobalAssistantPanelDelegate(Arc); - -impl Global for GlobalAssistantPanelDelegate {} - -pub struct TextThreadEditor { - text_thread: Entity, - fs: Arc, - slash_commands: Arc, - workspace: WeakEntity, - project: Entity, - lsp_adapter_delegate: Option>, - editor: Entity, - pending_thought_process: Option<(CreaseId, language::Anchor)>, - blocks: HashMap, - image_blocks: HashSet, - scroll_position: Option, - remote_id: Option, - pending_slash_command_creases: HashMap, CreaseId>, - invoked_slash_command_creases: HashMap, - _subscriptions: Vec, - last_error: Option, - pub(crate) slash_menu_handle: - PopoverMenuHandle>, - // 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>, - language_model_selector: Entity, - language_model_selector_menu_handle: PopoverMenuHandle, -} - -const MAX_TAB_TITLE_LEN: usize = 16; - -impl TextThreadEditor { - pub fn init(cx: &mut App) { - workspace::FollowableViewRegistry::register::(cx); - - cx.observe_new( - |workspace: &mut Workspace, _window, _cx: &mut Context| { - 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, - fs: Arc, - workspace: WeakEntity, - project: Entity, - lsp_adapter_delegate: Option>, - window: &mut Window, - cx: &mut Context, - ) -> 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::(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.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 { - &self.text_thread - } - - pub fn editor(&self) -> &Entity { - &self.editor - } - - pub fn insert_default_prompt(&mut self, window: &mut Window, cx: &mut Context) { - 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) { - 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.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.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, - ) { - 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 { - let selections = self.editor.update(cx, |editor, cx| { - editor - .selections - .all::(&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) { - 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::(&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, - ) { - 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, - name: &str, - arguments: &[String], - ensure_trailing_newline: bool, - workspace: WeakEntity, - window: &mut Window, - cx: &mut Context, - ) { - 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::>(); - 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, - event: &TextThreadEvent, - window: &mut Window, - cx: &mut Context, - ) { - 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, - ) { - 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::(), - 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, - ThoughtProcessStatus, - ), - >, - window: &mut Window, - cx: &mut Context, - ) -> Vec { - 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>, - expand_result: bool, - window: &mut Window, - cx: &mut Context, - ) { - 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, - event: &EditorEvent, - window: &mut Window, - cx: &mut Context, - ) { - 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, - event: &SearchEvent, - _window: &mut Window, - cx: &mut Context, - ) { - cx.emit(event.clone()); - } - - fn cursor_scroll_position( - &self, - window: &mut Window, - cx: &mut Context, - ) -> Option { - 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.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, - cx: &mut Context, - ) -> 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::(&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::(&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::(); - - // 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::(); - - (!selected_text.is_empty()).then_some((selected_text, false)) - } - }) - } - - pub fn insert_selection( - workspace: &mut Workspace, - _: &InsertIntoEditor, - window: &mut Window, - cx: &mut Context, - ) { - let Some(agent_panel_delegate) = ::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::(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, - ) { - let result = maybe!({ - let agent_panel_delegate = ::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::(), - 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, - ) { - let Some(agent_panel_delegate) = ::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::>(); - - 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, - added_worktrees: Vec>, - window: &mut Window, - cx: &mut Context, - ) { - 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, - ) { - let Some(agent_panel_delegate) = ::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::(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::>() - }); - Some((selections, buffer)) - }) { - agent_panel_delegate.quote_selection(workspace, selections, buffer, window, cx); - } - } - - pub fn quote_ranges( - &mut self, - ranges: Vec>, - snapshot: MultiBufferSnapshot, - window: &mut Window, - cx: &mut Context, - ) { - 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::(&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, - ) { - 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::(&editor.display_snapshot(cx)) - .head(); - if point.column > 0 { - editor.insert("\n", window, cx); - } - - let point = editor - .selections - .newest::(&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) { - 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) { - 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, - ) -> ( - String, - CopyMetadata, - Vec>, - ) { - 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::>() - }), - ) - }); - - 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, - ) { - 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::>() - } - _ => 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::(&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 = Vec::new(); - let mut paths: Vec = Vec::new(); - let mut metadata: Option = 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::(); - } - } - } - } - } - - 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::(&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::(&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::(&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.editor.update(cx, |editor, cx| { - editor.paste(&editor::actions::Paste, window, cx); - }); - } - - fn update_image_blocks(&mut self, cx: &mut Context) { - 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::>(); - - 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.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.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.text_thread - .update(cx, |text_thread, cx| text_thread.summarize(true, cx)); - } - - fn render_remaining_tokens(&self, cx: &App) -> Option> { - 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) -> 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) -> 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, - ) -> 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) -> Option { - 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) -> 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, - ) -> 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> { - 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, - status: ThoughtProcessStatus, -) -> Arc, &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, - icon_path: SharedString, - label: SharedString, -) -> Arc, &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; - -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, - &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) -> 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, -) -> 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, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct SelectedCreaseMetadata { - range_relative_to_selection: Range, - crease: CreaseMetadata, -} - -impl EventEmitter for TextThreadEditor {} -impl EventEmitter for TextThreadEditor {} - -impl Render for TextThreadEditor { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> 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 { - Some(self.title(cx).to_string().into()) - } - - fn as_searchable( - &self, - handle: &Entity, - _: &App, - ) -> Option> { - Some(Box::new(handle.clone())) - } - - fn set_nav_history( - &mut self, - nav_history: pane::ItemNavHistory, - window: &mut Window, - cx: &mut Context, - ) { - self.editor.update(cx, |editor, cx| { - Item::set_nav_history(editor, nav_history, window, cx) - }) - } - - fn navigate( - &mut self, - data: Arc, - window: &mut Window, - cx: &mut Context, - ) -> bool { - self.editor - .update(cx, |editor, cx| Item::navigate(editor, data, window, cx)) - } - - fn deactivated(&mut self, window: &mut Window, cx: &mut Context) { - 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, - _: &'a App, - ) -> Option { - if type_id == TypeId::of::() { - Some(self_handle.clone().into()) - } else if type_id == TypeId::of::() { - Some(self.editor.clone().into()) - } else { - None - } - } - - fn include_in_nav_history() -> bool { - false - } -} - -impl SearchableItem for TextThreadEditor { - type Match = ::Match; - - fn clear_matches(&mut self, window: &mut Window, cx: &mut Context) { - self.editor.update(cx, |editor, cx| { - editor.clear_matches(window, cx); - }); - } - - fn update_matches( - &mut self, - matches: &[Self::Match], - active_match_index: Option, - token: SearchToken, - window: &mut Window, - cx: &mut Context, - ) { - 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) -> 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.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.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.editor.update(cx, |editor, cx| { - editor.replace(identifier, query, token, window, cx) - }); - } - - fn find_matches( - &mut self, - query: Arc, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - 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, - ) -> Option { - self.editor.update(cx, |editor, cx| { - editor.active_match_index(direction, matches, token, window, cx) - }) - } -} - -impl FollowableItem for TextThreadEditor { - fn remote_id(&self) -> Option { - self.remote_id - } - - fn to_state_proto(&self, window: &mut Window, cx: &mut App) -> Option { - 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, - id: workspace::ViewId, - state: &mut Option, - window: &mut Window, - cx: &mut App, - ) -> Option>>> { - 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 = ::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 { - Editor::to_follow_event(event) - } - - fn add_event_to_update_proto( - &self, - event: &Self::Event, - update: &mut Option, - 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, - message: proto::update_view::Variant, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - 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, - window: &mut Window, - cx: &mut Context, - ) { - self.editor - .update(cx, |editor, cx| editor.set_leader_id(leader_id, window, cx)) - } - - fn dedup(&self, existing: &Self, _window: &Window, cx: &App) -> Option { - 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, -) -> 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::()), - } -} - -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, cx: &App) -> Option { - 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) -> Size { - 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, - cx: &mut App, -) -> Result>> { - project.update(cx, |project, cx| { - // TODO: Find the right worktree. - let Some(worktree) = project.worktrees(cx).next() else { - return Ok(None::>); - }; - 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)) - }) - }) -} - -#[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::>(); - - 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, - Entity, - 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, - message_ix: usize, - cx: &mut TestAppContext, - ) -> Range { - 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( - text_thread_editor: &Entity, - range: Range, - 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 { - 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 - ); - }); - }); - } -} diff --git a/crates/agent_ui/src/text_thread_history.rs b/crates/agent_ui/src/text_thread_history.rs deleted file mode 100644 index 7a2a4ff91ddae0531df200118b55151a8dbb4499..0000000000000000000000000000000000000000 --- a/crates/agent_ui/src/text_thread_history.rs +++ /dev/null @@ -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, - scroll_handle: UniformListScrollHandle, - selected_index: usize, - hovered_index: Option, - search_editor: Entity, - search_query: SharedString, - visible_items: Vec, - local_timezone: UtcOffset, - confirming_delete_history: bool, - _update_task: Task<()>, - _subscriptions: Vec, -} - -enum ListItemType { - BucketSeparator(TimeBucket), - Entry { - entry: SavedTextThreadMetadata, - format: EntryTimeFormat, - }, - SearchResult { - entry: SavedTextThreadMetadata, - positions: Vec, - }, -} - -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 for TextThreadHistory {} - -impl TextThreadHistory { - pub(crate) fn new( - text_thread_store: Entity, - window: &mut Window, - cx: &mut Context, - ) -> 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) { - let entries = self.text_thread_store.update(cx, |store, _| { - store.ordered_text_threads().cloned().collect::>() - }); - - 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, - cx: &App, - ) -> Task> { - 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, - cx: &App, - ) -> Task> { - 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) { - 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) { - 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, - ) { - 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.set_selected_index(0, Bias::Right, cx); - } - - fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context) { - 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.confirm_entry(self.selected_index, cx); - } - - fn confirm_entry(&mut self, ix: usize, cx: &mut Context) { - 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.remove_thread(self.selected_index, cx) - } - - fn remove_thread(&mut self, visible_item_ix: usize, cx: &mut Context) { - 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.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.confirming_delete_history = true; - cx.notify(); - } - - fn cancel_delete_history(&mut self, _window: &mut Window, cx: &mut Context) { - self.confirming_delete_history = false; - cx.notify(); - } - - fn render_list_items( - &mut self, - range: Range, - _window: &mut Window, - cx: &mut Context, - ) -> Vec { - 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) -> 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, - cx: &Context, - ) -> 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::(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) -> 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, 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 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); - } -} diff --git a/crates/agent_ui/src/ui/acp_onboarding_modal.rs b/crates/agent_ui/src/ui/acp_onboarding_modal.rs index cbb9ddd0e6dd0338a717f1606dae9754543f21f9..a771269ca12bb0d68aacdb93ff7bda6cbc6a0c89 100644 --- a/crates/agent_ui/src/ui/acp_onboarding_modal.rs +++ b/crates/agent_ui/src/ui/acp_onboarding_modal.rs @@ -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::(cx) { panel.update(cx, |panel, cx| { panel.new_agent_thread( - AgentType::Custom { + Agent::Custom { id: GEMINI_ID.into(), }, window, diff --git a/crates/agent_ui/src/ui/claude_agent_onboarding_modal.rs b/crates/agent_ui/src/ui/claude_agent_onboarding_modal.rs index 6bc74e4d8a29122c6a5facbc22c88df282313ac3..5b7e58eb4fd79a5075446dad997c2642fedf32a6 100644 --- a/crates/agent_ui/src/ui/claude_agent_onboarding_modal.rs +++ b/crates/agent_ui/src/ui/claude_agent_onboarding_modal.rs @@ -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::(cx) { panel.update(cx, |panel, cx| { panel.new_agent_thread( - AgentType::Custom { + Agent::Custom { id: CLAUDE_AGENT_ID.into(), }, window, diff --git a/crates/agent_ui/src/ui/mention_crease.rs b/crates/agent_ui/src/ui/mention_crease.rs index 91200684d7ca1891578bb70fd6db65b2885aed93..6e99647304d93fe91cd6b91dbd2bf3bfd82c7ab0 100644 --- a/crates/agent_ui/src/ui/mention_crease.rs +++ b/crates/agent_ui/src/ui/mention_crease.rs @@ -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); } diff --git a/crates/assistant_slash_command/Cargo.toml b/crates/assistant_slash_command/Cargo.toml deleted file mode 100644 index 1fc3e8448c5e2d0c278254b369ac49fd2e9ce33a..0000000000000000000000000000000000000000 --- a/crates/assistant_slash_command/Cargo.toml +++ /dev/null @@ -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"] } diff --git a/crates/assistant_slash_command/LICENSE-GPL b/crates/assistant_slash_command/LICENSE-GPL deleted file mode 120000 index 89e542f750cd3860a0598eff0dc34b56d7336dc4..0000000000000000000000000000000000000000 --- a/crates/assistant_slash_command/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/assistant_slash_command/src/assistant_slash_command.rs b/crates/assistant_slash_command/src/assistant_slash_command.rs deleted file mode 100644 index 883f2f98c2693be5ae915cdab53ea94786222341..0000000000000000000000000000000000000000 --- a/crates/assistant_slash_command/src/assistant_slash_command.rs +++ /dev/null @@ -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 -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 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>>; - -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, - arguments: &[String], - cancel: Arc, - workspace: Option>, - window: &mut Window, - cx: &mut App, - ) -> Task>>; - fn requires_argument(&self) -> bool; - fn accepts_arguments(&self) -> bool { - self.requires_argument() - } - fn run( - self: Arc, - arguments: &[String], - context_slash_command_output_sections: &[SlashCommandOutputSection], - context_buffer: BufferSnapshot, - workspace: WeakEntity, - // 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>, - window: &mut Window, - cx: &mut App, - ) -> Task; -} - -#[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, - }, - Content(SlashCommandContent), - EndSection, -} - -#[derive(Debug, Default, PartialEq, Clone)] -pub struct SlashCommandOutput { - pub text: String, - pub sections: Vec>, - 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> { - 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>, - ) -> Result { - 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 { - pub range: Range, - #[serde(deserialize_with = "deserialize_icon_with_fallback")] - pub icon: IconName, - pub label: SharedString, - pub metadata: Option, -} - -impl SlashCommandOutputSection { - 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, - /// Ranges within the line containing the command arguments. - pub arguments: Vec>, -} - -impl SlashCommandLine { - pub fn parse(line: &str) -> Option { - let mut call: Option = 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::>().await; - let events = events - .into_iter() - .filter_map(|event| event.ok()) - .collect::>(); - - 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::>().await; - let events = events - .into_iter() - .filter_map(|event| event.ok()) - .collect::>(); - - 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::>().await; - let events = events - .into_iter() - .filter_map(|event| event.ok()) - .collect::>(); - - 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 = 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 = serde_json::from_value(json).unwrap(); - assert_eq!(section.icon, IconName::Code); - } -} diff --git a/crates/assistant_slash_command/src/extension_slash_command.rs b/crates/assistant_slash_command/src/extension_slash_command.rs deleted file mode 100644 index 6dd2c05f192358f9fcd843add21df94e301dc6b7..0000000000000000000000000000000000000000 --- a/crates/assistant_slash_command/src/extension_slash_command.rs +++ /dev/null @@ -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, -} - -impl ExtensionSlashCommandProxy for SlashCommandRegistryProxy { - fn register_slash_command( - &self, - extension: Arc, - command: extension::SlashCommand, - ) { - self.slash_command_registry - .register_command(ExtensionSlashCommand::new(extension, command), false) - } - - fn unregister_slash_command(&self, command_name: Arc) { - 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); - -#[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 { - self.0.read_text_file(path).await - } - - async fn which(&self, binary_name: String) -> Option { - 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, - command: extension::SlashCommand, -} - -impl ExtensionSlashCommand { - pub fn new(extension: Arc, 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, - arguments: &[String], - _cancel: Arc, - _workspace: Option>, - _window: &mut Window, - cx: &mut App, - ) -> Task>> { - 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, - arguments: &[String], - _context_slash_command_output_sections: &[SlashCommandOutputSection], - _context_buffer: BufferSnapshot, - _workspace: WeakEntity, - delegate: Option>, - _window: &mut Window, - cx: &mut App, - ) -> Task { - 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()) - }) - } -} diff --git a/crates/assistant_slash_command/src/slash_command_registry.rs b/crates/assistant_slash_command/src/slash_command_registry.rs deleted file mode 100644 index 258869840b583e85413f912c02693f100da2b401..0000000000000000000000000000000000000000 --- a/crates/assistant_slash_command/src/slash_command_registry.rs +++ /dev/null @@ -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); - -impl Global for GlobalSlashCommandRegistry {} - -#[derive(Default)] -struct SlashCommandRegistryState { - commands: HashMap, Arc>, - featured_commands: BTreeSet>, -} - -#[derive(Default)] -pub struct SlashCommandRegistry { - state: RwLock, -} - -impl SlashCommandRegistry { - /// Returns the global [`SlashCommandRegistry`]. - pub fn global(cx: &App) -> Arc { - 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 { - cx.default_global::().0.clone() - } - - pub fn new() -> Arc { - 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 = 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> { - self.state.read().commands.keys().cloned().collect() - } - - /// Returns the names of registered, featured [`SlashCommand`]s. - pub fn featured_command_names(&self) -> Vec> { - self.state - .read() - .featured_commands - .iter() - .cloned() - .collect() - } - - /// Returns the [`SlashCommand`] with the given name. - pub fn command(&self, name: &str) -> Option> { - self.state.read().commands.get(name).cloned() - } -} diff --git a/crates/assistant_slash_command/src/slash_command_working_set.rs b/crates/assistant_slash_command/src/slash_command_working_set.rs deleted file mode 100644 index b920e1d2177b46e938ae1043b624cf10270c7f94..0000000000000000000000000000000000000000 --- a/crates/assistant_slash_command/src/slash_command_working_set.rs +++ /dev/null @@ -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, -} - -#[derive(Default)] -struct WorkingSetState { - context_server_commands_by_id: HashMap>, - context_server_commands_by_name: HashMap, Arc>, - next_command_id: SlashCommandId, -} - -impl SlashCommandWorkingSet { - pub fn command(&self, name: &str, cx: &App) -> Option> { - 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> { - 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> { - SlashCommandRegistry::global(cx).featured_command_names() - } - - pub fn insert(&self, command: Arc) -> 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())), - ); - } -} diff --git a/crates/assistant_slash_commands/Cargo.toml b/crates/assistant_slash_commands/Cargo.toml deleted file mode 100644 index bc156712f7ee98d8780e74a1510a9d209ffded53..0000000000000000000000000000000000000000 --- a/crates/assistant_slash_commands/Cargo.toml +++ /dev/null @@ -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 diff --git a/crates/assistant_slash_commands/LICENSE-GPL b/crates/assistant_slash_commands/LICENSE-GPL deleted file mode 120000 index 89e542f750cd3860a0598eff0dc34b56d7336dc4..0000000000000000000000000000000000000000 --- a/crates/assistant_slash_commands/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/assistant_slash_commands/src/assistant_slash_commands.rs b/crates/assistant_slash_commands/src/assistant_slash_commands.rs deleted file mode 100644 index bfb3784ffcc1e76680724d09586c1674df09016b..0000000000000000000000000000000000000000 --- a/crates/assistant_slash_commands/src/assistant_slash_commands.rs +++ /dev/null @@ -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; diff --git a/crates/assistant_slash_commands/src/default_command.rs b/crates/assistant_slash_commands/src/default_command.rs deleted file mode 100644 index 4ded6c846cf8e6f006bbf24497c5368b61c7a742..0000000000000000000000000000000000000000 --- a/crates/assistant_slash_commands/src/default_command.rs +++ /dev/null @@ -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, - _arguments: &[String], - _cancellation_flag: Arc, - _workspace: Option>, - _window: &mut Window, - _cx: &mut App, - ) -> Task>> { - Task::ready(Err(anyhow!("this command does not require argument"))) - } - - fn run( - self: Arc, - _arguments: &[String], - _context_slash_command_output_sections: &[SlashCommandOutputSection], - _context_buffer: BufferSnapshot, - _workspace: WeakEntity, - _delegate: Option>, - _window: &mut Window, - cx: &mut App, - ) -> Task { - 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()) - }) - } -} diff --git a/crates/assistant_slash_commands/src/delta_command.rs b/crates/assistant_slash_commands/src/delta_command.rs deleted file mode 100644 index ea05fca588d0a496eeb3a2d2128b3861ba8a1e30..0000000000000000000000000000000000000000 --- a/crates/assistant_slash_commands/src/delta_command.rs +++ /dev/null @@ -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, - _arguments: &[String], - _cancellation_flag: Arc, - _workspace: Option>, - _window: &mut Window, - _cx: &mut App, - ) -> Task>> { - Task::ready(Err(anyhow!("this command does not require argument"))) - } - - fn run( - self: Arc, - _arguments: &[String], - context_slash_command_output_sections: &[SlashCommandOutputSection], - context_buffer: BufferSnapshot, - workspace: WeakEntity, - delegate: Option>, - window: &mut Window, - cx: &mut App, - ) -> Task { - 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::(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()) - }) - } -} diff --git a/crates/assistant_slash_commands/src/diagnostics_command.rs b/crates/assistant_slash_commands/src/diagnostics_command.rs deleted file mode 100644 index fcede522d020340fa382b6a6baed9b6c983a3029..0000000000000000000000000000000000000000 --- a/crates/assistant_slash_commands/src/diagnostics_command.rs +++ /dev/null @@ -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, - workspace: &Entity, - cx: &mut App, - ) -> Task> { - if query.is_empty() { - let workspace = workspace.read(cx); - let entries = workspace.recent_navigation_history(Some(10), cx); - let path_prefix: Arc = 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::>(); - 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::>(); - - 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, - arguments: &[String], - cancellation_flag: Arc, - workspace: Option>, - _: &mut Window, - cx: &mut App, - ) -> Task>> { - 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 = 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, - arguments: &[String], - _context_slash_command_output_sections: &[SlashCommandOutputSection], - _context_buffer: BufferSnapshot, - workspace: WeakEntity, - _delegate: Option>, - window: &mut Window, - cx: &mut App, - ) -> Task { - 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, -} - -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, - options: Options, - cx: &mut App, -) -> Task>> { - 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, - }); -} diff --git a/crates/assistant_slash_commands/src/fetch_command.rs b/crates/assistant_slash_commands/src/fetch_command.rs deleted file mode 100644 index 6d3f66c9a23c896c765ba6c0a43b7a99dbc7ee73..0000000000000000000000000000000000000000 --- a/crates/assistant_slash_commands/src/fetch_command.rs +++ /dev/null @@ -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, url: &str) -> Result { - 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 = 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, - _arguments: &[String], - _cancel: Arc, - _workspace: Option>, - _window: &mut Window, - _cx: &mut App, - ) -> Task>> { - Task::ready(Ok(Vec::new())) - } - - fn run( - self: Arc, - arguments: &[String], - _context_slash_command_output_sections: &[SlashCommandOutputSection], - _context_buffer: BufferSnapshot, - workspace: WeakEntity, - _delegate: Option>, - _: &mut Window, - cx: &mut App, - ) -> Task { - 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()) - }) - } -} diff --git a/crates/assistant_slash_commands/src/file_command.rs b/crates/assistant_slash_commands/src/file_command.rs deleted file mode 100644 index ff6514e3359a5d6d8c569079b8e0e7a88a080e77..0000000000000000000000000000000000000000 --- a/crates/assistant_slash_commands/src/file_command.rs +++ /dev/null @@ -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, - workspace: &Entity, - cx: &mut App, - ) -> Task> { - 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::>(); - - let path_prefix: Arc = 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::>(); - 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::>(); - - 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, - arguments: &[String], - cancellation_flag: Arc, - workspace: Option>, - _: &mut Window, - cx: &mut App, - ) -> Task>> { - 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, - arguments: &[String], - _context_slash_command_output_sections: &[SlashCommandOutputSection], - _context_buffer: BufferSnapshot, - workspace: WeakEntity, - _delegate: Option>, - _: &mut Window, - cx: &mut App, - ) -> Task { - 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, - glob_inputs: &[String], - cx: &mut App, -) -> impl Stream> + 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::>>() - 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::>(); - - 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> = Vec::new(); - let mut folded_directory_path: Option> = None; - let mut folded_directory_names: Arc = 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>, -) -> 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, - path: Option<&str>, - is_directory: bool, - line_range: Option>, -) -> SlashCommandOutputSection { - 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); - } -} diff --git a/crates/assistant_slash_commands/src/now_command.rs b/crates/assistant_slash_commands/src/now_command.rs deleted file mode 100644 index aec21e7173bafd4cb07e7c37135fa0ad6fa88812..0000000000000000000000000000000000000000 --- a/crates/assistant_slash_commands/src/now_command.rs +++ /dev/null @@ -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, - _arguments: &[String], - _cancel: Arc, - _workspace: Option>, - _window: &mut Window, - _cx: &mut App, - ) -> Task>> { - Task::ready(Ok(Vec::new())) - } - - fn run( - self: Arc, - _arguments: &[String], - _context_slash_command_output_sections: &[SlashCommandOutputSection], - _context_buffer: BufferSnapshot, - _workspace: WeakEntity, - _delegate: Option>, - _window: &mut Window, - _cx: &mut App, - ) -> Task { - 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())) - } -} diff --git a/crates/assistant_slash_commands/src/prompt_command.rs b/crates/assistant_slash_commands/src/prompt_command.rs deleted file mode 100644 index 961dd266ad6bd8e91ccd04f3e74ab9534750f37b..0000000000000000000000000000000000000000 --- a/crates/assistant_slash_commands/src/prompt_command.rs +++ /dev/null @@ -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, - arguments: &[String], - _cancellation_flag: Arc, - _workspace: Option>, - _: &mut Window, - cx: &mut App, - ) -> Task>> { - 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 = 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, - arguments: &[String], - _context_slash_command_output_sections: &[SlashCommandOutputSection], - _context_buffer: BufferSnapshot, - _workspace: WeakEntity, - _delegate: Option>, - _: &mut Window, - cx: &mut App, - ) -> Task { - 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()) - }) - } -} diff --git a/crates/assistant_slash_commands/src/selection_command.rs b/crates/assistant_slash_commands/src/selection_command.rs deleted file mode 100644 index 98d66b7b0b21b265813ce2671e0628cde72ee4ea..0000000000000000000000000000000000000000 --- a/crates/assistant_slash_commands/src/selection_command.rs +++ /dev/null @@ -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, - _arguments: &[String], - _cancel: Arc, - _workspace: Option>, - _window: &mut Window, - _cx: &mut App, - ) -> Task>> { - Task::ready(Err(anyhow!("this command does not require argument"))) - } - - fn run( - self: Arc, - _arguments: &[String], - _context_slash_command_output_sections: &[SlashCommandOutputSection], - _context_buffer: BufferSnapshot, - workspace: WeakEntity, - _delegate: Option>, - _window: &mut Window, - cx: &mut App, - ) -> Task { - let mut events = vec![]; - - let Some(creases) = workspace - .update(cx, |workspace, cx| { - let editor = workspace - .active_item(cx) - .and_then(|item| item.act_as::(cx))?; - - editor.update(cx, |editor, cx| { - let selection_ranges = editor - .selections - .all_adjusted(&editor.display_snapshot(cx)) - .iter() - .map(|selection| selection.range()) - .collect::>(); - 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>, - 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, - 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::>() - .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::>() - .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, - snapshot: &MultiBufferSnapshot, - cx: &App, -) -> Option<(String, String)> { - let selected_text = snapshot.text_for_range(range.clone()).collect::(); - 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::>() - .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::>() - .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")); - } -} diff --git a/crates/assistant_slash_commands/src/streaming_example_command.rs b/crates/assistant_slash_commands/src/streaming_example_command.rs deleted file mode 100644 index e39fe55c268b4ccdb1fc412a9403f349e4f19fc3..0000000000000000000000000000000000000000 --- a/crates/assistant_slash_commands/src/streaming_example_command.rs +++ /dev/null @@ -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, - _arguments: &[String], - _cancel: Arc, - _workspace: Option>, - _window: &mut Window, - _cx: &mut App, - ) -> Task>> { - Task::ready(Ok(Vec::new())) - } - - fn run( - self: Arc, - _arguments: &[String], - _context_slash_command_output_sections: &[SlashCommandOutputSection], - _context_buffer: BufferSnapshot, - _workspace: WeakEntity, - _delegate: Option>, - _: &mut Window, - cx: &mut App, - ) -> Task { - 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())) - } -} diff --git a/crates/assistant_slash_commands/src/symbols_command.rs b/crates/assistant_slash_commands/src/symbols_command.rs deleted file mode 100644 index c537ced2966bf0bfde912ba326890a935a20f220..0000000000000000000000000000000000000000 --- a/crates/assistant_slash_commands/src/symbols_command.rs +++ /dev/null @@ -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, - _arguments: &[String], - _cancel: Arc, - _workspace: Option>, - _window: &mut Window, - _cx: &mut App, - ) -> Task>> { - Task::ready(Err(anyhow!("this command does not require argument"))) - } - - fn requires_argument(&self) -> bool { - false - } - - fn run( - self: Arc, - _arguments: &[String], - _context_slash_command_output_sections: &[SlashCommandOutputSection], - _context_buffer: BufferSnapshot, - workspace: WeakEntity, - _delegate: Option>, - _: &mut Window, - cx: &mut App, - ) -> Task { - 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::() - .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))) - } -} diff --git a/crates/assistant_slash_commands/src/tab_command.rs b/crates/assistant_slash_commands/src/tab_command.rs deleted file mode 100644 index a4c0ad412cca3eaf7d03d684cc3fb828be60a93d..0000000000000000000000000000000000000000 --- a/crates/assistant_slash_commands/src/tab_command.rs +++ /dev/null @@ -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, - arguments: &[String], - cancel: Arc, - workspace: Option>, - window: &mut Window, - cx: &mut App, - ) -> Task>> { - 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::>(); - 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, - arguments: &[String], - _context_slash_command_output_sections: &[SlashCommandOutputSection], - _context_buffer: BufferSnapshot, - workspace: WeakEntity, - _delegate: Option>, - window: &mut Window, - cx: &mut App, - ) -> Task { - 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>, - queries: &[String], - cancel: Arc, - strict_match: bool, - window: &mut Window, - cx: &mut App, -) -> Task, 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::(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::>(); - 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, -) -> anyhow::Result { - let active_editor = workspace - .active_item(cx) - .context("no active item")? - .downcast::() - .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, -) -> 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() -} diff --git a/crates/assistant_text_thread/Cargo.toml b/crates/assistant_text_thread/Cargo.toml deleted file mode 100644 index 195fa34f4f0379248a189ee59fcf779d18bc09c9..0000000000000000000000000000000000000000 --- a/crates/assistant_text_thread/Cargo.toml +++ /dev/null @@ -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"] } diff --git a/crates/assistant_text_thread/LICENSE-GPL b/crates/assistant_text_thread/LICENSE-GPL deleted file mode 120000 index 89e542f750cd3860a0598eff0dc34b56d7336dc4..0000000000000000000000000000000000000000 --- a/crates/assistant_text_thread/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/assistant_text_thread/src/assistant_text_thread.rs b/crates/assistant_text_thread/src/assistant_text_thread.rs deleted file mode 100644 index 6b0602121fe460e26eb13f8d1a186f26a434df26..0000000000000000000000000000000000000000 --- a/crates/assistant_text_thread/src/assistant_text_thread.rs +++ /dev/null @@ -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, _: &mut App) { - text_thread_store::init(&client.into()); -} diff --git a/crates/assistant_text_thread/src/assistant_text_thread_tests.rs b/crates/assistant_text_thread/src/assistant_text_thread_tests.rs deleted file mode 100644 index c4f1688dd0183bdfc81ed284f0a3e2681e8e4582..0000000000000000000000000000000000000000 --- a/crates/assistant_text_thread/src/assistant_text_thread_tests.rs +++ /dev/null @@ -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, - offsets: &[usize], - cx: &App, - ) -> Vec { - 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>, - command_outputs: HashMap>, - output_sections: HashSet>, - } - - 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, - ranges: &RefCell, - 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::(); - - 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::>>() - .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::>>() - .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![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![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![ - 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![ - 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![ - 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, - text_thread: &Entity, - 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, Arc) { - 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, cx: &App) -> Vec<(MessageId, Role, Range)> { - context - .read(cx) - .messages(cx) - .map(|message| (message.id, message.role, message.offset_range)) - .collect() -} - -fn messages_cache( - context: &Entity, - cx: &App, -) -> Vec<(MessageId, Option)> { - 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, - _arguments: &[String], - _cancel: Arc, - _workspace: Option>, - _window: &mut Window, - _cx: &mut App, - ) -> Task>> { - Task::ready(Ok(vec![])) - } - - fn requires_argument(&self) -> bool { - false - } - - fn run( - self: Arc, - _arguments: &[String], - _context_slash_command_output_sections: &[SlashCommandOutputSection], - _context_buffer: BufferSnapshot, - _workspace: WeakEntity, - _delegate: Option>, - _window: &mut Window, - _cx: &mut App, - ) -> Task { - Task::ready(Ok(SlashCommandOutput { - text: format!("Executed fake command: {}", self.0), - sections: vec![], - run_commands_in_text: false, - } - .into_event_stream())) - } -} diff --git a/crates/assistant_text_thread/src/context_server_command.rs b/crates/assistant_text_thread/src/context_server_command.rs deleted file mode 100644 index 55e5664f7aef67591e0f53c2fa670f41b1143f98..0000000000000000000000000000000000000000 --- a/crates/assistant_text_thread/src/context_server_command.rs +++ /dev/null @@ -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, - server_id: ContextServerId, - prompt: Prompt, -} - -impl ContextServerSlashCommand { - pub fn new(store: Entity, 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, - arguments: &[String], - _cancel: Arc, - _workspace: Option>, - _window: &mut Window, - cx: &mut App, - ) -> Task>> { - 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::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, - arguments: &[String], - _context_slash_command_output_sections: &[SlashCommandOutputSection], - _context_buffer: BufferSnapshot, - _workspace: WeakEntity, - _delegate: Option>, - _window: &mut Window, - cx: &mut App, - ) -> Task { - 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::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::>() - .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> { - 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, - } -} diff --git a/crates/assistant_text_thread/src/text_thread.rs b/crates/assistant_text_thread/src/text_thread.rs deleted file mode 100644 index 8b2fdc6187af9e525e07c54d3cbd08f32261f734..0000000000000000000000000000000000000000 --- a/crates/assistant_text_thread/src/text_thread.rs +++ /dev/null @@ -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, - name: String, - version: clock::Global, - }, - SlashCommandFinished { - id: InvokedSlashCommandId, - timestamp: clock::Lamport, - error_message: Option, - version: clock::Global, - }, - SlashCommandOutputSectionAdded { - timestamp: clock::Lamport, - section: SlashCommandOutputSection, - version: clock::Global, - }, - ThoughtProcessOutputSectionAdded { - timestamp: clock::Lamport, - section: ThoughtProcessOutputSection, - version: clock::Global, - }, - BufferOperation(language::Operation), -} - -impl TextThreadOperation { - pub fn from_proto(op: proto::ContextOperation) -> Result { - 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>, - new_path: Arc, - }, - StreamedCompletion, - StartedThoughtProcess(Range), - EndedThoughtProcess(language::Anchor), - InvokedSlashCommandChanged { - command_id: InvokedSlashCommandId, - }, - ParsedSlashCommandsUpdated { - removed: Vec>, - updated: Vec, - }, - SlashCommandOutputSectionAdded { - section: SlashCommandOutputSection, - }, - 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 { - 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 { - match self { - TextThreadSummary::Content(content) => Some(content.timestamp), - TextThreadSummary::Pending | TextThreadSummary::Error => None, - } - } -} - -impl PartialOrd for TextThreadSummary { - fn partial_cmp(&self, other: &Self) -> Option { - 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, -} - -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) -> 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 { - pub range: Range, -} - -impl ThoughtProcessOutputSection { - 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, - pub index_range: Range, - pub anchor_range: Range, - pub id: MessageId, - pub role: Role, - pub status: MessageStatus, - pub cache: Option, -} - -#[derive(Debug, Clone)] -pub enum Content { - Image { - anchor: language::Anchor, - image_id: u64, - render_image: Arc, - image: Shared>>, - }, -} - -impl Content { - fn range(&self) -> Range { - 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, - operations: Vec, - buffer: Entity, - pub(crate) parsed_slash_commands: Vec, - invoked_slash_commands: HashMap, - edits_since_last_parse: language::Subscription, - slash_commands: Arc, - pub(crate) slash_command_output_sections: Vec>, - thought_process_output_sections: Vec>, - pub(crate) message_anchors: Vec, - contents: Vec, - pub(crate) messages_metadata: HashMap, - summary: TextThreadSummary, - summary_task: Task>, - completion_count: usize, - pending_completions: Vec, - pub(crate) token_count: Option, - pending_token_count: Task>, - pending_save: Task>, - pending_cache_warming_task: Task>, - path: Option>, - _subscriptions: Vec, - language_registry: Arc, - prompt_builder: Arc, -} - -trait ContextAnnotation { - fn range(&self) -> &Range; -} - -impl ContextAnnotation for ParsedSlashCommand { - fn range(&self) -> &Range { - &self.source_range - } -} - -impl EventEmitter for TextThread {} - -impl TextThread { - pub fn local( - language_registry: Arc, - prompt_builder: Arc, - slash_commands: Arc, - cx: &mut Context, - ) -> 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, - prompt_builder: Arc, - slash_commands: Arc, - cx: &mut Context, - ) -> 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, - language_registry: Arc, - prompt_builder: Arc, - slash_commands: Arc, - cx: &mut Context, - ) -> 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 { - &self.slash_commands - } - - pub fn set_capability(&mut self, capability: language::Capability, cx: &mut Context) { - 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> { - 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::>(); - 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, - cx: &mut Context, - ) { - 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) { - 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, - 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.operations.push(op.clone()); - cx.emit(TextThreadEvent::Operation(op)); - } - - pub fn buffer(&self) -> &Entity { - &self.buffer - } - - pub fn language_registry(&self) -> Arc { - self.language_registry.clone() - } - - pub fn prompt_builder(&self) -> Arc { - self.prompt_builder.clone() - } - - pub fn path(&self) -> Option<&Arc> { - 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] { - &self.slash_command_output_sections - } - - pub fn thought_process_output_sections( - &self, - ) -> &[ThoughtProcessOutputSection] { - &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::(metadata.clone()).ok() - }) - .is_some() - }) - } - - fn set_language(&mut self, cx: &mut Context) { - 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, - event: &language::BufferEvent, - cx: &mut Context, - ) { - 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 { - self.token_count - } - - pub(crate) fn count_remaining_tokens(&mut self, cx: &mut Context) { - // 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, - speculative: bool, - cx: &mut Context, - ) -> 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 = 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 = sorted_messages - .into_iter() - .map(|message| message.id) - .collect(); - - let buffer = self.buffer.read(cx).snapshot(); - let invalidated_caches: HashSet = 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, cx: &mut Context) { - 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) { - let cached_message_ids: Vec = 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) { - 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, - buffer: &BufferSnapshot, - updated: &mut Vec, - removed: &mut Vec>, - 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::>(); - 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, - ) { - 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, - ) -> 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, - 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, - cx: &App, - ) -> Range { - self.indices_intersecting_buffer_range(&self.parsed_slash_commands, range, cx) - } - - fn indices_intersecting_buffer_range( - &self, - all_annotations: &[T], - range: Range, - cx: &App, - ) -> Range { - 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, - name: &str, - output: Task, - ensure_trailing_newline: bool, - cx: &mut Context, - ) { - 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, - } - - let mut pending_section_stack: Vec = Vec::new(); - let mut last_role: Option = 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, - cx: &mut Context, - ) { - 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, - cx: &mut Context, - ) { - 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.count_remaining_tokens(cx); - } - - fn get_last_valid_message_id(&self, cx: &Context) -> Option { - 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) -> Option { - 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 = "\n"; - const THOUGHT_PROCESS_END_MARKER: &str = "\n"; - - 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::() { - 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::>() - .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>, - cx: &App, - ) -> LanguageModelRequest { - let buffer = self.buffer.read(cx); - - let mut contents = self.contents(cx).peekable(); - - fn collect_text_content(buffer: &Buffer, range: Range) -> Option { - 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) -> 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, cx: &mut Context) { - 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, cx: &mut Context) { - 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, - 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, - ) -> Option { - 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, - ) -> 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) { - 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 { - 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, - cx: &mut Context, - ) -> (Option, Option) { - 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, - ) { - 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) { - 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 { - self.messages_for_offsets([offset], cx).pop() - } - - pub fn messages_for_offsets( - &self, - offsets: impl IntoIterator, - cx: &App, - ) -> Vec { - 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 + 'a, - cx: &'a App, - ) -> impl 'a + Iterator { - 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 { - self.messages_from_anchors(self.message_anchors.iter(), cx) - } - - pub fn messages_from_iters<'a>( - buffer: &'a Buffer, - metadata: &'a HashMap, - messages: impl Iterator + 'a, - ) -> impl 'a + Iterator { - 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, - fs: Arc, - cx: &mut Context, - ) { - 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 = 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) { - 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, -} - -#[derive(Debug)] -pub struct InvokedSlashCommand { - pub name: SharedString, - pub range: Range, - pub run_commands_in_ranges: Vec>, - pub status: InvokedSlashCommandStatus, - pub transaction: Option, - timestamp: clock::Lamport, -} - -#[derive(Debug)] -pub enum InvokedSlashCommandStatus { - Running(Task<()>), - Error(SharedString), - Finished, -} - -#[derive(Debug, Clone)] -pub enum PendingSlashCommandStatus { - Idle, - Running { _task: Shared> }, - 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, -} - -#[derive(Debug, Clone)] -pub enum PendingToolUseStatus { - Idle, - Running { _task: Shared> }, - 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, - pub zed: String, - pub version: String, - pub text: String, - pub messages: Vec, - pub summary: String, - pub slash_command_output_sections: - Vec>, - #[serde(default)] - pub thought_process_output_sections: Vec>, -} - -impl SavedTextThread { - pub const VERSION: &'static str = "0.4.0"; - - pub fn from_json(json: &str) -> Result { - let saved_context_json = serde_json::from_str::(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::( - saved_context_json, - )?), - SavedContextV0_3_0::VERSION => { - let saved_context = - serde_json::from_value::(saved_context_json)?; - Ok(saved_context.upgrade()) - } - SavedContextV0_2_0::VERSION => { - let saved_context = - serde_json::from_value::(saved_context_json)?; - Ok(saved_context.upgrade()) - } - SavedContextV0_1_0::VERSION => { - let saved_context = - serde_json::from_value::(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, - cx: &mut Context, - ) -> Vec { - 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, - zed: String, - version: String, - text: String, - messages: Vec, - message_metadata: HashMap, - summary: String, - slash_command_output_sections: Vec>, -} - -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, - zed: String, - version: String, - text: String, - messages: Vec, - message_metadata: HashMap, - 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, - zed: String, - version: String, - text: String, - messages: Vec, - message_metadata: HashMap, - summary: String, - api_url: Option, - 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, - pub mtime: chrono::DateTime, -} diff --git a/crates/assistant_text_thread/src/text_thread_store.rs b/crates/assistant_text_thread/src/text_thread_store.rs deleted file mode 100644 index 43fdf3a71aaaddc126ae2891734581dc47f83cd5..0000000000000000000000000000000000000000 --- a/crates/assistant_text_thread/src/text_thread_store.rs +++ /dev/null @@ -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, -} - -pub struct TextThreadStore { - text_threads: Vec, - text_threads_metadata: Vec, - context_server_slash_command_ids: HashMap>, - host_text_threads: Vec, - fs: Arc, - languages: Arc, - slash_commands: Arc, - _watch_updates: Task>, - client: Arc, - project: WeakEntity, - project_is_shared: bool, - client_subscription: Option, - _project_subscriptions: Vec, - prompt_builder: Arc, -} - -enum TextThreadHandle { - Weak(WeakEntity), - Strong(Entity), -} - -impl TextThreadHandle { - fn upgrade(&self) -> Option> { - match self { - TextThreadHandle::Weak(weak) => weak.upgrade(), - TextThreadHandle::Strong(strong) => Some(strong.clone()), - } - } - - fn downgrade(&self) -> WeakEntity { - match self { - TextThreadHandle::Weak(weak) => weak.clone(), - TextThreadHandle::Strong(strong) => strong.downgrade(), - } - } -} - -impl TextThreadStore { - pub fn new( - project: Entity, - prompt_builder: Arc, - slash_commands: Arc, - cx: &mut App, - ) -> Task>> { - 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| { - 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, cx: &mut Context) -> 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, - envelope: TypedEnvelope, - 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, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - 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, - _: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - 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, - envelope: TypedEnvelope, - 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, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - 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) { - 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, - event: &project::Event, - cx: &mut Context, - ) { - 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 { - 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 { - self.host_text_threads.iter() - } - - pub fn create(&mut self, cx: &mut Context) -> Entity { - 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) -> Task>> { - 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::>>() - }) - .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, - cx: &Context, - ) -> Task>> { - 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, cx: &mut Context) -> Task> { - 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) -> Task> { - let fs = self.fs.clone(); - let paths = self - .text_threads_metadata - .iter() - .map(|metadata| metadata.path.clone()) - .collect::>(); - - 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> { - 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> { - 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, - ) -> Task>> { - 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::>>() - }) - .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, cx: &mut Context) { - 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, - event: &TextThreadEvent, - cx: &mut Context, - ) { - 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) { - 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> { - 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::>(); - 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) -> Task> { - 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::::new(); - while let Some(path) = paths.next().await { - let path = path?; - if path.extension() != Some(OsStr::new("json")) { - continue; - } - - static ASSISTANT_CONTEXT_REGEX: LazyLock = - 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) { - 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, - event: &project::context_server_store::ServerStatusChangedEvent, - cx: &mut Context, - ) { - 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, - cx: &mut Context, - ) { - 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::(()) - .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::>(); - - 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::>() - }); - - 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()); - } -} diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 41f1ba2c14c6c09bcbf6861674a845b3954aa733..0703d88a2c0f2fd456141faa47243ad774a473e0 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -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 diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 6c05bd4e535df0235f708af0272b2eae71581fa2..3c4efe0580c18c938f8245de9f40bf216bab9c81 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -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::) - .add_request_handler(forward_mutating_project_request::) - .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) @@ -442,8 +439,6 @@ impl Server { .add_request_handler(disallow_guest_request::) .add_request_handler(disallow_guest_request::) .add_request_handler(forward_mutating_project_request::) - .add_message_handler(broadcast_project_message_from_host::) - .add_message_handler(update_context) .add_request_handler(forward_mutating_project_request::) .add_message_handler(broadcast_project_message_from_host::) .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, diff --git a/crates/collab/tests/integration/integration_tests.rs b/crates/collab/tests/integration/integration_tests.rs index 965e791102a373b718f36e92ea55a5753bbe32c7..3e423d2615f5d2a8e10028a11ffd09bae9c026b5 100644 --- a/crates/collab/tests/integration/integration_tests.rs +++ b/crates/collab/tests/integration/integration_tests.rs @@ -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::>(); - 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, diff --git a/crates/collab/tests/integration/test_server.rs b/crates/collab/tests/integration/test_server.rs index cca48bea973f178000d24bddcbb73252c5657b53..f077f5f35cb6110b4cf4ec927f2cf572bea27b06 100644 --- a/crates/collab/tests/integration/test_server.rs +++ b/crates/collab/tests/integration/test_server.rs @@ -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 diff --git a/crates/docs_preprocessor/src/main.rs b/crates/docs_preprocessor/src/main.rs index fc1bc404244a4896e7d13fbb0e9c81674438568f..af451d43268568960e0727741a88c78d41c61c54 100644 --- a/crates/docs_preprocessor/src/main.rs +++ b/crates/docs_preprocessor/src/main.rs @@ -543,9 +543,9 @@ fn template_and_validate_json_snippets(book: &mut Book, errors: &mut HashSet
 Arc {
     let prompt_builder = PromptBuilder::load(fs.clone(), stdout_is_a_pty, cx);
     agent_ui::init(
         fs.clone(),
-        client.clone(),
         prompt_builder,
         languages.clone(),
         true,
diff --git a/crates/extension/src/extension_host_proxy.rs b/crates/extension/src/extension_host_proxy.rs
index 5efc015483f10915a2e6646c3359bd47b90cfe69..c8bb7ef1343a3772c115f85306dd5631a6f4dd71 100644
--- a/crates/extension/src/extension_host_proxy.rs
+++ b/crates/extension/src/extension_host_proxy.rs
@@ -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);
@@ -29,7 +29,6 @@ pub struct ExtensionHostProxy {
     language_proxy: RwLock>>,
     language_server_proxy: RwLock>>,
     snippet_proxy: RwLock>>,
-    slash_command_proxy: RwLock>>,
     context_server_proxy: RwLock>>,
     debug_adapter_provider_proxy: RwLock>>,
     language_model_provider_proxy: RwLock>>,
@@ -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, command: SlashCommand);
-
-    fn unregister_slash_command(&self, command_name: Arc);
-}
-
-impl ExtensionSlashCommandProxy for ExtensionHostProxy {
-    fn register_slash_command(&self, extension: Arc, 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) {
-        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,
diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs
index 5418f630537c1acd98edc8c6af753d9358b23e8f..03f340a56a98eb826110b245505c2b92774a0e0f 100644
--- a/crates/extension_host/src/extension_host.rs
+++ b/crates/extension_host/src/extension_host.rs
@@ -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);
diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs
index fceae09e5a4fbe1116c73d6ff5ca8bf018480fd9..19bf62d8bbc476049548f65616e6ca1e12f5378a 100644
--- a/crates/extensions_ui/src/extensions_ui.rs
+++ b/crates/extensions_ui/src/extensions_ui.rs
@@ -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,
                     });
diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs
index acec738030ab2d36c2aebaaf45cf127e41bf385f..400b2a22bc6071b62c6ce22a2b1bf1053c8cf871 100644
--- a/crates/icons/src/icons.rs
+++ b/crates/icons/src/icons.rs
@@ -236,7 +236,6 @@ pub enum IconName {
     Terminal,
     TerminalAlt,
     TextSnippet,
-    TextThread,
     ThinkingMode,
     ThinkingModeOff,
     Thread,
diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs
index 1e7910fa54f91938ea0d8e34a7818384c3b81a0e..986654e6fcd455ad2aa64bbd0a5548eeedd4afdd 100644
--- a/crates/language/src/language_settings.rs
+++ b/crates/language/src/language_settings.rs
@@ -472,9 +472,6 @@ pub struct EditPredictionSettings {
     /// Settings specific to Ollama.
     pub ollama: Option,
     pub open_ai_compatible_api: Option,
-    /// 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>,
 }
 
@@ -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, (GlobSet, Vec)> = 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,
diff --git a/crates/language_model/Cargo.toml b/crates/language_model/Cargo.toml
index a586458e41bab0b12c5f92849659ed33c18f5a68..2e802e4205fb82aa89cc6beb67ed9e3e68ed1cf6 100644
--- a/crates/language_model/Cargo.toml
+++ b/crates/language_model/Cargo.toml
@@ -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
diff --git a/crates/language_model/src/language_model.rs b/crates/language_model/src/language_model.rs
index 0452c494a2ae0ce43d59de5ef26a75231249c642..2f715007ec4d8c2906c0254d4bb458d056d8585e 100644
--- a/crates/language_model/src/language_model.rs
+++ b/crates/language_model/src/language_model.rs
@@ -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;
 
diff --git a/crates/language_model/src/role.rs b/crates/language_model/src/role.rs
index 4b47ef36dd564e5950ce7d42a7e4f9263f3998b7..8abc0a74b271a3d434f8dbcf3093aee83e096a89 100644
--- a/crates/language_model/src/role.rs
+++ b/crates/language_model/src/role.rs
@@ -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,
diff --git a/crates/migrator/src/migrations.rs b/crates/migrator/src/migrations.rs
index d554ee1dd887d6048f55a584ed2534db944b3c08..c49df39d59abaa924edb6c986c63701952dce01e 100644
--- a/crates/migrator/src/migrations.rs
+++ b/crates/migrator/src/migrations.rs
@@ -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;
+}
diff --git a/crates/migrator/src/migrations/m_2026_03_31/settings.rs b/crates/migrator/src/migrations/m_2026_03_31/settings.rs
new file mode 100644
index 0000000000000000000000000000000000000000..1a3fdb109f3773bada7a5fd5c00b1947e556e4c9
--- /dev/null
+++ b/crates/migrator/src/migrations/m_2026_03_31/settings.rs
@@ -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) -> 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(())
+}
diff --git a/crates/migrator/src/migrator.rs b/crates/migrator/src/migrator.rs
index ceb6ec2e0e35f0dd3bbd23174637bba00baab6b3..46cccfc4055a78a27d12da54ee187a0fdc202917 100644
--- a/crates/migrator/src/migrator.rs
+++ b/crates/migrator/src/migrator.rs
@@ -247,6 +247,7 @@ pub fn migrate_settings(text: &str) -> Result> {
             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,
+        );
+    }
 }
diff --git a/crates/paths/src/paths.rs b/crates/paths/src/paths.rs
index 40e10fb3badaf2e00c6dbcc75af06e7b758faa81..c9b9c756217281d587491aac5cac81e7cd0baaf2 100644
--- a/crates/paths/src/paths.rs
+++ b/crates/paths/src/paths.rs
@@ -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 = 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 = 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.
diff --git a/crates/proto/proto/ai.proto b/crates/proto/proto/ai.proto
index 65433a4c36df40d49db0cf209eebd635369609f1..20c87d830a68c38c3682ff05d0fb5416099b17ff 100644
--- a/crates/proto/proto/ai.proto
+++ b/crates/proto/proto/ai.proto
@@ -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;
diff --git a/crates/proto/proto/call.proto b/crates/proto/proto/call.proto
index 31448a8819d13f50088aa7eafcd6af8b6d52bc17..4d2bf62eade7aaf633ea899cd106e8d9cb3be25d 100644
--- a/crates/proto/proto/call.proto
+++ b/crates/proto/proto/call.proto
@@ -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 {
diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto
index 1fd7dfb89b01c16c6099a0e79a9d320a788fd7e4..d165bcb9529a41294d2bc25572f454c425f8c3f0 100644
--- a/crates/proto/proto/zed.proto
+++ b/crates/proto/proto/zed.proto
@@ -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 {
diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs
index 88607abf6decdd167cf3594e56ad1eb6b79d3ac6..8c72fa08c57755dc45b9658db441a037d0a9fe2e 100644
--- a/crates/proto/src/proto.rs
+++ b/crates/proto/src/proto.rs
@@ -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,
diff --git a/crates/rules_library/src/rules_library.rs b/crates/rules_library/src/rules_library.rs
index 1c8e90794674dfc737480981954f91312add1ee5..23e7b83772f755089c49f824719af389ec589bd9 100644
--- a/crates/rules_library/src/rules_library.rs
+++ b/crates/rules_library/src/rules_library.rs
@@ -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,
     inline_assist_delegate: Box,
-    make_completion_provider: Rc Rc>,
     prompt_to_select: Option,
     cx: &mut App,
 ) -> Task>> {
@@ -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>,
     pending_load: Task<()>,
     inline_assist_delegate: Box,
-    make_completion_provider: Rc Rc>,
     _subscriptions: Vec,
 }
 
@@ -471,7 +467,6 @@ impl RulesLibrary {
         store: Entity,
         language_registry: Arc,
         inline_assist_delegate: Box,
-        make_completion_provider: Rc Rc>,
         rule_to_select: Option,
         window: &mut Window,
         cx: &mut Context,
@@ -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);
                             }
diff --git a/crates/settings/src/vscode_import.rs b/crates/settings/src/vscode_import.rs
index 8cb596d46e0bd21358043553252c187c6bbd1202..c40b38c460a17f30b1fce26c50b40a893f7724a8 100644
--- a/crates/settings/src/vscode_import.rs
+++ b/crates/settings/src/vscode_import.rs
@@ -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,
         }
diff --git a/crates/settings_content/src/agent.rs b/crates/settings_content/src/agent.rs
index 716e5fea3d48b969345f2d60bdd1cb84e9ce4d58..f9d3376a26b8d84d89e563b21a969bfca68ee2f7 100644
--- a/crates/settings_content/src/agent.rs
+++ b/crates/settings_content/src/agent.rs
@@ -146,10 +146,6 @@ pub struct AgentSettingsContent {
     ///
     /// Default: write
     pub default_profile: Option>,
-    /// Which view type to show by default in the agent panel.
-    ///
-    /// Default: "thread"
-    pub default_view: Option,
     /// Where new threads should start by default.
     ///
     /// Default: "local_project"
@@ -327,14 +323,6 @@ pub struct ContextServerPresetContent {
     pub tools: IndexMap, 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,
diff --git a/crates/settings_content/src/language.rs b/crates/settings_content/src/language.rs
index 4578d2eb589313e57688d7c604beb4eced83de29..c818ca431e8c651affe511fba0e66fcbb388f5ee 100644
--- a/crates/settings_content/src/language.rs
+++ b/crates/settings_content/src/language.rs
@@ -180,9 +180,6 @@ pub struct EditPredictionSettingsContent {
     pub ollama: Option,
     /// Settings specific to using custom OpenAI-compatible servers for edit prediction.
     pub open_ai_compatible_api: Option,
-    /// Whether edit predictions are enabled in the assistant prompt editor.
-    /// This has no effect if globally disabled.
-    pub enabled_in_text_threads: Option,
     /// The directory where manually captured edit prediction examples are stored.
     pub examples_dir: Option>,
 }
diff --git a/crates/settings_content/src/project.rs b/crates/settings_content/src/project.rs
index 85a39f389efc621e902154431278c2050c81a210..789f3786cb0d39444370d78e92d3d342773cafd5 100644
--- a/crates/settings_content/src/project.rs
+++ b/crates/settings_content/src/project.rs
@@ -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,
 
-    /// Settings for slash commands.
-    pub slash_commands: Option,
-
     /// The list of custom Git hosting providers.
     pub git_hosting_providers: Option>,
 
diff --git a/crates/settings_content/src/settings_content.rs b/crates/settings_content/src/settings_content.rs
index 861b6fee454edc4d18b8248b42315287a33c572c..6e0021d6c49d80628206151545476ffcd644516a 100644
--- a/crates/settings_content/src/settings_content.rs
+++ b/crates/settings_content/src/settings_content.rs
@@ -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,
-}
-
-/// 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,
-}
-
 /// Configuration of voice calls in Zed.
 #[with_fallible_options]
 #[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug)]
diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs
index e4eac81b067b2ead2e89153c9a444b4ebd016f64..08a597dc992913e144ba70e30c1a81b2ab8de1aa 100644
--- a/crates/settings_ui/src/page_data.rs
+++ b/crates/settings_ui/src/page_data.rs
@@ -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 {
diff --git a/crates/sidebar/Cargo.toml b/crates/sidebar/Cargo.toml
index 860a7c0ab394de4f2f0863d7a7d9f1b990a39315..04ed8808a14d4c6853b08669523d55a2ebba4482 100644
--- a/crates/sidebar/Cargo.toml
+++ b/crates/sidebar/Cargo.toml
@@ -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
diff --git a/crates/sidebar/src/sidebar_tests.rs b/crates/sidebar/src/sidebar_tests.rs
index 13649b5f79c32e811530e70f1da728b8740d21e4..18fa8c9375c8d9c30ca3c1369613c0385f1cc94d 100644
--- a/crates/sidebar/src/sidebar_tests.rs
+++ b/crates/sidebar/src/sidebar_tests.rs
@@ -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,
-    project: &Entity,
     cx: &mut gpui::VisualTestContext,
 ) -> Entity {
     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,
-    project: &Entity,
     cx: &mut gpui::VisualTestContext,
 ) -> (Entity, Entity) {
     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| ::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 = Vec::new();
diff --git a/crates/terminal_view/Cargo.toml b/crates/terminal_view/Cargo.toml
index ae4c19ff59b5b944588e06c3373de754d63feaf2..f74d8b83883a1186d91855429c40f375bfa22526 100644
--- a/crates/terminal_view/Cargo.toml
+++ b/crates/terminal_view/Cargo.toml
@@ -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
diff --git a/crates/terminal_view/src/terminal_slash_command.rs b/crates/terminal_view/src/terminal_slash_command.rs
deleted file mode 100644
index 13c2cef48c3596d77c1bc7f00587f17dfc1c75e5..0000000000000000000000000000000000000000
--- a/crates/terminal_view/src/terminal_slash_command.rs
+++ /dev/null
@@ -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,
-        _arguments: &[String],
-        _cancel: Arc,
-        _workspace: Option>,
-        _window: &mut Window,
-        _cx: &mut App,
-    ) -> Task>> {
-        Task::ready(Ok(Vec::new()))
-    }
-
-    fn run(
-        self: Arc,
-        arguments: &[String],
-        _context_slash_command_output_sections: &[SlashCommandOutputSection],
-        _context_buffer: BufferSnapshot,
-        workspace: WeakEntity,
-        _delegate: Option>,
-        _: &mut Window,
-        cx: &mut App,
-    ) -> Task {
-        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::().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,
-    cx: &mut App,
-) -> Option> {
-    if let Some(terminal_view) = workspace
-        .read(cx)
-        .active_item(cx)
-        .and_then(|item| item.act_as::(cx))
-    {
-        return Some(terminal_view);
-    }
-
-    let terminal_panel = workspace.read(cx).panel::(cx)?;
-    terminal_panel.read(cx).pane().and_then(|pane| {
-        pane.read(cx)
-            .active_item()
-            .and_then(|t| t.downcast::())
-    })
-}
diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs
index b97a0845dbac5447158db01179dbf0849c72ea87..0c9bbcbec32dcd0fbb8240d524b83f461ac778c3 100644
--- a/crates/terminal_view/src/terminal_view.rs
+++ b/crates/terminal_view/src/terminal_view.rs
@@ -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::(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 {
diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs
index 764a89d507c590f9d5a1f4b7ce40b30795fa450b..303f21b8ffa62f9d9f380d9c18beecd77775df20 100644
--- a/crates/zed/src/main.rs
+++ b/crates/zed/src/main.rs
@@ -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);
 
diff --git a/crates/zed/src/visual_test_runner.rs b/crates/zed/src/visual_test_runner.rs
index 57fbeeb9a991705ca1f9ae6cf00b9a17a41d822f..e5713e90df397a01af850af55338897f9d437e55 100644
--- a/crates/zed/src/visual_test_runner.rs
+++ b/crates/zed/src/visual_test_runner.rs
@@ -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::(cx)` returns None in the new workspace and
     // the creation flow's `focus_panel::` 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();
 
diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs
index 49593dda129623064c998176942442bbdcb65f7c..01e2354849f3a70399c680c44bd1a3cfbeb64dc4 100644
--- a/crates/zed/src/zed.rs
+++ b/crates/zed/src/zed.rs
@@ -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, cx: &mut App) -> WindowO
     }
 }
 
-pub fn initialize_workspace(
-    app_state: Arc,
-    prompt_builder: Arc,
-    cx: &mut App,
-) {
+pub fn initialize_workspace(app_state: Arc, cx: &mut App) {
     let mut _on_close_subscription = bind_on_window_closed(cx);
     cx.observe_global::(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,
-    window: &mut Window,
-    cx: &mut Context,
-) -> Task> {
+fn initialize_panels(window: &mut Window, cx: &mut Context) -> Task> {
     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(
 
 async fn initialize_agent_panel(
     workspace_handle: WeakEntity,
-    prompt_builder: Arc,
     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::(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) {
-            ::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| {
diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs
index 7d8811de705713df7ac8e3a161f14c9f9138ebfc..75b21c528a1e6952700264a154ab4c15045149b0 100644
--- a/crates/zed_actions/src/lib.rs
+++ b/crates/zed_actions/src/lib.rs
@@ -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"])]
diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md
index fad24930b75da256dd8adf405acebfdd8bb168f6..2dca46d99a4a274300e82f67b6f0eac96bb55ee9 100644
--- a/docs/src/SUMMARY.md
+++ b/docs/src/SUMMARY.md
@@ -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)
 
diff --git a/docs/src/ai/agent-panel.md b/docs/src/ai/agent-panel.md
index 7e183d38550d3624a0c9a48051e95ca4c568d72d..2da2f37a67edea48e0c34b14cab1ec0fc81a522b 100644
--- a/docs/src/ai/agent-panel.md
+++ b/docs/src/ai/agent-panel.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.
diff --git a/docs/src/ai/agent-settings.md b/docs/src/ai/agent-settings.md
index 3e152fc5671225abef4a6477b3f73be5d054a365..e1de9fba5e79d56ef73236b2e07c70c93819a2c7 100644
--- a/docs/src/ai/agent-settings.md
+++ b/docs/src/ai/agent-settings.md
@@ -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.
diff --git a/docs/src/ai/ai-improvement.md b/docs/src/ai/ai-improvement.md
index 26085bc3971eca633fa481469e26719161fbf7e0..b1fda4cbf6c6bbc7db395394a420afbbaecfa57d 100644
--- a/docs/src/ai/ai-improvement.md
+++ b/docs/src/ai/ai-improvement.md
@@ -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)).
diff --git a/docs/src/ai/inline-assistant.md b/docs/src/ai/inline-assistant.md
index f1391f8d58dc8746bceece6bcfa3ce091ea4785f..f560451dbbbac11b34910f9512c7fc7a0ad54853 100644
--- a/docs/src/ai/inline-assistant.md
+++ b/docs/src/ai/inline-assistant.md
@@ -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.
 
diff --git a/docs/src/ai/llm-providers.md b/docs/src/ai/llm-providers.md
index 24501ab2d356b8dc4098808ed8e9193cf6e171c6..7c34d13e9616bbcf9482f6f8a79699ad7e2f96ff 100644
--- a/docs/src/ai/llm-providers.md
+++ b/docs/src/ai/llm-providers.md
@@ -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).
 
diff --git a/docs/src/ai/models.md b/docs/src/ai/models.md
index a6bb25e929116faf6b75fae14e55f293c85829a5..ebd35041f7456fec314886d3b554730e1de83d7f 100644
--- a/docs/src/ai/models.md
+++ b/docs/src/ai/models.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.
diff --git a/docs/src/ai/overview.md b/docs/src/ai/overview.md
index 9463f7bbb11cdcb204915fca138e584baa1f9640..7ea435975ec5ba12f68a7a3ad19007fd65ba65e8 100644
--- a/docs/src/ai/overview.md
+++ b/docs/src/ai/overview.md
@@ -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.
diff --git a/docs/src/ai/rules.md b/docs/src/ai/rules.md
index 6a23673e824435d129b0230a55f090e8ddd73e0a..1fb47aa562d0b733bb0333e3c571430488c2503c 100644
--- a/docs/src/ai/rules.md
+++ b/docs/src/ai/rules.md
@@ -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.
diff --git a/docs/src/ai/text-threads.md b/docs/src/ai/text-threads.md
index 82ea45510edcd1a74e21477333f6633cd800f217..2315666460fd645f182bd6ff0934c5c1c9f18873 100644
--- a/docs/src/ai/text-threads.md
+++ b/docs/src/ai/text-threads.md
@@ -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 escape, or start a new conversation at any time via cmd-n|ctrl-n.
-
-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 escape.
-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 `
-
-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 `
-
-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 []`
-
-- ``: 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.
-
-
- -Slash commands in rules **only** work when they are used in text threads. Using them in non-text threads is not supported. - -
- -> **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. - -![Viewing assistant history](https://zed.dev/img/assistant/assistant-history.png) +See the [Agent Panel documentation](./agent-panel.md) to get started. diff --git a/docs/src/extensions.md b/docs/src/extensions.md index af44d981fd9e911235d5a70a1b0266037ed30ddc..9e46f4ab54e5a22f16a1ef156533d25511c36606 100644 --- a/docs/src/extensions.md +++ b/docs/src/extensions.md @@ -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) diff --git a/docs/src/extensions/developing-extensions.md b/docs/src/extensions/developing-extensions.md index e04afe1e53e3f3a2bd3b2d69e7675201aba63bb6..46bed8e223721be81806a3662752d3a4533ab173 100644 --- a/docs/src/extensions/developing-extensions.md +++ b/docs/src/extensions/developing-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 diff --git a/docs/src/extensions/slash-commands.md b/docs/src/extensions/slash-commands.md index 432af3bd61704eeeeed103b2d86badef662d7a44..a76852189301e7dbfe1a31af6a7743fe4215c4c6 100644 --- a/docs/src/extensions/slash-commands.md +++ b/docs/src/extensions/slash-commands.md @@ -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, - _worktree: Option<&Worktree>, - ) -> Result { - 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, - ) -> Result, 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. diff --git a/docs/src/migrate/intellij.md b/docs/src/migrate/intellij.md index bb10c8cdc3b9002e0aeb9a362a0a945de6f46176..adf0e20bef761385b66ad6bf55e387dd662088f4 100644 --- a/docs/src/migrate/intellij.md +++ b/docs/src/migrate/intellij.md @@ -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: diff --git a/docs/src/migrate/pycharm.md b/docs/src/migrate/pycharm.md index 6bef34f81cb380690764742a7b0310cbc81f2072..0ce769b06bcc1363a4dde1d9ae3c138c0b4539f1 100644 --- a/docs/src/migrate/pycharm.md +++ b/docs/src/migrate/pycharm.md @@ -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: diff --git a/docs/src/migrate/rustrover.md b/docs/src/migrate/rustrover.md index 5dc2ab10e9e3f70cd34f2582e3a2a39608168ed8..1e12202233ff1dc8f958b7acfc71a16723ed34ff 100644 --- a/docs/src/migrate/rustrover.md +++ b/docs/src/migrate/rustrover.md @@ -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: diff --git a/docs/src/migrate/webstorm.md b/docs/src/migrate/webstorm.md index eb41f5c245cdc33a9a78320997b546bee8e14f15..3708d8dec825caf23b831a4151ee60e95c04287d 100644 --- a/docs/src/migrate/webstorm.md +++ b/docs/src/migrate/webstorm.md @@ -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: diff --git a/docs/src/visual-customization.md b/docs/src/visual-customization.md index 0e18c59fda21014a80ea8f362486711e204016e0..3c285bc3d10fc3bcb5fba6f735304ede438104a3 100644 --- a/docs/src/visual-customization.md +++ b/docs/src/visual-customization.md @@ -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 ```