diff --git a/.github/ISSUE_TEMPLATE/99_other.yml b/.github/ISSUE_TEMPLATE/99_other.yml
new file mode 100644
index 0000000000..9383a576b1
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/99_other.yml
@@ -0,0 +1,19 @@
+name: Other [Staff Only]
+description: Zed Staff Only
+body:
+ - type: textarea
+ attributes:
+ label: Summary
+ value: |
+
+ SUMMARY_SENTENCE_HERE
+
+ ### Description
+
+ IF YOU DO NOT WORK FOR ZED INDUSTRIES DO NOT CREATE ISSUES WITH THIS TEMPLATE.
+ THEY WILL BE AUTO-CLOSED AND MAY RESULT IN YOU BEING BANNED FROM THE ZED ISSUE TRACKER.
+
+ FEATURE REQUESTS / SUPPORT REQUESTS SHOULD BE OPENED AS DISCUSSIONS:
+ https://github.com/zed-industries/zed/discussions/new/choose
+ validations:
+ required: true
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 6375d74f15..4ddb5173e7 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -594,7 +594,7 @@ jobs:
timeout-minutes: 60
name: Linux x86_x64 release bundle
runs-on:
- - buildjet-16vcpu-ubuntu-2004
+ - buildjet-16vcpu-ubuntu-2004 # ubuntu 20.04 for minimal glibc
if: |
startsWith(github.ref, 'refs/tags/v')
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
@@ -622,26 +622,23 @@ jobs:
- name: Create Linux .tar.gz bundle
run: script/bundle-linux
- - name: Upload Linux bundle to workflow run if main branch or specific label
+ - name: Upload Artifact to Workflow - zed (run-bundling)
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
- if: |
- github.ref == 'refs/heads/main'
- || contains(github.event.pull_request.labels.*.name, 'run-bundling')
+ if: contains(github.event.pull_request.labels.*.name, 'run-bundling')
with:
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-x86_64-unknown-linux-gnu.tar.gz
path: target/release/zed-*.tar.gz
- - name: Upload Linux remote server to workflow run if main branch or specific label
+ - name: Upload Artifact to Workflow - zed-remote-server (run-bundling)
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
- if: |
- github.ref == 'refs/heads/main'
- || contains(github.event.pull_request.labels.*.name, 'run-bundling')
+ if: contains(github.event.pull_request.labels.*.name, 'run-bundling')
with:
name: zed-remote-server-${{ github.event.pull_request.head.sha || github.sha }}-x86_64-unknown-linux-gnu.gz
path: target/zed-remote-server-linux-x86_64.gz
- - name: Upload app bundle to release
+ - name: Upload Artifacts to release
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
+ if: ${{ !(contains(github.event.pull_request.labels.*.name, 'run-bundling')) }}
with:
draft: true
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
@@ -680,29 +677,26 @@ jobs:
# This exports RELEASE_CHANNEL into env (GITHUB_ENV)
script/determine-release-channel
- - name: Create and upload Linux .tar.gz bundle
+ - name: Create and upload Linux .tar.gz bundles
run: script/bundle-linux
- - name: Upload Linux bundle to workflow run if main branch or specific label
+ - name: Upload Artifact to Workflow - zed (run-bundling)
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
- if: |
- github.ref == 'refs/heads/main'
- || contains(github.event.pull_request.labels.*.name, 'run-bundling')
+ if: contains(github.event.pull_request.labels.*.name, 'run-bundling')
with:
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-aarch64-unknown-linux-gnu.tar.gz
path: target/release/zed-*.tar.gz
- - name: Upload Linux remote server to workflow run if main branch or specific label
+ - name: Upload Artifact to Workflow - zed-remote-server (run-bundling)
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
- if: |
- github.ref == 'refs/heads/main'
- || contains(github.event.pull_request.labels.*.name, 'run-bundling')
+ if: contains(github.event.pull_request.labels.*.name, 'run-bundling')
with:
name: zed-remote-server-${{ github.event.pull_request.head.sha || github.sha }}-aarch64-unknown-linux-gnu.gz
path: target/zed-remote-server-linux-aarch64.gz
- - name: Upload app bundle to release
+ - name: Upload Artifacts to release
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
+ if: ${{ !(contains(github.event.pull_request.labels.*.name, 'run-bundling')) }}
with:
draft: true
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
diff --git a/.github/workflows/deploy_collab.yml b/.github/workflows/deploy_collab.yml
index d921a08bf1..eb5875afcc 100644
--- a/.github/workflows/deploy_collab.yml
+++ b/.github/workflows/deploy_collab.yml
@@ -117,12 +117,10 @@ jobs:
export ZED_KUBE_NAMESPACE=production
export ZED_COLLAB_LOAD_BALANCER_SIZE_UNIT=10
export ZED_API_LOAD_BALANCER_SIZE_UNIT=2
- export ZED_LLM_LOAD_BALANCER_SIZE_UNIT=2
elif [[ $GITHUB_REF_NAME = "collab-staging" ]]; then
export ZED_KUBE_NAMESPACE=staging
export ZED_COLLAB_LOAD_BALANCER_SIZE_UNIT=1
export ZED_API_LOAD_BALANCER_SIZE_UNIT=1
- export ZED_LLM_LOAD_BALANCER_SIZE_UNIT=1
else
echo "cowardly refusing to deploy from an unknown branch"
exit 1
@@ -147,9 +145,3 @@ jobs:
envsubst < crates/collab/k8s/collab.template.yml | kubectl apply -f -
kubectl -n "$ZED_KUBE_NAMESPACE" rollout status deployment/$ZED_SERVICE_NAME --watch
echo "deployed ${ZED_SERVICE_NAME} to ${ZED_KUBE_NAMESPACE}"
-
- export ZED_SERVICE_NAME=llm
- export ZED_LOAD_BALANCER_SIZE_UNIT=$ZED_LLM_LOAD_BALANCER_SIZE_UNIT
- envsubst < crates/collab/k8s/collab.template.yml | kubectl apply -f -
- kubectl -n "$ZED_KUBE_NAMESPACE" rollout status deployment/$ZED_SERVICE_NAME --watch
- echo "deployed ${ZED_SERVICE_NAME} to ${ZED_KUBE_NAMESPACE}"
diff --git a/Cargo.lock b/Cargo.lock
index 6ba576975a..87c50bd31f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -326,7 +326,6 @@ dependencies = [
"serde_json",
"strum",
"thiserror 2.0.12",
- "util",
"workspace-hack",
]
@@ -1183,6 +1182,18 @@ dependencies = [
"workspace-hack",
]
+[[package]]
+name = "auto_update_helper"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "log",
+ "simplelog",
+ "windows 0.61.1",
+ "winresource",
+ "workspace-hack",
+]
+
[[package]]
name = "auto_update_ui"
version = "0.1.0"
@@ -2932,7 +2943,6 @@ dependencies = [
name = "collab"
version = "0.44.0"
dependencies = [
- "anthropic",
"anyhow",
"assistant",
"assistant_context_editor",
@@ -3176,14 +3186,18 @@ dependencies = [
name = "component_preview"
version = "0.1.0"
dependencies = [
+ "anyhow",
"client",
"collections",
"component",
+ "db",
"gpui",
"languages",
"notifications",
"project",
+ "serde",
"ui",
+ "ui_input",
"workspace",
"workspace-hack",
]
@@ -3988,7 +4002,6 @@ dependencies = [
"node_runtime",
"parking_lot",
"paths",
- "regex",
"schemars",
"serde",
"serde_json",
@@ -4020,7 +4033,6 @@ dependencies = [
"gpui",
"language",
"paths",
- "regex",
"serde",
"serde_json",
"task",
@@ -4164,6 +4176,7 @@ dependencies = [
"collections",
"command_palette_hooks",
"dap",
+ "db",
"editor",
"env_logger 0.11.8",
"feature_flags",
@@ -4863,25 +4876,37 @@ dependencies = [
"assistant_settings",
"assistant_tool",
"assistant_tools",
+ "async-watch",
+ "chrono",
+ "clap",
"client",
+ "collections",
"context_server",
"dap",
"env_logger 0.11.8",
+ "extension",
"fs",
"futures 0.3.31",
"gpui",
"gpui_tokio",
+ "handlebars 4.5.0",
"language",
+ "language_extension",
"language_model",
"language_models",
+ "languages",
"node_runtime",
+ "paths",
"project",
"prompt_store",
"release_channel",
"reqwest_client",
"serde",
"settings",
+ "shellexpand 2.1.2",
"toml 0.8.20",
+ "unindent",
+ "util",
"workspace-hack",
]
@@ -4976,10 +5001,10 @@ dependencies = [
"async-tar",
"async-trait",
"collections",
- "convert_case 0.8.0",
"fs",
"futures 0.3.31",
"gpui",
+ "heck 0.5.0",
"http_client",
"language",
"log",
@@ -7654,6 +7679,7 @@ dependencies = [
name = "language_model_selector"
version = "0.1.0"
dependencies = [
+ "collections",
"feature_flags",
"gpui",
"language_model",
@@ -7704,6 +7730,7 @@ dependencies = [
"smol",
"strum",
"theme",
+ "thiserror 2.0.12",
"tiktoken-rs",
"tokio",
"ui",
@@ -17628,6 +17655,7 @@ dependencies = [
"ui",
"util",
"uuid",
+ "windows 0.61.1",
"workspace-hack",
"zed_actions",
]
@@ -17790,6 +17818,8 @@ dependencies = [
"wasmtime-cranelift",
"wasmtime-environ",
"winapi",
+ "windows-core 0.61.0",
+ "windows-numerics",
"windows-sys 0.48.0",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
@@ -18134,7 +18164,7 @@ dependencies = [
[[package]]
name = "zed"
-version = "0.182.0"
+version = "0.183.0"
dependencies = [
"activity_indicator",
"agent",
@@ -18230,7 +18260,6 @@ dependencies = [
"settings",
"settings_ui",
"shellexpand 2.1.2",
- "simplelog",
"smol",
"snippet_provider",
"snippets_ui",
@@ -18581,7 +18610,9 @@ name = "zlog"
version = "0.1.0"
dependencies = [
"anyhow",
+ "chrono",
"log",
+ "tempfile",
"workspace-hack",
]
diff --git a/Cargo.toml b/Cargo.toml
index 7ba482e8c7..844ff7e7c5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -15,6 +15,7 @@ members = [
"crates/assistant_tools",
"crates/audio",
"crates/auto_update",
+ "crates/auto_update_helper",
"crates/auto_update_ui",
"crates/aws_http_client",
"crates/bedrock",
@@ -224,6 +225,7 @@ assistant_tool = { path = "crates/assistant_tool" }
assistant_tools = { path = "crates/assistant_tools" }
audio = { path = "crates/audio" }
auto_update = { path = "crates/auto_update" }
+auto_update_helper = { path = "crates/auto_update_helper" }
auto_update_ui = { path = "crates/auto_update_ui" }
aws_http_client = { path = "crates/aws_http_client" }
bedrock = { path = "crates/bedrock" }
@@ -403,8 +405,12 @@ async-tungstenite = "0.29.1"
async-watch = "0.3.1"
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
aws-config = { version = "1.6.1", features = ["behavior-version-latest"] }
-aws-credential-types = { version = "1.2.2", features = ["hardcoded-credentials"] }
-aws-sdk-bedrockruntime = { version = "1.80.0", features = ["behavior-version-latest"] }
+aws-credential-types = { version = "1.2.2", features = [
+ "hardcoded-credentials",
+] }
+aws-sdk-bedrockruntime = { version = "1.80.0", features = [
+ "behavior-version-latest",
+] }
aws-smithy-runtime-api = { version = "1.7.4", features = ["http-1x", "client"] }
aws-smithy-types = { version = "1.3.0", features = ["http-body-1-x"] }
base64 = "0.22"
@@ -443,6 +449,7 @@ futures-lite = "1.13"
git2 = { version = "0.20.1", default-features = false }
globset = "0.4"
handlebars = "4.3"
+heck = "0.5"
heed = { version = "0.21.0", features = ["read-txn-no-tls"] }
hex = "0.4.3"
html5ever = "0.27.0"
@@ -619,12 +626,10 @@ features = [
[workspace.dependencies.windows]
version = "0.61"
features = [
- "Foundation_Collections",
"Foundation_Numerics",
"Storage_Search",
"Storage_Streams",
"System_Threading",
- "UI_StartScreen",
"UI_ViewManagement",
"Wdk_System_SystemServices",
"Win32_Globalization",
@@ -651,6 +656,7 @@ features = [
"Win32_System_SystemInformation",
"Win32_System_SystemServices",
"Win32_System_Threading",
+ "Win32_System_Variant",
"Win32_System_WinRT",
"Win32_UI_Controls",
"Win32_UI_HiDpi",
@@ -658,6 +664,7 @@ features = [
"Win32_UI_Input_KeyboardAndMouse",
"Win32_UI_Shell",
"Win32_UI_Shell_Common",
+ "Win32_UI_Shell_PropertiesSystem",
"Win32_UI_WindowsAndMessaging",
]
@@ -781,4 +788,12 @@ let_underscore_future = "allow"
too_many_arguments = "allow"
[workspace.metadata.cargo-machete]
-ignored = ["bindgen", "cbindgen", "prost_build", "serde", "component", "linkme", "workspace-hack"]
+ignored = [
+ "bindgen",
+ "cbindgen",
+ "prost_build",
+ "serde",
+ "component",
+ "linkme",
+ "workspace-hack",
+]
diff --git a/assets/icons/ai_anthropic_hosted.svg b/assets/icons/ai_anthropic_hosted.svg
deleted file mode 100644
index b088520490..0000000000
--- a/assets/icons/ai_anthropic_hosted.svg
+++ /dev/null
@@ -1,12 +0,0 @@
-
diff --git a/assets/icons/layout.svg b/assets/icons/layout.svg
new file mode 100644
index 0000000000..79464013b1
--- /dev/null
+++ b/assets/icons/layout.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json
index 2fd742c5ae..9b94ed32a1 100644
--- a/assets/keymaps/default-linux.json
+++ b/assets/keymaps/default-linux.json
@@ -150,7 +150,9 @@
"context": "AgentDiff",
"bindings": {
"ctrl-y": "agent::Keep",
- "ctrl-n": "agent::Reject"
+ "ctrl-n": "agent::Reject",
+ "ctrl-shift-y": "agent::KeepAll",
+ "ctrl-shift-n": "agent::RejectAll"
}
},
{
@@ -352,11 +354,11 @@
"alt-shift-left": "editor::SelectSmallerSyntaxNode", // Shrink Selection
"ctrl-shift-l": "editor::SelectAllMatches", // Select all occurrences of current selection
"ctrl-f2": "editor::SelectAllMatches", // Select all occurrences of current word
- "ctrl-d": ["editor::SelectNext", { "replace_newest": false }],
- "ctrl-shift-down": ["editor::SelectNext", { "replace_newest": false }], // Add selection to Next Find Match
- "ctrl-shift-up": ["editor::SelectPrevious", { "replace_newest": false }],
- "ctrl-k ctrl-d": ["editor::SelectNext", { "replace_newest": true }],
- "ctrl-k ctrl-shift-d": ["editor::SelectPrevious", { "replace_newest": true }],
+ "ctrl-d": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch / find_under_expand
+ "ctrl-shift-down": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch
+ "ctrl-shift-up": ["editor::SelectPrevious", { "replace_newest": false }], // editor.action.addSelectionToPreviousFindMatch
+ "ctrl-k ctrl-d": ["editor::SelectNext", { "replace_newest": true }], // editor.action.moveSelectionToNextFindMatch / find_under_expand_skip
+ "ctrl-k ctrl-shift-d": ["editor::SelectPrevious", { "replace_newest": true }], // editor.action.moveSelectionToPreviousFindMatch
"ctrl-k ctrl-i": "editor::Hover",
"ctrl-/": ["editor::ToggleComments", { "advance_downwards": false }],
"ctrl-u": "editor::UndoSelection",
@@ -780,6 +782,7 @@
"shift-tab": "git_panel::FocusEditor",
"escape": "git_panel::ToggleFocus",
"ctrl-enter": "git::Commit",
+ "ctrl-shift-enter": "git::Amend",
"alt-enter": "menu::SecondaryConfirm",
"delete": ["git::RestoreFile", { "skip_prompt": false }],
"backspace": ["git::RestoreFile", { "skip_prompt": false }],
@@ -788,12 +791,20 @@
"ctrl-delete": ["git::RestoreFile", { "skip_prompt": false }]
}
},
+ {
+ "context": "GitPanel && CommitEditor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "escape": "git::Cancel"
+ }
+ },
{
"context": "GitCommit > Editor",
"bindings": {
"escape": "menu::Cancel",
"enter": "editor::Newline",
"ctrl-enter": "git::Commit",
+ "ctrl-shift-enter": "git::Amend",
"alt-l": "git::GenerateCommitMessage"
}
},
@@ -815,6 +826,7 @@
"context": "GitDiff > Editor",
"bindings": {
"ctrl-enter": "git::Commit",
+ "ctrl-shift-enter": "git::Amend",
"ctrl-space": "git::StageAll",
"ctrl-shift-space": "git::UnstageAll"
}
@@ -833,6 +845,7 @@
"shift-tab": "git_panel::FocusChanges",
"enter": "editor::Newline",
"ctrl-enter": "git::Commit",
+ "ctrl-shift-enter": "git::Amend",
"alt-up": "git_panel::FocusChanges",
"alt-l": "git::GenerateCommitMessage"
}
diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json
index 034ac3b8a2..c5cf9e019b 100644
--- a/assets/keymaps/default-macos.json
+++ b/assets/keymaps/default-macos.json
@@ -242,7 +242,9 @@
"use_key_equivalents": true,
"bindings": {
"cmd-y": "agent::Keep",
- "cmd-n": "agent::Reject"
+ "cmd-n": "agent::Reject",
+ "cmd-shift-y": "agent::KeepAll",
+ "cmd-shift-n": "agent::RejectAll"
}
},
{
@@ -489,12 +491,15 @@
"alt-shift-down": "editor::DuplicateLineDown",
"ctrl-shift-right": "editor::SelectLargerSyntaxNode", // Expand Selection
"ctrl-shift-left": "editor::SelectSmallerSyntaxNode", // Shrink Selection
- "cmd-d": ["editor::SelectNext", { "replace_newest": false }], // Add selection to Next Find Match
+ "cmd-d": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch / find_under_expand
"cmd-shift-l": "editor::SelectAllMatches", // Select all occurrences of current selection
"cmd-f2": "editor::SelectAllMatches", // Select all occurrences of current word
- "ctrl-cmd-d": ["editor::SelectPrevious", { "replace_newest": false }],
- "cmd-k cmd-d": ["editor::SelectNext", { "replace_newest": true }],
- "cmd-k ctrl-cmd-d": ["editor::SelectPrevious", { "replace_newest": true }],
+ "cmd-k cmd-d": ["editor::SelectNext", { "replace_newest": true }], // editor.action.moveSelectionToNextFindMatch / find_under_expand_skip
+ // macOS binds `ctrl-cmd-d` to Show Dictionary which breaks these two binds
+ // To use `ctrl-cmd-d` or `ctrl-k ctrl-cmd-d` in Zed you must execute this command and then restart:
+ // defaults write com.apple.symbolichotkeys AppleSymbolicHotKeys -dict-add 70 'enabled'
+ "ctrl-cmd-d": ["editor::SelectPrevious", { "replace_newest": false }], // editor.action.addSelectionToPreviousFindMatch
+ "cmd-k ctrl-cmd-d": ["editor::SelectPrevious", { "replace_newest": true }], // editor.action.moveSelectionToPreviousFindMatch
"cmd-k cmd-i": "editor::Hover",
"cmd-/": ["editor::ToggleComments", { "advance_downwards": false }],
"cmd-u": "editor::UndoSelection",
@@ -850,17 +855,26 @@
"shift-tab": "git_panel::FocusEditor",
"escape": "git_panel::ToggleFocus",
"cmd-enter": "git::Commit",
+ "cmd-shift-enter": "git::Amend",
"backspace": ["git::RestoreFile", { "skip_prompt": false }],
"delete": ["git::RestoreFile", { "skip_prompt": false }],
"cmd-backspace": ["git::RestoreFile", { "skip_prompt": true }],
"cmd-delete": ["git::RestoreFile", { "skip_prompt": true }]
}
},
+ {
+ "context": "GitPanel && CommitEditor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "escape": "git::Cancel"
+ }
+ },
{
"context": "GitDiff > Editor",
"use_key_equivalents": true,
"bindings": {
"cmd-enter": "git::Commit",
+ "cmd-shift-enter": "git::Amend",
"cmd-ctrl-y": "git::StageAll",
"cmd-ctrl-shift-y": "git::UnstageAll"
}
@@ -871,6 +885,7 @@
"bindings": {
"enter": "editor::Newline",
"cmd-enter": "git::Commit",
+ "cmd-shift-enter": "git::Amend",
"tab": "git_panel::FocusChanges",
"shift-tab": "git_panel::FocusChanges",
"alt-up": "git_panel::FocusChanges",
@@ -900,6 +915,7 @@
"enter": "editor::Newline",
"escape": "menu::Cancel",
"cmd-enter": "git::Commit",
+ "cmd-shift-enter": "git::Amend",
"alt-tab": "git::GenerateCommitMessage"
}
},
diff --git a/assets/keymaps/linux/sublime_text.json b/assets/keymaps/linux/sublime_text.json
index 258b8a5629..c3f56350b9 100644
--- a/assets/keymaps/linux/sublime_text.json
+++ b/assets/keymaps/linux/sublime_text.json
@@ -37,6 +37,8 @@
"ctrl-shift-a": "editor::SelectLargerSyntaxNode",
"ctrl-shift-d": "editor::DuplicateSelection",
"alt-f3": "editor::SelectAllMatches", // find_all_under
+ // "ctrl-f3": "", // find_under (cancels any selections)
+ // "cmd-alt-shift-g": "" // find_under_prev (cancels any selections)
"f9": "editor::SortLinesCaseSensitive",
"ctrl-f9": "editor::SortLinesCaseInsensitive",
"f12": "editor::GoToDefinition",
diff --git a/assets/keymaps/macos/sublime_text.json b/assets/keymaps/macos/sublime_text.json
index d3929af9e9..6251ae0ccd 100644
--- a/assets/keymaps/macos/sublime_text.json
+++ b/assets/keymaps/macos/sublime_text.json
@@ -38,6 +38,8 @@
"cmd-shift-a": "editor::SelectLargerSyntaxNode",
"cmd-shift-d": "editor::DuplicateSelection",
"ctrl-cmd-g": "editor::SelectAllMatches", // find_all_under
+ // "cmd-alt-g": "", // find_under (cancels any selections)
+ // "cmd-alt-shift-g": "" // find_under_prev (cancels any selections)
"f5": "editor::SortLinesCaseSensitive",
"ctrl-f5": "editor::SortLinesCaseInsensitive",
"shift-f12": "editor::FindAllReferences",
diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json
index 40c96ed5d8..452731ebc2 100644
--- a/assets/keymaps/vim.json
+++ b/assets/keymaps/vim.json
@@ -539,6 +539,7 @@
"bindings": {
"d": "vim::CurrentLine",
"s": "vim::PushDeleteSurrounds",
+ "v": "vim::PushForcedMotion", // "d v"
"o": "editor::ToggleSelectedDiffHunks", // "d o"
"shift-o": "git::ToggleStaged",
"p": "git::Restore", // "d p"
@@ -587,6 +588,7 @@
"context": "vim_operator == y",
"bindings": {
"y": "vim::CurrentLine",
+ "v": "vim::PushForcedMotion",
"s": ["vim::PushAddSurrounds", {}]
}
},
diff --git a/assets/settings/default.json b/assets/settings/default.json
index 9276cf8117..98ee37f213 100644
--- a/assets/settings/default.json
+++ b/assets/settings/default.json
@@ -80,6 +80,8 @@
// Values are clamped to the [0.0, 1.0] range.
"inactive_opacity": 1.0
},
+ // Layout mode of the bottom dock. Defaults to "contained"
+ "bottom_dock_layout": "contained",
// The direction that you want to split panes horizontally. Defaults to "up"
"pane_split_direction_horizontal": "up",
// The direction that you want to split panes horizontally. Defaults to "left"
@@ -642,6 +644,7 @@
// We don't know which of the context server tools are safe for the "Ask" profile, so we don't enable them by default.
// "enable_all_context_servers": true,
"tools": {
+ "contents": true,
"diagnostics": true,
"fetch": true,
"list_directory": false,
@@ -661,6 +664,7 @@
"batch_tool": true,
"code_actions": true,
"code_symbols": true,
+ "contents": true,
"copy_path": false,
"create_file": true,
"delete_path": false,
diff --git a/crates/agent/src/active_thread.rs b/crates/agent/src/active_thread.rs
index 830646e38d..0ef73946d8 100644
--- a/crates/agent/src/active_thread.rs
+++ b/crates/agent/src/active_thread.rs
@@ -13,18 +13,18 @@ use assistant_settings::{AssistantSettings, NotifyWhenAgentWaiting};
use assistant_tool::ToolUseStatus;
use collections::{HashMap, HashSet};
use editor::scroll::Autoscroll;
-use editor::{Editor, MultiBuffer};
+use editor::{Editor, EditorElement, EditorStyle, MultiBuffer};
use gpui::{
AbsoluteLength, Animation, AnimationExt, AnyElement, App, ClickEvent, ClipboardItem,
DefiniteLength, EdgesRefinement, Empty, Entity, Focusable, Hsla, ListAlignment, ListState,
MouseButton, PlatformDisplay, ScrollHandle, Stateful, StyleRefinement, Subscription, Task,
- TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, WindowHandle,
+ TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, WindowHandle,
linear_color_stop, linear_gradient, list, percentage, pulsating_between,
};
use language::{Buffer, LanguageRegistry};
use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role, StopReason};
use markdown::parser::{CodeBlockKind, CodeBlockMetadata};
-use markdown::{Markdown, MarkdownElement, MarkdownStyle, ParsedMarkdown};
+use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle, ParsedMarkdown};
use project::ProjectItem as _;
use rope::Point;
use settings::{Settings as _, update_settings_file};
@@ -34,7 +34,9 @@ use std::sync::Arc;
use std::time::Duration;
use text::ToPoint;
use theme::ThemeSettings;
-use ui::{Disclosure, IconButton, KeyBinding, Scrollbar, ScrollbarState, Tooltip, prelude::*};
+use ui::{
+ Disclosure, IconButton, KeyBinding, Scrollbar, ScrollbarState, TextSize, Tooltip, prelude::*,
+};
use util::ResultExt as _;
use workspace::{OpenOptions, Workspace};
@@ -66,8 +68,6 @@ pub struct ActiveThread {
open_feedback_editors: HashMap>,
}
-const MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK: usize = 5;
-
struct RenderedMessage {
language_registry: Arc,
segments: Vec,
@@ -176,11 +176,37 @@ fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
});
MarkdownStyle {
- base_text_style: text_style,
+ base_text_style: text_style.clone(),
syntax: cx.theme().syntax().clone(),
selection_background_color: cx.theme().players().local().selection,
code_block_overflow_x_scroll: true,
table_overflow_x_scroll: true,
+ heading_level_styles: Some(HeadingLevelStyles {
+ h1: Some(TextStyleRefinement {
+ font_size: Some(rems(1.15).into()),
+ ..Default::default()
+ }),
+ h2: Some(TextStyleRefinement {
+ font_size: Some(rems(1.1).into()),
+ ..Default::default()
+ }),
+ h3: Some(TextStyleRefinement {
+ font_size: Some(rems(1.05).into()),
+ ..Default::default()
+ }),
+ h4: Some(TextStyleRefinement {
+ font_size: Some(rems(1.).into()),
+ ..Default::default()
+ }),
+ h5: Some(TextStyleRefinement {
+ font_size: Some(rems(0.95).into()),
+ ..Default::default()
+ }),
+ h6: Some(TextStyleRefinement {
+ font_size: Some(rems(0.875).into()),
+ ..Default::default()
+ }),
+ }),
code_block: StyleRefinement {
padding: EdgesRefinement {
top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
@@ -292,6 +318,8 @@ fn tool_use_markdown_style(window: &Window, cx: &mut App) -> MarkdownStyle {
}
}
+const MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK: usize = 10;
+
fn render_markdown_code_block(
message_id: MessageId,
ix: usize,
@@ -578,7 +606,7 @@ fn render_markdown_code_block(
if is_expanded {
this.h_full()
} else {
- this.max_h_40()
+ this.max_h_80()
}
},
)
@@ -1497,12 +1525,36 @@ impl ActiveThread {
.when(!message_is_empty, |parent| {
parent.child(
if let Some(edit_message_editor) = edit_message_editor.clone() {
+ let settings = ThemeSettings::get_global(cx);
+ let font_size = TextSize::Small.rems(cx);
+ let line_height = font_size.to_pixels(window.rem_size()) * 1.5;
+
+ let text_style = TextStyle {
+ color: cx.theme().colors().text,
+ font_family: settings.buffer_font.family.clone(),
+ font_fallbacks: settings.buffer_font.fallbacks.clone(),
+ font_features: settings.buffer_font.features.clone(),
+ font_size: font_size.into(),
+ line_height: line_height.into(),
+ ..Default::default()
+ };
+
div()
.key_context("EditMessageEditor")
.on_action(cx.listener(Self::cancel_editing_message))
.on_action(cx.listener(Self::confirm_editing_message))
.min_h_6()
- .child(edit_message_editor)
+ .pt_1()
+ .child(EditorElement::new(
+ &edit_message_editor,
+ EditorStyle {
+ background: colors.editor_background,
+ local_player: cx.theme().players().local(),
+ text: text_style,
+ syntax: cx.theme().syntax().clone(),
+ ..Default::default()
+ },
+ ))
.into_any()
} else {
div()
@@ -1667,11 +1719,9 @@ impl ActiveThread {
),
Role::Assistant => v_flex()
.id(("message-container", ix))
- .ml_2()
+ .ml_2p5()
.pl_2()
.pr_4()
- .border_l_1()
- .border_color(cx.theme().colors().border_variant)
.children(message_content)
.when(has_tool_uses, |parent| {
parent.children(
diff --git a/crates/agent/src/agent_diff.rs b/crates/agent/src/agent_diff.rs
index c3bc120ead..8fdcbbcb58 100644
--- a/crates/agent/src/agent_diff.rs
+++ b/crates/agent/src/agent_diff.rs
@@ -1,4 +1,4 @@
-use crate::{Keep, Reject, Thread, ThreadEvent};
+use crate::{Keep, KeepAll, Reject, RejectAll, Thread, ThreadEvent};
use anyhow::Result;
use buffer_diff::DiffHunkStatus;
use collections::HashSet;
@@ -843,7 +843,7 @@ impl ToolbarItemView for AgentDiffToolbar {
}
impl Render for AgentDiffToolbar {
- fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement {
+ fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement {
let agent_diff = match self.agent_diff(cx) {
Some(ad) => ad,
None => return div(),
@@ -855,6 +855,8 @@ impl Render for AgentDiffToolbar {
return div();
}
+ let focus_handle = agent_diff.focus_handle(cx);
+
h_group_xl()
.my_neg_1()
.items_center()
@@ -864,15 +866,25 @@ impl Render for AgentDiffToolbar {
.child(
h_group_sm()
.child(
- Button::new("reject-all", "Reject All").on_click(cx.listener(
- |this, _, window, cx| {
- this.dispatch_action(&crate::RejectAll, window, cx)
- },
- )),
+ Button::new("reject-all", "Reject All")
+ .key_binding({
+ KeyBinding::for_action_in(&RejectAll, &focus_handle, window, cx)
+ .map(|kb| kb.size(rems_from_px(12.)))
+ })
+ .on_click(cx.listener(|this, _, window, cx| {
+ this.dispatch_action(&RejectAll, window, cx)
+ })),
)
- .child(Button::new("keep-all", "Keep All").on_click(cx.listener(
- |this, _, window, cx| this.dispatch_action(&crate::KeepAll, window, cx),
- ))),
+ .child(
+ Button::new("keep-all", "Keep All")
+ .key_binding({
+ KeyBinding::for_action_in(&KeepAll, &focus_handle, window, cx)
+ .map(|kb| kb.size(rems_from_px(12.)))
+ })
+ .on_click(cx.listener(|this, _, window, cx| {
+ this.dispatch_action(&KeepAll, window, cx)
+ })),
+ ),
)
}
}
@@ -882,6 +894,7 @@ mod tests {
use super::*;
use crate::{ThreadStore, thread_store};
use assistant_settings::AssistantSettings;
+ use assistant_tool::ToolWorkingSet;
use context_server::ContextServerSettings;
use editor::EditorSettings;
use gpui::TestAppContext;
@@ -925,7 +938,7 @@ mod tests {
.update(|cx| {
ThreadStore::load(
project.clone(),
- Arc::default(),
+ cx.new(|_| ToolWorkingSet::default()),
Arc::new(PromptBuilder::new(None).unwrap()),
cx,
)
diff --git a/crates/agent/src/assistant_configuration.rs b/crates/agent/src/assistant_configuration.rs
index 8972d786e2..7616b1f8b0 100644
--- a/crates/agent/src/assistant_configuration.rs
+++ b/crates/agent/src/assistant_configuration.rs
@@ -12,7 +12,9 @@ use fs::Fs;
use gpui::{Action, AnyView, App, Entity, EventEmitter, FocusHandle, Focusable, Subscription};
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
use settings::{Settings, update_settings_file};
-use ui::{Disclosure, Divider, DividerColor, ElevationIndex, Indicator, Switch, prelude::*};
+use ui::{
+ Disclosure, Divider, DividerColor, ElevationIndex, Indicator, Switch, Tooltip, prelude::*,
+};
use util::ResultExt as _;
use zed_actions::ExtensionCategoryFilter;
@@ -27,7 +29,7 @@ pub struct AssistantConfiguration {
configuration_views_by_provider: HashMap,
context_server_manager: Entity,
expanded_context_server_tools: HashMap, bool>,
- tools: Arc,
+ tools: Entity,
_registry_subscription: Subscription,
}
@@ -35,7 +37,7 @@ impl AssistantConfiguration {
pub fn new(
fs: Arc,
context_server_manager: Entity,
- tools: Arc,
+ tools: Entity,
window: &mut Window,
cx: &mut Context,
) -> Self {
@@ -224,7 +226,7 @@ impl AssistantConfiguration {
fn render_context_servers_section(&mut self, cx: &mut Context) -> impl IntoElement {
let context_servers = self.context_server_manager.read(cx).all_servers().clone();
- let tools_by_source = self.tools.tools_by_source(cx);
+ let tools_by_source = self.tools.read(cx).tools_by_source(cx);
let empty = Vec::new();
const SUBHEADING: &str = "Connect to context servers via the Model Context Protocol either via Zed extensions or directly.";
@@ -236,7 +238,10 @@ impl AssistantConfiguration {
.child(
v_flex()
.gap_0p5()
- .child(Headline::new("Context Servers (MCP)").size(HeadlineSize::Small))
+ .child(
+ Headline::new("Model Context Protocol (MCP) Servers")
+ .size(HeadlineSize::Small),
+ )
.child(Label::new(SUBHEADING).color(Color::Muted)),
)
.children(context_servers.into_iter().map(|context_server| {
@@ -262,10 +267,9 @@ impl AssistantConfiguration {
.bg(cx.theme().colors().editor_background)
.child(
h_flex()
+ .p_1()
.justify_between()
- .px_2()
- .py_1()
- .when(are_tools_expanded, |element| {
+ .when(are_tools_expanded && tool_count > 1, |element| {
element
.border_b_1()
.border_color(cx.theme().colors().border)
@@ -275,6 +279,7 @@ impl AssistantConfiguration {
.gap_2()
.child(
Disclosure::new("tool-list-disclosure", are_tools_expanded)
+ .disabled(tool_count == 0)
.on_click(cx.listener({
let context_server_id = context_server.id();
move |this, _event, _window, _cx| {
@@ -295,10 +300,11 @@ impl AssistantConfiguration {
.child(Label::new(context_server.id()))
.child(
Label::new(format!("{tool_count} tools"))
- .color(Color::Muted),
+ .color(Color::Muted)
+ .size(LabelSize::Small),
),
)
- .child(h_flex().child(
+ .child(
Switch::new("context-server-switch", is_running.into()).on_click({
let context_server_manager =
self.context_server_manager.clone();
@@ -334,7 +340,7 @@ impl AssistantConfiguration {
}
}
}),
- )),
+ ),
)
.map(|parent| {
if !are_tools_expanded {
@@ -344,14 +350,29 @@ impl AssistantConfiguration {
parent.child(v_flex().children(tools.into_iter().enumerate().map(
|(ix, tool)| {
h_flex()
- .px_2()
+ .id("tool-item")
+ .pl_2()
+ .pr_1()
.py_1()
+ .gap_2()
+ .justify_between()
.when(ix < tool_count - 1, |element| {
element
.border_b_1()
- .border_color(cx.theme().colors().border)
+ .border_color(cx.theme().colors().border_variant)
})
- .child(Label::new(tool.name()))
+ .child(
+ Label::new(tool.name())
+ .buffer_font(cx)
+ .size(LabelSize::Small),
+ )
+ .child(
+ IconButton::new(("tool-description", ix), IconName::Info)
+ .shape(ui::IconButtonShape::Square)
+ .icon_size(IconSize::Small)
+ .icon_color(Color::Ignored)
+ .tooltip(Tooltip::text(tool.description())),
+ )
},
)))
})
@@ -362,7 +383,7 @@ impl AssistantConfiguration {
.gap_2()
.child(
h_flex().w_full().child(
- Button::new("add-context-server", "Add Context Server")
+ Button::new("add-context-server", "Add MCPs Directly")
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.full_width()
@@ -378,7 +399,7 @@ impl AssistantConfiguration {
h_flex().w_full().child(
Button::new(
"install-context-server-extensions",
- "Install Context Server Extensions",
+ "Install MCP Extensions",
)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
diff --git a/crates/agent/src/assistant_configuration/manage_profiles_modal.rs b/crates/agent/src/assistant_configuration/manage_profiles_modal.rs
index a4c72cbac9..6f5172a8d4 100644
--- a/crates/agent/src/assistant_configuration/manage_profiles_modal.rs
+++ b/crates/agent/src/assistant_configuration/manage_profiles_modal.rs
@@ -84,7 +84,7 @@ pub struct NewProfileMode {
pub struct ManageProfilesModal {
fs: Arc,
- tools: Arc,
+ tools: Entity,
thread_store: WeakEntity,
focus_handle: FocusHandle,
mode: Mode,
@@ -117,7 +117,7 @@ impl ManageProfilesModal {
pub fn new(
fs: Arc,
- tools: Arc,
+ tools: Entity,
thread_store: WeakEntity,
window: &mut Window,
cx: &mut Context,
diff --git a/crates/agent/src/assistant_configuration/tool_picker.rs b/crates/agent/src/assistant_configuration/tool_picker.rs
index eabd9e172b..2b105e87a2 100644
--- a/crates/agent/src/assistant_configuration/tool_picker.rs
+++ b/crates/agent/src/assistant_configuration/tool_picker.rs
@@ -60,7 +60,7 @@ pub struct ToolPickerDelegate {
impl ToolPickerDelegate {
pub fn new(
fs: Arc,
- tool_set: Arc,
+ tool_set: Entity,
thread_store: WeakEntity,
profile_id: AgentProfileId,
profile: AgentProfile,
@@ -68,7 +68,7 @@ impl ToolPickerDelegate {
) -> Self {
let mut tool_entries = Vec::new();
- for (source, tools) in tool_set.tools_by_source(cx) {
+ for (source, tools) in tool_set.read(cx).tools_by_source(cx) {
tool_entries.extend(tools.into_iter().map(|tool| ToolEntry {
name: tool.name().into(),
source: source.clone(),
@@ -192,7 +192,7 @@ impl PickerDelegate for ToolPickerDelegate {
if active_profile_id == &self.profile_id {
self.thread_store
.update(cx, |this, cx| {
- this.load_profile(&self.profile, cx);
+ this.load_profile(self.profile.clone(), cx);
})
.log_err();
}
diff --git a/crates/agent/src/assistant_model_selector.rs b/crates/agent/src/assistant_model_selector.rs
index 11726b2574..091071af29 100644
--- a/crates/agent/src/assistant_model_selector.rs
+++ b/crates/agent/src/assistant_model_selector.rs
@@ -80,17 +80,16 @@ impl AssistantModelSelector {
impl Render for AssistantModelSelector {
fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement {
- let model_registry = LanguageModelRegistry::read_global(cx);
+ let focus_handle = self.focus_handle.clone();
+ let model_registry = LanguageModelRegistry::read_global(cx);
let model = match self.model_type {
ModelType::Default => model_registry.default_model(),
ModelType::InlineAssistant => model_registry.inline_assistant_model(),
};
-
- let focus_handle = self.focus_handle.clone();
- let model_name = match model {
- Some(model) => model.model.name().0,
- _ => SharedString::from("No model selected"),
+ let (model_name, model_icon) = match model {
+ Some(model) => (model.model.name().0, Some(model.provider.icon())),
+ _ => (SharedString::from("No model selected"), None),
};
LanguageModelSelectorPopoverMenu::new(
@@ -100,10 +99,16 @@ impl Render for AssistantModelSelector {
.child(
h_flex()
.gap_0p5()
+ .children(
+ model_icon.map(|icon| {
+ Icon::new(icon).color(Color::Muted).size(IconSize::Small)
+ }),
+ )
.child(
Label::new(model_name)
.size(LabelSize::Small)
- .color(Color::Muted),
+ .color(Color::Muted)
+ .ml_1(),
)
.child(
Icon::new(IconName::ChevronDown)
diff --git a/crates/agent/src/assistant_panel.rs b/crates/agent/src/assistant_panel.rs
index 75f4db9ff3..fa953d93a8 100644
--- a/crates/agent/src/assistant_panel.rs
+++ b/crates/agent/src/assistant_panel.rs
@@ -44,8 +44,8 @@ use crate::thread::{Thread, ThreadError, ThreadId, TokenUsageRatio};
use crate::thread_history::{PastContext, PastThread, ThreadHistory};
use crate::thread_store::ThreadStore;
use crate::{
- AgentDiff, InlineAssistant, NewTextThread, NewThread, OpenActiveThreadAsMarkdown,
- OpenAgentDiff, OpenHistory, ThreadEvent, ToggleContextPicker,
+ AgentDiff, ExpandMessageEditor, InlineAssistant, NewTextThread, NewThread,
+ OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ThreadEvent, ToggleContextPicker,
};
pub fn init(cx: &mut App) {
@@ -90,6 +90,16 @@ pub fn init(cx: &mut App) {
let thread = panel.read(cx).thread.read(cx).thread().clone();
AgentDiff::deploy_in_workspace(thread, workspace, window, cx);
}
+ })
+ .register_action(|workspace, _: &ExpandMessageEditor, window, cx| {
+ if let Some(panel) = workspace.panel::(cx) {
+ workspace.focus_panel::(window, cx);
+ panel.update(cx, |panel, cx| {
+ panel.message_editor.update(cx, |editor, cx| {
+ editor.expand_message_editor(&ExpandMessageEditor, window, cx);
+ });
+ });
+ }
});
},
)
@@ -193,7 +203,7 @@ impl AssistantPanel {
cx: AsyncWindowContext,
) -> Task>> {
cx.spawn(async move |cx| {
- let tools = Arc::new(ToolWorkingSet::default());
+ let tools = cx.new(|_| ToolWorkingSet::default())?;
let thread_store = workspace
.update(cx, |workspace, cx| {
let project = workspace.project().clone();
@@ -559,6 +569,7 @@ impl AssistantPanel {
ActiveView::Configuration | ActiveView::History => {
self.active_view =
ActiveView::thread(self.thread.read(cx).thread().clone(), window, cx);
+ self.message_editor.focus_handle(cx).focus(window);
cx.notify();
}
_ => {}
@@ -1088,20 +1099,30 @@ impl AssistantPanel {
window,
cx,
|menu, _window, _cx| {
- menu.action(
+ menu
+ .when(!is_empty, |menu| {
+ menu.action(
+ "Start New From Summary",
+ Box::new(NewThread {
+ from_thread_id: Some(thread_id.clone()),
+ }),
+ ).separator()
+ })
+ .action(
"New Text Thread",
NewTextThread.boxed_clone(),
)
- .when(!is_empty, |menu| {
- menu.action(
- "Continue in New Thread",
- Box::new(NewThread {
- from_thread_id: Some(thread_id.clone()),
- }),
- )
- })
- .separator()
.action("Settings", OpenConfiguration.boxed_clone())
+ .separator()
+ .action(
+ "Install MCPs",
+ zed_actions::Extensions {
+ category_filter: Some(
+ zed_actions::ExtensionCategoryFilter::ContextServers,
+ ),
+ }
+ .boxed_clone(),
+ )
},
))
}),
diff --git a/crates/agent/src/context_picker.rs b/crates/agent/src/context_picker.rs
index bcbee38b73..9e578c4fc0 100644
--- a/crates/agent/src/context_picker.rs
+++ b/crates/agent/src/context_picker.rs
@@ -34,12 +34,6 @@ use crate::context_store::ContextStore;
use crate::thread::ThreadId;
use crate::thread_store::ThreadStore;
-#[derive(Debug, Clone, Copy)]
-pub enum ConfirmBehavior {
- KeepOpen,
- Close,
-}
-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ContextPickerMode {
File,
@@ -105,7 +99,6 @@ pub(super) struct ContextPicker {
workspace: WeakEntity,
context_store: WeakEntity,
thread_store: Option>,
- confirm_behavior: ConfirmBehavior,
_subscriptions: Vec,
}
@@ -114,7 +107,6 @@ impl ContextPicker {
workspace: WeakEntity,
thread_store: Option>,
context_store: WeakEntity,
- confirm_behavior: ConfirmBehavior,
window: &mut Window,
cx: &mut Context,
) -> Self {
@@ -143,7 +135,6 @@ impl ContextPicker {
workspace,
context_store,
thread_store,
- confirm_behavior,
_subscriptions: subscriptions,
}
}
@@ -166,37 +157,32 @@ impl ContextPicker {
let modes = supported_context_picker_modes(&self.thread_store);
- let menu = menu
- .when(has_recent, |menu| {
- menu.custom_row(|_, _| {
- div()
- .mb_1()
- .child(
- Label::new("Recent")
- .color(Color::Muted)
- .size(LabelSize::Small),
- )
- .into_any_element()
- })
+ menu.when(has_recent, |menu| {
+ menu.custom_row(|_, _| {
+ div()
+ .mb_1()
+ .child(
+ Label::new("Recent")
+ .color(Color::Muted)
+ .size(LabelSize::Small),
+ )
+ .into_any_element()
})
- .extend(recent_entries)
- .when(has_recent, |menu| menu.separator())
- .extend(modes.into_iter().map(|mode| {
- let context_picker = context_picker.clone();
+ })
+ .extend(recent_entries)
+ .when(has_recent, |menu| menu.separator())
+ .extend(modes.into_iter().map(|mode| {
+ let context_picker = context_picker.clone();
- ContextMenuEntry::new(mode.label())
- .icon(mode.icon())
- .icon_size(IconSize::XSmall)
- .icon_color(Color::Muted)
- .handler(move |window, cx| {
- context_picker.update(cx, |this, cx| this.select_mode(mode, window, cx))
- })
- }));
-
- match self.confirm_behavior {
- ConfirmBehavior::KeepOpen => menu.keep_open_on_confirm(),
- ConfirmBehavior::Close => menu,
- }
+ ContextMenuEntry::new(mode.label())
+ .icon(mode.icon())
+ .icon_size(IconSize::XSmall)
+ .icon_color(Color::Muted)
+ .handler(move |window, cx| {
+ context_picker.update(cx, |this, cx| this.select_mode(mode, window, cx))
+ })
+ }))
+ .keep_open_on_confirm()
});
cx.subscribe(&menu, move |_, _, _: &DismissEvent, cx| {
@@ -227,7 +213,6 @@ impl ContextPicker {
context_picker.clone(),
self.workspace.clone(),
self.context_store.clone(),
- self.confirm_behavior,
window,
cx,
)
@@ -239,7 +224,6 @@ impl ContextPicker {
context_picker.clone(),
self.workspace.clone(),
self.context_store.clone(),
- self.confirm_behavior,
window,
cx,
)
@@ -251,7 +235,6 @@ impl ContextPicker {
context_picker.clone(),
self.workspace.clone(),
self.context_store.clone(),
- self.confirm_behavior,
window,
cx,
)
@@ -264,7 +247,6 @@ impl ContextPicker {
thread_store.clone(),
context_picker.clone(),
self.context_store.clone(),
- self.confirm_behavior,
window,
cx,
)
diff --git a/crates/agent/src/context_picker/fetch_context_picker.rs b/crates/agent/src/context_picker/fetch_context_picker.rs
index c4a9dd1211..5c7795237b 100644
--- a/crates/agent/src/context_picker/fetch_context_picker.rs
+++ b/crates/agent/src/context_picker/fetch_context_picker.rs
@@ -11,7 +11,7 @@ use picker::{Picker, PickerDelegate};
use ui::{Context, ListItem, Window, prelude::*};
use workspace::Workspace;
-use crate::context_picker::{ConfirmBehavior, ContextPicker};
+use crate::context_picker::ContextPicker;
use crate::context_store::ContextStore;
pub struct FetchContextPicker {
@@ -23,16 +23,10 @@ impl FetchContextPicker {
context_picker: WeakEntity,
workspace: WeakEntity,
context_store: WeakEntity,
- confirm_behavior: ConfirmBehavior,
window: &mut Window,
cx: &mut Context,
) -> Self {
- let delegate = FetchContextPickerDelegate::new(
- context_picker,
- workspace,
- context_store,
- confirm_behavior,
- );
+ let delegate = FetchContextPickerDelegate::new(context_picker, workspace, context_store);
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
Self { picker }
@@ -62,7 +56,6 @@ pub struct FetchContextPickerDelegate {
context_picker: WeakEntity,
workspace: WeakEntity,
context_store: WeakEntity,
- confirm_behavior: ConfirmBehavior,
url: String,
}
@@ -71,13 +64,11 @@ impl FetchContextPickerDelegate {
context_picker: WeakEntity,
workspace: WeakEntity,
context_store: WeakEntity,
- confirm_behavior: ConfirmBehavior,
) -> Self {
FetchContextPickerDelegate {
context_picker,
workspace,
context_store,
- confirm_behavior,
url: String::new(),
}
}
@@ -204,25 +195,15 @@ impl PickerDelegate for FetchContextPickerDelegate {
let http_client = workspace.read(cx).client().http_client().clone();
let url = self.url.clone();
- let confirm_behavior = self.confirm_behavior;
cx.spawn_in(window, async move |this, cx| {
let text = cx
.background_spawn(fetch_url_content(http_client, url.clone()))
.await?;
- this.update_in(cx, |this, window, cx| {
- this.delegate
- .context_store
- .update(cx, |context_store, cx| {
- context_store.add_fetched_url(url, text, cx)
- })?;
-
- match confirm_behavior {
- ConfirmBehavior::KeepOpen => {}
- ConfirmBehavior::Close => this.delegate.dismissed(window, cx),
- }
-
- anyhow::Ok(())
+ this.update(cx, |this, cx| {
+ this.delegate.context_store.update(cx, |context_store, cx| {
+ context_store.add_fetched_url(url, text, cx)
+ })
})??;
anyhow::Ok(())
diff --git a/crates/agent/src/context_picker/file_context_picker.rs b/crates/agent/src/context_picker/file_context_picker.rs
index 965f4a530e..5981b471c2 100644
--- a/crates/agent/src/context_picker/file_context_picker.rs
+++ b/crates/agent/src/context_picker/file_context_picker.rs
@@ -11,9 +11,9 @@ use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, ProjectPath, WorktreeId};
use ui::{ListItem, Tooltip, prelude::*};
use util::ResultExt as _;
-use workspace::{Workspace, notifications::NotifyResultExt};
+use workspace::Workspace;
-use crate::context_picker::{ConfirmBehavior, ContextPicker};
+use crate::context_picker::ContextPicker;
use crate::context_store::{ContextStore, FileInclusion};
pub struct FileContextPicker {
@@ -25,16 +25,10 @@ impl FileContextPicker {
context_picker: WeakEntity,
workspace: WeakEntity,
context_store: WeakEntity,
- confirm_behavior: ConfirmBehavior,
window: &mut Window,
cx: &mut Context,
) -> Self {
- let delegate = FileContextPickerDelegate::new(
- context_picker,
- workspace,
- context_store,
- confirm_behavior,
- );
+ let delegate = FileContextPickerDelegate::new(context_picker, workspace, context_store);
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
Self { picker }
@@ -57,7 +51,6 @@ pub struct FileContextPickerDelegate {
context_picker: WeakEntity,
workspace: WeakEntity,
context_store: WeakEntity,
- confirm_behavior: ConfirmBehavior,
matches: Vec,
selected_index: usize,
}
@@ -67,13 +60,11 @@ impl FileContextPickerDelegate {
context_picker: WeakEntity,
workspace: WeakEntity,
context_store: WeakEntity,
- confirm_behavior: ConfirmBehavior,
) -> Self {
Self {
context_picker,
workspace,
context_store,
- confirm_behavior,
matches: Vec::new(),
selected_index: 0,
}
@@ -127,7 +118,7 @@ impl PickerDelegate for FileContextPickerDelegate {
})
}
- fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context>) {
+ fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context>) {
let Some(FileMatch { mat, .. }) = self.matches.get(self.selected_index) else {
return;
};
@@ -153,17 +144,7 @@ impl PickerDelegate for FileContextPickerDelegate {
return;
};
- let confirm_behavior = self.confirm_behavior;
- cx.spawn_in(window, async move |this, cx| {
- match task.await.notify_async_err(cx) {
- None => anyhow::Ok(()),
- Some(()) => this.update_in(cx, |this, window, cx| match confirm_behavior {
- ConfirmBehavior::KeepOpen => {}
- ConfirmBehavior::Close => this.delegate.dismissed(window, cx),
- }),
- }
- })
- .detach_and_log_err(cx);
+ task.detach_and_log_err(cx);
}
fn dismissed(&mut self, _: &mut Window, cx: &mut Context>) {
diff --git a/crates/agent/src/context_picker/symbol_context_picker.rs b/crates/agent/src/context_picker/symbol_context_picker.rs
index 608accc098..b76d4a8093 100644
--- a/crates/agent/src/context_picker/symbol_context_picker.rs
+++ b/crates/agent/src/context_picker/symbol_context_picker.rs
@@ -15,7 +15,7 @@ use ui::{ListItem, prelude::*};
use util::ResultExt as _;
use workspace::Workspace;
-use crate::context_picker::{ConfirmBehavior, ContextPicker};
+use crate::context_picker::ContextPicker;
use crate::context_store::ContextStore;
pub struct SymbolContextPicker {
@@ -27,16 +27,10 @@ impl SymbolContextPicker {
context_picker: WeakEntity,
workspace: WeakEntity,
context_store: WeakEntity,
- confirm_behavior: ConfirmBehavior,
window: &mut Window,
cx: &mut Context,
) -> Self {
- let delegate = SymbolContextPickerDelegate::new(
- context_picker,
- workspace,
- context_store,
- confirm_behavior,
- );
+ let delegate = SymbolContextPickerDelegate::new(context_picker, workspace, context_store);
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
Self { picker }
@@ -59,7 +53,6 @@ pub struct SymbolContextPickerDelegate {
context_picker: WeakEntity,
workspace: WeakEntity,
context_store: WeakEntity,
- confirm_behavior: ConfirmBehavior,
matches: Vec,
selected_index: usize,
}
@@ -69,13 +62,11 @@ impl SymbolContextPickerDelegate {
context_picker: WeakEntity,
workspace: WeakEntity,
context_store: WeakEntity,
- confirm_behavior: ConfirmBehavior,
) -> Self {
Self {
context_picker,
workspace,
context_store,
- confirm_behavior,
matches: Vec::new(),
selected_index: 0,
}
@@ -135,7 +126,7 @@ impl PickerDelegate for SymbolContextPickerDelegate {
})
}
- fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context>) {
+ fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context>) {
let Some(mat) = self.matches.get(self.selected_index) else {
return;
};
@@ -143,7 +134,6 @@ impl PickerDelegate for SymbolContextPickerDelegate {
return;
};
- let confirm_behavior = self.confirm_behavior;
let add_symbol_task = add_symbol(
mat.symbol.clone(),
true,
@@ -153,16 +143,12 @@ impl PickerDelegate for SymbolContextPickerDelegate {
);
let selected_index = self.selected_index;
- cx.spawn_in(window, async move |this, cx| {
+ cx.spawn(async move |this, cx| {
let included = add_symbol_task.await?;
- this.update_in(cx, |this, window, cx| {
+ this.update(cx, |this, _| {
if let Some(mat) = this.delegate.matches.get_mut(selected_index) {
mat.is_included = included;
}
- match confirm_behavior {
- ConfirmBehavior::KeepOpen => {}
- ConfirmBehavior::Close => this.delegate.dismissed(window, cx),
- }
})
})
.detach_and_log_err(cx);
diff --git a/crates/agent/src/context_picker/thread_context_picker.rs b/crates/agent/src/context_picker/thread_context_picker.rs
index 98f62b3073..941926a898 100644
--- a/crates/agent/src/context_picker/thread_context_picker.rs
+++ b/crates/agent/src/context_picker/thread_context_picker.rs
@@ -6,7 +6,7 @@ use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
use picker::{Picker, PickerDelegate};
use ui::{ListItem, prelude::*};
-use crate::context_picker::{ConfirmBehavior, ContextPicker};
+use crate::context_picker::ContextPicker;
use crate::context_store::{self, ContextStore};
use crate::thread::ThreadId;
use crate::thread_store::ThreadStore;
@@ -20,16 +20,11 @@ impl ThreadContextPicker {
thread_store: WeakEntity,
context_picker: WeakEntity,
context_store: WeakEntity,
- confirm_behavior: ConfirmBehavior,
window: &mut Window,
cx: &mut Context,
) -> Self {
- let delegate = ThreadContextPickerDelegate::new(
- thread_store,
- context_picker,
- context_store,
- confirm_behavior,
- );
+ let delegate =
+ ThreadContextPickerDelegate::new(thread_store, context_picker, context_store);
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
ThreadContextPicker { picker }
@@ -58,7 +53,6 @@ pub struct ThreadContextPickerDelegate {
thread_store: WeakEntity,
context_picker: WeakEntity,
context_store: WeakEntity,
- confirm_behavior: ConfirmBehavior,
matches: Vec,
selected_index: usize,
}
@@ -68,13 +62,11 @@ impl ThreadContextPickerDelegate {
thread_store: WeakEntity,
context_picker: WeakEntity,
context_store: WeakEntity,
- confirm_behavior: ConfirmBehavior,
) -> Self {
ThreadContextPickerDelegate {
thread_store,
context_picker,
context_store,
- confirm_behavior,
matches: Vec::new(),
selected_index: 0,
}
@@ -127,7 +119,7 @@ impl PickerDelegate for ThreadContextPickerDelegate {
})
}
- fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context>) {
+ fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context>) {
let Some(entry) = self.matches.get(self.selected_index) else {
return;
};
@@ -138,20 +130,15 @@ impl PickerDelegate for ThreadContextPickerDelegate {
let open_thread_task = thread_store.update(cx, |this, cx| this.open_thread(&entry.id, cx));
- cx.spawn_in(window, async move |this, cx| {
+ cx.spawn(async move |this, cx| {
let thread = open_thread_task.await?;
- this.update_in(cx, |this, window, cx| {
+ this.update(cx, |this, cx| {
this.delegate
.context_store
.update(cx, |context_store, cx| {
context_store.add_thread(thread, true, cx)
})
.ok();
-
- match this.delegate.confirm_behavior {
- ConfirmBehavior::KeepOpen => {}
- ConfirmBehavior::Close => this.delegate.dismissed(window, cx),
- }
})
})
.detach_and_log_err(cx);
diff --git a/crates/agent/src/context_strip.rs b/crates/agent/src/context_strip.rs
index afc61f46ce..6245f88998 100644
--- a/crates/agent/src/context_strip.rs
+++ b/crates/agent/src/context_strip.rs
@@ -15,7 +15,7 @@ use ui::{KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
use workspace::{Workspace, notifications::NotifyResultExt};
use crate::context::{ContextId, ContextKind};
-use crate::context_picker::{ConfirmBehavior, ContextPicker};
+use crate::context_picker::ContextPicker;
use crate::context_store::ContextStore;
use crate::thread::Thread;
use crate::thread_store::ThreadStore;
@@ -52,7 +52,6 @@ impl ContextStrip {
workspace.clone(),
thread_store.clone(),
context_store.downgrade(),
- ConfirmBehavior::KeepOpen,
window,
cx,
)
diff --git a/crates/agent/src/message_editor.rs b/crates/agent/src/message_editor.rs
index 81adea8945..c61e99ad04 100644
--- a/crates/agent/src/message_editor.rs
+++ b/crates/agent/src/message_editor.rs
@@ -1,6 +1,8 @@
+use std::collections::BTreeMap;
use std::sync::Arc;
use crate::assistant_model_selector::ModelType;
+use buffer_diff::BufferDiff;
use collections::HashSet;
use editor::actions::MoveUp;
use editor::{
@@ -10,8 +12,8 @@ use editor::{
use file_icons::FileIcons;
use fs::Fs;
use gpui::{
- Animation, AnimationExt, App, DismissEvent, Entity, Focusable, Subscription, TextStyle,
- WeakEntity, linear_color_stop, linear_gradient, point, pulsating_between,
+ Animation, AnimationExt, App, Entity, Focusable, Subscription, TextStyle, WeakEntity,
+ linear_color_stop, linear_gradient, point, pulsating_between,
};
use language::{Buffer, Language};
use language_model::{ConfiguredModel, LanguageModelRegistry};
@@ -21,12 +23,12 @@ use project::Project;
use settings::Settings;
use std::time::Duration;
use theme::ThemeSettings;
-use ui::{Disclosure, KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
+use ui::{Disclosure, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*};
use util::ResultExt as _;
use workspace::Workspace;
use crate::assistant_model_selector::AssistantModelSelector;
-use crate::context_picker::{ConfirmBehavior, ContextPicker, ContextPickerCompletionProvider};
+use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider};
use crate::context_store::{ContextStore, refresh_context_store_text};
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
use crate::profile_selector::ProfileSelector;
@@ -46,8 +48,6 @@ pub struct MessageEditor {
context_store: Entity,
context_strip: Entity,
context_picker_menu_handle: PopoverMenuHandle,
- inline_context_picker: Entity,
- inline_context_picker_menu_handle: PopoverMenuHandle,
model_selector: Entity,
profile_selector: Entity,
edits_expanded: bool,
@@ -56,7 +56,7 @@ pub struct MessageEditor {
_subscriptions: Vec,
}
-const MAX_EDITOR_LINES: usize = 10;
+const MAX_EDITOR_LINES: usize = 8;
impl MessageEditor {
pub fn new(
@@ -69,7 +69,6 @@ impl MessageEditor {
cx: &mut Context,
) -> Self {
let context_picker_menu_handle = PopoverMenuHandle::default();
- let inline_context_picker_menu_handle = PopoverMenuHandle::default();
let model_selector_menu_handle = PopoverMenuHandle::default();
let language = Language::new(
@@ -94,6 +93,7 @@ impl MessageEditor {
);
editor.set_placeholder_text("Ask anything, @ to mention, ↑ to select", cx);
editor.set_show_indent_guides(false, cx);
+ editor.set_soft_wrap();
editor.set_context_menu_options(ContextMenuOptions {
min_entries_visible: 12,
max_entries_visible: 12,
@@ -112,17 +112,6 @@ impl MessageEditor {
))));
});
- let inline_context_picker = cx.new(|cx| {
- ContextPicker::new(
- workspace.clone(),
- Some(thread_store.clone()),
- context_store.downgrade(),
- ConfirmBehavior::Close,
- window,
- cx,
- )
- });
-
let context_strip = cx.new(|cx| {
ContextStrip::new(
context_store.clone(),
@@ -135,14 +124,8 @@ impl MessageEditor {
)
});
- let subscriptions = vec![
- cx.subscribe_in(
- &inline_context_picker,
- window,
- Self::handle_inline_context_picker_event,
- ),
- cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event),
- ];
+ let subscriptions =
+ vec![cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event)];
Self {
editor: editor.clone(),
@@ -152,8 +135,6 @@ impl MessageEditor {
context_store,
context_strip,
context_picker_menu_handle,
- inline_context_picker,
- inline_context_picker_menu_handle,
model_selector: cx.new(|cx| {
AssistantModelSelector::new(
fs.clone(),
@@ -177,7 +158,7 @@ impl MessageEditor {
cx.notify();
}
- fn expand_message_editor(
+ pub fn expand_message_editor(
&mut self,
_: &ExpandMessageEditor,
_window: &mut Window,
@@ -316,17 +297,6 @@ impl MessageEditor {
.detach();
}
- fn handle_inline_context_picker_event(
- &mut self,
- _inline_context_picker: &Entity,
- _event: &DismissEvent,
- window: &mut Window,
- cx: &mut Context,
- ) {
- let editor_focus_handle = self.editor.focus_handle(cx);
- window.focus(&editor_focus_handle);
- }
-
fn handle_context_strip_event(
&mut self,
_context_strip: &Entity,
@@ -346,9 +316,7 @@ impl MessageEditor {
}
fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context) {
- if self.context_picker_menu_handle.is_deployed()
- || self.inline_context_picker_menu_handle.is_deployed()
- {
+ if self.context_picker_menu_handle.is_deployed() {
cx.propagate();
} else {
self.context_strip.focus_handle(cx).focus(window);
@@ -371,6 +339,503 @@ impl MessageEditor {
diff.update(cx, |diff, cx| diff.move_to_path(path_key, window, cx));
}
}
+
+ fn render_editor(
+ &self,
+ font_size: Rems,
+ line_height: Pixels,
+ window: &mut Window,
+ cx: &mut Context,
+ ) -> Div {
+ let thread = self.thread.read(cx);
+
+ let editor_bg_color = cx.theme().colors().editor_background;
+ let is_generating = thread.is_generating();
+ let focus_handle = self.editor.focus_handle(cx);
+
+ let is_model_selected = self.is_model_selected(cx);
+ let is_editor_empty = self.is_editor_empty(cx);
+
+ let is_editor_expanded = self.editor_is_expanded;
+ let expand_icon = if is_editor_expanded {
+ IconName::Minimize
+ } else {
+ IconName::Maximize
+ };
+
+ v_flex()
+ .key_context("MessageEditor")
+ .on_action(cx.listener(Self::chat))
+ .on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| {
+ this.profile_selector
+ .read(cx)
+ .menu_handle()
+ .toggle(window, cx);
+ }))
+ .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
+ this.model_selector
+ .update(cx, |model_selector, cx| model_selector.toggle(window, cx));
+ }))
+ .on_action(cx.listener(Self::toggle_context_picker))
+ .on_action(cx.listener(Self::remove_all_context))
+ .on_action(cx.listener(Self::move_up))
+ .on_action(cx.listener(Self::toggle_chat_mode))
+ .on_action(cx.listener(Self::expand_message_editor))
+ .gap_2()
+ .p_2()
+ .bg(editor_bg_color)
+ .border_t_1()
+ .border_color(cx.theme().colors().border)
+ .child(
+ h_flex()
+ .items_start()
+ .justify_between()
+ .child(self.context_strip.clone())
+ .child(
+ IconButton::new("toggle-height", expand_icon)
+ .icon_size(IconSize::XSmall)
+ .icon_color(Color::Muted)
+ .tooltip({
+ let focus_handle = focus_handle.clone();
+ move |window, cx| {
+ let expand_label = if is_editor_expanded {
+ "Minimize Message Editor".to_string()
+ } else {
+ "Expand Message Editor".to_string()
+ };
+
+ Tooltip::for_action_in(
+ expand_label,
+ &ExpandMessageEditor,
+ &focus_handle,
+ window,
+ cx,
+ )
+ }
+ })
+ .on_click(cx.listener(|_, _, window, cx| {
+ window.dispatch_action(Box::new(ExpandMessageEditor), cx);
+ })),
+ ),
+ )
+ .child(
+ v_flex()
+ .size_full()
+ .gap_4()
+ .when(is_editor_expanded, |this| {
+ this.h(vh(0.8, window)).justify_between()
+ })
+ .child(
+ div()
+ .min_h_16()
+ .when(is_editor_expanded, |this| this.h_full())
+ .child({
+ let settings = ThemeSettings::get_global(cx);
+
+ let text_style = TextStyle {
+ color: cx.theme().colors().text,
+ font_family: settings.buffer_font.family.clone(),
+ font_fallbacks: settings.buffer_font.fallbacks.clone(),
+ font_features: settings.buffer_font.features.clone(),
+ font_size: font_size.into(),
+ line_height: line_height.into(),
+ ..Default::default()
+ };
+
+ EditorElement::new(
+ &self.editor,
+ EditorStyle {
+ background: editor_bg_color,
+ local_player: cx.theme().players().local(),
+ text: text_style,
+ syntax: cx.theme().syntax().clone(),
+ ..Default::default()
+ },
+ )
+ .into_any()
+ }),
+ )
+ .child(
+ h_flex()
+ .flex_none()
+ .justify_between()
+ .child(h_flex().gap_2().child(self.profile_selector.clone()))
+ .child(h_flex().gap_1().child(self.model_selector.clone()).map({
+ let focus_handle = focus_handle.clone();
+ move |parent| {
+ if is_generating {
+ parent.child(
+ IconButton::new(
+ "stop-generation",
+ IconName::StopFilled,
+ )
+ .icon_color(Color::Error)
+ .style(ButtonStyle::Tinted(ui::TintColor::Error))
+ .tooltip(move |window, cx| {
+ Tooltip::for_action(
+ "Stop Generation",
+ &editor::actions::Cancel,
+ window,
+ cx,
+ )
+ })
+ .on_click({
+ let focus_handle = focus_handle.clone();
+ move |_event, window, cx| {
+ focus_handle.dispatch_action(
+ &editor::actions::Cancel,
+ window,
+ cx,
+ );
+ }
+ })
+ .with_animation(
+ "pulsating-label",
+ Animation::new(Duration::from_secs(2))
+ .repeat()
+ .with_easing(pulsating_between(0.4, 1.0)),
+ |icon_button, delta| icon_button.alpha(delta),
+ ),
+ )
+ } else {
+ parent.child(
+ IconButton::new("send-message", IconName::Send)
+ .icon_color(Color::Accent)
+ .style(ButtonStyle::Filled)
+ .disabled(
+ is_editor_empty
+ || !is_model_selected
+ || self.waiting_for_summaries_to_send,
+ )
+ .on_click({
+ let focus_handle = focus_handle.clone();
+ move |_event, window, cx| {
+ focus_handle
+ .dispatch_action(&Chat, window, cx);
+ }
+ })
+ .when(
+ !is_editor_empty && is_model_selected,
+ |button| {
+ button.tooltip(move |window, cx| {
+ Tooltip::for_action(
+ "Send", &Chat, window, cx,
+ )
+ })
+ },
+ )
+ .when(is_editor_empty, |button| {
+ button.tooltip(Tooltip::text(
+ "Type a message to submit",
+ ))
+ })
+ .when(!is_model_selected, |button| {
+ button.tooltip(Tooltip::text(
+ "Select a model to continue",
+ ))
+ }),
+ )
+ }
+ }
+ })),
+ ),
+ )
+ }
+
+ fn render_changed_buffers(
+ &self,
+ changed_buffers: &BTreeMap, Entity>,
+ window: &mut Window,
+ cx: &mut Context,
+ ) -> Div {
+ let focus_handle = self.editor.focus_handle(cx);
+
+ let editor_bg_color = cx.theme().colors().editor_background;
+ let border_color = cx.theme().colors().border;
+ let active_color = cx.theme().colors().element_selected;
+ let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
+ let is_edit_changes_expanded = self.edits_expanded;
+
+ v_flex()
+ .mx_2()
+ .bg(bg_edit_files_disclosure)
+ .border_1()
+ .border_b_0()
+ .border_color(border_color)
+ .rounded_t_md()
+ .shadow(smallvec::smallvec![gpui::BoxShadow {
+ color: gpui::black().opacity(0.15),
+ offset: point(px(1.), px(-1.)),
+ blur_radius: px(3.),
+ spread_radius: px(0.),
+ }])
+ .child(
+ h_flex()
+ .id("edits-container")
+ .cursor_pointer()
+ .p_1p5()
+ .justify_between()
+ .when(is_edit_changes_expanded, |this| {
+ this.border_b_1().border_color(border_color)
+ })
+ .on_click(
+ cx.listener(|this, _, window, cx| this.handle_review_click(window, cx)),
+ )
+ .child(
+ h_flex()
+ .gap_1()
+ .child(
+ Disclosure::new("edits-disclosure", is_edit_changes_expanded)
+ .on_click(cx.listener(|this, _ev, _window, cx| {
+ this.edits_expanded = !this.edits_expanded;
+ cx.notify();
+ })),
+ )
+ .child(
+ Label::new("Edits")
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ )
+ .child(Label::new("•").size(LabelSize::XSmall).color(Color::Muted))
+ .child(
+ Label::new(format!(
+ "{} {}",
+ changed_buffers.len(),
+ if changed_buffers.len() == 1 {
+ "file"
+ } else {
+ "files"
+ }
+ ))
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ ),
+ )
+ .child(
+ Button::new("review", "Review Changes")
+ .label_size(LabelSize::Small)
+ .key_binding(
+ KeyBinding::for_action_in(
+ &OpenAgentDiff,
+ &focus_handle,
+ window,
+ cx,
+ )
+ .map(|kb| kb.size(rems_from_px(12.))),
+ )
+ .on_click(cx.listener(|this, _, window, cx| {
+ this.handle_review_click(window, cx)
+ })),
+ ),
+ )
+ .when(is_edit_changes_expanded, |parent| {
+ parent.child(
+ v_flex().children(changed_buffers.into_iter().enumerate().flat_map(
+ |(index, (buffer, _diff))| {
+ let file = buffer.read(cx).file()?;
+ let path = file.path();
+
+ let parent_label = path.parent().and_then(|parent| {
+ let parent_str = parent.to_string_lossy();
+
+ if parent_str.is_empty() {
+ None
+ } else {
+ Some(
+ Label::new(format!(
+ "/{}{}",
+ parent_str,
+ std::path::MAIN_SEPARATOR_STR
+ ))
+ .color(Color::Muted)
+ .size(LabelSize::XSmall)
+ .buffer_font(cx),
+ )
+ }
+ });
+
+ let name_label = path.file_name().map(|name| {
+ Label::new(name.to_string_lossy().to_string())
+ .size(LabelSize::XSmall)
+ .buffer_font(cx)
+ });
+
+ let file_icon = FileIcons::get_icon(&path, cx)
+ .map(Icon::from_path)
+ .map(|icon| icon.color(Color::Muted).size(IconSize::Small))
+ .unwrap_or_else(|| {
+ Icon::new(IconName::File)
+ .color(Color::Muted)
+ .size(IconSize::Small)
+ });
+
+ let hover_color = cx
+ .theme()
+ .colors()
+ .element_background
+ .blend(cx.theme().colors().editor_foreground.opacity(0.025));
+
+ let overlay_gradient = linear_gradient(
+ 90.,
+ linear_color_stop(editor_bg_color, 1.),
+ linear_color_stop(editor_bg_color.opacity(0.2), 0.),
+ );
+
+ let overlay_gradient_hover = linear_gradient(
+ 90.,
+ linear_color_stop(hover_color, 1.),
+ linear_color_stop(hover_color.opacity(0.2), 0.),
+ );
+
+ let element = h_flex()
+ .group("edited-code")
+ .id(("file-container", index))
+ .cursor_pointer()
+ .relative()
+ .py_1()
+ .pl_2()
+ .pr_1()
+ .gap_2()
+ .justify_between()
+ .bg(cx.theme().colors().editor_background)
+ .hover(|style| style.bg(hover_color))
+ .when(index + 1 < changed_buffers.len(), |parent| {
+ parent.border_color(border_color).border_b_1()
+ })
+ .child(
+ h_flex()
+ .id("file-name")
+ .pr_8()
+ .gap_1p5()
+ .max_w_full()
+ .overflow_x_scroll()
+ .child(file_icon)
+ .child(
+ h_flex()
+ .gap_0p5()
+ .children(name_label)
+ .children(parent_label),
+ ) // TODO: show lines changed
+ .child(Label::new("+").color(Color::Created))
+ .child(Label::new("-").color(Color::Deleted)),
+ )
+ .child(
+ div().visible_on_hover("edited-code").child(
+ Button::new("review", "Review")
+ .label_size(LabelSize::Small)
+ .on_click({
+ let buffer = buffer.clone();
+ cx.listener(move |this, _, window, cx| {
+ this.handle_file_click(
+ buffer.clone(),
+ window,
+ cx,
+ );
+ })
+ }),
+ ),
+ )
+ .child(
+ div()
+ .id("gradient-overlay")
+ .absolute()
+ .h_5_6()
+ .w_12()
+ .bottom_0()
+ .right(px(52.))
+ .bg(overlay_gradient)
+ .group_hover("edited-code", |style| {
+ style.bg(overlay_gradient_hover)
+ }),
+ )
+ .on_click({
+ let buffer = buffer.clone();
+ cx.listener(move |this, _, window, cx| {
+ this.handle_file_click(buffer.clone(), window, cx);
+ })
+ });
+
+ Some(element)
+ },
+ )),
+ )
+ })
+ }
+
+ fn render_token_limit_callout(
+ &self,
+ line_height: Pixels,
+ token_usage_ratio: TokenUsageRatio,
+ cx: &mut Context,
+ ) -> Div {
+ let heading = if token_usage_ratio == TokenUsageRatio::Exceeded {
+ "Thread reached the token limit"
+ } else {
+ "Thread reaching the token limit soon"
+ };
+
+ h_flex()
+ .p_2()
+ .gap_2()
+ .flex_wrap()
+ .justify_between()
+ .bg(
+ if token_usage_ratio == TokenUsageRatio::Exceeded {
+ cx.theme().status().error_background.opacity(0.1)
+ } else {
+ cx.theme().status().warning_background.opacity(0.1)
+ })
+ .border_t_1()
+ .border_color(cx.theme().colors().border)
+ .child(
+ h_flex()
+ .gap_2()
+ .items_start()
+ .child(
+ h_flex()
+ .h(line_height)
+ .justify_center()
+ .child(
+ if token_usage_ratio == TokenUsageRatio::Exceeded {
+ Icon::new(IconName::X)
+ .color(Color::Error)
+ .size(IconSize::XSmall)
+ } else {
+ Icon::new(IconName::Warning)
+ .color(Color::Warning)
+ .size(IconSize::XSmall)
+ }
+ ),
+ )
+ .child(
+ v_flex()
+ .mr_auto()
+ .child(Label::new(heading).size(LabelSize::Small))
+ .child(
+ Label::new(
+ "Start a new thread from a summary to continue the conversation.",
+ )
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ ),
+ ),
+ )
+ .child(
+ Button::new("new-thread", "Start New Thread")
+ .on_click(cx.listener(|this, _, window, cx| {
+ let from_thread_id = Some(this.thread.read(cx).id().clone());
+
+ window.dispatch_action(Box::new(NewThread {
+ from_thread_id
+ }), cx);
+ }))
+ .icon(IconName::Plus)
+ .icon_position(IconPosition::Start)
+ .icon_size(IconSize::Small)
+ .style(ButtonStyle::Tinted(ui::TintColor::Accent))
+ .label_size(LabelSize::Small),
+ )
+ }
}
impl Focusable for MessageEditor {
@@ -381,35 +846,14 @@ impl Focusable for MessageEditor {
impl Render for MessageEditor {
fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement {
- let font_size = TextSize::Small.rems(cx);
- let line_height = font_size.to_pixels(window.rem_size()) * 1.5;
-
- let focus_handle = self.editor.focus_handle(cx);
- let focus_handle_clone = focus_handle.clone();
- let inline_context_picker = self.inline_context_picker.clone();
-
- let is_editor_expanded = self.editor_is_expanded;
- let expand_icon = if is_editor_expanded {
- IconName::Minimize
- } else {
- IconName::Maximize
- };
-
let thread = self.thread.read(cx);
- let is_generating = thread.is_generating();
let total_token_usage = thread.total_token_usage(cx);
- let is_model_selected = self.is_model_selected(cx);
- let is_editor_empty = self.is_editor_empty(cx);
- let is_edit_changes_expanded = self.edits_expanded;
let action_log = self.thread.read(cx).action_log();
let changed_buffers = action_log.read(cx).changed_buffers(cx);
- let changed_buffers_count = changed_buffers.len();
- let editor_bg_color = cx.theme().colors().editor_background;
- let border_color = cx.theme().colors().border;
- let active_color = cx.theme().colors().element_selected;
- let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
+ let font_size = TextSize::Small.rems(cx);
+ let line_height = font_size.to_pixels(window.rem_size()) * 1.5;
v_flex()
.size_full()
@@ -420,7 +864,7 @@ impl Render for MessageEditor {
.flex_none()
.px_2()
.py_2()
- .bg(editor_bg_color)
+ .bg(cx.theme().colors().editor_background)
.border_1()
.border_color(cx.theme().colors().border_variant)
.rounded_lg()
@@ -448,477 +892,19 @@ impl Render for MessageEditor {
),
)
})
- .when(changed_buffers_count > 0, |parent| {
- parent.child(
- v_flex()
- .mx_2()
- .bg(bg_edit_files_disclosure)
- .border_1()
- .border_b_0()
- .border_color(border_color)
- .rounded_t_md()
- .shadow(smallvec::smallvec![gpui::BoxShadow {
- color: gpui::black().opacity(0.15),
- offset: point(px(1.), px(-1.)),
- blur_radius: px(3.),
- spread_radius: px(0.),
- }])
- .child(
- h_flex()
- .id("edits-container")
- .cursor_pointer()
- .p_1p5()
- .justify_between()
- .when(is_edit_changes_expanded, |this| {
- this.border_b_1().border_color(border_color)
- })
- .on_click(cx.listener(|this, _, window, cx| {
- this.handle_review_click(window, cx)
- }))
- .child(
- h_flex()
- .gap_1()
- .child(
- Disclosure::new(
- "edits-disclosure",
- is_edit_changes_expanded,
- )
- .on_click(
- cx.listener(|this, _ev, _window, cx| {
- this.edits_expanded = !this.edits_expanded;
- cx.notify();
- }),
- ),
- )
- .child(
- Label::new("Edits")
- .size(LabelSize::Small)
- .color(Color::Muted),
- )
- .child(
- Label::new("•")
- .size(LabelSize::XSmall)
- .color(Color::Muted),
- )
- .child(
- Label::new(format!(
- "{} {}",
- changed_buffers_count,
- if changed_buffers_count == 1 {
- "file"
- } else {
- "files"
- }
- ))
- .size(LabelSize::Small)
- .color(Color::Muted),
- ),
- )
- .child(
- Button::new("review", "Review Changes")
- .label_size(LabelSize::Small)
- .key_binding(
- KeyBinding::for_action_in(
- &OpenAgentDiff,
- &focus_handle,
- window,
- cx,
- )
- .map(|kb| kb.size(rems_from_px(12.))),
- )
- .on_click(cx.listener(|this, _, window, cx| {
- this.handle_review_click(window, cx)
- })),
- ),
- )
- .when(is_edit_changes_expanded, |parent| {
- parent.child(
- v_flex().children(
- changed_buffers.into_iter().enumerate().flat_map(
- |(index, (buffer, _diff))| {
- let file = buffer.read(cx).file()?;
- let path = file.path();
-
- let parent_label = path.parent().and_then(|parent| {
- let parent_str = parent.to_string_lossy();
-
- if parent_str.is_empty() {
- None
- } else {
- Some(
- Label::new(format!(
- "/{}{}",
- parent_str,
- std::path::MAIN_SEPARATOR_STR
- ))
- .color(Color::Muted)
- .size(LabelSize::XSmall)
- .buffer_font(cx),
- )
- }
- });
-
- let name_label = path.file_name().map(|name| {
- Label::new(name.to_string_lossy().to_string())
- .size(LabelSize::XSmall)
- .buffer_font(cx)
- });
-
- let file_icon = FileIcons::get_icon(&path, cx)
- .map(Icon::from_path)
- .map(|icon| {
- icon.color(Color::Muted).size(IconSize::Small)
- })
- .unwrap_or_else(|| {
- Icon::new(IconName::File)
- .color(Color::Muted)
- .size(IconSize::Small)
- });
-
- let hover_color = cx.theme()
- .colors()
- .element_background
- .blend(cx.theme().colors().editor_foreground.opacity(0.025));
-
- let overlay_gradient = linear_gradient(
- 90.,
- linear_color_stop(
- editor_bg_color,
- 1.,
- ),
- linear_color_stop(
- editor_bg_color
- .opacity(0.2),
- 0.,
- ),
- );
-
- let overlay_gradient_hover = linear_gradient(
- 90.,
- linear_color_stop(
- hover_color,
- 1.,
- ),
- linear_color_stop(
- hover_color
- .opacity(0.2),
- 0.,
- ),
- );
-
- let element = h_flex()
- .group("edited-code")
- .id(("file-container", index))
- .cursor_pointer()
- .relative()
- .py_1()
- .pl_2()
- .pr_1()
- .gap_2()
- .justify_between()
- .bg(cx.theme().colors().editor_background)
- .hover(|style| style.bg(hover_color))
- .when(index + 1 < changed_buffers_count, |parent| {
- parent.border_color(border_color).border_b_1()
- })
- .child(
- h_flex()
- .id("file-name")
- .pr_8()
- .gap_1p5()
- .max_w_full()
- .overflow_x_scroll()
- .child(file_icon)
- .child(
- h_flex()
- .gap_0p5()
- .children(name_label)
- .children(parent_label)
- ) // TODO: show lines changed
- .child(
- Label::new("+")
- .color(Color::Created),
- )
- .child(
- Label::new("-")
- .color(Color::Deleted),
- ),
- )
- .child(
- div().visible_on_hover("edited-code").child(
- Button::new("review", "Review")
- .label_size(LabelSize::Small)
- .on_click({
- let buffer = buffer.clone();
- cx.listener(move |this, _, window, cx| {
- this.handle_file_click(buffer.clone(), window, cx);
- })
- })
- )
- )
- .child(
- div()
- .id("gradient-overlay")
- .absolute()
- .h_5_6()
- .w_12()
- .bottom_0()
- .right(px(52.))
- .bg(overlay_gradient)
- .group_hover("edited-code", |style| style.bg(overlay_gradient_hover))
- ,
- )
- .on_click({
- let buffer = buffer.clone();
- cx.listener(move |this, _, window, cx| {
- this.handle_file_click(buffer.clone(), window, cx);
- })
- });
-
- Some(element)
- },
- ),
- ),
- )
- }),
- )
+ .when(changed_buffers.len() > 0, |parent| {
+ parent.child(self.render_changed_buffers(&changed_buffers, window, cx))
})
- .child(
- v_flex()
- .key_context("MessageEditor")
- .on_action(cx.listener(Self::chat))
- .on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| {
- this.profile_selector
- .read(cx)
- .menu_handle()
- .toggle(window, cx);
- }))
- .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
- this.model_selector
- .update(cx, |model_selector, cx| model_selector.toggle(window, cx));
- }))
- .on_action(cx.listener(Self::toggle_context_picker))
- .on_action(cx.listener(Self::remove_all_context))
- .on_action(cx.listener(Self::move_up))
- .on_action(cx.listener(Self::toggle_chat_mode))
- .on_action(cx.listener(Self::expand_message_editor))
- .gap_2()
- .p_2()
- .bg(editor_bg_color)
- .border_t_1()
- .border_color(cx.theme().colors().border)
- .child(
- h_flex()
- .items_start()
- .justify_between()
- .child(self.context_strip.clone())
- .child(
- IconButton::new("toggle-height", expand_icon)
- .icon_size(IconSize::XSmall)
- .icon_color(Color::Muted)
- .tooltip(move |window, cx| {
- let focus_handle = focus_handle.clone();
- let expand_label = if is_editor_expanded {
- "Minimize Message Editor".to_string()
- } else {
- "Expand Message Editor".to_string()
- };
-
- Tooltip::for_action_in(
- expand_label,
- &ExpandMessageEditor,
- &focus_handle,
- window,
- cx,
- )
- })
- .on_click(cx.listener(|_, _, window, cx| {
- window.dispatch_action(Box::new(ExpandMessageEditor), cx);
- }))
- )
- )
- .child(
- v_flex()
- .size_full()
- .gap_4()
- .when(is_editor_expanded, |this| this.h(vh(0.8, window)).justify_between())
- .child(div().when(is_editor_expanded, |this| this.h_full()).child({
- let settings = ThemeSettings::get_global(cx);
-
- let text_style = TextStyle {
- color: cx.theme().colors().text,
- font_family: settings.buffer_font.family.clone(),
- font_fallbacks: settings.buffer_font.fallbacks.clone(),
- font_features: settings.buffer_font.features.clone(),
- font_size: font_size.into(),
- line_height: line_height.into(),
- ..Default::default()
- };
-
- EditorElement::new(
- &self.editor,
- EditorStyle {
- background: editor_bg_color,
- local_player: cx.theme().players().local(),
- text: text_style,
- syntax: cx.theme().syntax().clone(),
- ..Default::default()
- },
- ).into_any()
- }))
- .child(
- PopoverMenu::new("inline-context-picker")
- .menu(move |window, cx| {
- inline_context_picker.update(cx, |this, cx| {
- this.init(window, cx);
- });
- Some(inline_context_picker.clone())
- })
- .attach(gpui::Corner::TopLeft)
- .anchor(gpui::Corner::BottomLeft)
- .offset(gpui::Point {
- x: px(0.0),
- y: (-ThemeSettings::get_global(cx).ui_font_size(cx) * 2)
- - px(4.0),
- })
- .with_handle(self.inline_context_picker_menu_handle.clone()),
- )
- .child(
- h_flex()
- .flex_none()
- .justify_between()
- .child(h_flex().gap_2().child(self.profile_selector.clone()))
- .child(
- h_flex().gap_1()
- .child(self.model_selector.clone())
- .map(move |parent| {
- if is_generating {
- parent.child(
- IconButton::new("stop-generation", IconName::StopFilled)
- .icon_color(Color::Error)
- .style(ButtonStyle::Tinted(ui::TintColor::Error))
- .tooltip(move |window, cx| {
- Tooltip::for_action(
- "Stop Generation",
- &editor::actions::Cancel,
- window,
- cx,
- )
- })
- .on_click({
- let focus_handle = focus_handle_clone.clone();
- move |_event, window, cx| {
- focus_handle.dispatch_action(
- &editor::actions::Cancel,
- window,
- cx,
- );
- }
- })
- .with_animation(
- "pulsating-label",
- Animation::new(Duration::from_secs(2))
- .repeat()
- .with_easing(pulsating_between(0.4, 1.0)),
- |icon_button, delta| icon_button.alpha(delta),
- ),
- )
- } else {
- parent.child(
- IconButton::new("send-message", IconName::Send)
- .icon_color(Color::Accent)
- .style(ButtonStyle::Filled)
- .disabled(
- is_editor_empty
- || !is_model_selected
- || self.waiting_for_summaries_to_send
- )
- .on_click({
- let focus_handle = focus_handle_clone.clone();
- move |_event, window, cx| {
- focus_handle.dispatch_action(&Chat, window, cx);
- }
- })
- .when(!is_editor_empty && is_model_selected, |button| {
- button.tooltip(move |window, cx| {
- Tooltip::for_action(
- "Send",
- &Chat,
- window,
- cx,
- )
- })
- })
- .when(is_editor_empty, |button| {
- button.tooltip(Tooltip::text(
- "Type a message to submit",
- ))
- })
- .when(!is_model_selected, |button| {
- button.tooltip(Tooltip::text(
- "Select a model to continue",
- ))
- })
- )
- }
- })
- ),
- ),
- )
+ .child(self.render_editor(font_size, line_height, window, cx))
+ .when(
+ total_token_usage.ratio != TokenUsageRatio::Normal,
+ |parent| {
+ parent.child(self.render_token_limit_callout(
+ line_height,
+ total_token_usage.ratio,
+ cx,
+ ))
+ },
)
- .when(total_token_usage.ratio != TokenUsageRatio::Normal, |parent| {
- parent.child(
- h_flex()
- .p_2()
- .gap_2()
- .flex_wrap()
- .justify_between()
- .bg(cx.theme().status().warning_background.opacity(0.1))
- .border_t_1()
- .border_color(cx.theme().colors().border)
- .child(
- h_flex()
- .gap_2()
- .items_start()
- .child(
- h_flex()
- .h(line_height)
- .justify_center()
- .child(
- Icon::new(IconName::Warning)
- .color(Color::Warning)
- .size(IconSize::XSmall),
- ),
- )
- .child(
- v_flex()
- .mr_auto()
- .child(Label::new("Thread reaching the token limit soon").size(LabelSize::Small))
- .child(
- Label::new(
- "Start a new thread from a summary to continue the conversation.",
- )
- .size(LabelSize::Small)
- .color(Color::Muted),
- ),
- ),
- )
- .child(
- Button::new("new-thread", "Start New Thread")
- .on_click(cx.listener(|this, _, window, cx| {
- let from_thread_id = Some(this.thread.read(cx).id().clone());
-
- window.dispatch_action(Box::new(NewThread {
- from_thread_id
- }), cx);
- }))
- .icon(IconName::Plus)
- .icon_position(IconPosition::Start)
- .icon_size(IconSize::Small)
- .style(ButtonStyle::Tinted(ui::TintColor::Accent))
- .label_size(LabelSize::Small),
- ),
- )
- })
}
}
diff --git a/crates/agent/src/profile_selector.rs b/crates/agent/src/profile_selector.rs
index dfcafba5fc..c033bf9c58 100644
--- a/crates/agent/src/profile_selector.rs
+++ b/crates/agent/src/profile_selector.rs
@@ -86,7 +86,7 @@ impl ProfileSelector {
thread_store
.update(cx, |this, cx| {
- this.load_profile_by_id(&profile_id, cx);
+ this.load_profile_by_id(profile_id.clone(), cx);
})
.log_err();
}
diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs
index 749cf25780..80cade75ae 100644
--- a/crates/agent/src/thread.rs
+++ b/crates/agent/src/thread.rs
@@ -15,10 +15,11 @@ use futures::{FutureExt, StreamExt as _};
use git::repository::DiffType;
use gpui::{App, AppContext, Context, Entity, EventEmitter, SharedString, Task, WeakEntity};
use language_model::{
- ConfiguredModel, LanguageModel, LanguageModelCompletionEvent, LanguageModelRegistry,
- LanguageModelRequest, LanguageModelRequestMessage, LanguageModelRequestTool,
- LanguageModelToolResult, LanguageModelToolUseId, MaxMonthlySpendReachedError, MessageContent,
- PaymentRequiredError, Role, StopReason, TokenUsage,
+ ConfiguredModel, LanguageModel, LanguageModelCompletionEvent, LanguageModelId,
+ LanguageModelKnownError, LanguageModelRegistry, LanguageModelRequest,
+ LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
+ LanguageModelToolUseId, MaxMonthlySpendReachedError, MessageContent, PaymentRequiredError,
+ Role, StopReason, TokenUsage,
};
use project::Project;
use project::git_store::{GitStore, GitStoreCheckpoint, RepositoryState};
@@ -228,7 +229,7 @@ pub struct TotalTokenUsage {
pub ratio: TokenUsageRatio,
}
-#[derive(Default, PartialEq, Eq)]
+#[derive(Debug, Default, PartialEq, Eq)]
pub enum TokenUsageRatio {
#[default]
Normal,
@@ -253,22 +254,31 @@ pub struct Thread {
pending_completions: Vec,
project: Entity,
prompt_builder: Arc,
- tools: Arc,
+ tools: Entity,
tool_use: ToolUseState,
action_log: Entity,
last_restore_checkpoint: Option,
pending_checkpoint: Option,
initial_project_snapshot: Shared>>>,
cumulative_token_usage: TokenUsage,
+ exceeded_window_error: Option,
feedback: Option,
message_feedback: HashMap,
last_auto_capture_at: Option,
}
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ExceededWindowError {
+ /// Model used when last message exceeded context window
+ model_id: LanguageModelId,
+ /// Token count including last message
+ token_count: usize,
+}
+
impl Thread {
pub fn new(
project: Entity,
- tools: Arc,
+ tools: Entity,
prompt_builder: Arc,
system_prompt: SharedProjectContext,
cx: &mut Context,
@@ -301,6 +311,7 @@ impl Thread {
.shared()
},
cumulative_token_usage: TokenUsage::default(),
+ exceeded_window_error: None,
feedback: None,
message_feedback: HashMap::default(),
last_auto_capture_at: None,
@@ -311,7 +322,7 @@ impl Thread {
id: ThreadId,
serialized: SerializedThread,
project: Entity,
- tools: Arc,
+ tools: Entity,
prompt_builder: Arc,
project_context: SharedProjectContext,
cx: &mut Context,
@@ -367,6 +378,7 @@ impl Thread {
action_log: cx.new(|_| ActionLog::new(project)),
initial_project_snapshot: Task::ready(serialized.initial_project_snapshot).shared(),
cumulative_token_usage: serialized.cumulative_token_usage,
+ exceeded_window_error: None,
feedback: None,
message_feedback: HashMap::default(),
last_auto_capture_at: None,
@@ -446,7 +458,7 @@ impl Thread {
!self.pending_completions.is_empty() || !self.all_tools_finished()
}
- pub fn tools(&self) -> &Arc {
+ pub fn tools(&self) -> &Entity {
&self.tools
}
@@ -819,8 +831,9 @@ impl Thread {
})
.collect(),
initial_project_snapshot,
- cumulative_token_usage: this.cumulative_token_usage.clone(),
+ cumulative_token_usage: this.cumulative_token_usage,
detailed_summary_state: this.detailed_summary_state.clone(),
+ exceeded_window_error: this.exceeded_window_error.clone(),
})
})
}
@@ -835,13 +848,21 @@ impl Thread {
if model.supports_tools() {
request.tools = {
let mut tools = Vec::new();
- tools.extend(self.tools().enabled_tools(cx).into_iter().map(|tool| {
- LanguageModelRequestTool {
- name: tool.name(),
- description: tool.description(),
- input_schema: tool.input_schema(model.tool_input_format()),
- }
- }));
+ tools.extend(
+ self.tools()
+ .read(cx)
+ .enabled_tools(cx)
+ .into_iter()
+ .filter_map(|tool| {
+ // Skip tools that cannot be supported
+ let input_schema = tool.input_schema(model.tool_input_format()).ok()?;
+ Some(LanguageModelRequestTool {
+ name: tool.name(),
+ description: tool.description(),
+ input_schema,
+ })
+ }),
+ );
tools
};
@@ -1000,7 +1021,7 @@ impl Thread {
let task = cx.spawn(async move |thread, cx| {
let stream = model.stream_completion(request, &cx);
let initial_token_usage =
- thread.read_with(cx, |thread, _cx| thread.cumulative_token_usage.clone());
+ thread.read_with(cx, |thread, _cx| thread.cumulative_token_usage);
let stream_completion = async {
let mut events = stream.await?;
let mut stop_reason = StopReason::EndTurn;
@@ -1022,9 +1043,9 @@ impl Thread {
stop_reason = reason;
}
LanguageModelCompletionEvent::UsageUpdate(token_usage) => {
- thread.cumulative_token_usage =
- thread.cumulative_token_usage.clone() + token_usage.clone()
- - current_token_usage.clone();
+ thread.cumulative_token_usage = thread.cumulative_token_usage
+ + token_usage
+ - current_token_usage;
current_token_usage = token_usage;
}
LanguageModelCompletionEvent::Text(chunk) => {
@@ -1133,6 +1154,20 @@ impl Thread {
cx.emit(ThreadEvent::ShowError(
ThreadError::MaxMonthlySpendReached,
));
+ } else if let Some(known_error) =
+ error.downcast_ref::()
+ {
+ match known_error {
+ LanguageModelKnownError::ContextWindowLimitExceeded {
+ tokens,
+ } => {
+ thread.exceeded_window_error = Some(ExceededWindowError {
+ model_id: model.id(),
+ token_count: *tokens,
+ });
+ cx.notify();
+ }
+ }
} else {
let error_message = error
.chain()
@@ -1153,7 +1188,7 @@ impl Thread {
thread.auto_capture_telemetry(cx);
if let Ok(initial_usage) = initial_token_usage {
- let usage = thread.cumulative_token_usage.clone() - initial_usage;
+ let usage = thread.cumulative_token_usage - initial_usage;
telemetry::event!(
"Assistant Thread Completion",
@@ -1324,7 +1359,7 @@ impl Thread {
.collect::>();
for tool_use in pending_tool_uses.iter() {
- if let Some(tool) = self.tools.tool(&tool_use.name, cx) {
+ if let Some(tool) = self.tools.read(cx).tool(&tool_use.name, cx) {
if tool.needs_confirmation(&tool_use.input, cx)
&& !AssistantSettings::get_global(cx).always_allow_tool_actions
{
@@ -1376,7 +1411,7 @@ impl Thread {
) -> Task<()> {
let tool_name: Arc = tool.name().into();
- let tool_result = if self.tools.is_disabled(&tool.source(), &tool_name) {
+ let tool_result = if self.tools.read(cx).is_disabled(&tool.source(), &tool_name) {
ToolResult {
output: Task::ready(Err(anyhow!("tool is disabled: {tool_name}"))),
card: None,
@@ -1500,6 +1535,7 @@ impl Thread {
let enabled_tool_names: Vec = self
.tools()
+ .read(cx)
.enabled_tools(cx)
.iter()
.map(|tool| tool.name().to_string())
@@ -1797,10 +1833,6 @@ impl Thread {
&self.project
}
- pub fn cumulative_token_usage(&self) -> TokenUsage {
- self.cumulative_token_usage.clone()
- }
-
pub fn auto_capture_telemetry(&mut self, cx: &mut Context) {
if !cx.has_flag::() {
return;
@@ -1845,6 +1877,10 @@ impl Thread {
.detach();
}
+ pub fn cumulative_token_usage(&self) -> TokenUsage {
+ self.cumulative_token_usage
+ }
+
pub fn total_token_usage(&self, cx: &App) -> TotalTokenUsage {
let model_registry = LanguageModelRegistry::read_global(cx);
let Some(model) = model_registry.default_model() else {
@@ -1853,6 +1889,16 @@ impl Thread {
let max = model.model.max_token_count();
+ if let Some(exceeded_error) = &self.exceeded_window_error {
+ if model.model.id() == exceeded_error.model_id {
+ return TotalTokenUsage {
+ total: exceeded_error.token_count,
+ max,
+ ratio: TokenUsageRatio::Exceeded,
+ };
+ }
+ }
+
#[cfg(debug_assertions)]
let warning_threshold: f32 = std::env::var("ZED_THREAD_WARNING_THRESHOLD")
.unwrap_or("0.8".to_string())
@@ -2310,7 +2356,7 @@ fn main() {{
.update(|_, cx| {
ThreadStore::load(
project.clone(),
- Arc::default(),
+ cx.new(|_| ToolWorkingSet::default()),
Arc::new(PromptBuilder::new(None).unwrap()),
cx,
)
diff --git a/crates/agent/src/thread_history.rs b/crates/agent/src/thread_history.rs
index bb0d8ca3fd..ecf5e958a7 100644
--- a/crates/agent/src/thread_history.rs
+++ b/crates/agent/src/thread_history.rs
@@ -4,11 +4,14 @@ use assistant_context_editor::SavedContextMetadata;
use editor::{Editor, EditorEvent};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
- App, Entity, FocusHandle, Focusable, ScrollStrategy, Task, UniformListScrollHandle, WeakEntity,
- Window, uniform_list,
+ App, Entity, FocusHandle, Focusable, ScrollStrategy, Stateful, Task, UniformListScrollHandle,
+ WeakEntity, Window, uniform_list,
};
use time::{OffsetDateTime, UtcOffset};
-use ui::{HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Tooltip, prelude::*};
+use ui::{
+ HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Scrollbar, ScrollbarState,
+ Tooltip, prelude::*,
+};
use util::ResultExt;
use crate::history_store::{HistoryEntry, HistoryStore};
@@ -26,6 +29,8 @@ pub struct ThreadHistory {
matches: Vec,
_subscriptions: Vec,
_search_task: Option>,
+ scrollbar_visibility: bool,
+ scrollbar_state: ScrollbarState,
}
impl ThreadHistory {
@@ -58,10 +63,13 @@ impl ThreadHistory {
this.update_all_entries(cx);
});
+ let scroll_handle = UniformListScrollHandle::default();
+ let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
+
Self {
assistant_panel,
history_store,
- scroll_handle: UniformListScrollHandle::default(),
+ scroll_handle,
selected_index: 0,
search_query: SharedString::new_static(""),
all_entries: entries,
@@ -69,6 +77,8 @@ impl ThreadHistory {
search_editor,
_subscriptions: vec![search_editor_subscription, history_store_subscription],
_search_task: None,
+ scrollbar_visibility: true,
+ scrollbar_state,
}
}
@@ -220,6 +230,43 @@ impl ThreadHistory {
cx.notify();
}
+ fn render_scrollbar(&self, cx: &mut Context) -> Option> {
+ if !(self.scrollbar_visibility || self.scrollbar_state.is_dragging()) {
+ return None;
+ }
+
+ Some(
+ div()
+ .occlude()
+ .id("thread-history-scroll")
+ .h_full()
+ .bg(cx.theme().colors().panel_background.opacity(0.8))
+ .border_l_1()
+ .border_color(cx.theme().colors().border_variant)
+ .absolute()
+ .right_1()
+ .top_0()
+ .bottom_0()
+ .w_4()
+ .pl_1()
+ .cursor_default()
+ .on_mouse_move(cx.listener(|_, _, _window, cx| {
+ cx.notify();
+ cx.stop_propagation()
+ }))
+ .on_hover(|_, _window, cx| {
+ cx.stop_propagation();
+ })
+ .on_any_mouse_down(|_, _window, cx| {
+ cx.stop_propagation();
+ })
+ .on_scroll_wheel(cx.listener(|_, _, _window, cx| {
+ cx.notify();
+ }))
+ .children(Scrollbar::vertical(self.scrollbar_state.clone())),
+ )
+ }
+
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context) {
if let Some(entry) = self.get_match(self.selected_index) {
let task_result = match entry {
@@ -305,7 +352,11 @@ impl Render for ThreadHistory {
)
})
.child({
- let view = v_flex().overflow_hidden().flex_grow();
+ let view = v_flex()
+ .id("list-container")
+ .relative()
+ .overflow_hidden()
+ .flex_grow();
if self.all_entries.is_empty() {
view.justify_center()
@@ -322,59 +373,70 @@ impl Render for ThreadHistory {
),
)
} else {
- view.p_1().child(
- uniform_list(
- cx.entity().clone(),
- "thread-history",
- self.matched_count(),
- move |history, range, _window, _cx| {
- let range_start = range.start;
- let assistant_panel = history.assistant_panel.clone();
+ view.pr_5()
+ .child(
+ uniform_list(
+ cx.entity().clone(),
+ "thread-history",
+ self.matched_count(),
+ move |history, range, _window, _cx| {
+ let range_start = range.start;
+ let assistant_panel = history.assistant_panel.clone();
- let render_item = |index: usize,
- entry: &HistoryEntry,
- highlight_positions: Vec|
- -> Div {
- h_flex().w_full().pb_1().child(match entry {
- HistoryEntry::Thread(thread) => PastThread::new(
- thread.clone(),
- assistant_panel.clone(),
- selected_index == index + range_start,
- highlight_positions,
- )
- .into_any_element(),
- HistoryEntry::Context(context) => PastContext::new(
- context.clone(),
- assistant_panel.clone(),
- selected_index == index + range_start,
- highlight_positions,
- )
- .into_any_element(),
- })
- };
-
- if history.has_search_query() {
- history.matches[range]
- .iter()
- .enumerate()
- .filter_map(|(index, m)| {
- history.all_entries.get(m.candidate_id).map(|entry| {
- render_item(index, entry, m.positions.clone())
- })
+ let render_item = |index: usize,
+ entry: &HistoryEntry,
+ highlight_positions: Vec|
+ -> Div {
+ h_flex().w_full().pb_1().child(match entry {
+ HistoryEntry::Thread(thread) => PastThread::new(
+ thread.clone(),
+ assistant_panel.clone(),
+ selected_index == index + range_start,
+ highlight_positions,
+ )
+ .into_any_element(),
+ HistoryEntry::Context(context) => PastContext::new(
+ context.clone(),
+ assistant_panel.clone(),
+ selected_index == index + range_start,
+ highlight_positions,
+ )
+ .into_any_element(),
})
- .collect()
- } else {
- history.all_entries[range]
- .iter()
- .enumerate()
- .map(|(index, entry)| render_item(index, entry, vec![]))
- .collect()
- }
- },
+ };
+
+ if history.has_search_query() {
+ history.matches[range]
+ .iter()
+ .enumerate()
+ .filter_map(|(index, m)| {
+ history.all_entries.get(m.candidate_id).map(
+ |entry| {
+ render_item(
+ index,
+ entry,
+ m.positions.clone(),
+ )
+ },
+ )
+ })
+ .collect()
+ } else {
+ history.all_entries[range]
+ .iter()
+ .enumerate()
+ .map(|(index, entry)| render_item(index, entry, vec![]))
+ .collect()
+ }
+ },
+ )
+ .p_1()
+ .track_scroll(self.scroll_handle.clone())
+ .flex_grow(),
)
- .track_scroll(self.scroll_handle.clone())
- .flex_grow(),
- )
+ .when_some(self.render_scrollbar(cx), |div, scrollbar| {
+ div.child(scrollbar)
+ })
}
})
}
@@ -440,6 +502,7 @@ impl RenderOnce for PastThread {
IconButton::new("delete", IconName::TrashAlt)
.shape(IconButtonShape::Square)
.icon_size(IconSize::XSmall)
+ .icon_color(Color::Muted)
.tooltip(move |window, cx| {
Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx)
})
@@ -531,6 +594,7 @@ impl RenderOnce for PastContext {
IconButton::new("delete", IconName::TrashAlt)
.shape(IconButtonShape::Square)
.icon_size(IconSize::XSmall)
+ .icon_color(Color::Muted)
.tooltip(move |window, cx| {
Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx)
})
diff --git a/crates/agent/src/thread_store.rs b/crates/agent/src/thread_store.rs
index c8f8d239a2..6fb0f6c7a2 100644
--- a/crates/agent/src/thread_store.rs
+++ b/crates/agent/src/thread_store.rs
@@ -27,7 +27,9 @@ use serde::{Deserialize, Serialize};
use settings::{Settings as _, SettingsStore};
use util::ResultExt as _;
-use crate::thread::{DetailedSummaryState, MessageId, ProjectSnapshot, Thread, ThreadId};
+use crate::thread::{
+ DetailedSummaryState, ExceededWindowError, MessageId, ProjectSnapshot, Thread, ThreadId,
+};
const RULES_FILE_NAMES: [&'static str; 6] = [
".rules",
@@ -54,7 +56,7 @@ impl SharedProjectContext {
pub struct ThreadStore {
project: Entity,
- tools: Arc,
+ tools: Entity,
prompt_builder: Arc,
context_server_manager: Entity,
context_server_tool_ids: HashMap, Vec>,
@@ -72,7 +74,7 @@ impl EventEmitter for ThreadStore {}
impl ThreadStore {
pub fn load(
project: Entity,
- tools: Arc,
+ tools: Entity,
prompt_builder: Arc,
cx: &mut App,
) -> Task> {
@@ -86,7 +88,7 @@ impl ThreadStore {
fn new(
project: Entity,
- tools: Arc,
+ tools: Entity,
prompt_builder: Arc,
cx: &mut Context,
) -> Self {
@@ -246,7 +248,7 @@ impl ThreadStore {
self.context_server_manager.clone()
}
- pub fn tools(&self) -> Arc {
+ pub fn tools(&self) -> Entity {
self.tools.clone()
}
@@ -353,52 +355,60 @@ impl ThreadStore {
})
}
- fn load_default_profile(&self, cx: &Context) {
+ fn load_default_profile(&self, cx: &mut Context) {
let assistant_settings = AssistantSettings::get_global(cx);
- self.load_profile_by_id(&assistant_settings.default_profile, cx);
+ self.load_profile_by_id(assistant_settings.default_profile.clone(), cx);
}
- pub fn load_profile_by_id(&self, profile_id: &AgentProfileId, cx: &Context) {
+ pub fn load_profile_by_id(&self, profile_id: AgentProfileId, cx: &mut Context) {
let assistant_settings = AssistantSettings::get_global(cx);
- if let Some(profile) = assistant_settings.profiles.get(profile_id) {
- self.load_profile(profile, cx);
+ if let Some(profile) = assistant_settings.profiles.get(&profile_id) {
+ self.load_profile(profile.clone(), cx);
}
}
- pub fn load_profile(&self, profile: &AgentProfile, cx: &Context) {
- self.tools.disable_all_tools();
- self.tools.enable(
- ToolSource::Native,
- &profile
- .tools
- .iter()
- .filter_map(|(tool, enabled)| enabled.then(|| tool.clone()))
- .collect::>(),
- );
+ pub fn load_profile(&self, profile: AgentProfile, cx: &mut Context) {
+ self.tools.update(cx, |tools, cx| {
+ tools.disable_all_tools(cx);
+ tools.enable(
+ ToolSource::Native,
+ &profile
+ .tools
+ .iter()
+ .filter_map(|(tool, enabled)| enabled.then(|| tool.clone()))
+ .collect::>(),
+ cx,
+ );
+ });
if profile.enable_all_context_servers {
for context_server in self.context_server_manager.read(cx).all_servers() {
- self.tools.enable_source(
- ToolSource::ContextServer {
- id: context_server.id().into(),
- },
- cx,
- );
+ self.tools.update(cx, |tools, cx| {
+ tools.enable_source(
+ ToolSource::ContextServer {
+ id: context_server.id().into(),
+ },
+ cx,
+ );
+ });
}
} else {
for (context_server_id, preset) in &profile.context_servers {
- self.tools.enable(
- ToolSource::ContextServer {
- id: context_server_id.clone().into(),
- },
- &preset
- .tools
- .iter()
- .filter_map(|(tool, enabled)| enabled.then(|| tool.clone()))
- .collect::>(),
- )
+ self.tools.update(cx, |tools, cx| {
+ tools.enable(
+ ToolSource::ContextServer {
+ id: context_server_id.clone().into(),
+ },
+ &preset
+ .tools
+ .iter()
+ .filter_map(|(tool, enabled)| enabled.then(|| tool.clone()))
+ .collect::>(),
+ cx,
+ )
+ })
}
}
}
@@ -432,29 +442,36 @@ impl ThreadStore {
if protocol.capable(context_server::protocol::ServerCapability::Tools) {
if let Some(tools) = protocol.list_tools().await.log_err() {
- let tool_ids = tools
- .tools
- .into_iter()
- .map(|tool| {
- log::info!(
- "registering context server tool: {:?}",
- tool.name
- );
- tool_working_set.insert(Arc::new(
- ContextServerTool::new(
- context_server_manager.clone(),
- server.id(),
- tool,
- ),
- ))
+ let tool_ids = tool_working_set
+ .update(cx, |tool_working_set, _| {
+ tools
+ .tools
+ .into_iter()
+ .map(|tool| {
+ log::info!(
+ "registering context server tool: {:?}",
+ tool.name
+ );
+ tool_working_set.insert(Arc::new(
+ ContextServerTool::new(
+ context_server_manager.clone(),
+ server.id(),
+ tool,
+ ),
+ ))
+ })
+ .collect::>()
})
- .collect::>();
+ .log_err();
- this.update(cx, |this, cx| {
- this.context_server_tool_ids.insert(server_id, tool_ids);
- this.load_default_profile(cx);
- })
- .log_err();
+ if let Some(tool_ids) = tool_ids {
+ this.update(cx, |this, cx| {
+ this.context_server_tool_ids
+ .insert(server_id, tool_ids);
+ this.load_default_profile(cx);
+ })
+ .log_err();
+ }
}
}
}
@@ -464,7 +481,9 @@ impl ThreadStore {
}
context_server::manager::Event::ServerStopped { server_id } => {
if let Some(tool_ids) = self.context_server_tool_ids.remove(server_id) {
- tool_working_set.remove(&tool_ids);
+ tool_working_set.update(cx, |tool_working_set, _| {
+ tool_working_set.remove(&tool_ids);
+ });
self.load_default_profile(cx);
}
}
@@ -491,6 +510,8 @@ pub struct SerializedThread {
pub cumulative_token_usage: TokenUsage,
#[serde(default)]
pub detailed_summary_state: DetailedSummaryState,
+ #[serde(default)]
+ pub exceeded_window_error: Option,
}
impl SerializedThread {
@@ -577,6 +598,7 @@ impl LegacySerializedThread {
initial_project_snapshot: self.initial_project_snapshot,
cumulative_token_usage: TokenUsage::default(),
detailed_summary_state: DetailedSummaryState::default(),
+ exceeded_window_error: None,
}
}
}
diff --git a/crates/agent/src/tool_use.rs b/crates/agent/src/tool_use.rs
index 7abecbd4a0..32876a100c 100644
--- a/crates/agent/src/tool_use.rs
+++ b/crates/agent/src/tool_use.rs
@@ -5,7 +5,7 @@ use assistant_tool::{AnyToolCard, Tool, ToolUseStatus, ToolWorkingSet};
use collections::HashMap;
use futures::FutureExt as _;
use futures::future::Shared;
-use gpui::{App, SharedString, Task};
+use gpui::{App, Entity, SharedString, Task};
use language_model::{
LanguageModelRegistry, LanguageModelRequestMessage, LanguageModelToolResult,
LanguageModelToolUse, LanguageModelToolUseId, MessageContent, Role,
@@ -30,7 +30,7 @@ pub struct ToolUse {
pub const USING_TOOL_MARKER: &str = "";
pub struct ToolUseState {
- tools: Arc,
+ tools: Entity,
tool_uses_by_assistant_message: HashMap>,
tool_uses_by_user_message: HashMap>,
tool_results: HashMap,
@@ -39,7 +39,7 @@ pub struct ToolUseState {
}
impl ToolUseState {
- pub fn new(tools: Arc) -> Self {
+ pub fn new(tools: Entity) -> Self {
Self {
tools,
tool_uses_by_assistant_message: HashMap::default(),
@@ -54,7 +54,7 @@ impl ToolUseState {
///
/// Accepts a function to filter the tools that should be used to populate the state.
pub fn from_serialized_messages(
- tools: Arc,
+ tools: Entity,
messages: &[SerializedMessage],
mut filter_by_tool_name: impl FnMut(&str) -> bool,
) -> Self {
@@ -180,12 +180,12 @@ impl ToolUseState {
}
})();
- let (icon, needs_confirmation) = if let Some(tool) = self.tools.tool(&tool_use.name, cx)
- {
- (tool.icon(), tool.needs_confirmation(&tool_use.input, cx))
- } else {
- (IconName::Cog, false)
- };
+ let (icon, needs_confirmation) =
+ if let Some(tool) = self.tools.read(cx).tool(&tool_use.name, cx) {
+ (tool.icon(), tool.needs_confirmation(&tool_use.input, cx))
+ } else {
+ (IconName::Cog, false)
+ };
tool_uses.push(ToolUse {
id: tool_use.id.clone(),
@@ -207,7 +207,7 @@ impl ToolUseState {
input: &serde_json::Value,
cx: &App,
) -> SharedString {
- if let Some(tool) = self.tools.tool(tool_name, cx) {
+ if let Some(tool) = self.tools.read(cx).tool(tool_name, cx) {
tool.ui_text(input).into()
} else {
format!("Unknown tool {tool_name:?}").into()
diff --git a/crates/anthropic/Cargo.toml b/crates/anthropic/Cargo.toml
index 1735579729..8e82c7cdd6 100644
--- a/crates/anthropic/Cargo.toml
+++ b/crates/anthropic/Cargo.toml
@@ -25,5 +25,4 @@ serde.workspace = true
serde_json.workspace = true
strum.workspace = true
thiserror.workspace = true
-util.workspace = true
workspace-hack.workspace = true
diff --git a/crates/anthropic/src/anthropic.rs b/crates/anthropic/src/anthropic.rs
index e0c215bc3a..266d3c7642 100644
--- a/crates/anthropic/src/anthropic.rs
+++ b/crates/anthropic/src/anthropic.rs
@@ -10,7 +10,6 @@ use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
use serde::{Deserialize, Serialize};
use strum::{EnumIter, EnumString};
use thiserror::Error;
-use util::ResultExt as _;
pub use supported_countries::*;
@@ -363,11 +362,25 @@ pub struct RateLimitInfo {
impl RateLimitInfo {
fn from_headers(headers: &HeaderMap) -> Self {
+ // Check if any rate limit headers exist
+ let has_rate_limit_headers = headers
+ .keys()
+ .any(|k| k.as_str().starts_with("anthropic-ratelimit-"));
+
+ if !has_rate_limit_headers {
+ return Self {
+ requests: None,
+ tokens: None,
+ input_tokens: None,
+ output_tokens: None,
+ };
+ }
+
Self {
- requests: RateLimit::from_headers("requests", headers).log_err(),
- tokens: RateLimit::from_headers("tokens", headers).log_err(),
- input_tokens: RateLimit::from_headers("input-tokens", headers).log_err(),
- output_tokens: RateLimit::from_headers("output-tokens", headers).log_err(),
+ requests: RateLimit::from_headers("requests", headers).ok(),
+ tokens: RateLimit::from_headers("tokens", headers).ok(),
+ input_tokens: RateLimit::from_headers("input-tokens", headers).ok(),
+ output_tokens: RateLimit::from_headers("output-tokens", headers).ok(),
}
}
}
@@ -724,4 +737,54 @@ impl ApiError {
pub fn is_rate_limit_error(&self) -> bool {
matches!(self.error_type.as_str(), "rate_limit_error")
}
+
+ pub fn match_window_exceeded(&self) -> Option {
+ let Some(ApiErrorCode::InvalidRequestError) = self.code() else {
+ return None;
+ };
+
+ parse_prompt_too_long(&self.message)
+ }
+}
+
+pub fn parse_prompt_too_long(message: &str) -> Option {
+ message
+ .strip_prefix("prompt is too long: ")?
+ .split_once(" tokens")?
+ .0
+ .parse::()
+ .ok()
+}
+
+#[test]
+fn test_match_window_exceeded() {
+ let error = ApiError {
+ error_type: "invalid_request_error".to_string(),
+ message: "prompt is too long: 220000 tokens > 200000".to_string(),
+ };
+ assert_eq!(error.match_window_exceeded(), Some(220_000));
+
+ let error = ApiError {
+ error_type: "invalid_request_error".to_string(),
+ message: "prompt is too long: 1234953 tokens".to_string(),
+ };
+ assert_eq!(error.match_window_exceeded(), Some(1234953));
+
+ let error = ApiError {
+ error_type: "invalid_request_error".to_string(),
+ message: "not a prompt length error".to_string(),
+ };
+ assert_eq!(error.match_window_exceeded(), None);
+
+ let error = ApiError {
+ error_type: "rate_limit_error".to_string(),
+ message: "prompt is too long: 12345 tokens".to_string(),
+ };
+ assert_eq!(error.match_window_exceeded(), None);
+
+ let error = ApiError {
+ error_type: "invalid_request_error".to_string(),
+ message: "prompt is too long: invalid tokens".to_string(),
+ };
+ assert_eq!(error.match_window_exceeded(), None);
}
diff --git a/crates/assistant_tool/src/action_log.rs b/crates/assistant_tool/src/action_log.rs
index 1c0911c189..fa305a512e 100644
--- a/crates/assistant_tool/src/action_log.rs
+++ b/crates/assistant_tool/src/action_log.rs
@@ -4,7 +4,7 @@ use collections::BTreeMap;
use futures::{StreamExt, channel::mpsc};
use gpui::{App, AppContext, AsyncApp, Context, Entity, Subscription, Task, WeakEntity};
use language::{Anchor, Buffer, BufferEvent, DiskState, Point};
-use project::{Project, ProjectItem};
+use project::{Project, ProjectItem, lsp_store::OpenLspBufferHandle};
use std::{cmp, ops::Range, sync::Arc};
use text::{Edit, Patch, Rope};
use util::RangeExt;
@@ -49,6 +49,10 @@ impl ActionLog {
.tracked_buffers
.entry(buffer.clone())
.or_insert_with(|| {
+ let open_lsp_handle = self.project.update(cx, |project, cx| {
+ project.register_buffer_with_language_servers(&buffer, cx)
+ });
+
let text_snapshot = buffer.read(cx).text_snapshot();
let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
let (diff_update_tx, diff_update_rx) = mpsc::unbounded();
@@ -76,6 +80,7 @@ impl ActionLog {
version: buffer.read(cx).version(),
diff,
diff_update: diff_update_tx,
+ _open_lsp_handle: open_lsp_handle,
_maintain_diff: cx.spawn({
let buffer = buffer.clone();
async move |this, cx| {
@@ -615,6 +620,7 @@ struct TrackedBuffer {
diff: Entity,
snapshot: text::BufferSnapshot,
diff_update: mpsc::UnboundedSender<(ChangeAuthor, text::BufferSnapshot)>,
+ _open_lsp_handle: OpenLspBufferHandle,
_maintain_diff: Task<()>,
_subscription: Subscription,
}
diff --git a/crates/assistant_tool/src/assistant_tool.rs b/crates/assistant_tool/src/assistant_tool.rs
index c91ac3b8a5..cb7f0ff518 100644
--- a/crates/assistant_tool/src/assistant_tool.rs
+++ b/crates/assistant_tool/src/assistant_tool.rs
@@ -1,5 +1,6 @@
mod action_log;
mod tool_registry;
+mod tool_schema;
mod tool_working_set;
use std::fmt;
@@ -20,6 +21,7 @@ use project::Project;
pub use crate::action_log::*;
pub use crate::tool_registry::*;
+pub use crate::tool_schema::*;
pub use crate::tool_working_set::*;
pub fn init(cx: &mut App) {
@@ -139,8 +141,8 @@ pub trait Tool: 'static + Send + Sync {
fn needs_confirmation(&self, input: &serde_json::Value, cx: &App) -> bool;
/// Returns the JSON schema that describes the tool's input.
- fn input_schema(&self, _: LanguageModelToolSchemaFormat) -> serde_json::Value {
- serde_json::Value::Object(serde_json::Map::default())
+ fn input_schema(&self, _: LanguageModelToolSchemaFormat) -> Result {
+ Ok(serde_json::Value::Object(serde_json::Map::default()))
}
/// Returns markdown to be displayed in the UI for this tool.
diff --git a/crates/assistant_tool/src/tool_schema.rs b/crates/assistant_tool/src/tool_schema.rs
new file mode 100644
index 0000000000..225c1c22ef
--- /dev/null
+++ b/crates/assistant_tool/src/tool_schema.rs
@@ -0,0 +1,236 @@
+use anyhow::Result;
+use serde_json::Value;
+
+use crate::LanguageModelToolSchemaFormat;
+
+/// Tries to adapt a JSON schema representation to be compatible with the specified format.
+///
+/// If the json cannot be made compatible with the specified format, an error is returned.
+pub fn adapt_schema_to_format(
+ json: &mut Value,
+ format: LanguageModelToolSchemaFormat,
+) -> Result<()> {
+ match format {
+ LanguageModelToolSchemaFormat::JsonSchema => Ok(()),
+ LanguageModelToolSchemaFormat::JsonSchemaSubset => adapt_to_json_schema_subset(json),
+ }
+}
+
+/// Tries to adapt the json schema so that it is compatible with https://ai.google.dev/api/caching#Schema
+fn adapt_to_json_schema_subset(json: &mut Value) -> Result<()> {
+ if let Value::Object(obj) = json {
+ const UNSUPPORTED_KEYS: [&str; 4] = ["if", "then", "else", "$ref"];
+
+ for key in UNSUPPORTED_KEYS {
+ if obj.contains_key(key) {
+ return Err(anyhow::anyhow!(
+ "Schema cannot be made compatible because it contains \"{}\" ",
+ key
+ ));
+ }
+ }
+
+ const KEYS_TO_REMOVE: [&str; 2] = ["format", "$schema"];
+ for key in KEYS_TO_REMOVE {
+ obj.remove(key);
+ }
+
+ if let Some(default) = obj.get("default") {
+ let is_null = default.is_null();
+ // Default is not supported, so we need to remove it
+ obj.remove("default");
+ if is_null {
+ obj.insert("nullable".to_string(), Value::Bool(true));
+ }
+ }
+
+ // If a type is not specified for an input parameter, add a default type
+ if obj.contains_key("description")
+ && !obj.contains_key("type")
+ && !(obj.contains_key("anyOf")
+ || obj.contains_key("oneOf")
+ || obj.contains_key("allOf"))
+ {
+ obj.insert("type".to_string(), Value::String("string".to_string()));
+ }
+
+ // Handle oneOf -> anyOf conversion
+ if let Some(subschemas) = obj.get_mut("oneOf") {
+ if subschemas.is_array() {
+ let subschemas_clone = subschemas.clone();
+ obj.remove("oneOf");
+ obj.insert("anyOf".to_string(), subschemas_clone);
+ }
+ }
+
+ // Recursively process all nested objects and arrays
+ for (_, value) in obj.iter_mut() {
+ if let Value::Object(_) | Value::Array(_) = value {
+ adapt_to_json_schema_subset(value)?;
+ }
+ }
+ } else if let Value::Array(arr) = json {
+ for item in arr.iter_mut() {
+ adapt_to_json_schema_subset(item)?;
+ }
+ }
+ Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use serde_json::json;
+
+ #[test]
+ fn test_transform_default_null_to_nullable() {
+ let mut json = json!({
+ "description": "A test field",
+ "type": "string",
+ "default": null
+ });
+
+ adapt_to_json_schema_subset(&mut json).unwrap();
+
+ assert_eq!(
+ json,
+ json!({
+ "description": "A test field",
+ "type": "string",
+ "nullable": true
+ })
+ );
+ }
+
+ #[test]
+ fn test_transform_adds_type_when_missing() {
+ let mut json = json!({
+ "description": "A test field without type"
+ });
+
+ adapt_to_json_schema_subset(&mut json).unwrap();
+
+ assert_eq!(
+ json,
+ json!({
+ "description": "A test field without type",
+ "type": "string"
+ })
+ );
+ }
+
+ #[test]
+ fn test_transform_removes_format() {
+ let mut json = json!({
+ "description": "A test field",
+ "type": "integer",
+ "format": "uint32"
+ });
+
+ adapt_to_json_schema_subset(&mut json).unwrap();
+
+ assert_eq!(
+ json,
+ json!({
+ "description": "A test field",
+ "type": "integer"
+ })
+ );
+ }
+
+ #[test]
+ fn test_transform_one_of_to_any_of() {
+ let mut json = json!({
+ "description": "A test field",
+ "oneOf": [
+ { "type": "string" },
+ { "type": "integer" }
+ ]
+ });
+
+ adapt_to_json_schema_subset(&mut json).unwrap();
+
+ assert_eq!(
+ json,
+ json!({
+ "description": "A test field",
+ "anyOf": [
+ { "type": "string" },
+ { "type": "integer" }
+ ]
+ })
+ );
+ }
+
+ #[test]
+ fn test_transform_nested_objects() {
+ let mut json = json!({
+ "type": "object",
+ "properties": {
+ "nested": {
+ "oneOf": [
+ { "type": "string" },
+ { "type": "null" }
+ ],
+ "format": "email"
+ }
+ }
+ });
+
+ adapt_to_json_schema_subset(&mut json).unwrap();
+
+ assert_eq!(
+ json,
+ json!({
+ "type": "object",
+ "properties": {
+ "nested": {
+ "anyOf": [
+ { "type": "string" },
+ { "type": "null" }
+ ]
+ }
+ }
+ })
+ );
+ }
+
+ #[test]
+ fn test_transform_fails_if_unsupported_keys_exist() {
+ let mut json = json!({
+ "type": "object",
+ "properties": {
+ "$ref": "#/definitions/User",
+ }
+ });
+
+ assert!(adapt_to_json_schema_subset(&mut json).is_err());
+
+ let mut json = json!({
+ "type": "object",
+ "properties": {
+ "if": "...",
+ }
+ });
+
+ assert!(adapt_to_json_schema_subset(&mut json).is_err());
+
+ let mut json = json!({
+ "type": "object",
+ "properties": {
+ "then": "...",
+ }
+ });
+
+ assert!(adapt_to_json_schema_subset(&mut json).is_err());
+
+ let mut json = json!({
+ "type": "object",
+ "properties": {
+ "else": "...",
+ }
+ });
+
+ assert!(adapt_to_json_schema_subset(&mut json).is_err());
+ }
+}
diff --git a/crates/assistant_tool/src/tool_working_set.rs b/crates/assistant_tool/src/tool_working_set.rs
index 97060cfdad..c7e20d3517 100644
--- a/crates/assistant_tool/src/tool_working_set.rs
+++ b/crates/assistant_tool/src/tool_working_set.rs
@@ -1,8 +1,7 @@
use std::sync::Arc;
use collections::{HashMap, HashSet, IndexMap};
-use gpui::App;
-use parking_lot::Mutex;
+use gpui::{App, Context, EventEmitter};
use crate::{Tool, ToolRegistry, ToolSource};
@@ -12,11 +11,6 @@ pub struct ToolId(usize);
/// A working set of tools for use in one instance of the Assistant Panel.
#[derive(Default)]
pub struct ToolWorkingSet {
- state: Mutex,
-}
-
-#[derive(Default)]
-struct WorkingSetState {
context_server_tools_by_id: HashMap>,
context_server_tools_by_name: HashMap>,
enabled_sources: HashSet,
@@ -24,99 +18,27 @@ struct WorkingSetState {
next_tool_id: ToolId,
}
+pub enum ToolWorkingSetEvent {
+ EnabledToolsChanged,
+}
+
+impl EventEmitter for ToolWorkingSet {}
+
impl ToolWorkingSet {
pub fn tool(&self, name: &str, cx: &App) -> Option> {
- self.state
- .lock()
- .context_server_tools_by_name
+ self.context_server_tools_by_name
.get(name)
.cloned()
.or_else(|| ToolRegistry::global(cx).tool(name))
}
pub fn tools(&self, cx: &App) -> Vec> {
- self.state.lock().tools(cx)
- }
-
- pub fn tools_by_source(&self, cx: &App) -> IndexMap>> {
- self.state.lock().tools_by_source(cx)
- }
-
- pub fn enabled_tools(&self, cx: &App) -> Vec> {
- self.state.lock().enabled_tools(cx)
- }
-
- pub fn disable_all_tools(&self) {
- let mut state = self.state.lock();
- state.disable_all_tools();
- }
-
- pub fn enable_source(&self, source: ToolSource, cx: &App) {
- let mut state = self.state.lock();
- state.enable_source(source, cx);
- }
-
- pub fn disable_source(&self, source: &ToolSource) {
- let mut state = self.state.lock();
- state.disable_source(source);
- }
-
- pub fn insert(&self, tool: Arc) -> ToolId {
- let mut state = self.state.lock();
- let tool_id = state.next_tool_id;
- state.next_tool_id.0 += 1;
- state
- .context_server_tools_by_id
- .insert(tool_id, tool.clone());
- state.tools_changed();
- tool_id
- }
-
- pub fn is_enabled(&self, source: &ToolSource, name: &Arc) -> bool {
- self.state.lock().is_enabled(source, name)
- }
-
- pub fn is_disabled(&self, source: &ToolSource, name: &Arc) -> bool {
- self.state.lock().is_disabled(source, name)
- }
-
- pub fn enable(&self, source: ToolSource, tools_to_enable: &[Arc]) {
- let mut state = self.state.lock();
- state.enable(source, tools_to_enable);
- }
-
- pub fn disable(&self, source: ToolSource, tools_to_disable: &[Arc]) {
- let mut state = self.state.lock();
- state.disable(source, tools_to_disable);
- }
-
- pub fn remove(&self, tool_ids_to_remove: &[ToolId]) {
- let mut state = self.state.lock();
- state
- .context_server_tools_by_id
- .retain(|id, _| !tool_ids_to_remove.contains(id));
- state.tools_changed();
- }
-}
-
-impl WorkingSetState {
- fn tools_changed(&mut self) {
- self.context_server_tools_by_name.clear();
- self.context_server_tools_by_name.extend(
- self.context_server_tools_by_id
- .values()
- .map(|tool| (tool.name(), tool.clone())),
- );
- }
-
- fn tools(&self, cx: &App) -> Vec> {
let mut tools = ToolRegistry::global(cx).tools();
tools.extend(self.context_server_tools_by_id.values().cloned());
-
tools
}
- fn tools_by_source(&self, cx: &App) -> IndexMap>> {
+ pub fn tools_by_source(&self, cx: &App) -> IndexMap>> {
let mut tools_by_source = IndexMap::default();
for tool in self.tools(cx) {
@@ -135,7 +57,7 @@ impl WorkingSetState {
tools_by_source
}
- fn enabled_tools(&self, cx: &App) -> Vec> {
+ pub fn enabled_tools(&self, cx: &App) -> Vec> {
let all_tools = self.tools(cx);
all_tools
@@ -144,31 +66,12 @@ impl WorkingSetState {
.collect()
}
- fn is_enabled(&self, source: &ToolSource, name: &Arc) -> bool {
- self.enabled_tools_by_source
- .get(source)
- .map_or(false, |enabled_tools| enabled_tools.contains(name))
+ pub fn disable_all_tools(&mut self, cx: &mut Context) {
+ self.enabled_tools_by_source.clear();
+ cx.emit(ToolWorkingSetEvent::EnabledToolsChanged);
}
- fn is_disabled(&self, source: &ToolSource, name: &Arc) -> bool {
- !self.is_enabled(source, name)
- }
-
- fn enable(&mut self, source: ToolSource, tools_to_enable: &[Arc]) {
- self.enabled_tools_by_source
- .entry(source)
- .or_default()
- .extend(tools_to_enable.into_iter().cloned());
- }
-
- fn disable(&mut self, source: ToolSource, tools_to_disable: &[Arc]) {
- self.enabled_tools_by_source
- .entry(source)
- .or_default()
- .retain(|name| !tools_to_disable.contains(name));
- }
-
- fn enable_source(&mut self, source: ToolSource, cx: &App) {
+ pub fn enable_source(&mut self, source: ToolSource, cx: &mut Context) {
self.enabled_sources.insert(source.clone());
let tools_by_source = self.tools_by_source(cx);
@@ -181,14 +84,72 @@ impl WorkingSetState {
.collect::>(),
);
}
+ cx.emit(ToolWorkingSetEvent::EnabledToolsChanged);
}
- fn disable_source(&mut self, source: &ToolSource) {
+ pub fn disable_source(&mut self, source: &ToolSource, cx: &mut Context) {
self.enabled_sources.remove(source);
self.enabled_tools_by_source.remove(source);
+ cx.emit(ToolWorkingSetEvent::EnabledToolsChanged);
}
- fn disable_all_tools(&mut self) {
- self.enabled_tools_by_source.clear();
+ pub fn insert(&mut self, tool: Arc) -> ToolId {
+ let tool_id = self.next_tool_id;
+ self.next_tool_id.0 += 1;
+ self.context_server_tools_by_id
+ .insert(tool_id, tool.clone());
+ self.tools_changed();
+ tool_id
+ }
+
+ pub fn is_enabled(&self, source: &ToolSource, name: &Arc) -> bool {
+ self.enabled_tools_by_source
+ .get(source)
+ .map_or(false, |enabled_tools| enabled_tools.contains(name))
+ }
+
+ pub fn is_disabled(&self, source: &ToolSource, name: &Arc) -> bool {
+ !self.is_enabled(source, name)
+ }
+
+ pub fn enable(
+ &mut self,
+ source: ToolSource,
+ tools_to_enable: &[Arc],
+ cx: &mut Context,
+ ) {
+ self.enabled_tools_by_source
+ .entry(source)
+ .or_default()
+ .extend(tools_to_enable.into_iter().cloned());
+ cx.emit(ToolWorkingSetEvent::EnabledToolsChanged);
+ }
+
+ pub fn disable(
+ &mut self,
+ source: ToolSource,
+ tools_to_disable: &[Arc],
+ cx: &mut Context,
+ ) {
+ self.enabled_tools_by_source
+ .entry(source)
+ .or_default()
+ .retain(|name| !tools_to_disable.contains(name));
+ cx.emit(ToolWorkingSetEvent::EnabledToolsChanged);
+ }
+
+ pub fn remove(&mut self, tool_ids_to_remove: &[ToolId]) {
+ self.context_server_tools_by_id
+ .retain(|id, _| !tool_ids_to_remove.contains(id));
+ self.tools_changed();
+ }
+
+ fn tools_changed(&mut self) {
+ self.context_server_tools_by_name.clear();
+ self.context_server_tools_by_name.extend(
+ self.context_server_tools_by_id
+ .values()
+ .map(|tool| (tool.name(), tool.clone())),
+ );
}
}
diff --git a/crates/assistant_tools/src/assistant_tools.rs b/crates/assistant_tools/src/assistant_tools.rs
index d7d901fd86..adf273798a 100644
--- a/crates/assistant_tools/src/assistant_tools.rs
+++ b/crates/assistant_tools/src/assistant_tools.rs
@@ -1,6 +1,7 @@
mod batch_tool;
mod code_action_tool;
mod code_symbols_tool;
+mod contents_tool;
mod copy_path_tool;
mod create_directory_tool;
mod create_file_tool;
@@ -35,6 +36,7 @@ use web_search_tool::WebSearchTool;
use crate::batch_tool::BatchTool;
use crate::code_action_tool::CodeActionTool;
use crate::code_symbols_tool::CodeSymbolsTool;
+use crate::contents_tool::ContentsTool;
use crate::create_directory_tool::CreateDirectoryTool;
use crate::create_file_tool::CreateFileTool;
use crate::delete_path_tool::DeletePathTool;
@@ -59,6 +61,7 @@ pub fn init(http_client: Arc, cx: &mut App) {
registry.register_tool(BatchTool);
registry.register_tool(CodeActionTool);
registry.register_tool(CodeSymbolsTool);
+ registry.register_tool(ContentsTool);
registry.register_tool(CopyPathTool);
registry.register_tool(CreateDirectoryTool);
registry.register_tool(CreateFileTool);
@@ -79,3 +82,42 @@ pub fn init(http_client: Arc, cx: &mut App) {
registry.register_tool(ThinkingTool);
registry.register_tool(WebSearchTool);
}
+
+#[cfg(test)]
+mod tests {
+ use http_client::FakeHttpClient;
+
+ use super::*;
+
+ #[gpui::test]
+ fn test_builtin_tool_schema_compatibility(cx: &mut App) {
+ crate::init(
+ Arc::new(http_client::HttpClientWithUrl::new(
+ FakeHttpClient::with_200_response(),
+ "https://zed.dev",
+ None,
+ )),
+ cx,
+ );
+
+ for tool in ToolRegistry::global(cx).tools() {
+ let actual_schema = tool
+ .input_schema(language_model::LanguageModelToolSchemaFormat::JsonSchemaSubset)
+ .unwrap();
+ let mut expected_schema = actual_schema.clone();
+ assistant_tool::adapt_schema_to_format(
+ &mut expected_schema,
+ language_model::LanguageModelToolSchemaFormat::JsonSchemaSubset,
+ )
+ .unwrap();
+
+ let error_message = format!(
+ "Tool schema for `{}` is not compatible with `language_model::LanguageModelToolSchemaFormat::JsonSchemaSubset` (Gemini Models).\n\
+ Are you using `schema::json_schema_for(format)` to generate the schema?",
+ tool.name(),
+ );
+
+ assert_eq!(actual_schema, expected_schema, "{}", error_message)
+ }
+ }
+}
diff --git a/crates/assistant_tools/src/batch_tool.rs b/crates/assistant_tools/src/batch_tool.rs
index a195cee4d6..87e70e1e62 100644
--- a/crates/assistant_tools/src/batch_tool.rs
+++ b/crates/assistant_tools/src/batch_tool.rs
@@ -172,7 +172,7 @@ impl Tool for BatchTool {
IconName::Cog
}
- fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value {
+ fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result {
json_schema_for::(format)
}
diff --git a/crates/assistant_tools/src/code_action_tool.rs b/crates/assistant_tools/src/code_action_tool.rs
index 119cce7669..4bf39f1deb 100644
--- a/crates/assistant_tools/src/code_action_tool.rs
+++ b/crates/assistant_tools/src/code_action_tool.rs
@@ -2,7 +2,7 @@ use anyhow::{Context as _, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, Entity, Task};
use language::{self, Anchor, Buffer, ToPointUtf16};
-use language_model::LanguageModelRequestMessage;
+use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::{self, LspAction, Project};
use regex::Regex;
use schemars::JsonSchema;
@@ -10,6 +10,8 @@ use serde::{Deserialize, Serialize};
use std::{ops::Range, sync::Arc};
use ui::IconName;
+use crate::schema::json_schema_for;
+
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct CodeActionToolInput {
/// The relative path to the file containing the text range.
@@ -95,12 +97,8 @@ impl Tool for CodeActionTool {
IconName::Wand
}
- fn input_schema(
- &self,
- _format: language_model::LanguageModelToolSchemaFormat,
- ) -> serde_json::Value {
- let schema = schemars::schema_for!(CodeActionToolInput);
- serde_json::to_value(&schema).unwrap()
+ fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result {
+ json_schema_for::(format)
}
fn ui_text(&self, input: &serde_json::Value) -> String {
diff --git a/crates/assistant_tools/src/code_symbols_tool.rs b/crates/assistant_tools/src/code_symbols_tool.rs
index eabc18c486..78dea96d5b 100644
--- a/crates/assistant_tools/src/code_symbols_tool.rs
+++ b/crates/assistant_tools/src/code_symbols_tool.rs
@@ -91,7 +91,7 @@ impl Tool for CodeSymbolsTool {
IconName::Code
}
- fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value {
+ fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result {
json_schema_for::(format)
}
diff --git a/crates/assistant_tools/src/contents_tool.rs b/crates/assistant_tools/src/contents_tool.rs
new file mode 100644
index 0000000000..be7c4927cb
--- /dev/null
+++ b/crates/assistant_tools/src/contents_tool.rs
@@ -0,0 +1,239 @@
+use std::sync::Arc;
+
+use crate::{code_symbols_tool::file_outline, schema::json_schema_for};
+use anyhow::{Result, anyhow};
+use assistant_tool::{ActionLog, Tool};
+use gpui::{App, Entity, Task};
+use itertools::Itertools;
+use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
+use project::Project;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use std::{fmt::Write, path::Path};
+use ui::IconName;
+use util::markdown::MarkdownString;
+
+/// If the model requests to read a file whose size exceeds this, then
+/// the tool will return the file's symbol outline instead of its contents,
+/// and suggest trying again using line ranges from the outline.
+const MAX_FILE_SIZE_TO_READ: usize = 16384;
+
+/// If the model requests to list the entries in a directory with more
+/// entries than this, then the tool will return a subset of the entries
+/// and suggest trying again.
+const MAX_DIR_ENTRIES: usize = 1024;
+
+#[derive(Debug, Serialize, Deserialize, JsonSchema)]
+pub struct ContentsToolInput {
+ /// The relative path of the file or directory to access.
+ ///
+ /// This path should never be absolute, and the first component
+ /// of the path should always be a root directory in a project.
+ ///
+ ///
+ /// If the project has the following root directories:
+ ///
+ /// - directory1
+ /// - directory2
+ ///
+ /// If you want to access `file.txt` in `directory1`, you should use the path `directory1/file.txt`.
+ /// If you want to list contents in the directory `directory2/subfolder`, you should use the path `directory2/subfolder`.
+ ///
+ pub path: String,
+
+ /// Optional position (1-based index) to start reading on, if you want to read a subset of the contents.
+ /// When reading a file, this refers to a line number in the file (e.g. 1 is the first line).
+ /// When reading a directory, this refers to the number of the directory entry (e.g. 1 is the first entry).
+ ///
+ /// Defaults to 1.
+ pub start: Option,
+
+ /// Optional position (1-based index) to end reading on, if you want to read a subset of the contents.
+ /// When reading a file, this refers to a line number in the file (e.g. 1 is the first line).
+ /// When reading a directory, this refers to the number of the directory entry (e.g. 1 is the first entry).
+ ///
+ /// Defaults to reading until the end of the file or directory.
+ pub end: Option,
+}
+
+pub struct ContentsTool;
+
+impl Tool for ContentsTool {
+ fn name(&self) -> String {
+ "contents".into()
+ }
+
+ fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
+ false
+ }
+
+ fn description(&self) -> String {
+ include_str!("./contents_tool/description.md").into()
+ }
+
+ fn icon(&self) -> IconName {
+ IconName::FileSearch
+ }
+
+ fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result {
+ json_schema_for::(format)
+ }
+
+ fn ui_text(&self, input: &serde_json::Value) -> String {
+ match serde_json::from_value::(input.clone()) {
+ Ok(input) => {
+ let path = MarkdownString::inline_code(&input.path);
+
+ match (input.start, input.end) {
+ (Some(start), None) => format!("Read {path} (from line {start})"),
+ (Some(start), Some(end)) => {
+ format!("Read {path} (lines {start}-{end})")
+ }
+ _ => format!("Read {path}"),
+ }
+ }
+ Err(_) => "Read file or directory".to_string(),
+ }
+ }
+
+ fn run(
+ self: Arc,
+ input: serde_json::Value,
+ _messages: &[LanguageModelRequestMessage],
+ project: Entity,
+ action_log: Entity,
+ cx: &mut App,
+ ) -> Task> {
+ let input = match serde_json::from_value::(input) {
+ Ok(input) => input,
+ Err(err) => return Task::ready(Err(anyhow!(err))),
+ };
+
+ // Sometimes models will return these even though we tell it to give a path and not a glob.
+ // When this happens, just list the root worktree directories.
+ if matches!(input.path.as_str(), "." | "" | "./" | "*") {
+ let output = project
+ .read(cx)
+ .worktrees(cx)
+ .filter_map(|worktree| {
+ worktree.read(cx).root_entry().and_then(|entry| {
+ if entry.is_dir() {
+ entry.path.to_str()
+ } else {
+ None
+ }
+ })
+ })
+ .collect::>()
+ .join("\n");
+
+ return Task::ready(Ok(output));
+ }
+
+ let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
+ return Task::ready(Err(anyhow!("Path {} not found in project", &input.path)));
+ };
+
+ let Some(worktree) = project
+ .read(cx)
+ .worktree_for_id(project_path.worktree_id, cx)
+ else {
+ return Task::ready(Err(anyhow!("Worktree not found")));
+ };
+ let worktree = worktree.read(cx);
+
+ let Some(entry) = worktree.entry_for_path(&project_path.path) else {
+ return Task::ready(Err(anyhow!("Path not found: {}", input.path)));
+ };
+
+ // If it's a directory, list its contents
+ if entry.is_dir() {
+ let mut output = String::new();
+ let start_index = input
+ .start
+ .map(|line| (line as usize).saturating_sub(1))
+ .unwrap_or(0);
+ let end_index = input
+ .end
+ .map(|line| (line as usize).saturating_sub(1))
+ .unwrap_or(MAX_DIR_ENTRIES);
+ let mut skipped = 0;
+
+ for (index, entry) in worktree.child_entries(&project_path.path).enumerate() {
+ if index >= start_index && index <= end_index {
+ writeln!(
+ output,
+ "{}",
+ Path::new(worktree.root_name()).join(&entry.path).display(),
+ )
+ .unwrap();
+ } else {
+ skipped += 1;
+ }
+ }
+
+ if output.is_empty() {
+ output.push_str(&input.path);
+ output.push_str(" is empty.");
+ }
+
+ if skipped > 0 {
+ write!(
+ output,
+ "\n\nNote: Skipped {skipped} entries. Adjust start and end to see other entries.",
+ ).ok();
+ }
+
+ Task::ready(Ok(output))
+ } else {
+ // It's a file, so read its contents
+ let file_path = input.path.clone();
+ cx.spawn(async move |cx| {
+ let buffer = cx
+ .update(|cx| {
+ project.update(cx, |project, cx| project.open_buffer(project_path, cx))
+ })?
+ .await?;
+
+ if input.start.is_some() || input.end.is_some() {
+ let result = buffer.read_with(cx, |buffer, _cx| {
+ let text = buffer.text();
+ let start = input.start.unwrap_or(1);
+ let lines = text.split('\n').skip(start as usize - 1);
+ if let Some(end) = input.end {
+ let count = end.saturating_sub(start).max(1); // Ensure at least 1 line
+ Itertools::intersperse(lines.take(count as usize), "\n").collect()
+ } else {
+ Itertools::intersperse(lines, "\n").collect()
+ }
+ })?;
+
+ action_log.update(cx, |log, cx| {
+ log.buffer_read(buffer, cx);
+ })?;
+
+ Ok(result)
+ } else {
+ // No line ranges specified, so check file size to see if it's too big.
+ let file_size = buffer.read_with(cx, |buffer, _cx| buffer.text().len())?;
+
+ if file_size <= MAX_FILE_SIZE_TO_READ {
+ let result = buffer.read_with(cx, |buffer, _cx| buffer.text())?;
+
+ action_log.update(cx, |log, cx| {
+ log.buffer_read(buffer, cx);
+ })?;
+
+ Ok(result)
+ } else {
+ // File is too big, so return its outline and a suggestion to
+ // read again with a line number range specified.
+ let outline = file_outline(project, file_path, action_log, None, 0, cx).await?;
+
+ Ok(format!("This file was too big to read all at once. Here is an outline of its symbols:\n\n{outline}\n\nUsing the line numbers in this outline, you can call this tool again while specifying the start and end fields to see the implementations of symbols in the outline."))
+ }
+ }
+ })
+ }
+ }
+}
diff --git a/crates/assistant_tools/src/contents_tool/description.md b/crates/assistant_tools/src/contents_tool/description.md
new file mode 100644
index 0000000000..b532f7c534
--- /dev/null
+++ b/crates/assistant_tools/src/contents_tool/description.md
@@ -0,0 +1,9 @@
+Reads the contents of a path on the filesystem.
+
+If the path is a directory, this lists all files and directories within that path.
+If the path is a file, this returns the file's contents.
+
+When reading a file, if the file is too big and no line range is specified, an outline of the file's code symbols is listed instead, which can be used to request specific line ranges in a subsequent call.
+
+Similarly, if a directory has too many entries to show at once, a subset of entries will be shown,
+and subsequent requests can use starting and ending line numbers to get other subsets.
diff --git a/crates/assistant_tools/src/copy_path_tool.rs b/crates/assistant_tools/src/copy_path_tool.rs
index 3d7e407605..d2cc3f006b 100644
--- a/crates/assistant_tools/src/copy_path_tool.rs
+++ b/crates/assistant_tools/src/copy_path_tool.rs
@@ -55,7 +55,7 @@ impl Tool for CopyPathTool {
IconName::Clipboard
}
- fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value {
+ fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result {
json_schema_for::(format)
}
diff --git a/crates/assistant_tools/src/create_directory_tool.rs b/crates/assistant_tools/src/create_directory_tool.rs
index bf8cd1027e..aa5538d6e9 100644
--- a/crates/assistant_tools/src/create_directory_tool.rs
+++ b/crates/assistant_tools/src/create_directory_tool.rs
@@ -45,7 +45,7 @@ impl Tool for CreateDirectoryTool {
IconName::Folder
}
- fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value {
+ fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result