Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ trim_trailing_whitespace = false
indent_size = 4

[Makefile]
indent_style = tab
indent_style = tab
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,7 @@ Each provider has a dedicated adapter in `lua/agentic/acp/adapters/`:
- `opencode_acp_adapter.lua` - OpenCode ACP adapter
- `cursor_acp_adapter.lua` - Cursor Agent ACP adapter
- `auggie_acp_adapter.lua` - Auggie ACP adapter
- `copilot_acp_adapter.lua` - Copilot ACP adapter

These adapters implement provider-specific message formatting, tool call
handling, and protocol quirks.
Expand Down
20 changes: 11 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,15 @@ tools like `nvm`, `fnm`, etc...

**You are free to chose** any installation method you prefer!

| Provider | Install |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [claude-code-acp][claude-code-acp] | `pnpm add -g @zed-industries/claude-code-acp`<br/> **OR** `npm i -g @zed-industries/claude-code-acp`<br/> **OR** [Download binary][claude-code-acp-releases] |
| [gemini-cli][gemini-cli] | `pnpm add -g @google/gemini-cli`<br/> **OR** `npm i -g @google/gemini-cli`<br/> **OR** `brew install --cask gemini` |
| [codex-acp][codex-acp] | `pnpm add -g @zed-industries/codex-acp`<br/> **OR** `npm i -g @zed-industries/codex-acp`<br/> **OR** [Download binary][codex-acp-releases] |
| [opencode][opencode] | `pnpm add -g opencode-ai`<br/> **OR** `npm i -g opencode-ai`<br/> **OR** `brew install opencode`<br/> **OR** `curl -fsSL https://opencode.ai/install \| bash` |
| [cursor-agent][cursor-agent] | `pnpm add -g @blowmage/cursor-agent-acp`<br/> **OR** `npm i -g @blowmage/cursor-agent-acp` |
| [auggie][auggie] | `pnpm add -g @augmentcode/auggie`<br/> **OR** `npm i -g @augmentcode/auggie`<br/> **OR** See [Auggie docs][auggie-docs] |
| Provider | Install |
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [claude-code-acp][claude-code-acp] | `pnpm add -g @zed-industries/claude-code-acp`<br/> **OR** `npm i -g @zed-industries/claude-code-acp`<br/> **OR** [Download binary][claude-code-acp-releases] |
| [gemini-cli][gemini-cli] | `pnpm add -g @google/gemini-cli`<br/> **OR** `npm i -g @google/gemini-cli`<br/> **OR** `brew install --cask gemini` |
| [codex-acp][codex-acp] | `pnpm add -g @zed-industries/codex-acp`<br/> **OR** `npm i -g @zed-industries/codex-acp`<br/> **OR** [Download binary][codex-acp-releases] |
| [opencode][opencode] | `pnpm add -g opencode-ai`<br/> **OR** `npm i -g opencode-ai`<br/> **OR** `brew install opencode`<br/> **OR** `curl -fsSL https://opencode.ai/install \| bash` |
| [copilot-cli][copilot-cli] | `pnpm add -g @github/copilot`<br/> **OR** `npm i -g @github/copilot`<br/> **OR** `brew install copilot-cli`<br/> **OR** `curl -fsSL https://gh.io/copilot-install \| bash` |
| [cursor-agent][cursor-agent] | `pnpm add -g @blowmage/cursor-agent-acp`<br/> **OR** `npm i -g @blowmage/cursor-agent-acp` |
| [auggie][auggie] | `pnpm add -g @augmentcode/auggie`<br/> **OR** `npm i -g @augmentcode/auggie`<br/> **OR** See [Auggie docs][auggie-docs] |

> [!WARNING]
> These install commands are here for convenience, please always refer to the
Expand All @@ -122,7 +123,7 @@ tools like `nvm`, `fnm`, etc...
"carlos-algms/agentic.nvim",

opts = {
-- Available by default: "claude-acp" | "gemini-acp" | "codex-acp" | "opencode-acp" | "cursor-acp" | "auggie-acp"
-- Available by default: "claude-acp" | "gemini-acp" | "codex-acp" | "opencode-acp" | "cursor-acp" | "auggie-acp" | "copilot-acp"
provider = "claude-acp", -- setting the name here is all you need to get started
},

Expand Down Expand Up @@ -748,3 +749,4 @@ the the acknowledgments 😊.
[cursor-agent]: https://github.com/blowmage/cursor-agent-acp-npm
[auggie]: https://www.npmjs.com/package/@augmentcode/auggie
[auggie-docs]: https://docs.augmentcode.com/cli/setup-auggie
[copilot-cli]: https://github.com/github/copilot-cli
149 changes: 149 additions & 0 deletions lua/agentic/acp/adapters/copilot_acp_adapter.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
-- FIXIT: Copilot DON'T send modes like plan, ask for edits, etc. We need to remove it from the header and add a notification
-- FIXIT: Diff is NOT visible when editing
-- FIXIT: check if it's possible to dedupe the _handle_tool_call_update across adapters
-- FIXIT: Check it's possible to always update the tool call in the chat widget when there's body, for all adapters
-- Copilot is sending descriptive message for skill loads, etc, others might also

local ACPClient = require("agentic.acp.acp_client")
local FileSystem = require("agentic.utils.file_system")
local Logger = require("agentic.utils.logger")

--- @class agentic.acp.CopilotRawInput
--- @field path? string file path on the diff tool calls
--- @field new_str? string new string for edit tool calls
--- @field old_str? string old string for edit tool calls
--- @field url? string URL for fetch tool calls
--- @field skill? string skill name for skill tool calls
--- @field command? string command for execute tool calls
--- @field description? string

