diff --git a/.github/ISSUE_TEMPLATE/10_bug_report.yml b/.github/ISSUE_TEMPLATE/10_bug_report.yml
index 1bf6c80e40..e132eca1e5 100644
--- a/.github/ISSUE_TEMPLATE/10_bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/10_bug_report.yml
@@ -14,7 +14,7 @@ body:
### Description
diff --git a/.github/workflows/cherry-pick.yml b/.github/workflows/cherry-pick.yml
new file mode 100644
index 0000000000..2dba9d5ba5
--- /dev/null
+++ b/.github/workflows/cherry-pick.yml
@@ -0,0 +1,151 @@
+name: Cherry Pick
+
+on:
+ issue_comment:
+ types: [created]
+ pull_request_target:
+ types: [closed]
+
+jobs:
+ cherry-pick:
+ # This job will run when a PR is merged with a specific comment,
+ # or when a comment is added to an already merged PR.
+ runs-on: ubuntu-latest
+ # Use pull_request_target so that we can add comments back to the PR
+ # if the cherry-pick fails.
+ permissions:
+ pull-requests: write
+ contents: write
+ issues: write
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0 # Required to get all history for cherry-picking
+
+ - name: Extract info and determine trigger
+ id: info
+ run: |
+ # Default to failure unless a valid trigger is found
+ echo "valid=false" >> $GITHUB_OUTPUT
+
+ if [[ "${{ github.event_name }}" == "pull_request_target" && "${{ github.event.pull_request.merged }}" == "true" ]]; then
+ echo "Triggered by PR merge"
+ PR_NUMBER="${{ github.event.pull_request.number }}"
+
+ # Check PR body first, then fall back to comments
+ TEXT_TO_SEARCH="${{ github.event.pull_request.body }}"
+ if [[ ! "$TEXT_TO_SEARCH" =~ /cherry-pick[[:space:]]+(stable|preview) ]]; then
+ echo "Command not found in PR body. Checking comments..."
+ TEXT_TO_SEARCH=$(gh pr view $PR_NUMBER --json comments -q '.comments[].body' | tail -n 100)
+ fi
+
+ if [[ "$TEXT_TO_SEARCH" =~ /cherry-pick[[:space:]]+(stable|preview) ]]; then
+ echo "Found cherry-pick command."
+ MERGE_SHA="${{ github.event.pull_request.merge_commit_sha }}"
+ # Get the last matching command in the text
+ CHANNEL=$(echo "$TEXT_TO_SEARCH" | grep -oP '/cherry-pick[[:space:]]+\K(stable|preview)' | tail -n1)
+
+ echo "valid=true" >> $GITHUB_OUTPUT
+ echo "merge_sha=$MERGE_SHA" >> $GITHUB_OUTPUT
+ echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT
+ echo "channel=$CHANNEL" >> $GITHUB_OUTPUT
+ else
+ echo "No cherry-pick command found in PR body or recent comments. Exiting."
+ exit 0
+ fi
+
+ elif [[ "${{ github.event_name }}" == "issue_comment" && "${{ github.event.issue.pull_request }}" != "" ]]; then
+ echo "Triggered by issue comment"
+ COMMENT_BODY="${{ github.event.comment.body }}"
+ if [[ ! "$COMMENT_BODY" =~ /cherry-pick[[:space:]]+(stable|preview) ]]; then
+ echo "Comment does not contain cherry-pick command. Exiting."
+ exit 0
+ fi
+
+ PR_NUMBER="${{ github.event.issue.number }}"
+
+ # Check if the PR is merged
+ MERGE_SHA=$(gh pr view $PR_NUMBER --json mergeCommit -q .mergeCommit.oid)
+ if [[ -z "$MERGE_SHA" ]]; then
+ echo "PR #$PR_NUMBER is not merged. Exiting."
+ exit 0
+ fi
+
+ CHANNEL=$(echo "$COMMENT_BODY" | grep -oP '/cherry-pick[[:space:]]+\K(stable|preview)' | head -n1)
+
+ echo "valid=true" >> $GITHUB_OUTPUT
+ echo "merge_sha=$MERGE_SHA" >> $GITHUB_OUTPUT
+ echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT
+ echo "channel=$CHANNEL" >> $GITHUB_OUTPUT
+ fi
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Cherry-pick
+ if: steps.info.outputs.valid == 'true'
+ run: |
+ set -e
+
+ CHANNEL="${{ steps.info.outputs.channel }}"
+ MERGE_SHA="${{ steps.info.outputs.merge_sha }}"
+ PR_NUMBER="${{ steps.info.outputs.pr_number }}"
+
+ # Get the latest version for the channel
+ echo "Fetching latest version for '$CHANNEL' channel..."
+ query=""
+ case $CHANNEL in
+ stable)
+ ;;
+ preview)
+ query="&preview=1"
+ ;;
+ *)
+ echo "Invalid channel: $CHANNEL" >&2
+ exit 1
+ ;;
+ esac
+ LATEST_VERSION=$(curl -s "https://zed.dev/api/releases/latest?asset=zed&os=macos&arch=aarch64$query" | jq -r .version)
+
+ if [[ -z "$LATEST_VERSION" ]]; then
+ echo "Could not fetch latest version for channel '$CHANNEL'"
+ gh pr comment $PR_NUMBER --body "Could not fetch latest version for channel '$CHANNEL' from zed.dev API."
+ exit 1
+ fi
+ echo "Latest version is $LATEST_VERSION"
+
+ # Construct target branch name (e.g., v0.85.4 -> v0.85.x)
+ TARGET_BRANCH=$(echo "$LATEST_VERSION" | sed -E 's/v([0-9]+\.[0-9]+)\..*/v\1.x/')
+ echo "Target branch is $TARGET_BRANCH"
+
+ # Configure git
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+
+ # Create and push the cherry-pick branch
+ NEW_BRANCH="cherry-pick/pr-${PR_NUMBER}-to-${TARGET_BRANCH}"
+
+ git fetch origin $TARGET_BRANCH
+ git checkout -b $NEW_BRANCH "origin/$TARGET_BRANCH"
+
+ echo "Attempting to cherry-pick $MERGE_SHA..."
+ if ! git cherry-pick $MERGE_SHA; then
+ echo "Cherry-pick failed. Please resolve conflicts manually."
+ gh pr comment $PR_NUMBER --body "Automated cherry-pick to \`$TARGET_BRANCH\` failed due to conflicts. Please resolve them manually."
+ exit 1
+ fi
+
+ echo "Pushing new branch $NEW_BRANCH..."
+ git push -u origin $NEW_BRANCH
+
+ # Create the pull request
+ echo "Creating pull request..."
+ gh pr create \
+ --title "Cherry-pick PR #${PR_NUMBER} to ${TARGET_BRANCH}" \
+ --body "This PR cherry-picks the changes from #${PR_NUMBER} to the \`$TARGET_BRANCH\` branch." \
+ --base $TARGET_BRANCH \
+ --head $NEW_BRANCH \
+ --reviewer "${{ github.actor }}"
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/Cargo.lock b/Cargo.lock
index 42649b137f..c835b503ad 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -13521,7 +13521,6 @@ dependencies = [
"smol",
"sysinfo",
"telemetry_events",
- "thiserror 2.0.12",
"toml 0.8.20",
"unindent",
"util",
diff --git a/assets/icons/terminal_ghost.svg b/assets/icons/terminal_ghost.svg
deleted file mode 100644
index 7d0d0e068e..0000000000
--- a/assets/icons/terminal_ghost.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
diff --git a/assets/images/acp_grid.svg b/assets/images/acp_grid.svg
deleted file mode 100644
index 8ebff8e1bc..0000000000
--- a/assets/images/acp_grid.svg
+++ /dev/null
@@ -1,1257 +0,0 @@
-
diff --git a/assets/images/acp_logo.svg b/assets/images/acp_logo.svg
deleted file mode 100644
index efaa46707b..0000000000
--- a/assets/images/acp_logo.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/assets/images/acp_logo_serif.svg b/assets/images/acp_logo_serif.svg
deleted file mode 100644
index 6bc359cf82..0000000000
--- a/assets/images/acp_logo_serif.svg
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json
index 3cca560c00..e84f4834af 100644
--- a/assets/keymaps/default-linux.json
+++ b/assets/keymaps/default-linux.json
@@ -40,7 +40,7 @@
"shift-f11": "debugger::StepOut",
"f11": "zed::ToggleFullScreen",
"ctrl-alt-z": "edit_prediction::RateCompletions",
- "ctrl-alt-shift-i": "edit_prediction::ToggleMenu",
+ "ctrl-shift-i": "edit_prediction::ToggleMenu",
"ctrl-alt-l": "lsp_tool::ToggleMenu"
}
},
@@ -120,7 +120,7 @@
"alt-g m": "git::OpenModifiedFiles",
"menu": "editor::OpenContextMenu",
"shift-f10": "editor::OpenContextMenu",
- "ctrl-alt-shift-e": "editor::ToggleEditPrediction",
+ "ctrl-shift-e": "editor::ToggleEditPrediction",
"f9": "editor::ToggleBreakpoint",
"shift-f9": "editor::EditLogBreakpoint"
}
diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json
deleted file mode 100644
index c7a6c3149c..0000000000
--- a/assets/keymaps/default-windows.json
+++ /dev/null
@@ -1,1260 +0,0 @@
-[
- // Standard Windows bindings
- {
- "use_key_equivalents": true,
- "bindings": {
- "home": "menu::SelectFirst",
- "shift-pageup": "menu::SelectFirst",
- "pageup": "menu::SelectFirst",
- "end": "menu::SelectLast",
- "shift-pagedown": "menu::SelectLast",
- "pagedown": "menu::SelectLast",
- "ctrl-n": "menu::SelectNext",
- "tab": "menu::SelectNext",
- "down": "menu::SelectNext",
- "ctrl-p": "menu::SelectPrevious",
- "shift-tab": "menu::SelectPrevious",
- "up": "menu::SelectPrevious",
- "enter": "menu::Confirm",
- "ctrl-enter": "menu::SecondaryConfirm",
- "ctrl-escape": "menu::Cancel",
- "ctrl-c": "menu::Cancel",
- "escape": "menu::Cancel",
- "shift-alt-enter": "menu::Restart",
- "alt-enter": ["picker::ConfirmInput", { "secondary": false }],
- "ctrl-alt-enter": ["picker::ConfirmInput", { "secondary": true }],
- "ctrl-shift-w": "workspace::CloseWindow",
- "shift-escape": "workspace::ToggleZoom",
- "open": "workspace::Open",
- "ctrl-o": "workspace::Open",
- "ctrl-=": ["zed::IncreaseBufferFontSize", { "persist": false }],
- "ctrl-shift-=": ["zed::IncreaseBufferFontSize", { "persist": false }],
- "ctrl--": ["zed::DecreaseBufferFontSize", { "persist": false }],
- "ctrl-0": ["zed::ResetBufferFontSize", { "persist": false }],
- "ctrl-,": "zed::OpenSettings",
- "ctrl-q": "zed::Quit",
- "f4": "debugger::Start",
- "shift-f5": "debugger::Stop",
- "ctrl-shift-f5": "debugger::RerunSession",
- "f6": "debugger::Pause",
- "f7": "debugger::StepOver",
- "ctrl-f11": "debugger::StepInto",
- "shift-f11": "debugger::StepOut",
- "f11": "zed::ToggleFullScreen",
- "ctrl-shift-i": "edit_prediction::ToggleMenu",
- "shift-alt-l": "lsp_tool::ToggleMenu"
- }
- },
- {
- "context": "Picker || menu",
- "use_key_equivalents": true,
- "bindings": {
- "up": "menu::SelectPrevious",
- "down": "menu::SelectNext"
- }
- },
- {
- "context": "Editor",
- "use_key_equivalents": true,
- "bindings": {
- "escape": "editor::Cancel",
- "shift-backspace": "editor::Backspace",
- "backspace": "editor::Backspace",
- "delete": "editor::Delete",
- "tab": "editor::Tab",
- "shift-tab": "editor::Backtab",
- "ctrl-k": "editor::CutToEndOfLine",
- "ctrl-k ctrl-q": "editor::Rewrap",
- "ctrl-k q": "editor::Rewrap",
- "ctrl-backspace": "editor::DeleteToPreviousWordStart",
- "ctrl-delete": "editor::DeleteToNextWordEnd",
- "cut": "editor::Cut",
- "shift-delete": "editor::Cut",
- "ctrl-x": "editor::Cut",
- "copy": "editor::Copy",
- "ctrl-insert": "editor::Copy",
- "ctrl-c": "editor::Copy",
- "paste": "editor::Paste",
- "shift-insert": "editor::Paste",
- "ctrl-v": "editor::Paste",
- "undo": "editor::Undo",
- "ctrl-z": "editor::Undo",
- "redo": "editor::Redo",
- "ctrl-y": "editor::Redo",
- "ctrl-shift-z": "editor::Redo",
- "up": "editor::MoveUp",
- "ctrl-up": "editor::LineUp",
- "ctrl-down": "editor::LineDown",
- "pageup": "editor::MovePageUp",
- "alt-pageup": "editor::PageUp",
- "shift-pageup": "editor::SelectPageUp",
- "home": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
- "down": "editor::MoveDown",
- "pagedown": "editor::MovePageDown",
- "alt-pagedown": "editor::PageDown",
- "shift-pagedown": "editor::SelectPageDown",
- "end": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": true }],
- "left": "editor::MoveLeft",
- "right": "editor::MoveRight",
- "ctrl-left": "editor::MoveToPreviousWordStart",
- "ctrl-right": "editor::MoveToNextWordEnd",
- "ctrl-home": "editor::MoveToBeginning",
- "ctrl-end": "editor::MoveToEnd",
- "shift-up": "editor::SelectUp",
- "shift-down": "editor::SelectDown",
- "shift-left": "editor::SelectLeft",
- "shift-right": "editor::SelectRight",
- "ctrl-shift-left": "editor::SelectToPreviousWordStart",
- "ctrl-shift-right": "editor::SelectToNextWordEnd",
- "ctrl-shift-home": "editor::SelectToBeginning",
- "ctrl-shift-end": "editor::SelectToEnd",
- "ctrl-a": "editor::SelectAll",
- "ctrl-l": "editor::SelectLine",
- "shift-alt-f": "editor::Format",
- "shift-alt-o": "editor::OrganizeImports",
- "shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
- "shift-end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
- "ctrl-alt-space": "editor::ShowCharacterPalette",
- "ctrl-;": "editor::ToggleLineNumbers",
- "ctrl-'": "editor::ToggleSelectedDiffHunks",
- "ctrl-\"": "editor::ExpandAllDiffHunks",
- "ctrl-i": "editor::ShowSignatureHelp",
- "alt-g b": "git::Blame",
- "alt-g m": "git::OpenModifiedFiles",
- "menu": "editor::OpenContextMenu",
- "shift-f10": "editor::OpenContextMenu",
- "ctrl-shift-e": "editor::ToggleEditPrediction",
- "f9": "editor::ToggleBreakpoint",
- "shift-f9": "editor::EditLogBreakpoint"
- }
- },
- {
- "context": "Editor && mode == full",
- "use_key_equivalents": true,
- "bindings": {
- "shift-enter": "editor::Newline",
- "enter": "editor::Newline",
- "ctrl-enter": "editor::NewlineAbove",
- "ctrl-shift-enter": "editor::NewlineBelow",
- "ctrl-k ctrl-z": "editor::ToggleSoftWrap",
- "ctrl-k z": "editor::ToggleSoftWrap",
- "find": "buffer_search::Deploy",
- "ctrl-f": "buffer_search::Deploy",
- "ctrl-h": "buffer_search::DeployReplace",
- "ctrl-shift-.": "assistant::QuoteSelection",
- "ctrl-shift-,": "assistant::InsertIntoEditor",
- "shift-alt-e": "editor::SelectEnclosingSymbol",
- "ctrl-shift-backspace": "editor::GoToPreviousChange",
- "ctrl-shift-alt-backspace": "editor::GoToNextChange",
- "alt-enter": "editor::OpenSelectionsInMultibuffer"
- }
- },
- {
- "context": "Editor && mode == full && edit_prediction",
- "use_key_equivalents": true,
- "bindings": {
- "alt-]": "editor::NextEditPrediction",
- "alt-[": "editor::PreviousEditPrediction"
- }
- },
- {
- "context": "Editor && !edit_prediction",
- "use_key_equivalents": true,
- "bindings": {
- "alt-\\": "editor::ShowEditPrediction"
- }
- },
- {
- "context": "Editor && mode == auto_height",
- "use_key_equivalents": true,
- "bindings": {
- "ctrl-enter": "editor::Newline",
- "shift-enter": "editor::Newline",
- "ctrl-shift-enter": "editor::NewlineBelow"
- }
- },
- {
- "context": "Markdown",
- "use_key_equivalents": true,
- "bindings": {
- "copy": "markdown::Copy",
- "ctrl-c": "markdown::Copy"
- }
- },
- {
- "context": "Editor && jupyter && !ContextEditor",
- "use_key_equivalents": true,
- "bindings": {
- "ctrl-shift-enter": "repl::Run",
- "ctrl-alt-enter": "repl::RunInPlace"
- }
- },
- {
- "context": "Editor && !agent_diff",
- "use_key_equivalents": true,
- "bindings": {
- "ctrl-k ctrl-r": "git::Restore",
- "alt-y": "git::StageAndNext",
- "shift-alt-y": "git::UnstageAndNext"
- }
- },
- {
- "context": "Editor && editor_agent_diff",
- "use_key_equivalents": true,
- "bindings": {
- "ctrl-y": "agent::Keep",
- "ctrl-n": "agent::Reject",
- "ctrl-shift-y": "agent::KeepAll",
- "ctrl-shift-n": "agent::RejectAll",
- "ctrl-shift-r": "agent::OpenAgentDiff"
- }
- },
- {
- "context": "AgentDiff",
- "use_key_equivalents": true,
- "bindings": {
- "ctrl-y": "agent::Keep",
- "ctrl-n": "agent::Reject",
- "ctrl-shift-y": "agent::KeepAll",
- "ctrl-shift-n": "agent::RejectAll"
- }
- },
- {
- "context": "ContextEditor > Editor",
- "use_key_equivalents": true,
- "bindings": {
- "ctrl-enter": "assistant::Assist",
- "ctrl-s": "workspace::Save",
- "save": "workspace::Save",
- "ctrl-shift-,": "assistant::InsertIntoEditor",
- "shift-enter": "assistant::Split",
- "ctrl-r": "assistant::CycleMessageRole",
- "enter": "assistant::ConfirmCommand",
- "alt-enter": "editor::Newline",
- "ctrl-k c": "assistant::CopyCode",
- "ctrl-g": "search::SelectNextMatch",
- "ctrl-shift-g": "search::SelectPreviousMatch",
- "ctrl-k l": "agent::OpenRulesLibrary"
- }
- },
- {
- "context": "AgentPanel",
- "use_key_equivalents": true,
- "bindings": {
- "ctrl-n": "agent::NewThread",
- "shift-alt-n": "agent::NewTextThread",
- "ctrl-shift-h": "agent::OpenHistory",
- "shift-alt-c": "agent::OpenSettings",
- "shift-alt-p": "agent::OpenRulesLibrary",
- "ctrl-i": "agent::ToggleProfileSelector",
- "shift-alt-/": "agent::ToggleModelSelector",
- "ctrl-shift-a": "agent::ToggleContextPicker",
- "ctrl-shift-j": "agent::ToggleNavigationMenu",
- "ctrl-shift-i": "agent::ToggleOptionsMenu",
- // "ctrl-shift-alt-n": "agent::ToggleNewThreadMenu",
- "shift-alt-escape": "agent::ExpandMessageEditor",
- "ctrl-shift-.": "assistant::QuoteSelection",
- "shift-alt-e": "agent::RemoveAllContext",
- "ctrl-shift-e": "project_panel::ToggleFocus",
- "ctrl-shift-enter": "agent::ContinueThread",
- "super-ctrl-b": "agent::ToggleBurnMode",
- "alt-enter": "agent::ContinueWithBurnMode"
- }
- },
- {
- "context": "AgentPanel > NavigationMenu",
- "use_key_equivalents": true,
- "bindings": {
- "shift-backspace": "agent::DeleteRecentlyOpenThread"
- }
- },
- {
- "context": "AgentPanel > Markdown",
- "use_key_equivalents": true,
- "bindings": {
- "copy": "markdown::CopyAsMarkdown",
- "ctrl-c": "markdown::CopyAsMarkdown"
- }
- },
- {
- "context": "AgentPanel && prompt_editor",
- "use_key_equivalents": true,
- "bindings": {
- "ctrl-n": "agent::NewTextThread",
- "ctrl-alt-t": "agent::NewThread"
- }
- },
- {
- "context": "AgentPanel && external_agent_thread",
- "use_key_equivalents": true,
- "bindings": {
- "ctrl-n": "agent::NewExternalAgentThread",
- "ctrl-alt-t": "agent::NewThread"
- }
- },
- {
- "context": "MessageEditor && !Picker > Editor && !use_modifier_to_send",
- "use_key_equivalents": true,
- "bindings": {
- "enter": "agent::Chat",
- "ctrl-enter": "agent::ChatWithFollow",
- "ctrl-i": "agent::ToggleProfileSelector",
- "ctrl-shift-r": "agent::OpenAgentDiff",
- "ctrl-shift-y": "agent::KeepAll",
- "ctrl-shift-n": "agent::RejectAll"
- }
- },
- {
- "context": "MessageEditor && !Picker > Editor && use_modifier_to_send",
- "use_key_equivalents": true,
- "bindings": {
- "ctrl-enter": "agent::Chat",
- "enter": "editor::Newline",
- "ctrl-i": "agent::ToggleProfileSelector",
- "ctrl-shift-r": "agent::OpenAgentDiff",
- "ctrl-shift-y": "agent::KeepAll",
- "ctrl-shift-n": "agent::RejectAll"
- }
- },
- {
- "context": "EditMessageEditor > Editor",
- "use_key_equivalents": true,
- "bindings": {
- "escape": "menu::Cancel",
- "enter": "menu::Confirm",
- "alt-enter": "editor::Newline"
- }
- },
- {
- "context": "AgentFeedbackMessageEditor > Editor",
- "use_key_equivalents": true,
- "bindings": {
- "escape": "menu::Cancel",
- "enter": "menu::Confirm",
- "alt-enter": "editor::Newline"
- }
- },
- {
- "context": "ContextStrip",
- "use_key_equivalents": true,
- "bindings": {
- "up": "agent::FocusUp",
- "right": "agent::FocusRight",
- "left": "agent::FocusLeft",
- "down": "agent::FocusDown",
- "backspace": "agent::RemoveFocusedContext",
- "enter": "agent::AcceptSuggestedContext"
- }
- },
- {
- "context": "AcpThread > Editor",
- "use_key_equivalents": true,
- "bindings": {
- "enter": "agent::Chat",
- "ctrl-shift-r": "agent::OpenAgentDiff",
- "ctrl-shift-y": "agent::KeepAll",
- "ctrl-shift-n": "agent::RejectAll"
- }
- },
- {
- "context": "ThreadHistory",
- "use_key_equivalents": true,
- "bindings": {
- "backspace": "agent::RemoveSelectedThread"
- }
- },
- {
- "context": "PromptLibrary",
- "use_key_equivalents": true,
- "bindings": {
- "new": "rules_library::NewRule",
- "ctrl-n": "rules_library::NewRule",
- "ctrl-shift-s": "rules_library::ToggleDefaultRule"
- }
- },
- {
- "context": "BufferSearchBar",
- "use_key_equivalents": true,
- "bindings": {
- "escape": "buffer_search::Dismiss",
- "tab": "buffer_search::FocusEditor",
- "enter": "search::SelectNextMatch",
- "shift-enter": "search::SelectPreviousMatch",
- "alt-enter": "search::SelectAllMatches",
- "find": "search::FocusSearch",
- "ctrl-f": "search::FocusSearch",
- "ctrl-h": "search::ToggleReplace",
- "ctrl-l": "search::ToggleSelection"
- }
- },
- {
- "context": "BufferSearchBar && in_replace > Editor",
- "use_key_equivalents": true,
- "bindings": {
- "enter": "search::ReplaceNext",
- "ctrl-enter": "search::ReplaceAll"
- }
- },
- {
- "context": "BufferSearchBar && !in_replace > Editor",
- "use_key_equivalents": true,
- "bindings": {
- "up": "search::PreviousHistoryQuery",
- "down": "search::NextHistoryQuery"
- }
- },
- {
- "context": "ProjectSearchBar",
- "use_key_equivalents": true,
- "bindings": {
- "escape": "project_search::ToggleFocus",
- "shift-find": "search::FocusSearch",
- "ctrl-shift-f": "search::FocusSearch",
- "ctrl-shift-h": "search::ToggleReplace",
- "alt-r": "search::ToggleRegex" // vscode
- }
- },
- {
- "context": "ProjectSearchBar > Editor",
- "use_key_equivalents": true,
- "bindings": {
- "up": "search::PreviousHistoryQuery",
- "down": "search::NextHistoryQuery"
- }
- },
- {
- "context": "ProjectSearchBar && in_replace > Editor",
- "use_key_equivalents": true,
- "bindings": {
- "enter": "search::ReplaceNext",
- "ctrl-alt-enter": "search::ReplaceAll"
- }
- },
- {
- "context": "ProjectSearchView",
- "use_key_equivalents": true,
- "bindings": {
- "escape": "project_search::ToggleFocus",
- "ctrl-shift-h": "search::ToggleReplace",
- "alt-r": "search::ToggleRegex" // vscode
- }
- },
- {
- "context": "Pane",
- "use_key_equivalents": true,
- "bindings": {
- "alt-1": ["pane::ActivateItem", 0],
- "alt-2": ["pane::ActivateItem", 1],
- "alt-3": ["pane::ActivateItem", 2],
- "alt-4": ["pane::ActivateItem", 3],
- "alt-5": ["pane::ActivateItem", 4],
- "alt-6": ["pane::ActivateItem", 5],
- "alt-7": ["pane::ActivateItem", 6],
- "alt-8": ["pane::ActivateItem", 7],
- "alt-9": ["pane::ActivateItem", 8],
- "alt-0": "pane::ActivateLastItem",
- "ctrl-pageup": "pane::ActivatePreviousItem",
- "ctrl-pagedown": "pane::ActivateNextItem",
- "ctrl-shift-pageup": "pane::SwapItemLeft",
- "ctrl-shift-pagedown": "pane::SwapItemRight",
- "ctrl-f4": ["pane::CloseActiveItem", { "close_pinned": false }],
- "ctrl-w": ["pane::CloseActiveItem", { "close_pinned": false }],
- "ctrl-shift-alt-t": ["pane::CloseOtherItems", { "close_pinned": false }],
- "ctrl-shift-alt-w": "workspace::CloseInactiveTabsAndPanes",
- "ctrl-k e": ["pane::CloseItemsToTheLeft", { "close_pinned": false }],
- "ctrl-k t": ["pane::CloseItemsToTheRight", { "close_pinned": false }],
- "ctrl-k u": ["pane::CloseCleanItems", { "close_pinned": false }],
- "ctrl-k w": ["pane::CloseAllItems", { "close_pinned": false }],
- "ctrl-k ctrl-w": "workspace::CloseAllItemsAndPanes",
- "back": "pane::GoBack",
- "alt--": "pane::GoBack",
- "alt-=": "pane::GoForward",
- "forward": "pane::GoForward",
- "f3": "search::SelectNextMatch",
- "shift-f3": "search::SelectPreviousMatch",
- "shift-find": "project_search::ToggleFocus",
- "ctrl-shift-f": "project_search::ToggleFocus",
- "shift-alt-h": "search::ToggleReplace",
- "alt-l": "search::ToggleSelection",
- "alt-enter": "search::SelectAllMatches",
- "alt-c": "search::ToggleCaseSensitive",
- "alt-w": "search::ToggleWholeWord",
- "alt-find": "project_search::ToggleFilters",
- "alt-f": "project_search::ToggleFilters",
- "alt-r": "search::ToggleRegex",
- // "ctrl-shift-alt-x": "search::ToggleRegex",
- "ctrl-k shift-enter": "pane::TogglePinTab"
- }
- },
- // Bindings from VS Code
- {
- "context": "Editor",
- "use_key_equivalents": true,
- "bindings": {
- "ctrl-[": "editor::Outdent",
- "ctrl-]": "editor::Indent",
- "ctrl-shift-alt-up": "editor::AddSelectionAbove", // Insert Cursor Above
- "ctrl-shift-alt-down": "editor::AddSelectionBelow", // Insert Cursor Below
- "ctrl-shift-k": "editor::DeleteLine",
- "alt-up": "editor::MoveLineUp",
- "alt-down": "editor::MoveLineDown",
- "shift-alt-up": "editor::DuplicateLineUp",
- "shift-alt-down": "editor::DuplicateLineDown",
- "shift-alt-right": "editor::SelectLargerSyntaxNode", // Expand Selection
- "shift-alt-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 }], // 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-k ctrl-b": "editor::BlameHover",
- "ctrl-/": ["editor::ToggleComments", { "advance_downwards": false }],
- "f8": ["editor::GoToDiagnostic", { "severity": { "min": "hint", "max": "error" } }],
- "shift-f8": ["editor::GoToPreviousDiagnostic", { "severity": { "min": "hint", "max": "error" } }],
- "f2": "editor::Rename",
- "f12": "editor::GoToDefinition",
- "alt-f12": "editor::GoToDefinitionSplit",
- "ctrl-shift-f10": "editor::GoToDefinitionSplit",
- "ctrl-f12": "editor::GoToImplementation",
- "shift-f12": "editor::GoToTypeDefinition",
- "ctrl-alt-f12": "editor::GoToTypeDefinitionSplit",
- "shift-alt-f12": "editor::FindAllReferences",
- "ctrl-m": "editor::MoveToEnclosingBracket", // from jetbrains
- "ctrl-shift-\\": "editor::MoveToEnclosingBracket",
- "ctrl-shift-[": "editor::Fold",
- "ctrl-shift-]": "editor::UnfoldLines",
- "ctrl-k ctrl-l": "editor::ToggleFold",
- "ctrl-k ctrl-[": "editor::FoldRecursive",
- "ctrl-k ctrl-]": "editor::UnfoldRecursive",
- "ctrl-k ctrl-1": ["editor::FoldAtLevel", 1],
- "ctrl-k ctrl-2": ["editor::FoldAtLevel", 2],
- "ctrl-k ctrl-3": ["editor::FoldAtLevel", 3],
- "ctrl-k ctrl-4": ["editor::FoldAtLevel", 4],
- "ctrl-k ctrl-5": ["editor::FoldAtLevel", 5],
- "ctrl-k ctrl-6": ["editor::FoldAtLevel", 6],
- "ctrl-k ctrl-7": ["editor::FoldAtLevel", 7],
- "ctrl-k ctrl-8": ["editor::FoldAtLevel", 8],
- "ctrl-k ctrl-9": ["editor::FoldAtLevel", 9],
- "ctrl-k ctrl-0": "editor::FoldAll",
- "ctrl-k ctrl-j": "editor::UnfoldAll",
- "ctrl-space": "editor::ShowCompletions",
- "ctrl-shift-space": "editor::ShowWordCompletions",
- "ctrl-.": "editor::ToggleCodeActions",
- "ctrl-k r": "editor::RevealInFileManager",
- "ctrl-k p": "editor::CopyPath",
- "ctrl-\\": "pane::SplitRight",
- "ctrl-shift-alt-c": "editor::DisplayCursorNames",
- "alt-.": "editor::GoToHunk",
- "alt-,": "editor::GoToPreviousHunk"
- }
- },
- {
- "context": "Editor && extension == md",
- "use_key_equivalents": true,
- "bindings": {
- "ctrl-k v": "markdown::OpenPreviewToTheSide",
- "ctrl-shift-v": "markdown::OpenPreview"
- }
- },
- {
- "context": "Editor && extension == svg",
- "use_key_equivalents": true,
- "bindings": {
- "ctrl-k v": "svg::OpenPreviewToTheSide",
- "ctrl-shift-v": "svg::OpenPreview"
- }
- },
- {
- "context": "Editor && mode == full",
- "use_key_equivalents": true,
- "bindings": {
- "ctrl-shift-o": "outline::Toggle",
- "ctrl-g": "go_to_line::Toggle"
- }
- },
- {
- "context": "Workspace",
- "use_key_equivalents": true,
- "bindings": {
- "alt-open": ["projects::OpenRecent", { "create_new_window": false }],
- // Change the default action on `menu::Confirm` by setting the parameter
- // "ctrl-alt-o": ["projects::OpenRecent", { "create_new_window": true }],
- "ctrl-r": ["projects::OpenRecent", { "create_new_window": false }],
- "shift-alt-open": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }],
- // Change to open path modal for existing remote connection by setting the parameter
- // "ctrl-shift-alt-o": "["projects::OpenRemote", { "from_existing_connection": true }]",
- "ctrl-shift-alt-o": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }],
- "shift-alt-b": "branches::OpenRecent",
- "shift-alt-enter": "toast::RunAction",
- "ctrl-shift-`": "workspace::NewTerminal",
- "save": "workspace::Save",
- "ctrl-s": "workspace::Save",
- "ctrl-k ctrl-shift-s": "workspace::SaveWithoutFormat",
- "shift-save": "workspace::SaveAs",
- "ctrl-shift-s": "workspace::SaveAs",
- "new": "workspace::NewFile",
- "ctrl-n": "workspace::NewFile",
- "shift-new": "workspace::NewWindow",
- "ctrl-shift-n": "workspace::NewWindow",
- "ctrl-`": "terminal_panel::ToggleFocus",
- "f10": ["app_menu::OpenApplicationMenu", "Zed"],
- "alt-1": ["workspace::ActivatePane", 0],
- "alt-2": ["workspace::ActivatePane", 1],
- "alt-3": ["workspace::ActivatePane", 2],
- "alt-4": ["workspace::ActivatePane", 3],
- "alt-5": ["workspace::ActivatePane", 4],
- "alt-6": ["workspace::ActivatePane", 5],
- "alt-7": ["workspace::ActivatePane", 6],
- "alt-8": ["workspace::ActivatePane", 7],
- "alt-9": ["workspace::ActivatePane", 8],
- "ctrl-alt-b": "workspace::ToggleRightDock",
- "ctrl-b": "workspace::ToggleLeftDock",
- "ctrl-j": "workspace::ToggleBottomDock",
- "ctrl-shift-y": "workspace::CloseAllDocks",
- "alt-r": "workspace::ResetActiveDockSize",
- // For 0px parameter, uses UI font size value.
- "shift-alt--": ["workspace::DecreaseActiveDockSize", { "px": 0 }],
- "shift-alt-=": ["workspace::IncreaseActiveDockSize", { "px": 0 }],
- "shift-alt-0": "workspace::ResetOpenDocksSize",
- "ctrl-shift-alt--": ["workspace::DecreaseOpenDocksSize", { "px": 0 }],
- "ctrl-shift-alt-=": ["workspace::IncreaseOpenDocksSize", { "px": 0 }],
- "shift-find": "pane::DeploySearch",
- "ctrl-shift-f": "pane::DeploySearch",
- "ctrl-shift-h": ["pane::DeploySearch", { "replace_enabled": true }],
- "ctrl-shift-t": "pane::ReopenClosedItem",
- "ctrl-k ctrl-s": "zed::OpenKeymapEditor",
- "ctrl-k ctrl-t": "theme_selector::Toggle",
- "ctrl-alt-super-p": "settings_profile_selector::Toggle",
- "ctrl-t": "project_symbols::Toggle",
- "ctrl-p": "file_finder::Toggle",
- "ctrl-tab": "tab_switcher::Toggle",
- "ctrl-shift-tab": ["tab_switcher::Toggle", { "select_last": true }],
- "ctrl-e": "file_finder::Toggle",
- "f1": "command_palette::Toggle",
- "ctrl-shift-p": "command_palette::Toggle",
- "ctrl-shift-m": "diagnostics::Deploy",
- "ctrl-shift-e": "project_panel::ToggleFocus",
- "ctrl-shift-b": "outline_panel::ToggleFocus",
- "ctrl-shift-g": "git_panel::ToggleFocus",
- "ctrl-shift-d": "debug_panel::ToggleFocus",
- "ctrl-shift-/": "agent::ToggleFocus",
- "alt-save": "workspace::SaveAll",
- "ctrl-k s": "workspace::SaveAll",
- "ctrl-k m": "language_selector::Toggle",
- "escape": "workspace::Unfollow",
- "ctrl-k ctrl-left": "workspace::ActivatePaneLeft",
- "ctrl-k ctrl-right": "workspace::ActivatePaneRight",
- "ctrl-k ctrl-up": "workspace::ActivatePaneUp",
- "ctrl-k ctrl-down": "workspace::ActivatePaneDown",
- "ctrl-k shift-left": "workspace::SwapPaneLeft",
- "ctrl-k shift-right": "workspace::SwapPaneRight",
- "ctrl-k shift-up": "workspace::SwapPaneUp",
- "ctrl-k shift-down": "workspace::SwapPaneDown",
- "ctrl-shift-x": "zed::Extensions",
- "ctrl-shift-r": "task::Rerun",
- "alt-t": "task::Rerun",
- "shift-alt-t": "task::Spawn",
- "shift-alt-r": ["task::Spawn", { "reveal_target": "center" }],
- // also possible to spawn tasks by name:
- // "foo-bar": ["task::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }]
- // or by tag:
- // "foo-bar": ["task::Spawn", { "task_tag": "MyTag" }],
- "f5": "debugger::Rerun",
- "ctrl-f4": "workspace::CloseActiveDock",
- "ctrl-w": "workspace::CloseActiveDock"
- }
- },
- {
- "context": "Workspace && debugger_running",
- "use_key_equivalents": true,
- "bindings": {
- "f5": "zed::NoAction"
- }
- },
- {
- "context": "Workspace && debugger_stopped",
- "use_key_equivalents": true,
- "bindings": {
- "f5": "debugger::Continue"
- }
- },
- {
- "context": "ApplicationMenu",
- "use_key_equivalents": true,
- "bindings": {
- "f10": "menu::Cancel",
- "left": "app_menu::ActivateMenuLeft",
- "right": "app_menu::ActivateMenuRight"
- }
- },
- // Bindings from Sublime Text
- {
- "context": "Editor",
- "use_key_equivalents": true,
- "bindings": {
- "ctrl-u": "editor::UndoSelection",
- "ctrl-shift-u": "editor::RedoSelection",
- "ctrl-shift-j": "editor::JoinLines",
- "ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",
- "shift-alt-h": "editor::DeleteToPreviousSubwordStart",
- "ctrl-alt-delete": "editor::DeleteToNextSubwordEnd",
- "shift-alt-d": "editor::DeleteToNextSubwordEnd",
- "ctrl-alt-left": "editor::MoveToPreviousSubwordStart",
- "ctrl-alt-right": "editor::MoveToNextSubwordEnd",
- "ctrl-shift-alt-left": "editor::SelectToPreviousSubwordStart",
- "ctrl-shift-alt-right": "editor::SelectToNextSubwordEnd"
- }
- },
- // Bindings from Atom
- {
- "context": "Pane",
- "use_key_equivalents": true,
- "bindings": {
- "ctrl-k up": "pane::SplitUp",
- "ctrl-k down": "pane::SplitDown",
- "ctrl-k left": "pane::SplitLeft",
- "ctrl-k right": "pane::SplitRight"
- }
- },
- // Bindings that should be unified with bindings for more general actions
- {
- "context": "Editor && renaming",
- "use_key_equivalents": true,
- "bindings": {
- "enter": "editor::ConfirmRename"
- }
- },
- {
- "context": "Editor && showing_completions",
- "use_key_equivalents": true,
- "bindings": {
- "enter": "editor::ConfirmCompletion",
- "shift-enter": "editor::ConfirmCompletionReplace",
- "tab": "editor::ComposeCompletion"
- }
- },
- // Bindings for accepting edit predictions
- //
- // alt-l is provided as an alternative to tab/alt-tab. and will be displayed in the UI. This is
- // because alt-tab may not be available, as it is often used for window switching.
- {
- "context": "Editor && edit_prediction",
- "use_key_equivalents": true,
- "bindings": {
- "alt-tab": "editor::AcceptEditPrediction",
- "alt-l": "editor::AcceptEditPrediction",
- "tab": "editor::AcceptEditPrediction",
- "alt-right": "editor::AcceptPartialEditPrediction"
- }
- },
- {
- "context": "Editor && edit_prediction_conflict",
- "use_key_equivalents": true,
- "bindings": {
- "alt-tab": "editor::AcceptEditPrediction",
- "alt-l": "editor::AcceptEditPrediction",
- "alt-right": "editor::AcceptPartialEditPrediction"
- }
- },
- {
- "context": "Editor && showing_code_actions",
- "use_key_equivalents": true,
- "bindings": {
- "enter": "editor::ConfirmCodeAction"
- }
- },
- {
- "context": "Editor && (showing_code_actions || showing_completions)",
- "use_key_equivalents": true,
- "bindings": {
- "ctrl-p": "editor::ContextMenuPrevious",
- "up": "editor::ContextMenuPrevious",
- "ctrl-n": "editor::ContextMenuNext",
- "down": "editor::ContextMenuNext",
- "pageup": "editor::ContextMenuFirst",
- "pagedown": "editor::ContextMenuLast"
- }
- },
- {
- "context": "Editor && showing_signature_help && !showing_completions",
- "use_key_equivalents": true,
- "bindings": {
- "up": "editor::SignatureHelpPrevious",
- "down": "editor::SignatureHelpNext"
- }
- },
- // Custom bindings
- {
- "use_key_equivalents": true,
- "bindings": {
- "ctrl-shift-alt-f": "workspace::FollowNextCollaborator",
- // Only available in debug builds: opens an element inspector for development.
- "shift-alt-i": "dev::ToggleInspector"
- }
- },
- {
- "context": "!Terminal",
- "use_key_equivalents": true,
- "bindings": {
- "ctrl-shift-c": "collab_panel::ToggleFocus"
- }
- },
- {
- "context": "!ContextEditor > Editor && mode == full",
- "use_key_equivalents": true,
- "bindings": {
- "alt-enter": "editor::OpenExcerpts",
- "shift-enter": "editor::ExpandExcerpts",
- "ctrl-alt-enter": "editor::OpenExcerptsSplit",
- "ctrl-shift-e": "pane::RevealInProjectPanel",
- "ctrl-f8": "editor::GoToHunk",
- "ctrl-shift-f8": "editor::GoToPreviousHunk",
- "ctrl-enter": "assistant::InlineAssist",
- "ctrl-shift-;": "editor::ToggleInlayHints"
- }
- },
- {
- "context": "PromptEditor",
- "use_key_equivalents": true,
- "bindings": {
- "ctrl-[": "agent::CyclePreviousInlineAssist",
- "ctrl-]": "agent::CycleNextInlineAssist",
- "shift-alt-e": "agent::RemoveAllContext"
- }
- },
- {
- "context": "Prompt",
- "use_key_equivalents": true,
- "bindings": {
- "left": "menu::SelectPrevious",
- "right": "menu::SelectNext",
- "h": "menu::SelectPrevious",
- "l": "menu::SelectNext"
- }
- },
- {
- "context": "ProjectSearchBar && !in_replace",
- "use_key_equivalents": true,
- "bindings": {
- "ctrl-enter": "project_search::SearchInNew"
- }
- },
- {
- "context": "OutlinePanel && not_editing",
- "use_key_equivalents": true,
- "bindings": {
- "left": "outline_panel::CollapseSelectedEntry",
- "right": "outline_panel::ExpandSelectedEntry",
- "alt-copy": "outline_panel::CopyPath",
- "shift-alt-c": "outline_panel::CopyPath",
- "shift-alt-copy": "workspace::CopyRelativePath",
- "ctrl-shift-alt-c": "workspace::CopyRelativePath",
- "ctrl-alt-r": "outline_panel::RevealInFileManager",
- "space": "outline_panel::OpenSelectedEntry",
- "shift-down": "menu::SelectNext",
- "shift-up": "menu::SelectPrevious",
- "alt-enter": "editor::OpenExcerpts",
- "ctrl-alt-enter": "editor::OpenExcerptsSplit"
- }
- },
- {
- "context": "ProjectPanel",
- "use_key_equivalents": true,
- "bindings": {
- "left": "project_panel::CollapseSelectedEntry",
- "right": "project_panel::ExpandSelectedEntry",
- "new": "project_panel::NewFile",
- "ctrl-n": "project_panel::NewFile",
- "alt-new": "project_panel::NewDirectory",
- "alt-n": "project_panel::NewDirectory",
- "cut": "project_panel::Cut",
- "ctrl-x": "project_panel::Cut",
- "copy": "project_panel::Copy",
- "ctrl-insert": "project_panel::Copy",
- "ctrl-c": "project_panel::Copy",
- "paste": "project_panel::Paste",
- "shift-insert": "project_panel::Paste",
- "ctrl-v": "project_panel::Paste",
- "alt-copy": "project_panel::CopyPath",
- "shift-alt-c": "project_panel::CopyPath",
- "shift-alt-copy": "workspace::CopyRelativePath",
- "ctrl-k ctrl-shift-c": "workspace::CopyRelativePath",
- "enter": "project_panel::Rename",
- "f2": "project_panel::Rename",
- "backspace": ["project_panel::Trash", { "skip_prompt": false }],
- "delete": ["project_panel::Trash", { "skip_prompt": false }],
- "shift-delete": ["project_panel::Delete", { "skip_prompt": false }],
- "ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }],
- "ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }],
- "ctrl-alt-r": "project_panel::RevealInFileManager",
- "ctrl-shift-enter": "project_panel::OpenWithSystem",
- "alt-d": "project_panel::CompareMarkedFiles",
- "shift-find": "project_panel::NewSearchInDirectory",
- "ctrl-k ctrl-shift-f": "project_panel::NewSearchInDirectory",
- "shift-down": "menu::SelectNext",
- "shift-up": "menu::SelectPrevious",
- "escape": "menu::Cancel"
- }
- },
- {
- "context": "ProjectPanel && not_editing",
- "use_key_equivalents": true,
- "bindings": {
- "space": "project_panel::Open"
- }
- },
- {
- "context": "GitPanel && ChangesList",
- "use_key_equivalents": true,
- "bindings": {
- "up": "menu::SelectPrevious",
- "down": "menu::SelectNext",
- "enter": "menu::Confirm",
- "alt-y": "git::StageFile",
- "shift-alt-y": "git::UnstageFile",
- "space": "git::ToggleStaged",
- "shift-space": "git::StageRange",
- "tab": "git_panel::FocusEditor",
- "shift-tab": "git_panel::FocusEditor",
- "escape": "git_panel::ToggleFocus",
- "alt-enter": "menu::SecondaryConfirm",
- "delete": ["git::RestoreFile", { "skip_prompt": false }],
- "backspace": ["git::RestoreFile", { "skip_prompt": false }],
- "shift-delete": ["git::RestoreFile", { "skip_prompt": false }],
- "ctrl-backspace": ["git::RestoreFile", { "skip_prompt": false }],
- "ctrl-delete": ["git::RestoreFile", { "skip_prompt": false }]
- }
- },
- {
- "context": "GitPanel && CommitEditor",
- "use_key_equivalents": true,
- "bindings": {
- "escape": "git::Cancel"
- }
- },
- {
- "context": "GitCommit > Editor",
- "use_key_equivalents": true,
- "bindings": {
- "escape": "menu::Cancel",
- "enter": "editor::Newline",
- "ctrl-enter": "git::Commit",
- "ctrl-shift-enter": "git::Amend",
- "alt-l": "git::GenerateCommitMessage"
- }
- },
- {
- "context": "GitPanel",
- "use_key_equivalents": true,
- "bindings": {
- "ctrl-g ctrl-g": "git::Fetch",
- "ctrl-g up": "git::Push",
- "ctrl-g down": "git::Pull",
- "ctrl-g shift-up": "git::ForcePush",
- "ctrl-g d": "git::Diff",
- "ctrl-g backspace": "git::RestoreTrackedFiles",
- "ctrl-g shift-backspace": "git::TrashUntrackedFiles",
- "ctrl-space": "git::StageAll",
- "ctrl-shift-space": "git::UnstageAll",
- "ctrl-enter": "git::Commit",
- "ctrl-shift-enter": "git::Amend"
- }
- },
- {
- "context": "GitDiff > Editor",
- "use_key_equivalents": true,
- "bindings": {
- "ctrl-enter": "git::Commit",
- "ctrl-shift-enter": "git::Amend",
- "ctrl-space": "git::StageAll",
- "ctrl-shift-space": "git::UnstageAll"
- }
- },
- {
- "context": "AskPass > Editor",
- "use_key_equivalents": true,
- "bindings": {
- "enter": "menu::Confirm"
- }
- },
- {
- "context": "CommitEditor > Editor",
- "use_key_equivalents": true,
- "bindings": {
- "escape": "git_panel::FocusChanges",
- "tab": "git_panel::FocusChanges",
- "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"
- }
- },
- {
- "context": "DebugPanel",
- "use_key_equivalents": true,
- "bindings": {
- "ctrl-t": "debugger::ToggleThreadPicker",
- "ctrl-i": "debugger::ToggleSessionPicker",
- "shift-alt-escape": "debugger::ToggleExpandItem"
- }
- },
- {
- "context": "VariableList",
- "use_key_equivalents": true,
- "bindings": {
- "left": "variable_list::CollapseSelectedEntry",
- "right": "variable_list::ExpandSelectedEntry",
- "enter": "variable_list::EditVariable",
- "ctrl-c": "variable_list::CopyVariableValue",
- "ctrl-alt-c": "variable_list::CopyVariableName",
- "delete": "variable_list::RemoveWatch",
- "backspace": "variable_list::RemoveWatch",
- "alt-enter": "variable_list::AddWatch"
- }
- },
- {
- "context": "BreakpointList",
- "use_key_equivalents": true,
- "bindings": {
- "space": "debugger::ToggleEnableBreakpoint",
- "backspace": "debugger::UnsetBreakpoint",
- "left": "debugger::PreviousBreakpointProperty",
- "right": "debugger::NextBreakpointProperty"
- }
- },
- {
- "context": "CollabPanel && not_editing",
- "use_key_equivalents": true,
- "bindings": {
- "ctrl-backspace": "collab_panel::Remove",
- "space": "menu::Confirm"
- }
- },
- {
- "context": "CollabPanel",
- "use_key_equivalents": true,
- "bindings": {
- "alt-up": "collab_panel::MoveChannelUp",
- "alt-down": "collab_panel::MoveChannelDown"
- }
- },
- {
- "context": "(CollabPanel && editing) > Editor",
- "use_key_equivalents": true,
- "bindings": {
- "space": "collab_panel::InsertSpace"
- }
- },
- {
- "context": "ChannelModal",
- "use_key_equivalents": true,
- "bindings": {
- "tab": "channel_modal::ToggleMode"
- }
- },
- {
- "context": "Picker > Editor",
- "use_key_equivalents": true,
- "bindings": {
- "escape": "menu::Cancel",
- "up": "menu::SelectPrevious",
- "down": "menu::SelectNext",
- "tab": "picker::ConfirmCompletion",
- "alt-enter": ["picker::ConfirmInput", { "secondary": false }]
- }
- },
- {
- "context": "ChannelModal > Picker > Editor",
- "use_key_equivalents": true,
- "bindings": {
- "tab": "channel_modal::ToggleMode"
- }
- },
- {
- "context": "FileFinder || (FileFinder > Picker > Editor)",
- "use_key_equivalents": true,
- "bindings": {
- "ctrl-p": "file_finder::Toggle",
- "ctrl-shift-a": "file_finder::ToggleSplitMenu",
- "ctrl-shift-i": "file_finder::ToggleFilterMenu"
- }
- },
- {
- "context": "FileFinder || (FileFinder > Picker > Editor) || (FileFinder > Picker > menu)",
- "use_key_equivalents": true,
- "bindings": {
- "ctrl-shift-p": "file_finder::SelectPrevious",
- "ctrl-j": "pane::SplitDown",
- "ctrl-k": "pane::SplitUp",
- "ctrl-h": "pane::SplitLeft",
- "ctrl-l": "pane::SplitRight"
- }
- },
- {
- "context": "TabSwitcher",
- "use_key_equivalents": true,
- "bindings": {
- "ctrl-shift-tab": "menu::SelectPrevious",
- "ctrl-up": "menu::SelectPrevious",
- "ctrl-down": "menu::SelectNext",
- "ctrl-backspace": "tab_switcher::CloseSelectedItem"
- }
- },
- {
- "context": "Terminal",
- "use_key_equivalents": true,
- "bindings": {
- "ctrl-alt-space": "terminal::ShowCharacterPalette",
- "copy": "terminal::Copy",
- "ctrl-insert": "terminal::Copy",
- "ctrl-shift-c": "terminal::Copy",
- "paste": "terminal::Paste",
- "shift-insert": "terminal::Paste",
- "ctrl-shift-v": "terminal::Paste",
- "ctrl-enter": "assistant::InlineAssist",
- "alt-b": ["terminal::SendText", "\u001bb"],
- "alt-f": ["terminal::SendText", "\u001bf"],
- "alt-.": ["terminal::SendText", "\u001b."],
- "ctrl-delete": ["terminal::SendText", "\u001bd"],
- // Overrides for conflicting keybindings
- "ctrl-b": ["terminal::SendKeystroke", "ctrl-b"],
- "ctrl-c": ["terminal::SendKeystroke", "ctrl-c"],
- "ctrl-e": ["terminal::SendKeystroke", "ctrl-e"],
- "ctrl-o": ["terminal::SendKeystroke", "ctrl-o"],
- "ctrl-w": ["terminal::SendKeystroke", "ctrl-w"],
- "ctrl-backspace": ["terminal::SendKeystroke", "ctrl-w"],
- "ctrl-shift-a": "editor::SelectAll",
- "find": "buffer_search::Deploy",
- "ctrl-shift-f": "buffer_search::Deploy",
- "ctrl-shift-l": "terminal::Clear",
- "ctrl-shift-w": "pane::CloseActiveItem",
- "up": ["terminal::SendKeystroke", "up"],
- "pageup": ["terminal::SendKeystroke", "pageup"],
- "down": ["terminal::SendKeystroke", "down"],
- "pagedown": ["terminal::SendKeystroke", "pagedown"],
- "escape": ["terminal::SendKeystroke", "escape"],
- "enter": ["terminal::SendKeystroke", "enter"],
- "shift-pageup": "terminal::ScrollPageUp",
- "shift-pagedown": "terminal::ScrollPageDown",
- "shift-up": "terminal::ScrollLineUp",
- "shift-down": "terminal::ScrollLineDown",
- "shift-home": "terminal::ScrollToTop",
- "shift-end": "terminal::ScrollToBottom",
- "ctrl-shift-space": "terminal::ToggleViMode",
- "ctrl-shift-r": "terminal::RerunTask",
- "ctrl-alt-r": "terminal::RerunTask",
- "alt-t": "terminal::RerunTask"
- }
- },
- {
- "context": "ZedPredictModal",
- "use_key_equivalents": true,
- "bindings": {
- "escape": "menu::Cancel"
- }
- },
- {
- "context": "ConfigureContextServerModal > Editor",
- "use_key_equivalents": true,
- "bindings": {
- "escape": "menu::Cancel",
- "enter": "editor::Newline",
- "ctrl-enter": "menu::Confirm"
- }
- },
- {
- "context": "OnboardingAiConfigurationModal",
- "use_key_equivalents": true,
- "bindings": {
- "escape": "menu::Cancel"
- }
- },
- {
- "context": "Diagnostics",
- "use_key_equivalents": true,
- "bindings": {
- "ctrl-r": "diagnostics::ToggleDiagnosticsRefresh"
- }
- },
- {
- "context": "DebugConsole > Editor",
- "use_key_equivalents": true,
- "bindings": {
- "enter": "menu::Confirm",
- "alt-enter": "console::WatchExpression"
- }
- },
- {
- "context": "RunModal",
- "use_key_equivalents": true,
- "bindings": {
- "ctrl-tab": "pane::ActivateNextItem",
- "ctrl-shift-tab": "pane::ActivatePreviousItem"
- }
- },
- {
- "context": "MarkdownPreview",
- "use_key_equivalents": true,
- "bindings": {
- "pageup": "markdown::MovePageUp",
- "pagedown": "markdown::MovePageDown"
- }
- },
- {
- "context": "KeymapEditor",
- "use_key_equivalents": true,
- "bindings": {
- "ctrl-f": "search::FocusSearch",
- "alt-find": "keymap_editor::ToggleKeystrokeSearch",
- "alt-f": "keymap_editor::ToggleKeystrokeSearch",
- "alt-c": "keymap_editor::ToggleConflictFilter",
- "enter": "keymap_editor::EditBinding",
- "alt-enter": "keymap_editor::CreateBinding",
- "ctrl-c": "keymap_editor::CopyAction",
- "ctrl-shift-c": "keymap_editor::CopyContext",
- "ctrl-t": "keymap_editor::ShowMatchingKeybinds"
- }
- },
- {
- "context": "KeystrokeInput",
- "use_key_equivalents": true,
- "bindings": {
- "enter": "keystroke_input::StartRecording",
- "escape escape escape": "keystroke_input::StopRecording",
- "delete": "keystroke_input::ClearKeystrokes"
- }
- },
- {
- "context": "KeybindEditorModal",
- "use_key_equivalents": true,
- "bindings": {
- "ctrl-enter": "menu::Confirm",
- "escape": "menu::Cancel"
- }
- },
- {
- "context": "KeybindEditorModal > Editor",
- "use_key_equivalents": true,
- "bindings": {
- "up": "menu::SelectPrevious",
- "down": "menu::SelectNext"
- }
- },
- {
- "context": "Onboarding",
- "use_key_equivalents": true,
- "bindings": {
- "ctrl-1": "onboarding::ActivateBasicsPage",
- "ctrl-2": "onboarding::ActivateEditingPage",
- "ctrl-3": "onboarding::ActivateAISetupPage",
- "ctrl-escape": "onboarding::Finish",
- "alt-tab": "onboarding::SignIn",
- "shift-alt-a": "onboarding::OpenAccount"
- }
- }
-]
diff --git a/assets/keymaps/linux/emacs.json b/assets/keymaps/linux/emacs.json
index 62910e297b..0ff3796f03 100755
--- a/assets/keymaps/linux/emacs.json
+++ b/assets/keymaps/linux/emacs.json
@@ -38,7 +38,6 @@
"alt-;": ["editor::ToggleComments", { "advance_downwards": false }],
"ctrl-x ctrl-;": "editor::ToggleComments",
"alt-.": "editor::GoToDefinition", // xref-find-definitions
- "alt-?": "editor::FindAllReferences", // xref-find-references
"alt-,": "pane::GoBack", // xref-pop-marker-stack
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer
"ctrl-d": "editor::Delete", // delete-char
diff --git a/assets/keymaps/macos/emacs.json b/assets/keymaps/macos/emacs.json
index 62910e297b..0ff3796f03 100755
--- a/assets/keymaps/macos/emacs.json
+++ b/assets/keymaps/macos/emacs.json
@@ -38,7 +38,6 @@
"alt-;": ["editor::ToggleComments", { "advance_downwards": false }],
"ctrl-x ctrl-;": "editor::ToggleComments",
"alt-.": "editor::GoToDefinition", // xref-find-definitions
- "alt-?": "editor::FindAllReferences", // xref-find-references
"alt-,": "pane::GoBack", // xref-pop-marker-stack
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer
"ctrl-d": "editor::Delete", // delete-char
diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json
index 67add61bd3..62e50b3c8c 100644
--- a/assets/keymaps/vim.json
+++ b/assets/keymaps/vim.json
@@ -428,13 +428,11 @@
"g h": "vim::StartOfLine",
"g s": "vim::FirstNonWhitespace", // "g s" default behavior is "space s"
"g e": "vim::EndOfDocument",
- "g .": "vim::HelixGotoLastModification", // go to last modification
"g r": "editor::FindAllReferences", // zed specific
"g t": "vim::WindowTop",
"g c": "vim::WindowMiddle",
"g b": "vim::WindowBottom",
- "shift-r": "editor::Paste",
"x": "editor::SelectLine",
"shift-x": "editor::SelectLine",
"%": "editor::SelectAll",
diff --git a/assets/settings/default.json b/assets/settings/default.json
index 804198090f..f0b9e11e57 100644
--- a/assets/settings/default.json
+++ b/assets/settings/default.json
@@ -653,8 +653,6 @@
// "never"
"show": "always"
},
- // Whether to enable drag-and-drop operations in the project panel.
- "drag_and_drop": true,
// Whether to hide the root entry when only one folder is open in the window.
"hide_root": false
},
diff --git a/assets/settings/initial_tasks.json b/assets/settings/initial_tasks.json
index 5cead67b6d..a79c550671 100644
--- a/assets/settings/initial_tasks.json
+++ b/assets/settings/initial_tasks.json
@@ -43,8 +43,8 @@
// "args": ["--login"]
// }
// }
- "shell": "system"
+ "shell": "system",
// Represents the tags for inline runnable indicators, or spawning multiple tasks at once.
- // "tags": []
+ "tags": []
}
]
diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs
index 4ded647a74..779f9964da 100644
--- a/crates/acp_thread/src/acp_thread.rs
+++ b/crates/acp_thread/src/acp_thread.rs
@@ -183,15 +183,16 @@ impl ToolCall {
language_registry: Arc,
cx: &mut App,
) -> Self {
- let title = if let Some((first_line, _)) = tool_call.title.split_once("\n") {
- first_line.to_owned() + "…"
- } else {
- tool_call.title
- };
Self {
id: tool_call.id,
- label: cx
- .new(|cx| Markdown::new(title.into(), Some(language_registry.clone()), None, cx)),
+ label: cx.new(|cx| {
+ Markdown::new(
+ tool_call.title.into(),
+ Some(language_registry.clone()),
+ None,
+ cx,
+ )
+ }),
kind: tool_call.kind,
content: tool_call
.content
@@ -232,11 +233,7 @@ impl ToolCall {
if let Some(title) = title {
self.label.update(cx, |label, cx| {
- if let Some((first_line, _)) = title.split_once("\n") {
- label.replace(first_line.to_owned() + "…", cx)
- } else {
- label.replace(title, cx);
- }
+ label.replace(title, cx);
});
}
diff --git a/crates/acp_tools/src/acp_tools.rs b/crates/acp_tools/src/acp_tools.rs
index e20a040e9d..ee12b04cde 100644
--- a/crates/acp_tools/src/acp_tools.rs
+++ b/crates/acp_tools/src/acp_tools.rs
@@ -21,12 +21,12 @@ use ui::prelude::*;
use util::ResultExt as _;
use workspace::{Item, Workspace};
-actions!(dev, [OpenAcpLogs]);
+actions!(acp, [OpenDebugTools]);
pub fn init(cx: &mut App) {
cx.observe_new(
|workspace: &mut Workspace, _window, _cx: &mut Context| {
- workspace.register_action(|workspace, _: &OpenAcpLogs, window, cx| {
+ workspace.register_action(|workspace, _: &OpenDebugTools, window, cx| {
let acp_tools =
Box::new(cx.new(|cx| AcpTools::new(workspace.project().clone(), cx)));
workspace.add_item_to_active_pane(acp_tools, None, true, window, cx);
diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs
index 7b70fde56a..899e360ab0 100644
--- a/crates/agent/src/thread.rs
+++ b/crates/agent/src/thread.rs
@@ -664,7 +664,7 @@ impl Thread {
}
pub fn get_or_init_configured_model(&mut self, cx: &App) -> Option {
- if self.configured_model.is_none() {
+ if self.configured_model.is_none() || self.messages.is_empty() {
self.configured_model = LanguageModelRegistry::read_global(cx).default_model();
}
self.configured_model.clone()
@@ -2097,7 +2097,7 @@ impl Thread {
}
pub fn summarize(&mut self, cx: &mut Context) {
- let Some(model) = LanguageModelRegistry::read_global(cx).thread_summary_model() else {
+ let Some(model) = LanguageModelRegistry::read_global(cx).thread_summary_model(cx) else {
println!("No thread summary model");
return;
};
@@ -2416,7 +2416,7 @@ impl Thread {
}
let Some(ConfiguredModel { model, provider }) =
- LanguageModelRegistry::read_global(cx).thread_summary_model()
+ LanguageModelRegistry::read_global(cx).thread_summary_model(cx)
else {
return;
};
@@ -5410,13 +5410,10 @@ fn main() {{
}),
cx,
);
- registry.set_thread_summary_model(
- Some(ConfiguredModel {
- provider,
- model: model.clone(),
- }),
- cx,
- );
+ registry.set_thread_summary_model(Some(ConfiguredModel {
+ provider,
+ model: model.clone(),
+ }));
})
});
diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs
index 6fa36d33d5..ecfaea4b49 100644
--- a/crates/agent2/src/agent.rs
+++ b/crates/agent2/src/agent.rs
@@ -228,7 +228,7 @@ impl NativeAgent {
) -> Entity {
let connection = Rc::new(NativeAgentConnection(cx.entity()));
let registry = LanguageModelRegistry::read_global(cx);
- let summarization_model = registry.thread_summary_model().map(|c| c.model);
+ let summarization_model = registry.thread_summary_model(cx).map(|c| c.model);
thread_handle.update(cx, |thread, cx| {
thread.set_summarization_model(summarization_model, cx);
@@ -524,7 +524,7 @@ impl NativeAgent {
let registry = LanguageModelRegistry::read_global(cx);
let default_model = registry.default_model().map(|m| m.model);
- let summarization_model = registry.thread_summary_model().map(|m| m.model);
+ let summarization_model = registry.thread_summary_model(cx).map(|m| m.model);
for session in self.sessions.values_mut() {
session.thread.update(cx, |thread, cx| {
diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs
index fbeee46a48..864fbf8b10 100644
--- a/crates/agent2/src/tests/mod.rs
+++ b/crates/agent2/src/tests/mod.rs
@@ -72,7 +72,6 @@ async fn test_echo(cx: &mut TestAppContext) {
}
#[gpui::test]
-#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows
async fn test_thinking(cx: &mut TestAppContext) {
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
let fake_model = model.as_fake();
@@ -472,7 +471,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
tool_name: ToolRequiringPermission::name().into(),
is_error: true,
content: "Permission to run tool denied by user".into(),
- output: Some("Permission to run tool denied by user".into())
+ output: None
})
]
);
@@ -1348,7 +1347,6 @@ async fn test_cancellation(cx: &mut TestAppContext) {
}
#[gpui::test]
-#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows
async fn test_in_progress_send_canceled_by_next_send(cx: &mut TestAppContext) {
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
let fake_model = model.as_fake();
@@ -1822,11 +1820,11 @@ async fn test_agent_connection(cx: &mut TestAppContext) {
let clock = Arc::new(clock::FakeSystemClock::new());
let client = Client::new(clock, http_client, cx);
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
+ Project::init_settings(cx);
+ agent_settings::init(cx);
language_model::init(client.clone(), cx);
language_models::init(user_store, client.clone(), cx);
- Project::init_settings(cx);
LanguageModelRegistry::test(cx);
- agent_settings::init(cx);
});
cx.executor().forbid_parking();
diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs
index 97ea1caf1d..1b1c014b79 100644
--- a/crates/agent2/src/thread.rs
+++ b/crates/agent2/src/thread.rs
@@ -732,17 +732,7 @@ impl Thread {
stream.update_tool_call_fields(
&tool_use.id,
acp::ToolCallUpdateFields {
- status: Some(
- tool_result
- .as_ref()
- .map_or(acp::ToolCallStatus::Failed, |result| {
- if result.is_error {
- acp::ToolCallStatus::Failed
- } else {
- acp::ToolCallStatus::Completed
- }
- }),
- ),
+ status: Some(acp::ToolCallStatus::Completed),
raw_output: output,
..Default::default()
},
@@ -1567,7 +1557,7 @@ impl Thread {
tool_name: tool_use.name,
is_error: true,
content: LanguageModelToolResultContent::Text(Arc::from(error.to_string())),
- output: Some(error.to_string().into()),
+ output: None,
},
}
}))
@@ -2469,30 +2459,6 @@ impl ToolCallEventStreamReceiver {
}
}
- pub async fn expect_update_fields(&mut self) -> acp::ToolCallUpdateFields {
- let event = self.0.next().await;
- if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(
- update,
- )))) = event
- {
- update.fields
- } else {
- panic!("Expected update fields but got: {:?}", event);
- }
- }
-
- pub async fn expect_diff(&mut self) -> Entity {
- let event = self.0.next().await;
- if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateDiff(
- update,
- )))) = event
- {
- update.diff
- } else {
- panic!("Expected diff but got: {:?}", event);
- }
- }
-
pub async fn expect_terminal(&mut self) -> Entity {
let event = self.0.next().await;
if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateTerminal(
diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs
index f86bfd25f7..5a68d0c70a 100644
--- a/crates/agent2/src/tools/edit_file_tool.rs
+++ b/crates/agent2/src/tools/edit_file_tool.rs
@@ -273,13 +273,6 @@ impl AgentTool for EditFileTool {
let diff = cx.new(|cx| Diff::new(buffer.clone(), cx))?;
event_stream.update_diff(diff.clone());
- let _finalize_diff = util::defer({
- let diff = diff.downgrade();
- let mut cx = cx.clone();
- move || {
- diff.update(&mut cx, |diff, cx| diff.finalize(cx)).ok();
- }
- });
let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
let old_text = cx
@@ -396,6 +389,8 @@ impl AgentTool for EditFileTool {
})
.await;
+ diff.update(cx, |diff, cx| diff.finalize(cx)).ok();
+
let input_path = input.path.display();
if unified_diff.is_empty() {
anyhow::ensure!(
@@ -1550,100 +1545,6 @@ mod tests {
);
}
- #[gpui::test]
- async fn test_diff_finalization(cx: &mut TestAppContext) {
- init_test(cx);
- let fs = project::FakeFs::new(cx.executor());
- fs.insert_tree("/", json!({"main.rs": ""})).await;
-
- let project = Project::test(fs.clone(), [path!("/").as_ref()], cx).await;
- let languages = project.read_with(cx, |project, _cx| project.languages().clone());
- let context_server_registry =
- cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
- let model = Arc::new(FakeLanguageModel::default());
- let thread = cx.new(|cx| {
- Thread::new(
- project.clone(),
- cx.new(|_cx| ProjectContext::default()),
- context_server_registry.clone(),
- Templates::new(),
- Some(model.clone()),
- cx,
- )
- });
-
- // Ensure the diff is finalized after the edit completes.
- {
- let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone()));
- let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
- let edit = cx.update(|cx| {
- tool.run(
- EditFileToolInput {
- display_description: "Edit file".into(),
- path: path!("/main.rs").into(),
- mode: EditFileMode::Edit,
- },
- stream_tx,
- cx,
- )
- });
- stream_rx.expect_update_fields().await;
- let diff = stream_rx.expect_diff().await;
- diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_))));
- cx.run_until_parked();
- model.end_last_completion_stream();
- edit.await.unwrap();
- diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_))));
- }
-
- // Ensure the diff is finalized if an error occurs while editing.
- {
- model.forbid_requests();
- let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone()));
- let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
- let edit = cx.update(|cx| {
- tool.run(
- EditFileToolInput {
- display_description: "Edit file".into(),
- path: path!("/main.rs").into(),
- mode: EditFileMode::Edit,
- },
- stream_tx,
- cx,
- )
- });
- stream_rx.expect_update_fields().await;
- let diff = stream_rx.expect_diff().await;
- diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_))));
- edit.await.unwrap_err();
- diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_))));
- model.allow_requests();
- }
-
- // Ensure the diff is finalized if the tool call gets dropped.
- {
- let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone()));
- let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
- let edit = cx.update(|cx| {
- tool.run(
- EditFileToolInput {
- display_description: "Edit file".into(),
- path: path!("/main.rs").into(),
- mode: EditFileMode::Edit,
- },
- stream_tx,
- cx,
- )
- });
- stream_rx.expect_update_fields().await;
- let diff = stream_rx.expect_diff().await;
- diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_))));
- drop(edit);
- cx.run_until_parked();
- diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_))));
- }
- }
-
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
diff --git a/crates/agent2/src/tools/read_file_tool.rs b/crates/agent2/src/tools/read_file_tool.rs
index e771c26eca..fea9732093 100644
--- a/crates/agent2/src/tools/read_file_tool.rs
+++ b/crates/agent2/src/tools/read_file_tool.rs
@@ -11,7 +11,6 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
use std::{path::Path, sync::Arc};
-use util::markdown::MarkdownCodeBlock;
use crate::{AgentTool, ToolCallEventStream};
@@ -244,19 +243,6 @@ impl AgentTool for ReadFileTool {
}]),
..Default::default()
});
- if let Ok(LanguageModelToolResultContent::Text(text)) = &result {
- let markdown = MarkdownCodeBlock {
- tag: &input.path,
- text,
- }
- .to_string();
- event_stream.update_fields(ToolCallUpdateFields {
- content: Some(vec![acp::ToolCallContent::Content {
- content: markdown.into(),
- }]),
- ..Default::default()
- })
- }
}
})?;
diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs
index b4e897374a..5a4efe12e5 100644
--- a/crates/agent_servers/src/acp.rs
+++ b/crates/agent_servers/src/acp.rs
@@ -162,34 +162,12 @@ impl AgentConnection for AcpConnection {
let conn = self.connection.clone();
let sessions = self.sessions.clone();
let cwd = cwd.to_path_buf();
- let context_server_store = project.read(cx).context_server_store().read(cx);
- let mcp_servers = context_server_store
- .configured_server_ids()
- .iter()
- .filter_map(|id| {
- let configuration = context_server_store.configuration_for_server(id)?;
- let command = configuration.command();
- Some(acp::McpServer {
- name: id.0.to_string(),
- command: command.path.clone(),
- args: command.args.clone(),
- env: if let Some(env) = command.env.as_ref() {
- env.iter()
- .map(|(name, value)| acp::EnvVariable {
- name: name.clone(),
- value: value.clone(),
- })
- .collect()
- } else {
- vec![]
- },
- })
- })
- .collect();
-
cx.spawn(async move |cx| {
let response = conn
- .new_session(acp::NewSessionRequest { mcp_servers, cwd })
+ .new_session(acp::NewSessionRequest {
+ mcp_servers: vec![],
+ cwd,
+ })
.await
.map_err(|err| {
if err.code == acp::ErrorCode::AUTH_REQUIRED.code {
@@ -288,9 +266,7 @@ impl AgentConnection for AcpConnection {
match serde_json::from_value(data.clone()) {
Ok(ErrorDetails { details }) => {
- if suppress_abort_err
- && (details.contains("This operation was aborted")
- || details.contains("The user aborted a request"))
+ if suppress_abort_err && details.contains("This operation was aborted")
{
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::Cancelled,
diff --git a/crates/agent_servers/src/acp/v0.rs b/crates/agent_servers/src/acp/v0.rs
new file mode 100644
index 0000000000..be96048929
--- /dev/null
+++ b/crates/agent_servers/src/acp/v0.rs
@@ -0,0 +1,524 @@
+// Translates old acp agents into the new schema
+use action_log::ActionLog;
+use agent_client_protocol as acp;
+use agentic_coding_protocol::{self as acp_old, AgentRequest as _};
+use anyhow::{Context as _, Result, anyhow};
+use futures::channel::oneshot;
+use gpui::{AppContext as _, AsyncApp, Entity, Task, WeakEntity};
+use project::Project;
+use std::{any::Any, cell::RefCell, path::Path, rc::Rc};
+use ui::App;
+use util::ResultExt as _;
+
+use crate::AgentServerCommand;
+use acp_thread::{AcpThread, AgentConnection, AuthRequired};
+
+#[derive(Clone)]
+struct OldAcpClientDelegate {
+ thread: Rc>>,
+ cx: AsyncApp,
+ next_tool_call_id: Rc>,
+ // sent_buffer_versions: HashMap, HashMap>,
+}
+
+impl OldAcpClientDelegate {
+ fn new(thread: Rc>>, cx: AsyncApp) -> Self {
+ Self {
+ thread,
+ cx,
+ next_tool_call_id: Rc::new(RefCell::new(0)),
+ }
+ }
+}
+
+impl acp_old::Client for OldAcpClientDelegate {
+ async fn stream_assistant_message_chunk(
+ &self,
+ params: acp_old::StreamAssistantMessageChunkParams,
+ ) -> Result<(), acp_old::Error> {
+ let cx = &mut self.cx.clone();
+
+ cx.update(|cx| {
+ self.thread
+ .borrow()
+ .update(cx, |thread, cx| match params.chunk {
+ acp_old::AssistantMessageChunk::Text { text } => {
+ thread.push_assistant_content_block(text.into(), false, cx)
+ }
+ acp_old::AssistantMessageChunk::Thought { thought } => {
+ thread.push_assistant_content_block(thought.into(), true, cx)
+ }
+ })
+ .log_err();
+ })?;
+
+ Ok(())
+ }
+
+ async fn request_tool_call_confirmation(
+ &self,
+ request: acp_old::RequestToolCallConfirmationParams,
+ ) -> Result {
+ let cx = &mut self.cx.clone();
+
+ let old_acp_id = *self.next_tool_call_id.borrow() + 1;
+ self.next_tool_call_id.replace(old_acp_id);
+
+ let tool_call = into_new_tool_call(
+ acp::ToolCallId(old_acp_id.to_string().into()),
+ request.tool_call,
+ );
+
+ let mut options = match request.confirmation {
+ acp_old::ToolCallConfirmation::Edit { .. } => vec![(
+ acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
+ acp::PermissionOptionKind::AllowAlways,
+ "Always Allow Edits".to_string(),
+ )],
+ acp_old::ToolCallConfirmation::Execute { root_command, .. } => vec![(
+ acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
+ acp::PermissionOptionKind::AllowAlways,
+ format!("Always Allow {}", root_command),
+ )],
+ acp_old::ToolCallConfirmation::Mcp {
+ server_name,
+ tool_name,
+ ..
+ } => vec![
+ (
+ acp_old::ToolCallConfirmationOutcome::AlwaysAllowMcpServer,
+ acp::PermissionOptionKind::AllowAlways,
+ format!("Always Allow {}", server_name),
+ ),
+ (
+ acp_old::ToolCallConfirmationOutcome::AlwaysAllowTool,
+ acp::PermissionOptionKind::AllowAlways,
+ format!("Always Allow {}", tool_name),
+ ),
+ ],
+ acp_old::ToolCallConfirmation::Fetch { .. } => vec![(
+ acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
+ acp::PermissionOptionKind::AllowAlways,
+ "Always Allow".to_string(),
+ )],
+ acp_old::ToolCallConfirmation::Other { .. } => vec![(
+ acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
+ acp::PermissionOptionKind::AllowAlways,
+ "Always Allow".to_string(),
+ )],
+ };
+
+ options.extend([
+ (
+ acp_old::ToolCallConfirmationOutcome::Allow,
+ acp::PermissionOptionKind::AllowOnce,
+ "Allow".to_string(),
+ ),
+ (
+ acp_old::ToolCallConfirmationOutcome::Reject,
+ acp::PermissionOptionKind::RejectOnce,
+ "Reject".to_string(),
+ ),
+ ]);
+
+ let mut outcomes = Vec::with_capacity(options.len());
+ let mut acp_options = Vec::with_capacity(options.len());
+
+ for (index, (outcome, kind, label)) in options.into_iter().enumerate() {
+ outcomes.push(outcome);
+ acp_options.push(acp::PermissionOption {
+ id: acp::PermissionOptionId(index.to_string().into()),
+ name: label,
+ kind,
+ })
+ }
+
+ let response = cx
+ .update(|cx| {
+ self.thread.borrow().update(cx, |thread, cx| {
+ thread.request_tool_call_authorization(tool_call.into(), acp_options, cx)
+ })
+ })??
+ .context("Failed to update thread")?
+ .await;
+
+ let outcome = match response {
+ Ok(option_id) => outcomes[option_id.0.parse::().unwrap_or(0)],
+ Err(oneshot::Canceled) => acp_old::ToolCallConfirmationOutcome::Cancel,
+ };
+
+ Ok(acp_old::RequestToolCallConfirmationResponse {
+ id: acp_old::ToolCallId(old_acp_id),
+ outcome,
+ })
+ }
+
+ async fn push_tool_call(
+ &self,
+ request: acp_old::PushToolCallParams,
+ ) -> Result {
+ let cx = &mut self.cx.clone();
+
+ let old_acp_id = *self.next_tool_call_id.borrow() + 1;
+ self.next_tool_call_id.replace(old_acp_id);
+
+ cx.update(|cx| {
+ self.thread.borrow().update(cx, |thread, cx| {
+ thread.upsert_tool_call(
+ into_new_tool_call(acp::ToolCallId(old_acp_id.to_string().into()), request),
+ cx,
+ )
+ })
+ })??
+ .context("Failed to update thread")?;
+
+ Ok(acp_old::PushToolCallResponse {
+ id: acp_old::ToolCallId(old_acp_id),
+ })
+ }
+
+ async fn update_tool_call(
+ &self,
+ request: acp_old::UpdateToolCallParams,
+ ) -> Result<(), acp_old::Error> {
+ let cx = &mut self.cx.clone();
+
+ cx.update(|cx| {
+ self.thread.borrow().update(cx, |thread, cx| {
+ thread.update_tool_call(
+ acp::ToolCallUpdate {
+ id: acp::ToolCallId(request.tool_call_id.0.to_string().into()),
+ fields: acp::ToolCallUpdateFields {
+ status: Some(into_new_tool_call_status(request.status)),
+ content: Some(
+ request
+ .content
+ .into_iter()
+ .map(into_new_tool_call_content)
+ .collect::>(),
+ ),
+ ..Default::default()
+ },
+ },
+ cx,
+ )
+ })
+ })?
+ .context("Failed to update thread")??;
+
+ Ok(())
+ }
+
+ async fn update_plan(&self, request: acp_old::UpdatePlanParams) -> Result<(), acp_old::Error> {
+ let cx = &mut self.cx.clone();
+
+ cx.update(|cx| {
+ self.thread.borrow().update(cx, |thread, cx| {
+ thread.update_plan(
+ acp::Plan {
+ entries: request
+ .entries
+ .into_iter()
+ .map(into_new_plan_entry)
+ .collect(),
+ },
+ cx,
+ )
+ })
+ })?
+ .context("Failed to update thread")?;
+
+ Ok(())
+ }
+
+ async fn read_text_file(
+ &self,
+ acp_old::ReadTextFileParams { path, line, limit }: acp_old::ReadTextFileParams,
+ ) -> Result {
+ let content = self
+ .cx
+ .update(|cx| {
+ self.thread.borrow().update(cx, |thread, cx| {
+ thread.read_text_file(path, line, limit, false, cx)
+ })
+ })?
+ .context("Failed to update thread")?
+ .await?;
+ Ok(acp_old::ReadTextFileResponse { content })
+ }
+
+ async fn write_text_file(
+ &self,
+ acp_old::WriteTextFileParams { path, content }: acp_old::WriteTextFileParams,
+ ) -> Result<(), acp_old::Error> {
+ self.cx
+ .update(|cx| {
+ self.thread
+ .borrow()
+ .update(cx, |thread, cx| thread.write_text_file(path, content, cx))
+ })?
+ .context("Failed to update thread")?
+ .await?;
+
+ Ok(())
+ }
+}
+
+fn into_new_tool_call(id: acp::ToolCallId, request: acp_old::PushToolCallParams) -> acp::ToolCall {
+ acp::ToolCall {
+ id,
+ title: request.label,
+ kind: acp_kind_from_old_icon(request.icon),
+ status: acp::ToolCallStatus::InProgress,
+ content: request
+ .content
+ .into_iter()
+ .map(into_new_tool_call_content)
+ .collect(),
+ locations: request
+ .locations
+ .into_iter()
+ .map(into_new_tool_call_location)
+ .collect(),
+ raw_input: None,
+ raw_output: None,
+ }
+}
+
+fn acp_kind_from_old_icon(icon: acp_old::Icon) -> acp::ToolKind {
+ match icon {
+ acp_old::Icon::FileSearch => acp::ToolKind::Search,
+ acp_old::Icon::Folder => acp::ToolKind::Search,
+ acp_old::Icon::Globe => acp::ToolKind::Search,
+ acp_old::Icon::Hammer => acp::ToolKind::Other,
+ acp_old::Icon::LightBulb => acp::ToolKind::Think,
+ acp_old::Icon::Pencil => acp::ToolKind::Edit,
+ acp_old::Icon::Regex => acp::ToolKind::Search,
+ acp_old::Icon::Terminal => acp::ToolKind::Execute,
+ }
+}
+
+fn into_new_tool_call_status(status: acp_old::ToolCallStatus) -> acp::ToolCallStatus {
+ match status {
+ acp_old::ToolCallStatus::Running => acp::ToolCallStatus::InProgress,
+ acp_old::ToolCallStatus::Finished => acp::ToolCallStatus::Completed,
+ acp_old::ToolCallStatus::Error => acp::ToolCallStatus::Failed,
+ }
+}
+
+fn into_new_tool_call_content(content: acp_old::ToolCallContent) -> acp::ToolCallContent {
+ match content {
+ acp_old::ToolCallContent::Markdown { markdown } => markdown.into(),
+ acp_old::ToolCallContent::Diff { diff } => acp::ToolCallContent::Diff {
+ diff: into_new_diff(diff),
+ },
+ }
+}
+
+fn into_new_diff(diff: acp_old::Diff) -> acp::Diff {
+ acp::Diff {
+ path: diff.path,
+ old_text: diff.old_text,
+ new_text: diff.new_text,
+ }
+}
+
+fn into_new_tool_call_location(location: acp_old::ToolCallLocation) -> acp::ToolCallLocation {
+ acp::ToolCallLocation {
+ path: location.path,
+ line: location.line,
+ }
+}
+
+fn into_new_plan_entry(entry: acp_old::PlanEntry) -> acp::PlanEntry {
+ acp::PlanEntry {
+ content: entry.content,
+ priority: into_new_plan_priority(entry.priority),
+ status: into_new_plan_status(entry.status),
+ }
+}
+
+fn into_new_plan_priority(priority: acp_old::PlanEntryPriority) -> acp::PlanEntryPriority {
+ match priority {
+ acp_old::PlanEntryPriority::Low => acp::PlanEntryPriority::Low,
+ acp_old::PlanEntryPriority::Medium => acp::PlanEntryPriority::Medium,
+ acp_old::PlanEntryPriority::High => acp::PlanEntryPriority::High,
+ }
+}
+
+fn into_new_plan_status(status: acp_old::PlanEntryStatus) -> acp::PlanEntryStatus {
+ match status {
+ acp_old::PlanEntryStatus::Pending => acp::PlanEntryStatus::Pending,
+ acp_old::PlanEntryStatus::InProgress => acp::PlanEntryStatus::InProgress,
+ acp_old::PlanEntryStatus::Completed => acp::PlanEntryStatus::Completed,
+ }
+}
+
+pub struct AcpConnection {
+ pub name: &'static str,
+ pub connection: acp_old::AgentConnection,
+ pub _child_status: Task>,
+ pub current_thread: Rc>>,
+}
+
+impl AcpConnection {
+ pub fn stdio(
+ name: &'static str,
+ command: AgentServerCommand,
+ root_dir: &Path,
+ cx: &mut AsyncApp,
+ ) -> Task> {
+ let root_dir = root_dir.to_path_buf();
+
+ cx.spawn(async move |cx| {
+ let mut child = util::command::new_smol_command(&command.path)
+ .args(command.args.iter())
+ .current_dir(root_dir)
+ .stdin(std::process::Stdio::piped())
+ .stdout(std::process::Stdio::piped())
+ .stderr(std::process::Stdio::inherit())
+ .kill_on_drop(true)
+ .spawn()?;
+
+ let stdin = child.stdin.take().unwrap();
+ let stdout = child.stdout.take().unwrap();
+ log::trace!("Spawned (pid: {})", child.id());
+
+ let foreground_executor = cx.foreground_executor().clone();
+
+ let thread_rc = Rc::new(RefCell::new(WeakEntity::new_invalid()));
+
+ let (connection, io_fut) = acp_old::AgentConnection::connect_to_agent(
+ OldAcpClientDelegate::new(thread_rc.clone(), cx.clone()),
+ stdin,
+ stdout,
+ move |fut| foreground_executor.spawn(fut).detach(),
+ );
+
+ let io_task = cx.background_spawn(async move {
+ io_fut.await.log_err();
+ });
+
+ let child_status = cx.background_spawn(async move {
+ let result = match child.status().await {
+ Err(e) => Err(anyhow!(e)),
+ Ok(result) if result.success() => Ok(()),
+ Ok(result) => Err(anyhow!(result)),
+ };
+ drop(io_task);
+ result
+ });
+
+ Ok(Self {
+ name,
+ connection,
+ _child_status: child_status,
+ current_thread: thread_rc,
+ })
+ })
+ }
+}
+
+impl AgentConnection for AcpConnection {
+ fn new_thread(
+ self: Rc,
+ project: Entity,
+ _cwd: &Path,
+ cx: &mut App,
+ ) -> Task>> {
+ let task = self.connection.request_any(
+ acp_old::InitializeParams {
+ protocol_version: acp_old::ProtocolVersion::latest(),
+ }
+ .into_any(),
+ );
+ let current_thread = self.current_thread.clone();
+ cx.spawn(async move |cx| {
+ let result = task.await?;
+ let result = acp_old::InitializeParams::response_from_any(result)?;
+
+ if !result.is_authenticated {
+ anyhow::bail!(AuthRequired::new())
+ }
+
+ cx.update(|cx| {
+ let thread = cx.new(|cx| {
+ let session_id = acp::SessionId("acp-old-no-id".into());
+ let action_log = cx.new(|_| ActionLog::new(project.clone()));
+ AcpThread::new(self.name, self.clone(), project, action_log, session_id)
+ });
+ current_thread.replace(thread.downgrade());
+ thread
+ })
+ })
+ }
+
+ fn auth_methods(&self) -> &[acp::AuthMethod] {
+ &[]
+ }
+
+ fn authenticate(&self, _method_id: acp::AuthMethodId, cx: &mut App) -> Task> {
+ let task = self
+ .connection
+ .request_any(acp_old::AuthenticateParams.into_any());
+ cx.foreground_executor().spawn(async move {
+ task.await?;
+ Ok(())
+ })
+ }
+
+ fn prompt(
+ &self,
+ _id: Option,
+ params: acp::PromptRequest,
+ cx: &mut App,
+ ) -> Task> {
+ let chunks = params
+ .prompt
+ .into_iter()
+ .filter_map(|block| match block {
+ acp::ContentBlock::Text(text) => {
+ Some(acp_old::UserMessageChunk::Text { text: text.text })
+ }
+ acp::ContentBlock::ResourceLink(link) => Some(acp_old::UserMessageChunk::Path {
+ path: link.uri.into(),
+ }),
+ _ => None,
+ })
+ .collect();
+
+ let task = self
+ .connection
+ .request_any(acp_old::SendUserMessageParams { chunks }.into_any());
+ cx.foreground_executor().spawn(async move {
+ task.await?;
+ anyhow::Ok(acp::PromptResponse {
+ stop_reason: acp::StopReason::EndTurn,
+ })
+ })
+ }
+
+ fn prompt_capabilities(&self) -> acp::PromptCapabilities {
+ acp::PromptCapabilities {
+ image: false,
+ audio: false,
+ embedded_context: false,
+ }
+ }
+
+ fn cancel(&self, _session_id: &acp::SessionId, cx: &mut App) {
+ let task = self
+ .connection
+ .request_any(acp_old::CancelSendMessageParams.into_any());
+ cx.foreground_executor()
+ .spawn(async move {
+ task.await?;
+ anyhow::Ok(())
+ })
+ .detach_and_log_err(cx)
+ }
+
+ fn into_any(self: Rc) -> Rc {
+ self
+ }
+}
diff --git a/crates/agent_servers/src/acp/v1.rs b/crates/agent_servers/src/acp/v1.rs
new file mode 100644
index 0000000000..1945ad2483
--- /dev/null
+++ b/crates/agent_servers/src/acp/v1.rs
@@ -0,0 +1,376 @@
+use acp_tools::AcpConnectionRegistry;
+use action_log::ActionLog;
+use agent_client_protocol::{self as acp, Agent as _, ErrorCode};
+use anyhow::anyhow;
+use collections::HashMap;
+use futures::AsyncBufReadExt as _;
+use futures::channel::oneshot;
+use futures::io::BufReader;
+use project::Project;
+use serde::Deserialize;
+use std::path::Path;
+use std::rc::Rc;
+use std::{any::Any, cell::RefCell};
+
+use anyhow::{Context as _, Result};
+use gpui::{App, AppContext as _, AsyncApp, Entity, Task, WeakEntity};
+
+use crate::{AgentServerCommand, acp::UnsupportedVersion};
+use acp_thread::{AcpThread, AgentConnection, AuthRequired, LoadError};
+
+pub struct AcpConnection {
+ server_name: &'static str,
+ connection: Rc,
+ sessions: Rc>>,
+ auth_methods: Vec,
+ prompt_capabilities: acp::PromptCapabilities,
+ _io_task: Task>,
+}
+
+pub struct AcpSession {
+ thread: WeakEntity,
+ suppress_abort_err: bool,
+}
+
+const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::V1;
+
+impl AcpConnection {
+ pub async fn stdio(
+ server_name: &'static str,
+ command: AgentServerCommand,
+ root_dir: &Path,
+ cx: &mut AsyncApp,
+ ) -> Result {
+ let mut child = util::command::new_smol_command(&command.path)
+ .args(command.args.iter().map(|arg| arg.as_str()))
+ .envs(command.env.iter().flatten())
+ .current_dir(root_dir)
+ .stdin(std::process::Stdio::piped())
+ .stdout(std::process::Stdio::piped())
+ .stderr(std::process::Stdio::piped())
+ .kill_on_drop(true)
+ .spawn()?;
+
+ let stdout = child.stdout.take().context("Failed to take stdout")?;
+ let stdin = child.stdin.take().context("Failed to take stdin")?;
+ let stderr = child.stderr.take().context("Failed to take stderr")?;
+ log::trace!("Spawned (pid: {})", child.id());
+
+ let sessions = Rc::new(RefCell::new(HashMap::default()));
+
+ let client = ClientDelegate {
+ sessions: sessions.clone(),
+ cx: cx.clone(),
+ };
+ let (connection, io_task) = acp::ClientSideConnection::new(client, stdin, stdout, {
+ let foreground_executor = cx.foreground_executor().clone();
+ move |fut| {
+ foreground_executor.spawn(fut).detach();
+ }
+ });
+
+ let io_task = cx.background_spawn(io_task);
+
+ cx.background_spawn(async move {
+ let mut stderr = BufReader::new(stderr);
+ let mut line = String::new();
+ while let Ok(n) = stderr.read_line(&mut line).await
+ && n > 0
+ {
+ log::warn!("agent stderr: {}", &line);
+ line.clear();
+ }
+ })
+ .detach();
+
+ cx.spawn({
+ let sessions = sessions.clone();
+ async move |cx| {
+ let status = child.status().await?;
+
+ for session in sessions.borrow().values() {
+ session
+ .thread
+ .update(cx, |thread, cx| {
+ thread.emit_load_error(LoadError::Exited { status }, cx)
+ })
+ .ok();
+ }
+
+ anyhow::Ok(())
+ }
+ })
+ .detach();
+
+ let connection = Rc::new(connection);
+
+ cx.update(|cx| {
+ AcpConnectionRegistry::default_global(cx).update(cx, |registry, cx| {
+ registry.set_active_connection(server_name, &connection, cx)
+ });
+ })?;
+
+ let response = connection
+ .initialize(acp::InitializeRequest {
+ protocol_version: acp::VERSION,
+ client_capabilities: acp::ClientCapabilities {
+ fs: acp::FileSystemCapability {
+ read_text_file: true,
+ write_text_file: true,
+ },
+ },
+ })
+ .await?;
+
+ if response.protocol_version < MINIMUM_SUPPORTED_VERSION {
+ return Err(UnsupportedVersion.into());
+ }
+
+ Ok(Self {
+ auth_methods: response.auth_methods,
+ connection,
+ server_name,
+ sessions,
+ prompt_capabilities: response.agent_capabilities.prompt_capabilities,
+ _io_task: io_task,
+ })
+ }
+}
+
+impl AgentConnection for AcpConnection {
+ fn new_thread(
+ self: Rc,
+ project: Entity,
+ cwd: &Path,
+ cx: &mut App,
+ ) -> Task>> {
+ let conn = self.connection.clone();
+ let sessions = self.sessions.clone();
+ let cwd = cwd.to_path_buf();
+ cx.spawn(async move |cx| {
+ let response = conn
+ .new_session(acp::NewSessionRequest {
+ mcp_servers: vec![],
+ cwd,
+ })
+ .await
+ .map_err(|err| {
+ if err.code == acp::ErrorCode::AUTH_REQUIRED.code {
+ let mut error = AuthRequired::new();
+
+ if err.message != acp::ErrorCode::AUTH_REQUIRED.message {
+ error = error.with_description(err.message);
+ }
+
+ anyhow!(error)
+ } else {
+ anyhow!(err)
+ }
+ })?;
+
+ let session_id = response.session_id;
+ let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
+ let thread = cx.new(|_cx| {
+ AcpThread::new(
+ self.server_name,
+ self.clone(),
+ project,
+ action_log,
+ session_id.clone(),
+ )
+ })?;
+
+ let session = AcpSession {
+ thread: thread.downgrade(),
+ suppress_abort_err: false,
+ };
+ sessions.borrow_mut().insert(session_id, session);
+
+ Ok(thread)
+ })
+ }
+
+ fn auth_methods(&self) -> &[acp::AuthMethod] {
+ &self.auth_methods
+ }
+
+ fn authenticate(&self, method_id: acp::AuthMethodId, cx: &mut App) -> Task> {
+ let conn = self.connection.clone();
+ cx.foreground_executor().spawn(async move {
+ let result = conn
+ .authenticate(acp::AuthenticateRequest {
+ method_id: method_id.clone(),
+ })
+ .await?;
+
+ Ok(result)
+ })
+ }
+
+ fn prompt(
+ &self,
+ _id: Option,
+ params: acp::PromptRequest,
+ cx: &mut App,
+ ) -> Task> {
+ let conn = self.connection.clone();
+ let sessions = self.sessions.clone();
+ let session_id = params.session_id.clone();
+ cx.foreground_executor().spawn(async move {
+ let result = conn.prompt(params).await;
+
+ let mut suppress_abort_err = false;
+
+ if let Some(session) = sessions.borrow_mut().get_mut(&session_id) {
+ suppress_abort_err = session.suppress_abort_err;
+ session.suppress_abort_err = false;
+ }
+
+ match result {
+ Ok(response) => Ok(response),
+ Err(err) => {
+ if err.code != ErrorCode::INTERNAL_ERROR.code {
+ anyhow::bail!(err)
+ }
+
+ let Some(data) = &err.data else {
+ anyhow::bail!(err)
+ };
+
+ // Temporary workaround until the following PR is generally available:
+ // https://github.com/google-gemini/gemini-cli/pull/6656
+
+ #[derive(Deserialize)]
+ #[serde(deny_unknown_fields)]
+ struct ErrorDetails {
+ details: Box,
+ }
+
+ match serde_json::from_value(data.clone()) {
+ Ok(ErrorDetails { details }) => {
+ if suppress_abort_err && details.contains("This operation was aborted")
+ {
+ Ok(acp::PromptResponse {
+ stop_reason: acp::StopReason::Cancelled,
+ })
+ } else {
+ Err(anyhow!(details))
+ }
+ }
+ Err(_) => Err(anyhow!(err)),
+ }
+ }
+ }
+ })
+ }
+
+ fn prompt_capabilities(&self) -> acp::PromptCapabilities {
+ self.prompt_capabilities
+ }
+
+ fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
+ if let Some(session) = self.sessions.borrow_mut().get_mut(session_id) {
+ session.suppress_abort_err = true;
+ }
+ let conn = self.connection.clone();
+ let params = acp::CancelNotification {
+ session_id: session_id.clone(),
+ };
+ cx.foreground_executor()
+ .spawn(async move { conn.cancel(params).await })
+ .detach();
+ }
+
+ fn into_any(self: Rc) -> Rc {
+ self
+ }
+}
+
+struct ClientDelegate {
+ sessions: Rc>>,
+ cx: AsyncApp,
+}
+
+impl acp::Client for ClientDelegate {
+ async fn request_permission(
+ &self,
+ arguments: acp::RequestPermissionRequest,
+ ) -> Result {
+ let cx = &mut self.cx.clone();
+ let rx = self
+ .sessions
+ .borrow()
+ .get(&arguments.session_id)
+ .context("Failed to get session")?
+ .thread
+ .update(cx, |thread, cx| {
+ thread.request_tool_call_authorization(arguments.tool_call, arguments.options, cx)
+ })?;
+
+ let result = rx?.await;
+
+ let outcome = match result {
+ Ok(option) => acp::RequestPermissionOutcome::Selected { option_id: option },
+ Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Cancelled,
+ };
+
+ Ok(acp::RequestPermissionResponse { outcome })
+ }
+
+ async fn write_text_file(
+ &self,
+ arguments: acp::WriteTextFileRequest,
+ ) -> Result<(), acp::Error> {
+ let cx = &mut self.cx.clone();
+ let task = self
+ .sessions
+ .borrow()
+ .get(&arguments.session_id)
+ .context("Failed to get session")?
+ .thread
+ .update(cx, |thread, cx| {
+ thread.write_text_file(arguments.path, arguments.content, cx)
+ })?;
+
+ task.await?;
+
+ Ok(())
+ }
+
+ async fn read_text_file(
+ &self,
+ arguments: acp::ReadTextFileRequest,
+ ) -> Result {
+ let cx = &mut self.cx.clone();
+ let task = self
+ .sessions
+ .borrow()
+ .get(&arguments.session_id)
+ .context("Failed to get session")?
+ .thread
+ .update(cx, |thread, cx| {
+ thread.read_text_file(arguments.path, arguments.line, arguments.limit, false, cx)
+ })?;
+
+ let content = task.await?;
+
+ Ok(acp::ReadTextFileResponse { content })
+ }
+
+ async fn session_notification(
+ &self,
+ notification: acp::SessionNotification,
+ ) -> Result<(), acp::Error> {
+ let cx = &mut self.cx.clone();
+ let sessions = self.sessions.borrow();
+ let session = sessions
+ .get(¬ification.session_id)
+ .context("Failed to get session")?;
+
+ session.thread.update(cx, |thread, cx| {
+ thread.handle_session_update(notification.update, cx)
+ })??;
+
+ Ok(())
+ }
+}
diff --git a/crates/agent_ui/src/acp/entry_view_state.rs b/crates/agent_ui/src/acp/entry_view_state.rs
index becf6953fd..0e4080d689 100644
--- a/crates/agent_ui/src/acp/entry_view_state.rs
+++ b/crates/agent_ui/src/acp/entry_view_state.rs
@@ -6,7 +6,7 @@ use agent2::HistoryStore;
use collections::HashMap;
use editor::{Editor, EditorMode, MinimapVisibility};
use gpui::{
- AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, Focusable, ScrollHandle,
+ AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, Focusable,
TextStyleRefinement, WeakEntity, Window,
};
use language::language_settings::SoftWrap;
@@ -154,22 +154,10 @@ impl EntryViewState {
});
}
}
- AgentThreadEntry::AssistantMessage(message) => {
- let entry = if let Some(Entry::AssistantMessage(entry)) =
- self.entries.get_mut(index)
- {
- entry
- } else {
- self.set_entry(
- index,
- Entry::AssistantMessage(AssistantMessageEntry::default()),
- );
- let Some(Entry::AssistantMessage(entry)) = self.entries.get_mut(index) else {
- unreachable!()
- };
- entry
- };
- entry.sync(message);
+ AgentThreadEntry::AssistantMessage(_) => {
+ if index == self.entries.len() {
+ self.entries.push(Entry::empty())
+ }
}
};
}
@@ -189,7 +177,7 @@ impl EntryViewState {
pub fn settings_changed(&mut self, cx: &mut App) {
for entry in self.entries.iter() {
match entry {
- Entry::UserMessage { .. } | Entry::AssistantMessage { .. } => {}
+ Entry::UserMessage { .. } => {}
Entry::Content(response_views) => {
for view in response_views.values() {
if let Ok(diff_editor) = view.clone().downcast::() {
@@ -220,29 +208,9 @@ pub enum ViewEvent {
MessageEditorEvent(Entity, MessageEditorEvent),
}
-#[derive(Default, Debug)]
-pub struct AssistantMessageEntry {
- scroll_handles_by_chunk_index: HashMap,
-}
-
-impl AssistantMessageEntry {
- pub fn scroll_handle_for_chunk(&self, ix: usize) -> Option {
- self.scroll_handles_by_chunk_index.get(&ix).cloned()
- }
-
- pub fn sync(&mut self, message: &acp_thread::AssistantMessage) {
- if let Some(acp_thread::AssistantMessageChunk::Thought { .. }) = message.chunks.last() {
- let ix = message.chunks.len() - 1;
- let handle = self.scroll_handles_by_chunk_index.entry(ix).or_default();
- handle.scroll_to_bottom();
- }
- }
-}
-
#[derive(Debug)]
pub enum Entry {
UserMessage(Entity),
- AssistantMessage(AssistantMessageEntry),
Content(HashMap),
}
@@ -250,7 +218,7 @@ impl Entry {
pub fn message_editor(&self) -> Option<&Entity> {
match self {
Self::UserMessage(editor) => Some(editor),
- Self::AssistantMessage(_) | Self::Content(_) => None,
+ Entry::Content(_) => None,
}
}
@@ -271,16 +239,6 @@ impl Entry {
.map(|entity| entity.downcast::().unwrap())
}
- pub fn scroll_handle_for_assistant_message_chunk(
- &self,
- chunk_ix: usize,
- ) -> Option {
- match self {
- Self::AssistantMessage(message) => message.scroll_handle_for_chunk(chunk_ix),
- Self::UserMessage(_) | Self::Content(_) => None,
- }
- }
-
fn content_map(&self) -> Option<&HashMap> {
match self {
Self::Content(map) => Some(map),
@@ -296,7 +254,7 @@ impl Entry {
pub fn has_content(&self) -> bool {
match self {
Self::Content(map) => !map.is_empty(),
- Self::UserMessage(_) | Self::AssistantMessage(_) => false,
+ Self::UserMessage(_) => false,
}
}
}
diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs
index c68c3a3e93..837ce6f90a 100644
--- a/crates/agent_ui/src/acp/thread_view.rs
+++ b/crates/agent_ui/src/acp/thread_view.rs
@@ -20,11 +20,11 @@ use file_icons::FileIcons;
use fs::Fs;
use gpui::{
Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem,
- CursorStyle, EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length,
- ListOffset, ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement,
- Subscription, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity,
- Window, WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, percentage,
- point, prelude::*, pulsating_between,
+ EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length, ListOffset,
+ ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement, Subscription,
+ Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window,
+ WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, percentage, point,
+ prelude::*, pulsating_between,
};
use language::Buffer;
@@ -43,7 +43,7 @@ use text::Anchor;
use theme::ThemeSettings;
use ui::{
Callout, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding, PopoverMenuHandle,
- Scrollbar, ScrollbarState, SpinnerLabel, TintColor, Tooltip, prelude::*,
+ Scrollbar, ScrollbarState, SpinnerLabel, Tooltip, prelude::*,
};
use util::{ResultExt, size::format_file_size, time::duration_alt_display};
use workspace::{CollaboratorId, Workspace};
@@ -66,6 +66,7 @@ use crate::{
KeepAll, OpenAgentDiff, OpenHistory, RejectAll, ToggleBurnMode, ToggleProfileSelector,
};
+const RESPONSE_PADDING_X: Pixels = px(19.);
pub const MIN_EDITOR_LINES: usize = 4;
pub const MAX_EDITOR_LINES: usize = 8;
@@ -278,7 +279,6 @@ pub struct AcpThreadView {
editing_message: Option,
prompt_capabilities: Rc>,
is_loading_contents: bool,
- install_command_markdown: Entity,
_cancel_task: Option>,
_subscriptions: [Subscription; 3],
}
@@ -392,7 +392,6 @@ impl AcpThreadView {
hovered_recent_history_item: None,
prompt_capabilities,
is_loading_contents: false,
- install_command_markdown: cx.new(|cx| Markdown::new("".into(), None, None, cx)),
_subscriptions: subscriptions,
_cancel_task: None,
focus_handle: cx.focus_handle(),
@@ -668,12 +667,7 @@ impl AcpThreadView {
match &self.thread_state {
ThreadState::Ready { .. } | ThreadState::Unauthenticated { .. } => "New Thread".into(),
ThreadState::Loading { .. } => "Loading…".into(),
- ThreadState::LoadError(error) => match error {
- LoadError::NotInstalled { .. } => format!("Install {}", self.agent.name()).into(),
- LoadError::Unsupported { .. } => format!("Upgrade {}", self.agent.name()).into(),
- LoadError::Exited { .. } => format!("{} Exited", self.agent.name()).into(),
- LoadError::Other(_) => format!("Error Loading {}", self.agent.name()).into(),
- },
+ ThreadState::LoadError(_) => "Failed to load".into(),
}
}
@@ -1340,10 +1334,6 @@ impl AcpThreadView {
window: &mut Window,
cx: &Context,
) -> AnyElement {
- let is_generating = self
- .thread()
- .is_some_and(|thread| thread.read(cx).status() != ThreadStatus::Idle);
-
let primary = match &entry {
AgentThreadEntry::UserMessage(message) => {
let Some(editor) = self
@@ -1503,20 +1493,6 @@ impl AcpThreadView {
.into_any()
}
AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => {
- let is_last = entry_ix + 1 == total_entries;
- let pending_thinking_chunk_ix = if is_generating && is_last {
- chunks
- .iter()
- .enumerate()
- .next_back()
- .filter(|(_, segment)| {
- matches!(segment, AssistantMessageChunk::Thought { .. })
- })
- .map(|(index, _)| index)
- } else {
- None
- };
-
let style = default_markdown_style(false, false, window, cx);
let message_body = v_flex()
.w_full()
@@ -1535,7 +1511,6 @@ impl AcpThreadView {
entry_ix,
chunk_ix,
md.clone(),
- Some(chunk_ix) == pending_thinking_chunk_ix,
window,
cx,
)
@@ -1549,7 +1524,7 @@ impl AcpThreadView {
v_flex()
.px_5()
.py_1()
- .when(is_last, |this| this.pb_4())
+ .when(entry_ix + 1 == total_entries, |this| this.pb_4())
.w_full()
.text_ui(cx)
.child(message_body)
@@ -1558,7 +1533,7 @@ impl AcpThreadView {
AgentThreadEntry::ToolCall(tool_call) => {
let has_terminals = tool_call.terminals().next().is_some();
- div().w_full().map(|this| {
+ div().w_full().py_1().px_5().map(|this| {
if has_terminals {
this.children(tool_call.terminals().map(|terminal| {
self.render_terminal_tool_call(
@@ -1634,90 +1609,64 @@ impl AcpThreadView {
entry_ix: usize,
chunk_ix: usize,
chunk: Entity,
- pending: bool,
window: &Window,
cx: &Context,
) -> AnyElement {
let header_id = SharedString::from(format!("thinking-block-header-{}", entry_ix));
let card_header_id = SharedString::from("inner-card-header");
-
let key = (entry_ix, chunk_ix);
-
let is_open = self.expanded_thinking_blocks.contains(&key);
- let editor_bg = cx.theme().colors().editor_background;
- let gradient_overlay = div()
- .rounded_b_lg()
- .h_full()
- .absolute()
- .w_full()
- .bottom_0()
- .left_0()
- .bg(linear_gradient(
- 180.,
- linear_color_stop(editor_bg, 1.),
- linear_color_stop(editor_bg.opacity(0.2), 0.),
- ));
-
- let scroll_handle = self
- .entry_view_state
- .read(cx)
- .entry(entry_ix)
- .and_then(|entry| entry.scroll_handle_for_assistant_message_chunk(chunk_ix));
v_flex()
- .rounded_md()
- .border_1()
- .border_color(self.tool_card_border_color(cx))
.child(
h_flex()
.id(header_id)
.group(&card_header_id)
.relative()
.w_full()
- .py_0p5()
- .px_1p5()
- .rounded_t_md()
- .bg(self.tool_card_header_bg(cx))
- .justify_between()
- .border_b_1()
- .border_color(self.tool_card_border_color(cx))
+ .gap_1p5()
.child(
h_flex()
- .h(window.line_height())
- .gap_1p5()
- .child(
- Icon::new(IconName::ToolThink)
- .size(IconSize::Small)
- .color(Color::Muted),
- )
+ .size_4()
+ .justify_center()
.child(
div()
- .text_size(self.tool_name_font_size())
- .text_color(cx.theme().colors().text_muted)
- .map(|this| {
- if pending {
- this.child("Thinking")
- } else {
- this.child("Thought Process")
- }
- }),
+ .group_hover(&card_header_id, |s| s.invisible().w_0())
+ .child(
+ Icon::new(IconName::ToolThink)
+ .size(IconSize::Small)
+ .color(Color::Muted),
+ ),
+ )
+ .child(
+ h_flex()
+ .absolute()
+ .inset_0()
+ .invisible()
+ .justify_center()
+ .group_hover(&card_header_id, |s| s.visible())
+ .child(
+ Disclosure::new(("expand", entry_ix), is_open)
+ .opened_icon(IconName::ChevronUp)
+ .closed_icon(IconName::ChevronRight)
+ .on_click(cx.listener({
+ move |this, _event, _window, cx| {
+ if is_open {
+ this.expanded_thinking_blocks.remove(&key);
+ } else {
+ this.expanded_thinking_blocks.insert(key);
+ }
+ cx.notify();
+ }
+ })),
+ ),
),
)
.child(
- Disclosure::new(("expand", entry_ix), is_open)
- .opened_icon(IconName::ChevronUp)
- .closed_icon(IconName::ChevronDown)
- .visible_on_hover(&card_header_id)
- .on_click(cx.listener({
- move |this, _event, _window, cx| {
- if is_open {
- this.expanded_thinking_blocks.remove(&key);
- } else {
- this.expanded_thinking_blocks.insert(key);
- }
- cx.notify();
- }
- })),
+ div()
+ .text_size(self.tool_name_font_size())
+ .text_color(cx.theme().colors().text_muted)
+ .child("Thinking"),
)
.on_click(cx.listener({
move |this, _event, _window, cx| {
@@ -1730,28 +1679,22 @@ impl AcpThreadView {
}
})),
)
- .child(
- div()
- .relative()
- .bg(editor_bg)
- .rounded_b_lg()
- .child(
- div()
- .id(("thinking-content", chunk_ix))
- .when_some(scroll_handle, |this, scroll_handle| {
- this.track_scroll(&scroll_handle)
- })
- .p_2()
- .when(!is_open, |this| this.max_h_20())
- .text_ui_sm(cx)
- .overflow_hidden()
- .child(self.render_markdown(
- chunk,
- default_markdown_style(false, false, window, cx),
- )),
- )
- .when(!is_open && pending, |this| this.child(gradient_overlay)),
- )
+ .when(is_open, |this| {
+ this.child(
+ div()
+ .relative()
+ .mt_1p5()
+ .ml(px(7.))
+ .pl_4()
+ .border_l_1()
+ .border_color(self.tool_card_border_color(cx))
+ .text_ui_sm(cx)
+ .child(self.render_markdown(
+ chunk,
+ default_markdown_style(false, false, window, cx),
+ )),
+ )
+ })
.into_any_element()
}
@@ -1762,6 +1705,7 @@ impl AcpThreadView {
window: &Window,
cx: &Context,
) -> Div {
+ let header_id = SharedString::from(format!("outer-tool-call-header-{}", entry_ix));
let card_header_id = SharedString::from("inner-tool-call-header");
let tool_icon =
@@ -1790,7 +1734,11 @@ impl AcpThreadView {
_ => false,
};
- let has_location = tool_call.locations.len() == 1;
+ let failed_tool_call = matches!(
+ tool_call.status,
+ ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed
+ );
+
let needs_confirmation = matches!(
tool_call.status,
ToolCallStatus::WaitingForConfirmation { .. }
@@ -1803,31 +1751,23 @@ impl AcpThreadView {
let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id);
- let gradient_overlay = {
+ let gradient_overlay = |color: Hsla| {
div()
.absolute()
.top_0()
.right_0()
.w_12()
.h_full()
- .map(|this| {
- if use_card_layout {
- this.bg(linear_gradient(
- 90.,
- linear_color_stop(self.tool_card_header_bg(cx), 1.),
- linear_color_stop(self.tool_card_header_bg(cx).opacity(0.2), 0.),
- ))
- } else {
- this.bg(linear_gradient(
- 90.,
- linear_color_stop(cx.theme().colors().panel_background, 1.),
- linear_color_stop(
- cx.theme().colors().panel_background.opacity(0.2),
- 0.,
- ),
- ))
- }
- })
+ .bg(linear_gradient(
+ 90.,
+ linear_color_stop(color, 1.),
+ linear_color_stop(color.opacity(0.2), 0.),
+ ))
+ };
+ let gradient_color = if use_card_layout {
+ self.tool_card_header_bg(cx)
+ } else {
+ cx.theme().colors().panel_background
};
let tool_output_display = if is_open {
@@ -1878,58 +1818,40 @@ impl AcpThreadView {
};
v_flex()
- .map(|this| {
- if use_card_layout {
- this.my_2()
- .rounded_md()
- .border_1()
- .border_color(self.tool_card_border_color(cx))
- .bg(cx.theme().colors().editor_background)
- .overflow_hidden()
- } else {
- this.my_1()
- }
+ .when(use_card_layout, |this| {
+ this.rounded_md()
+ .border_1()
+ .border_color(self.tool_card_border_color(cx))
+ .bg(cx.theme().colors().editor_background)
+ .overflow_hidden()
})
- .map(|this| {
- if has_location && !use_card_layout {
- this.ml_4()
- } else {
- this.ml_5()
- }
- })
- .mr_5()
.child(
h_flex()
+ .id(header_id)
.group(&card_header_id)
.relative()
.w_full()
+ .max_w_full()
.gap_1()
- .justify_between()
.when(use_card_layout, |this| {
- this.p_0p5()
+ this.pl_1p5()
+ .pr_1()
+ .py_0p5()
.rounded_t_md()
- .bg(self.tool_card_header_bg(cx))
- .when(is_open && !failed_or_canceled, |this| {
+ .when(is_open && !failed_tool_call, |this| {
this.border_b_1()
.border_color(self.tool_card_border_color(cx))
})
+ .bg(self.tool_card_header_bg(cx))
})
.child(
h_flex()
.relative()
.w_full()
- .h(window.line_height())
+ .h(window.line_height() - px(2.))
.text_size(self.tool_name_font_size())
- .gap_1p5()
- .when(has_location || use_card_layout, |this| this.px_1())
- .when(has_location, |this| {
- this.cursor(CursorStyle::PointingHand)
- .rounded_sm()
- .hover(|s| s.bg(cx.theme().colors().element_hover.opacity(0.5)))
- })
- .overflow_hidden()
.child(tool_icon)
- .child(if has_location {
+ .child(if tool_call.locations.len() == 1 {
let name = tool_call.locations[0]
.path
.file_name()
@@ -1940,6 +1862,13 @@ impl AcpThreadView {
h_flex()
.id(("open-tool-call-location", entry_ix))
.w_full()
+ .max_w_full()
+ .px_1p5()
+ .rounded_sm()
+ .overflow_x_scroll()
+ .hover(|label| {
+ label.bg(cx.theme().colors().element_hover.opacity(0.5))
+ })
.map(|this| {
if use_card_layout {
this.text_color(cx.theme().colors().text)
@@ -1949,28 +1878,31 @@ impl AcpThreadView {
})
.child(name)
.tooltip(Tooltip::text("Jump to File"))
+ .cursor(gpui::CursorStyle::PointingHand)
.on_click(cx.listener(move |this, _, window, cx| {
this.open_tool_call_location(entry_ix, 0, window, cx);
}))
.into_any_element()
} else {
h_flex()
+ .relative()
.w_full()
- .child(self.render_markdown(
+ .max_w_full()
+ .ml_1p5()
+ .overflow_hidden()
+ .child(h_flex().pr_8().child(self.render_markdown(
tool_call.label.clone(),
default_markdown_style(false, true, window, cx),
- ))
+ )))
+ .child(gradient_overlay(gradient_color))
.into_any()
- })
- .when(!has_location, |this| this.child(gradient_overlay)),
+ }),
)
- .when(is_collapsible || failed_or_canceled, |this| {
- this.child(
- h_flex()
- .px_1()
- .gap_px()
- .when(is_collapsible, |this| {
- this.child(
+ .child(
+ h_flex()
+ .gap_px()
+ .when(is_collapsible, |this| {
+ this.child(
Disclosure::new(("expand", entry_ix), is_open)
.opened_icon(IconName::ChevronUp)
.closed_icon(IconName::ChevronDown)
@@ -1987,16 +1919,15 @@ impl AcpThreadView {
}
})),
)
- })
- .when(failed_or_canceled, |this| {
- this.child(
- Icon::new(IconName::Close)
- .color(Color::Error)
- .size(IconSize::Small),
- )
- }),
- )
- }),
+ })
+ .when(failed_or_canceled, |this| {
+ this.child(
+ Icon::new(IconName::Close)
+ .color(Color::Error)
+ .size(IconSize::Small),
+ )
+ }),
+ ),
)
.children(tool_output_display)
}
@@ -2037,7 +1968,7 @@ impl AcpThreadView {
v_flex()
.mt_1p5()
- .ml(rems(0.4))
+ .ml(px(7.))
.px_3p5()
.gap_2()
.border_l_1()
@@ -2094,7 +2025,7 @@ impl AcpThreadView {
let button_id = SharedString::from(format!("item-{}", uri));
div()
- .ml(rems(0.4))
+ .ml(px(7.))
.pl_2p5()
.border_l_1()
.border_color(self.tool_card_border_color(cx))
@@ -2282,12 +2213,6 @@ impl AcpThreadView {
started_at.elapsed()
};
- let header_id =
- SharedString::from(format!("terminal-tool-header-{}", terminal.entity_id()));
- let header_group = SharedString::from(format!(
- "terminal-tool-header-group-{}",
- terminal.entity_id()
- ));
let header_bg = cx
.theme()
.colors()
@@ -2303,7 +2228,10 @@ impl AcpThreadView {
let is_expanded = self.expanded_tool_calls.contains(&tool_call.id);
let header = h_flex()
- .id(header_id)
+ .id(SharedString::from(format!(
+ "terminal-tool-header-{}",
+ terminal.entity_id()
+ )))
.flex_none()
.gap_1()
.justify_between()
@@ -2367,6 +2295,23 @@ impl AcpThreadView {
),
)
})
+ .when(tool_failed || command_failed, |header| {
+ header.child(
+ div()
+ .id(("terminal-tool-error-code-indicator", terminal.entity_id()))
+ .child(
+ Icon::new(IconName::Close)
+ .size(IconSize::Small)
+ .color(Color::Error),
+ )
+ .when_some(output.and_then(|o| o.exit_status), |this, status| {
+ this.tooltip(Tooltip::text(format!(
+ "Exited with code {}",
+ status.code().unwrap_or(-1),
+ )))
+ }),
+ )
+ })
.when(truncated_output, |header| {
let tooltip = if let Some(output) = output {
if output_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES {
@@ -2419,7 +2364,6 @@ impl AcpThreadView {
)
.opened_icon(IconName::ChevronUp)
.closed_icon(IconName::ChevronDown)
- .visible_on_hover(&header_group)
.on_click(cx.listener({
let id = tool_call.id.clone();
move |this, _event, _window, _cx| {
@@ -2428,26 +2372,8 @@ impl AcpThreadView {
} else {
this.expanded_tool_calls.insert(id.clone());
}
- }
- })),
- )
- .when(tool_failed || command_failed, |header| {
- header.child(
- div()
- .id(("terminal-tool-error-code-indicator", terminal.entity_id()))
- .child(
- Icon::new(IconName::Close)
- .size(IconSize::Small)
- .color(Color::Error),
- )
- .when_some(output.and_then(|o| o.exit_status), |this, status| {
- this.tooltip(Tooltip::text(format!(
- "Exited with code {}",
- status.code().unwrap_or(-1),
- )))
- }),
- )
- });
+ }})),
+ );
let terminal_view = self
.entry_view_state
@@ -2457,8 +2383,7 @@ impl AcpThreadView {
let show_output = is_expanded && terminal_view.is_some();
v_flex()
- .my_2()
- .mx_5()
+ .mb_2()
.border_1()
.when(tool_failed || command_failed, |card| card.border_dashed())
.border_color(border_color)
@@ -2466,10 +2391,9 @@ impl AcpThreadView {
.overflow_hidden()
.child(
v_flex()
- .group(&header_group)
.py_1p5()
- .pr_1p5()
.pl_2()
+ .pr_1p5()
.gap_0p5()
.bg(header_bg)
.text_xs()
@@ -2841,26 +2765,125 @@ impl AcpThreadView {
)
}
- fn render_load_error(
- &self,
- e: &LoadError,
- window: &mut Window,
- cx: &mut Context,
- ) -> AnyElement {
- let (message, action_slot): (SharedString, _) = match e {
+ fn render_load_error(&self, e: &LoadError, cx: &Context) -> AnyElement {
+ let (message, action_slot) = match e {
LoadError::NotInstalled {
- error_message: _,
- install_message: _,
+ error_message,
+ install_message,
install_command,
} => {
- return self.render_not_installed(install_command.clone(), false, window, cx);
+ let install_command = install_command.clone();
+ let button = Button::new("install", install_message)
+ .tooltip(Tooltip::text(install_command.clone()))
+ .style(ButtonStyle::Outlined)
+ .label_size(LabelSize::Small)
+ .icon(IconName::Download)
+ .icon_size(IconSize::Small)
+ .icon_color(Color::Muted)
+ .icon_position(IconPosition::Start)
+ .on_click(cx.listener(move |this, _, window, cx| {
+ telemetry::event!("Agent Install CLI", agent = this.agent.telemetry_id());
+
+ let task = this
+ .workspace
+ .update(cx, |workspace, cx| {
+ let project = workspace.project().read(cx);
+ let cwd = project.first_project_directory(cx);
+ let shell = project.terminal_settings(&cwd, cx).shell.clone();
+ let spawn_in_terminal = task::SpawnInTerminal {
+ id: task::TaskId(install_command.clone()),
+ full_label: install_command.clone(),
+ label: install_command.clone(),
+ command: Some(install_command.clone()),
+ args: Vec::new(),
+ command_label: install_command.clone(),
+ cwd,
+ env: Default::default(),
+ use_new_terminal: true,
+ allow_concurrent_runs: true,
+ reveal: Default::default(),
+ reveal_target: Default::default(),
+ hide: Default::default(),
+ shell,
+ show_summary: true,
+ show_command: true,
+ show_rerun: false,
+ };
+ workspace.spawn_in_terminal(spawn_in_terminal, window, cx)
+ })
+ .ok();
+ let Some(task) = task else { return };
+ cx.spawn_in(window, async move |this, cx| {
+ if let Some(Ok(_)) = task.await {
+ this.update_in(cx, |this, window, cx| {
+ this.reset(window, cx);
+ })
+ .ok();
+ }
+ })
+ .detach()
+ }));
+
+ (error_message.clone(), Some(button.into_any_element()))
}
LoadError::Unsupported {
- error_message: _,
- upgrade_message: _,
+ error_message,
+ upgrade_message,
upgrade_command,
} => {
- return self.render_not_installed(upgrade_command.clone(), true, window, cx);
+ let upgrade_command = upgrade_command.clone();
+ let button = Button::new("upgrade", upgrade_message)
+ .tooltip(Tooltip::text(upgrade_command.clone()))
+ .style(ButtonStyle::Outlined)
+ .label_size(LabelSize::Small)
+ .icon(IconName::Download)
+ .icon_size(IconSize::Small)
+ .icon_color(Color::Muted)
+ .icon_position(IconPosition::Start)
+ .on_click(cx.listener(move |this, _, window, cx| {
+ telemetry::event!("Agent Upgrade CLI", agent = this.agent.telemetry_id());
+
+ let task = this
+ .workspace
+ .update(cx, |workspace, cx| {
+ let project = workspace.project().read(cx);
+ let cwd = project.first_project_directory(cx);
+ let shell = project.terminal_settings(&cwd, cx).shell.clone();
+ let spawn_in_terminal = task::SpawnInTerminal {
+ id: task::TaskId(upgrade_command.to_string()),
+ full_label: upgrade_command.clone(),
+ label: upgrade_command.clone(),
+ command: Some(upgrade_command.clone()),
+ args: Vec::new(),
+ command_label: upgrade_command.clone(),
+ cwd,
+ env: Default::default(),
+ use_new_terminal: true,
+ allow_concurrent_runs: true,
+ reveal: Default::default(),
+ reveal_target: Default::default(),
+ hide: Default::default(),
+ shell,
+ show_summary: true,
+ show_command: true,
+ show_rerun: false,
+ };
+ workspace.spawn_in_terminal(spawn_in_terminal, window, cx)
+ })
+ .ok();
+ let Some(task) = task else { return };
+ cx.spawn_in(window, async move |this, cx| {
+ if let Some(Ok(_)) = task.await {
+ this.update_in(cx, |this, window, cx| {
+ this.reset(window, cx);
+ })
+ .ok();
+ }
+ })
+ .detach()
+ }));
+
+ (error_message.clone(), Some(button.into_any_element()))
}
LoadError::Exited { .. } => ("Server exited with status {status}".into(), None),
LoadError::Other(msg) => (
@@ -2878,121 +2901,6 @@ impl AcpThreadView {
.into_any_element()
}
- fn install_agent(&self, install_command: String, window: &mut Window, cx: &mut Context) {
- telemetry::event!("Agent Install CLI", agent = self.agent.telemetry_id());
- let task = self
- .workspace
- .update(cx, |workspace, cx| {
- let project = workspace.project().read(cx);
- let cwd = project.first_project_directory(cx);
- let shell = project.terminal_settings(&cwd, cx).shell.clone();
- let spawn_in_terminal = task::SpawnInTerminal {
- id: task::TaskId(install_command.clone()),
- full_label: install_command.clone(),
- label: install_command.clone(),
- command: Some(install_command.clone()),
- args: Vec::new(),
- command_label: install_command.clone(),
- cwd,
- env: Default::default(),
- use_new_terminal: true,
- allow_concurrent_runs: true,
- reveal: Default::default(),
- reveal_target: Default::default(),
- hide: Default::default(),
- shell,
- show_summary: true,
- show_command: true,
- show_rerun: false,
- };
- workspace.spawn_in_terminal(spawn_in_terminal, window, cx)
- })
- .ok();
- let Some(task) = task else { return };
- cx.spawn_in(window, async move |this, cx| {
- if let Some(Ok(_)) = task.await {
- this.update_in(cx, |this, window, cx| {
- this.reset(window, cx);
- })
- .ok();
- }
- })
- .detach()
- }
-
- fn render_not_installed(
- &self,
- install_command: String,
- is_upgrade: bool,
- window: &mut Window,
- cx: &mut Context,
- ) -> AnyElement {
- self.install_command_markdown.update(cx, |markdown, cx| {
- if !markdown.source().contains(&install_command) {
- markdown.replace(format!("```\n{}\n```", install_command), cx);
- }
- });
-
- let (heading_label, description_label, button_label, or_label) = if is_upgrade {
- (
- "Upgrade Gemini CLI in Zed",
- "Get access to the latest version with support for Zed.",
- "Upgrade Gemini CLI",
- "Or, to upgrade it manually:",
- )
- } else {
- (
- "Get Started with Gemini CLI in Zed",
- "Use Google's new coding agent directly in Zed.",
- "Install Gemini CLI",
- "Or, to install it manually:",
- )
- };
-
- v_flex()
- .w_full()
- .p_3p5()
- .gap_2p5()
- .border_t_1()
- .border_color(cx.theme().colors().border)
- .bg(linear_gradient(
- 180.,
- linear_color_stop(cx.theme().colors().editor_background.opacity(0.4), 4.),
- linear_color_stop(cx.theme().status().info_background.opacity(0.), 0.),
- ))
- .child(
- v_flex().gap_0p5().child(Label::new(heading_label)).child(
- Label::new(description_label)
- .size(LabelSize::Small)
- .color(Color::Muted),
- ),
- )
- .child(
- Button::new("install_gemini", button_label)
- .full_width()
- .size(ButtonSize::Medium)
- .style(ButtonStyle::Tinted(TintColor::Accent))
- .label_size(LabelSize::Small)
- .icon(IconName::TerminalGhost)
- .icon_color(Color::Muted)
- .icon_size(IconSize::Small)
- .icon_position(IconPosition::Start)
- .on_click(cx.listener(move |this, _, window, cx| {
- this.install_agent(install_command.clone(), window, cx)
- })),
- )
- .child(
- Label::new(or_label)
- .size(LabelSize::Small)
- .color(Color::Muted),
- )
- .child(MarkdownElement::new(
- self.install_command_markdown.clone(),
- default_markdown_style(false, false, window, cx),
- ))
- .into_any_element()
- }
-
fn render_activity_bar(
&self,
thread_entity: &Entity,
@@ -4244,14 +4152,13 @@ impl AcpThreadView {
) -> impl IntoElement {
let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating);
if is_generating {
- return h_flex().id("thread-controls-container").child(
+ return h_flex().id("thread-controls-container").ml_1().child(
div()
.py_2()
- .px_5()
+ .px(rems_from_px(22.))
.child(SpinnerLabel::new().size(LabelSize::Small)),
);
}
-
let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown)
.shape(ui::IconButtonShape::Square)
.icon_size(IconSize::Small)
@@ -4277,10 +4184,12 @@ impl AcpThreadView {
.id("thread-controls-container")
.group("thread-controls-container")
.w_full()
- .py_2()
- .px_5()
+ .mr_1()
+ .pt_1()
+ .pb_2()
+ .px(RESPONSE_PADDING_X)
.gap_px()
- .opacity(0.6)
+ .opacity(0.4)
.hover(|style| style.opacity(1.))
.flex_wrap()
.justify_end();
@@ -4291,50 +4200,56 @@ impl AcpThreadView {
.is_some_and(|thread| thread.read(cx).connection().telemetry().is_some())
{
let feedback = self.thread_feedback.feedback;
-
- container = container
- .child(
- div().visible_on_hover("thread-controls-container").child(
- Label::new(match feedback {
+ container = container.child(
+ div().visible_on_hover("thread-controls-container").child(
+ Label::new(
+ match feedback {
Some(ThreadFeedback::Positive) => "Thanks for your feedback!",
- Some(ThreadFeedback::Negative) => {
- "We appreciate your feedback and will use it to improve."
- }
- None => {
- "Rating the thread sends all of your current conversation to the Zed team."
- }
- })
- .color(Color::Muted)
- .size(LabelSize::XSmall)
- .truncate(),
- ),
- )
- .child(
- IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
- .shape(ui::IconButtonShape::Square)
- .icon_size(IconSize::Small)
- .icon_color(match feedback {
- Some(ThreadFeedback::Positive) => Color::Accent,
- _ => Color::Ignored,
- })
- .tooltip(Tooltip::text("Helpful Response"))
- .on_click(cx.listener(move |this, _, window, cx| {
- this.handle_feedback_click(ThreadFeedback::Positive, window, cx);
- })),
- )
- .child(
- IconButton::new("feedback-thumbs-down", IconName::ThumbsDown)
- .shape(ui::IconButtonShape::Square)
- .icon_size(IconSize::Small)
- .icon_color(match feedback {
- Some(ThreadFeedback::Negative) => Color::Accent,
- _ => Color::Ignored,
- })
- .tooltip(Tooltip::text("Not Helpful"))
- .on_click(cx.listener(move |this, _, window, cx| {
- this.handle_feedback_click(ThreadFeedback::Negative, window, cx);
- })),
- );
+ Some(ThreadFeedback::Negative) => "We appreciate your feedback and will use it to improve.",
+ None => "Rating the thread sends all of your current conversation to the Zed team.",
+ }
+ )
+ .color(Color::Muted)
+ .size(LabelSize::XSmall)
+ .truncate(),
+ ),
+ ).child(
+ h_flex()
+ .child(
+ IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
+ .shape(ui::IconButtonShape::Square)
+ .icon_size(IconSize::Small)
+ .icon_color(match feedback {
+ Some(ThreadFeedback::Positive) => Color::Accent,
+ _ => Color::Ignored,
+ })
+ .tooltip(Tooltip::text("Helpful Response"))
+ .on_click(cx.listener(move |this, _, window, cx| {
+ this.handle_feedback_click(
+ ThreadFeedback::Positive,
+ window,
+ cx,
+ );
+ })),
+ )
+ .child(
+ IconButton::new("feedback-thumbs-down", IconName::ThumbsDown)
+ .shape(ui::IconButtonShape::Square)
+ .icon_size(IconSize::Small)
+ .icon_color(match feedback {
+ Some(ThreadFeedback::Negative) => Color::Accent,
+ _ => Color::Ignored,
+ })
+ .tooltip(Tooltip::text("Not Helpful"))
+ .on_click(cx.listener(move |this, _, window, cx| {
+ this.handle_feedback_click(
+ ThreadFeedback::Negative,
+ window,
+ cx,
+ );
+ })),
+ )
+ )
}
container.child(open_as_markdown).child(scroll_to_top)
@@ -4966,7 +4881,7 @@ impl Render for AcpThreadView {
.size_full()
.items_center()
.justify_end()
- .child(self.render_load_error(e, window, cx)),
+ .child(self.render_load_error(e, cx)),
ThreadState::Ready { .. } => v_flex().flex_1().map(|this| {
if has_messages {
this.child(
diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs
index 224f49cc3e..52fb7eed4b 100644
--- a/crates/agent_ui/src/agent_configuration.rs
+++ b/crates/agent_ui/src/agent_configuration.rs
@@ -3,23 +3,20 @@ mod configure_context_server_modal;
mod manage_profiles_modal;
mod tool_picker;
-use std::{ops::Range, sync::Arc, time::Duration};
+use std::{sync::Arc, time::Duration};
-use agent_servers::{AgentServerCommand, AgentServerSettings, AllAgentServersSettings, Gemini};
+use agent_servers::{AgentServerCommand, AllAgentServersSettings, Gemini};
use agent_settings::AgentSettings;
-use anyhow::Result;
use assistant_tool::{ToolSource, ToolWorkingSet};
use cloud_llm_client::Plan;
use collections::HashMap;
use context_server::ContextServerId;
-use editor::{Editor, SelectionEffects, scroll::Autoscroll};
use extension::ExtensionManifest;
use extension_host::ExtensionStore;
use fs::Fs;
use gpui::{
- Action, Animation, AnimationExt as _, AnyView, App, AsyncWindowContext, Corner, Entity,
- EventEmitter, FocusHandle, Focusable, Hsla, ScrollHandle, Subscription, Task, Transformation,
- WeakEntity, percentage,
+ Action, Animation, AnimationExt as _, AnyView, App, Corner, Entity, EventEmitter, FocusHandle,
+ Focusable, Hsla, ScrollHandle, Subscription, Task, Transformation, WeakEntity, percentage,
};
use language::LanguageRegistry;
use language_model::{
@@ -37,7 +34,7 @@ use ui::{
Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip, prelude::*,
};
use util::ResultExt as _;
-use workspace::{Workspace, create_and_open_local_file};
+use workspace::Workspace;
use zed_actions::ExtensionCategoryFilter;
pub(crate) use configure_context_server_modal::ConfigureContextServerModal;
@@ -1061,39 +1058,10 @@ impl AgentConfiguration {
.child(
v_flex()
.gap_0p5()
- .child(
- h_flex()
- .w_full()
- .gap_2()
- .justify_between()
- .child(Headline::new("External Agents"))
- .child(
- Button::new("add-agent", "Add Agent")
- .icon_position(IconPosition::Start)
- .icon(IconName::Plus)
- .icon_size(IconSize::Small)
- .icon_color(Color::Muted)
- .label_size(LabelSize::Small)
- .on_click(
- move |_, window, cx| {
- if let Some(workspace) = window.root().flatten() {
- let workspace = workspace.downgrade();
- window
- .spawn(cx, async |cx| {
- open_new_agent_servers_entry_in_settings_editor(
- workspace,
- cx,
- ).await
- })
- .detach_and_log_err(cx);
- }
- }
- ),
- )
- )
+ .child(Headline::new("External Agents"))
.child(
Label::new(
- "Bring the agent of your choice to Zed via our new Agent Client Protocol.",
+ "Use the full power of Zed's UI with your favorite agent, connected via the Agent Client Protocol.",
)
.color(Color::Muted),
),
@@ -1356,109 +1324,3 @@ fn show_unable_to_uninstall_extension_with_context_server(
workspace.toggle_status_toast(status_toast, cx);
}
-
-async fn open_new_agent_servers_entry_in_settings_editor(
- workspace: WeakEntity,
- cx: &mut AsyncWindowContext,
-) -> Result<()> {
- let settings_editor = workspace
- .update_in(cx, |_, window, cx| {
- create_and_open_local_file(paths::settings_file(), window, cx, || {
- settings::initial_user_settings_content().as_ref().into()
- })
- })?
- .await?
- .downcast::()
- .unwrap();
-
- settings_editor
- .downgrade()
- .update_in(cx, |item, window, cx| {
- let text = item.buffer().read(cx).snapshot(cx).text();
-
- let settings = cx.global::();
-
- let mut unique_server_name = None;
- let edits = settings.edits_for_update::(&text, |file| {
- let server_name: Option = (0..u8::MAX)
- .map(|i| {
- if i == 0 {
- "your_agent".into()
- } else {
- format!("your_agent_{}", i).into()
- }
- })
- .find(|name| !file.custom.contains_key(name));
- if let Some(server_name) = server_name {
- unique_server_name = Some(server_name.clone());
- file.custom.insert(
- server_name,
- AgentServerSettings {
- command: AgentServerCommand {
- path: "path_to_executable".into(),
- args: vec![],
- env: Some(HashMap::default()),
- },
- },
- );
- }
- });
-
- if edits.is_empty() {
- return;
- }
-
- let ranges = edits
- .iter()
- .map(|(range, _)| range.clone())
- .collect::>();
-
- item.edit(edits, cx);
- if let Some((unique_server_name, buffer)) =
- unique_server_name.zip(item.buffer().read(cx).as_singleton())
- {
- let snapshot = buffer.read(cx).snapshot();
- if let Some(range) =
- find_text_in_buffer(&unique_server_name, ranges[0].start, &snapshot)
- {
- item.change_selections(
- SelectionEffects::scroll(Autoscroll::newest()),
- window,
- cx,
- |selections| {
- selections.select_ranges(vec![range]);
- },
- );
- }
- }
- })
-}
-
-fn find_text_in_buffer(
- text: &str,
- start: usize,
- snapshot: &language::BufferSnapshot,
-) -> Option> {
- let chars = text.chars().collect::>();
-
- let mut offset = start;
- let mut char_offset = 0;
- for c in snapshot.chars_at(start) {
- if char_offset >= chars.len() {
- break;
- }
- offset += 1;
-
- if c == chars[char_offset] {
- char_offset += 1;
- } else {
- char_offset = 0;
- }
- }
-
- if char_offset == chars.len() {
- Some(offset.saturating_sub(chars.len())..offset)
- } else {
- None
- }
-}
diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs
index d1cf748733..1eafb8dd4d 100644
--- a/crates/agent_ui/src/agent_panel.rs
+++ b/crates/agent_ui/src/agent_panel.rs
@@ -9,12 +9,10 @@ use agent_servers::AgentServerSettings;
use agent2::{DbThreadMetadata, HistoryEntry};
use db::kvp::{Dismissable, KEY_VALUE_STORE};
use serde::{Deserialize, Serialize};
-use zed_actions::OpenBrowser;
use zed_actions::agent::ReauthenticateAgent;
use crate::acp::{AcpThreadHistory, ThreadHistoryEvent};
use crate::agent_diff::AgentDiffThread;
-use crate::ui::AcpOnboardingModal;
use crate::{
AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode,
DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread,
@@ -78,10 +76,7 @@ use workspace::{
};
use zed_actions::{
DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize,
- agent::{
- OpenAcpOnboardingModal, OpenOnboardingModal, OpenSettings, ResetOnboarding,
- ToggleModelSelector,
- },
+ agent::{OpenOnboardingModal, OpenSettings, ResetOnboarding, ToggleModelSelector},
assistant::{OpenRulesLibrary, ToggleFocus},
};
@@ -205,9 +200,6 @@ pub fn init(cx: &mut App) {
.register_action(|workspace, _: &OpenOnboardingModal, window, cx| {
AgentOnboardingModal::toggle(workspace, window, cx)
})
- .register_action(|workspace, _: &OpenAcpOnboardingModal, window, cx| {
- AcpOnboardingModal::toggle(workspace, window, cx)
- })
.register_action(|_workspace, _: &ResetOnboarding, window, cx| {
window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx);
window.refresh();
@@ -598,6 +590,17 @@ impl AgentPanel {
None
};
+ // Wait for the Gemini/Native feature flag to be available.
+ let client = workspace.read_with(cx, |workspace, _| workspace.client().clone())?;
+ if !client.status().borrow().is_signed_out() {
+ cx.update(|_, cx| {
+ cx.wait_for_flag_or_timeout::(
+ Duration::from_secs(2),
+ )
+ })?
+ .await;
+ }
+
let panel = workspace.update_in(cx, |workspace, window, cx| {
let panel = cx.new(|cx| {
Self::new(
@@ -1848,6 +1851,19 @@ impl AgentPanel {
menu
}
+ pub fn set_selected_agent(
+ &mut self,
+ agent: AgentType,
+ window: &mut Window,
+ cx: &mut Context,
+ ) {
+ if self.selected_agent != agent {
+ self.selected_agent = agent.clone();
+ self.serialize(cx);
+ }
+ self.new_agent_thread(agent, window, cx);
+ }
+
pub fn selected_agent(&self) -> AgentType {
self.selected_agent.clone()
}
@@ -1858,11 +1874,6 @@ impl AgentPanel {
window: &mut Window,
cx: &mut Context,
) {
- if self.selected_agent != agent {
- self.selected_agent = agent.clone();
- self.serialize(cx);
- }
-
match agent {
AgentType::Zed => {
window.dispatch_action(
@@ -2543,7 +2554,7 @@ impl AgentPanel {
workspace.panel::(cx)
{
panel.update(cx, |panel, cx| {
- panel.new_agent_thread(
+ panel.set_selected_agent(
AgentType::NativeAgent,
window,
cx,
@@ -2569,7 +2580,7 @@ impl AgentPanel {
workspace.panel::(cx)
{
panel.update(cx, |panel, cx| {
- panel.new_agent_thread(
+ panel.set_selected_agent(
AgentType::TextThread,
window,
cx,
@@ -2597,7 +2608,7 @@ impl AgentPanel {
workspace.panel::(cx)
{
panel.update(cx, |panel, cx| {
- panel.new_agent_thread(
+ panel.set_selected_agent(
AgentType::Gemini,
window,
cx,
@@ -2624,7 +2635,7 @@ impl AgentPanel {
workspace.panel::(cx)
{
panel.update(cx, |panel, cx| {
- panel.new_agent_thread(
+ panel.set_selected_agent(
AgentType::ClaudeCode,
window,
cx,
@@ -2657,7 +2668,7 @@ impl AgentPanel {
workspace.panel::(cx)
{
panel.update(cx, |panel, cx| {
- panel.new_agent_thread(
+ panel.set_selected_agent(
AgentType::Custom {
name: agent_name
.clone(),
@@ -2678,15 +2689,6 @@ impl AgentPanel {
}
menu
- })
- .when(cx.has_flag::(), |menu| {
- menu.separator().link(
- "Add Other Agents",
- OpenBrowser {
- url: zed_urls::external_agents_docs(cx),
- }
- .boxed_clone(),
- )
});
menu
}))
diff --git a/crates/agent_ui/src/language_model_selector.rs b/crates/agent_ui/src/language_model_selector.rs
index 3633e533da..aceca79dbf 100644
--- a/crates/agent_ui/src/language_model_selector.rs
+++ b/crates/agent_ui/src/language_model_selector.rs
@@ -6,8 +6,7 @@ use feature_flags::ZedProFeatureFlag;
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
use gpui::{Action, AnyElement, App, BackgroundExecutor, DismissEvent, Subscription, Task};
use language_model::{
- AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelProviderId,
- LanguageModelRegistry,
+ ConfiguredModel, LanguageModel, LanguageModelProviderId, LanguageModelRegistry,
};
use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate};
@@ -77,7 +76,6 @@ pub struct LanguageModelPickerDelegate {
all_models: Arc,
filtered_entries: Vec,
selected_index: usize,
- _authenticate_all_providers_task: Task<()>,
_subscriptions: Vec,
}
@@ -98,7 +96,6 @@ impl LanguageModelPickerDelegate {
selected_index: Self::get_active_model_index(&entries, get_active_model(cx)),
filtered_entries: entries,
get_active_model: Arc::new(get_active_model),
- _authenticate_all_providers_task: Self::authenticate_all_providers(cx),
_subscriptions: vec![cx.subscribe_in(
&LanguageModelRegistry::global(cx),
window,
@@ -142,56 +139,6 @@ impl LanguageModelPickerDelegate {
.unwrap_or(0)
}
- /// Authenticates all providers in the [`LanguageModelRegistry`].
- ///
- /// We do this so that we can populate the language selector with all of the
- /// models from the configured providers.
- fn authenticate_all_providers(cx: &mut App) -> Task<()> {
- let authenticate_all_providers = LanguageModelRegistry::global(cx)
- .read(cx)
- .providers()
- .iter()
- .map(|provider| (provider.id(), provider.name(), provider.authenticate(cx)))
- .collect::>();
-
- cx.spawn(async move |_cx| {
- for (provider_id, provider_name, authenticate_task) in authenticate_all_providers {
- if let Err(err) = authenticate_task.await {
- if matches!(err, AuthenticateError::CredentialsNotFound) {
- // Since we're authenticating these providers in the
- // background for the purposes of populating the
- // language selector, we don't care about providers
- // where the credentials are not found.
- } else {
- // Some providers have noisy failure states that we
- // don't want to spam the logs with every time the
- // language model selector is initialized.
- //
- // Ideally these should have more clear failure modes
- // that we know are safe to ignore here, like what we do
- // with `CredentialsNotFound` above.
- match provider_id.0.as_ref() {
- "lmstudio" | "ollama" => {
- // LM Studio and Ollama both make fetch requests to the local APIs to determine if they are "authenticated".
- //
- // These fail noisily, so we don't log them.
- }
- "copilot_chat" => {
- // Copilot Chat returns an error if Copilot is not enabled, so we don't log those errors.
- }
- _ => {
- log::error!(
- "Failed to authenticate provider: {}: {err}",
- provider_name.0
- );
- }
- }
- }
- }
- }
- })
- }
-
pub fn active_model(&self, cx: &App) -> Option {
(self.get_active_model)(cx)
}
diff --git a/crates/agent_ui/src/ui.rs b/crates/agent_ui/src/ui.rs
index 600698b07e..ada973cddf 100644
--- a/crates/agent_ui/src/ui.rs
+++ b/crates/agent_ui/src/ui.rs
@@ -1,4 +1,3 @@
-mod acp_onboarding_modal;
mod agent_notification;
mod burn_mode_tooltip;
mod context_pill;
@@ -7,7 +6,6 @@ mod onboarding_modal;
pub mod preview;
mod unavailable_editing_tooltip;
-pub use acp_onboarding_modal::*;
pub use agent_notification::*;
pub use burn_mode_tooltip::*;
pub use context_pill::*;
diff --git a/crates/agent_ui/src/ui/acp_onboarding_modal.rs b/crates/agent_ui/src/ui/acp_onboarding_modal.rs
deleted file mode 100644
index 0ed9de7221..0000000000
--- a/crates/agent_ui/src/ui/acp_onboarding_modal.rs
+++ /dev/null
@@ -1,254 +0,0 @@
-use client::zed_urls;
-use gpui::{
- ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseDownEvent, Render,
- linear_color_stop, linear_gradient,
-};
-use ui::{TintColor, Vector, VectorName, prelude::*};
-use workspace::{ModalView, Workspace};
-
-use crate::agent_panel::{AgentPanel, AgentType};
-
-macro_rules! acp_onboarding_event {
- ($name:expr) => {
- telemetry::event!($name, source = "ACP Onboarding");
- };
- ($name:expr, $($key:ident $(= $value:expr)?),+ $(,)?) => {
- telemetry::event!($name, source = "ACP Onboarding", $($key $(= $value)?),+);
- };
-}
-
-pub struct AcpOnboardingModal {
- focus_handle: FocusHandle,
- workspace: Entity,
-}
-
-impl AcpOnboardingModal {
- pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context) {
- let workspace_entity = cx.entity();
- workspace.toggle_modal(window, cx, |_window, cx| Self {
- workspace: workspace_entity,
- focus_handle: cx.focus_handle(),
- });
- }
-
- fn open_panel(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context) {
- self.workspace.update(cx, |workspace, cx| {
- workspace.focus_panel::(window, cx);
-
- if let Some(panel) = workspace.panel::(cx) {
- panel.update(cx, |panel, cx| {
- panel.new_agent_thread(AgentType::Gemini, window, cx);
- });
- }
- });
-
- cx.emit(DismissEvent);
-
- acp_onboarding_event!("Open Panel Clicked");
- }
-
- fn view_docs(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context) {
- cx.open_url(&zed_urls::external_agents_docs(cx));
- cx.notify();
-
- acp_onboarding_event!("Documentation Link Clicked");
- }
-
- fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context) {
- cx.emit(DismissEvent);
- }
-}
-
-impl EventEmitter for AcpOnboardingModal {}
-
-impl Focusable for AcpOnboardingModal {
- fn focus_handle(&self, _cx: &App) -> FocusHandle {
- self.focus_handle.clone()
- }
-}
-
-impl ModalView for AcpOnboardingModal {}
-
-impl Render for AcpOnboardingModal {
- fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement {
- let illustration_element = |label: bool, opacity: f32| {
- h_flex()
- .px_1()
- .py_0p5()
- .gap_1()
- .rounded_sm()
- .bg(cx.theme().colors().element_active.opacity(0.05))
- .border_1()
- .border_color(cx.theme().colors().border)
- .border_dashed()
- .child(
- Icon::new(IconName::Stop)
- .size(IconSize::Small)
- .color(Color::Custom(cx.theme().colors().text_muted.opacity(0.15))),
- )
- .map(|this| {
- if label {
- this.child(
- Label::new("Your Agent Here")
- .size(LabelSize::Small)
- .color(Color::Muted),
- )
- } else {
- this.child(
- div().w_16().h_1().rounded_full().bg(cx
- .theme()
- .colors()
- .element_active
- .opacity(0.6)),
- )
- }
- })
- .opacity(opacity)
- };
-
- let illustration = h_flex()
- .relative()
- .h(rems_from_px(126.))
- .bg(cx.theme().colors().editor_background)
- .border_b_1()
- .border_color(cx.theme().colors().border_variant)
- .justify_center()
- .gap_8()
- .rounded_t_md()
- .overflow_hidden()
- .child(
- div().absolute().inset_0().w(px(515.)).h(px(126.)).child(
- Vector::new(VectorName::AcpGrid, rems_from_px(515.), rems_from_px(126.))
- .color(ui::Color::Custom(cx.theme().colors().text.opacity(0.02))),
- ),
- )
- .child(div().absolute().inset_0().size_full().bg(linear_gradient(
- 0.,
- linear_color_stop(
- cx.theme().colors().elevated_surface_background.opacity(0.1),
- 0.9,
- ),
- linear_color_stop(
- cx.theme().colors().elevated_surface_background.opacity(0.),
- 0.,
- ),
- )))
- .child(
- div()
- .absolute()
- .inset_0()
- .size_full()
- .bg(gpui::black().opacity(0.15)),
- )
- .child(
- h_flex()
- .gap_4()
- .child(
- Vector::new(VectorName::AcpLogo, rems_from_px(106.), rems_from_px(40.))
- .color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))),
- )
- .child(
- Vector::new(
- VectorName::AcpLogoSerif,
- rems_from_px(111.),
- rems_from_px(41.),
- )
- .color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))),
- ),
- )
- .child(
- v_flex()
- .gap_1p5()
- .child(illustration_element(false, 0.15))
- .child(illustration_element(true, 0.3))
- .child(
- h_flex()
- .pl_1()
- .pr_2()
- .py_0p5()
- .gap_1()
- .rounded_sm()
- .bg(cx.theme().colors().element_active.opacity(0.2))
- .border_1()
- .border_color(cx.theme().colors().border)
- .child(
- Icon::new(IconName::AiGemini)
- .size(IconSize::Small)
- .color(Color::Muted),
- )
- .child(Label::new("New Gemini CLI Thread").size(LabelSize::Small)),
- )
- .child(illustration_element(true, 0.3))
- .child(illustration_element(false, 0.15)),
- );
-
- let heading = v_flex()
- .w_full()
- .gap_1()
- .child(
- Label::new("Now Available")
- .size(LabelSize::Small)
- .color(Color::Muted),
- )
- .child(Headline::new("Bring Your Own Agent to Zed").size(HeadlineSize::Large));
-
- let copy = "Bring the agent of your choice to Zed via our new Agent Client Protocol (ACP), starting with Google's Gemini CLI integration.";
-
- let open_panel_button = Button::new("open-panel", "Start with Gemini CLI")
- .icon_size(IconSize::Indicator)
- .style(ButtonStyle::Tinted(TintColor::Accent))
- .full_width()
- .on_click(cx.listener(Self::open_panel));
-
- let docs_button = Button::new("add-other-agents", "Add Other Agents")
- .icon(IconName::ArrowUpRight)
- .icon_size(IconSize::Indicator)
- .icon_color(Color::Muted)
- .full_width()
- .on_click(cx.listener(Self::view_docs));
-
- let close_button = h_flex().absolute().top_2().right_2().child(
- IconButton::new("cancel", IconName::Close).on_click(cx.listener(
- |_, _: &ClickEvent, _window, cx| {
- acp_onboarding_event!("Canceled", trigger = "X click");
- cx.emit(DismissEvent);
- },
- )),
- );
-
- v_flex()
- .id("acp-onboarding")
- .key_context("AcpOnboardingModal")
- .relative()
- .w(rems(34.))
- .h_full()
- .elevation_3(cx)
- .track_focus(&self.focus_handle(cx))
- .overflow_hidden()
- .on_action(cx.listener(Self::cancel))
- .on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| {
- acp_onboarding_event!("Canceled", trigger = "Action");
- cx.emit(DismissEvent);
- }))
- .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
- this.focus_handle.focus(window);
- }))
- .child(illustration)
- .child(
- v_flex()
- .p_4()
- .gap_2()
- .child(heading)
- .child(Label::new(copy).color(Color::Muted))
- .child(
- v_flex()
- .w_full()
- .mt_2()
- .gap_1()
- .child(open_panel_button)
- .child(docs_button),
- ),
- )
- .child(close_button)
- }
-}
diff --git a/crates/client/src/zed_urls.rs b/crates/client/src/zed_urls.rs
index 7193c09947..9df41906d7 100644
--- a/crates/client/src/zed_urls.rs
+++ b/crates/client/src/zed_urls.rs
@@ -43,11 +43,3 @@ pub fn ai_privacy_and_security(cx: &App) -> String {
server_url = server_url(cx)
)
}
-
-/// Returns the URL to Zed AI's external agents documentation.
-pub fn external_agents_docs(cx: &App) -> String {
- format!(
- "{server_url}/docs/ai/external-agents",
- server_url = server_url(cx)
- )
-}
diff --git a/crates/command_palette/src/persistence.rs b/crates/command_palette/src/persistence.rs
index 01cf403083..5be97c36bc 100644
--- a/crates/command_palette/src/persistence.rs
+++ b/crates/command_palette/src/persistence.rs
@@ -1,10 +1,7 @@
use anyhow::Result;
use db::{
- query,
- sqlez::{
- bindable::Column, domain::Domain, statement::Statement,
- thread_safe_connection::ThreadSafeConnection,
- },
+ define_connection, query,
+ sqlez::{bindable::Column, statement::Statement},
sqlez_macros::sql,
};
use serde::{Deserialize, Serialize};
@@ -53,11 +50,8 @@ impl Column for SerializedCommandInvocation {
}
}
-pub struct CommandPaletteDB(ThreadSafeConnection);
-
-impl Domain for CommandPaletteDB {
- const NAME: &str = stringify!(CommandPaletteDB);
- const MIGRATIONS: &[&str] = &[sql!(
+define_connection!(pub static ref COMMAND_PALETTE_HISTORY: CommandPaletteDB<()> =
+ &[sql!(
CREATE TABLE IF NOT EXISTS command_invocations(
id INTEGER PRIMARY KEY AUTOINCREMENT,
command_name TEXT NOT NULL,
@@ -65,9 +59,7 @@ impl Domain for CommandPaletteDB {
last_invoked INTEGER DEFAULT (unixepoch()) NOT NULL
) STRICT;
)];
-}
-
-db::static_connection!(COMMAND_PALETTE_HISTORY, CommandPaletteDB, []);
+);
impl CommandPaletteDB {
pub async fn write_command_invocation(
diff --git a/crates/db/src/db.rs b/crates/db/src/db.rs
index 0802bd8bb7..8b790cbec8 100644
--- a/crates/db/src/db.rs
+++ b/crates/db/src/db.rs
@@ -110,14 +110,11 @@ pub async fn open_test_db(db_name: &str) -> ThreadSafeConnection {
}
/// Implements a basic DB wrapper for a given domain
-///
-/// Arguments:
-/// - static variable name for connection
-/// - type of connection wrapper
-/// - dependencies, whose migrations should be run prior to this domain's migrations
#[macro_export]
-macro_rules! static_connection {
- ($id:ident, $t:ident, [ $($d:ty),* ] $(, $global:ident)?) => {
+macro_rules! define_connection {
+ (pub static ref $id:ident: $t:ident<()> = $migrations:expr; $($global:ident)?) => {
+ pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection);
+
impl ::std::ops::Deref for $t {
type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection;
@@ -126,6 +123,16 @@ macro_rules! static_connection {
}
}
+ impl $crate::sqlez::domain::Domain for $t {
+ fn name() -> &'static str {
+ stringify!($t)
+ }
+
+ fn migrations() -> &'static [&'static str] {
+ $migrations
+ }
+ }
+
impl $t {
#[cfg(any(test, feature = "test-support"))]
pub async fn open_test_db(name: &'static str) -> Self {
@@ -135,8 +142,7 @@ macro_rules! static_connection {
#[cfg(any(test, feature = "test-support"))]
pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| {
- #[allow(unused_parens)]
- $t($crate::smol::block_on($crate::open_test_db::<($($d,)* $t)>(stringify!($id))))
+ $t($crate::smol::block_on($crate::open_test_db::<$t>(stringify!($id))))
});
#[cfg(not(any(test, feature = "test-support")))]
@@ -147,10 +153,46 @@ macro_rules! static_connection {
} else {
$crate::RELEASE_CHANNEL.dev_name()
};
- #[allow(unused_parens)]
- $t($crate::smol::block_on($crate::open_db::<($($d,)* $t)>(db_dir, scope)))
+ $t($crate::smol::block_on($crate::open_db::<$t>(db_dir, scope)))
});
- }
+ };
+ (pub static ref $id:ident: $t:ident<$($d:ty),+> = $migrations:expr; $($global:ident)?) => {
+ pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection);
+
+ impl ::std::ops::Deref for $t {
+ type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+ }
+
+ impl $crate::sqlez::domain::Domain for $t {
+ fn name() -> &'static str {
+ stringify!($t)
+ }
+
+ fn migrations() -> &'static [&'static str] {
+ $migrations
+ }
+ }
+
+ #[cfg(any(test, feature = "test-support"))]
+ pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| {
+ $t($crate::smol::block_on($crate::open_test_db::<($($d),+, $t)>(stringify!($id))))
+ });
+
+ #[cfg(not(any(test, feature = "test-support")))]
+ pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| {
+ let db_dir = $crate::database_dir();
+ let scope = if false $(|| stringify!($global) == "global")? {
+ "global"
+ } else {
+ $crate::RELEASE_CHANNEL.dev_name()
+ };
+ $t($crate::smol::block_on($crate::open_db::<($($d),+, $t)>(db_dir, scope)))
+ });
+ };
}
pub fn write_and_log(cx: &App, db_write: impl FnOnce() -> F + Send + 'static)
@@ -177,12 +219,17 @@ mod tests {
enum BadDB {}
impl Domain for BadDB {
- const NAME: &str = "db_tests";
- const MIGRATIONS: &[&str] = &[
- sql!(CREATE TABLE test(value);),
- // failure because test already exists
- sql!(CREATE TABLE test(value);),
- ];
+ fn name() -> &'static str {
+ "db_tests"
+ }
+
+ fn migrations() -> &'static [&'static str] {
+ &[
+ sql!(CREATE TABLE test(value);),
+ // failure because test already exists
+ sql!(CREATE TABLE test(value);),
+ ]
+ }
}
let tempdir = tempfile::Builder::new()
@@ -204,15 +251,25 @@ mod tests {
enum CorruptedDB {}
impl Domain for CorruptedDB {
- const NAME: &str = "db_tests";
- const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test(value);)];
+ fn name() -> &'static str {
+ "db_tests"
+ }
+
+ fn migrations() -> &'static [&'static str] {
+ &[sql!(CREATE TABLE test(value);)]
+ }
}
enum GoodDB {}
impl Domain for GoodDB {
- const NAME: &str = "db_tests"; //Notice same name
- const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test2(value);)];
+ fn name() -> &'static str {
+ "db_tests" //Notice same name
+ }
+
+ fn migrations() -> &'static [&'static str] {
+ &[sql!(CREATE TABLE test2(value);)] //But different migration
+ }
}
let tempdir = tempfile::Builder::new()
@@ -248,16 +305,25 @@ mod tests {
enum CorruptedDB {}
impl Domain for CorruptedDB {
- const NAME: &str = "db_tests";
+ fn name() -> &'static str {
+ "db_tests"
+ }
- const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test(value);)];
+ fn migrations() -> &'static [&'static str] {
+ &[sql!(CREATE TABLE test(value);)]
+ }
}
enum GoodDB {}
impl Domain for GoodDB {
- const NAME: &str = "db_tests"; //Notice same name
- const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test2(value);)]; // But different migration
+ fn name() -> &'static str {
+ "db_tests" //Notice same name
+ }
+
+ fn migrations() -> &'static [&'static str] {
+ &[sql!(CREATE TABLE test2(value);)] //But different migration
+ }
}
let tempdir = tempfile::Builder::new()
diff --git a/crates/db/src/kvp.rs b/crates/db/src/kvp.rs
index 8ea877b35b..256b789c9b 100644
--- a/crates/db/src/kvp.rs
+++ b/crates/db/src/kvp.rs
@@ -2,26 +2,16 @@ use gpui::App;
use sqlez_macros::sql;
use util::ResultExt as _;
-use crate::{
- query,
- sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection},
- write_and_log,
-};
+use crate::{define_connection, query, write_and_log};
-pub struct KeyValueStore(crate::sqlez::thread_safe_connection::ThreadSafeConnection);
-
-impl Domain for KeyValueStore {
- const NAME: &str = stringify!(KeyValueStore);
-
- const MIGRATIONS: &[&str] = &[sql!(
+define_connection!(pub static ref KEY_VALUE_STORE: KeyValueStore<()> =
+ &[sql!(
CREATE TABLE IF NOT EXISTS kv_store(
key TEXT PRIMARY KEY,
value TEXT NOT NULL
) STRICT;
)];
-}
-
-crate::static_connection!(KEY_VALUE_STORE, KeyValueStore, []);
+);
pub trait Dismissable {
const KEY: &'static str;
@@ -101,19 +91,15 @@ mod tests {
}
}
-pub struct GlobalKeyValueStore(ThreadSafeConnection);
-
-impl Domain for GlobalKeyValueStore {
- const NAME: &str = stringify!(GlobalKeyValueStore);
- const MIGRATIONS: &[&str] = &[sql!(
+define_connection!(pub static ref GLOBAL_KEY_VALUE_STORE: GlobalKeyValueStore<()> =
+ &[sql!(
CREATE TABLE IF NOT EXISTS kv_store(
key TEXT PRIMARY KEY,
value TEXT NOT NULL
) STRICT;
)];
-}
-
-crate::static_connection!(GLOBAL_KEY_VALUE_STORE, GlobalKeyValueStore, [], global);
+ global
+);
impl GlobalKeyValueStore {
query! {
diff --git a/crates/docs_preprocessor/src/main.rs b/crates/docs_preprocessor/src/main.rs
index c8c3dc54b7..c900eb692a 100644
--- a/crates/docs_preprocessor/src/main.rs
+++ b/crates/docs_preprocessor/src/main.rs
@@ -19,10 +19,6 @@ static KEYMAP_LINUX: LazyLock = LazyLock::new(|| {
load_keymap("keymaps/default-linux.json").expect("Failed to load Linux keymap")
});
-static KEYMAP_WINDOWS: LazyLock = LazyLock::new(|| {
- load_keymap("keymaps/default-windows.json").expect("Failed to load Windows keymap")
-});
-
static ALL_ACTIONS: LazyLock> = LazyLock::new(dump_all_gpui_actions);
const FRONT_MATTER_COMMENT: &str = "";
@@ -220,7 +216,6 @@ fn find_binding(os: &str, action: &str) -> Option {
let keymap = match os {
"macos" => &KEYMAP_MACOS,
"linux" | "freebsd" => &KEYMAP_LINUX,
- "windows" => &KEYMAP_WINDOWS,
_ => unreachable!("Not a valid OS: {}", os),
};
diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs
index 80680ae9c0..29e009fdf8 100644
--- a/crates/editor/src/editor.rs
+++ b/crates/editor/src/editor.rs
@@ -2588,7 +2588,7 @@ impl Editor {
|| binding
.keystrokes()
.first()
- .is_some_and(|keystroke| keystroke.display_modifiers.modified())
+ .is_some_and(|keystroke| keystroke.modifiers.modified())
}))
}
@@ -7686,16 +7686,16 @@ impl Editor {
.keystroke()
{
modifiers_held = modifiers_held
- || (&accept_keystroke.display_modifiers == modifiers
- && accept_keystroke.display_modifiers.modified());
+ || (&accept_keystroke.modifiers == modifiers
+ && accept_keystroke.modifiers.modified());
};
if let Some(accept_partial_keystroke) = self
.accept_edit_prediction_keybind(true, window, cx)
.keystroke()
{
modifiers_held = modifiers_held
- || (&accept_partial_keystroke.display_modifiers == modifiers
- && accept_partial_keystroke.display_modifiers.modified());
+ || (&accept_partial_keystroke.modifiers == modifiers
+ && accept_partial_keystroke.modifiers.modified());
}
if modifiers_held {
@@ -9044,7 +9044,7 @@ impl Editor {
let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac;
- let modifiers_color = if accept_keystroke.display_modifiers == window.modifiers() {
+ let modifiers_color = if accept_keystroke.modifiers == window.modifiers() {
Color::Accent
} else {
Color::Muted
@@ -9056,19 +9056,19 @@ impl Editor {
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
.text_size(TextSize::XSmall.rems(cx))
.child(h_flex().children(ui::render_modifiers(
- &accept_keystroke.display_modifiers,
+ &accept_keystroke.modifiers,
PlatformStyle::platform(),
Some(modifiers_color),
Some(IconSize::XSmall.rems().into()),
true,
)))
.when(is_platform_style_mac, |parent| {
- parent.child(accept_keystroke.display_key.clone())
+ parent.child(accept_keystroke.key.clone())
})
.when(!is_platform_style_mac, |parent| {
parent.child(
Key::new(
- util::capitalize(&accept_keystroke.display_key),
+ util::capitalize(&accept_keystroke.key),
Some(Color::Default),
)
.size(Some(IconSize::XSmall.rems().into())),
@@ -9171,7 +9171,7 @@ impl Editor {
max_width: Pixels,
cursor_point: Point,
style: &EditorStyle,
- accept_keystroke: Option<&gpui::KeybindingKeystroke>,
+ accept_keystroke: Option<&gpui::Keystroke>,
_window: &Window,
cx: &mut Context,
) -> Option {
@@ -9249,7 +9249,7 @@ impl Editor {
accept_keystroke.as_ref(),
|el, accept_keystroke| {
el.child(h_flex().children(ui::render_modifiers(
- &accept_keystroke.display_modifiers,
+ &accept_keystroke.modifiers,
PlatformStyle::platform(),
Some(Color::Default),
Some(IconSize::XSmall.rems().into()),
@@ -9319,7 +9319,7 @@ impl Editor {
.child(completion),
)
.when_some(accept_keystroke, |el, accept_keystroke| {
- if !accept_keystroke.display_modifiers.modified() {
+ if !accept_keystroke.modifiers.modified() {
return el;
}
@@ -9338,7 +9338,7 @@ impl Editor {
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
.when(is_platform_style_mac, |parent| parent.gap_1())
.child(h_flex().children(ui::render_modifiers(
- &accept_keystroke.display_modifiers,
+ &accept_keystroke.modifiers,
PlatformStyle::platform(),
Some(if !has_completion {
Color::Muted
diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs
index 91034829f7..4f3580da07 100644
--- a/crates/editor/src/element.rs
+++ b/crates/editor/src/element.rs
@@ -43,10 +43,10 @@ use gpui::{
Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle,
DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId,
GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero,
- KeybindingKeystroke, Length, ModifiersChangedEvent, MouseButton, MouseClickEvent,
- MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta,
- ScrollHandle, ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement,
- Style, Styled, TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill,
+ Keystroke, Length, ModifiersChangedEvent, MouseButton, MouseClickEvent, MouseDownEvent,
+ MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollHandle,
+ ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled,
+ TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill,
linear_color_stop, linear_gradient, outline, point, px, quad, relative, size, solid_background,
transparent_black,
};
@@ -7150,7 +7150,7 @@ fn header_jump_data(
pub struct AcceptEditPredictionBinding(pub(crate) Option);
impl AcceptEditPredictionBinding {
- pub fn keystroke(&self) -> Option<&KeybindingKeystroke> {
+ pub fn keystroke(&self) -> Option<&Keystroke> {
if let Some(binding) = self.0.as_ref() {
match &binding.keystrokes() {
[keystroke, ..] => Some(keystroke),
diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs
index b7110190fd..641e8a97ed 100644
--- a/crates/editor/src/items.rs
+++ b/crates/editor/src/items.rs
@@ -1404,7 +1404,7 @@ impl ProjectItem for Editor {
}
fn for_broken_project_item(
- abs_path: &Path,
+ abs_path: PathBuf,
is_local: bool,
e: &anyhow::Error,
window: &mut Window,
diff --git a/crates/editor/src/persistence.rs b/crates/editor/src/persistence.rs
index ec7c149b4e..88fde53947 100644
--- a/crates/editor/src/persistence.rs
+++ b/crates/editor/src/persistence.rs
@@ -1,17 +1,13 @@
use anyhow::Result;
-use db::{
- query,
- sqlez::{
- bindable::{Bind, Column, StaticColumnCount},
- domain::Domain,
- statement::Statement,
- },
- sqlez_macros::sql,
-};
+use db::sqlez::bindable::{Bind, Column, StaticColumnCount};
+use db::sqlez::statement::Statement;
use fs::MTime;
use itertools::Itertools as _;
use std::path::PathBuf;
+use db::sqlez_macros::sql;
+use db::{define_connection, query};
+
use workspace::{ItemId, WorkspaceDb, WorkspaceId};
#[derive(Clone, Debug, PartialEq, Default)]
@@ -87,11 +83,7 @@ impl Column for SerializedEditor {
}
}
-pub struct EditorDb(db::sqlez::thread_safe_connection::ThreadSafeConnection);
-
-impl Domain for EditorDb {
- const NAME: &str = stringify!(EditorDb);
-
+define_connection!(
// Current schema shape using pseudo-rust syntax:
// editors(
// item_id: usize,
@@ -121,8 +113,7 @@ impl Domain for EditorDb {
// start: usize,
// end: usize,
// )
-
- const MIGRATIONS: &[&str] = &[
+ pub static ref DB: EditorDb = &[
sql! (
CREATE TABLE editors(
item_id INTEGER NOT NULL,
@@ -198,9 +189,7 @@ impl Domain for EditorDb {
) STRICT;
),
];
-}
-
-db::static_connection!(DB, EditorDb, [WorkspaceDb]);
+);
// https://www.sqlite.org/limits.html
// > <..> the maximum value of a host parameter number is SQLITE_MAX_VARIABLE_NUMBER,
diff --git a/crates/feature_flags/src/feature_flags.rs b/crates/feature_flags/src/feature_flags.rs
index f5f7fc42b3..422979c429 100644
--- a/crates/feature_flags/src/feature_flags.rs
+++ b/crates/feature_flags/src/feature_flags.rs
@@ -98,10 +98,6 @@ impl FeatureFlag for GeminiAndNativeFeatureFlag {
// integration too, and we'd like to turn Gemini/Native on in new builds
// without enabling Claude Code in old builds.
const NAME: &'static str = "gemini-and-native";
-
- fn enabled_for_all() -> bool {
- true
- }
}
pub struct ClaudeCodeFeatureFlag;
@@ -205,7 +201,7 @@ impl FeatureFlagAppExt for App {
fn has_flag(&self) -> bool {
self.try_global::()
.map(|flags| flags.has_flag::())
- .unwrap_or(T::enabled_for_all())
+ .unwrap_or(false)
}
fn is_staff(&self) -> bool {
diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs
index 4ecb4a8829..958a609a09 100644
--- a/crates/git_ui/src/git_panel.rs
+++ b/crates/git_ui/src/git_panel.rs
@@ -4466,7 +4466,7 @@ fn current_language_model(cx: &Context<'_, GitPanel>) -> Option,
pub(crate) keymap: Rc>,
pub(crate) keyboard_layout: Box,
- pub(crate) keyboard_mapper: Rc,
pub(crate) global_action_listeners:
FxHashMap>>,
pending_effects: VecDeque,
@@ -313,7 +312,6 @@ impl App {
let text_system = Arc::new(TextSystem::new(platform.text_system()));
let entities = EntityMap::new();
let keyboard_layout = platform.keyboard_layout();
- let keyboard_mapper = platform.keyboard_mapper();
let app = Rc::new_cyclic(|this| AppCell {
app: RefCell::new(App {
@@ -339,7 +337,6 @@ impl App {
focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
keymap: Rc::new(RefCell::new(Keymap::default())),
keyboard_layout,
- keyboard_mapper,
global_action_listeners: FxHashMap::default(),
pending_effects: VecDeque::new(),
pending_notifications: FxHashSet::default(),
@@ -379,7 +376,6 @@ impl App {
if let Some(app) = app.upgrade() {
let cx = &mut app.borrow_mut();
cx.keyboard_layout = cx.platform.keyboard_layout();
- cx.keyboard_mapper = cx.platform.keyboard_mapper();
cx.keyboard_layout_observers
.clone()
.retain(&(), move |callback| (callback)(cx));
@@ -428,11 +424,6 @@ impl App {
self.keyboard_layout.as_ref()
}
- /// Get the current keyboard mapper.
- pub fn keyboard_mapper(&self) -> &Rc {
- &self.keyboard_mapper
- }
-
/// Invokes a handler when the current keyboard layout changes
pub fn on_keyboard_layout_change(&self, mut callback: F) -> Subscription
where
diff --git a/crates/gpui/src/keymap.rs b/crates/gpui/src/keymap.rs
index b3db09d821..757205fcc3 100644
--- a/crates/gpui/src/keymap.rs
+++ b/crates/gpui/src/keymap.rs
@@ -4,7 +4,7 @@ mod context;
pub use binding::*;
pub use context::*;
-use crate::{Action, AsKeystroke, Keystroke, is_no_action};
+use crate::{Action, Keystroke, is_no_action};
use collections::{HashMap, HashSet};
use smallvec::SmallVec;
use std::any::TypeId;
@@ -141,7 +141,7 @@ impl Keymap {
/// only.
pub fn bindings_for_input(
&self,
- input: &[impl AsKeystroke],
+ input: &[Keystroke],
context_stack: &[KeyContext],
) -> (SmallVec<[KeyBinding; 1]>, bool) {
let mut matched_bindings = SmallVec::<[(usize, BindingIndex, &KeyBinding); 1]>::new();
@@ -192,6 +192,7 @@ impl Keymap {
(bindings, !pending.is_empty())
}
+
/// Check if the given binding is enabled, given a certain key context.
/// Returns the deepest depth at which the binding matches, or None if it doesn't match.
fn binding_enabled(&self, binding: &KeyBinding, contexts: &[KeyContext]) -> Option {
@@ -638,7 +639,7 @@ mod tests {
fn assert_bindings(keymap: &Keymap, action: &dyn Action, expected: &[&str]) {
let actual = keymap
.bindings_for_action(action)
- .map(|binding| binding.keystrokes[0].inner.unparse())
+ .map(|binding| binding.keystrokes[0].unparse())
.collect::>();
assert_eq!(actual, expected, "{:?}", action);
}
diff --git a/crates/gpui/src/keymap/binding.rs b/crates/gpui/src/keymap/binding.rs
index a7cf9d5c54..729498d153 100644
--- a/crates/gpui/src/keymap/binding.rs
+++ b/crates/gpui/src/keymap/binding.rs
@@ -1,15 +1,14 @@
use std::rc::Rc;
-use crate::{
- Action, AsKeystroke, DummyKeyboardMapper, InvalidKeystrokeError, KeyBindingContextPredicate,
- KeybindingKeystroke, Keystroke, PlatformKeyboardMapper, SharedString,
-};
+use collections::HashMap;
+
+use crate::{Action, InvalidKeystrokeError, KeyBindingContextPredicate, Keystroke, SharedString};
use smallvec::SmallVec;
/// A keybinding and its associated metadata, from the keymap.
pub struct KeyBinding {
pub(crate) action: Box,
- pub(crate) keystrokes: SmallVec<[KeybindingKeystroke; 2]>,
+ pub(crate) keystrokes: SmallVec<[Keystroke; 2]>,
pub(crate) context_predicate: Option>,
pub(crate) meta: Option,
/// The json input string used when building the keybinding, if any
@@ -33,15 +32,7 @@ impl KeyBinding {
pub fn new(keystrokes: &str, action: A, context: Option<&str>) -> Self {
let context_predicate =
context.map(|context| KeyBindingContextPredicate::parse(context).unwrap().into());
- Self::load(
- keystrokes,
- Box::new(action),
- context_predicate,
- false,
- None,
- &DummyKeyboardMapper,
- )
- .unwrap()
+ Self::load(keystrokes, Box::new(action), context_predicate, None, None).unwrap()
}
/// Load a keybinding from the given raw data.
@@ -49,22 +40,24 @@ impl KeyBinding {
keystrokes: &str,
action: Box,
context_predicate: Option>,
- use_key_equivalents: bool,
+ key_equivalents: Option<&HashMap>,
action_input: Option,
- keyboard_mapper: &dyn PlatformKeyboardMapper,
) -> std::result::Result {
- let keystrokes: SmallVec<[KeybindingKeystroke; 2]> = keystrokes
+ let mut keystrokes: SmallVec<[Keystroke; 2]> = keystrokes
.split_whitespace()
- .map(|source| {
- let keystroke = Keystroke::parse(source)?;
- Ok(KeybindingKeystroke::new(
- keystroke,
- use_key_equivalents,
- keyboard_mapper,
- ))
- })
+ .map(Keystroke::parse)
.collect::>()?;
+ if let Some(equivalents) = key_equivalents {
+ for keystroke in keystrokes.iter_mut() {
+ if keystroke.key.chars().count() == 1
+ && let Some(key) = equivalents.get(&keystroke.key.chars().next().unwrap())
+ {
+ keystroke.key = key.to_string();
+ }
+ }
+ }
+
Ok(Self {
keystrokes,
action,
@@ -86,13 +79,13 @@ impl KeyBinding {
}
/// Check if the given keystrokes match this binding.
- pub fn match_keystrokes(&self, typed: &[impl AsKeystroke]) -> Option {
+ pub fn match_keystrokes(&self, typed: &[Keystroke]) -> Option {
if self.keystrokes.len() < typed.len() {
return None;
}
for (target, typed) in self.keystrokes.iter().zip(typed.iter()) {
- if !typed.as_keystroke().should_match(target) {
+ if !typed.should_match(target) {
return None;
}
}
@@ -101,7 +94,7 @@ impl KeyBinding {
}
/// Get the keystrokes associated with this binding
- pub fn keystrokes(&self) -> &[KeybindingKeystroke] {
+ pub fn keystrokes(&self) -> &[Keystroke] {
self.keystrokes.as_slice()
}
diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs
index f64710bc56..4d2feeaf1d 100644
--- a/crates/gpui/src/platform.rs
+++ b/crates/gpui/src/platform.rs
@@ -231,6 +231,7 @@ pub(crate) trait Platform: 'static {
fn on_quit(&self, callback: Box);
fn on_reopen(&self, callback: Box);
+ fn on_keyboard_layout_change(&self, callback: Box);
fn set_menus(&self, menus: Vec |