diff --git a/assets/icons/terminal_ghost.svg b/assets/icons/terminal_ghost.svg
new file mode 100644
index 0000000000..7d0d0e068e
--- /dev/null
+++ b/assets/icons/terminal_ghost.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/images/acp_grid.svg b/assets/images/acp_grid.svg
new file mode 100644
index 0000000000..8ebff8e1bc
--- /dev/null
+++ b/assets/images/acp_grid.svg
@@ -0,0 +1,1257 @@
+
diff --git a/assets/images/acp_logo.svg b/assets/images/acp_logo.svg
new file mode 100644
index 0000000000..efaa46707b
--- /dev/null
+++ b/assets/images/acp_logo.svg
@@ -0,0 +1 @@
+
diff --git a/assets/images/acp_logo_serif.svg b/assets/images/acp_logo_serif.svg
new file mode 100644
index 0000000000..6bc359cf82
--- /dev/null
+++ b/assets/images/acp_logo_serif.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json
new file mode 100644
index 0000000000..c7a6c3149c
--- /dev/null
+++ b/assets/keymaps/default-windows.json
@@ -0,0 +1,1260 @@
+[
+ // 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/crates/agent_ui/src/acp/entry_view_state.rs b/crates/agent_ui/src/acp/entry_view_state.rs
index 0e4080d689..becf6953fd 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,
+ AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, Focusable, ScrollHandle,
TextStyleRefinement, WeakEntity, Window,
};
use language::language_settings::SoftWrap;
@@ -154,10 +154,22 @@ impl EntryViewState {
});
}
}
- AgentThreadEntry::AssistantMessage(_) => {
- if index == self.entries.len() {
- self.entries.push(Entry::empty())
- }
+ 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);
}
};
}
@@ -177,7 +189,7 @@ impl EntryViewState {
pub fn settings_changed(&mut self, cx: &mut App) {
for entry in self.entries.iter() {
match entry {
- Entry::UserMessage { .. } => {}
+ Entry::UserMessage { .. } | Entry::AssistantMessage { .. } => {}
Entry::Content(response_views) => {
for view in response_views.values() {
if let Ok(diff_editor) = view.clone().downcast::() {
@@ -208,9 +220,29 @@ 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),
}
@@ -218,7 +250,7 @@ impl Entry {
pub fn message_editor(&self) -> Option<&Entity> {
match self {
Self::UserMessage(editor) => Some(editor),
- Entry::Content(_) => None,
+ Self::AssistantMessage(_) | Self::Content(_) => None,
}
}
@@ -239,6 +271,16 @@ 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),
@@ -254,7 +296,7 @@ impl Entry {
pub fn has_content(&self) -> bool {
match self {
Self::Content(map) => !map.is_empty(),
- Self::UserMessage(_) => false,
+ Self::UserMessage(_) | Self::AssistantMessage(_) => false,
}
}
}
diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs
index 6d8f8fb82e..c68c3a3e93 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,
- 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,
+ 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,
};
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, Tooltip, prelude::*,
+ Scrollbar, ScrollbarState, SpinnerLabel, TintColor, Tooltip, prelude::*,
};
use util::{ResultExt, size::format_file_size, time::duration_alt_display};
use workspace::{CollaboratorId, Workspace};
@@ -66,7 +66,6 @@ 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;
@@ -279,6 +278,7 @@ pub struct AcpThreadView {
editing_message: Option,
prompt_capabilities: Rc>,
is_loading_contents: bool,
+ install_command_markdown: Entity,
_cancel_task: Option>,
_subscriptions: [Subscription; 3],
}
@@ -392,6 +392,7 @@ 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(),
@@ -667,7 +668,12 @@ impl AcpThreadView {
match &self.thread_state {
ThreadState::Ready { .. } | ThreadState::Unauthenticated { .. } => "New Thread".into(),
ThreadState::Loading { .. } => "Loading…".into(),
- ThreadState::LoadError(_) => "Failed to load".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(),
+ },
}
}
@@ -1334,6 +1340,10 @@ 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
@@ -1493,6 +1503,20 @@ 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()
@@ -1511,6 +1535,7 @@ impl AcpThreadView {
entry_ix,
chunk_ix,
md.clone(),
+ Some(chunk_ix) == pending_thinking_chunk_ix,
window,
cx,
)
@@ -1524,7 +1549,7 @@ impl AcpThreadView {
v_flex()
.px_5()
.py_1()
- .when(entry_ix + 1 == total_entries, |this| this.pb_4())
+ .when(is_last, |this| this.pb_4())
.w_full()
.text_ui(cx)
.child(message_body)
@@ -1533,7 +1558,7 @@ impl AcpThreadView {
AgentThreadEntry::ToolCall(tool_call) => {
let has_terminals = tool_call.terminals().next().is_some();
- div().w_full().py_1().px_5().map(|this| {
+ div().w_full().map(|this| {
if has_terminals {
this.children(tool_call.terminals().map(|terminal| {
self.render_terminal_tool_call(
@@ -1609,64 +1634,90 @@ 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()
- .gap_1p5()
+ .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))
.child(
h_flex()
- .size_4()
- .justify_center()
+ .h(window.line_height())
+ .gap_1p5()
.child(
- div()
- .group_hover(&card_header_id, |s| s.invisible().w_0())
- .child(
- Icon::new(IconName::ToolThink)
- .size(IconSize::Small)
- .color(Color::Muted),
- ),
+ 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();
- }
- })),
- ),
+ 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")
+ }
+ }),
),
)
.child(
- div()
- .text_size(self.tool_name_font_size())
- .text_color(cx.theme().colors().text_muted)
- .child("Thinking"),
+ 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();
+ }
+ })),
)
.on_click(cx.listener({
move |this, _event, _window, cx| {
@@ -1679,22 +1730,28 @@ impl AcpThreadView {
}
})),
)
- .when(is_open, |this| {
- this.child(
- div()
- .relative()
- .mt_1p5()
- .ml(rems(0.4))
- .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),
- )),
- )
- })
+ .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)),
+ )
.into_any_element()
}
@@ -1705,7 +1762,6 @@ 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 =
@@ -1734,11 +1790,7 @@ impl AcpThreadView {
_ => false,
};
- let failed_tool_call = matches!(
- tool_call.status,
- ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed
- );
-
+ let has_location = tool_call.locations.len() == 1;
let needs_confirmation = matches!(
tool_call.status,
ToolCallStatus::WaitingForConfirmation { .. }
@@ -1751,23 +1803,31 @@ impl AcpThreadView {
let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id);
- let gradient_overlay = |color: Hsla| {
+ let gradient_overlay = {
div()
.absolute()
.top_0()
.right_0()
.w_12()
.h_full()
- .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
+ .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.,
+ ),
+ ))
+ }
+ })
};
let tool_output_display = if is_open {
@@ -1818,41 +1878,58 @@ impl AcpThreadView {
};
v_flex()
- .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 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()
+ }
})
+ .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.pl_1p5()
- .pr_1()
- .py_0p5()
+ this.p_0p5()
.rounded_t_md()
- .when(is_open && !failed_tool_call, |this| {
+ .bg(self.tool_card_header_bg(cx))
+ .when(is_open && !failed_or_canceled, |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() - px(2.))
+ .h(window.line_height())
.text_size(self.tool_name_font_size())
- .gap_0p5()
+ .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 tool_call.locations.len() == 1 {
+ .child(if has_location {
let name = tool_call.locations[0]
.path
.file_name()
@@ -1863,13 +1940,6 @@ 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)
@@ -1879,31 +1949,28 @@ 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()
- .max_w_full()
- .ml_1p5()
- .overflow_hidden()
- .child(h_flex().pr_8().child(self.render_markdown(
+ .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)),
)
- .child(
- h_flex()
- .gap_px()
- .when(is_collapsible, |this| {
- this.child(
+ .when(is_collapsible || failed_or_canceled, |this| {
+ this.child(
+ h_flex()
+ .px_1()
+ .gap_px()
+ .when(is_collapsible, |this| {
+ this.child(
Disclosure::new(("expand", entry_ix), is_open)
.opened_icon(IconName::ChevronUp)
.closed_icon(IconName::ChevronDown)
@@ -1920,15 +1987,16 @@ 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)
}
@@ -2214,6 +2282,12 @@ 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()
@@ -2229,10 +2303,7 @@ impl AcpThreadView {
let is_expanded = self.expanded_tool_calls.contains(&tool_call.id);
let header = h_flex()
- .id(SharedString::from(format!(
- "terminal-tool-header-{}",
- terminal.entity_id()
- )))
+ .id(header_id)
.flex_none()
.gap_1()
.justify_between()
@@ -2296,23 +2367,6 @@ 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 {
@@ -2365,6 +2419,7 @@ 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| {
@@ -2373,8 +2428,26 @@ 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
@@ -2384,7 +2457,8 @@ impl AcpThreadView {
let show_output = is_expanded && terminal_view.is_some();
v_flex()
- .mb_2()
+ .my_2()
+ .mx_5()
.border_1()
.when(tool_failed || command_failed, |card| card.border_dashed())
.border_color(border_color)
@@ -2392,9 +2466,10 @@ impl AcpThreadView {
.overflow_hidden()
.child(
v_flex()
+ .group(&header_group)
.py_1p5()
- .pl_2()
.pr_1p5()
+ .pl_2()
.gap_0p5()
.bg(header_bg)
.text_xs()
@@ -2766,125 +2841,26 @@ impl AcpThreadView {
)
}
- fn render_load_error(&self, e: &LoadError, cx: &Context) -> AnyElement {
- let (message, action_slot) = match e {
+ fn render_load_error(
+ &self,
+ e: &LoadError,
+ window: &mut Window,
+ cx: &mut Context,
+ ) -> AnyElement {
+ let (message, action_slot): (SharedString, _) = match e {
LoadError::NotInstalled {
- error_message,
- install_message,
+ error_message: _,
+ install_message: _,
install_command,
} => {
- 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()))
+ return self.render_not_installed(install_command.clone(), false, window, cx);
}
LoadError::Unsupported {
- error_message,
- upgrade_message,
+ error_message: _,
+ upgrade_message: _,
upgrade_command,
} => {
- 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()))
+ return self.render_not_installed(upgrade_command.clone(), true, window, cx);
}
LoadError::Exited { .. } => ("Server exited with status {status}".into(), None),
LoadError::Other(msg) => (
@@ -2902,6 +2878,121 @@ 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,
@@ -4153,13 +4244,14 @@ impl AcpThreadView {
) -> impl IntoElement {
let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating);
if is_generating {
- return h_flex().id("thread-controls-container").ml_1().child(
+ return h_flex().id("thread-controls-container").child(
div()
.py_2()
- .px(rems_from_px(22.))
+ .px_5()
.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)
@@ -4185,12 +4277,10 @@ impl AcpThreadView {
.id("thread-controls-container")
.group("thread-controls-container")
.w_full()
- .mr_1()
- .pt_1()
- .pb_2()
- .px(RESPONSE_PADDING_X)
+ .py_2()
+ .px_5()
.gap_px()
- .opacity(0.4)
+ .opacity(0.6)
.hover(|style| style.opacity(1.))
.flex_wrap()
.justify_end();
@@ -4201,56 +4291,50 @@ 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(
- 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,
- );
- })),
- )
- )
+ 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);
+ })),
+ );
}
container.child(open_as_markdown).child(scroll_to_top)
@@ -4882,7 +4966,7 @@ impl Render for AcpThreadView {
.size_full()
.items_center()
.justify_end()
- .child(self.render_load_error(e, cx)),
+ .child(self.render_load_error(e, window, 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 c279115880..224f49cc3e 100644
--- a/crates/agent_ui/src/agent_configuration.rs
+++ b/crates/agent_ui/src/agent_configuration.rs
@@ -1093,7 +1093,7 @@ impl AgentConfiguration {
)
.child(
Label::new(
- "Use the full power of Zed's UI with your favorite agent, connected via the Agent Client Protocol.",
+ "Bring the agent of your choice to Zed via our new Agent Client Protocol.",
)
.color(Color::Muted),
),
diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs
index 267c76d73f..d1cf748733 100644
--- a/crates/agent_ui/src/agent_panel.rs
+++ b/crates/agent_ui/src/agent_panel.rs
@@ -14,6 +14,7 @@ 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,
@@ -77,7 +78,10 @@ use workspace::{
};
use zed_actions::{
DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize,
- agent::{OpenOnboardingModal, OpenSettings, ResetOnboarding, ToggleModelSelector},
+ agent::{
+ OpenAcpOnboardingModal, OpenOnboardingModal, OpenSettings, ResetOnboarding,
+ ToggleModelSelector,
+ },
assistant::{OpenRulesLibrary, ToggleFocus},
};
@@ -201,6 +205,9 @@ 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();
@@ -1841,19 +1848,6 @@ 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()
}
@@ -1864,6 +1858,11 @@ 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(
@@ -2544,7 +2543,7 @@ impl AgentPanel {
workspace.panel::(cx)
{
panel.update(cx, |panel, cx| {
- panel.set_selected_agent(
+ panel.new_agent_thread(
AgentType::NativeAgent,
window,
cx,
@@ -2570,7 +2569,7 @@ impl AgentPanel {
workspace.panel::(cx)
{
panel.update(cx, |panel, cx| {
- panel.set_selected_agent(
+ panel.new_agent_thread(
AgentType::TextThread,
window,
cx,
@@ -2598,7 +2597,7 @@ impl AgentPanel {
workspace.panel::(cx)
{
panel.update(cx, |panel, cx| {
- panel.set_selected_agent(
+ panel.new_agent_thread(
AgentType::Gemini,
window,
cx,
@@ -2625,7 +2624,7 @@ impl AgentPanel {
workspace.panel::(cx)
{
panel.update(cx, |panel, cx| {
- panel.set_selected_agent(
+ panel.new_agent_thread(
AgentType::ClaudeCode,
window,
cx,
@@ -2658,7 +2657,7 @@ impl AgentPanel {
workspace.panel::(cx)
{
panel.update(cx, |panel, cx| {
- panel.set_selected_agent(
+ panel.new_agent_thread(
AgentType::Custom {
name: agent_name
.clone(),
@@ -2682,9 +2681,9 @@ impl AgentPanel {
})
.when(cx.has_flag::(), |menu| {
menu.separator().link(
- "Add Your Own Agent",
+ "Add Other Agents",
OpenBrowser {
- url: "https://agentclientprotocol.com/".into(),
+ url: zed_urls::external_agents_docs(cx),
}
.boxed_clone(),
)
diff --git a/crates/agent_ui/src/ui.rs b/crates/agent_ui/src/ui.rs
index ada973cddf..600698b07e 100644
--- a/crates/agent_ui/src/ui.rs
+++ b/crates/agent_ui/src/ui.rs
@@ -1,3 +1,4 @@
+mod acp_onboarding_modal;
mod agent_notification;
mod burn_mode_tooltip;
mod context_pill;
@@ -6,6 +7,7 @@ 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
new file mode 100644
index 0000000000..0ed9de7221
--- /dev/null
+++ b/crates/agent_ui/src/ui/acp_onboarding_modal.rs
@@ -0,0 +1,254 @@
+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 9df41906d7..7193c09947 100644
--- a/crates/client/src/zed_urls.rs
+++ b/crates/client/src/zed_urls.rs
@@ -43,3 +43,11 @@ 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/docs_preprocessor/src/main.rs b/crates/docs_preprocessor/src/main.rs
index c900eb692a..c8c3dc54b7 100644
--- a/crates/docs_preprocessor/src/main.rs
+++ b/crates/docs_preprocessor/src/main.rs
@@ -19,6 +19,10 @@ 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 = "";
@@ -216,6 +220,7 @@ 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 29e009fdf8..80680ae9c0 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.modifiers.modified())
+ .is_some_and(|keystroke| keystroke.display_modifiers.modified())
}))
}
@@ -7686,16 +7686,16 @@ impl Editor {
.keystroke()
{
modifiers_held = modifiers_held
- || (&accept_keystroke.modifiers == modifiers
- && accept_keystroke.modifiers.modified());
+ || (&accept_keystroke.display_modifiers == modifiers
+ && accept_keystroke.display_modifiers.modified());
};
if let Some(accept_partial_keystroke) = self
.accept_edit_prediction_keybind(true, window, cx)
.keystroke()
{
modifiers_held = modifiers_held
- || (&accept_partial_keystroke.modifiers == modifiers
- && accept_partial_keystroke.modifiers.modified());
+ || (&accept_partial_keystroke.display_modifiers == modifiers
+ && accept_partial_keystroke.display_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.modifiers == window.modifiers() {
+ let modifiers_color = if accept_keystroke.display_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.modifiers,
+ &accept_keystroke.display_modifiers,
PlatformStyle::platform(),
Some(modifiers_color),
Some(IconSize::XSmall.rems().into()),
true,
)))
.when(is_platform_style_mac, |parent| {
- parent.child(accept_keystroke.key.clone())
+ parent.child(accept_keystroke.display_key.clone())
})
.when(!is_platform_style_mac, |parent| {
parent.child(
Key::new(
- util::capitalize(&accept_keystroke.key),
+ util::capitalize(&accept_keystroke.display_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::Keystroke>,
+ accept_keystroke: Option<&gpui::KeybindingKeystroke>,
_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.modifiers,
+ &accept_keystroke.display_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.modifiers.modified() {
+ if !accept_keystroke.display_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.modifiers,
+ &accept_keystroke.display_modifiers,
PlatformStyle::platform(),
Some(if !has_completion {
Color::Muted
diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs
index 4f3580da07..91034829f7 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,
- 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,
+ 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,
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<&Keystroke> {
+ pub fn keystroke(&self) -> Option<&KeybindingKeystroke> {
if let Some(binding) = self.0.as_ref() {
match &binding.keystrokes() {
[keystroke, ..] => Some(keystroke),
diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs
index bbd59fa7bc..b59d7e717a 100644
--- a/crates/gpui/src/app.rs
+++ b/crates/gpui/src/app.rs
@@ -37,10 +37,10 @@ use crate::{
AssetSource, BackgroundExecutor, Bounds, ClipboardItem, CursorStyle, DispatchPhase, DisplayId,
EventEmitter, FocusHandle, FocusMap, ForegroundExecutor, Global, KeyBinding, KeyContext,
Keymap, Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform,
- PlatformDisplay, PlatformKeyboardLayout, Point, PromptBuilder, PromptButton, PromptHandle,
- PromptLevel, Render, RenderImage, RenderablePromptHandle, Reservation, ScreenCaptureSource,
- SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, Window, WindowAppearance,
- WindowHandle, WindowId, WindowInvalidator,
+ PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, Point, PromptBuilder,
+ PromptButton, PromptHandle, PromptLevel, Render, RenderImage, RenderablePromptHandle,
+ Reservation, ScreenCaptureSource, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem,
+ Window, WindowAppearance, WindowHandle, WindowId, WindowInvalidator,
colors::{Colors, GlobalColors},
current_platform, hash, init_app_menus,
};
@@ -263,6 +263,7 @@ pub struct App {
pub(crate) focus_handles: Arc,
pub(crate) keymap: Rc>,
pub(crate) keyboard_layout: Box,
+ pub(crate) keyboard_mapper: Rc,
pub(crate) global_action_listeners:
FxHashMap>>,
pending_effects: VecDeque,
@@ -312,6 +313,7 @@ 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 {
@@ -337,6 +339,7 @@ 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(),
@@ -376,6 +379,7 @@ 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));
@@ -424,6 +428,11 @@ 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 757205fcc3..b3db09d821 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, Keystroke, is_no_action};
+use crate::{Action, AsKeystroke, 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: &[Keystroke],
+ input: &[impl AsKeystroke],
context_stack: &[KeyContext],
) -> (SmallVec<[KeyBinding; 1]>, bool) {
let mut matched_bindings = SmallVec::<[(usize, BindingIndex, &KeyBinding); 1]>::new();
@@ -192,7 +192,6 @@ 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 {
@@ -639,7 +638,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].unparse())
+ .map(|binding| binding.keystrokes[0].inner.unparse())
.collect::>();
assert_eq!(actual, expected, "{:?}", action);
}
diff --git a/crates/gpui/src/keymap/binding.rs b/crates/gpui/src/keymap/binding.rs
index 729498d153..a7cf9d5c54 100644
--- a/crates/gpui/src/keymap/binding.rs
+++ b/crates/gpui/src/keymap/binding.rs
@@ -1,14 +1,15 @@
use std::rc::Rc;
-use collections::HashMap;
-
-use crate::{Action, InvalidKeystrokeError, KeyBindingContextPredicate, Keystroke, SharedString};
+use crate::{
+ Action, AsKeystroke, DummyKeyboardMapper, InvalidKeystrokeError, KeyBindingContextPredicate,
+ KeybindingKeystroke, Keystroke, PlatformKeyboardMapper, SharedString,
+};
use smallvec::SmallVec;
/// A keybinding and its associated metadata, from the keymap.
pub struct KeyBinding {
pub(crate) action: Box,
- pub(crate) keystrokes: SmallVec<[Keystroke; 2]>,
+ pub(crate) keystrokes: SmallVec<[KeybindingKeystroke; 2]>,
pub(crate) context_predicate: Option>,
pub(crate) meta: Option,
/// The json input string used when building the keybinding, if any
@@ -32,7 +33,15 @@ 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, None, None).unwrap()
+ Self::load(
+ keystrokes,
+ Box::new(action),
+ context_predicate,
+ false,
+ None,
+ &DummyKeyboardMapper,
+ )
+ .unwrap()
}
/// Load a keybinding from the given raw data.
@@ -40,24 +49,22 @@ impl KeyBinding {
keystrokes: &str,
action: Box,
context_predicate: Option>,
- key_equivalents: Option<&HashMap>,
+ use_key_equivalents: bool,
action_input: Option,
+ keyboard_mapper: &dyn PlatformKeyboardMapper,
) -> std::result::Result {
- let mut keystrokes: SmallVec<[Keystroke; 2]> = keystrokes
+ let keystrokes: SmallVec<[KeybindingKeystroke; 2]> = keystrokes
.split_whitespace()
- .map(Keystroke::parse)
+ .map(|source| {
+ let keystroke = Keystroke::parse(source)?;
+ Ok(KeybindingKeystroke::new(
+ keystroke,
+ use_key_equivalents,
+ keyboard_mapper,
+ ))
+ })
.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,
@@ -79,13 +86,13 @@ impl KeyBinding {
}
/// Check if the given keystrokes match this binding.
- pub fn match_keystrokes(&self, typed: &[Keystroke]) -> Option {
+ pub fn match_keystrokes(&self, typed: &[impl AsKeystroke]) -> Option {
if self.keystrokes.len() < typed.len() {
return None;
}
for (target, typed) in self.keystrokes.iter().zip(typed.iter()) {
- if !typed.should_match(target) {
+ if !typed.as_keystroke().should_match(target) {
return None;
}
}
@@ -94,7 +101,7 @@ impl KeyBinding {
}
/// Get the keystrokes associated with this binding
- pub fn keystrokes(&self) -> &[Keystroke] {
+ pub fn keystrokes(&self) -> &[KeybindingKeystroke] {
self.keystrokes.as_slice()
}
diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs
index 4d2feeaf1d..f64710bc56 100644
--- a/crates/gpui/src/platform.rs
+++ b/crates/gpui/src/platform.rs
@@ -231,7 +231,6 @@ 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 |