--- @class agentic.acp.CopilotToolCallMessage : agentic.acp.ToolCallMessage
--- @field rawInput? agentic.acp.CopilotRawInput

--- Copilot-specific adapter that extends ACPClient with Copilot-specific behaviors
--- @class agentic.acp.CopilotACPAdapter : agentic.acp.ACPClient
local CopilotACPAdapter = setmetatable({}, { __index = ACPClient })
CopilotACPAdapter.__index = CopilotACPAdapter

--- @param config agentic.acp.ACPProviderConfig
--- @param on_ready fun(client: agentic.acp.ACPClient)
--- @return agentic.acp.CopilotACPAdapter
function CopilotACPAdapter:new(config, on_ready)
-- Call parent constructor with parent class
self = ACPClient.new(ACPClient, config, on_ready)

-- Re-metatable to child class for proper inheritance chain
self = setmetatable(self, CopilotACPAdapter) --[[@as agentic.acp.CopilotACPAdapter]]

return self
end

--- @protected
--- @param session_id string
--- @param update agentic.acp.CopilotToolCallMessage
function CopilotACPAdapter:__handle_tool_call(session_id, update)
-- expected state, copilot is sending an empty content first, followed by the actual content
if not update.rawInput or vim.tbl_isempty(update.rawInput) then
return
end

local kind = update.kind

-- -- Detect sub-agent tasks: Copilot sends these as "think" with subagent_type in rawInput
-- if kind == "think" and update.rawInput.subagent_type then
-- kind = "SubAgent"
-- end

--- @type agentic.ui.MessageWriter.ToolCallBlock
local message = {
tool_call_id = update.toolCallId,
kind = kind,
status = update.status,
argument = update.title,
}

if kind == "read" or kind == "edit" then
message.argument = FileSystem.to_smart_path(update.rawInput.path)

if kind == "edit" then
local new_string = update.rawInput.new_str or ""
local old_string = update.rawInput.old_str or ""

-- -- Copilot might send content when creating new files
-- new_string = update.rawInput.content or new_string

message.diff = {
new = vim.split(new_string, "\n"),
old = vim.split(old_string, "\n"),
all = false, -- Copilot doesn't send replace_all info ??
}
elseif not message.argument or message.argument == "" then
message.argument = update.title or update.rawInput.path or ""
end
elseif kind == "execute" then
if update.rawInput.command ~= nil then
message.argument = update.rawInput.command
message.body = {
update.title or update.rawInput.description or "",
}
end
elseif kind == "fetch" then
if update.rawInput.url ~= nil then
message.argument = update.rawInput.url
message.body = {
update.title or "",
}
end
elseif kind == "other" then
if update.rawInput.skill ~= nil then
message.kind = "Skill"
message.argument = update.rawInput.skill
end
end

self:__with_subscriber(session_id, function(subscriber)
subscriber.on_tool_call(message)
end)
end

--- @protected
--- @param session_id string
--- @param update agentic.acp.ToolCallUpdate
function CopilotACPAdapter:__handle_tool_call_update(session_id, update)
if not update.status then
return
end

--- @type agentic.ui.MessageWriter.ToolCallBase
local message = {
tool_call_id = update.toolCallId,
status = update.status,
}

if update.content and update.content[1] then
local content = update.content[1]

if
content.type == "content"
and content.content
and content.content.text
then
message.body = vim.split(content.content.text, "\n")
elseif content.type == "diff" then -- luacheck: ignore 542 -- intentional empty block
-- ignore on purpose, diffs come only on tool call, not updates
else
Logger.debug("Unknown tool call update content type", {
content_type = content.type,
content = content.content,
session_id = session_id,
tool_call_id = update.toolCallId,
})
end
end

self:__with_subscriber(session_id, function(subscriber)
subscriber.on_tool_call_update(message)
end)
end

return CopilotACPAdapter
4 changes: 4 additions & 0 deletions lua/agentic/acp/agent_instance.lua
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ function AgentInstance.get_instance(provider_name, on_ready)
local AuggieACPAdapter =
require("agentic.acp.adapters.auggie_acp_adapter")
client = AuggieACPAdapter:new(config, on_ready)
elseif provider_name == "copilot-acp" then
local CopilotACPAdapter =
require("agentic.acp.adapters.copilot_acp_adapter")
client = CopilotACPAdapter:new(config, on_ready)
else
error("Unsupported ACP provider: " .. provider_name)
end
Expand Down
11 changes: 11 additions & 0 deletions lua/agentic/config_default.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
--- | "codex-acp"
--- | "opencode-acp"
--- | "cursor-acp"
--- | "copilot-acp"
--- | "auggie-acp"

--- @alias agentic.UserConfig.HeaderRenderFn fun(parts: agentic.ui.ChatWidget.HeaderParts): string|nil
Expand Down Expand Up @@ -100,6 +101,16 @@ local ConfigDefault = {
},
env = {},
},

["copilot-acp"] = {
name = "Copilot ACP",
command = "copilot",
args = {
"--acp",
"--stdio",
},
env = {},
},
},

--- @class agentic.UserConfig.Windows.Chat
Expand Down
3 changes: 3 additions & 0 deletions lua/agentic/session_manager.lua
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,9 @@ function SessionManager:_show_diff_in_buffer(tool_call_id)
local tracker = tool_call_id
and self.message_writer.tool_call_blocks[tool_call_id]

-- FIXIT: Copilot is sending wrong tool call id in the permission request
-- instead of tying it to the edit tool call, it always send `write-permission`
-- https://github.com/github/copilot-cli/issues/989#issuecomment-3826381550
if not tracker or tracker.kind ~= "edit" or tracker.diff == nil then
return
end
Expand Down