From f7eb423baccbb27f5b5608fb91acee2d6bc769c7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 12 Jul 2025 00:20:16 +0000 Subject: [PATCH 01/75] chore(doc): auto generate docs --- doc/CopilotChat.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 980baeda..9faa9a44 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 July 09 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 July 12 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 7559fd25928f8f3cf311ff25b95bdc5f9ec736d7 Mon Sep 17 00:00:00 2001 From: "Md. Iftakhar Awal Chowdhury" <42291930+AtifChy@users.noreply.github.com> Date: Fri, 25 Jul 2025 05:47:50 +0600 Subject: [PATCH 02/75] feat: add Windows_NT support in Makefile and dynamic library loading (#1190) * feat: add Windows_NT support in Makefile and dynamic library loading * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- Makefile | 3 +++ README.md | 1 - lua/CopilotChat/tiktoken.lua | 17 +++++++++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index cd71bc6e..c5d53c52 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,9 @@ ifeq ($(UNAME), Linux) else ifeq ($(UNAME), Darwin) OS := macOS EXT := dylib +else ifeq ($(UNAME), Windows_NT) + OS := windows + EXT := dll else $(error Unsupported operating system: $(UNAME)) endif diff --git a/README.md b/README.md index 6761c5e6..66a56cf7 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,6 @@ CopilotChat.nvim is a Neovim plugin that brings GitHub Copilot Chat capabilities ## Optional Dependencies - [tiktoken_core](https://github.com/gptlang/lua-tiktoken) - For accurate token counting - - Arch Linux: Install [`luajit-tiktoken-bin`](https://aur.archlinux.org/packages/luajit-tiktoken-bin) or [`lua51-tiktoken-bin`](https://aur.archlinux.org/packages/lua51-tiktoken-bin) from AUR - Via luarocks: `sudo luarocks install --lua-version 5.1 tiktoken_core` - Manual: Download from [lua-tiktoken releases](https://github.com/gptlang/lua-tiktoken/releases) and save as `tiktoken_core.so` in your Lua path diff --git a/lua/CopilotChat/tiktoken.lua b/lua/CopilotChat/tiktoken.lua index 9bfa2945..a4582cb4 100644 --- a/lua/CopilotChat/tiktoken.lua +++ b/lua/CopilotChat/tiktoken.lua @@ -2,6 +2,23 @@ local notify = require('CopilotChat.notify') local utils = require('CopilotChat.utils') local current_tokenizer = nil +--- @return string +local function get_lib_extension() + if jit.os:lower() == 'mac' or jit.os:lower() == 'osx' then + return '.dylib' + end + if jit.os:lower() == 'windows' then + return '.dll' + end + return '.so' +end + +package.cpath = package.cpath + .. ';' + .. debug.getinfo(1).source:match('@?(.*/)') + .. '../../build/?' + .. get_lib_extension() + local tiktoken_ok, tiktoken_core = pcall(require, 'tiktoken_core') if not tiktoken_ok then tiktoken_core = nil From 4d0d949b7367b10c9f3ab9011ac54dd3107fb0a4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 24 Jul 2025 23:48:05 +0000 Subject: [PATCH 03/75] chore(doc): auto generate docs --- doc/CopilotChat.txt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 9faa9a44..e5863a11 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 July 12 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 July 24 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -61,14 +61,12 @@ capabilities directly into your editor. It provides: OPTIONAL DEPENDENCIES *CopilotChat-optional-dependencies* -- tiktoken_core - For accurate token - counting +- tiktoken_core - For accurate token counting - Arch Linux: Install `luajit-tiktoken-bin` or `lua51-tiktoken-bin` from AUR - Via luarocks: `sudo luarocks install --lua-version 5.1 tiktoken_core` - Manual: Download from lua-tiktoken releases and save as `tiktoken_core.so` in your Lua path - git - For git diff context features -- ripgrep - For improved search - performance +- ripgrep - For improved search performance - lynx - For improved URL context features From d0537a749e11a68ebaea3967b9c698f998a700fe Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Fri, 25 Jul 2025 01:50:11 +0200 Subject: [PATCH 04/75] docs: add AtifChy as a contributor for code, and doc (#1192) * docs: update README.md [skip ci] * docs: update .all-contributorsrc [skip ci] * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .all-contributorsrc | 7 +++++++ README.md | 1 + 2 files changed, 8 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index b753dc3f..d335de60 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -417,6 +417,13 @@ "avatar_url": "https://avatars.githubusercontent.com/u/509703?v=4", "profile": "https://a14n.net", "contributions": ["code"] + }, + { + "login": "AtifChy", + "name": "Md. Iftakhar Awal Chowdhury", + "avatar_url": "https://avatars.githubusercontent.com/u/42291930?v=4", + "profile": "https://github.com/AtifChy", + "contributions": ["code", "doc"] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 66a56cf7..a08f6b0b 100644 --- a/README.md +++ b/README.md @@ -888,6 +888,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Anton Ε½danov
Anton Ε½danov

πŸ“– πŸ’» Fredrik Averpil
Fredrik Averpil

πŸ’» Aaron D Borden
Aaron D Borden

πŸ’» + Md. Iftakhar Awal Chowdhury
Md. Iftakhar Awal Chowdhury

πŸ’» πŸ“– From 057b8e46d955748b1426e7b174d7af3e58f5191b Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 28 Jul 2025 02:21:46 +0200 Subject: [PATCH 05/75] feat(context)!: switch from contexts to function calling (#1029) This change modernizes the CopilotChat architecture by implementing a tools-based approach using function calling. Each tool has a schema definition that enables structured parameter collection and validation. Resources are now handled more consistently, with a clearer distinction between content types. https://platform.openai.com/docs/guides/function-calling?api-mode=responses https://modelcontextprotocol.info/specification/2024-11-05/server/tools/ BREAKING CHANGE: The context API has changed from callback-based input handling to schema-based definitions. BREAKING CHANGE: config.contexts renamed to config.tools BREAKING CHANGE: config.context removed, use config.sticky BREAKING CHANGE: diagnostics moved to separate tool call, selection and buffer calls no longer include them by default BREAKING CHANGE: non-resource based tool calls can no longer be soft stored in sticky, now they are auto expanded to promot BREAKING CHANGE: viewing full context is no longer possible (as now tools can have bigger side effects), gi renamed to gc, now also includes selection BREAKING CHANGE: filenames renamed to glob BREAKING CHANGE: files removed (use glob together with tool calling instead, or buffers/quickfix) BREAKING CHANGE: copilot extension agents removed, tools + mcp servers can replace this feature and maintaining them was pain, they can still be implemented via custom providers anyway BREAKING CHANGE: actions and integrations action removed as they were deprecated for a while Closes #1045 Closes #1053 Closes #1076 Closes #1090 Closes #1096 Closes #526 Signed-off-by: Tomas Slusny --- README.md | 297 +++---- lua/CopilotChat/actions.lua | 49 -- lua/CopilotChat/client.lua | 470 ++++------- lua/CopilotChat/config.lua | 60 +- lua/CopilotChat/config/contexts.lua | 352 -------- lua/CopilotChat/config/functions.lua | 503 ++++++++++++ lua/CopilotChat/config/mappings.lua | 136 ++-- lua/CopilotChat/config/prompts.lua | 95 ++- lua/CopilotChat/config/providers.lua | 173 ++-- lua/CopilotChat/functions.lua | 198 +++++ lua/CopilotChat/health.lua | 3 +- lua/CopilotChat/init.lua | 770 +++++++++--------- lua/CopilotChat/integrations/fzflua.lua | 42 - lua/CopilotChat/integrations/snacks.lua | 54 -- lua/CopilotChat/integrations/telescope.lua | 65 -- .../{context.lua => resources.lua} | 147 ++-- lua/CopilotChat/select.lua | 32 +- lua/CopilotChat/ui/chat.lua | 451 ++++++---- lua/CopilotChat/ui/overlay.lua | 8 +- lua/CopilotChat/ui/spinner.lua | 2 +- lua/CopilotChat/utils.lua | 312 ++++--- plugin/CopilotChat.lua | 35 +- 22 files changed, 2150 insertions(+), 2104 deletions(-) delete mode 100644 lua/CopilotChat/actions.lua delete mode 100644 lua/CopilotChat/config/contexts.lua create mode 100644 lua/CopilotChat/config/functions.lua create mode 100644 lua/CopilotChat/functions.lua delete mode 100644 lua/CopilotChat/integrations/fzflua.lua delete mode 100644 lua/CopilotChat/integrations/snacks.lua delete mode 100644 lua/CopilotChat/integrations/telescope.lua rename lua/CopilotChat/{context.lua => resources.lua} (81%) diff --git a/README.md b/README.md index a08f6b0b..6c6eb93f 100644 --- a/README.md +++ b/README.md @@ -18,14 +18,14 @@ https://github.com/user-attachments/assets/8cad5643-63b2-4641-a5c4-68bc313f20e6 CopilotChat.nvim is a Neovim plugin that brings GitHub Copilot Chat capabilities directly into your editor. It provides: -- πŸ€– GitHub Copilot Chat integration with official model and agent support (GPT-4o, Claude 3.7 Sonnet, Gemini 2.0 Flash, and more) +- πŸ€– GitHub Copilot Chat integration with official model support (GPT-4o, Claude 3.7 Sonnet, Gemini 2.0 Flash, and more) - πŸ’» Rich workspace context powered by smart embeddings system -- πŸ”’ Explicit context sharing - only sends what you specifically request, either as context or selection (by default visual selection) -- πŸ”Œ Modular provider architecture supporting both official and custom LLM backends (Ollama, LM Studio, Mistral.ai and more) +- πŸ”’ Explicit data sharing - only sends what you specifically request, either as resource or selection (by default visual selection) +- πŸ”Œ Modular provider architecture supporting both official and custom LLM backends (Ollama, Gemini, Mistral.ai and more) - πŸ“ Interactive chat UI with completion, diffs and quickfix integration - 🎯 Powerful prompt system with composable templates and sticky prompts -- πŸ”„ Extensible context providers for granular workspace understanding (buffers, files, git diffs, URLs, and more) -- ⚑ Efficient token usage with tiktoken token counting and memory management +- πŸ”„ Extensible function calling system for granular workspace understanding (buffers, files, git diffs, URLs, and more) +- ⚑ Efficient token usage with tiktoken token counting and history management # Requirements @@ -61,8 +61,7 @@ Plugin features that use picker: - `:CopilotChatPrompts` - for selecting prompts - `:CopilotChatModels` - for selecting models -- `:CopilotChatAgents` - for selecting agents -- `#:` - for selecting context input +- `#:` - for selecting function input # Installation @@ -76,7 +75,7 @@ return { { "github/copilot.vim" }, -- or zbirenbaum/copilot.lua { "nvim-lua/plenary.nvim", branch = "master" }, -- for curl, log and async functions }, - build = "make tiktoken", -- Only on MacOS or Linux + build = "make tiktoken", opts = { -- See Configuration section for options }, @@ -147,7 +146,6 @@ Commands are used to control the chat interface: | `:CopilotChatLoad ?` | Load chat history | | `:CopilotChatPrompts` | View/select prompt templates | | `:CopilotChatModels` | View/select available models | -| `:CopilotChatAgents` | View/select available agents | | `:CopilotChat` | Use specific prompt template | ## Key Mappings @@ -252,7 +250,7 @@ Define your own system prompts in the configuration (similar to `prompts`): ### Sticky Prompts -Sticky prompts persist across chat sessions. They're useful for maintaining context or agent selection. They work as follows: +Sticky prompts persist across chat sessions. They're useful for maintaining model or resource selection. They work as follows: 1. Prefix text with `> ` using markdown blockquote syntax 2. The prompt will be copied at the start of every new chat prompt @@ -261,7 +259,7 @@ Sticky prompts persist across chat sessions. They're useful for maintaining cont Examples: ```markdown -> #files +> #glob:`*.lua` > List all files in the workspace > @models Using Mistral-small @@ -273,15 +271,12 @@ You can also set default sticky prompts in the configuration: ```lua { sticky = { - '@models Using Mistral-small', - '#files', + '#glob:*.lua', } } ``` -## Models and Agents - -### Models +## Models You can control which AI model to use in three ways: @@ -294,69 +289,67 @@ For supported models, see: - [Copilot Chat Models](https://docs.github.com/en/copilot/using-github-copilot/ai-models/changing-the-ai-model-for-copilot-chat#ai-models-for-copilot-chat) - [GitHub Marketplace Models](https://github.com/marketplace/models) (experimental, limited usage) -### Agents - -Agents determine the AI assistant's capabilities. Control agents in three ways: - -1. List available agents with `:CopilotChatAgents` -2. Set agent in prompt with `@agent_name` -3. Configure default agent via `agent` config key - -The default "noop" agent is `none`. For more information: - -- [Extension Agents Documentation](https://docs.github.com/en/copilot/using-github-copilot/using-extensions-to-integrate-external-tools-with-copilot-chat) -- [Available Agents](https://github.com/marketplace?type=apps&copilot_app=true) - -## Contexts - -Contexts provide additional information to the chat. Add context using `#context_name[:input]` syntax: - -| Context | Input Support | Description | -| ----------- | ------------- | ----------------------------------- | -| `buffer` | βœ“ (number) | Current or specified buffer content | -| `buffers` | βœ“ (type) | All buffers content (listed/all) | -| `file` | βœ“ (path) | Content of specified file | -| `files` | βœ“ (glob) | Workspace files | -| `filenames` | βœ“ (glob) | Workspace file names | -| `git` | βœ“ (ref) | Git diff (unstaged/staged/commit) | -| `url` | βœ“ (url) | Content from URL | -| `register` | βœ“ (name) | Content of vim register | -| `quickfix` | - | Quickfix list file contents | -| `system` | βœ“ (command) | Output of shell command | - -> [!TIP] -> The AI is aware of these context providers and may request additional context -> if needed by asking you to input a specific context command like `#file:path/to/file.js`. +## Functions + +Functions provide additional information and behaviour to the chat. +Tools can be organized into groups by setting the `group` property. Tools assigned to a group are not automatically made available to the LLM - they must be explicitly activated. +To use grouped tools in your prompt, include `@group_name` in your message. This allows the LLM to access and use all tools in that group during the current interaction. +Add tools using `#tool_name[:input]` syntax: + +| Function | Input Support | Description | +| ------------- | ------------- | ------------------------------------------------------ | +| `buffer` | βœ“ (name) | Retrieves content from a specific buffer | +| `buffers` | βœ“ (scope) | Fetches content from multiple buffers (listed/visible) | +| `diagnostics` | βœ“ (scope) | Collects code diagnostics (errors, warnings) | +| `file` | βœ“ (path) | Reads content from a specified file path | +| `gitdiff` | βœ“ (sha) | Retrieves git diff information (unstaged/staged/sha) | +| `gitstatus` | - | Retrieves git status information | +| `glob` | βœ“ (pattern) | Lists filenames matching a pattern in workspace | +| `grep` | βœ“ (pattern) | Searches for a pattern across files in workspace | +| `quickfix` | - | Includes content of files in quickfix list | +| `register` | βœ“ (register) | Provides access to specified Vim register | +| `url` | βœ“ (url) | Fetches content from a specified URL | Examples: ```markdown -> #buffer -> #buffer:2 -> #files:\*.lua -> #filenames +> #buffer:init.lua +> #buffers:visible +> #diagnostics:current +> #file:path/to/file.js > #git:staged +> #glob:`**/*.lua` +> #grep:`function setup` +> #quickfix +> #register:+ > #url:https://example.com -> #system:`ls -la | grep lua` ``` -Define your own contexts in the configuration with input handling and resolution: +Define your own functions in the configuration with input handling and schema: ```lua { - contexts = { + functions = { birthday = { - input = function(callback) - vim.ui.select({ 'user', 'napoleon' }, { - prompt = 'Select birthday> ', - }, callback) - end, + description = "Retrieves birthday information for a person", + uri = "birthday://{name}", + schema = { + type = 'object', + required = { 'name' }, + properties = { + name = { + type = 'string', + enum = { 'Alice', 'Bob', 'Charlie' }, + description = "Person's name", + }, + }, + }, resolve = function(input) return { { - content = input .. ' birthday info', - filename = input .. '_birthday', - filetype = 'text', + uri = 'birthday://' .. input.name, + mimetype = 'text/plain', + data = input.name .. ' birthday info', } } end @@ -365,9 +358,9 @@ Define your own contexts in the configuration with input handling and resolution } ``` -### External Contexts +### External Functions -For external contexts, see the [contexts discussion page](https://github.com/CopilotC-Nvim/CopilotChat.nvim/discussions/categories/contexts). +For external functions implementations, see the [discussion page](https://github.com/CopilotC-Nvim/CopilotChat.nvim/discussions/categories/functions). ## Selections @@ -429,9 +422,6 @@ Custom providers can implement these methods: -- Optional: Get available models get_models?(headers: table): table, - - -- Optional: Get available agents - get_agents?(headers: table): table, } ``` @@ -453,19 +443,17 @@ Below are all available configuration options with their default values: system_prompt = 'COPILOT_INSTRUCTIONS', -- System prompt to use (can be specified manually in prompt via /). model = 'gpt-4.1', -- Default model to use, see ':CopilotChatModels' for available models (can be specified manually in prompt via $). - agent = 'copilot', -- Default agent to use, see ':CopilotChatAgents' for available agents (can be specified manually in prompt via @). - context = nil, -- Default context or array of contexts to use (can be specified manually in prompt via #). - sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat. + tools = nil, -- Default tool or array of tools (or groups) to share with LLM (can be specified manually in prompt via @). + sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat (can be specified manually in prompt via >). - temperature = 0.1, -- GPT result temperature + temperature = 0.1, -- Result temperature headless = false, -- Do not write to chat buffer and use history (useful for using custom processing) - stream = nil, -- Function called when receiving stream updates (returned string is appended to the chat buffer) - callback = nil, -- Function called when full response is received (retuned string is stored to history) - remember_as_sticky = true, -- Remember model/agent/context as sticky prompts when asking questions + callback = nil, -- Function called when full response is received + remember_as_sticky = true, -- Remember model as sticky prompts when asking questions -- default selection -- see select.lua for implementation - selection = select.visual, + selection = require('CopilotChat.select').visual, -- default window options window = { @@ -483,9 +471,9 @@ Below are all available configuration options with their default values: }, show_help = true, -- Shows help message as virtual lines when waiting for user input + show_folds = true, -- Shows folds for sections in chat highlight_selection = true, -- Highlight selection highlight_headers = true, -- Highlight headers in chat, disable if using markdown renderers (like render-markdown.nvim) - references_display = 'virtual', -- 'virtual', 'write', Display references in chat as virtual text or write to buffer auto_follow_cursor = true, -- Auto-follow cursor in chat auto_insert_mode = false, -- Automatically enter insert mode when opening window and on new prompt insert_at_end = false, -- Move cursor to end of buffer when inserting text @@ -503,129 +491,29 @@ Below are all available configuration options with their default values: log_path = vim.fn.stdpath('state') .. '/CopilotChat.log', -- Default path to log file history_path = vim.fn.stdpath('data') .. '/copilotchat_history', -- Default path to stored history - question_header = '# User ', -- Header to use for user questions - answer_header = '# Copilot ', -- Header to use for AI answers - error_header = '# Error ', -- Header to use for errors + headers = { + user = '## User ', -- Header to use for user questions + assistant = '## Copilot ', -- Header to use for AI answers + tool = '## Tool ', -- Header to use for tool calls + }, + separator = '───', -- Separator to use in chat -- default providers -- see config/providers.lua for implementation - providers = { - copilot = { - }, - github_models = { - }, - copilot_embeddings = { - }, - }, + providers = require('CopilotChat.config.providers'), - -- default contexts - -- see config/contexts.lua for implementation - contexts = { - buffer = { - }, - buffers = { - }, - file = { - }, - files = { - }, - git = { - }, - url = { - }, - register = { - }, - quickfix = { - }, - system = { - } - }, + -- default functions + -- see config/functions.lua for implementation + functions = require('CopilotChat.config.functions'), -- default prompts -- see config/prompts.lua for implementation - prompts = { - Explain = { - prompt = 'Write an explanation for the selected code as paragraphs of text.', - system_prompt = 'COPILOT_EXPLAIN', - }, - Review = { - prompt = 'Review the selected code.', - system_prompt = 'COPILOT_REVIEW', - }, - Fix = { - prompt = 'There is a problem in this code. Identify the issues and rewrite the code with fixes. Explain what was wrong and how your changes address the problems.', - }, - Optimize = { - prompt = 'Optimize the selected code to improve performance and readability. Explain your optimization strategy and the benefits of your changes.', - }, - Docs = { - prompt = 'Please add documentation comments to the selected code.', - }, - Tests = { - prompt = 'Please generate tests for my code.', - }, - Commit = { - prompt = 'Write commit message for the change with commitizen convention. Keep the title under 50 characters and wrap message at 72 characters. Format as a gitcommit code block.', - context = 'git:staged', - }, - }, + prompts = require('CopilotChat.config.prompts'), -- default mappings -- see config/mappings.lua for implementation - mappings = { - complete = { - insert = '', - }, - close = { - normal = 'q', - insert = '', - }, - reset = { - normal = '', - insert = '', - }, - submit_prompt = { - normal = '', - insert = '', - }, - toggle_sticky = { - normal = 'grr', - }, - clear_stickies = { - normal = 'grx', - }, - accept_diff = { - normal = '', - insert = '', - }, - jump_to_diff = { - normal = 'gj', - }, - quickfix_answers = { - normal = 'gqa', - }, - quickfix_diffs = { - normal = 'gqd', - }, - yank_diff = { - normal = 'gy', - register = '"', -- Default register to use for yanking - }, - show_diff = { - normal = 'gd', - full_diff = false, -- Show full diff instead of unified diff when showing diff window - }, - show_info = { - normal = 'gi', - }, - show_context = { - normal = 'gc', - }, - show_help = { - normal = 'gh', - }, - }, + mappings = require('CopilotChat.config.mappings'), } ``` @@ -659,8 +547,8 @@ Types of copilot highlights: - `CopilotChatStatus` - Status and spinner in chat buffer - `CopilotChatHelp` - Help messages in chat buffer (help, references) - `CopilotChatSelection` - Selection highlight in source buffer -- `CopilotChatKeyword` - Keyword highlight in chat buffer (e.g. prompts, contexts) -- `CopilotChatInput` - Input highlight in chat buffer (for contexts) +- `CopilotChatKeyword` - Keyword highlight in chat buffer (e.g. prompts, tools) +- `CopilotChatAnnotation` - Annotation highlight in chat buffer (file headers, tool call headers, tool call body) # API Reference @@ -673,8 +561,7 @@ local chat = require("CopilotChat") chat.ask(prompt, config) -- Ask a question with optional config chat.response() -- Get the last response text chat.resolve_prompt() -- Resolve prompt references -chat.resolve_context() -- Resolve context embeddings (WARN: async, requires plenary.async.run) -chat.resolve_agent() -- Resolve agent from prompt (WARN: async, requires plenary.async.run) +chat.resolve_tools() -- Resolve tools that are available for automatic use by LLM chat.resolve_model() -- Resolve model from prompt (WARN: async, requires plenary.async.run) -- Window Management @@ -692,10 +579,9 @@ chat.set_source(winnr) -- Set the source window chat.get_selection() -- Get the current selection chat.set_selection(bufnr, start_line, end_line, clear) -- Set or clear selection --- Prompt & Context Management +-- Prompt & Model Management chat.select_prompt(config) -- Open prompt selector with optional config chat.select_model() -- Open model selector -chat.select_agent() -- Open agent selector chat.prompts() -- Get all available prompts -- Completion @@ -723,10 +609,12 @@ local window = require("CopilotChat").chat window:visible() -- Check if chat window is visible window:focused() -- Check if chat window is focused +-- Message Management +window:get_message(role) -- Get last chat message by role (user, assistant, tool) +window:add_message({ role, content }, replace) -- Add or replace a message in chat +window:add_sticky(sticky) -- Add sticky prompt to chat message + -- Content Management -window:get_prompt() -- Get current prompt from chat window -window:set_prompt(prompt) -- Set prompt in chat window -window:add_sticky(sticky) -- Add sticky prompt to chat window window:append(text) -- Append text to chat window window:clear() -- Clear chat window content window:finish() -- Finish writing to chat window @@ -736,9 +624,9 @@ window:follow() -- Move cursor to end of chat content window:focus() -- Focus the chat window -- Advanced Features -window:get_closest_section() -- Get section closest to cursor -window:get_closest_block() -- Get code block closest to cursor -window:overlay(opts) -- Show overlay with specified options +window:get_closest_message(role) -- Get message closest to cursor +window:get_closest_block(role) -- Get code block closest to cursor +window:overlay(opts) -- Show overlay with specified options ``` ## Example Usage @@ -746,19 +634,18 @@ window:overlay(opts) -- Show overlay with specified options ```lua -- Open chat, ask a question and handle response require("CopilotChat").open() -require("CopilotChat").ask("Explain this code", { +require("CopilotChat").ask("#buffer Explain this code", { callback = function(response) vim.notify("Got response: " .. response:sub(1, 50) .. "...") return response end, - context = "buffer" }) -- Save and load chat history require("CopilotChat").save("my_debugging_session") require("CopilotChat").load("my_debugging_session") --- Use custom context and model +-- Use custom sticky and model require("CopilotChat").ask("How can I optimize this?", { model = "gpt-4.1", context = {"buffer", "git:staged"} diff --git a/lua/CopilotChat/actions.lua b/lua/CopilotChat/actions.lua deleted file mode 100644 index 2ab795b7..00000000 --- a/lua/CopilotChat/actions.lua +++ /dev/null @@ -1,49 +0,0 @@ ----@class CopilotChat.integrations.actions ----@field prompt string: The prompt to display ----@field actions table: A table with the actions to pick from - -local chat = require('CopilotChat') - -local M = {} - ---- User prompt actions ----@param config CopilotChat.config.shared?: The chat configuration ----@return CopilotChat.integrations.actions?: The prompt actions ----@deprecated Use |CopilotChat.select_prompt| instead -function M.prompt_actions(config) - local actions = {} - for name, prompt in pairs(chat.prompts()) do - if prompt.prompt then - actions[name] = vim.tbl_extend('keep', prompt, config or {}) - end - end - return { - prompt = 'Copilot Chat Prompt Actions', - actions = actions, - } -end - ---- Pick an action from a list of actions ----@param pick_actions CopilotChat.integrations.actions?: A table with the actions to pick from ----@param opts table?: vim.ui.select options ----@deprecated Use |CopilotChat.select_prompt| instead -function M.pick(pick_actions, opts) - if not pick_actions or not pick_actions.actions or vim.tbl_isempty(pick_actions.actions) then - return - end - - opts = vim.tbl_extend('force', { - prompt = pick_actions.prompt .. '> ', - }, opts or {}) - - vim.ui.select(vim.tbl_keys(pick_actions.actions), opts, function(selected) - if not selected then - return - end - vim.defer_fn(function() - chat.ask(pick_actions.actions[selected].prompt, pick_actions.actions[selected]) - end, 100) - end) -end - -return M diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index dddd1b89..b6460d0a 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -1,19 +1,56 @@ ----@class CopilotChat.Client.ask +---@class CopilotChat.client.AskOptions ---@field headless boolean ----@field contexts table? ----@field selection CopilotChat.select.selection? ----@field embeddings table? +---@field history table +---@field selection CopilotChat.select.Selection? +---@field tools table? +---@field resources table? ---@field system_prompt string ---@field model string ----@field agent string? ---@field temperature number ---@field on_progress? fun(response: string):nil ----@class CopilotChat.Client.model : CopilotChat.Provider.model ----@field provider string - ----@class CopilotChat.Client.agent : CopilotChat.Provider.agent ----@field provider string +---@class CopilotChat.client.Message +---@field role string +---@field content string +---@field tool_call_id string? +---@field tool_calls table? + +---@class CopilotChat.client.AskResponse +---@field message CopilotChat.client.Message +---@field token_count number +---@field token_max_count number + +---@class CopilotChat.client.ToolCall +---@field id number +---@field index number +---@field name string +---@field arguments string + +---@class CopilotChat.client.Tool +---@field name string name of the tool +---@field description string description of the tool +---@field schema table? schema of the tool + +---@class CopilotChat.client.Embed +---@field index number +---@field embedding table + +---@class CopilotChat.client.Resource +---@field name string +---@field type string +---@field data string + +---@class CopilotChat.client.EmbeddedResource : CopilotChat.client.Resource, CopilotChat.client.Embed + +---@class CopilotChat.client.Model +---@field provider string? +---@field id string +---@field name string +---@field tokenizer string? +---@field max_input_tokens number? +---@field max_output_tokens number? +---@field streaming boolean? +---@field tools boolean? local log = require('plenary.log') local tiktoken = require('CopilotChat.tiktoken') @@ -22,7 +59,7 @@ local utils = require('CopilotChat.utils') local class = utils.class --- Constants -local CONTEXT_FORMAT = '[#file:%s](#file:%s-context)' +local RESOURCE_FORMAT = '# %s\n```%s\n%s\n```' local LINE_CHARACTERS = 100 local BIG_FILE_THRESHOLD = 1000 * LINE_CHARACTERS local BIG_EMBED_THRESHOLD = 200 * LINE_CHARACTERS @@ -30,8 +67,8 @@ local TRUNCATED = '... (truncated)' --- Resolve provider function ---@param model string ----@param models table ----@param providers table +---@param models table +---@param providers table ---@return string, function local function resolve_provider_function(name, model, models, providers) local model_config = models[model] @@ -65,23 +102,18 @@ local function resolve_provider_function(name, model, models, providers) end --- Generate content block with line numbers, truncating if necessary ----@param content string: The content ----@param outline string?: The outline +---@param content string ---@param threshold number: The threshold for truncation ----@param start_line number|nil: The starting line number +---@param start_line number?: The starting line number ---@return string -local function generate_content_block(content, outline, threshold, start_line) +local function generate_content_block(content, threshold, start_line) local total_chars = #content - if total_chars > threshold and outline then - content = outline - total_chars = #content - end if total_chars > threshold then content = content:sub(1, threshold) content = content .. '\n' .. TRUNCATED end - if start_line ~= -1 then + if start_line ~= nil then local lines = vim.split(content, '\n') local total_lines = #lines local max_length = #tostring(total_lines) @@ -96,44 +128,19 @@ local function generate_content_block(content, outline, threshold, start_line) return content end ---- Generate diagnostics message ----@param diagnostics table ----@return string -local function generate_diagnostics(diagnostics) - local out = {} - for _, diagnostic in ipairs(diagnostics) do - table.insert( - out, - string.format( - '%s line=%d-%d: %s', - diagnostic.severity, - diagnostic.start_line, - diagnostic.end_line, - diagnostic.content - ) - ) - end - return table.concat(out, '\n') -end - --- Generate messages for the given selection ---- @param selection CopilotChat.select.selection? ---- @return table -local function generate_selection_messages(selection) - if not selection then - return {} - end - +--- @param selection CopilotChat.select.Selection +--- @return CopilotChat.client.Message? +local function generate_selection_message(selection) local filename = selection.filename or 'unknown' local filetype = selection.filetype or 'text' local content = selection.content if not content or content == '' then - return {} + return nil end - local out = string.format('# FILE:%s CONTEXT\n', filename:upper()) - out = out .. "User's active selection:\n" + local out = "User's active selection:\n" if selection.start_line and selection.end_line then out = out .. string.format('Excerpt from %s, lines %s to %s:\n', filename, selection.start_line, selection.end_line) end @@ -141,103 +148,45 @@ local function generate_selection_messages(selection) .. string.format( '```%s\n%s\n```', filetype, - generate_content_block(content, nil, BIG_FILE_THRESHOLD, selection.start_line) + generate_content_block(content, BIG_FILE_THRESHOLD, selection.start_line) ) - if selection.diagnostics then - out = out - .. string.format("\nDiagnostics in user's active selection:\n%s", generate_diagnostics(selection.diagnostics)) - end - return { - { - name = filename, - context = string.format(CONTEXT_FORMAT, filename, filename), - content = out, - role = 'user', - }, + content = out, + role = 'user', } end ---- Generate messages for the given embeddings ---- @param embeddings table? ---- @return table -local function generate_embeddings_messages(embeddings) - if not embeddings then - return {} - end - - return vim.tbl_map(function(embedding) - local out = string.format( - '# FILE:%s CONTEXT\n```%s\n%s\n```', - embedding.filename:upper(), - embedding.filetype or 'text', - generate_content_block(embedding.content, embedding.outline, BIG_FILE_THRESHOLD) - ) - - if embedding.diagnostics then - out = out - .. string.format( - '\nFILE:%s DIAGNOSTICS:\n%s', - embedding.filename:upper(), - generate_diagnostics(embedding.diagnostics) - ) - end - - return { - name = embedding.filename, - context = string.format(CONTEXT_FORMAT, embedding.filename, embedding.filename), - content = out, - role = 'user', - } - end, embeddings) +--- Generate messages for the given resources +--- @param resources CopilotChat.client.Resource[] +--- @return table +local function generate_resource_messages(resources) + return vim + .iter(resources or {}) + :filter(function(resource) + return resource.data and resource.data ~= '' + end) + :map(function(resource) + local content = generate_content_block(resource.data, BIG_FILE_THRESHOLD, 1) + + return { + content = string.format(RESOURCE_FORMAT, resource.name, resource.type, content), + role = 'user', + } + end) + :totable() end --- Generate ask request ---- @param history table ---- @param contexts table? --- @param prompt string --- @param system_prompt string ---- @param generated_messages table -local function generate_ask_request(history, contexts, prompt, system_prompt, generated_messages) +--- @param history table +--- @param generated_messages table +local function generate_ask_request(prompt, system_prompt, history, generated_messages) local messages = {} system_prompt = vim.trim(system_prompt) - -- Include context help - if contexts and not vim.tbl_isempty(contexts) then - local help_text = [[When you need additional context, request it using this format: - -> #:`` - -Examples: -> #file:`path/to/file.js` (loads specific file) -> #buffers:`visible` (loads all visible buffers) -> #git:`staged` (loads git staged changes) -> #system:`uname -a` (loads system information) - -Guidelines: -- Always request context when needed rather than guessing about files or code -- Use the > format on a new line when requesting context -- Output context commands directly - never ask if the user wants to provide information -- Assume the user will provide requested context in their next response - -Available context providers and their usage:]] - - local context_names = vim.tbl_keys(contexts) - table.sort(context_names) - for _, name in ipairs(context_names) do - local description = contexts[name] - description = description:gsub('\n', '\n ') - help_text = help_text .. '\n\n - #' .. name .. ': ' .. description - end - - if system_prompt ~= '' then - system_prompt = system_prompt .. '\n\n' - end - system_prompt = system_prompt .. help_text - end - -- Include system prompt if not utils.empty(system_prompt) then table.insert(messages, { @@ -246,74 +195,48 @@ Available context providers and their usage:]] }) end - local context_references = {} - - -- Include embeddings and history + -- Include generated messages and history for _, message in ipairs(generated_messages) do table.insert(messages, { content = message.content, role = message.role, }) - - if message.context then - context_references[message.context] = true - end end for _, message in ipairs(history) do table.insert(messages, message) end - - -- Include context references - prompt = vim.trim(prompt) - if not vim.tbl_isempty(context_references) then - if prompt ~= '' then - prompt = '\n\n' .. prompt - end - prompt = table.concat(vim.tbl_keys(context_references), '\n') .. prompt - end - - -- Include user prompt - if not utils.empty(prompt) then + if not utils.empty(prompt) and utils.empty(history) then + -- Include user prompt if we have no history table.insert(messages, { content = prompt, role = 'user', }) end - log.debug('System prompt:\n', system_prompt) - log.debug('Prompt:\n', prompt) return messages end --- Generate embedding request ---- @param inputs table +--- @param inputs table --- @param threshold number --- @return table local function generate_embedding_request(inputs, threshold) return vim.tbl_map(function(embedding) - local content = generate_content_block(embedding.outline or embedding.content, nil, threshold, -1) - if embedding.filetype == 'raw' then - return content - else - return string.format('File: `%s`\n```%s\n%s\n```', embedding.filename, embedding.filetype, content) - end + local content = generate_content_block(embedding.data, threshold) + return string.format(RESOURCE_FORMAT, embedding.name, embedding.type, content) end, inputs) end ----@class CopilotChat.Client : Class ----@field history table ----@field providers table ----@field provider_cache table ----@field models table? ----@field agents table? ----@field current_job string? ----@field headers table? +---@class CopilotChat.client.Client : Class +---@field private providers table +---@field private provider_cache table +---@field private models table? +---@field private current_job string? +---@field private headers table? local Client = class(function(self) - self.history = {} self.providers = {} self.provider_cache = {} self.models = nil - self.agents = nil self.current_job = nil self.headers = nil end) @@ -336,7 +259,7 @@ function Client:authenticate(provider_name) end --- Fetch models from the Copilot API ----@return table +---@return table function Client:fetch_models() if self.models then return self.models @@ -372,67 +295,23 @@ function Client:fetch_models() end end - log.debug('Fetched models:', vim.inspect(models)) + log.debug('Fetched models:', #vim.tbl_keys(models)) self.models = models return self.models end ---- Fetch agents from the Copilot API ----@return table -function Client:fetch_agents() - if self.agents then - return self.agents - end - - local agents = {} - local provider_order = vim.tbl_keys(self.providers) - table.sort(provider_order) - for _, provider_name in ipairs(provider_order) do - local provider = self.providers[provider_name] - if not provider.disabled and provider.get_agents then - notify.publish(notify.STATUS, 'Fetching agents from ' .. provider_name) - local ok, headers = pcall(self.authenticate, self, provider_name) - if not ok then - log.warn('Failed to authenticate with ' .. provider_name .. ': ' .. headers) - goto continue - end - local ok, provider_agents = pcall(provider.get_agents, headers) - if not ok then - log.warn('Failed to fetch agents from ' .. provider_name .. ': ' .. provider_agents) - goto continue - end - - for _, agent in ipairs(provider_agents) do - agent.provider = provider_name - if agents[agent.id] then - agent.id = agent.id .. ':' .. provider_name - end - agents[agent.id] = agent - end - - ::continue:: - end - end - - self.agents = agents - return self.agents -end - --- Ask a question to Copilot ---@param prompt string: The prompt to send to Copilot ----@param opts CopilotChat.Client.ask: Options for the request ----@return string?, table?, number?, number? +---@param opts CopilotChat.client.AskOptions: Options for the request +---@return CopilotChat.client.AskResponse? function Client:ask(prompt, opts) opts = opts or {} - - if opts.agent == 'none' or opts.agent == 'copilot' then - opts.agent = nil - end - local job_id = utils.uuid() log.debug('Model:', opts.model) - log.debug('Agent:', opts.agent) + log.debug('Tools:', #opts.tools) + log.debug('Resources:', #opts.resources) + log.debug('History:', #opts.history) local models = self:fetch_models() local model_config = models[opts.model] @@ -440,12 +319,6 @@ function Client:ask(prompt, opts) error('Model not found: ' .. opts.model) end - local agents = self:fetch_agents() - local agent_config = opts.agent and agents[opts.agent] - if opts.agent and not agent_config then - error('Agent not found: ' .. opts.agent) - end - local provider_name = model_config.provider if not provider_name then error('Provider not found for model: ' .. opts.model) @@ -459,10 +332,8 @@ function Client:ask(prompt, opts) model = vim.tbl_extend('force', model_config, { id = opts.model:gsub(':' .. provider_name .. '$', ''), }), - agent = agent_config and vim.tbl_extend('force', agent_config, { - id = opts.agent and opts.agent:gsub(':' .. provider_name .. '$', ''), - }), temperature = opts.temperature, + tools = opts.tools, } local max_tokens = model_config.max_input_tokens @@ -477,37 +348,26 @@ function Client:ask(prompt, opts) notify.publish(notify.STATUS, 'Generating request') end - local history = not opts.headless and vim.list_slice(self.history) or {} - local references = utils.ordered_map() + local history = not opts.headless and vim.deepcopy(opts.history) or {} + local tool_calls = utils.ordered_map() local generated_messages = {} - local selection_messages = generate_selection_messages(opts.selection) - local embeddings_messages = generate_embeddings_messages(opts.embeddings) - - for _, message in ipairs(selection_messages) do - table.insert(generated_messages, message) - references:set(message.name, { - name = utils.filename(message.name), - url = message.name, - }) + local selection_message = opts.selection and generate_selection_message(opts.selection) + local resource_messages = generate_resource_messages(opts.resources) + + if selection_message then + table.insert(generated_messages, selection_message) end if max_tokens then - -- Count tokens from selection messages - local selection_tokens = 0 - for _, message in ipairs(selection_messages) do - selection_tokens = selection_tokens + tiktoken.count(message.content) - end - -- Count required tokens that we cannot reduce + local selection_tokens = selection_message and tiktoken.count(selection_message.content) or 0 local prompt_tokens = tiktoken.count(prompt) local system_tokens = tiktoken.count(opts.system_prompt) - local required_tokens = prompt_tokens + system_tokens + selection_tokens - - -- Reserve space for first embedding - local reserved_tokens = #embeddings_messages > 0 and tiktoken.count(embeddings_messages[1].content) or 0 + local resource_tokens = #resource_messages > 0 and tiktoken.count(resource_messages[1].content) or 0 + local required_tokens = prompt_tokens + system_tokens + selection_tokens + resource_tokens -- Calculate how many tokens we can use for history - local history_limit = max_tokens - required_tokens - reserved_tokens + local history_limit = max_tokens - required_tokens local history_tokens = 0 for _, msg in ipairs(history) do history_tokens = history_tokens + tiktoken.count(msg.content) @@ -521,35 +381,25 @@ function Client:ask(prompt, opts) -- Now add as many files as possible with remaining token budget local remaining_tokens = max_tokens - required_tokens - history_tokens - for _, message in ipairs(embeddings_messages) do + for _, message in ipairs(resource_messages) do local tokens = tiktoken.count(message.content) if remaining_tokens - tokens >= 0 then remaining_tokens = remaining_tokens - tokens table.insert(generated_messages, message) - references:set(message.name, { - name = utils.filename(message.name), - url = message.name, - }) else break end end else -- Add all embedding messages as we cant limit them - for _, message in ipairs(embeddings_messages) do + for _, message in ipairs(resource_messages) do table.insert(generated_messages, message) - references:set(message.name, { - name = utils.filename(message.name), - url = message.name, - }) end end - log.debug('References:', #generated_messages) - - local last_message = nil local errored = false local finished = false + local token_count = 0 local response_buffer = utils.string_buffer() local function finish_stream(err, job) @@ -571,7 +421,6 @@ function Client:ask(prompt, opts) return end - log.debug('Response line:', line) if not opts.headless then notify.publish(notify.STATUS, '') end @@ -589,11 +438,19 @@ function Client:ask(prompt, opts) end local out = provider.prepare_output(content, options) - last_message = out - if out.references then - for _, reference in ipairs(out.references) do - references:set(reference.name, reference) + if out.total_tokens then + token_count = out.total_tokens + end + + if out.tool_calls then + for _, tool_call in ipairs(out.tool_calls) do + local val = tool_calls:get(tool_call.index) + if not val then + tool_calls:set(tool_call.index, tool_call) + else + val.arguments = val.arguments .. tool_call.arguments + end end end @@ -606,7 +463,7 @@ function Client:ask(prompt, opts) if out.finish_reason then local reason = out.finish_reason - if reason == 'stop' then + if reason == 'stop' or reason == 'tool_calls' then reason = nil else reason = 'Early stop: ' .. reason @@ -656,10 +513,8 @@ function Client:ask(prompt, opts) end local headers = self:authenticate(provider_name) - local request = provider.prepare_input( - generate_ask_request(history, opts.contexts, prompt, opts.system_prompt, generated_messages), - options - ) + local request = + provider.prepare_input(generate_ask_request(prompt, opts.system_prompt, history, generated_messages), options) local is_stream = request.stream local args = { @@ -681,12 +536,6 @@ function Client:ask(prompt, opts) self.current_job = nil end - if response then - log.debug('Response status:', response.status) - log.debug('Response body:\n', response.body) - log.debug('Response headers:\n', response.headers) - end - if err then local error_msg = 'Failed to get response: ' .. err @@ -716,7 +565,7 @@ function Client:ask(prompt, opts) if response then if is_stream then - if utils.empty(response_text) then + if utils.empty(response_text) and not finished then for _, line in ipairs(vim.split(response.body, '\n')) do parse_stream_line(line) end @@ -727,12 +576,15 @@ function Client:ask(prompt, opts) response_text = response_buffer:tostring() end - if utils.empty(response_text) then - error('Failed to get response: empty response') - return - end - - return response_text, references:values(), last_message and last_message.total_tokens or 0, max_tokens + return { + message = { + role = 'assistant', + content = response_text, + tool_calls = #tool_calls:values() > 0 and tool_calls:values() or nil, + }, + token_count = token_count, + token_max_count = max_tokens, + } end --- List available models @@ -755,40 +607,20 @@ function Client:list_models() end, result) end ---- List available agents ----@return table -function Client:list_agents() - local agents = self:fetch_agents() - local result = vim.tbl_keys(agents) - - table.sort(result, function(a, b) - a = agents[a] - b = agents[b] - if a.provider ~= b.provider then - return a.provider < b.provider - end - return a.id < b.id - end) - - local out = vim.tbl_map(function(id) - return agents[id] - end, result) - table.insert(out, 1, { id = 'none', name = 'None', description = 'No agent', provider = 'none' }) - return out -end - --- Generate embeddings for the given inputs ----@param inputs table: The inputs to embed +---@param inputs table: The inputs to embed ---@param model string ----@return table +---@return table function Client:embed(inputs, model) if not inputs or #inputs == 0 then + ---@diagnostic disable-next-line: return-type-mismatch return inputs end local models = self:fetch_models() local ok, provider_name, embed = pcall(resolve_provider_function, 'embed', model, models, self.providers) if not ok then + ---@diagnostic disable-next-line: return-type-mismatch return inputs end @@ -867,14 +699,6 @@ function Client:stop() return false end ---- Reset the history and stop any running job ----@return boolean -function Client:reset() - local stopped = self:stop() - self.history = {} - return stopped -end - --- Check if there is a running job ---@return boolean function Client:running() @@ -889,5 +713,5 @@ function Client:load_providers(providers) end end ---- @type CopilotChat.Client +--- @type CopilotChat.client.Client return Client() diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 30c428e5..5e900219 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -1,8 +1,6 @@ -local select = require('CopilotChat.select') - ---@alias CopilotChat.config.Layout 'vertical'|'horizontal'|'float'|'replace' ----@class CopilotChat.config.window +---@class CopilotChat.config.Window ---@field layout? CopilotChat.config.Layout|fun():CopilotChat.config.Layout ---@field relative 'editor'|'win'|'cursor'|'mouse'? ---@field border 'none'|'single'|'double'|'rounded'|'solid'|'shadow'? @@ -14,32 +12,28 @@ local select = require('CopilotChat.select') ---@field footer string? ---@field zindex number? ----@class CopilotChat.config.shared +---@class CopilotChat.config.Shared ---@field system_prompt string? ---@field model string? ----@field agent string? ----@field context string|table|nil +---@field tools string|table|nil ---@field sticky string|table|nil ---@field temperature number? ---@field headless boolean? ----@field stream nil|fun(chunk: string, source: CopilotChat.source):string ----@field callback nil|fun(response: string, source: CopilotChat.source):string +---@field callback nil|fun(response: string, source: CopilotChat.source) ---@field remember_as_sticky boolean? ----@field include_contexts_in_prompt boolean? ----@field selection false|nil|fun(source: CopilotChat.source):CopilotChat.select.selection? ----@field window CopilotChat.config.window? +---@field selection false|nil|fun(source: CopilotChat.source):CopilotChat.select.Selection? +---@field window CopilotChat.config.Window? ---@field show_help boolean? ---@field show_folds boolean? ---@field highlight_selection boolean? ---@field highlight_headers boolean? ----@field references_display 'virtual'|'write'? ---@field auto_follow_cursor boolean? ---@field auto_insert_mode boolean? ---@field insert_at_end boolean? ---@field clear_chat_on_new_prompt boolean? --- CopilotChat default configuration ----@class CopilotChat.config : CopilotChat.config.shared +---@class CopilotChat.config.Config : CopilotChat.config.Shared ---@field debug boolean? ---@field log_level 'trace'|'debug'|'info'|'warn'|'error'|'fatal'? ---@field proxy string? @@ -47,13 +41,11 @@ local select = require('CopilotChat.select') ---@field chat_autocomplete boolean? ---@field log_path string? ---@field history_path string? ----@field question_header string? ----@field answer_header string? ----@field error_header string? +---@field headers table? ---@field separator string? ----@field providers table? ----@field contexts table? ----@field prompts table? +---@field providers table? +---@field functions table? +---@field prompts table? ---@field mappings CopilotChat.config.mappings? return { @@ -62,20 +54,16 @@ return { system_prompt = 'COPILOT_INSTRUCTIONS', -- System prompt to use (can be specified manually in prompt via /). model = 'gpt-4.1', -- Default model to use, see ':CopilotChatModels' for available models (can be specified manually in prompt via $). - agent = 'none', -- Default agent to use, see ':CopilotChatAgents' for available agents (can be specified manually in prompt via @). - context = nil, -- Default context or array of contexts to use (can be specified manually in prompt via #). - sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat. + tools = nil, -- Default tool or array of tools (or groups) to share with LLM (can be specified manually in prompt via @). + sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat (can be specified manually in prompt via >). - temperature = 0.1, -- GPT result temperature + temperature = 0.1, -- Result temperature headless = false, -- Do not write to chat buffer and use history (useful for using custom processing) - stream = nil, -- Function called when receiving stream updates (returned string is appended to the chat buffer) - callback = nil, -- Function called when full response is received (retuned string is stored to history) - remember_as_sticky = true, -- Remember model/agent/context as sticky prompts when asking questions - - include_contexts_in_prompt = true, -- Include contexts in prompt + callback = nil, -- Function called when full response is received + remember_as_sticky = true, -- Remember model as sticky prompts when asking questions -- default selection - selection = select.visual, + selection = require('CopilotChat.select').visual, -- default window options window = { @@ -96,7 +84,6 @@ return { show_folds = true, -- Shows folds for sections in chat highlight_selection = true, -- Highlight selection highlight_headers = true, -- Highlight headers in chat, disable if using markdown renderers (like render-markdown.nvim) - references_display = 'virtual', -- 'virtual', 'write', Display references in chat as virtual text or write to buffer auto_follow_cursor = true, -- Auto-follow cursor in chat auto_insert_mode = false, -- Automatically enter insert mode when opening window and on new prompt insert_at_end = false, -- Move cursor to end of buffer when inserting text @@ -114,16 +101,19 @@ return { log_path = vim.fn.stdpath('state') .. '/CopilotChat.log', -- Default path to log file history_path = vim.fn.stdpath('data') .. '/copilotchat_history', -- Default path to stored history - question_header = '## User ', -- Header to use for user questions - answer_header = '## Copilot ', -- Header to use for AI answers - error_header = '## Error ', -- Header to use for errors + headers = { + user = '## User ', -- Header to use for user questions + assistant = '## Copilot ', -- Header to use for AI answers + tool = '## Tool ', -- Header to use for tool calls + }, + separator = '───', -- Separator to use in chat -- default providers providers = require('CopilotChat.config.providers'), - -- default contexts - contexts = require('CopilotChat.config.contexts'), + -- default functions + functions = require('CopilotChat.config.functions'), -- default prompts prompts = require('CopilotChat.config.prompts'), diff --git a/lua/CopilotChat/config/contexts.lua b/lua/CopilotChat/config/contexts.lua deleted file mode 100644 index 64758f76..00000000 --- a/lua/CopilotChat/config/contexts.lua +++ /dev/null @@ -1,352 +0,0 @@ -local context = require('CopilotChat.context') -local utils = require('CopilotChat.utils') - ----@class CopilotChat.config.context ----@field description string? ----@field input fun(callback: fun(input: string?), source: CopilotChat.source)? ----@field resolve fun(input: string?, source: CopilotChat.source, prompt: string):table - ----@type table -return { - buffer = { - description = 'Includes specified buffer in chat context. Supports input (default current).', - input = function(callback) - vim.ui.select( - vim.tbl_map( - function(buf) - return { id = buf, name = vim.fn.fnamemodify(vim.api.nvim_buf_get_name(buf), ':p:.') } - end, - vim.tbl_filter(function(buf) - return utils.buf_valid(buf) and vim.fn.buflisted(buf) == 1 - end, vim.api.nvim_list_bufs()) - ), - { - prompt = 'Select a buffer> ', - format_item = function(item) - return item.name - end, - }, - function(choice) - callback(choice and choice.id) - end - ) - end, - resolve = function(input, source) - input = input and tonumber(input) or source.bufnr - - utils.schedule_main() - return { - context.get_buffer(input), - } - end, - }, - - buffers = { - description = 'Includes all buffers in chat context. Supports input (default listed).', - input = function(callback) - vim.ui.select({ 'listed', 'visible' }, { - prompt = 'Select buffer scope> ', - }, callback) - end, - resolve = function(input) - input = input or 'listed' - - utils.schedule_main() - return vim.tbl_map( - context.get_buffer, - vim.tbl_filter(function(b) - return utils.buf_valid(b) and vim.fn.buflisted(b) == 1 and (input == 'listed' or #vim.fn.win_findbuf(b) > 0) - end, vim.api.nvim_list_bufs()) - ) - end, - }, - - file = { - description = 'Includes content of provided file in chat context. Supports input.', - input = function(callback, source) - local files = utils.scan_dir(source.cwd(), { - max_count = 0, - }) - - utils.schedule_main() - vim.ui.select(files, { - prompt = 'Select a file> ', - }, callback) - end, - resolve = function(input) - if not input or input == '' then - return {} - end - - utils.schedule_main() - return { - context.get_file(utils.filepath(input), utils.filetype(input)), - } - end, - }, - - files = { - description = 'Includes all non-hidden files in the current workspace in chat context. Supports input (glob pattern).', - input = function(callback) - vim.ui.input({ - prompt = 'Enter glob> ', - }, callback) - end, - resolve = function(input, source) - local files = utils.scan_dir(source.cwd(), { - glob = input, - }) - - utils.schedule_main() - files = vim.tbl_filter( - function(file) - return file.ft ~= nil - end, - vim.tbl_map(function(file) - return { - name = utils.filepath(file), - ft = utils.filetype(file), - } - end, files) - ) - - return vim - .iter(files) - :map(function(file) - return context.get_file(file.name, file.ft) - end) - :filter(function(file_data) - return file_data ~= nil - end) - :totable() - end, - }, - - filenames = { - description = 'Includes names of all non-hidden files in the current workspace in chat context. Supports input (glob pattern).', - input = function(callback) - vim.ui.input({ - prompt = 'Enter glob> ', - }, callback) - end, - resolve = function(input, source) - local out = {} - local files = utils.scan_dir(source.cwd(), { - glob = input, - }) - - local chunk_size = 100 - for i = 1, #files, chunk_size do - local chunk = {} - for j = i, math.min(i + chunk_size - 1, #files) do - table.insert(chunk, files[j]) - end - - local chunk_number = math.floor(i / chunk_size) - local chunk_name = chunk_number == 0 and 'file_map' or 'file_map' .. tostring(chunk_number) - - table.insert(out, { - content = table.concat(chunk, '\n'), - filename = chunk_name, - filetype = 'text', - score = 0.1, - }) - end - - return out - end, - }, - - git = { - description = 'Requires `git`. Includes current git diff in chat context. Supports input (default unstaged, also accepts commit number).', - input = function(callback) - vim.ui.select({ 'unstaged', 'staged' }, { - prompt = 'Select diff type> ', - }, callback) - end, - resolve = function(input, source) - input = input or 'unstaged' - local cmd = { - 'git', - '-C', - source.cwd(), - 'diff', - '--no-color', - '--no-ext-diff', - } - - if input == 'staged' then - table.insert(cmd, '--staged') - elseif input == 'unstaged' then - table.insert(cmd, '--') - else - table.insert(cmd, input) - end - - local out = utils.system(cmd) - - return { - { - content = out.stdout, - filename = 'git_diff_' .. input, - filetype = 'diff', - }, - } - end, - }, - - url = { - description = 'Includes content of provided URL in chat context. Supports input.', - input = function(callback) - vim.ui.input({ - prompt = 'Enter URL> ', - default = 'https://', - }, callback) - end, - resolve = function(input) - return { - context.get_url(input), - } - end, - }, - - register = { - description = 'Includes contents of register in chat context. Supports input (default +, e.g clipboard).', - input = function(callback) - local choices = utils.kv_list({ - ['+'] = 'synchronized with the system clipboard', - ['*'] = 'synchronized with the selection clipboard', - ['"'] = 'last deleted, changed, or yanked content', - ['0'] = 'last yank', - ['-'] = 'deleted or changed content smaller than one line', - ['.'] = 'last inserted text', - ['%'] = 'name of the current file', - [':'] = 'most recent executed command', - ['#'] = 'alternate buffer', - ['='] = 'result of an expression', - ['/'] = 'last search pattern', - }) - - vim.ui.select(choices, { - prompt = 'Select a register> ', - format_item = function(choice) - return choice.key .. ' - ' .. choice.value - end, - }, function(choice) - callback(choice and choice.key) - end) - end, - resolve = function(input) - input = input or '+' - - utils.schedule_main() - local lines = vim.fn.getreg(input) - if not lines or lines == '' then - return {} - end - - return { - { - content = lines, - filename = 'vim_register_' .. input, - filetype = '', - }, - } - end, - }, - - quickfix = { - description = 'Includes quickfix list file contents in chat context.', - resolve = function() - utils.schedule_main() - - local items = vim.fn.getqflist() - if not items or #items == 0 then - return {} - end - - local unique_files = {} - for _, item in ipairs(items) do - local filename = item.filename or vim.api.nvim_buf_get_name(item.bufnr) - if filename then - unique_files[filename] = true - end - end - - local files = vim.tbl_filter( - function(file) - return file.ft ~= nil - end, - vim.tbl_map(function(file) - return { - name = utils.filepath(file), - ft = utils.filetype(file), - } - end, vim.tbl_keys(unique_files)) - ) - - return vim - .iter(files) - :map(function(file) - return context.get_file(file.name, file.ft) - end) - :filter(function(file_data) - return file_data ~= nil - end) - :totable() - end, - }, - - system = { - description = [[Includes output of provided system shell command in chat context. Supports input. - -Important: -- Only use system commands as last resort, they are run every time the context is requested. -- For example instead of curl use the url context, instead of finding and grepping try to check if there is any context that can query the data you need instead. -- If you absolutely need to run a system command, try to use read-only commands and avoid commands that modify the system state. -]], - input = function(callback) - vim.ui.input({ - prompt = 'Enter command> ', - }, callback) - end, - resolve = function(input) - if not input or input == '' then - return {} - end - - utils.schedule_main() - - local shell, shell_flag - if vim.fn.has('win32') == 1 then - shell, shell_flag = 'cmd.exe', '/c' - else - shell, shell_flag = 'sh', '-c' - end - - local out = utils.system({ shell, shell_flag, input }) - if not out then - return {} - end - - local out_type = 'command_output' - local out_text = out.stdout - if out.code ~= 0 then - out_type = 'command_error' - if out.stderr and out.stderr ~= '' then - out_text = out.stderr - elseif not out_text or out_text == '' then - out_text = 'Command failed with exit code ' .. out.code - end - end - - return { - { - content = out_text, - filename = out_type .. '_' .. input:gsub('[^%w]', '_'):sub(1, 20), - filetype = 'text', - }, - } - end, - }, -} diff --git a/lua/CopilotChat/config/functions.lua b/lua/CopilotChat/config/functions.lua new file mode 100644 index 00000000..07b11573 --- /dev/null +++ b/lua/CopilotChat/config/functions.lua @@ -0,0 +1,503 @@ +local resources = require('CopilotChat.resources') +local utils = require('CopilotChat.utils') + +---@class CopilotChat.config.functions.Result +---@field data string +---@field mimetype string? +---@field uri string? + +---@class CopilotChat.config.functions.Function +---@field description string? +---@field schema table? +---@field group string? +---@field uri string? +---@field resolve fun(input: table, source: CopilotChat.source, prompt: string):table + +---@type table +return { + file = { + group = 'copilot', + uri = 'file://{path}', + description = 'Reads content from a specified file path, even if the file is not currently loaded as a buffer.', + + schema = { + type = 'object', + required = { 'path' }, + properties = { + path = { + type = 'string', + description = 'Path to file to include in chat context.', + enum = function(source) + return utils.glob(source.cwd(), { + max_count = 0, + }) + end, + }, + }, + }, + + resolve = function(input) + local data, mimetype = resources.get_file(input.path) + if not data then + error('File not found: ' .. input.path) + end + + return { + { + uri = 'file://' .. input.path, + mimetype = mimetype, + data = data, + }, + } + end, + }, + + glob = { + group = 'copilot', + uri = 'files://glob/{pattern}', + description = 'Lists filenames matching a pattern in your workspace. Useful for discovering relevant files or understanding the project structure.', + + schema = { + type = 'object', + required = { 'pattern' }, + properties = { + pattern = { + type = 'string', + description = 'Glob pattern to match files.', + default = '**/*', + }, + }, + }, + + resolve = function(input, source) + local files = utils.glob(source.cwd(), { + pattern = input.pattern, + }) + + return { + { + uri = 'files://glob/' .. input.pattern, + mimetype = 'text/plain', + data = table.concat(files, '\n'), + }, + } + end, + }, + + grep = { + group = 'copilot', + uri = 'files://grep/{pattern}', + description = 'Searches for a pattern across files in your workspace. Helpful for finding specific code elements or patterns.', + + schema = { + type = 'object', + required = { 'pattern' }, + properties = { + pattern = { + type = 'string', + description = 'Pattern to search for.', + }, + }, + }, + + resolve = function(input, source) + local files = utils.grep(source.cwd(), { + pattern = input.pattern, + }) + + return { + { + uri = 'files://grep/' .. input.pattern, + mimetype = 'text/plain', + data = table.concat(files, '\n'), + }, + } + end, + }, + + buffer = { + group = 'copilot', + uri = 'neovim://buffer/{name}', + description = 'Retrieves content from a specific buffer. Useful for discussing or analyzing code from a particular file that is currently loaded.', + + schema = { + type = 'object', + required = { 'name' }, + properties = { + name = { + type = 'string', + description = 'Buffer filename to include in chat context.', + enum = function() + return vim + .iter(vim.api.nvim_list_bufs()) + :filter(function(buf) + return buf and utils.buf_valid(buf) and vim.fn.buflisted(buf) == 1 + end) + :map(function(buf) + return vim.api.nvim_buf_get_name(buf) + end) + :totable() + end, + }, + }, + }, + + resolve = function(input, source) + utils.schedule_main() + local name = input.name or vim.api.nvim_buf_get_name(source.bufnr) + local found_buf = nil + for _, buf in ipairs(vim.api.nvim_list_bufs()) do + if vim.api.nvim_buf_get_name(buf) == name then + found_buf = buf + break + end + end + if not found_buf then + error('Buffer not found: ' .. name) + end + local data, mimetype = resources.get_buffer(found_buf) + if not data then + error('Buffer not found: ' .. name) + end + return { + { + uri = 'neovim://buffer/' .. name, + mimetype = mimetype, + data = data, + }, + } + end, + }, + + buffers = { + group = 'copilot', + uri = 'neovim://buffers/{scope}', + description = 'Fetches content from multiple buffers. Helps with discussing or analyzing code across multiple files simultaneously.', + + schema = { + type = 'object', + required = { 'scope' }, + properties = { + scope = { + type = 'string', + description = 'Scope of buffers to include in chat context.', + enum = { 'listed', 'visible' }, + default = 'listed', + }, + }, + }, + + resolve = function(input) + utils.schedule_main() + return vim + .iter(vim.api.nvim_list_bufs()) + :filter(function(bufnr) + return utils.buf_valid(bufnr) + and vim.fn.buflisted(bufnr) == 1 + and (input.scope == 'listed' or #vim.fn.win_findbuf(bufnr) > 0) + end) + :map(function(bufnr) + local name = vim.api.nvim_buf_get_name(bufnr) + local data, mimetype = resources.get_buffer(bufnr) + if not data then + return nil + end + return { + uri = 'neovim://buffer/' .. name, + mimetype = mimetype, + data = data, + } + end) + :filter(function(file_data) + return file_data ~= nil + end) + :totable() + end, + }, + + quickfix = { + group = 'copilot', + uri = 'neovim://quickfix', + description = 'Includes the content of all files referenced in the current quickfix list. Useful for discussing compilation errors, search results, or other collected locations.', + + resolve = function() + utils.schedule_main() + + local items = vim.fn.getqflist() + if not items or #items == 0 then + return {} + end + + local unique_files = {} + for _, item in ipairs(items) do + local filename = item.filename or vim.api.nvim_buf_get_name(item.bufnr) + if filename then + unique_files[filename] = true + end + end + + return vim + .iter(vim.tbl_keys(unique_files)) + :map(function(file) + local data, mimetype = resources.get_file(file) + if not data then + return nil + end + return { + uri = 'file://' .. file, + mimetype = mimetype, + data = data, + } + end) + :filter(function(file_data) + return file_data ~= nil + end) + :totable() + end, + }, + + diagnostics = { + group = 'copilot', + uri = 'neovim://diagnostics/{scope}', + description = 'Collects code diagnostics (errors, warnings, etc.) from specified buffers. Helpful for troubleshooting and fixing code issues.', + + schema = { + type = 'object', + required = { 'scope' }, + properties = { + scope = { + type = 'string', + description = 'Scope of buffers to use for retrieving diagnostics.', + enum = { 'current', 'listed', 'visible' }, + default = 'current', + }, + severity = { + type = 'string', + description = 'Minimum severity level of diagnostics to include.', + enum = { 'error', 'warn', 'info', 'hint' }, + default = 'warn', + }, + }, + }, + + resolve = function(input, source) + utils.schedule_main() + local out = {} + local scope = input.scope or 'current' + local buffers = {} + + -- Get buffers based on scope + if scope == 'current' then + if source and source.bufnr and utils.buf_valid(source.bufnr) then + buffers = { source.bufnr } + end + elseif scope == 'listed' then + buffers = vim.tbl_filter(function(b) + return utils.buf_valid(b) and vim.fn.buflisted(b) == 1 + end, vim.api.nvim_list_bufs()) + elseif scope == 'visible' then + buffers = vim.tbl_filter(function(b) + return utils.buf_valid(b) and vim.fn.buflisted(b) == 1 and #vim.fn.win_findbuf(b) > 0 + end, vim.api.nvim_list_bufs()) + else + buffers = vim.tbl_filter(function(b) + return utils.buf_valid(b) and vim.api.nvim_buf_get_name(b) == input.scope + end, vim.api.nvim_list_bufs()) + end + + -- Collect diagnostics for each buffer + for _, bufnr in ipairs(buffers) do + local name = vim.api.nvim_buf_get_name(bufnr) + local diagnostics = vim.diagnostic.get(bufnr, { + severity = { + min = vim.diagnostic.severity[input.severity:upper()], + }, + }) + + if #diagnostics > 0 then + local diag_lines = {} + for _, diag in ipairs(diagnostics) do + local severity = vim.diagnostic.severity[diag.severity] or 'UNKNOWN' + local line_text = vim.api.nvim_buf_get_lines(bufnr, diag.lnum, diag.lnum + 1, false)[1] or '' + + table.insert( + diag_lines, + string.format( + '%s line=%d-%d: %s\n > %s', + severity, + diag.lnum + 1, + diag.end_lnum and (diag.end_lnum + 1) or (diag.lnum + 1), + diag.message, + line_text + ) + ) + end + + table.insert(out, { + uri = 'neovim://diagnostics/' .. name, + mimetype = 'text/plain', + data = table.concat(diag_lines, '\n'), + }) + end + end + + return out + end, + }, + + register = { + group = 'copilot', + uri = 'neovim://register/{register}', + description = 'Provides access to the content of a specified Vim register. Useful for discussing yanked text, clipboard content, or previously executed commands.', + + schema = { + type = 'object', + required = { 'register' }, + properties = { + register = { + type = 'string', + description = 'Register to include in chat context.', + enum = { + '+', + '*', + '"', + '0', + '-', + '.', + '%', + ':', + '#', + '=', + '/', + }, + default = '+', + }, + }, + }, + + resolve = function(input) + utils.schedule_main() + local lines = vim.fn.getreg(input.register) + if not lines or lines == '' then + return {} + end + + return { + { + uri = 'neovim://register/' .. input.register, + mimetype = 'text/plain', + data = lines, + }, + } + end, + }, + + gitdiff = { + group = 'copilot', + uri = 'git://diff/{target}', + description = 'Retrieves git diff information. Requires git to be installed. Useful for discussing code changes or explaining the purpose of modifications.', + + schema = { + type = 'object', + required = { 'target' }, + properties = { + target = { + type = 'string', + description = 'Target to diff against.', + enum = { 'unstaged', 'staged', '' }, + default = 'unstaged', + }, + }, + }, + + resolve = function(input, source) + local cmd = { + 'git', + '-C', + source.cwd(), + 'diff', + '--no-color', + '--no-ext-diff', + } + + if input.target == 'staged' then + table.insert(cmd, '--staged') + elseif input.target == 'unstaged' then + table.insert(cmd, '--') + else + table.insert(cmd, input.target) + end + + local out = utils.system(cmd) + + return { + { + uri = 'git://diff/' .. input.target, + mimetype = 'text/plain', + data = out.stdout, + }, + } + end, + }, + + gitstatus = { + group = 'copilot', + uri = 'git://status', + description = 'Retrieves the status of the current git repository. Useful for discussing changes, commits, and other git-related tasks.', + + resolve = function(_, source) + local cmd = { + 'git', + '-C', + source.cwd(), + 'status', + } + + local out = utils.system(cmd) + + return { + { + uri = 'git://status', + mimetype = 'text/plain', + data = out.stdout, + }, + } + end, + }, + + url = { + group = 'copilot', + uri = 'https://{url}', + description = 'Fetches content from a specified URL. Useful for referencing documentation, examples, or other online resources.', + + schema = { + type = 'object', + required = { 'url' }, + properties = { + url = { + type = 'string', + description = 'URL to include in chat context.', + }, + }, + }, + + resolve = function(input) + if not input.url:match('^https?://') then + input.url = 'https://' .. input.url + end + + local data, mimetype = resources.get_url(input.url) + if not data then + error('URL not found: ' .. input.url) + end + + return { + { + uri = input.url, + mimetype = mimetype, + data = data, + }, + } + end, + }, +} diff --git a/lua/CopilotChat/config/mappings.lua b/lua/CopilotChat/config/mappings.lua index baf2f82c..a527f0c3 100644 --- a/lua/CopilotChat/config/mappings.lua +++ b/lua/CopilotChat/config/mappings.lua @@ -1,9 +1,8 @@ local async = require('plenary.async') local copilot = require('CopilotChat') -local client = require('CopilotChat.client') local utils = require('CopilotChat.utils') ----@class CopilotChat.config.mappings.diff +---@class CopilotChat.config.mappings.Diff ---@field change string ---@field reference string ---@field filename string @@ -13,8 +12,8 @@ local utils = require('CopilotChat.utils') ---@field bufnr number? --- Get diff data from a block ----@param block CopilotChat.ui.Chat.Section.Block? ----@return CopilotChat.config.mappings.diff? +---@param block CopilotChat.ui.chat.Block? +---@return CopilotChat.config.mappings.Diff? local function get_diff(block) -- If no block found, return nil if not block then @@ -44,7 +43,7 @@ local function get_diff(block) end filename = header.filename - filetype = header.filetype or vim.filetype.match({ filename = filename }) + filetype = header.filetype or utils.filetype(filename) start_line = header.start_line end_line = header.end_line @@ -64,7 +63,7 @@ local function get_diff(block) change = block.content, reference = reference or '', filetype = filetype or '', - filename = utils.filepath(filename), + filename = filename, start_line = start_line, end_line = end_line, bufnr = bufnr, @@ -72,9 +71,9 @@ local function get_diff(block) end --- Prepare a buffer for applying a diff ----@param diff CopilotChat.config.mappings.diff? +---@param diff CopilotChat.config.mappings.Diff? ---@param source CopilotChat.source? ----@return CopilotChat.config.mappings.diff? +---@return CopilotChat.config.mappings.Diff? local function prepare_diff_buffer(diff, source) if not diff then return diff @@ -163,19 +162,20 @@ return { normal = '', insert = '', callback = function() - local section = copilot.chat:get_closest_section('question') - if not section or section.answer then + local message = copilot.chat:get_closest_message('user') + if not message then return end - copilot.ask(section.content) + copilot.ask(message.content) end, }, toggle_sticky = { normal = 'grr', callback = function() - local section = copilot.chat:get_prompt() + local message = copilot.chat:get_message('user') + local section = message and message.section if not section then return end @@ -205,12 +205,13 @@ return { clear_stickies = { normal = 'grx', callback = function() - local section = copilot.chat:get_prompt() + local message = copilot.chat:get_message('user') + local section = message and message.section if not section then return end - local lines = vim.split(section.content, '\n') + local lines = vim.split(message.content, '\n') local new_lines = {} local changed = false @@ -223,7 +224,8 @@ return { end if changed then - copilot.chat:set_prompt(vim.trim(table.concat(new_lines, '\n'))) + message.content = table.concat(new_lines, '\n') + copilot.chat:add_message(message, true) end end, }, @@ -262,7 +264,7 @@ return { callback = function() local items = {} for i, section in ipairs(copilot.chat.sections) do - if section.answer then + if section.role == 'assistant' then local prev_section = copilot.chat.sections[i - 1] local text = '' if prev_section then @@ -352,7 +354,8 @@ return { -- Apply all diffs from same file if #modified > 0 then -- Find all diffs from the same file in this section - local section = copilot.chat:get_closest_section('answer') + local message = copilot.chat:get_closest_message('assistant') + local section = message and message.section local same_file_diffs = {} if section then for _, block in ipairs(section.blocks) do @@ -422,20 +425,23 @@ return { }, show_info = { - normal = 'gi', + normal = 'gc', callback = function(source) - local section = copilot.chat:get_closest_section('question') - if not section or section.answer then + local message = copilot.chat:get_closest_message('user') + if not message then return end local lines = {} - local config, prompt = copilot.resolve_prompt(section.content) + local config, prompt = copilot.resolve_prompt(message.content) local system_prompt = config.system_prompt async.run(function() - local selected_agent = copilot.resolve_agent(prompt, config) local selected_model = copilot.resolve_model(prompt, config) + local selected_tools, resolved_resources = copilot.resolve_functions(prompt, config) + selected_tools = vim.tbl_map(function(tool) + return tool.name + end, selected_tools) utils.schedule_main() table.insert(lines, '**Logs**: `' .. copilot.config.log_path .. '`') @@ -454,82 +460,55 @@ return { table.insert(lines, '') end - if selected_agent then - table.insert(lines, '**Agent**: `' .. selected_agent .. '`') + if not utils.empty(selected_tools) then + table.insert(lines, '**Tools**') + table.insert(lines, '```') + table.insert(lines, table.concat(selected_tools, ', ')) + table.insert(lines, '```') table.insert(lines, '') end if system_prompt then table.insert(lines, '**System Prompt**') - table.insert(lines, '```') + table.insert(lines, '````') for _, line in ipairs(vim.split(vim.trim(system_prompt), '\n')) do table.insert(lines, line) end - table.insert(lines, '```') + table.insert(lines, '````') table.insert(lines, '') end - if client.memory then - table.insert(lines, '**Memory**') - table.insert(lines, '```markdown') - for _, line in ipairs(vim.split(client.memory.content, '\n')) do + local selection = copilot.get_selection() + if selection then + table.insert(lines, '**Selection**') + table.insert(lines, '') + table.insert( + lines, + string.format('**%s** (%s-%s)', selection.filename, selection.start_line, selection.end_line) + ) + table.insert(lines, string.format('````%s', selection.filetype)) + for _, line in ipairs(vim.split(selection.content, '\n')) do table.insert(lines, line) end - table.insert(lines, '```') + table.insert(lines, '````') table.insert(lines, '') end - if not utils.empty(client.history) then - table.insert(lines, ('**History** (#%s, truncated)'):format(#client.history)) + if not utils.empty(resolved_resources) then + table.insert(lines, '**Resources**') table.insert(lines, '') - - for _, message in ipairs(client.history) do - table.insert(lines, '**' .. message.role .. '**') - table.insert(lines, '`' .. vim.split(message.content, '\n')[1] .. '`') - end end - copilot.chat:overlay({ - text = vim.trim(table.concat(lines, '\n')) .. '\n', - }) - end) - end, - }, - - show_context = { - normal = 'gc', - callback = function() - local section = copilot.chat:get_closest_section('question') - if not section or section.answer then - return - end - - local lines = {} - - local selection = copilot.get_selection() - if selection then - table.insert(lines, '**Selection**') - table.insert(lines, '```' .. selection.filetype) - for _, line in ipairs(vim.split(selection.content, '\n')) do - table.insert(lines, line) - end - table.insert(lines, '```') - table.insert(lines, '') - end - - async.run(function() - local embeddings = copilot.resolve_context(section.content) - - for _, embedding in ipairs(embeddings) do - local embed_lines = vim.split(embedding.content, '\n') - local preview = vim.list_slice(embed_lines, 1, math.min(10, #embed_lines)) - local header = string.format('**%s** (%s lines)', embedding.filename, #embed_lines) - if #embed_lines > 10 then + for _, resource in ipairs(resolved_resources) do + local resource_lines = vim.split(resource.data, '\n') + local preview = vim.list_slice(resource_lines, 1, math.min(10, #resource_lines)) + local header = string.format('**%s** (%s lines)', resource.name, #resource_lines) + if #resource_lines > 10 then header = header .. ' (truncated)' end table.insert(lines, header) - table.insert(lines, '```' .. embedding.filetype) + table.insert(lines, '```' .. resource.type) for _, line in ipairs(preview) do table.insert(lines, line) end @@ -537,7 +516,6 @@ return { table.insert(lines, '') end - utils.schedule_main() copilot.chat:overlay({ text = vim.trim(table.concat(lines, '\n')) .. '\n', }) @@ -549,9 +527,9 @@ return { normal = 'gh', callback = function() local chat_help = '**`Special tokens`**\n' - chat_help = chat_help .. '`@` to select an agent\n' - chat_help = chat_help .. '`#` to select a context\n' - chat_help = chat_help .. '`#:` to select input for context\n' + chat_help = chat_help .. '`@` to share function\n' + chat_help = chat_help .. '`#` to add resource\n' + chat_help = chat_help .. '`#:` to add resource with input\n' chat_help = chat_help .. '`/` to select a prompt\n' chat_help = chat_help .. '`$` to select a model\n' chat_help = chat_help .. '`> ` to make a sticky prompt (copied to next prompt)\n' diff --git a/lua/CopilotChat/config/prompts.lua b/lua/CopilotChat/config/prompts.lua index 85552adf..9fca7d8e 100644 --- a/lua/CopilotChat/config/prompts.lua +++ b/lua/CopilotChat/config/prompts.lua @@ -1,35 +1,77 @@ -local COPILOT_BASE = string.format( - [[ +local COPILOT_BASE = [[ When asked for your name, you must respond with "GitHub Copilot". Follow the user's requirements carefully & to the letter. -Follow Microsoft content policies. -Avoid content that violates copyrights. -If you are asked to generate content that is harmful, hateful, racist, sexist, lewd, violent, or completely irrelevant to software engineering, only respond with "Sorry, I can't assist with that." Keep your answers short and impersonal. -The user works in an IDE called Neovim which has a concept for editors with open files, integrated unit test support, an output pane that shows the output of running the code as well as an integrated terminal. -The user is working on a %s machine. Please respond with system specific commands if applicable. + +The user works in editor called Neovim which has these core concepts: +- Buffer: An in-memory text content that may be associated with a file +- Window: A viewport that displays a buffer +- Tab: A collection of windows +- Quickfix/Location lists: Lists of positions in files, often used for errors or search results +- Registers: Named storage for text and commands (like clipboard) +- Normal/Insert/Visual/Command modes: Different interaction states +- LSP (Language Server Protocol): Provides code intelligence features like completion, diagnostics, and code actions +- Treesitter: Provides syntax highlighting, code folding, and structural text editing based on syntax tree parsing +The user is working on a {OS_NAME} machine. Please respond with system specific commands if applicable. +The user is currently in workspace directory {DIR} (typically the project root). Current file paths will be relative to this directory. + + +The user will ask a question or request a task that may require analysis to answer correctly. +If you can infer the project type (languages, frameworks, libraries) from context, consider them when making changes. +For implementing features, break down the request into concepts and provide a clear solution. +Think creatively to provide complete solutions based on the information available. +Never fabricate or hallucinate file contents you haven't actually seen. + + +If tools are explicitly defined in your system context: +- Follow JSON schema precisely when using tools, including all required properties and outputting valid JSON. +- Use appropriate tools for tasks rather than asking for manual actions. +- Execute actions directly when you indicate you'll do so, without asking for permission. +- Only use tools that exist and use proper invocation procedures - no multi_tool_use.parallel. +- Before using tools to retrieve information, check if it's already available in context: + 1. Content shared via "#:" references or headers + 2. Code blocks with file path labels + 3. Other contextual sharing like selected text or conversation history +- If you don't have explicit tool definitions in your system context, assume NO tools are available and clearly state this limitation when asked. NEVER pretend to retrieve content you cannot access. + + You will receive code snippets that include line number prefixes - use these to maintain correct position references but remove them when generating output. +Always use code blocks to present code changes, even if the user doesn't ask for it. When presenting code changes: - -1. For each change, first provide a header outside code blocks with format: - [file:]() line:- - -2. Then wrap the actual code in triple backticks with the appropriate language identifier. - -3. Keep changes minimal and focused to produce short diffs. - -4. Include complete replacement code for the specified line range with: +1. For each change, use the following markdown code block format with triple backticks: + ``` path= start_line= end_line= + + ``` + + Examples: + + ```lua path=lua/CopilotChat/init.lua start_line=40 end_line=50 + local function example() + print("This is an example function.") + end + ``` + + ```python path=scripts/example.py start_line=10 end_line=15 + def example_function(): + print("This is an example function.") + ``` + + ```json path=config/settings.json start_line=5 end_line=8 + { + "setting": "value", + "enabled": true + } + ``` +2. Keep changes minimal and focused to produce short diffs. +3. Include complete replacement code for the specified line range with: - Proper indentation matching the source - All necessary lines (no eliding with comments) - No line number prefixes in the code - -5. Address any diagnostics issues when fixing code. - -6. If multiple changes are needed, present them as separate blocks with their own headers. -]], - vim.uv.os_uname().sysname -) +4. Address any diagnostics issues when fixing code. +5. If multiple changes are needed, present them as separate code blocks. + +]] local COPILOT_INSTRUCTIONS = [[ You are a code-focused AI programming assistant that specializes in practical software engineering solutions. @@ -76,12 +118,12 @@ End with: "**`To clear buffer highlights, please ask a different question.`**" If no issues found, confirm the code is well-written and explain why. ]] ----@class CopilotChat.config.prompt : CopilotChat.config.shared +---@class CopilotChat.config.prompts.Prompt : CopilotChat.config.Shared ---@field prompt string? ---@field description string? ---@field mapping string? ----@type table +---@type table return { COPILOT_BASE = { system_prompt = COPILOT_BASE, @@ -141,7 +183,6 @@ return { end end vim.diagnostic.set(vim.api.nvim_create_namespace('copilot-chat-diagnostics'), source.bufnr, diagnostics) - return response end, }, @@ -163,6 +204,6 @@ return { Commit = { prompt = 'Write commit message for the change with commitizen convention. Keep the title under 50 characters and wrap message at 72 characters. Format as a gitcommit code block.', - context = 'git:staged', + sticky = '#git:staged', }, } diff --git a/lua/CopilotChat/config/providers.lua b/lua/CopilotChat/config/providers.lua index c34a398b..65619117 100644 --- a/lua/CopilotChat/config/providers.lua +++ b/lua/CopilotChat/config/providers.lua @@ -67,52 +67,27 @@ local function get_github_token() error('Failed to find GitHub token') end ----@class CopilotChat.Provider.model ----@field id string ----@field name string ----@field tokenizer string? ----@field max_input_tokens number? ----@field max_output_tokens number? - ----@class CopilotChat.Provider.agent ----@field id string ----@field name string ----@field description string? - ----@class CopilotChat.Provider.embed ----@field index number ----@field embedding table - ----@class CopilotChat.Provider.options ----@field model CopilotChat.Provider.model ----@field agent CopilotChat.Provider.agent? +---@class CopilotChat.config.providers.Options +---@field model CopilotChat.client.Model ---@field temperature number? +---@field tools table? ----@class CopilotChat.Provider.input ----@field role string ----@field content string - ----@class CopilotChat.Provider.reference ----@field name string ----@field url string - ----@class CopilotChat.Provider.output +---@class CopilotChat.config.providers.Output ---@field content string ---@field finish_reason string? ---@field total_tokens number? ----@field references table? +---@field tool_calls table ----@class CopilotChat.Provider +---@class CopilotChat.config.providers.Provider ---@field disabled nil|boolean ---@field get_headers nil|fun():table,number? ----@field get_agents nil|fun(headers:table):table ----@field get_models nil|fun(headers:table):table ----@field embed nil|string|fun(inputs:table, headers:table):table ----@field prepare_input nil|fun(inputs:table, opts:CopilotChat.Provider.options):table ----@field prepare_output nil|fun(output:table, opts:CopilotChat.Provider.options):CopilotChat.Provider.output ----@field get_url nil|fun(opts:CopilotChat.Provider.options):string - ----@type table +---@field get_models nil|fun(headers:table):table +---@field embed nil|string|fun(inputs:table, headers:table):table +---@field prepare_input nil|fun(inputs:table, opts:CopilotChat.config.providers.Options):table +---@field prepare_output nil|fun(output:table, opts:CopilotChat.config.providers.Options):CopilotChat.config.providers.Output +---@field get_url nil|fun(opts:CopilotChat.config.providers.Options):string + +---@type table local M = {} M.copilot = { @@ -139,25 +114,6 @@ M.copilot = { response.body.expires_at end, - get_agents = function(headers) - local response, err = utils.curl_get('https://api.githubcopilot.com/agents', { - json_response = true, - headers = headers, - }) - - if err then - error(err) - end - - return vim.tbl_map(function(agent) - return { - id = agent.slug, - name = agent.name, - description = agent.description, - } - end, response.body.agents) - end, - get_models = function(headers) local response, err = utils.curl_get('https://api.githubcopilot.com/models', { json_response = true, @@ -171,7 +127,7 @@ M.copilot = { local models = vim .iter(response.body.data) :filter(function(model) - return model.capabilities.type == 'chat' and not vim.endswith(model.id, 'paygo') + return model.capabilities.type == 'chat' and model.model_picker_enabled end) :map(function(model) return { @@ -180,6 +136,8 @@ M.copilot = { tokenizer = model.capabilities.tokenizer, max_input_tokens = model.capabilities.limits.max_prompt_tokens, max_output_tokens = model.capabilities.limits.max_output_tokens, + streaming = model.capabilities.supports.streaming, + tools = model.capabilities.supports.tool_calls, policy = not model['policy'] or model['policy']['state'] == 'enabled', version = model.version, } @@ -212,24 +170,59 @@ M.copilot = { local is_o1 = vim.startswith(opts.model.id, 'o1') inputs = vim.tbl_map(function(input) + local output = { + role = input.role, + content = input.content, + } + if is_o1 then if input.role == 'system' then - input.role = 'user' + output.role = 'user' end end - return input + if input.tool_call_id then + output.tool_call_id = input.tool_call_id + end + + if input.tool_calls then + output.tool_calls = vim.tbl_map(function(tool_call) + return { + id = tool_call.id, + type = 'function', + ['function'] = { + name = tool_call.name, + arguments = tool_call.arguments or nil, + }, + } + end, input.tool_calls) + end + + return output end, inputs) local out = { messages = inputs, model = opts.model.id, + stream = opts.model.streaming or false, } + if opts.tools and opts.model.tools then + out.tools = vim.tbl_map(function(tool) + return { + type = 'function', + ['function'] = { + name = tool.name, + description = tool.description, + parameters = tool.schema, + }, + } + end, opts.tools) + end + if not is_o1 then out.n = 1 out.top_p = 1 - out.stream = true out.temperature = opts.temperature end @@ -241,46 +234,51 @@ M.copilot = { end, prepare_output = function(output) - local references = {} - - if output.copilot_references then - for _, reference in ipairs(output.copilot_references) do - local metadata = reference.metadata - if metadata and metadata.display_name and metadata.display_url then - table.insert(references, { - name = metadata.display_name, - url = metadata.display_url, - }) + local tool_calls = {} + + local choice + if output.choices and #output.choices > 0 then + for _, choice in ipairs(output.choices) do + local message = choice.message or choice.delta + if message and message.tool_calls then + for i, tool_call in ipairs(message.tool_calls) do + local fn = tool_call['function'] + if fn then + local index = tool_call.index or i + local id = utils.empty(tool_call.id) and ('tooluse_' .. index) or tool_call.id + table.insert(tool_calls, { + id = id, + index = index, + name = fn.name, + arguments = fn.arguments or '', + }) + end + end end end - end - local message - if output.choices and #output.choices > 0 then - message = output.choices[1] + choice = output.choices[1] else - message = output + choice = output end - local content = message.message and message.message.content or message.delta and message.delta.content - - local usage = message.usage and message.usage.total_tokens or output.usage and output.usage.total_tokens - - local finish_reason = message.finish_reason or message.done_reason or output.finish_reason or output.done_reason + local message = choice.message or choice.delta + local content = message and message.content + local usage = choice.usage and choice.usage.total_tokens + if not usage then + usage = output.usage and output.usage.total_tokens + end + local finish_reason = choice.finish_reason or choice.done_reason or output.finish_reason or output.done_reason return { content = content, finish_reason = finish_reason, total_tokens = usage, - references = references, + tool_calls = tool_calls, } end, - get_url = function(opts) - if opts.agent then - return 'https://api.githubcopilot.com/agents/' .. opts.agent.id .. '?chat' - end - + get_url = function() return 'https://api.githubcopilot.com/chat/completions' end, } @@ -336,6 +334,7 @@ M.github_models = { tokenizer = 'o200k_base', max_input_tokens = max_input_tokens, max_output_tokens = max_output_tokens, + streaming = true, } end) :totable() diff --git a/lua/CopilotChat/functions.lua b/lua/CopilotChat/functions.lua new file mode 100644 index 00000000..8a28ca81 --- /dev/null +++ b/lua/CopilotChat/functions.lua @@ -0,0 +1,198 @@ +local utils = require('CopilotChat.utils') + +local M = {} + +local INPUT_SEPARATOR = ';;' + +local function sorted_propnames(schema) + local prop_names = vim.tbl_keys(schema.properties) + local required_set = {} + if schema.required then + for _, name in ipairs(schema.required) do + required_set[name] = true + end + end + + -- Sort properties with priority: required without default > required with default > optional + table.sort(prop_names, function(a, b) + local a_required = required_set[a] or false + local b_required = required_set[b] or false + local a_has_default = schema.properties[a].default ~= nil + local b_has_default = schema.properties[b].default ~= nil + + -- First priority: required properties without default + if a_required and not a_has_default and (not b_required or b_has_default) then + return true + end + if b_required and not b_has_default and (not a_required or a_has_default) then + return false + end + + -- Second priority: required properties with default + if a_required and not b_required then + return true + end + if b_required and not a_required then + return false + end + + -- Finally sort alphabetically + return a < b + end) + + return prop_names +end + +local function filter_schema(tbl) + if type(tbl) ~= 'table' then + return tbl + end + + local result = {} + for k, v in pairs(tbl) do + if type(v) ~= 'function' and k ~= 'examples' then + result[k] = type(v) == 'table' and filter_schema(v) or v + end + end + return result +end + +---@param uri string The URI to parse +---@param pattern string The pattern to match against (e.g., 'file://{path}') +---@return table|nil inputs Extracted parameters or nil if no match +function M.match_uri(uri, pattern) + -- Convert the pattern into a Lua pattern by escaping special characters + -- and replacing {name} placeholders with capture groups + local lua_pattern = pattern:gsub('([%(%)%.%%%+%-%*%?%[%]%^%$])', '%%%1') + + -- Extract parameter names from the pattern + local param_names = {} + for param in pattern:gmatch('{([^}:*]+)[^}]*}') do + table.insert(param_names, param) + -- Replace {param} with a capture group in our Lua pattern + -- Use non-greedy capture to handle multiple params properly + lua_pattern = lua_pattern:gsub('{' .. param .. '[^}]*}', '(.-)') + end + + -- If no parameters, just do a direct comparison + if #param_names == 0 then + return uri == pattern and {} or nil + end + + -- Match the URI against our constructed pattern + local matches = { uri:match('^' .. lua_pattern .. '$') } + + -- If match failed, return nil + if #matches == 0 or matches[1] == nil then + return nil + end + + -- Build the result table mapping parameter names to their values + local result = {} + for i, param_name in ipairs(param_names) do + result[param_name] = matches[i] + end + + return result +end + +--- Prepare the schema for use +---@param tools table +---@return table +function M.parse_tools(tools) + local tool_names = vim.tbl_keys(tools) + table.sort(tool_names) + return vim.tbl_map(function(name) + local tool = tools[name] + local schema = tool.schema + + if schema then + schema = filter_schema(schema) + end + + return { + name = name, + description = tool.description, + schema = schema, + } + end, tool_names) +end + +--- Parse context input string into a table based on the schema +---@param input string|table|nil +---@param schema table? +---@return table +function M.parse_input(input, schema) + if not schema or not schema.properties then + return {} + end + + if type(input) == 'table' then + return input + end + + local parts = vim.split(input or '', INPUT_SEPARATOR) + local result = {} + local prop_names = sorted_propnames(schema) + + -- Map input parts to schema properties in sorted order + local i = 1 + for _, prop_name in ipairs(prop_names) do + local prop_schema = schema.properties[prop_name] + local value = not utils.empty(parts[i]) and parts[i] or nil + if value == nil and prop_schema.default ~= nil then + value = prop_schema.default + end + + result[prop_name] = value + i = i + 1 + if i > #parts then + break + end + end + + return result +end + +--- Get input from the user based on the schema +---@param schema table? +---@param source CopilotChat.source +---@return string? +function M.enter_input(schema, source) + if not schema or not schema.properties then + return nil + end + + local prop_names = sorted_propnames(schema) + local out = {} + + for _, prop_name in ipairs(prop_names) do + local cfg = schema.properties[prop_name] + if not schema.required or vim.tbl_contains(schema.required, prop_name) then + if cfg.enum then + local choices = type(cfg.enum) == 'table' and cfg.enum or cfg.enum(source) + local choice = utils.select(choices, { + prompt = string.format('Select %s> ', prop_name), + }) + + table.insert(out, choice or '') + elseif cfg.type == 'boolean' then + table.insert(out, utils.select({ 'true', 'false' }, { + prompt = string.format('Select %s> ', prop_name), + }) or '') + else + table.insert(out, utils.input({ + prompt = string.format('Enter %s> ', prop_name), + }) or '') + end + end + end + + local out = vim.trim(table.concat(out, INPUT_SEPARATOR)) + if out:match('%s+') then + out = string.format('`%s`', out) + end + return out +end + +return M diff --git a/lua/CopilotChat/health.lua b/lua/CopilotChat/health.lua index 5d02df19..cf1e568a 100644 --- a/lua/CopilotChat/health.lua +++ b/lua/CopilotChat/health.lua @@ -4,7 +4,6 @@ local start = vim.health.start or vim.health.report_start local error = vim.health.error or vim.health.report_error local warn = vim.health.warn or vim.health.report_warn local ok = vim.health.ok or vim.health.report_ok -local info = vim.health.info or vim.health.report_info --- Run a command and handle potential errors ---@param executable string @@ -42,7 +41,7 @@ end function M.check() start('CopilotChat.nvim [core]') - local vim_version = vim.trim(vim.api.nvim_command_output('version')) + local vim_version = vim.trim(vim.api.nvim_exec2('version', { output = true }).output) if vim.fn.has('nvim-0.10.0') == 1 then ok('nvim: ' .. vim_version) else diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 1062e1a4..1f592349 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -1,17 +1,21 @@ local async = require('plenary.async') local log = require('plenary.log') -local context = require('CopilotChat.context') +local functions = require('CopilotChat.functions') +local resources = require('CopilotChat.resources') local client = require('CopilotChat.client') local notify = require('CopilotChat.notify') local utils = require('CopilotChat.utils') local PLUGIN_NAME = 'CopilotChat' -local WORD = '([^%s]+)' -local WORD_INPUT = '([^%s:]+:`[^`]+`)' +local WORD = '([^%s:]+)' +local WORD_NO_INPUT = '([^%s]+)' +local WORD_WITH_INPUT_QUOTED = WORD .. ':`([^`]+)`' +local WORD_WITH_INPUT_UNQUOTED = WORD .. ':?([^%s`]*)' +local BLOCK_OUTPUT_FORMAT = '```%s\n%s\n```' ---@class CopilotChat ----@field config CopilotChat.config ----@field chat CopilotChat.ui.Chat +---@field config CopilotChat.config.Config +---@field chat CopilotChat.ui.chat.Chat local M = {} --- @class CopilotChat.source @@ -21,23 +25,19 @@ local M = {} --- @class CopilotChat.state --- @field source CopilotChat.source? ---- @field last_prompt string? ---- @field last_response string? ---- @field highlights_loaded boolean +--- @field sticky string[]? local state = { -- Current state tracking source = nil, -- Last state tracking - last_prompt = nil, - last_response = nil, - highlights_loaded = false, + sticky = nil, } --- Insert sticky values from config into prompt ---@param prompt string ----@param config CopilotChat.config.shared -local function insert_sticky(prompt, config, override_sticky) +---@param config CopilotChat.config.Shared +local function insert_sticky(prompt, config) local lines = vim.split(prompt or '', '\n') local stickies = utils.ordered_map() @@ -58,8 +58,10 @@ local function insert_sticky(prompt, config, override_sticky) stickies:set('$' .. config.model, true) end - if config.remember_as_sticky and config.agent and config.agent ~= M.config.agent then - stickies:set('@' .. config.agent, true) + if config.remember_as_sticky and config.tools and not vim.deep_equal(config.tools, M.config.tools) then + for _, tool in ipairs(utils.to_table(config.tools)) do + stickies:set('@' .. tool, true) + end end if @@ -71,32 +73,18 @@ local function insert_sticky(prompt, config, override_sticky) stickies:set('/' .. config.system_prompt, true) end - if config.remember_as_sticky and config.context and not vim.deep_equal(config.context, M.config.context) then - if type(config.context) == 'table' then - ---@diagnostic disable-next-line: param-type-mismatch - for _, context in ipairs(config.context) do - stickies:set('#' .. context, true) - end - else - stickies:set('#' .. config.context, true) - end - end - - if config.sticky and (override_sticky or not vim.deep_equal(config.sticky, M.config.sticky)) then - if type(config.sticky) == 'table' then - ---@diagnostic disable-next-line: param-type-mismatch - for _, sticky in ipairs(config.sticky) do - stickies:set(sticky, true) - end - else - stickies:set(config.sticky, true) + if config.sticky and not vim.deep_equal(config.sticky, M.config.sticky) then + for _, sticky in ipairs(utils.to_table(config.sticky)) do + stickies:set(sticky, true) end end -- Insert stickies at start of prompt local prompt_lines = {} for _, sticky in ipairs(stickies:keys()) do - table.insert(prompt_lines, '> ' .. sticky) + if sticky ~= '' then + table.insert(prompt_lines, '> ' .. sticky) + end end if #prompt_lines > 0 then table.insert(prompt_lines, '') @@ -130,68 +118,44 @@ local function update_highlights() strict = false, }) end - - if state.highlights_loaded then - return - end - - async.run(function() - local items = M.complete_items() - utils.schedule_main() - - for _, item in ipairs(items) do - local pattern = vim.fn.escape(item.word, '.-$^*[]') - if vim.startswith(item.word, '#') then - vim.cmd('syntax match CopilotChatKeyword "' .. pattern .. '\\(:.\\+\\)\\?" containedin=ALL') - else - vim.cmd('syntax match CopilotChatKeyword "' .. pattern .. '" containedin=ALL') - end - end - - vim.cmd('syntax match CopilotChatInput ":\\(.\\+\\)" contained containedin=CopilotChatKeyword') - state.highlights_loaded = true - end) end --- Finish writing to chat buffer. ---@param start_of_chat boolean? local function finish(start_of_chat) - if not start_of_chat then - M.chat:append('\n\n') - end - - M.chat:append(M.config.question_header .. M.config.separator .. '\n\n') - - -- Insert sticky values from config into prompt if start_of_chat then - state.last_prompt = insert_sticky(state.last_prompt, M.config, true) + local sticky = {} + if M.config.sticky then + for _, sticky_line in ipairs(utils.to_table(M.config.sticky)) do + table.insert(sticky, sticky_line) + end + end + state.sticky = sticky end - -- Reinsert sticky prompts from last prompt and last response - local lines = {} - if state.last_prompt then - lines = vim.split(state.last_prompt, '\n') - end - if state.last_response then - for _, line in ipairs(vim.split(state.last_response, '\n')) do - table.insert(lines, line) + local prompt_content = '' + local last_message = M.chat.messages[#M.chat.messages] + local tool_calls = last_message and last_message.tool_calls or {} + + if not utils.empty(state.sticky) then + for _, sticky in ipairs(state.sticky) do + prompt_content = prompt_content .. '> ' .. sticky .. '\n' end + prompt_content = prompt_content .. '\n' end - local has_sticky = false - local in_code_block = false - for _, line in ipairs(lines) do - if line:match('^```') then - in_code_block = not in_code_block - end - if vim.startswith(line, '> ') and not in_code_block then - M.chat:append(line .. '\n') - has_sticky = true + + if not utils.empty(tool_calls) then + for _, tool_call in ipairs(tool_calls) do + prompt_content = prompt_content .. string.format('#%s:%s\n', tool_call.name, tool_call.id) end - end - if has_sticky then - M.chat:append('\n') + prompt_content = prompt_content .. '\n' end + M.chat:add_message({ + role = 'user', + content = prompt_content, + }) + M.chat:finish() end @@ -199,20 +163,13 @@ end ---@param err string|table|nil local function show_error(err) err = err or 'Unknown error' + err = utils.make_string(err) - if type(err) == 'string' then - while true do - local new_err = err:gsub('^[^:]+:%d+: ', '') - if new_err == err then - break - end - err = new_err - end - else - err = utils.make_string(err) - end + M.chat:add_message({ + role = 'assistant', + content = '\n' .. string.format(BLOCK_OUTPUT_FORMAT, 'error', err) .. '\n', + }) - M.chat:append('\n' .. M.config.error_header .. '\n```error\n' .. err .. '\n```') finish() end @@ -261,15 +218,171 @@ local function update_source() M.set_source(use_prev_window and vim.fn.win_getid(vim.fn.winnr('#')) or vim.api.nvim_get_current_win()) end +--- Call and resolve function calls from the prompt. +---@param prompt string? +---@param config CopilotChat.config.Shared? +---@return table, table, table, string +---@async +function M.resolve_functions(prompt, config) + config, prompt = M.resolve_prompt(prompt, config) + local enabled_tools = {} + local resolved_resources = {} + local resolved_tools = {} + local matches = utils.to_table(config.tools) + local tool_calls = {} + for _, message in ipairs(M.chat.messages) do + if message.tool_calls then + for _, tool_call in ipairs(message.tool_calls) do + table.insert(tool_calls, tool_call) + end + end + end + + -- Check for @tool pattern to find enabled tools + prompt = prompt:gsub('@' .. WORD, function(match) + for name, tool in pairs(M.config.functions) do + if name == match or tool.group == match then + table.insert(matches, match) + return '' + end + end + return '@' .. match + end) + for _, match in ipairs(matches) do + for name, tool in pairs(M.config.functions) do + if name == match or tool.group == match then + enabled_tools[name] = tool + end + end + end + + local matches = utils.ordered_map() + + -- Check for #word:`input` pattern + for word, input in prompt:gmatch('#' .. WORD_WITH_INPUT_QUOTED) do + local pattern = string.format('#%s:`%s`', word, input) + matches:set(pattern, { + word = word, + input = input, + }) + end + + -- Check for #word:input pattern + for word, input in prompt:gmatch('#' .. WORD_WITH_INPUT_UNQUOTED) do + local pattern = utils.empty(input) and string.format('#%s', word) or string.format('#%s:%s', word, input) + matches:set(pattern, { + word = word, + input = input, + }) + end + + -- Check for ##word:input pattern + for word in prompt:gmatch('##' .. WORD_NO_INPUT) do + local pattern = string.format('##%s', word) + matches:set(pattern, { + word = word, + }) + end + + -- Resolve each tool reference + local function expand_tool(name, input) + notify.publish(notify.STATUS, 'Running function: ' .. name) + + local tool_id = nil + if not utils.empty(tool_calls) then + for _, tool_call in ipairs(tool_calls) do + if tool_call.name == name and vim.trim(tool_call.id) == vim.trim(input) and enabled_tools[name] then + input = utils.empty(tool_call.arguments) and {} or utils.json_decode(tool_call.arguments) + tool_id = tool_call.id + break + end + end + end + + local tool = enabled_tools[name] + if not tool then + -- Check if input matches uri + for tool_name, tool_spec in pairs(M.config.functions) do + if tool_spec.uri then + local match = functions.match_uri(name, tool_spec.uri) + if match then + name = tool_name + tool = tool_spec + input = match + break + end + end + end + end + if not tool and not tool_id then + tool = M.config.functions[name] + end + if not tool then + -- If tool is not found, return the original pattern + return nil + end + if not tool_id and not tool.uri then + -- If this is a tool that is not resource and was not called by LLM, reject it + return nil + end + + local result = '' + local ok, output = pcall(tool.resolve, functions.parse_input(input, tool.schema), state.source or {}, prompt) + if not ok then + result = string.format(BLOCK_OUTPUT_FORMAT, 'error', utils.make_string(output)) + else + for _, content in ipairs(output) do + if content then + local content_out = nil + if content.uri then + content_out = '##' .. content.uri + table.insert(resolved_resources, resources.to_resource(content)) + if tool_id then + table.insert(state.sticky, content_out) + end + else + content_out = string.format(BLOCK_OUTPUT_FORMAT, utils.mimetype_to_filetype(content.mimetype), content.data) + end + + if not utils.empty(result) then + result = result .. '\n' + end + result = result .. content_out + end + end + end + + if tool_id then + table.insert(resolved_tools, { + id = tool_id, + result = result, + }) + + return nil + end + + return result + end + + -- Resolve and process all tools + for _, pattern in ipairs(matches:keys()) do + local match = matches:get(pattern) + local out = expand_tool(match.word, match.input) or pattern + prompt = prompt:gsub(vim.pesc(pattern), out, 1) + end + + return functions.parse_tools(enabled_tools), resolved_resources, resolved_tools, prompt +end + --- Resolve the final prompt and config from prompt template. ---@param prompt string? ----@param config CopilotChat.config.shared? ----@return CopilotChat.config.prompt, string +---@param config CopilotChat.config.Shared? +---@return CopilotChat.config.prompts.Prompt, string function M.resolve_prompt(prompt, config) if not prompt then - local section = M.chat:get_prompt() - if section then - prompt = section.content + local message = M.chat:get_message('user') + if message then + prompt = message.content end end @@ -303,107 +416,20 @@ function M.resolve_prompt(prompt, config) if prompts_to_use[config.system_prompt] then config.system_prompt = prompts_to_use[config.system_prompt].system_prompt end - return config, prompt -end - ---- Resolve the context embeddings from the prompt. ----@param prompt string? ----@param config CopilotChat.config.shared? ----@return table, string ----@async -function M.resolve_context(prompt, config) - config, prompt = M.resolve_prompt(prompt, config) - - local contexts = {} - local function parse_context(prompt_context) - local split = vim.split(prompt_context, ':') - local context_name = table.remove(split, 1) - local context_input = vim.trim(table.concat(split, ':')) - if vim.startswith(context_input, '`') and vim.endswith(context_input, '`') then - context_input = context_input:sub(2, -2) - end - - if M.config.contexts[context_name] then - table.insert(contexts, { - name = context_name, - input = (context_input ~= '' and context_input or nil), - }) - - return true - end - - return false - end - prompt = prompt:gsub('#' .. WORD_INPUT, function(match) - return parse_context(match) and '' or '#' .. match - end) - - prompt = prompt:gsub('#' .. WORD, function(match) - return parse_context(match) and '' or '#' .. match - end) - - if config.context then - if type(config.context) == 'table' then - ---@diagnostic disable-next-line: param-type-mismatch - for _, config_context in ipairs(config.context) do - parse_context(config_context) - end - else - parse_context(config.context) - end - end - - local embeddings = utils.ordered_map() - for _, context_data in ipairs(contexts) do - local context_value = M.config.contexts[context_data.name] - notify.publish( - notify.STATUS, - 'Resolving context: ' .. context_data.name .. (context_data.input and ' with input: ' .. context_data.input or '') - ) - - local ok, resolved_embeddings = pcall(context_value.resolve, context_data.input, state.source or {}, prompt) - if ok then - for _, embedding in ipairs(resolved_embeddings) do - if embedding then - embeddings:set(embedding.filename, embedding) - end - end - else - log.error('Failed to resolve context: ' .. context_data.name, resolved_embeddings) + if config.system_prompt then + config.system_prompt = config.system_prompt:gsub('{OS_NAME}', jit.os) + if state.source then + config.system_prompt = config.system_prompt:gsub('{DIR}', state.source.cwd()) end end - return embeddings:values(), prompt -end - ---- Resolve the agent from the prompt. ----@param prompt string? ----@param config CopilotChat.config.shared? ----@return string, string ----@async -function M.resolve_agent(prompt, config) - config, prompt = M.resolve_prompt(prompt, config) - - local agents = vim.tbl_map(function(agent) - return agent.id - end, client:list_agents()) - - local selected_agent = config.agent or '' - prompt = prompt:gsub('@' .. WORD, function(match) - if vim.tbl_contains(agents, match) then - selected_agent = match - return '' - end - return '@' .. match - end) - - return selected_agent, prompt + return config, prompt end --- Resolve the model from the prompt. ---@param prompt string? ----@param config CopilotChat.config.shared? +---@param config CopilotChat.config.Shared? ---@return string, string ---@async function M.resolve_model(prompt, config) @@ -457,7 +483,7 @@ function M.set_source(source_winnr) end --- Get the selection from the source buffer. ----@return CopilotChat.select.selection? +---@return CopilotChat.select.Selection? function M.get_selection() local config = vim.tbl_deep_extend('force', M.config, M.chat.config) local selection = config.selection @@ -506,8 +532,8 @@ function M.set_selection(bufnr, start_line, end_line, clear) end --- Trigger the completion for the chat window. ----@param without_context boolean? -function M.trigger_complete(without_context) +---@param without_input boolean? +function M.trigger_complete(without_input) local info = M.complete_info() local bufnr = vim.api.nvim_get_current_buf() local line = vim.api.nvim_get_current_line() @@ -523,23 +549,18 @@ function M.trigger_complete(without_context) return end - if not without_context and vim.startswith(prefix, '#') and vim.endswith(prefix, ':') then - local found_context = M.config.contexts[prefix:sub(2, -2)] - if found_context and found_context.input then + if not without_input and vim.startswith(prefix, '#') and vim.endswith(prefix, ':') then + local found_tool = M.config.functions[prefix:sub(2, -2)] + if found_tool and found_tool.schema then async.run(function() - found_context.input(function(value) - if not value then - return - end - - local value_str = vim.trim(tostring(value)) - if value_str:find('%s') then - value_str = '`' .. value_str .. '`' - end + local value = functions.enter_input(found_tool.schema, state.source) + if not value then + return + end - vim.api.nvim_buf_set_text(bufnr, row - 1, col, row - 1, col, { value_str }) - vim.api.nvim_win_set_cursor(0, { row, col + #value_str }) - end, state.source or {}) + utils.schedule_main() + vim.api.nvim_buf_set_text(bufnr, row - 1, col, row - 1, col, { value }) + vim.api.nvim_win_set_cursor(0, { row, col + #value }) end) end @@ -577,7 +598,6 @@ end ---@async function M.complete_items() local models = client:list_models() - local agents = client:list_agents() local prompts_to_use = M.prompts() local items = {} @@ -616,32 +636,59 @@ function M.complete_items() } end - for _, agent in pairs(agents) do + local groups = {} + for name, tool in pairs(M.config.functions) do + if tool.group then + groups[tool.group] = groups[tool.group] or {} + groups[tool.group][name] = tool + end + end + for name, group in pairs(groups) do + local group_tools = vim.tbl_keys(group) items[#items + 1] = { - word = '@' .. agent.id, - abbr = agent.id, - kind = agent.provider, - info = agent.description, - menu = agent.name, + word = '@' .. name, + abbr = name, + kind = 'group', + info = table.concat(group_tools, '\n'), + menu = string.format('%s tools', #group_tools), icase = 1, dup = 0, empty = 0, } end - - for name, value in pairs(M.config.contexts) do + for name, tool in pairs(M.config.functions) do items[#items + 1] = { - word = '#' .. name, + word = '@' .. name, abbr = name, - kind = 'context', - info = value.description or '', - menu = value.input and string.format('#%s:', name) or string.format('#%s', name), + kind = 'tool', + info = tool.description, + menu = tool.group or '', icase = 1, dup = 0, empty = 0, } end + local tools_to_use = functions.parse_tools(M.config.functions) + for _, tool in pairs(tools_to_use) do + local uri = M.config.functions[tool.name].uri + if uri then + local info = + string.format('%s\n\n%s', tool.description, tool.schema and vim.inspect(tool.schema, { indent = ' ' }) or '') + + items[#items + 1] = { + word = '#' .. tool.name, + abbr = tool.name, + kind = 'resource', + info = info, + menu = uri, + icase = 1, + dup = 0, + empty = 0, + } + end + end + table.sort(items, function(a, b) if a.kind == b.kind then return a.word < b.word @@ -653,7 +700,7 @@ function M.complete_items() end --- Get the prompts to use. ----@return table +---@return table function M.prompts() local prompts_to_use = {} @@ -676,18 +723,22 @@ function M.prompts() end --- Open the chat window. ----@param config CopilotChat.config.shared? +---@param config CopilotChat.config.Shared? function M.open(config) config = vim.tbl_deep_extend('force', M.config, config or {}) utils.return_to_normal_mode() M.chat:open(config) - local section = M.chat:get_prompt() - if section then - local prompt = insert_sticky(section.content, config) + local message = M.chat:get_message('user') + if message then + local prompt = insert_sticky(message.content, config) if prompt then - M.chat:set_prompt(prompt) + M.chat:add_message({ + role = 'user', + content = '\n' .. prompt, + }, true) + M.chat:finish() end end @@ -701,7 +752,7 @@ function M.close() end --- Toggle the chat window. ----@param config CopilotChat.config.shared? +---@param config CopilotChat.config.Shared? function M.toggle(config) if M.chat:visible() then M.close() @@ -710,12 +761,6 @@ function M.toggle(config) end end ---- Get the last response. ---- @returns string -function M.response() - return state.last_response -end - --- Select default Copilot GPT model. function M.select_model() async.run(function() @@ -725,6 +770,8 @@ function M.select_model() id = model.id, name = model.name, provider = model.provider, + streaming = model.streaming, + tools = model.tools, selected = model.id == M.config.model, } end, models) @@ -733,53 +780,39 @@ function M.select_model() vim.ui.select(choices, { prompt = 'Select a model> ', format_item = function(item) - local out = string.format('%s (%s:%s)', item.name, item.provider, item.id) + local indicators = {} + local out = item.name + if item.selected then out = '* ' .. out end - return out - end, - }, function(choice) - if choice then - M.config.model = choice.id - end - end) - end) -end ---- Select default Copilot agent. -function M.select_agent() - async.run(function() - local agents = client:list_agents() - local choices = vim.tbl_map(function(agent) - return { - id = agent.id, - name = agent.name, - provider = agent.provider, - selected = agent.id == M.config.agent, - } - end, agents) + if item.provider then + table.insert(indicators, item.provider) + end + if item.streaming then + table.insert(indicators, 'streaming') + end + if item.tools then + table.insert(indicators, 'tools') + end - utils.schedule_main() - vim.ui.select(choices, { - prompt = 'Select an agent> ', - format_item = function(item) - local out = string.format('%s (%s:%s)', item.name, item.provider, item.id) - if item.selected then - out = '* ' .. out + if #indicators > 0 then + out = out .. ' [' .. table.concat(indicators, ', ') .. ']' end + return out end, }, function(choice) if choice then - M.config.agent = choice.id + M.config.model = choice.id end end) end) end --- Select a prompt template to use. ----@param config CopilotChat.config.shared? +---@param config CopilotChat.config.Shared? function M.select_prompt(config) local prompts = M.prompts() local keys = vim.tbl_keys(prompts) @@ -813,7 +846,7 @@ end --- Ask a question to the Copilot model. ---@param prompt string? ----@param config CopilotChat.config.shared? +---@param config CopilotChat.config.Shared? function M.ask(prompt, config) prompt = prompt or '' if prompt == '' then @@ -836,10 +869,18 @@ function M.ask(prompt, config) M.open(config) end - state.last_prompt = prompt - M.chat:set_prompt(prompt) - M.chat:append('\n\n' .. M.config.answer_header .. M.config.separator .. '\n\n') - M.chat:follow() + local sticky = {} + local in_code_block = false + for _, line in ipairs(vim.split(prompt, '\n')) do + if line:match('^```') then + in_code_block = not in_code_block + end + if vim.startswith(line, '> ') and not in_code_block then + table.insert(sticky, line:sub(3)) + end + end + + state.sticky = sticky else update_source() end @@ -848,16 +889,6 @@ function M.ask(prompt, config) config, prompt = M.resolve_prompt(prompt, config) local system_prompt = config.system_prompt or '' - -- Resolve context name and description - local contexts = {} - if config.include_contexts_in_prompt then - for name, context in pairs(M.config.contexts) do - if context.description then - contexts[name] = context.description - end - end - end - -- Remove sticky prefix prompt = table.concat( vim.tbl_map(function(l) @@ -870,39 +901,55 @@ function M.ask(prompt, config) local selection = M.get_selection() local ok, err = pcall(async.run, function() - local selected_agent, prompt = M.resolve_agent(prompt, config) + local selected_tools, resolved_resources, resolved_tools, prompt = M.resolve_functions(prompt, config) local selected_model, prompt = M.resolve_model(prompt, config) - local embeddings, prompt = M.resolve_context(prompt, config) + local query_ok, processed_resources = pcall(resources.process_resources, prompt, selected_model, resolved_resources) + if query_ok then + resolved_resources = processed_resources + else + log.warn('Failed to process resources', processed_resources) + end - local query_ok, filtered_embeddings = - pcall(context.filter_embeddings, prompt, selected_model, config.headless, embeddings) + prompt = vim.trim(prompt) - if not query_ok then + if not config.headless then utils.schedule_main() - log.error(filtered_embeddings) - if not config.headless then - show_error(filtered_embeddings) + + if not utils.empty(resolved_tools) then + M.chat:remove_message('user') + else + M.chat:add_message({ + role = 'user', + content = '\n' .. prompt .. '\n', + }, true) end - return + + for _, tool in ipairs(resolved_tools) do + M.chat:add_message({ + role = 'tool', + tool_call_id = tool.id, + content = tool.result .. '\n', + }) + end + + M.chat:follow() end - local ask_ok, response, references, token_count, token_max_count = pcall(client.ask, client, prompt, { + local ask_ok, ask_response = pcall(client.ask, client, prompt, { headless = config.headless, - contexts = contexts, + history = M.chat.messages, selection = selection, - embeddings = filtered_embeddings, + resources = resolved_resources, + tools = selected_tools, system_prompt = system_prompt, model = selected_model, - agent = selected_agent, temperature = config.temperature, on_progress = vim.schedule_wrap(function(token) - local out = config.stream and config.stream(token, state.source) or nil - if out == nil then - out = token - end - local to_print = not config.headless and out - if to_print and to_print ~= '' then - M.chat:append(token) + if not config.headless then + M.chat:add_message({ + content = token, + role = 'assistant', + }) end end), }) @@ -910,48 +957,44 @@ function M.ask(prompt, config) utils.schedule_main() if not ask_ok then - log.error(response) + log.error(ask_response) if not config.headless then - show_error(response) + show_error(ask_response) end return end -- If there was no error and no response, it means job was cancelled - if response == nil then + if ask_response == nil then return end - -- Call the callback function and store to history - local out = config.callback and config.callback(response, state.source) or nil - if out == nil then - out = response - end - local to_store = not config.headless and out - if to_store and to_store ~= '' then - table.insert(client.history, { - content = prompt, - role = 'user', - }) - table.insert(client.history, { - content = to_store, - role = 'assistant', - }) + local response = ask_response.message + local token_count = ask_response.token_count + local token_max_count = ask_response.token_max_count + + -- Call the callback function + if config.callback then + local callback_ok, callback_response = pcall(config.callback, response.content, state.source) + if not callback_ok then + log.error('Callback error: ' .. callback_response) + if not config.headless then + show_error(callback_response) + end + return + end end if not config.headless then - state.last_response = response - M.chat.references = references + response.content = vim.trim(response.content) + if utils.empty(response.content) then + response.content = '' + else + response.content = '\n' .. response.content .. '\n' + end + M.chat:add_message(response, true) M.chat.token_count = token_count M.chat.token_max_count = token_max_count - - if not utils.empty(references) and config.references_display == 'write' then - M.chat:append('\n\n**`References`**:') - for _, ref in ipairs(references) do - M.chat:append(string.format('\n[%s](%s)', ref.name, ref.url)) - end - end - finish() end end) @@ -967,26 +1010,19 @@ end --- Stop current copilot output and optionally reset the chat ten show the help message. ---@param reset boolean? function M.stop(reset) - local stopped = false + local stopped = client:stop() if reset then - client:reset() M.chat:clear() vim.diagnostic.reset(vim.api.nvim_create_namespace('copilot-chat-diagnostics')) - state.last_prompt = nil - state.last_response = nil -- Clear the selection if state.source then M.set_selection(state.source.bufnr, 0, 0, true) end - - stopped = true - else - stopped = client:stop() end - if stopped then + if stopped or reset then finish(reset) end end @@ -1009,15 +1045,10 @@ function M.save(name, history_path) return end - local prompt = M.chat:get_prompt() - local history = vim.list_slice(client.history) - if prompt then - table.insert(history, { - content = prompt.content, - role = 'user', - }) + local history = vim.deepcopy(M.chat.messages) + for _, message in ipairs(history) do + message.section = nil end - history_path = vim.fs.normalize(history_path) vim.fn.mkdir(history_path, 'p') history_path = history_path .. '/' .. name .. '.json' @@ -1059,32 +1090,11 @@ function M.load(name, history_path) }, }) - client:reset() - M.chat:clear() - - client.history = history - for i, message in ipairs(history) do - if message.role == 'user' then - if i > 1 then - M.chat:append('\n\n') - end - M.chat:append(M.config.question_header .. M.config.separator .. '\n\n') - M.chat:append(message.content) - elseif message.role == 'assistant' then - M.chat:append('\n\n' .. M.config.answer_header .. M.config.separator .. '\n\n') - M.chat:append(message.content) - end - end - log.info('Loaded history from ' .. history_path) - if #history > 0 then - local last = history[#history] - if last and last.role == 'user' then - M.chat:append('\n\n') - M.chat:finish() - return - end + M.stop(true) + for _, message in ipairs(history) do + M.chat:add_message(message) end finish(#history == 0) @@ -1100,11 +1110,20 @@ function M.log_level(level) plugin = PLUGIN_NAME, level = level, outfile = M.config.log_path, + fmt_msg = function(is_console, mode_name, src_path, src_line, msg) + local nameupper = mode_name:upper() + if is_console then + return string.format('[%s] %s', nameupper, msg) + else + local lineinfo = src_path .. ':' .. src_line + return string.format('[%-6s%s] %s: %s\n', nameupper, os.date(), lineinfo, msg) + end + end, }, true) end --- Set up the plugin ----@param config CopilotChat.config? +---@param config CopilotChat.config.Config? function M.setup(config) M.config = vim.tbl_deep_extend('force', require('CopilotChat.config'), config or {}) state.highlights_loaded = false @@ -1130,8 +1149,7 @@ function M.setup(config) M.chat:delete() end M.chat = require('CopilotChat.ui.chat')( - M.config.question_header, - M.config.answer_header, + M.config.headers, M.config.separator, utils.key_to_info('show_help', M.config.mappings.show_help), function(bufnr) diff --git a/lua/CopilotChat/integrations/fzflua.lua b/lua/CopilotChat/integrations/fzflua.lua deleted file mode 100644 index 174d0139..00000000 --- a/lua/CopilotChat/integrations/fzflua.lua +++ /dev/null @@ -1,42 +0,0 @@ -local fzflua = require('fzf-lua') -local chat = require('CopilotChat') -local utils = require('CopilotChat.utils') - -local M = {} - ---- Pick an action from a list of actions ----@param pick_actions CopilotChat.integrations.actions?: A table with the actions to pick from ----@param opts table?: fzf-lua options ----@deprecated Use |CopilotChat.select_prompt| instead -function M.pick(pick_actions, opts) - if not pick_actions or not pick_actions.actions or vim.tbl_isempty(pick_actions.actions) then - return - end - - utils.return_to_normal_mode() - opts = vim.tbl_extend('force', { - prompt = pick_actions.prompt .. '> ', - preview = function(items) - return pick_actions.actions[items[1]].prompt - end, - winopts = { - preview = { - wrap = 'wrap', - }, - }, - actions = { - ['default'] = function(selected) - if not selected or vim.tbl_isempty(selected) then - return - end - vim.defer_fn(function() - chat.ask(pick_actions.actions[selected[1]].prompt, pick_actions.actions[selected[1]]) - end, 100) - end, - }, - }, opts or {}) - - fzflua.fzf_exec(vim.tbl_keys(pick_actions.actions), opts) -end - -return M diff --git a/lua/CopilotChat/integrations/snacks.lua b/lua/CopilotChat/integrations/snacks.lua deleted file mode 100644 index 14a2daaa..00000000 --- a/lua/CopilotChat/integrations/snacks.lua +++ /dev/null @@ -1,54 +0,0 @@ -local snacks = require('snacks') -local chat = require('CopilotChat') -local utils = require('CopilotChat.utils') - -local M = {} - ---- Pick an action from a list of actions ----@param pick_actions CopilotChat.integrations.actions?: A table with the actions to pick from ----@param opts table?: snacks options ----@deprecated Use |CopilotChat.select_prompt| instead -function M.pick(pick_actions, opts) - if not pick_actions or not pick_actions.actions or vim.tbl_isempty(pick_actions.actions) then - return - end - - utils.return_to_normal_mode() - opts = vim.tbl_extend('force', { - items = vim.tbl_map(function(name) - return { - id = name, - text = name, - file = name, - preview = { - text = pick_actions.actions[name].prompt, - ft = 'text', - }, - } - end, vim.tbl_keys(pick_actions.actions)), - preview = 'preview', - win = { - preview = { - wo = { - wrap = true, - linebreak = true, - }, - }, - }, - title = pick_actions.prompt, - confirm = function(picker) - local selected = picker:current() - if selected then - local action = pick_actions.actions[selected.id] - vim.defer_fn(function() - chat.ask(action.prompt, action) - end, 100) - end - picker:close() - end, - }, opts or {}) - - snacks.picker(opts) -end - -return M diff --git a/lua/CopilotChat/integrations/telescope.lua b/lua/CopilotChat/integrations/telescope.lua deleted file mode 100644 index 5e14d913..00000000 --- a/lua/CopilotChat/integrations/telescope.lua +++ /dev/null @@ -1,65 +0,0 @@ -local actions = require('telescope.actions') -local action_state = require('telescope.actions.state') -local pickers = require('telescope.pickers') -local finders = require('telescope.finders') -local themes = require('telescope.themes') -local conf = require('telescope.config').values -local previewers = require('telescope.previewers') -local chat = require('CopilotChat') -local utils = require('CopilotChat.utils') - -local M = {} - ---- Pick an action from a list of actions ----@param pick_actions CopilotChat.integrations.actions?: A table with the actions to pick from ----@param opts table?: Telescope options ----@deprecated Use |CopilotChat.select_prompt| instead -function M.pick(pick_actions, opts) - if not pick_actions or not pick_actions.actions or vim.tbl_isempty(pick_actions.actions) then - return - end - - utils.return_to_normal_mode() - - if not (opts and opts.theme) then - opts = themes.get_dropdown(opts or {}) - end - - pickers - .new(opts, { - prompt_title = pick_actions.prompt, - finder = finders.new_table({ - results = vim.tbl_keys(pick_actions.actions), - }), - previewer = previewers.new_buffer_previewer({ - define_preview = function(self, entry) - vim.api.nvim_win_set_option(self.state.winid, 'wrap', true) - vim.api.nvim_buf_set_lines( - self.state.bufnr, - 0, - -1, - false, - vim.split(pick_actions.actions[entry[1]].prompt or '', '\n') - ) - end, - }), - sorter = conf.generic_sorter(opts), - attach_mappings = function(prompt_bufnr) - actions.select_default:replace(function() - actions.close(prompt_bufnr) - local selected = action_state.get_selected_entry() - if not selected or vim.tbl_isempty(selected) then - return - end - - vim.defer_fn(function() - chat.ask(pick_actions.actions[selected[1]].prompt, pick_actions.actions[selected[1]]) - end, 100) - end) - return true - end, - }) - :find() -end - -return M diff --git a/lua/CopilotChat/context.lua b/lua/CopilotChat/resources.lua similarity index 81% rename from lua/CopilotChat/context.lua rename to lua/CopilotChat/resources.lua index 3d50ca3d..da79d6ec 100644 --- a/lua/CopilotChat/context.lua +++ b/lua/CopilotChat/resources.lua @@ -1,4 +1,4 @@ ----@class CopilotChat.context.symbol +---@class CopilotChat.resources.Symbol ---@field name string? ---@field signature string ---@field type string @@ -7,16 +7,6 @@ ---@field end_row number ---@field end_col number ----@class CopilotChat.context.embed ----@field content string ----@field filename string ----@field filetype string ----@field outline string? ----@field diagnostics table? ----@field symbols table? ----@field embedding table? ----@field score number? - local async = require('plenary.async') local log = require('plenary.log') local client = require('CopilotChat.client') @@ -94,9 +84,9 @@ local function spatial_distance_cosine(a, b) end --- Rank data by relatedness to the query ----@param query CopilotChat.context.embed ----@param data table ----@return table +---@param query CopilotChat.client.EmbeddedResource +---@param data table +---@return table local function data_ranked_by_relatedness(query, data) for _, item in ipairs(data) do local score = spatial_distance_cosine(item.embedding, query.embedding) @@ -189,8 +179,8 @@ end --- Rank data by symbols and filenames ---@param query string ----@param data table ----@return table +---@param data table +---@return table local function data_ranked_by_symbols(query, data) -- Get query trigrams including compound versions local query_trigrams = {} @@ -211,7 +201,7 @@ local function data_ranked_by_symbols(query, data) local max_score = 0 for _, entry in ipairs(data) do - local basename = utils.filename(entry.filename):gsub('%..*$', '') + local basename = utils.filename(entry.name):gsub('%..*$', '') -- Get trigrams for basename and compound version local file_trigrams = get_trigrams(basename) @@ -327,9 +317,9 @@ end --- Build an outline and symbols from a string ---@param content string ---@param ft string ----@return string?, table? +---@return string?, table? local function get_outline(content, ft) - if not ft or ft == '' or ft == 'text' or ft == 'raw' then + if not ft or ft == '' then return nil end @@ -399,47 +389,36 @@ end --- Get data for a file ---@param filename string ----@param filetype string? ----@return CopilotChat.context.embed? -function M.get_file(filename, filetype) +---@return string?, string? +function M.get_file(filename) + local filetype = utils.filetype(filename) if not filetype then return nil end - local modified = utils.file_mtime(filename) if not modified then return nil end - local cached = file_cache[filename] - if cached and cached._modified >= modified then - return { - content = cached.content, - _modified = cached._modified, - filename = filename, - filetype = filetype, + local data = file_cache[filename] + if not data or data._modified < modified then + local content = utils.read_file(filename) + if not content or content == '' then + return nil + end + data = { + content = content, + _modified = modified, } + file_cache[filename] = data end - local content = utils.read_file(filename) - if not content or content == '' then - return nil - end - - local out = { - content = content, - filename = filename, - filetype = filetype, - _modified = modified, - } - - file_cache[filename] = out - return out + return data.content, utils.filetype_to_mimetype(filetype) end --- Get data for a buffer ---@param bufnr number ----@return CopilotChat.context.embed? +---@return string?, string? function M.get_buffer(bufnr) if not utils.buf_valid(bufnr) then return nil @@ -450,23 +429,18 @@ function M.get_buffer(bufnr) return nil end - return { - content = table.concat(content, '\n'), - filename = utils.filepath(vim.api.nvim_buf_get_name(bufnr)), - filetype = vim.bo[bufnr].filetype, - score = 0.1, - diagnostics = utils.diagnostics(bufnr), - } + return table.concat(content, '\n'), utils.filetype_to_mimetype(vim.bo[bufnr].filetype) end --- Get the content of an URL ---@param url string ----@return CopilotChat.context.embed? +---@return string?, string? function M.get_url(url) if not url or url == '' then return nil end + local ft = utils.filetype(url) local content = url_cache[url] if not content then local ok, out = async.util.apcall(utils.system, { 'lynx', '-dump', url }) @@ -504,37 +478,41 @@ function M.get_url(url) url_cache[url] = content end + return content, utils.filetype_to_mimetype(ft) +end + +--- Transform a resource into a format suitable for the client +---@param resource CopilotChat.config.functions.Result +---@return CopilotChat.client.Resource +function M.to_resource(resource) return { - content = content, - filename = url, - filetype = 'text', + name = utils.uri_to_filename(resource.uri), + type = utils.mimetype_to_filetype(resource.mimetype), + data = resource.data, } end ---- Filter embeddings based on the query +--- Process resources based on the query ---@param prompt string ---@param model string ----@param headless boolean ----@param embeddings table ----@return table -function M.filter_embeddings(prompt, model, headless, embeddings) +---@param resources table +---@return table +function M.process_resources(prompt, model, resources) -- If we dont need to embed anything, just return directly - if #embeddings < MULTI_FILE_THRESHOLD then - return embeddings + if #resources < MULTI_FILE_THRESHOLD then + return resources end notify.publish(notify.STATUS, 'Preparing embedding outline') - for _, input in ipairs(embeddings) do - -- Precalculate hash and attributes for caching - local hash = input.filename .. utils.quick_hash(input.content) + -- Get the outlines for each resource + for _, input in ipairs(resources) do + local hash = input.name .. utils.quick_hash(input.data) input._hash = hash - input.filename = input.filename or 'unknown' - input.filetype = input.filetype or 'text' local outline = outline_cache[hash] if not outline then - local outline_text, symbols = get_outline(input.content, input.filetype) + local outline_text, symbols = get_outline(input.data, input.type) if outline_text then outline = { outline = outline_text, @@ -555,32 +533,18 @@ function M.filter_embeddings(prompt, model, headless, embeddings) -- Build query from history and prompt local query = prompt - if not headless then - query = table.concat( - vim - .iter(client.history) - :filter(function(m) - return m.role == 'user' - end) - :map(function(m) - return vim.trim(m.content) - end) - :totable(), - '\n' - ) .. '\n' .. prompt - end -- Rank embeddings by symbols - embeddings = data_ranked_by_symbols(query, embeddings) - log.debug('Ranked data:', #embeddings) - for i, item in ipairs(embeddings) do - log.debug(string.format('%s: %s - %s', i, item.score, item.filename)) + resources = data_ranked_by_symbols(query, resources) + log.debug('Ranked data:', #resources) + for i, item in ipairs(resources) do + log.debug(string.format('%s: %s - %s', i, item.score, item.name)) end -- Prepare embeddings for processing local to_process = {} local results = {} - for _, input in ipairs(embeddings) do + for _, input in ipairs(resources) do local hash = input._hash local embed = embedding_cache[hash] if embed then @@ -591,14 +555,13 @@ function M.filter_embeddings(prompt, model, headless, embeddings) end end table.insert(to_process, { - content = query, - filename = 'query', - filetype = 'raw', + type = 'text', + data = query, }) -- Embed the data and process the results for _, input in ipairs(client:embed(to_process, model)) do - if input.filetype ~= 'raw' then + if input._hash then embedding_cache[input._hash] = input.embedding end table.insert(results, input) diff --git a/lua/CopilotChat/select.lua b/lua/CopilotChat/select.lua index 2254c913..8bef366c 100644 --- a/lua/CopilotChat/select.lua +++ b/lua/CopilotChat/select.lua @@ -1,18 +1,16 @@ ----@class CopilotChat.select.selection +---@class CopilotChat.select.Selection ---@field content string ---@field start_line number ---@field end_line number ---@field filename string ---@field filetype string ---@field bufnr number ----@field diagnostics table? -local utils = require('CopilotChat.utils') local M = {} --- Select and process current visual selection --- @param source CopilotChat.source ---- @return CopilotChat.select.selection|nil +--- @return CopilotChat.select.Selection|nil function M.visual(source) local bufnr = source.bufnr local start_line = unpack(vim.api.nvim_buf_get_mark(bufnr, '<')) @@ -35,18 +33,17 @@ function M.visual(source) return { content = lines_content, - filename = utils.filepath(vim.api.nvim_buf_get_name(bufnr)), + filename = vim.api.nvim_buf_get_name(bufnr), filetype = vim.bo[bufnr].filetype, start_line = start_line, end_line = finish_line, bufnr = bufnr, - diagnostics = utils.diagnostics(bufnr, start_line, finish_line), } end --- Select and process whole buffer --- @param source CopilotChat.source ---- @return CopilotChat.select.selection|nil +--- @return CopilotChat.select.Selection|nil function M.buffer(source) local bufnr = source.bufnr local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) @@ -54,22 +51,19 @@ function M.buffer(source) return nil end - local out = { + return { content = table.concat(lines, '\n'), - filename = utils.filepath(vim.api.nvim_buf_get_name(bufnr)), + filename = vim.api.nvim_buf_get_name(bufnr), filetype = vim.bo[bufnr].filetype, start_line = 1, end_line = #lines, bufnr = bufnr, } - - out.diagnostics = utils.diagnostics(bufnr, out.start_line, out.end_line) - return out end --- Select and process current line --- @param source CopilotChat.source ---- @return CopilotChat.select.selection|nil +--- @return CopilotChat.select.Selection|nil function M.line(source) local bufnr = source.bufnr local winnr = source.winnr @@ -79,22 +73,19 @@ function M.line(source) return nil end - local out = { + return { content = line, - filename = utils.filepath(vim.api.nvim_buf_get_name(bufnr)), + filename = vim.api.nvim_buf_get_name(bufnr), filetype = vim.bo[bufnr].filetype, start_line = cursor[1], end_line = cursor[1], bufnr = bufnr, } - - out.diagnostics = utils.diagnostics(bufnr, out.start_line, out.end_line) - return out end --- Select and process contents of unnamed register ("). This register contains last deleted, changed or yanked content. --- @param source CopilotChat.source ---- @return CopilotChat.select.selection|nil +--- @return CopilotChat.select.Selection|nil function M.unnamed(source) local bufnr = source.bufnr local start_line = unpack(vim.api.nvim_buf_get_mark(bufnr, '[')) @@ -117,12 +108,11 @@ function M.unnamed(source) return { content = lines_content, - filename = utils.filepath(vim.api.nvim_buf_get_name(bufnr)), + filename = vim.api.nvim_buf_get_name(bufnr), filetype = vim.bo[bufnr].filetype, start_line = start_line, end_line = finish_line, bufnr = bufnr, - diagnostics = utils.diagnostics(bufnr, start_line, finish_line), } end diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 82af9789..f54e9435 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -14,71 +14,71 @@ function CopilotChatFoldExpr(lnum, separator) end local HEADER_PATTERNS = { - '%[file:.+%]%((.+)%) line:(%d+)-?(%d*)', - '%[file:(.+)%] line:(%d+)-?(%d*)', + '^```?(%w+)%s+path=(%S+)%s+start_line=(%d+)%s+end_line=(%d+)$', + '^```(%w+)$', } ---@param header? string ----@return string?, number?, number? +---@return string?, string?, number?, number? local function match_header(header) if not header then return end for _, pattern in ipairs(HEADER_PATTERNS) do - local filename, start_line, end_line = header:match(pattern) - if filename then - return utils.filepath(filename), tonumber(start_line) or 1, tonumber(end_line) or tonumber(start_line) or 1 + local type, path, start_line, end_line = header:match(pattern) + if path then + return type, path, tonumber(start_line) or 1, tonumber(end_line) or tonumber(start_line) or 1 + elseif type then + return type, 'block' end end end ----@class CopilotChat.ui.Chat.Section.Block.Header +---@class CopilotChat.ui.chat.Header ---@field filename string ---@field start_line number ---@field end_line number ---@field filetype string ----@class CopilotChat.ui.Chat.Section.Block ----@field header CopilotChat.ui.Chat.Section.Block.Header +---@class CopilotChat.ui.chat.Block +---@field header CopilotChat.ui.chat.Header ---@field start_line number ---@field end_line number ---@field content string? ----@class CopilotChat.ui.Chat.Section ----@field answer boolean +---@class CopilotChat.ui.chat.Section ---@field start_line number ---@field end_line number ----@field blocks table ----@field content string? +---@field blocks table + +---@class CopilotChat.ui.chat.Message : CopilotChat.client.Message +---@field id string +---@field section CopilotChat.ui.chat.Section? ----@class CopilotChat.ui.Chat : CopilotChat.ui.Overlay +---@class CopilotChat.ui.chat.Chat : CopilotChat.ui.overlay.Overlay ---@field winnr number? ----@field config CopilotChat.config.shared ----@field layout CopilotChat.config.Layout? ----@field sections table ----@field references table +---@field config CopilotChat.config.Shared ---@field token_count number? ---@field token_max_count number? ----@field private question_header string ----@field private answer_header string +---@field messages table +---@field private layout CopilotChat.config.Layout? +---@field private headers table ---@field private separator string ---@field private header_ns number ----@field private spinner CopilotChat.ui.Spinner ----@field private chat_overlay CopilotChat.ui.Overlay -local Chat = class(function(self, question_header, answer_header, separator, help, on_buf_create) +---@field private spinner CopilotChat.ui.spinner.Spinner +---@field private chat_overlay CopilotChat.ui.overlay.Overlay +local Chat = class(function(self, headers, separator, help, on_buf_create) Overlay.init(self, 'copilot-chat', help, on_buf_create) self.winnr = nil - self.sections = {} self.config = {} - self.layout = nil - self.references = {} self.token_count = nil self.token_max_count = nil + self.messages = {} - self.question_header = question_header - self.answer_header = answer_header + self.layout = nil + self.headers = headers or {} self.separator = separator self.header_ns = vim.api.nvim_create_namespace('copilot-chat-headers') @@ -110,10 +110,10 @@ function Chat:focused() return self:visible() and vim.api.nvim_get_current_win() == self.winnr end ---- Get the closest section to the cursor. ----@param type? "answer"|"question" If specified, only considers sections of the given type ----@return CopilotChat.ui.Chat.Section? -function Chat:get_closest_section(type) +--- Get the closest message to the cursor. +---@param role string? If specified, only considers sections of the given role +---@return CopilotChat.ui.chat.Message? +function Chat:get_closest_message(role) if not self:visible() then return nil end @@ -121,25 +121,23 @@ function Chat:get_closest_section(type) self:render() local cursor_pos = vim.api.nvim_win_get_cursor(self.winnr) local cursor_line = cursor_pos[1] - local closest_section = nil + local closest_message = nil local max_line_below_cursor = -1 - for _, section in ipairs(self.sections) do - local matches_type = not type - or (type == 'answer' and section.answer) - or (type == 'question' and not section.answer) - - if matches_type and section.start_line <= cursor_line and section.start_line > max_line_below_cursor then + for _, message in ipairs(self.messages) do + local section = message.section + local matches_role = not role or message.role == role + if matches_role and section.start_line <= cursor_line and section.start_line > max_line_below_cursor then max_line_below_cursor = section.start_line - closest_section = section + closest_message = message end end - return closest_section + return closest_message end --- Get the closest code block to the cursor. ----@return CopilotChat.ui.Chat.Section.Block? +---@return CopilotChat.ui.chat.Block? function Chat:get_closest_block() if not self:visible() then return nil @@ -151,7 +149,8 @@ function Chat:get_closest_block() local closest_block = nil local max_line_below_cursor = -1 - for _, section in pairs(self.sections) do + for _, message in pairs(self.messages) do + local section = message.section for _, block in ipairs(section.blocks) do if block.start_line <= cursor_line and block.start_line > max_line_below_cursor then max_line_below_cursor = block.start_line @@ -163,39 +162,20 @@ function Chat:get_closest_block() return closest_block end ---- Get the prompt in the chat window. ----@return CopilotChat.ui.Chat.Section? -function Chat:get_prompt() +--- Get last message by role in the chat window. +---@return CopilotChat.ui.chat.Message? +function Chat:get_message(role) if not self:visible() then return end - self:render() - local section = self.sections[#self.sections] - if not section or section.answer then - return - end - - return section -end - ---- Set the prompt in the chat window. ----@param prompt string? -function Chat:set_prompt(prompt) - if not self:visible() then - return - end - - local section = self:get_prompt() - if not section then - return + for i = #self.messages, 1, -1 do + local message = self.messages[i] + local matches_role = not role or message.role == role + if matches_role then + return message + end end - - local modifiable = vim.bo[self.bufnr].modifiable - vim.bo[self.bufnr].modifiable = true - local lines = prompt and vim.split('\n' .. prompt, '\n') or {} - vim.api.nvim_buf_set_lines(self.bufnr, section.start_line - 1, section.end_line, false, lines) - vim.bo[self.bufnr].modifiable = modifiable end --- Add a sticky line to the prompt in the chat window. @@ -205,8 +185,8 @@ function Chat:add_sticky(sticky) return end - local prompt = self:get_prompt() - if not prompt then + local prompt = self:get_message('user') + if not prompt or not prompt.section then return end @@ -239,7 +219,7 @@ function Chat:add_sticky(sticky) return end - insert_line = prompt.start_line + insert_line - 1 + insert_line = prompt.section.start_line + insert_line - 1 local to_insert = first_one and { '> ' .. sticky, '' } or { '> ' .. sticky } local modifiable = vim.bo[self.bufnr].modifiable vim.bo[self.bufnr].modifiable = true @@ -265,7 +245,7 @@ function Chat:overlay(opts) end --- Open the chat window. ----@param config CopilotChat.config.shared +---@param config CopilotChat.config.Shared function Chat:open(config) self:validate() @@ -415,6 +395,88 @@ function Chat:finish() end end +function Chat:add_message(message, replace) + local current_message = self.messages[#self.messages] + local needs_header = false + + -- Check if we need to add a header (role change or first message) + if not current_message or current_message.role ~= message.role then + needs_header = true + end + + -- Add appropriate header based on role + if needs_header then + message.id = message.id or utils.uuid() + local header = self.headers[message.role] + if current_message then + header = '\n' .. header + end + self:append(header .. '(' .. message.id .. ')' .. self.separator .. '\n\n') + elseif replace and current_message then + self:append('') + self:render() + + current_message.content = message.content + local section = current_message.section + + if section then + vim.bo[self.bufnr].modifiable = true + vim.api.nvim_buf_set_lines( + self.bufnr, + section.start_line - 1, + section.end_line, + false, + vim.split(message.content, '\n') + ) + vim.bo[self.bufnr].modifiable = false + end + + self:append('') + return + end + + -- Handle message content combining or creation + if current_message and current_message.role == message.role then + current_message.content = current_message.content .. message.content + self:append(message.content) + else + table.insert(self.messages, message) + self:append(message.content) + end +end + +function Chat:remove_message(role) + if not self:visible() then + return + end + + self:render() + local message = self:get_closest_message(role) + if not message then + return + end + + local section = message.section + if not section then + return + end + + -- Remove the section from the buffer + vim.bo[self.bufnr].modifiable = true + vim.api.nvim_buf_set_lines(self.bufnr, section.start_line - 2, section.end_line + 1, false, {}) + vim.bo[self.bufnr].modifiable = false + + -- Remove the message from the messages list + for i, msg in ipairs(self.messages) do + if msg.id == message.id then + table.remove(self.messages, i) + break + end + end + + self:render() +end + --- Append text to the chat window. ---@param str string function Chat:append(str) @@ -451,9 +513,9 @@ end --- Clear the chat window. function Chat:clear() self:validate() - self.references = {} self.token_count = nil self.token_max_count = nil + self.messages = {} vim.bo[self.bufnr].modifiable = true vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, false, {}) vim.bo[self.bufnr].modifiable = false @@ -493,81 +555,79 @@ end function Chat:render() vim.api.nvim_buf_clear_namespace(self.bufnr, self.header_ns, 0, -1) local lines = vim.api.nvim_buf_get_lines(self.bufnr, 0, -1, false) - local line_count = #lines - local sections = {} - local current_section = nil + local new_messages = {} + local current_message = nil local current_block = nil + local function parse_header(header, line) + return line:match('^' .. vim.pesc(header) .. '%(([%w%-]+)%)' .. vim.pesc(self.separator) .. '$') + end + for l, line in ipairs(lines) do - local separator_found = false - - if line == self.answer_header .. self.separator then - separator_found = true - if current_section then - current_section.end_line = l - 1 - current_section.content = - vim.trim(table.concat(vim.list_slice(lines, current_section.start_line, current_section.end_line), '\n')) - table.insert(sections, current_section) - end - current_section = { - answer = true, - start_line = l + 1, - blocks = {}, - } - elseif line == self.question_header .. self.separator then - separator_found = true - if current_section then - current_section.end_line = l - 1 - current_section.content = - vim.trim(table.concat(vim.list_slice(lines, current_section.start_line, current_section.end_line), '\n')) - table.insert(sections, current_section) - end - current_section = { - answer = false, - start_line = l + 1, - blocks = {}, - } - elseif l == line_count then - if current_section then - current_section.end_line = l - current_section.content = - vim.trim(table.concat(vim.list_slice(lines, current_section.start_line, current_section.end_line), '\n')) - table.insert(sections, current_section) - end - end + -- Detect section header with ID + for header_name, header_value in pairs(self.headers) do + local id = parse_header(header_value, line) + if id then + -- Draw the separator as virtual text over the header line, hiding the id and anything after the header + if self.config.highlight_headers then + local sep_col = vim.fn.strwidth(header_value) + vim.api.nvim_buf_set_extmark(self.bufnr, self.header_ns, l - 1, sep_col, { + virt_text = { + { string.rep(self.separator, vim.go.columns), 'CopilotChatSeparator' }, + }, + virt_text_win_col = sep_col, + priority = 200, + strict = false, + }) + vim.api.nvim_buf_set_extmark(self.bufnr, self.header_ns, l - 1, 0, { + end_col = sep_col, + hl_group = 'CopilotChatHeader', + priority = 100, + strict = false, + }) + end - -- Highlight separators - if self.config.highlight_headers and separator_found then - local sep = vim.fn.strwidth(line) - vim.fn.strwidth(self.separator) - -- separator line - vim.api.nvim_buf_set_extmark(self.bufnr, self.header_ns, l - 1, sep, { - virt_text_win_col = sep, - virt_text = { - { string.rep(self.separator, vim.go.columns), 'CopilotChatSeparator' }, - }, - priority = 100, - strict = false, - }) - -- header hl group - vim.api.nvim_buf_set_extmark(self.bufnr, self.header_ns, l - 1, 0, { - end_col = sep + 1, - hl_group = 'CopilotChatHeader', - priority = 100, - strict = false, - }) - end + -- Finish previous message + if current_message then + current_message.section.end_line = l - 1 + current_message.content = vim.trim( + table.concat( + vim.list_slice(lines, current_message.section.start_line, current_message.section.end_line), + '\n' + ) + ) + end - -- Parse code blocks - if current_section and current_section.answer then - local filetype = line:match('^```(%w+)$') - if filetype and not current_block then - local filename, start_line, end_line = match_header(lines[l - 1]) - if not filename then - filename, start_line, end_line = match_header(lines[l - 2]) + -- Find existing message by id or create new + local old_msg = nil + for _, msg in ipairs(self.messages) do + if msg.id == id then + old_msg = msg + break + end + end + if not old_msg then + old_msg = { id = id, role = header_name } end - filename = filename or 'code-block' + -- Attach section info + old_msg.section = { + role = header_name, + start_line = l + 1, + blocks = {}, + } + table.insert(new_messages, old_msg) + current_message = old_msg + current_block = nil + break + end + end + + -- Code blocks + if current_message and current_message.role == 'assistant' then + local filetype, filename, start_line, end_line = match_header(line) + if filetype and filename and not current_block then current_block = { header = { filename = filename, @@ -577,18 +637,96 @@ function Chat:render() }, start_line = l + 1, } + local text = string.format('[%s] %s', filetype, filename) + if start_line and end_line then + text = text .. string.format(' lines %d-%d', start_line, end_line) + end + vim.api.nvim_buf_set_extmark(self.bufnr, self.header_ns, l, 0, { + virt_lines_above = true, + virt_lines = { { { text, 'CopilotChatAnnotationHeader' } } }, + priority = 100, + strict = false, + }) elseif line == '```' and current_block then current_block.end_line = l - 1 current_block.content = table.concat(vim.list_slice(lines, current_block.start_line, current_block.end_line), '\n') - table.insert(current_section.blocks, current_block) + table.insert(current_message.section.blocks, current_block) current_block = nil end end + + -- If last line, finish last message + if l == #lines and current_message then + current_message.section.end_line = l + current_message.content = vim.trim( + table.concat(vim.list_slice(lines, current_message.section.start_line, current_message.section.end_line), '\n') + ) + end + + -- Highlight response calls + for _, message in ipairs(self.messages) do + for _, tool_call in ipairs(message.tool_calls or {}) do + if line:match(string.format('#%s:%s', tool_call.name, vim.pesc(tool_call.id))) then + vim.api.nvim_buf_add_highlight(self.bufnr, self.header_ns, 'CopilotChatAnnotationHeader', l - 1, 0, #line) + if not utils.empty(tool_call.arguments) then + vim.api.nvim_buf_set_extmark(self.bufnr, self.header_ns, l - 1, 0, { + virt_lines = vim.tbl_map(function(json_line) + return { { json_line, 'CopilotChatAnnotation' } } + end, vim.split(vim.inspect(utils.json_decode(tool_call.arguments)), '\n')), + priority = 100, + strict = false, + }) + end + break + end + end + end + end + + -- Replace self.messages with new_messages (preserving tool_calls, etc.) + self.messages = new_messages + + -- Show tool call details as virt lines + for _, message in ipairs(self.messages) do + if message.tool_calls and #message.tool_calls > 0 then + local section = message.section + if section and section.end_line then + local virt_lines = { { { 'Tool calls:', 'CopilotChatAnnotationHeader' } } } + for _, tc in ipairs(message.tool_calls) do + table.insert(virt_lines, { { string.format(' %s:%s', tc.name, tostring(tc.id)), 'CopilotChatAnnotation' } }) + for _, json_line in ipairs(vim.split(vim.inspect(utils.json_decode(tc.arguments)), '\n')) do + table.insert(virt_lines, { { ' ' .. json_line, 'CopilotChatAnnotation' } }) + end + end + vim.api.nvim_buf_set_extmark(self.bufnr, self.header_ns, section.end_line - 1, 0, { + virt_lines = virt_lines, + virt_lines_above = true, + priority = 100, + strict = false, + }) + end + end + + if message.tool_call_id then + local section = message.section + if section and section.start_line then + local virt_lines = { + { { 'Tool: ' .. message.tool_call_id, 'CopilotChatAnnotationHeader' } }, + } + vim.api.nvim_buf_set_extmark(self.bufnr, self.header_ns, section.start_line, 0, { + virt_lines = virt_lines, + virt_lines_above = true, + priority = 100, + strict = false, + }) + end + end end - local last_section = sections[#sections] - if last_section and not last_section.answer then + -- Show help as before, using last user message + local last_message = self.messages[#self.messages] + if last_message and last_message.role == 'user' then local msg = self.config.show_help and self.help or '' if self.token_count and self.token_max_count then if msg ~= '' then @@ -596,29 +734,10 @@ function Chat:render() end msg = msg .. self.token_count .. '/' .. self.token_max_count .. ' tokens used' end - - self:show_help(msg, last_section.start_line - last_section.end_line - 1) - - if not utils.empty(self.references) and self.config.references_display == 'virtual' then - msg = 'References:\n' - for _, ref in ipairs(self.references) do - msg = msg .. ' ' .. ref.name .. '\n' - end - - vim.api.nvim_buf_set_extmark(self.bufnr, self.header_ns, last_section.start_line - 2, 0, { - hl_mode = 'combine', - priority = 100, - virt_lines_above = true, - virt_lines = vim.tbl_map(function(t) - return { { t, 'CopilotChatHelp' } } - end, vim.split(msg, '\n')), - }) - end + self:show_help(msg, last_message.section.start_line - last_message.section.end_line - 1) else self:show_help() end - - self.sections = sections end --- Get the last line and column of the chat window. diff --git a/lua/CopilotChat/ui/overlay.lua b/lua/CopilotChat/ui/overlay.lua index 9b70cb5e..a23c022e 100644 --- a/lua/CopilotChat/ui/overlay.lua +++ b/lua/CopilotChat/ui/overlay.lua @@ -1,7 +1,7 @@ local utils = require('CopilotChat.utils') local class = utils.class ----@class CopilotChat.ui.Overlay : Class +---@class CopilotChat.ui.overlay.Overlay : Class ---@field bufnr number? ---@field protected name string ---@field protected help string @@ -19,10 +19,6 @@ local Overlay = class(function(self, name, help, on_buf_create) self.on_hide = nil self.help_ns = vim.api.nvim_create_namespace('copilot-chat-help') - self.hl_ns = vim.api.nvim_create_namespace('copilot-chat-highlights') - vim.api.nvim_set_hl(self.hl_ns, '@diff.plus', { bg = utils.blend_color('DiffAdd', 20) }) - vim.api.nvim_set_hl(self.hl_ns, '@diff.minus', { bg = utils.blend_color('DiffDelete', 20) }) - vim.api.nvim_set_hl(self.hl_ns, '@diff.delta', { bg = utils.blend_color('DiffChange', 20) }) end) --- Show the overlay buffer @@ -38,7 +34,6 @@ function Overlay:show(text, winnr, filetype, syntax, on_show, on_hide) end self:validate() - vim.api.nvim_win_set_hl_ns(winnr, self.hl_ns) text = text .. '\n' self.cursor = vim.api.nvim_win_get_cursor(winnr) @@ -122,7 +117,6 @@ function Overlay:restore(winnr, bufnr) end vim.api.nvim_win_set_buf(winnr, bufnr) - vim.api.nvim_win_set_hl_ns(winnr, 0) if self.cursor then vim.api.nvim_win_set_cursor(winnr, self.cursor) diff --git a/lua/CopilotChat/ui/spinner.lua b/lua/CopilotChat/ui/spinner.lua index 4b69ae44..0f582032 100644 --- a/lua/CopilotChat/ui/spinner.lua +++ b/lua/CopilotChat/ui/spinner.lua @@ -14,7 +14,7 @@ local spinner_frames = { '⠏', } ----@class CopilotChat.ui.Spinner : Class +---@class CopilotChat.ui.spinner.Spinner : Class ---@field bufnr number ---@field status string? ---@field private index number diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index afd92c5d..1cf1a151 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -1,6 +1,7 @@ local async = require('plenary.async') local curl = require('plenary.curl') local scandir = require('plenary.scandir') +local log = require('plenary.log') local M = {} M.timers = {} @@ -102,6 +103,24 @@ function M.ordered_map() } end +--- Convert arguments to a table +---@param ... any The arguments +---@return table +function M.to_table(...) + local result = {} + for i = 1, select('#', ...) do + local x = select(i, ...) + if type(x) == 'table' then + for _, v in ipairs(x) do + table.insert(result, v) + end + elseif x ~= nil then + table.insert(result, x) + end + end + return result +end + ---@class StringBuffer ---@field add fun(self:StringBuffer, s:string) ---@field set fun(self:StringBuffer, s:string) @@ -149,26 +168,6 @@ function M.temp_file(text) return temp_file end ---- Blend a color with the neovim background ----@param color_name string The color name ----@param blend number The blend percentage ----@return string? -function M.blend_color(color_name, blend) - local color_int = vim.api.nvim_get_hl(0, { name = color_name }).fg - local bg_int = vim.api.nvim_get_hl(0, { name = 'Normal' }).bg - - if not color_int or not bg_int then - return - end - - local color = { (color_int / 65536) % 256, (color_int / 256) % 256, color_int % 256 } - local bg = { (bg_int / 65536) % 256, (bg_int / 256) % 256, bg_int % 256 } - local r = math.floor((color[1] * blend + bg[1] * (100 - blend)) / 100) - local g = math.floor((color[2] * blend + bg[2] * (100 - blend)) / 100) - local b = math.floor((color[3] * blend + bg[3] * (100 - blend)) / 100) - return string.format('#%02x%02x%02x', r, g, b) -end - --- Return to normal mode function M.return_to_normal_mode() local mode = vim.fn.mode():lower() @@ -179,11 +178,6 @@ function M.return_to_normal_mode() end end ---- Mark a function as deprecated -function M.deprecate(old, new) - vim.deprecate(old, new, '3.0.X', 'CopilotChat.nvim', false) -end - --- Debounce a function function M.debounce(id, fn, delay) if M.timers[id] then @@ -193,21 +187,6 @@ function M.debounce(id, fn, delay) M.timers[id] = vim.defer_fn(fn, delay) end ---- Create key-value list from table ----@param tbl table The table ----@return table -function M.kv_list(tbl) - local result = {} - for k, v in pairs(tbl) do - table.insert(result, { - key = k, - value = v, - }) - end - - return result -end - --- Check if a buffer is valid --- Check if the buffer is not a terminal ---@param bufnr number? The buffer number @@ -246,6 +225,53 @@ function M.filetype(filename) return ft end +--- Get the mimetype from filetype +---@param filetype string? +---@return string +function M.filetype_to_mimetype(filetype) + if not filetype or filetype == '' then + return 'text/plain' + end + if filetype == 'json' or filetype == 'yaml' then + return 'application/' .. filetype + end + if filetype == 'html' or filetype == 'css' then + return 'text/' .. filetype + end + return 'text/x-' .. filetype +end + +--- Get the filetype from mimetype +---@param mimetype string? +---@return string +function M.mimetype_to_filetype(mimetype) + if not mimetype or mimetype == '' then + return 'text' + end + + local out = mimetype:gsub('^text/x%-', '') + out = out:gsub('^text/', '') + out = out:gsub('^application/', '') + out = out:gsub('^image/', '') + out = out:gsub('^video/', '') + out = out:gsub('^audio/', '') + return out +end + +--- Convert a URI to a file name +---@param uri string The URI +---@return string +function M.uri_to_filename(uri) + if not uri or uri == '' then + return uri + end + local ok, fname = pcall(vim.uri_to_fname, uri) + if not ok or M.empty(fname) then + return uri + end + return fname +end + --- Get the file name ---@param filepath string The file path ---@return string @@ -253,13 +279,6 @@ function M.filename(filepath) return vim.fs.basename(filepath) end ---- Get the file path ----@param filename string The file name ----@return string -function M.filepath(filename) - return vim.fn.fnamemodify(filename, ':p:.') -end - --- Generate a UUID ---@return string function M.uuid() @@ -291,6 +310,13 @@ function M.make_string(...) x = vim.inspect(x) else x = tostring(x) + while true do + local new_x = x:gsub('^[^:]+:%d+: ', '') + if new_x == x then + break + end + x = new_x + end end t[#t + 1] = x @@ -329,8 +355,10 @@ end ---@param opts table? The options ---@async M.curl_get = async.wrap(function(url, opts, callback) + log.debug('GET request:', url, opts) local args = { on_error = function(err) + log.debug('GET error:', err) callback(nil, err and err.stderr or err) end, } @@ -339,6 +367,7 @@ M.curl_get = async.wrap(function(url, opts, callback) args = vim.tbl_deep_extend('force', args, opts or {}) args.callback = function(response) + log.debug('GET response:', response) if response and not vim.startswith(tostring(response.status), '20') then callback(response, response.body) return @@ -366,9 +395,10 @@ end, 3) ---@param opts table? The options ---@async M.curl_post = async.wrap(function(url, opts, callback) + log.debug('POST request:', url, opts) local args = { - callback = callback, on_error = function(err) + log.debug('POST error:', err) callback(nil, err and err.stderr or err) end, } @@ -376,13 +406,8 @@ M.curl_post = async.wrap(function(url, opts, callback) args = vim.tbl_deep_extend('force', M.curl_args, args) args = vim.tbl_deep_extend('force', args, opts or {}) - if args.json_response then - args.headers = vim.tbl_deep_extend('force', args.headers or {}, { - Accept = 'application/json', - }) - end - args.callback = function(response) + log.debug('POST response:', url, response) if response and not vim.startswith(tostring(response.status), '20') then callback(response, response.body) return @@ -402,6 +427,12 @@ M.curl_post = async.wrap(function(url, opts, callback) end end + if args.json_response then + args.headers = vim.tbl_deep_extend('force', args.headers or {}, { + Accept = 'application/json', + }) + end + if args.json_request then args.headers = vim.tbl_deep_extend('force', args.headers or {}, { ['Content-Type'] = 'application/json', @@ -413,7 +444,18 @@ M.curl_post = async.wrap(function(url, opts, callback) curl.post(url, args) end, 3) ----@class CopilotChat.utils.scan_dir_opts +local function filter_files(files, max_count) + files = vim.tbl_filter(function(file) + return file ~= '' and M.filetype(file) ~= nil + end, files) + if max_count and max_count > 0 then + files = vim.list_slice(files, 1, max_count) + end + + return files +end + +---@class CopilotChat.utils.ScanOpts ---@field max_count number? The maximum number of files to scan ---@field max_depth number? The maximum depth to scan ---@field glob? string The glob pattern to match files @@ -421,30 +463,19 @@ end, 3) ---@field no_ignore? boolean Whether to respect or ignore .gitignore --- Scan a directory ----@param path string The directory path ----@param opts CopilotChat.utils.scan_dir_opts? The options +---@param path string +---@param opts CopilotChat.utils.ScanOpts? ---@async -M.scan_dir = async.wrap(function(path, opts, callback) +M.glob = async.wrap(function(path, opts, callback) opts = vim.tbl_deep_extend('force', M.scan_args, opts or {}) - local function filter_files(files) - files = vim.tbl_filter(function(file) - return file ~= '' and M.filetype(file) ~= nil - end, files) - if opts.max_count and opts.max_count > 0 then - files = vim.list_slice(files, 1, opts.max_count) - end - - return files - end - -- Use ripgrep if available if vim.fn.executable('rg') == 1 then local cmd = { 'rg' } - if opts.glob then + if opts.pattern then table.insert(cmd, '-g') - table.insert(cmd, opts.glob) + table.insert(cmd, opts.pattern) end if opts.max_depth then @@ -466,7 +497,7 @@ M.scan_dir = async.wrap(function(path, opts, callback) vim.system(cmd, { text = true }, function(result) local files = {} if result and result.code == 0 and result.stdout ~= '' then - files = filter_files(vim.split(result.stdout, '\n')) + files = filter_files(vim.split(result.stdout, '\n'), opts.max_count) end callback(files) @@ -484,12 +515,71 @@ M.scan_dir = async.wrap(function(path, opts, callback) search_pattern = opts.glob and M.glob_to_pattern(opts.glob) or nil, respect_gitignore = not opts.no_ignore, on_exit = function(files) - callback(filter_files(files)) + callback(filter_files(files, opts.max_count)) end, }) ) end, 3) +--- Grep a directory +---@param path string The path to search +---@param opts CopilotChat.utils.ScanOpts? +M.grep = async.wrap(function(path, opts, callback) + opts = vim.tbl_deep_extend('force', M.scan_args, opts or {}) + local cmd = {} + + if vim.fn.executable('rg') == 1 then + table.insert(cmd, 'rg') + + if opts.max_depth then + table.insert(cmd, '--max-depth') + table.insert(cmd, tostring(opts.max_depth)) + end + + if opts.no_ignore then + table.insert(cmd, '--no-ignore') + end + + if opts.hidden then + table.insert(cmd, '--hidden') + end + + table.insert(cmd, '--files-with-matches') + table.insert(cmd, '--ignore-case') + + if opts.pattern then + table.insert(cmd, '-e') + table.insert(cmd, "'" .. opts.pattern .. "'") + end + + table.insert(cmd, path) + elseif vim.fn.executable('grep') == 1 then + table.insert(cmd, 'grep') + table.insert(cmd, '-rli') + + if opts.pattern then + table.insert(cmd, '-e') + table.insert(cmd, "'" .. opts.pattern .. "'") + end + + table.insert(cmd, path) + end + + if M.empty(cmd) then + error('No executable found for grep') + return + end + + vim.system(cmd, { text = true }, function(result) + local files = {} + if result and result.code == 0 and result.stdout ~= '' then + files = filter_files(vim.split(result.stdout, '\n'), opts.max_count) + end + + callback(files) + end) +end, 3) + --- Get last modified time of a file ---@param path string The file path ---@return number? @@ -587,6 +677,46 @@ M.ts_parse = async.wrap(function(parser, callback) end end, 2) +--- Wait for a user input +M.input = async.wrap(function(opts, callback) + local fn = function() + vim.ui.input(opts, function(input) + if input == nil or input == '' then + callback(nil) + return + end + + callback(input) + end) + end + + if vim.in_fast_event() then + vim.schedule(fn) + else + fn() + end +end, 2) + +--- Select an item from a list +M.select = async.wrap(function(choices, opts, callback) + local fn = function() + vim.ui.select(choices, opts, function(item) + if item == nil or item == '' then + callback(nil) + return + end + + callback(item) + end) + end + + if vim.in_fast_event() then + vim.schedule(fn) + else + fn() + end +end, 3) + --- Get the info for a key. ---@param name string ---@param key table @@ -770,40 +900,4 @@ function M.glob_to_pattern(g) return p end ----@class CopilotChat.Diagnostic ----@field content string ----@field start_line number ----@field end_line number ----@field severity string - ---- Get diagnostics in a given range ---- @param bufnr number ---- @param start_line number? ---- @param end_line number? ---- @return table|nil -function M.diagnostics(bufnr, start_line, end_line) - local diagnostics = vim.diagnostic.get(bufnr) - local range_diagnostics = {} - local severity = { - [1] = 'ERROR', - [2] = 'WARNING', - [3] = 'INFORMATION', - [4] = 'HINT', - } - - for _, diagnostic in ipairs(diagnostics) do - local lnum = diagnostic.lnum + 1 - if (not start_line or lnum >= start_line) and (not end_line or lnum <= end_line) then - table.insert(range_diagnostics, { - severity = severity[diagnostic.severity], - content = diagnostic.message, - start_line = lnum, - end_line = diagnostic.end_lnum and diagnostic.end_lnum + 1 or lnum, - }) - end - end - - return #range_diagnostics > 0 and range_diagnostics or nil -end - return M diff --git a/plugin/CopilotChat.lua b/plugin/CopilotChat.lua index 5674d6bc..de5e0158 100644 --- a/plugin/CopilotChat.lua +++ b/plugin/CopilotChat.lua @@ -8,14 +8,29 @@ if vim.fn.has('nvim-' .. min_version) ~= 1 then return end +local group = vim.api.nvim_create_augroup('CopilotChat', {}) + -- Setup highlights -vim.api.nvim_set_hl(0, 'CopilotChatStatus', { link = 'DiagnosticHint', default = true }) -vim.api.nvim_set_hl(0, 'CopilotChatHelp', { link = 'DiagnosticInfo', default = true }) -vim.api.nvim_set_hl(0, 'CopilotChatKeyword', { link = 'Keyword', default = true }) -vim.api.nvim_set_hl(0, 'CopilotChatInput', { link = 'Special', default = true }) -vim.api.nvim_set_hl(0, 'CopilotChatSelection', { link = 'Visual', default = true }) -vim.api.nvim_set_hl(0, 'CopilotChatHeader', { link = '@markup.heading.2.markdown', default = true }) -vim.api.nvim_set_hl(0, 'CopilotChatSeparator', { link = '@punctuation.special.markdown', default = true }) +local function setup_highlights() + vim.api.nvim_set_hl(0, 'CopilotChatHeader', { link = '@markup.heading.2.markdown', default = true }) + vim.api.nvim_set_hl(0, 'CopilotChatSeparator', { link = '@punctuation.special.markdown', default = true }) + vim.api.nvim_set_hl(0, 'CopilotChatStatus', { link = 'DiagnosticHint', default = true }) + vim.api.nvim_set_hl(0, 'CopilotChatHelp', { link = 'DiagnosticInfo', default = true }) + vim.api.nvim_set_hl(0, 'CopilotChatKeyword', { link = 'Keyword', default = true }) + vim.api.nvim_set_hl(0, 'CopilotChatSelection', { link = 'Visual', default = true }) + vim.api.nvim_set_hl(0, 'CopilotChatAnnotation', { link = 'ColorColumn', default = true }) + + local fg = vim.api.nvim_get_hl(0, { name = 'CopilotChatStatus', link = false }).fg + local bg = vim.api.nvim_get_hl(0, { name = 'CopilotChatAnnotation', link = false }).bg + vim.api.nvim_set_hl(0, 'CopilotChatAnnotationHeader', { fg = fg, bg = bg }) +end +vim.api.nvim_create_autocmd('ColorScheme', { + group = group, + callback = function() + setup_highlights() + end, +}) +setup_highlights() -- Setup commands vim.api.nvim_create_user_command('CopilotChat', function(args) @@ -39,10 +54,6 @@ vim.api.nvim_create_user_command('CopilotChatModels', function() local chat = require('CopilotChat') chat.select_model() end, { force = true }) -vim.api.nvim_create_user_command('CopilotChatAgents', function() - local chat = require('CopilotChat') - chat.select_agent() -end, { force = true }) vim.api.nvim_create_user_command('CopilotChatOpen', function() local chat = require('CopilotChat') chat.open() @@ -90,7 +101,7 @@ end, { nargs = '*', force = true, complete = complete_load }) -- with "rooter" plugins, LSP and stuff as vim.fn.getcwd() when -- i pass window number inside doesnt work vim.api.nvim_create_autocmd({ 'VimEnter', 'WinEnter', 'DirChanged' }, { - group = vim.api.nvim_create_augroup('CopilotChat', {}), + group = group, callback = function() vim.w.cchat_cwd = vim.fn.getcwd() end, From 32de1bbfacfb7905afb521c7ea22deaa92442d1e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 28 Jul 2025 00:22:08 +0000 Subject: [PATCH 06/75] chore(doc): auto generate docs --- doc/CopilotChat.txt | 312 +++++++++++++++----------------------------- 1 file changed, 106 insertions(+), 206 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index e5863a11..f8e9ea04 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 July 24 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 July 28 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -14,8 +14,8 @@ Table of Contents *CopilotChat-table-of-contents* - Commands |CopilotChat-commands| - Key Mappings |CopilotChat-key-mappings| - Prompts |CopilotChat-prompts| - - Models and Agents |CopilotChat-models-and-agents| - - Contexts |CopilotChat-contexts| + - Models |CopilotChat-models| + - Functions |CopilotChat-functions| - Selections |CopilotChat-selections| - Providers |CopilotChat-providers| 4. Configuration |CopilotChat-configuration| @@ -36,14 +36,14 @@ Table of Contents *CopilotChat-table-of-contents* CopilotChat.nvim is a Neovim plugin that brings GitHub Copilot Chat capabilities directly into your editor. It provides: -- πŸ€– GitHub Copilot Chat integration with official model and agent support (GPT-4o, Claude 3.7 Sonnet, Gemini 2.0 Flash, and more) +- πŸ€– GitHub Copilot Chat integration with official model support (GPT-4o, Claude 3.7 Sonnet, Gemini 2.0 Flash, and more) - πŸ’» Rich workspace context powered by smart embeddings system -- πŸ”’ Explicit context sharing - only sends what you specifically request, either as context or selection (by default visual selection) -- πŸ”Œ Modular provider architecture supporting both official and custom LLM backends (Ollama, LM Studio, Mistral.ai and more) +- πŸ”’ Explicit data sharing - only sends what you specifically request, either as resource or selection (by default visual selection) +- πŸ”Œ Modular provider architecture supporting both official and custom LLM backends (Ollama, Gemini, Mistral.ai and more) - πŸ“ Interactive chat UI with completion, diffs and quickfix integration - 🎯 Powerful prompt system with composable templates and sticky prompts -- πŸ”„ Extensible context providers for granular workspace understanding (buffers, files, git diffs, URLs, and more) -- ⚑ Efficient token usage with tiktoken token counting and memory management +- πŸ”„ Extensible function calling system for granular workspace understanding (buffers, files, git diffs, URLs, and more) +- ⚑ Efficient token usage with tiktoken token counting and history management ============================================================================== @@ -85,8 +85,7 @@ Plugin features that use picker: - `:CopilotChatPrompts` - for selecting prompts - `:CopilotChatModels` - for selecting models -- `:CopilotChatAgents` - for selecting agents -- `#:` - for selecting context input +- `#:` - for selecting function input ============================================================================== @@ -103,7 +102,7 @@ LAZY.NVIM *CopilotChat-lazy.nvim* { "github/copilot.vim" }, -- or zbirenbaum/copilot.lua { "nvim-lua/plenary.nvim", branch = "master" }, -- for curl, log and async functions }, - build = "make tiktoken", -- Only on MacOS or Linux + build = "make tiktoken", opts = { -- See Configuration section for options }, @@ -181,7 +180,6 @@ Commands are used to control the chat interface: :CopilotChatLoad ? Load chat history :CopilotChatPrompts View/select prompt templates :CopilotChatModels View/select available models - :CopilotChatAgents View/select available agents :CopilotChat Use specific prompt template KEY MAPPINGS *CopilotChat-key-mappings* @@ -292,7 +290,7 @@ Define your own system prompts in the configuration (similar to `prompts`): STICKY PROMPTS ~ Sticky prompts persist across chat sessions. They’re useful for maintaining -context or agent selection. They work as follows: +model or resource selection. They work as follows: 1. Prefix text with `>` using markdown blockquote syntax 2. The prompt will be copied at the start of every new chat prompt @@ -301,7 +299,7 @@ context or agent selection. They work as follows: Examples: >markdown - > #files + > #glob:`*.lua` > List all files in the workspace > @models Using Mistral-small @@ -313,17 +311,13 @@ You can also set default sticky prompts in the configuration: >lua { sticky = { - '@models Using Mistral-small', - '#files', + '#glob:*.lua', } } < -MODELS AND AGENTS *CopilotChat-models-and-agents* - - -MODELS ~ +MODELS *CopilotChat-models* You can control which AI model to use in three ways: @@ -337,72 +331,84 @@ For supported models, see: - GitHub Marketplace Models (experimental, limited usage) -AGENTS ~ +FUNCTIONS *CopilotChat-functions* + +Functions provide additional information and behaviour to the chat. Tools can +be organized into groups by setting the `group` property. Tools assigned to a +group are not automatically made available to the LLM - they must be explicitly +activated. To use grouped tools in your prompt, include `@group_name` in your +message. This allows the LLM to access and use all tools in that group during +the current interaction. Add tools using `#tool_name[:input]` syntax: -Agents determine the AI assistant’s capabilities. Control agents in three -ways: + -------------------------------------------------------------------------- + Function Input Description + Support + ------------- ------------ ----------------------------------------------- + buffer βœ“ (name) Retrieves content from a specific buffer -1. List available agents with `:CopilotChatAgents` -2. Set agent in prompt with `@agent_name` -3. Configure default agent via `agent` config key + buffers βœ“ (scope) Fetches content from multiple buffers + (listed/visible) -The default "noop" agent is `none`. For more information: + diagnostics βœ“ (scope) Collects code diagnostics (errors, warnings) -- Extension Agents Documentation -- Available Agents + file βœ“ (path) Reads content from a specified file path + gitdiff βœ“ (sha) Retrieves git diff information + (unstaged/staged/sha) -CONTEXTS *CopilotChat-contexts* + gitstatus - Retrieves git status information -Contexts provide additional information to the chat. Add context using -`#context_name[:input]` syntax: + glob βœ“ (pattern) Lists filenames matching a pattern in workspace - Context Input Support Description - ----------- --------------- ------------------------------------- - buffer βœ“ (number) Current or specified buffer content - buffers βœ“ (type) All buffers content (listed/all) - file βœ“ (path) Content of specified file - files βœ“ (glob) Workspace files - filenames βœ“ (glob) Workspace file names - git βœ“ (ref) Git diff (unstaged/staged/commit) - url βœ“ (url) Content from URL - register βœ“ (name) Content of vim register - quickfix - Quickfix list file contents - system βœ“ (command) Output of shell command + grep βœ“ (pattern) Searches for a pattern across files in + workspace - [!TIP] The AI is aware of these context providers and may request additional - context if needed by asking you to input a specific context command like - `#file:path/to/file.js`. + quickfix - Includes content of files in quickfix list + + register βœ“ (register) Provides access to specified Vim register + + url βœ“ (url) Fetches content from a specified URL + -------------------------------------------------------------------------- Examples: >markdown - > #buffer - > #buffer:2 - > #files:\*.lua - > #filenames + > #buffer:init.lua + > #buffers:visible + > #diagnostics:current + > #file:path/to/file.js > #git:staged + > #glob:`**/*.lua` + > #grep:`function setup` + > #quickfix + > #register:+ > #url:https://example.com - > #system:`ls -la | grep lua` < -Define your own contexts in the configuration with input handling and -resolution: +Define your own functions in the configuration with input handling and schema: >lua { - contexts = { + functions = { birthday = { - input = function(callback) - vim.ui.select({ 'user', 'napoleon' }, { - prompt = 'Select birthday> ', - }, callback) - end, + description = "Retrieves birthday information for a person", + uri = "birthday://{name}", + schema = { + type = 'object', + required = { 'name' }, + properties = { + name = { + type = 'string', + enum = { 'Alice', 'Bob', 'Charlie' }, + description = "Person's name", + }, + }, + }, resolve = function(input) return { { - content = input .. ' birthday info', - filename = input .. '_birthday', - filetype = 'text', + uri = 'birthday://' .. input.name, + mimetype = 'text/plain', + data = input.name .. ' birthday info', } } end @@ -412,10 +418,10 @@ resolution: < -EXTERNAL CONTEXTS ~ +EXTERNAL FUNCTIONS ~ -For external contexts, see the contexts discussion page -. +For external functions implementations, see the discussion page +. SELECTIONS *CopilotChat-selections* @@ -481,9 +487,6 @@ Custom providers can implement these methods: -- Optional: Get available models get_models?(headers: table): table, - - -- Optional: Get available agents - get_agents?(headers: table): table, } < @@ -511,19 +514,17 @@ Below are all available configuration options with their default values: system_prompt = 'COPILOT_INSTRUCTIONS', -- System prompt to use (can be specified manually in prompt via /). model = 'gpt-4.1', -- Default model to use, see ':CopilotChatModels' for available models (can be specified manually in prompt via $). - agent = 'copilot', -- Default agent to use, see ':CopilotChatAgents' for available agents (can be specified manually in prompt via @). - context = nil, -- Default context or array of contexts to use (can be specified manually in prompt via #). - sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat. + tools = nil, -- Default tool or array of tools (or groups) to share with LLM (can be specified manually in prompt via @). + sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat (can be specified manually in prompt via >). - temperature = 0.1, -- GPT result temperature + temperature = 0.1, -- Result temperature headless = false, -- Do not write to chat buffer and use history (useful for using custom processing) - stream = nil, -- Function called when receiving stream updates (returned string is appended to the chat buffer) - callback = nil, -- Function called when full response is received (retuned string is stored to history) - remember_as_sticky = true, -- Remember model/agent/context as sticky prompts when asking questions + callback = nil, -- Function called when full response is received + remember_as_sticky = true, -- Remember model as sticky prompts when asking questions -- default selection -- see select.lua for implementation - selection = select.visual, + selection = require('CopilotChat.select').visual, -- default window options window = { @@ -541,9 +542,9 @@ Below are all available configuration options with their default values: }, show_help = true, -- Shows help message as virtual lines when waiting for user input + show_folds = true, -- Shows folds for sections in chat highlight_selection = true, -- Highlight selection highlight_headers = true, -- Highlight headers in chat, disable if using markdown renderers (like render-markdown.nvim) - references_display = 'virtual', -- 'virtual', 'write', Display references in chat as virtual text or write to buffer auto_follow_cursor = true, -- Auto-follow cursor in chat auto_insert_mode = false, -- Automatically enter insert mode when opening window and on new prompt insert_at_end = false, -- Move cursor to end of buffer when inserting text @@ -561,129 +562,29 @@ Below are all available configuration options with their default values: log_path = vim.fn.stdpath('state') .. '/CopilotChat.log', -- Default path to log file history_path = vim.fn.stdpath('data') .. '/copilotchat_history', -- Default path to stored history - question_header = '# User ', -- Header to use for user questions - answer_header = '# Copilot ', -- Header to use for AI answers - error_header = '# Error ', -- Header to use for errors + headers = { + user = '## User ', -- Header to use for user questions + assistant = '## Copilot ', -- Header to use for AI answers + tool = '## Tool ', -- Header to use for tool calls + }, + separator = '───', -- Separator to use in chat -- default providers -- see config/providers.lua for implementation - providers = { - copilot = { - }, - github_models = { - }, - copilot_embeddings = { - }, - }, + providers = require('CopilotChat.config.providers'), - -- default contexts - -- see config/contexts.lua for implementation - contexts = { - buffer = { - }, - buffers = { - }, - file = { - }, - files = { - }, - git = { - }, - url = { - }, - register = { - }, - quickfix = { - }, - system = { - } - }, + -- default functions + -- see config/functions.lua for implementation + functions = require('CopilotChat.config.functions'), -- default prompts -- see config/prompts.lua for implementation - prompts = { - Explain = { - prompt = 'Write an explanation for the selected code as paragraphs of text.', - system_prompt = 'COPILOT_EXPLAIN', - }, - Review = { - prompt = 'Review the selected code.', - system_prompt = 'COPILOT_REVIEW', - }, - Fix = { - prompt = 'There is a problem in this code. Identify the issues and rewrite the code with fixes. Explain what was wrong and how your changes address the problems.', - }, - Optimize = { - prompt = 'Optimize the selected code to improve performance and readability. Explain your optimization strategy and the benefits of your changes.', - }, - Docs = { - prompt = 'Please add documentation comments to the selected code.', - }, - Tests = { - prompt = 'Please generate tests for my code.', - }, - Commit = { - prompt = 'Write commit message for the change with commitizen convention. Keep the title under 50 characters and wrap message at 72 characters. Format as a gitcommit code block.', - context = 'git:staged', - }, - }, + prompts = require('CopilotChat.config.prompts'), -- default mappings -- see config/mappings.lua for implementation - mappings = { - complete = { - insert = '', - }, - close = { - normal = 'q', - insert = '', - }, - reset = { - normal = '', - insert = '', - }, - submit_prompt = { - normal = '', - insert = '', - }, - toggle_sticky = { - normal = 'grr', - }, - clear_stickies = { - normal = 'grx', - }, - accept_diff = { - normal = '', - insert = '', - }, - jump_to_diff = { - normal = 'gj', - }, - quickfix_answers = { - normal = 'gqa', - }, - quickfix_diffs = { - normal = 'gqd', - }, - yank_diff = { - normal = 'gy', - register = '"', -- Default register to use for yanking - }, - show_diff = { - normal = 'gd', - full_diff = false, -- Show full diff instead of unified diff when showing diff window - }, - show_info = { - normal = 'gi', - }, - show_context = { - normal = 'gc', - }, - show_help = { - normal = 'gh', - }, - }, + mappings = require('CopilotChat.config.mappings'), } < @@ -719,8 +620,8 @@ Types of copilot highlights: - `CopilotChatStatus` - Status and spinner in chat buffer - `CopilotChatHelp` - Help messages in chat buffer (help, references) - `CopilotChatSelection` - Selection highlight in source buffer -- `CopilotChatKeyword` - Keyword highlight in chat buffer (e.g.Β prompts, contexts) -- `CopilotChatInput` - Input highlight in chat buffer (for contexts) +- `CopilotChatKeyword` - Keyword highlight in chat buffer (e.g.Β prompts, tools) +- `CopilotChatAnnotation` - Annotation highlight in chat buffer (file headers, tool call headers, tool call body) ============================================================================== @@ -736,8 +637,7 @@ CORE *CopilotChat-core* chat.ask(prompt, config) -- Ask a question with optional config chat.response() -- Get the last response text chat.resolve_prompt() -- Resolve prompt references - chat.resolve_context() -- Resolve context embeddings (WARN: async, requires plenary.async.run) - chat.resolve_agent() -- Resolve agent from prompt (WARN: async, requires plenary.async.run) + chat.resolve_tools() -- Resolve tools that are available for automatic use by LLM chat.resolve_model() -- Resolve model from prompt (WARN: async, requires plenary.async.run) -- Window Management @@ -755,10 +655,9 @@ CORE *CopilotChat-core* chat.get_selection() -- Get the current selection chat.set_selection(bufnr, start_line, end_line, clear) -- Set or clear selection - -- Prompt & Context Management + -- Prompt & Model Management chat.select_prompt(config) -- Open prompt selector with optional config chat.select_model() -- Open model selector - chat.select_agent() -- Open agent selector chat.prompts() -- Get all available prompts -- Completion @@ -787,10 +686,12 @@ You can also access the chat window UI methods through the `chat.chat` object: window:visible() -- Check if chat window is visible window:focused() -- Check if chat window is focused + -- Message Management + window:get_message(role) -- Get last chat message by role (user, assistant, tool) + window:add_message({ role, content }, replace) -- Add or replace a message in chat + window:add_sticky(sticky) -- Add sticky prompt to chat message + -- Content Management - window:get_prompt() -- Get current prompt from chat window - window:set_prompt(prompt) -- Set prompt in chat window - window:add_sticky(sticky) -- Add sticky prompt to chat window window:append(text) -- Append text to chat window window:clear() -- Clear chat window content window:finish() -- Finish writing to chat window @@ -800,9 +701,9 @@ You can also access the chat window UI methods through the `chat.chat` object: window:focus() -- Focus the chat window -- Advanced Features - window:get_closest_section() -- Get section closest to cursor - window:get_closest_block() -- Get code block closest to cursor - window:overlay(opts) -- Show overlay with specified options + window:get_closest_message(role) -- Get message closest to cursor + window:get_closest_block(role) -- Get code block closest to cursor + window:overlay(opts) -- Show overlay with specified options < @@ -811,19 +712,18 @@ EXAMPLE USAGE *CopilotChat-example-usage* >lua -- Open chat, ask a question and handle response require("CopilotChat").open() - require("CopilotChat").ask("Explain this code", { + require("CopilotChat").ask("#buffer Explain this code", { callback = function(response) vim.notify("Got response: " .. response:sub(1, 50) .. "...") return response end, - context = "buffer" }) -- Save and load chat history require("CopilotChat").save("my_debugging_session") require("CopilotChat").load("my_debugging_session") - -- Use custom context and model + -- Use custom sticky and model require("CopilotChat").ask("How can I optimize this?", { model = "gpt-4.1", context = {"buffer", "git:staged"} @@ -880,7 +780,7 @@ See CONTRIBUTING.md for detailed guidelines. Thanks goes to these wonderful people (emoji key ): -gptlangπŸ’» πŸ“–Dung Duc Huynh (Kaka)πŸ’» πŸ“–Ahmed HaracicπŸ’»TrΓ­ Thiện Nguyα»…nπŸ’»He ZhizhouπŸ’»Guruprakash RajakkannuπŸ’»kristofkaπŸ’»PostCyberPunkπŸ“–Katsuhiko NishimraπŸ’»Erno HopearuohoπŸ’»Shaun GarwoodπŸ’»neutrinoA4πŸ’» πŸ“–Jack MuratoreπŸ’»Adriel VelazquezπŸ’» πŸ“–Tomas SlusnyπŸ’» πŸ“–NisalπŸ“–Tobias GΓ₯rdhusπŸ“–Petr DlouhΓ½πŸ“–Dylan MadisettiπŸ’»Aaron WeisbergπŸ’» πŸ“–Jose TlacuiloπŸ’» πŸ“–Kevin TraverπŸ’» πŸ“–dTryπŸ’»Arata FurukawaπŸ’»LingπŸ’»Ivan FrolovπŸ’»Folke LemaitreπŸ’» πŸ“–GitMurfπŸ’»Dmitrii LipinπŸ’»jinzhongjiaπŸ“–guillπŸ’»Sjon-Paul BrownπŸ’»Renzo MondragΓ³nπŸ’» πŸ“–fjchen7πŸ’»RadosΕ‚aw WoΕΊniakπŸ’»JakubPecenkaπŸ’»thomastthaiπŸ“–TomΓ‘Ε‘ JanouΕ‘ekπŸ’»Toddneal StallworthπŸ“–Sergey AlexandrovπŸ’»LΓ©opold MebazaaπŸ’»JunKi JinπŸ’»abdennourzahafπŸ“–JosiahπŸ’»Tony FischerπŸ’» πŸ“–Kohei WadaπŸ’»Sebastian YaghoubiπŸ“–johncmingπŸ’»Rokas BrazdΕΎionisπŸ’»SolaπŸ“– πŸ’»Mani ChandraπŸ’»Nischal BasutiπŸ“–Teo LjungbergπŸ’»Joe PriceπŸ’»Yufan YouπŸ“– πŸ’»Manish KumarπŸ’»Anton Ε½danovπŸ“– πŸ’»Fredrik AverpilπŸ’»Aaron D BordenπŸ’»This project follows the all-contributors +gptlangπŸ’» πŸ“–Dung Duc Huynh (Kaka)πŸ’» πŸ“–Ahmed HaracicπŸ’»TrΓ­ Thiện Nguyα»…nπŸ’»He ZhizhouπŸ’»Guruprakash RajakkannuπŸ’»kristofkaπŸ’»PostCyberPunkπŸ“–Katsuhiko NishimraπŸ’»Erno HopearuohoπŸ’»Shaun GarwoodπŸ’»neutrinoA4πŸ’» πŸ“–Jack MuratoreπŸ’»Adriel VelazquezπŸ’» πŸ“–Tomas SlusnyπŸ’» πŸ“–NisalπŸ“–Tobias GΓ₯rdhusπŸ“–Petr DlouhΓ½πŸ“–Dylan MadisettiπŸ’»Aaron WeisbergπŸ’» πŸ“–Jose TlacuiloπŸ’» πŸ“–Kevin TraverπŸ’» πŸ“–dTryπŸ’»Arata FurukawaπŸ’»LingπŸ’»Ivan FrolovπŸ’»Folke LemaitreπŸ’» πŸ“–GitMurfπŸ’»Dmitrii LipinπŸ’»jinzhongjiaπŸ“–guillπŸ’»Sjon-Paul BrownπŸ’»Renzo MondragΓ³nπŸ’» πŸ“–fjchen7πŸ’»RadosΕ‚aw WoΕΊniakπŸ’»JakubPecenkaπŸ’»thomastthaiπŸ“–TomΓ‘Ε‘ JanouΕ‘ekπŸ’»Toddneal StallworthπŸ“–Sergey AlexandrovπŸ’»LΓ©opold MebazaaπŸ’»JunKi JinπŸ’»abdennourzahafπŸ“–JosiahπŸ’»Tony FischerπŸ’» πŸ“–Kohei WadaπŸ’»Sebastian YaghoubiπŸ“–johncmingπŸ’»Rokas BrazdΕΎionisπŸ’»SolaπŸ“– πŸ’»Mani ChandraπŸ’»Nischal BasutiπŸ“–Teo LjungbergπŸ’»Joe PriceπŸ’»Yufan YouπŸ“– πŸ’»Manish KumarπŸ’»Anton Ε½danovπŸ“– πŸ’»Fredrik AverpilπŸ’»Aaron D BordenπŸ’»Md. Iftakhar Awal ChowdhuryπŸ’» πŸ“–This project follows the all-contributors specification. Contributions of any kind are welcome! From b4b7f9c2bb34d43b18dbbe0a889881630e217bc3 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 28 Jul 2025 04:30:14 +0200 Subject: [PATCH 07/75] fix: update to latest lua actions and update README (#1196) Signed-off-by: Tomas Slusny --- .github/workflows/ci.yml | 8 +++++--- README.md | 5 ++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d5831a56..0a96b7f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,12 +53,14 @@ jobs: version: nightly - name: luajit - uses: leafo/gh-actions-lua@v10 + uses: leafo/gh-actions-lua@v11 with: - luaVersion: "luajit-openresty" + luaVersion: "luajit-2.1" - name: luarocks - uses: leafo/gh-actions-luarocks@v4 + uses: leafo/gh-actions-luarocks@v5 + with: + luarocksVersion: "3.12.2" - name: run test shell: bash diff --git a/README.md b/README.md index 6c6eb93f..0535af19 100644 --- a/README.md +++ b/README.md @@ -166,8 +166,7 @@ Default mappings in the chat interface: | - | `gqd` | Add all diffs from chat to quickfix list | | - | `gy` | Yank nearest diff to register | | - | `gd` | Show diff between source and nearest diff | -| - | `gi` | Show info about current chat | -| - | `gc` | Show current chat context | +| - | `gc` | Show info about current chat | | - | `gh` | Show help message | The mappings can be customized by setting the `mappings` table in your configuration. Each mapping can have: @@ -648,7 +647,7 @@ require("CopilotChat").load("my_debugging_session") -- Use custom sticky and model require("CopilotChat").ask("How can I optimize this?", { model = "gpt-4.1", - context = {"buffer", "git:staged"} + sticky = {"#buffer", "#gitdiff:staged"} }) ``` From 84a5728b91970b162a116ad000c7084eeef4315c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 28 Jul 2025 02:30:31 +0000 Subject: [PATCH 08/75] chore(doc): auto generate docs --- doc/CopilotChat.txt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index f8e9ea04..d2567481 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -200,8 +200,7 @@ Default mappings in the chat interface: - gqd Add all diffs from chat to quickfix list - gy Yank nearest diff to register - gd Show diff between source and nearest diff - - gi Show info about current chat - - gc Show current chat context + - gc Show info about current chat - gh Show help message The mappings can be customized by setting the `mappings` table in your configuration. Each mapping can have: @@ -726,7 +725,7 @@ EXAMPLE USAGE *CopilotChat-example-usage* -- Use custom sticky and model require("CopilotChat").ask("How can I optimize this?", { model = "gpt-4.1", - context = {"buffer", "git:staged"} + sticky = {"#buffer", "#gitdiff:staged"} }) < From a9e2e657f5b5b8ec212283466038688d98ea4038 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 28 Jul 2025 04:39:05 +0200 Subject: [PATCH 09/75] chore: allow manual dispatch on actions (#1197) Signed-off-by: Tomas Slusny --- .github/workflows/ci.yml | 1 + .github/workflows/release.yml | 4 ++-- .github/workflows/todo.yml | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a96b7f3..e722714f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,7 @@ on: branches: [main] pull_request: branches: [main] + workflow_dispatch: jobs: lint: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a809e59e..cc8398cc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,8 +1,8 @@ name: Release on: push: - branches: - - main + branches: [main] + workflow_dispatch: permissions: contents: write diff --git a/.github/workflows/todo.yml b/.github/workflows/todo.yml index d70ff362..ebf3e56e 100644 --- a/.github/workflows/todo.yml +++ b/.github/workflows/todo.yml @@ -1,6 +1,7 @@ name: "Convert TODO to Issue" on: push: + branches: [main] workflow_dispatch: inputs: MANUAL_COMMIT_REF: From dd0616661505a3c4892ddcdb9517b720a74e59b8 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 28 Jul 2025 05:28:13 +0200 Subject: [PATCH 10/75] fix(functions): properly handle multiple tool calls at once (#1198) Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 3 ++- lua/CopilotChat/ui/chat.lua | 29 ++++++++++++----------------- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 1f592349..c4da582e 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -926,9 +926,10 @@ function M.ask(prompt, config) for _, tool in ipairs(resolved_tools) do M.chat:add_message({ + id = tool.id, role = 'tool', tool_call_id = tool.id, - content = tool.result .. '\n', + content = '\n' .. tool.result .. '\n', }) end diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index f54e9435..0b1fdc32 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -397,22 +397,23 @@ end function Chat:add_message(message, replace) local current_message = self.messages[#self.messages] - local needs_header = false + local is_new = not current_message + or current_message.role ~= message.role + or (message.id and current_message.id ~= message.id) - -- Check if we need to add a header (role change or first message) - if not current_message or current_message.role ~= message.role then - needs_header = true - end - - -- Add appropriate header based on role - if needs_header then + if is_new then + -- Add appropriate header based on role and generate a new ID if not provided message.id = message.id or utils.uuid() local header = self.headers[message.role] if current_message then header = '\n' .. header end + + table.insert(self.messages, message) self:append(header .. '(' .. message.id .. ')' .. self.separator .. '\n\n') + self:append(message.content) elseif replace and current_message then + -- Replace the content of the current message self:append('') self:render() @@ -432,15 +433,9 @@ function Chat:add_message(message, replace) end self:append('') - return - end - - -- Handle message content combining or creation - if current_message and current_message.role == message.role then - current_message.content = current_message.content .. message.content - self:append(message.content) else - table.insert(self.messages, message) + -- Append to the current message + current_message.content = current_message.content .. message.content self:append(message.content) end end @@ -561,7 +556,7 @@ function Chat:render() local current_block = nil local function parse_header(header, line) - return line:match('^' .. vim.pesc(header) .. '%(([%w%-]+)%)' .. vim.pesc(self.separator) .. '$') + return line:match('^' .. vim.pesc(header) .. '%(([^)]+)%)' .. vim.pesc(self.separator) .. '$') end for l, line in ipairs(lines) do From e0df6d1242af29b6262b0eb3e4248568c57c4b3e Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 28 Jul 2025 05:34:36 +0200 Subject: [PATCH 11/75] fix(quickfix): use new chat messages instead of old chat sections for populating qf (#1199) Signed-off-by: Tomas Slusny --- lua/CopilotChat/config/mappings.lua | 58 +++++++++++++++-------------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/lua/CopilotChat/config/mappings.lua b/lua/CopilotChat/config/mappings.lua index a527f0c3..bdcb93ce 100644 --- a/lua/CopilotChat/config/mappings.lua +++ b/lua/CopilotChat/config/mappings.lua @@ -263,18 +263,18 @@ return { normal = 'gqa', callback = function() local items = {} - for i, section in ipairs(copilot.chat.sections) do - if section.role == 'assistant' then - local prev_section = copilot.chat.sections[i - 1] + for i, message in ipairs(copilot.chat.messages) do + if message.section and message.role == 'assistant' then + local prev_message = copilot.chat.messages[i - 1] local text = '' - if prev_section then - text = prev_section.content + if prev_message then + text = prev_message.content end table.insert(items, { bufnr = copilot.chat.bufnr, - lnum = section.start_line, - end_lnum = section.end_line, + lnum = message.section.start_line, + end_lnum = message.section.end_line, text = text, }) end @@ -291,32 +291,34 @@ return { local selection = copilot.get_selection() local items = {} - for _, section in ipairs(copilot.chat.sections) do - for _, block in ipairs(section.blocks) do - local header = block.header + for _, message in ipairs(copilot.chat.messages) do + if message.section then + for _, block in ipairs(message.section.blocks) do + local header = block.header - if not header.start_line and selection then - header.filename = selection.filename .. ' (selection)' - header.start_line = selection.start_line - header.end_line = selection.end_line - end + if not header.start_line and selection then + header.filename = selection.filename .. ' (selection)' + header.start_line = selection.start_line + header.end_line = selection.end_line + end - local text = string.format('%s (%s)', header.filename, header.filetype) - if header.start_line and header.end_line then - text = text .. string.format(' [lines %d-%d]', header.start_line, header.end_line) - end + local text = string.format('%s (%s)', header.filename, header.filetype) + if header.start_line and header.end_line then + text = text .. string.format(' [lines %d-%d]', header.start_line, header.end_line) + end - table.insert(items, { - bufnr = copilot.chat.bufnr, - lnum = block.start_line, - end_lnum = block.end_line, - text = text, - }) + table.insert(items, { + bufnr = copilot.chat.bufnr, + lnum = block.start_line, + end_lnum = block.end_line, + text = text, + }) + end end - end - vim.fn.setqflist(items) - vim.cmd('copen') + vim.fn.setqflist(items) + vim.cmd('copen') + end end, }, From 946069a03946ce35619cbacc3a6757819d096ac5 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 28 Jul 2025 16:45:55 +0200 Subject: [PATCH 12/75] fix(functions): properly resolve defaults for diagnostics (#1201) Closes #1200 Signed-off-by: Tomas Slusny --- lua/CopilotChat/config/functions.lua | 4 ++-- lua/CopilotChat/functions.lua | 7 +------ 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/lua/CopilotChat/config/functions.lua b/lua/CopilotChat/config/functions.lua index 07b11573..1644a03f 100644 --- a/lua/CopilotChat/config/functions.lua +++ b/lua/CopilotChat/config/functions.lua @@ -258,12 +258,12 @@ return { diagnostics = { group = 'copilot', - uri = 'neovim://diagnostics/{scope}', + uri = 'neovim://diagnostics/{scope}/{severity}', description = 'Collects code diagnostics (errors, warnings, etc.) from specified buffers. Helpful for troubleshooting and fixing code issues.', schema = { type = 'object', - required = { 'scope' }, + required = { 'scope', 'severity' }, properties = { scope = { type = 'string', diff --git a/lua/CopilotChat/functions.lua b/lua/CopilotChat/functions.lua index 8a28ca81..6cc287bb 100644 --- a/lua/CopilotChat/functions.lua +++ b/lua/CopilotChat/functions.lua @@ -136,8 +136,7 @@ function M.parse_input(input, schema) local prop_names = sorted_propnames(schema) -- Map input parts to schema properties in sorted order - local i = 1 - for _, prop_name in ipairs(prop_names) do + for i, prop_name in ipairs(prop_names) do local prop_schema = schema.properties[prop_name] local value = not utils.empty(parts[i]) and parts[i] or nil if value == nil and prop_schema.default ~= nil then @@ -145,10 +144,6 @@ function M.parse_input(input, schema) end result[prop_name] = value - i = i + 1 - if i > #parts then - break - end end return result From 6ac77aaa68a0ce7fe3c8c41622ab1986f8f6d2c7 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 28 Jul 2025 17:09:55 +0200 Subject: [PATCH 13/75] feat(resources)!: add option to enable resource processing (#1202) This adds option to enable resource processing, disabled by default. BREAKING CHANGE: intelligent resource processing is now disabled by default, use config.resource_processing: true to reenable Signed-off-by: Tomas Slusny --- README.md | 4 +++- lua/CopilotChat/client.lua | 20 +++----------------- lua/CopilotChat/config.lua | 5 ++++- lua/CopilotChat/init.lua | 14 +++++++++----- 4 files changed, 19 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 0535af19..931ee8ab 100644 --- a/README.md +++ b/README.md @@ -445,10 +445,12 @@ Below are all available configuration options with their default values: tools = nil, -- Default tool or array of tools (or groups) to share with LLM (can be specified manually in prompt via @). sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat (can be specified manually in prompt via >). + resource_processing = false, -- Enable intelligent resource processing (skips unnecessary resources to save tokens) + temperature = 0.1, -- Result temperature headless = false, -- Do not write to chat buffer and use history (useful for using custom processing) callback = nil, -- Function called when full response is received - remember_as_sticky = true, -- Remember model as sticky prompts when asking questions + remember_as_sticky = true, -- Remember config as sticky prompts when asking questions -- default selection -- see select.lua for implementation diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index b6460d0a..93143754 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -61,9 +61,7 @@ local class = utils.class --- Constants local RESOURCE_FORMAT = '# %s\n```%s\n%s\n```' local LINE_CHARACTERS = 100 -local BIG_FILE_THRESHOLD = 1000 * LINE_CHARACTERS local BIG_EMBED_THRESHOLD = 200 * LINE_CHARACTERS -local TRUNCATED = '... (truncated)' --- Resolve provider function ---@param model string @@ -103,16 +101,9 @@ end --- Generate content block with line numbers, truncating if necessary ---@param content string ----@param threshold number: The threshold for truncation ---@param start_line number?: The starting line number ---@return string -local function generate_content_block(content, threshold, start_line) - local total_chars = #content - if total_chars > threshold then - content = content:sub(1, threshold) - content = content .. '\n' .. TRUNCATED - end - +local function generate_content_block(content, start_line) if start_line ~= nil then local lines = vim.split(content, '\n') local total_lines = #lines @@ -144,12 +135,7 @@ local function generate_selection_message(selection) if selection.start_line and selection.end_line then out = out .. string.format('Excerpt from %s, lines %s to %s:\n', filename, selection.start_line, selection.end_line) end - out = out - .. string.format( - '```%s\n%s\n```', - filetype, - generate_content_block(content, BIG_FILE_THRESHOLD, selection.start_line) - ) + out = out .. string.format('```%s\n%s\n```', filetype, generate_content_block(content, selection.start_line)) return { content = out, @@ -167,7 +153,7 @@ local function generate_resource_messages(resources) return resource.data and resource.data ~= '' end) :map(function(resource) - local content = generate_content_block(resource.data, BIG_FILE_THRESHOLD, 1) + local content = generate_content_block(resource.data, 1) return { content = string.format(RESOURCE_FORMAT, resource.name, resource.type, content), diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 5e900219..c456a5b1 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -17,6 +17,7 @@ ---@field model string? ---@field tools string|table|nil ---@field sticky string|table|nil +---@field resource_processing boolean? ---@field temperature number? ---@field headless boolean? ---@field callback nil|fun(response: string, source: CopilotChat.source) @@ -57,10 +58,12 @@ return { tools = nil, -- Default tool or array of tools (or groups) to share with LLM (can be specified manually in prompt via @). sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat (can be specified manually in prompt via >). + resource_processing = false, -- Enable intelligent resource processing (skips unnecessary resources to save tokens) + temperature = 0.1, -- Result temperature headless = false, -- Do not write to chat buffer and use history (useful for using custom processing) callback = nil, -- Function called when full response is received - remember_as_sticky = true, -- Remember model as sticky prompts when asking questions + remember_as_sticky = true, -- Remember config as sticky prompts when asking questions -- default selection selection = require('CopilotChat.select').visual, diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index c4da582e..426ccad3 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -903,11 +903,15 @@ function M.ask(prompt, config) local ok, err = pcall(async.run, function() local selected_tools, resolved_resources, resolved_tools, prompt = M.resolve_functions(prompt, config) local selected_model, prompt = M.resolve_model(prompt, config) - local query_ok, processed_resources = pcall(resources.process_resources, prompt, selected_model, resolved_resources) - if query_ok then - resolved_resources = processed_resources - else - log.warn('Failed to process resources', processed_resources) + + if config.resource_processing then + local query_ok, processed_resources = + pcall(resources.process_resources, prompt, selected_model, resolved_resources) + if query_ok then + resolved_resources = processed_resources + else + log.warn('Failed to process resources', processed_resources) + end end prompt = vim.trim(prompt) From 885801c69f812dbe89c719760f0bca2e51ab060c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 28 Jul 2025 15:10:11 +0000 Subject: [PATCH 14/75] chore(doc): auto generate docs --- doc/CopilotChat.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index d2567481..5e852bdf 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -516,10 +516,12 @@ Below are all available configuration options with their default values: tools = nil, -- Default tool or array of tools (or groups) to share with LLM (can be specified manually in prompt via @). sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat (can be specified manually in prompt via >). + resource_processing = false, -- Enable intelligent resource processing (skips unnecessary resources to save tokens) + temperature = 0.1, -- Result temperature headless = false, -- Do not write to chat buffer and use history (useful for using custom processing) callback = nil, -- Function called when full response is received - remember_as_sticky = true, -- Remember model as sticky prompts when asking questions + remember_as_sticky = true, -- Remember config as sticky prompts when asking questions -- default selection -- see select.lua for implementation From 9d9b2809e1240f9525752ae145799b88d22cd7af Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 28 Jul 2025 20:58:37 +0200 Subject: [PATCH 15/75] feat(ui): improve chat responsiveness by starting spinner early (#1205) Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 15 ++------------- lua/CopilotChat/ui/chat.lua | 35 ++++++++++++++++++++--------------- 2 files changed, 22 insertions(+), 28 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 426ccad3..4e00344b 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -729,19 +729,6 @@ function M.open(config) utils.return_to_normal_mode() M.chat:open(config) - - local message = M.chat:get_message('user') - if message then - local prompt = insert_sticky(message.content, config) - if prompt then - M.chat:add_message({ - role = 'user', - content = '\n' .. prompt, - }, true) - M.chat:finish() - end - end - M.chat:follow() M.chat:focus() end @@ -869,6 +856,8 @@ function M.ask(prompt, config) M.open(config) end + M.chat:start() + local sticky = {} local in_code_block = false for _, line in ipairs(vim.split(prompt, '\n')) do diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 0b1fdc32..669dfa6c 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -382,6 +382,21 @@ function Chat:follow() vim.api.nvim_win_set_cursor(self.winnr, { last_line + 1, last_column }) end +--- Prepare the chat window for writing. +function Chat:start() + self:validate() + + if self:focused() then + utils.return_to_normal_mode() + end + + if self.spinner then + self.spinner:start() + end + + vim.bo[self.bufnr].modifiable = false +end + --- Finish writing to the chat window. function Chat:finish() if not self.spinner then @@ -414,9 +429,7 @@ function Chat:add_message(message, replace) self:append(message.content) elseif replace and current_message then -- Replace the content of the current message - self:append('') self:render() - current_message.content = message.content local section = current_message.section @@ -430,9 +443,8 @@ function Chat:add_message(message, replace) vim.split(message.content, '\n') ) vim.bo[self.bufnr].modifiable = false + self:append('') end - - self:append('') else -- Append to the current message current_message.content = current_message.content .. message.content @@ -476,15 +488,6 @@ end ---@param str string function Chat:append(str) self:validate() - vim.bo[self.bufnr].modifiable = true - - if self:focused() then - utils.return_to_normal_mode() - end - - if self.spinner then - self.spinner:start() - end -- Decide if we should follow cursor after appending text. local should_follow_cursor = self.config.auto_follow_cursor @@ -496,13 +499,14 @@ function Chat:append(str) end local last_line, last_column, _ = self:last() + + vim.bo[self.bufnr].modifiable = true vim.api.nvim_buf_set_text(self.bufnr, last_line, last_column, last_line, last_column, vim.split(str, '\n')) + vim.bo[self.bufnr].modifiable = false if should_follow_cursor then self:follow() end - - vim.bo[self.bufnr].modifiable = false end --- Clear the chat window. @@ -548,6 +552,7 @@ end --- Render the chat window. ---@protected function Chat:render() + self:validate() vim.api.nvim_buf_clear_namespace(self.bufnr, self.header_ns, 0, -1) local lines = vim.api.nvim_buf_get_lines(self.bufnr, 0, -1, false) From dab50896c7e1e80142dd297e6fc75590735b3e9c Mon Sep 17 00:00:00 2001 From: Danilo Horta Date: Mon, 28 Jul 2025 21:58:46 +0100 Subject: [PATCH 16/75] fix: update sticky reference for commit messages (#1207) Changes the commit prompt's sticky reference from '#git:staged' to '#gitdiff:staged' to properly match the expected format for retrieving staged changes when generating commit messages. --- lua/CopilotChat/config/prompts.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/CopilotChat/config/prompts.lua b/lua/CopilotChat/config/prompts.lua index 9fca7d8e..2b56bdda 100644 --- a/lua/CopilotChat/config/prompts.lua +++ b/lua/CopilotChat/config/prompts.lua @@ -204,6 +204,6 @@ return { Commit = { prompt = 'Write commit message for the change with commitizen convention. Keep the title under 50 characters and wrap message at 72 characters. Format as a gitcommit code block.', - sticky = '#git:staged', + sticky = '#gitdiff:staged', }, } From cbb846f9c3979bc8fe5dc7589cdab542adcd224b Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Mon, 28 Jul 2025 23:00:00 +0200 Subject: [PATCH 17/75] docs: add danilohorta as a contributor for code (#1208) * docs: update README.md [skip ci] * docs: update .all-contributorsrc [skip ci] * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .all-contributorsrc | 7 +++++++ README.md | 1 + 2 files changed, 8 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index d335de60..3e77a246 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -424,6 +424,13 @@ "avatar_url": "https://avatars.githubusercontent.com/u/42291930?v=4", "profile": "https://github.com/AtifChy", "contributions": ["code", "doc"] + }, + { + "login": "danilohorta", + "name": "Danilo Horta", + "avatar_url": "https://avatars.githubusercontent.com/u/214497460?v=4", + "profile": "https://github.com/danilohorta", + "contributions": ["code"] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 931ee8ab..c093a813 100644 --- a/README.md +++ b/README.md @@ -777,6 +777,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Fredrik Averpil
Fredrik Averpil

πŸ’» Aaron D Borden
Aaron D Borden

πŸ’» Md. Iftakhar Awal Chowdhury
Md. Iftakhar Awal Chowdhury

πŸ’» πŸ“– + Danilo Horta
Danilo Horta

πŸ’» From e632470171cd82a95c2675360120833c159e7ae0 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 29 Jul 2025 00:48:01 +0200 Subject: [PATCH 18/75] fix(functions): if enum returns only 1 choice auto accept it (#1209) Signed-off-by: Tomas Slusny --- lua/CopilotChat/functions.lua | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lua/CopilotChat/functions.lua b/lua/CopilotChat/functions.lua index 6cc287bb..c23957e5 100644 --- a/lua/CopilotChat/functions.lua +++ b/lua/CopilotChat/functions.lua @@ -166,9 +166,14 @@ function M.enter_input(schema, source) if not schema.required or vim.tbl_contains(schema.required, prop_name) then if cfg.enum then local choices = type(cfg.enum) == 'table' and cfg.enum or cfg.enum(source) - local choice = utils.select(choices, { - prompt = string.format('Select %s> ', prop_name), - }) + local choice + if #choices == 1 then + choice = choices[1] + else + choice = utils.select(choices, { + prompt = string.format('Select %s> ', prop_name), + }) + end table.insert(out, choice or '') elseif cfg.type == 'boolean' then From 4c80c0bbb7d3018908ee99165f3a43cc582aeec4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 28 Jul 2025 22:48:23 +0000 Subject: [PATCH 19/75] chore(doc): auto generate docs --- doc/CopilotChat.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 5e852bdf..aaaa5e72 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -781,7 +781,7 @@ See CONTRIBUTING.md for detailed guidelines. Thanks goes to these wonderful people (emoji key ): -gptlangπŸ’» πŸ“–Dung Duc Huynh (Kaka)πŸ’» πŸ“–Ahmed HaracicπŸ’»TrΓ­ Thiện Nguyα»…nπŸ’»He ZhizhouπŸ’»Guruprakash RajakkannuπŸ’»kristofkaπŸ’»PostCyberPunkπŸ“–Katsuhiko NishimraπŸ’»Erno HopearuohoπŸ’»Shaun GarwoodπŸ’»neutrinoA4πŸ’» πŸ“–Jack MuratoreπŸ’»Adriel VelazquezπŸ’» πŸ“–Tomas SlusnyπŸ’» πŸ“–NisalπŸ“–Tobias GΓ₯rdhusπŸ“–Petr DlouhΓ½πŸ“–Dylan MadisettiπŸ’»Aaron WeisbergπŸ’» πŸ“–Jose TlacuiloπŸ’» πŸ“–Kevin TraverπŸ’» πŸ“–dTryπŸ’»Arata FurukawaπŸ’»LingπŸ’»Ivan FrolovπŸ’»Folke LemaitreπŸ’» πŸ“–GitMurfπŸ’»Dmitrii LipinπŸ’»jinzhongjiaπŸ“–guillπŸ’»Sjon-Paul BrownπŸ’»Renzo MondragΓ³nπŸ’» πŸ“–fjchen7πŸ’»RadosΕ‚aw WoΕΊniakπŸ’»JakubPecenkaπŸ’»thomastthaiπŸ“–TomΓ‘Ε‘ JanouΕ‘ekπŸ’»Toddneal StallworthπŸ“–Sergey AlexandrovπŸ’»LΓ©opold MebazaaπŸ’»JunKi JinπŸ’»abdennourzahafπŸ“–JosiahπŸ’»Tony FischerπŸ’» πŸ“–Kohei WadaπŸ’»Sebastian YaghoubiπŸ“–johncmingπŸ’»Rokas BrazdΕΎionisπŸ’»SolaπŸ“– πŸ’»Mani ChandraπŸ’»Nischal BasutiπŸ“–Teo LjungbergπŸ’»Joe PriceπŸ’»Yufan YouπŸ“– πŸ’»Manish KumarπŸ’»Anton Ε½danovπŸ“– πŸ’»Fredrik AverpilπŸ’»Aaron D BordenπŸ’»Md. Iftakhar Awal ChowdhuryπŸ’» πŸ“–This project follows the all-contributors +gptlangπŸ’» πŸ“–Dung Duc Huynh (Kaka)πŸ’» πŸ“–Ahmed HaracicπŸ’»TrΓ­ Thiện Nguyα»…nπŸ’»He ZhizhouπŸ’»Guruprakash RajakkannuπŸ’»kristofkaπŸ’»PostCyberPunkπŸ“–Katsuhiko NishimraπŸ’»Erno HopearuohoπŸ’»Shaun GarwoodπŸ’»neutrinoA4πŸ’» πŸ“–Jack MuratoreπŸ’»Adriel VelazquezπŸ’» πŸ“–Tomas SlusnyπŸ’» πŸ“–NisalπŸ“–Tobias GΓ₯rdhusπŸ“–Petr DlouhΓ½πŸ“–Dylan MadisettiπŸ’»Aaron WeisbergπŸ’» πŸ“–Jose TlacuiloπŸ’» πŸ“–Kevin TraverπŸ’» πŸ“–dTryπŸ’»Arata FurukawaπŸ’»LingπŸ’»Ivan FrolovπŸ’»Folke LemaitreπŸ’» πŸ“–GitMurfπŸ’»Dmitrii LipinπŸ’»jinzhongjiaπŸ“–guillπŸ’»Sjon-Paul BrownπŸ’»Renzo MondragΓ³nπŸ’» πŸ“–fjchen7πŸ’»RadosΕ‚aw WoΕΊniakπŸ’»JakubPecenkaπŸ’»thomastthaiπŸ“–TomΓ‘Ε‘ JanouΕ‘ekπŸ’»Toddneal StallworthπŸ“–Sergey AlexandrovπŸ’»LΓ©opold MebazaaπŸ’»JunKi JinπŸ’»abdennourzahafπŸ“–JosiahπŸ’»Tony FischerπŸ’» πŸ“–Kohei WadaπŸ’»Sebastian YaghoubiπŸ“–johncmingπŸ’»Rokas BrazdΕΎionisπŸ’»SolaπŸ“– πŸ’»Mani ChandraπŸ’»Nischal BasutiπŸ“–Teo LjungbergπŸ’»Joe PriceπŸ’»Yufan YouπŸ“– πŸ’»Manish KumarπŸ’»Anton Ε½danovπŸ“– πŸ’»Fredrik AverpilπŸ’»Aaron D BordenπŸ’»Md. Iftakhar Awal ChowdhuryπŸ’» πŸ“–Danilo HortaπŸ’»This project follows the all-contributors specification. Contributions of any kind are welcome! From 1d6911fef13952c9b56347485f090baeff77a7e4 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 29 Jul 2025 09:53:21 +0200 Subject: [PATCH 20/75] fix: add back sticky loading on opening window (#1210) Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 4e00344b..6e2de658 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -729,6 +729,19 @@ function M.open(config) utils.return_to_normal_mode() M.chat:open(config) + + local message = M.chat:get_message('user') + if message then + local prompt = insert_sticky(message.content, config) + if prompt then + M.chat:add_message({ + role = 'user', + content = '\n' .. prompt, + }, true) + M.chat:finish() + end + end + M.chat:follow() M.chat:focus() end From bfd1e22216146546ca3b9c2eb8f7afd220998601 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 29 Jul 2025 07:53:38 +0000 Subject: [PATCH 21/75] chore(doc): auto generate docs --- doc/CopilotChat.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index aaaa5e72..799f0f62 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 July 28 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 July 29 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 8a5cda1d90c4d4756dda39cfd748e52cbcde5a99 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 29 Jul 2025 11:27:37 +0200 Subject: [PATCH 22/75] fix(functions): if schema.properties is empty, do not send schema (#1211) This breaks stuff especially with github mcp server Signed-off-by: Tomas Slusny --- lua/CopilotChat/functions.lua | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lua/CopilotChat/functions.lua b/lua/CopilotChat/functions.lua index c23957e5..c9d8488b 100644 --- a/lua/CopilotChat/functions.lua +++ b/lua/CopilotChat/functions.lua @@ -48,10 +48,16 @@ local function filter_schema(tbl) return tbl end + if utils.empty(tbl.properties) then + return nil + end + local result = {} for k, v in pairs(tbl) do - if type(v) ~= 'function' and k ~= 'examples' then - result[k] = type(v) == 'table' and filter_schema(v) or v + if not utils.empty(v) then + if type(v) ~= 'function' and k ~= 'examples' then + result[k] = type(v) == 'table' and filter_schema(v) or v + end end end return result From d905917a025e4c056db28b3082dd474475bad8cd Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 29 Jul 2025 12:35:07 +0200 Subject: [PATCH 23/75] fix(functions): properly escape percent signs in uri inputs (#1212) Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 6e2de658..9900e62f 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -366,9 +366,12 @@ function M.resolve_functions(prompt, config) -- Resolve and process all tools for _, pattern in ipairs(matches:keys()) do - local match = matches:get(pattern) - local out = expand_tool(match.word, match.input) or pattern - prompt = prompt:gsub(vim.pesc(pattern), out, 1) + if not utils.empty(pattern) then + local match = matches:get(pattern) + local out = expand_tool(match.word, match.input) or pattern + out = out:gsub('%%', '%%%%') -- Escape percent signs for gsub + prompt = prompt:gsub(vim.pesc(pattern), out, 1) + end end return functions.parse_tools(enabled_tools), resolved_resources, resolved_tools, prompt From c87f6af0a15cfdaf09826d6fdf5f15ef6a0f782f Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 29 Jul 2025 17:21:39 +0200 Subject: [PATCH 24/75] chore: unify client:models function and improve variable naming (#1213) Signed-off-by: Tomas Slusny --- lua/CopilotChat/client.lua | 40 +++++++++----------------------------- lua/CopilotChat/init.lua | 32 ++++++++++++++++++++++++------ 2 files changed, 35 insertions(+), 37 deletions(-) diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index 93143754..b0a48b85 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -216,15 +216,13 @@ end ---@class CopilotChat.client.Client : Class ---@field private providers table ---@field private provider_cache table ----@field private models table? +---@field private model_cache table? ---@field private current_job string? ----@field private headers table? local Client = class(function(self) self.providers = {} self.provider_cache = {} - self.models = nil + self.model_cache = nil self.current_job = nil - self.headers = nil end) --- Authenticate with GitHub and get the required headers @@ -246,9 +244,9 @@ end --- Fetch models from the Copilot API ---@return table -function Client:fetch_models() - if self.models then - return self.models +function Client:models() + if self.model_cache then + return self.model_cache end local models = {} @@ -282,8 +280,8 @@ function Client:fetch_models() end log.debug('Fetched models:', #vim.tbl_keys(models)) - self.models = models - return self.models + self.model_cache = models + return self.model_cache end --- Ask a question to Copilot @@ -299,7 +297,7 @@ function Client:ask(prompt, opts) log.debug('Resources:', #opts.resources) log.debug('History:', #opts.history) - local models = self:fetch_models() + local models = self:models() local model_config = models[opts.model] if not model_config then error('Model not found: ' .. opts.model) @@ -573,26 +571,6 @@ function Client:ask(prompt, opts) } end ---- List available models ----@return table -function Client:list_models() - local models = self:fetch_models() - local result = vim.tbl_keys(models) - - table.sort(result, function(a, b) - a = models[a] - b = models[b] - if a.provider ~= b.provider then - return a.provider < b.provider - end - return a.id < b.id - end) - - return vim.tbl_map(function(id) - return models[id] - end, result) -end - --- Generate embeddings for the given inputs ---@param inputs table: The inputs to embed ---@param model string @@ -603,7 +581,7 @@ function Client:embed(inputs, model) return inputs end - local models = self:fetch_models() + local models = self:models() local ok, provider_name, embed = pcall(resolve_provider_function, 'embed', model, models, self.providers) if not ok then ---@diagnostic disable-next-line: return-type-mismatch diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 9900e62f..9ef35e85 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -120,6 +120,26 @@ local function update_highlights() end end +--- List available models. +--- @return CopilotChat.client.Model[] +local function list_models() + local models = client:models() + local result = vim.tbl_keys(models) + + table.sort(result, function(a, b) + a = models[a] + b = models[b] + if a.provider ~= b.provider then + return a.provider < b.provider + end + return a.id < b.id + end) + + return vim.tbl_map(function(id) + return models[id] + end, result) +end + --- Finish writing to chat buffer. ---@param start_of_chat boolean? local function finish(start_of_chat) @@ -284,8 +304,8 @@ function M.resolve_functions(prompt, config) }) end - -- Resolve each tool reference - local function expand_tool(name, input) + -- Resolve each function reference + local function expand_function(name, input) notify.publish(notify.STATUS, 'Running function: ' .. name) local tool_id = nil @@ -368,7 +388,7 @@ function M.resolve_functions(prompt, config) for _, pattern in ipairs(matches:keys()) do if not utils.empty(pattern) then local match = matches:get(pattern) - local out = expand_tool(match.word, match.input) or pattern + local out = expand_function(match.word, match.input) or pattern out = out:gsub('%%', '%%%%') -- Escape percent signs for gsub prompt = prompt:gsub(vim.pesc(pattern), out, 1) end @@ -440,7 +460,7 @@ function M.resolve_model(prompt, config) local models = vim.tbl_map(function(model) return model.id - end, client:list_models()) + end, list_models()) local selected_model = config.model or '' prompt = prompt:gsub('%$' .. WORD, function(match) @@ -600,7 +620,7 @@ end ---@return table ---@async function M.complete_items() - local models = client:list_models() + local models = list_models() local prompts_to_use = M.prompts() local items = {} @@ -767,7 +787,7 @@ end --- Select default Copilot GPT model. function M.select_model() async.run(function() - local models = client:list_models() + local models = list_models() local choices = vim.tbl_map(function(model) return { id = model.id, From b738fb40de3a4bcbb835b8ff6ab2d171acc5d2dd Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 29 Jul 2025 18:05:29 +0200 Subject: [PATCH 25/75] fix: check for explicit uri input properly (#1214) Previous check for empty schema broke this, the check should be done only after input type is checked Signed-off-by: Tomas Slusny --- lua/CopilotChat/functions.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lua/CopilotChat/functions.lua b/lua/CopilotChat/functions.lua index c9d8488b..dbcf5bcd 100644 --- a/lua/CopilotChat/functions.lua +++ b/lua/CopilotChat/functions.lua @@ -129,14 +129,14 @@ end ---@param schema table? ---@return table function M.parse_input(input, schema) - if not schema or not schema.properties then - return {} - end - if type(input) == 'table' then return input end + if not schema or not schema.properties then + return {} + end + local parts = vim.split(input or '', INPUT_SEPARATOR) local result = {} local prop_names = sorted_propnames(schema) From 450fcecf2f71d0469e9c98f5967252092714ed03 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Tue, 29 Jul 2025 21:04:45 +0200 Subject: [PATCH 26/75] feat: display group as kind when listing resources (#1215) Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 9ef35e85..72155fbd 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -702,7 +702,7 @@ function M.complete_items() items[#items + 1] = { word = '#' .. tool.name, abbr = tool.name, - kind = 'resource', + kind = M.config.functions[tool.name].group or 'resource', info = info, menu = uri, icase = 1, From 9c4501e7ae92020f2d9b828086016ee70e7fa52c Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 30 Jul 2025 10:27:02 +0200 Subject: [PATCH 27/75] feat(providers)!: new github models api, in-built authorization without copilot.vim dep (#1218) - Switch to new github models api. This brings device code as prerequisite so add support for retrieving device code as well - With device code flow, add support for it for regular copilot auth as well - Disable github_models provider by default as now it requires device code flow BREAKING CHANGE: github_models provider is now disabled by default, enable with `providers.github_models.disabled = false` Closes #1140 Signed-off-by: Tomas Slusny --- README.md | 11 +- lua/CopilotChat/config/providers.lua | 212 ++++++++++++++++++--------- lua/CopilotChat/init.lua | 1 + lua/CopilotChat/notify.lua | 1 + lua/CopilotChat/ui/chat.lua | 6 + lua/CopilotChat/utils.lua | 23 +++ 6 files changed, 178 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index c093a813..5daa432f 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ CopilotChat.nvim is a Neovim plugin that brings GitHub Copilot Chat capabilities - [Neovim 0.10.0+](https://neovim.io/) - Older versions are not officially supported - [curl](https://curl.se/) - Version 8.0.0+ recommended for best compatibility - [Copilot chat in the IDE](https://github.com/settings/copilot) enabled in GitHub settings +- [plenary.nvim](https://github.com/nvim-lua/plenary.nvim) - Plugin dependency > [!WARNING] > For Neovim < 0.11.0, add `noinsert` or `noselect` to your `completeopt` otherwise chat autocompletion will not work. @@ -39,6 +40,8 @@ CopilotChat.nvim is a Neovim plugin that brings GitHub Copilot Chat capabilities ## Optional Dependencies +- [copilot.vim](https://github.com/github/copilot.vim) - For `:Copilot setup` authorization, otherwise in-built method i used + - [tiktoken_core](https://github.com/gptlang/lua-tiktoken) - For accurate token counting - Arch Linux: Install [`luajit-tiktoken-bin`](https://aur.archlinux.org/packages/luajit-tiktoken-bin) or [`lua51-tiktoken-bin`](https://aur.archlinux.org/packages/lua51-tiktoken-bin) from AUR - Via luarocks: `sudo luarocks install --lua-version 5.1 tiktoken_core` @@ -72,7 +75,6 @@ return { { "CopilotC-Nvim/CopilotChat.nvim", dependencies = { - { "github/copilot.vim" }, -- or zbirenbaum/copilot.lua { "nvim-lua/plenary.nvim", branch = "master" }, -- for curl, log and async functions }, build = "make tiktoken", @@ -92,7 +94,6 @@ Similar to the lazy setup, you can use the following configuration: ```vim call plug#begin() -Plug 'github/copilot.vim' Plug 'nvim-lua/plenary.nvim' Plug 'CopilotC-Nvim/CopilotChat.nvim' call plug#end() @@ -112,9 +113,7 @@ EOF mkdir -p ~/.config/nvim/pack/copilotchat/start cd ~/.config/nvim/pack/copilotchat/start -git clone https://github.com/github/copilot.vim git clone https://github.com/nvim-lua/plenary.nvim - git clone https://github.com/CopilotC-Nvim/CopilotChat.nvim ``` @@ -392,8 +391,8 @@ Providers are modules that implement integration with different AI providers. ### Built-in Providers - `copilot` - Default GitHub Copilot provider used for chat -- `github_models` - Provider for GitHub Marketplace models -- `copilot_embeddings` - Provider for Copilot embeddings, not standalone +- `github_models` - Provider for GitHub Marketplace models (disabled by default, enable it via `providers.github_models.disabled = false`) +- `copilot_embeddings` - Provider for Copilot embeddings, not standalone, used by `copilot` and `github_models` providers ### Provider Interface diff --git a/lua/CopilotChat/config/providers.lua b/lua/CopilotChat/config/providers.lua index 65619117..0e406bd6 100644 --- a/lua/CopilotChat/config/providers.lua +++ b/lua/CopilotChat/config/providers.lua @@ -1,70 +1,160 @@ +local notify = require('CopilotChat.notify') local utils = require('CopilotChat.utils') +local plenary_utils = require('plenary.async.util') local EDITOR_VERSION = 'Neovim/' .. vim.version().major .. '.' .. vim.version().minor .. '.' .. vim.version().patch -local cached_github_token = nil +local token_cache = nil +local unsaved_token_cache = {} +local function load_tokens() + if token_cache then + return token_cache + end + + local config_path = vim.fs.normalize(vim.fn.stdpath('data') .. '/copilot_chat') + local cache_file = config_path .. '/tokens.json' + local file = utils.read_file(cache_file) + if file then + token_cache = vim.json.decode(file) + else + token_cache = {} + end + + return token_cache +end + +local function get_token(tag) + if unsaved_token_cache[tag] then + return unsaved_token_cache[tag] + end + + local tokens = load_tokens() + return tokens[tag] +end -local function config_path() - local config = vim.fs.normalize('$XDG_CONFIG_HOME') - if config and vim.uv.fs_stat(config) then - return config +local function set_token(tag, token, save) + if not save then + unsaved_token_cache[tag] = token + return token end - if vim.fn.has('win32') > 0 then - config = vim.fs.normalize('$LOCALAPPDATA') - if not config or not vim.uv.fs_stat(config) then - config = vim.fs.normalize('$HOME/AppData/Local') + + local tokens = load_tokens() + tokens[tag] = token + local config_path = vim.fs.normalize(vim.fn.stdpath('data') .. '/copilot_chat') + utils.write_file(config_path .. '/tokens.json', vim.json.encode(tokens)) + return token +end + +--- Get the github token using device flow +---@return string +local function github_device_flow(tag, client_id, scope) + local function request_device_code() + local res = utils.curl_post('https://github.com/login/device/code', { + body = { + client_id = client_id, + scope = scope, + }, + headers = { ['Accept'] = 'application/json' }, + }) + + local data = vim.json.decode(res.body) + return data + end + + local function poll_for_token(device_code, interval) + while true do + plenary_utils.sleep(interval * 1000) + + local res = utils.curl_post('https://github.com/login/oauth/access_token', { + body = { + client_id = client_id, + device_code = device_code, + grant_type = 'urn:ietf:params:oauth:grant-type:device_code', + }, + headers = { ['Accept'] = 'application/json' }, + }) + local data = vim.json.decode(res.body) + if data.access_token then + return data.access_token + elseif data.error ~= 'authorization_pending' then + error('Auth error: ' .. (data.error or 'unknown')) + end end - else - config = vim.fs.normalize('$HOME/.config') end - if config and vim.uv.fs_stat(config) then - return config + + local token = get_token(tag) + if token then + return token end + + local code_data = request_device_code() + notify.publish( + notify.MESSAGE, + '[' .. tag .. '] Visit ' .. code_data.verification_uri .. ' and enter code: ' .. code_data.user_code + ) + notify.publish(notify.STATUS, '[' .. tag .. '] Waiting for GitHub models authorization...') + token = poll_for_token(code_data.device_code, code_data.interval) + return set_token(tag, token, true) end --- Get the github copilot oauth cached token (gu_ token) ---@return string -local function get_github_token() - if cached_github_token then - return cached_github_token +local function get_github_token(tag) + local function config_path() + local config = vim.fs.normalize('$XDG_CONFIG_HOME') + if config and vim.uv.fs_stat(config) then + return config + end + if vim.fn.has('win32') > 0 then + config = vim.fs.normalize('$LOCALAPPDATA') + if not config or not vim.uv.fs_stat(config) then + config = vim.fs.normalize('$HOME/AppData/Local') + end + else + config = vim.fs.normalize('$HOME/.config') + end + if config and vim.uv.fs_stat(config) then + return config + end + end + + local token = get_token(tag) + if token then + return token end -- loading token from the environment only in GitHub Codespaces - local token = os.getenv('GITHUB_TOKEN') local codespaces = os.getenv('CODESPACES') + token = os.getenv('GITHUB_TOKEN') if token and codespaces then - cached_github_token = token - return token + return set_token(tag, token, false) end -- loading token from the file local config_path = config_path() - if not config_path then - error('Failed to find config path for GitHub token') - end + if config_path then + -- token can be sometimes in apps.json sometimes in hosts.json + local file_paths = { + config_path .. '/github-copilot/hosts.json', + config_path .. '/github-copilot/apps.json', + } - -- token can be sometimes in apps.json sometimes in hosts.json - local file_paths = { - config_path .. '/github-copilot/hosts.json', - config_path .. '/github-copilot/apps.json', - } - - for _, file_path in ipairs(file_paths) do - local file_data = utils.read_file(file_path) - if file_data then - local parsed_data = utils.json_decode(file_data) - if parsed_data then - for key, value in pairs(parsed_data) do - if string.find(key, 'github.com') then - cached_github_token = value.oauth_token - return value.oauth_token + for _, file_path in ipairs(file_paths) do + local file_data = utils.read_file(file_path) + if file_data then + local parsed_data = utils.json_decode(file_data) + if parsed_data then + for key, value in pairs(parsed_data) do + if string.find(key, 'github.com') and value and value.oauth_token then + return set_token(tag, value.oauth_token, true) + end end end end end end - error('Failed to find GitHub token') + return github_device_flow(tag, 'Iv1.b507a08c87ecfe98', '') end ---@class CopilotChat.config.providers.Options @@ -97,7 +187,7 @@ M.copilot = { local response, err = utils.curl_get('https://api.github.com/copilot_internal/v2/token', { json_response = true, headers = { - ['Authorization'] = 'Token ' .. get_github_token(), + ['Authorization'] = 'Token ' .. get_github_token('copilot'), }, }) @@ -284,30 +374,19 @@ M.copilot = { } M.github_models = { + disabled = true, embed = 'copilot_embeddings', get_headers = function() return { - ['Authorization'] = 'Bearer ' .. get_github_token(), - ['x-ms-useragent'] = EDITOR_VERSION, - ['x-ms-user-agent'] = EDITOR_VERSION, + ['Authorization'] = 'Bearer ' .. github_device_flow('github_models', 'Ov23liqtJusaUH38tIoK', 'read:user copilot'), } end, get_models = function(headers) - local response, err = utils.curl_post('https://api.catalog.azureml.ms/asset-gallery/v1.0/models', { - headers = headers, - json_request = true, + local response, err = utils.curl_get('https://models.github.ai/catalog/models', { json_response = true, - body = { - filters = { - { field = 'freePlayground', values = { 'true' }, operator = 'eq' }, - { field = 'labels', values = { 'latest' }, operator = 'eq' }, - }, - order = { - { field = 'displayName', direction = 'asc' }, - }, - }, + headers = headers, }) if err then @@ -315,26 +394,19 @@ M.github_models = { end return vim - .iter(response.body.summaries) - :filter(function(model) - return vim.tbl_contains(model.inferenceTasks, 'chat-completion') - end) + .iter(response.body) :map(function(model) - local context_window = model.modelLimits.textLimits.inputContextWindow - local max_output_tokens = model.modelLimits.textLimits.maxOutputTokens - local max_input_tokens = context_window - max_output_tokens - if max_input_tokens <= 0 then - max_output_tokens = 4096 - max_input_tokens = context_window - max_output_tokens - end - + local max_output_tokens = model.limits.max_output_tokens + local max_input_tokens = model.limits.max_input_tokens return { - id = model.name, - name = model.displayName, + id = model.id, + name = model.name, tokenizer = 'o200k_base', max_input_tokens = max_input_tokens, max_output_tokens = max_output_tokens, - streaming = true, + streaming = vim.tbl_contains(model.capabilities, 'streaming'), + tools = vim.tbl_contains(model.capabilities, 'tool-calling'), + version = model.version, } end) :totable() @@ -344,7 +416,7 @@ M.github_models = { prepare_output = M.copilot.prepare_output, get_url = function() - return 'https://models.inference.ai.azure.com/chat/completions' + return 'https://models.github.ai/inference/chat/completions' end, } diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 72155fbd..71e80f3f 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -893,6 +893,7 @@ function M.ask(prompt, config) end M.chat:start() + M.chat:append('\n') local sticky = {} local in_code_block = false diff --git a/lua/CopilotChat/notify.lua b/lua/CopilotChat/notify.lua index db1af837..99aa499a 100644 --- a/lua/CopilotChat/notify.lua +++ b/lua/CopilotChat/notify.lua @@ -3,6 +3,7 @@ local log = require('plenary.log') local M = {} M.STATUS = 'status' +M.MESSAGE = 'message' M.listeners = {} diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 669dfa6c..bcd513fd 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -1,5 +1,6 @@ local Overlay = require('CopilotChat.ui.overlay') local Spinner = require('CopilotChat.ui.spinner') +local notify = require('CopilotChat.notify') local utils = require('CopilotChat.utils') local class = utils.class @@ -95,6 +96,11 @@ local Chat = class(function(self, headers, separator, help, on_buf_create) end, }) end) + + notify.listen(notify.MESSAGE, function(msg) + utils.schedule_main() + self:append('\n' .. msg .. '\n') + end) end, Overlay) --- Returns whether the chat window is visible. diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index 1cf1a151..b1400837 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -615,6 +615,29 @@ function M.read_file(path) return data end +--- Write data to a file +---@param path string The file path +---@param data string The data to write +---@return boolean +function M.write_file(path, data) + M.schedule_main() + vim.fn.mkdir(vim.fn.fnamemodify(path, ':p:h'), 'p') + + local err, fd = async.uv.fs_open(path, 'w', 438) + if err or not fd then + return false + end + + local err = async.uv.fs_write(fd, data, 0) + if err then + async.uv.fs_close(fd) + return false + end + + async.uv.fs_close(fd) + return true +end + --- Call a system command ---@param cmd table The command ---@async From f45a7ee8da7ef15fd88f8a83ea7b76b50c8d6eb2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 30 Jul 2025 08:27:25 +0000 Subject: [PATCH 28/75] chore(doc): auto generate docs --- doc/CopilotChat.txt | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 799f0f62..979b45fc 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 July 29 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 July 30 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -52,6 +52,7 @@ capabilities directly into your editor. It provides: - Neovim 0.10.0+ - Older versions are not officially supported - curl - Version 8.0.0+ recommended for best compatibility - Copilot chat in the IDE enabled in GitHub settings +- plenary.nvim - Plugin dependency [!WARNING] For Neovim < 0.11.0, add `noinsert` or `noselect` to your @@ -61,12 +62,16 @@ capabilities directly into your editor. It provides: OPTIONAL DEPENDENCIES *CopilotChat-optional-dependencies* -- tiktoken_core - For accurate token counting +- copilot.vim - For `:Copilot setup` + authorization, otherwise in-built method i used +- tiktoken_core - For accurate token + counting - Arch Linux: Install `luajit-tiktoken-bin` or `lua51-tiktoken-bin` from AUR - Via luarocks: `sudo luarocks install --lua-version 5.1 tiktoken_core` - Manual: Download from lua-tiktoken releases and save as `tiktoken_core.so` in your Lua path - git - For git diff context features -- ripgrep - For improved search performance +- ripgrep - For improved search + performance - lynx - For improved URL context features @@ -99,7 +104,6 @@ LAZY.NVIM *CopilotChat-lazy.nvim* { "CopilotC-Nvim/CopilotChat.nvim", dependencies = { - { "github/copilot.vim" }, -- or zbirenbaum/copilot.lua { "nvim-lua/plenary.nvim", branch = "master" }, -- for curl, log and async functions }, build = "make tiktoken", @@ -121,7 +125,6 @@ Similar to the lazy setup, you can use the following configuration: >vim call plug#begin() - Plug 'github/copilot.vim' Plug 'nvim-lua/plenary.nvim' Plug 'CopilotC-Nvim/CopilotChat.nvim' call plug#end() @@ -142,9 +145,7 @@ MANUAL *CopilotChat-manual* mkdir -p ~/.config/nvim/pack/copilotchat/start cd ~/.config/nvim/pack/copilotchat/start - git clone https://github.com/github/copilot.vim git clone https://github.com/nvim-lua/plenary.nvim - git clone https://github.com/CopilotC-Nvim/CopilotChat.nvim < @@ -456,8 +457,8 @@ Providers are modules that implement integration with different AI providers. BUILT-IN PROVIDERS ~ - `copilot` - Default GitHub Copilot provider used for chat -- `github_models` - Provider for GitHub Marketplace models -- `copilot_embeddings` - Provider for Copilot embeddings, not standalone +- `github_models` - Provider for GitHub Marketplace models (disabled by default, enable it via `providers.github_models.disabled = false`) +- `copilot_embeddings` - Provider for Copilot embeddings, not standalone, used by `copilot` and `github_models` providers PROVIDER INTERFACE ~ From d9f4e29c3b46b827443b1832209d22d05c1a69af Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 30 Jul 2025 10:34:23 +0200 Subject: [PATCH 29/75] fix(healthcheck): chance copilot.vim dependency to optional (#1219) Signed-off-by: Tomas Slusny --- lua/CopilotChat/health.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/CopilotChat/health.lua b/lua/CopilotChat/health.lua index cf1e568a..a75416ee 100644 --- a/lua/CopilotChat/health.lua +++ b/lua/CopilotChat/health.lua @@ -98,8 +98,8 @@ function M.check() if has_copilot or copilot_loaded then ok('copilot: ' .. (has_copilot and 'copilot.lua' or 'copilot.vim')) else - error( - 'copilot: missing, required for 2 factor authentication. Install "github/copilot.vim" or "zbirenbaum/copilot.lua" plugins.' + warn( + 'copilot: missing, optional for improved auth implementation. Install "github/copilot.vim" or "zbirenbaum/copilot.lua" plugins.' ) end From 950fdb6ab56754929d4db91c73139b33e645deec Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 30 Jul 2025 11:16:16 +0200 Subject: [PATCH 30/75] feat(functions): automatically parse schema from url templates (#1220) Signed-off-by: Tomas Slusny --- lua/CopilotChat/functions.lua | 52 ++++++++++++++++++++++++++++++----- lua/CopilotChat/init.lua | 34 ++++++++++++++--------- lua/CopilotChat/tiktoken.lua | 10 +++++-- 3 files changed, 74 insertions(+), 22 deletions(-) diff --git a/lua/CopilotChat/functions.lua b/lua/CopilotChat/functions.lua index dbcf5bcd..04c5b103 100644 --- a/lua/CopilotChat/functions.lua +++ b/lua/CopilotChat/functions.lua @@ -3,6 +3,7 @@ local utils = require('CopilotChat.utils') local M = {} local INPUT_SEPARATOR = ';;' +local URI_PARAM_PATTERN = '{([^}:*]+)[^}]*}' local function sorted_propnames(schema) local prop_names = vim.tbl_keys(schema.properties) @@ -63,6 +64,17 @@ local function filter_schema(tbl) return result end +--- Convert a URI template to a URL by replacing parameters with values from input +---@param uri_template string The URI template containing parameters in the form {param} +---@param input table A table containing parameter values, e.g., { path = '/my/file.txt' } +---@return string The resulting URL with parameters replaced +function M.uri_to_url(uri_template, input) + -- Replace {param} in the template with input[param] or empty string + return (uri_template:gsub(URI_PARAM_PATTERN, function(param) + return input[param] or '' + end)) +end + ---@param uri string The URI to parse ---@param pattern string The pattern to match against (e.g., 'file://{path}') ---@return table|nil inputs Extracted parameters or nil if no match @@ -73,7 +85,7 @@ function M.match_uri(uri, pattern) -- Extract parameter names from the pattern local param_names = {} - for param in pattern:gmatch('{([^}:*]+)[^}]*}') do + for param in pattern:gmatch(URI_PARAM_PATTERN) do table.insert(param_names, param) -- Replace {param} with a capture group in our Lua pattern -- Use non-greedy capture to handle multiple params properly @@ -102,6 +114,37 @@ function M.match_uri(uri, pattern) return result end +---@param tool CopilotChat.config.functions.Function +function M.parse_schema(tool) + local schema = tool.schema + + -- If schema is missing but uri is present, generate a default schema from uri + if not schema and tool.uri then + -- Extract parameter names from the uri pattern, e.g. file://{path} + local param_names = {} + for param in tool.uri:gmatch(URI_PARAM_PATTERN) do + table.insert(param_names, param) + end + if #param_names > 0 then + schema = { + type = 'object', + properties = {}, + required = {}, + } + for _, param in ipairs(param_names) do + schema.properties[param] = { type = 'string' } + table.insert(schema.required, param) + end + end + end + + if schema then + schema = filter_schema(schema) + end + + return schema +end + --- Prepare the schema for use ---@param tools table ---@return table @@ -110,16 +153,11 @@ function M.parse_tools(tools) table.sort(tool_names) return vim.tbl_map(function(name) local tool = tools[name] - local schema = tool.schema - - if schema then - schema = filter_schema(schema) - end return { name = name, description = tool.description, - schema = schema, + schema = M.parse_schema(tool), } end, tool_names) end diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 71e80f3f..91aaf143 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -245,6 +245,12 @@ end ---@async function M.resolve_functions(prompt, config) config, prompt = M.resolve_prompt(prompt, config) + + local tools = {} + for _, tool in ipairs(functions.parse_tools(M.config.functions)) do + tools[tool.name] = tool + end + local enabled_tools = {} local resolved_resources = {} local resolved_tools = {} @@ -271,7 +277,7 @@ function M.resolve_functions(prompt, config) for _, match in ipairs(matches) do for name, tool in pairs(M.config.functions) do if name == match or tool.group == match then - enabled_tools[name] = tool + enabled_tools[name] = true end end end @@ -311,7 +317,7 @@ function M.resolve_functions(prompt, config) local tool_id = nil if not utils.empty(tool_calls) then for _, tool_call in ipairs(tool_calls) do - if tool_call.name == name and vim.trim(tool_call.id) == vim.trim(input) and enabled_tools[name] then + if tool_call.name == name and vim.trim(tool_call.id) == vim.trim(input) then input = utils.empty(tool_call.arguments) and {} or utils.json_decode(tool_call.arguments) tool_id = tool_call.id break @@ -319,7 +325,7 @@ function M.resolve_functions(prompt, config) end end - local tool = enabled_tools[name] + local tool = M.config.functions[name] if not tool then -- Check if input matches uri for tool_name, tool_spec in pairs(M.config.functions) do @@ -334,20 +340,16 @@ function M.resolve_functions(prompt, config) end end end - if not tool and not tool_id then - tool = M.config.functions[name] - end if not tool then - -- If tool is not found, return the original pattern return nil end - if not tool_id and not tool.uri then - -- If this is a tool that is not resource and was not called by LLM, reject it + if tool_id and not enabled_tools[name] and not tool.uri then return nil end + local schema = tools[name] and tools[name].schema or nil local result = '' - local ok, output = pcall(tool.resolve, functions.parse_input(input, tool.schema), state.source or {}, prompt) + local ok, output = pcall(tool.resolve, functions.parse_input(input, schema), state.source or {}, input) if not ok then result = string.format(BLOCK_OUTPUT_FORMAT, 'error', utils.make_string(output)) else @@ -394,7 +396,12 @@ function M.resolve_functions(prompt, config) end end - return functions.parse_tools(enabled_tools), resolved_resources, resolved_tools, prompt + return vim.tbl_map(function(name) + return tools[name] + end, vim.tbl_keys(enabled_tools)), + resolved_resources, + resolved_tools, + prompt end --- Resolve the final prompt and config from prompt template. @@ -574,9 +581,10 @@ function M.trigger_complete(without_input) if not without_input and vim.startswith(prefix, '#') and vim.endswith(prefix, ':') then local found_tool = M.config.functions[prefix:sub(2, -2)] - if found_tool and found_tool.schema then + local found_schema = found_tool and functions.parse_schema(found_tool) + if found_tool and found_schema then async.run(function() - local value = functions.enter_input(found_tool.schema, state.source) + local value = functions.enter_input(found_schema, state.source) if not value then return end diff --git a/lua/CopilotChat/tiktoken.lua b/lua/CopilotChat/tiktoken.lua index a4582cb4..dde3d2b5 100644 --- a/lua/CopilotChat/tiktoken.lua +++ b/lua/CopilotChat/tiktoken.lua @@ -92,7 +92,13 @@ function M.encode(prompt) if type(prompt) ~= 'string' then error('Prompt must be a string') end - return tiktoken_core.encode(prompt) + + local ok, result = pcall(tiktoken_core.encode, prompt) + if not ok then + return nil + end + + return result end --- Count the tokens in a prompt @@ -105,7 +111,7 @@ function M.count(prompt) local tokens = M.encode(prompt) if not tokens then - return 0 + return math.ceil(#prompt * 0.5) -- Fallback to 1/2 character count end return #tokens end From c03bd1df78b276aa5be2f173c2a31ad273164f15 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 30 Jul 2025 11:20:25 +0200 Subject: [PATCH 31/75] fix(functions): properly send prompt as 3rd function resolve param (#1221) Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 91aaf143..39e40959 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -349,7 +349,7 @@ function M.resolve_functions(prompt, config) local schema = tools[name] and tools[name].schema or nil local result = '' - local ok, output = pcall(tool.resolve, functions.parse_input(input, schema), state.source or {}, input) + local ok, output = pcall(tool.resolve, functions.parse_input(input, schema), state.source or {}, prompt) if not ok then result = string.format(BLOCK_OUTPUT_FORMAT, 'error', utils.make_string(output)) else From 1f96d53c3f10f176ca25065a23e610d7b4a72b99 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 30 Jul 2025 12:02:37 +0200 Subject: [PATCH 32/75] fix(ui): fix check for auto follow cursor (#1222) Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 2 -- lua/CopilotChat/ui/chat.lua | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 39e40959..5baa0fa8 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -970,8 +970,6 @@ function M.ask(prompt, config) content = '\n' .. tool.result .. '\n', }) end - - M.chat:follow() end local ask_ok, ask_response = pcall(client.ask, client, prompt, { diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index bcd513fd..55964682 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -501,7 +501,7 @@ function Chat:append(str) local current_pos = vim.api.nvim_win_get_cursor(self.winnr) local line_count = vim.api.nvim_buf_line_count(self.bufnr) -- Follow only if the cursor is currently at the last line. - should_follow_cursor = current_pos[1] == line_count + should_follow_cursor = current_pos[1] >= line_count - 1 end local last_line, last_column, _ = self:last() From 294bcb620ff66183e142cd8a43a7c77d5bc77a16 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 30 Jul 2025 13:22:38 +0200 Subject: [PATCH 33/75] fix(providers): do not save copilot.vim token (#1223) Signed-off-by: Tomas Slusny --- lua/CopilotChat/config/providers.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lua/CopilotChat/config/providers.lua b/lua/CopilotChat/config/providers.lua index 0e406bd6..eee655a8 100644 --- a/lua/CopilotChat/config/providers.lua +++ b/lua/CopilotChat/config/providers.lua @@ -92,7 +92,7 @@ local function github_device_flow(tag, client_id, scope) notify.MESSAGE, '[' .. tag .. '] Visit ' .. code_data.verification_uri .. ' and enter code: ' .. code_data.user_code ) - notify.publish(notify.STATUS, '[' .. tag .. '] Waiting for GitHub models authorization...') + notify.publish(notify.STATUS, '[' .. tag .. '] Waiting for authorization...') token = poll_for_token(code_data.device_code, code_data.interval) return set_token(tag, token, true) end @@ -146,7 +146,7 @@ local function get_github_token(tag) if parsed_data then for key, value in pairs(parsed_data) do if string.find(key, 'github.com') and value and value.oauth_token then - return set_token(tag, value.oauth_token, true) + return set_token(tag, value.oauth_token, false) end end end @@ -187,7 +187,7 @@ M.copilot = { local response, err = utils.curl_get('https://api.github.com/copilot_internal/v2/token', { json_response = true, headers = { - ['Authorization'] = 'Token ' .. get_github_token('copilot'), + ['Authorization'] = 'Token ' .. get_github_token('github_copilot'), }, }) From 67ed258c6ccc0a9bfbb6dfcbe3d5e19e22888e73 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 30 Jul 2025 17:24:51 +0200 Subject: [PATCH 34/75] fix(ui): do not allow empty separator (#1224) This even working before was a miracle, the plugin is fairly reliant on separator not being empty string Signed-off-by: Tomas Slusny --- README.md | 2 +- lua/CopilotChat/config.lua | 2 +- lua/CopilotChat/init.lua | 10 +++++++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5daa432f..e10f3d61 100644 --- a/README.md +++ b/README.md @@ -473,7 +473,7 @@ Below are all available configuration options with their default values: show_help = true, -- Shows help message as virtual lines when waiting for user input show_folds = true, -- Shows folds for sections in chat highlight_selection = true, -- Highlight selection - highlight_headers = true, -- Highlight headers in chat, disable if using markdown renderers (like render-markdown.nvim) + highlight_headers = true, -- Highlight headers in chat auto_follow_cursor = true, -- Auto-follow cursor in chat auto_insert_mode = false, -- Automatically enter insert mode when opening window and on new prompt insert_at_end = false, -- Move cursor to end of buffer when inserting text diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index c456a5b1..f2f2c708 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -86,7 +86,7 @@ return { show_help = true, -- Shows help message as virtual lines when waiting for user input show_folds = true, -- Shows folds for sections in chat highlight_selection = true, -- Highlight selection - highlight_headers = true, -- Highlight headers in chat, disable if using markdown renderers (like render-markdown.nvim) + highlight_headers = true, -- Highlight headers in chat auto_follow_cursor = true, -- Auto-follow cursor in chat auto_insert_mode = false, -- Automatically enter insert mode when opening window and on new prompt insert_at_end = false, -- Move cursor to end of buffer when inserting text diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 5baa0fa8..fa2e4308 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -1162,7 +1162,8 @@ end --- Set up the plugin ---@param config CopilotChat.config.Config? function M.setup(config) - M.config = vim.tbl_deep_extend('force', require('CopilotChat.config'), config or {}) + local default_config = require('CopilotChat.config') + M.config = vim.tbl_deep_extend('force', default_config, config or {}) state.highlights_loaded = false -- Save proxy and insecure settings @@ -1181,6 +1182,13 @@ function M.setup(config) M.log_level(M.config.log_level) end + if not M.config.separator or M.config.separator == '' then + log.warn( + 'Empty separator is not allowed, using default separator instead. Set `separator` in config to change this.' + ) + M.config.separator = default_config.separator + end + if M.chat then M.chat:close(state.source and state.source.bufnr or nil) M.chat:delete() From 1b5cb07d0ef679d14f0896b724de47c640c69a6d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 30 Jul 2025 15:25:16 +0000 Subject: [PATCH 35/75] chore(doc): auto generate docs --- doc/CopilotChat.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 979b45fc..5bbef619 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -546,7 +546,7 @@ Below are all available configuration options with their default values: show_help = true, -- Shows help message as virtual lines when waiting for user input show_folds = true, -- Shows folds for sections in chat highlight_selection = true, -- Highlight selection - highlight_headers = true, -- Highlight headers in chat, disable if using markdown renderers (like render-markdown.nvim) + highlight_headers = true, -- Highlight headers in chat auto_follow_cursor = true, -- Auto-follow cursor in chat auto_insert_mode = false, -- Automatically enter insert mode when opening window and on new prompt insert_at_end = false, -- Move cursor to end of buffer when inserting text From 8071a6979b5569ce03f7f4d7192814da4c2d4e0b Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 30 Jul 2025 19:19:44 +0200 Subject: [PATCH 36/75] feat(ui): highlight copilotchat keywords (#1225) Signed-off-by: Tomas Slusny --- lua/CopilotChat/ui/chat.lua | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 55964682..0f7663a5 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -66,7 +66,6 @@ end ---@field private layout CopilotChat.config.Layout? ---@field private headers table ---@field private separator string ----@field private header_ns number ---@field private spinner CopilotChat.ui.spinner.Spinner ---@field private chat_overlay CopilotChat.ui.overlay.Overlay local Chat = class(function(self, headers, separator, help, on_buf_create) @@ -81,7 +80,6 @@ local Chat = class(function(self, headers, separator, help, on_buf_create) self.layout = nil self.headers = headers or {} self.separator = separator - self.header_ns = vim.api.nvim_create_namespace('copilot-chat-headers') self.spinner = Spinner() self.chat_overlay = Overlay('copilot-overlay', 'q to close', function(bufnr) @@ -559,7 +557,10 @@ end ---@protected function Chat:render() self:validate() - vim.api.nvim_buf_clear_namespace(self.bufnr, self.header_ns, 0, -1) + + local highlight_ns = vim.api.nvim_create_namespace('copilot-chat-headers') + vim.api.nvim_buf_clear_namespace(self.bufnr, highlight_ns, 0, -1) + local lines = vim.api.nvim_buf_get_lines(self.bufnr, 0, -1, false) local new_messages = {} @@ -578,7 +579,7 @@ function Chat:render() -- Draw the separator as virtual text over the header line, hiding the id and anything after the header if self.config.highlight_headers then local sep_col = vim.fn.strwidth(header_value) - vim.api.nvim_buf_set_extmark(self.bufnr, self.header_ns, l - 1, sep_col, { + vim.api.nvim_buf_set_extmark(self.bufnr, highlight_ns, l - 1, sep_col, { virt_text = { { string.rep(self.separator, vim.go.columns), 'CopilotChatSeparator' }, }, @@ -586,7 +587,7 @@ function Chat:render() priority = 200, strict = false, }) - vim.api.nvim_buf_set_extmark(self.bufnr, self.header_ns, l - 1, 0, { + vim.api.nvim_buf_set_extmark(self.bufnr, highlight_ns, l - 1, 0, { end_col = sep_col, hl_group = 'CopilotChatHeader', priority = 100, @@ -647,7 +648,7 @@ function Chat:render() if start_line and end_line then text = text .. string.format(' lines %d-%d', start_line, end_line) end - vim.api.nvim_buf_set_extmark(self.bufnr, self.header_ns, l, 0, { + vim.api.nvim_buf_set_extmark(self.bufnr, highlight_ns, l, 0, { virt_lines_above = true, virt_lines = { { { text, 'CopilotChatAnnotationHeader' } } }, priority = 100, @@ -674,9 +675,9 @@ function Chat:render() for _, message in ipairs(self.messages) do for _, tool_call in ipairs(message.tool_calls or {}) do if line:match(string.format('#%s:%s', tool_call.name, vim.pesc(tool_call.id))) then - vim.api.nvim_buf_add_highlight(self.bufnr, self.header_ns, 'CopilotChatAnnotationHeader', l - 1, 0, #line) + vim.api.nvim_buf_add_highlight(self.bufnr, highlight_ns, 'CopilotChatAnnotationHeader', l - 1, 0, #line) if not utils.empty(tool_call.arguments) then - vim.api.nvim_buf_set_extmark(self.bufnr, self.header_ns, l - 1, 0, { + vim.api.nvim_buf_set_extmark(self.bufnr, highlight_ns, l - 1, 0, { virt_lines = vim.tbl_map(function(json_line) return { { json_line, 'CopilotChatAnnotation' } } end, vim.split(vim.inspect(utils.json_decode(tool_call.arguments)), '\n')), @@ -688,6 +689,20 @@ function Chat:render() end end end + + -- Highlight keywords + -- FIXME: This is not optimal, but i cant figure out how to do it better as treesitter keeps overriding it + local patterns = { + '()#?#[^ ]+()', + '()@[^ ]+()', + '()%$[^ ]+()', + '()/[^ ]+()', + } + for _, pattern in ipairs(patterns) do + for s, e in line:gmatch(pattern) do + vim.api.nvim_buf_add_highlight(self.bufnr, highlight_ns, 'CopilotChatKeyword', l - 1, s - 1, e - 1) + end + end end -- Replace self.messages with new_messages (preserving tool_calls, etc.) @@ -705,7 +720,7 @@ function Chat:render() table.insert(virt_lines, { { ' ' .. json_line, 'CopilotChatAnnotation' } }) end end - vim.api.nvim_buf_set_extmark(self.bufnr, self.header_ns, section.end_line - 1, 0, { + vim.api.nvim_buf_set_extmark(self.bufnr, highlight_ns, section.end_line - 1, 0, { virt_lines = virt_lines, virt_lines_above = true, priority = 100, @@ -720,7 +735,7 @@ function Chat:render() local virt_lines = { { { 'Tool: ' .. message.tool_call_id, 'CopilotChatAnnotationHeader' } }, } - vim.api.nvim_buf_set_extmark(self.bufnr, self.header_ns, section.start_line, 0, { + vim.api.nvim_buf_set_extmark(self.bufnr, highlight_ns, section.start_line, 0, { virt_lines = virt_lines, virt_lines_above = true, priority = 100, From b124b94264140a5d352512b38b7a46d85ee59b24 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 30 Jul 2025 22:54:27 +0200 Subject: [PATCH 37/75] fix(functions): use vim.filetype.match for non bulk file reads (#1226) To improve accuracy, use vim.filetype.match instead of plenary filetype when not filtering lists of files. Closes #1181 Signed-off-by: Tomas Slusny --- lua/CopilotChat/config/functions.lua | 1 + lua/CopilotChat/utils.lua | 26 ++++++++++++++++---------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/lua/CopilotChat/config/functions.lua b/lua/CopilotChat/config/functions.lua index 1644a03f..65363811 100644 --- a/lua/CopilotChat/config/functions.lua +++ b/lua/CopilotChat/config/functions.lua @@ -37,6 +37,7 @@ return { }, resolve = function(input) + utils.schedule_main() local data, mimetype = resources.get_file(input.path) if not data then error('File not found: ' .. input.path) diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index b1400837..ae3e6313 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -214,15 +214,7 @@ end ---@param filename string The file name ---@return string|nil function M.filetype(filename) - local filetype = require('plenary.filetype') - local ft = filetype.detect(filename, { - fs_access = false, - }) - - if ft == '' then - return nil - end - return ft + return vim.filetype.match({ filename = filename }) end --- Get the mimetype from filetype @@ -445,8 +437,22 @@ M.curl_post = async.wrap(function(url, opts, callback) end, 3) local function filter_files(files, max_count) + local filetype = require('plenary.filetype') + files = vim.tbl_filter(function(file) - return file ~= '' and M.filetype(file) ~= nil + if file == nil or file == '' then + return false + end + + local ft = filetype.detect(file, { + fs_access = false, + }) + + if ft == '' or not ft then + return false + end + + return true end, files) if max_count and max_count > 0 then files = vim.list_slice(files, 1, max_count) From a01bbd6779f4bee23c29ebcfe0d2f5fa5664b5bf Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 30 Jul 2025 23:03:15 +0200 Subject: [PATCH 38/75] feat(ui): add window.blend option for controllin float transparency (#1227) Closes #1126 Signed-off-by: Tomas Slusny --- README.md | 1 + lua/CopilotChat/config.lua | 2 ++ lua/CopilotChat/ui/chat.lua | 1 + 3 files changed, 4 insertions(+) diff --git a/README.md b/README.md index e10f3d61..7958837a 100644 --- a/README.md +++ b/README.md @@ -468,6 +468,7 @@ Below are all available configuration options with their default values: title = 'Copilot Chat', -- title of chat window footer = nil, -- footer of chat window zindex = 1, -- determines if window is on top or below other floating windows + blend = 0, -- window blend (transparency), 0-100, 0 is opaque, 100 is fully transparent }, show_help = true, -- Shows help message as virtual lines when waiting for user input diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index f2f2c708..8809fdc6 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -11,6 +11,7 @@ ---@field title string? ---@field footer string? ---@field zindex number? +---@field blend number? ---@class CopilotChat.config.Shared ---@field system_prompt string? @@ -81,6 +82,7 @@ return { title = 'Copilot Chat', -- title of chat window footer = nil, -- footer of chat window zindex = 1, -- determines if window is on top or below other floating windows + blend = 0, -- window blend (transparency), 0-100, 0 is opaque, 100 is fully transparent }, show_help = true, -- Shows help message as virtual lines when waiting for user input diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 0f7663a5..e2cf7503 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -289,6 +289,7 @@ function Chat:open(config) } self.winnr = vim.api.nvim_open_win(self.bufnr, false, win_opts) + vim.wo[self.winnr].winblend = window.blend or 0 elseif layout == 'vertical' then local orig = vim.api.nvim_get_current_win() local cmd = 'vsplit' From 72ac912877a55ea6c61d803dee38704c9e7c255c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 30 Jul 2025 21:03:35 +0000 Subject: [PATCH 39/75] chore(doc): auto generate docs --- doc/CopilotChat.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 5bbef619..02932d3c 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -541,6 +541,7 @@ Below are all available configuration options with their default values: title = 'Copilot Chat', -- title of chat window footer = nil, -- footer of chat window zindex = 1, -- determines if window is on top or below other floating windows + blend = 0, -- window blend (transparency), 0-100, 0 is opaque, 100 is fully transparent }, show_help = true, -- Shows help message as virtual lines when waiting for user input From 1713ce6c8ec700a7833236a8dadfae8a0742b14d Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 31 Jul 2025 16:11:18 +0200 Subject: [PATCH 40/75] feat(providers): add info output to panel for copilot with stats (#1229) Signed-off-by: Tomas Slusny --- README.md | 3 ++ lua/CopilotChat/client.lua | 31 +++++++++++++++++ lua/CopilotChat/config/mappings.lua | 10 ++++++ lua/CopilotChat/config/providers.lua | 51 ++++++++++++++++++++++++++++ 4 files changed, 95 insertions(+) diff --git a/README.md b/README.md index 7958837a..19e790c6 100644 --- a/README.md +++ b/README.md @@ -406,6 +406,9 @@ Custom providers can implement these methods: -- Optional: Embeddings provider name or function embed?: string|function, + -- Optional: Extra info about the provider displayed in info panel + get_info?(): string[] + -- Optional: Get extra request headers with optional expiration time get_headers?(): table, number?, diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index b0a48b85..11c353b6 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -284,6 +284,37 @@ function Client:models() return self.model_cache end +--- Get information about all providers +---@return table +function Client:info() + local infos = {} + local now = math.floor(os.time()) + local CACHE_TTL = 300 -- 5 minutes + + for provider_name, provider in pairs(self.providers) do + if not provider.disabled and provider.get_info then + local cache = self.provider_cache[provider_name] + if cache and cache.info and cache.info_expires_at and cache.info_expires_at > now then + infos[provider_name] = cache.info + else + local ok, info = pcall(provider.get_info, self:authenticate(provider_name)) + if ok then + infos[provider_name] = info + if cache then + cache.info = info + cache.info_expires_at = now + CACHE_TTL + end + else + log.warn('Failed to get info for provider ' .. provider_name .. ': ' .. info) + end + end + end + end + + log.debug('Fetched provider infos:', #vim.tbl_keys(infos)) + return infos +end + --- Ask a question to Copilot ---@param prompt string: The prompt to send to Copilot ---@param opts CopilotChat.client.AskOptions: Options for the request diff --git a/lua/CopilotChat/config/mappings.lua b/lua/CopilotChat/config/mappings.lua index bdcb93ce..73cb25a0 100644 --- a/lua/CopilotChat/config/mappings.lua +++ b/lua/CopilotChat/config/mappings.lua @@ -1,5 +1,6 @@ local async = require('plenary.async') local copilot = require('CopilotChat') +local client = require('CopilotChat.client') local utils = require('CopilotChat.utils') ---@class CopilotChat.config.mappings.Diff @@ -439,6 +440,7 @@ return { local system_prompt = config.system_prompt async.run(function() + local infos = client:info() local selected_model = copilot.resolve_model(prompt, config) local selected_tools, resolved_resources = copilot.resolve_functions(prompt, config) selected_tools = vim.tbl_map(function(tool) @@ -451,6 +453,14 @@ return { table.insert(lines, '**Temp Files**: `' .. vim.fn.fnamemodify(os.tmpname(), ':h') .. '`') table.insert(lines, '') + for provider, infolines in pairs(infos) do + table.insert(lines, '**Provider**: `' .. provider .. '`') + for _, line in ipairs(infolines) do + table.insert(lines, line) + end + table.insert(lines, '') + end + if source and utils.buf_valid(source.bufnr) then local source_name = vim.api.nvim_buf_get_name(source.bufnr) table.insert(lines, '**Source**: `' .. source_name .. '`') diff --git a/lua/CopilotChat/config/providers.lua b/lua/CopilotChat/config/providers.lua index eee655a8..5b35a2b4 100644 --- a/lua/CopilotChat/config/providers.lua +++ b/lua/CopilotChat/config/providers.lua @@ -171,6 +171,7 @@ end ---@class CopilotChat.config.providers.Provider ---@field disabled nil|boolean ---@field get_headers nil|fun():table,number? +---@field get_info nil|fun(headers:table):string[] ---@field get_models nil|fun(headers:table):table ---@field embed nil|string|fun(inputs:table, headers:table):table ---@field prepare_input nil|fun(inputs:table, opts:CopilotChat.config.providers.Options):table @@ -204,6 +205,56 @@ M.copilot = { response.body.expires_at end, + get_info = function(headers) + local response, err = utils.curl_get('https://api.github.com/copilot_internal/user', { + json_response = true, + headers = { + ['Authorization'] = 'Token ' .. get_github_token('github_copilot'), + }, + }) + + if err then + error(err) + end + + local stats = response.body + local lines = {} + + if not stats or not stats.quota_snapshots then + return { 'No Copilot stats available.' } + end + + local function usage_line(name, snap) + if not snap then + return + end + + table.insert(lines, string.format(' **%s**', name)) + + if snap.unlimited then + table.insert(lines, ' Usage: Unlimited') + else + local used = snap.entitlement - snap.remaining + local percent = snap.entitlement > 0 and (used / snap.entitlement * 100) or 0 + table.insert(lines, string.format(' Usage: %d / %d (%.1f%%)', used, snap.entitlement, percent)) + table.insert(lines, string.format(' Remaining: %d', snap.remaining)) + if snap.overage_permitted ~= nil then + table.insert(lines, ' Overage: ' .. (snap.overage_permitted and 'Permitted' or 'Not Permitted')) + end + end + end + + usage_line('Premium requests', stats.quota_snapshots.premium_interactions) + usage_line('Chat', stats.quota_snapshots.chat) + usage_line('Completions', stats.quota_snapshots.completions) + + if stats.quota_reset_date then + table.insert(lines, string.format(' **Quota** resets on: %s', stats.quota_reset_date)) + end + + return lines + end, + get_models = function(headers) local response, err = utils.curl_get('https://api.githubcopilot.com/models', { json_response = true, From 776d4d4d8f693c0ded0d235d9195f6ddef20a8a6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 31 Jul 2025 14:11:41 +0000 Subject: [PATCH 41/75] chore(doc): auto generate docs --- doc/CopilotChat.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 02932d3c..ff6f1ec8 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 July 30 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 July 31 ============================================================================== Table of Contents *CopilotChat-table-of-contents* @@ -473,6 +473,9 @@ Custom providers can implement these methods: -- Optional: Embeddings provider name or function embed?: string|function, + -- Optional: Extra info about the provider displayed in info panel + get_info?(): string[] + -- Optional: Get extra request headers with optional expiration time get_headers?(): table, number?, From f53069c595a3b12bbe8b9b711917f9ef33c22a0a Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Thu, 31 Jul 2025 17:38:14 +0200 Subject: [PATCH 42/75] fix: properly validate source window when retrieving cwd (#1231) Closes #1230 Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index fa2e4308..d6dd3061 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -498,6 +498,9 @@ function M.set_source(source_winnr) bufnr = source_bufnr, winnr = source_winnr, cwd = function() + if not vim.api.nvim_win_is_valid(source_winnr) then + return '.' + end local dir = vim.w[source_winnr].cchat_cwd if not dir or dir == '' then return '.' From 82be513c07a27f55860d55144c54040d1c93cf2a Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 1 Aug 2025 01:20:29 +0200 Subject: [PATCH 43/75] fix(chat): improve how sticky prompts are stored and parsed (#1233) Preserve sticky from current user message as well when calling .ask directly. Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 66 +++++++++++++++++++++++++--------------- 1 file changed, 41 insertions(+), 25 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index d6dd3061..0fda4bad 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -38,14 +38,24 @@ local state = { ---@param prompt string ---@param config CopilotChat.config.Shared local function insert_sticky(prompt, config) + local existing_prompt = M.chat:get_message('user') + local combined_prompt = (existing_prompt and existing_prompt.content or '') .. '\n' .. (prompt or '') local lines = vim.split(prompt or '', '\n') local stickies = utils.ordered_map() local sticky_indices = {} + local in_code_block = false + for _, line in ipairs(vim.split(combined_prompt, '\n')) do + if line:match('^```') then + in_code_block = not in_code_block + end + if vim.startswith(line, '> ') and not in_code_block then + stickies:set(vim.trim(line:sub(3)), true) + end + end for i, line in ipairs(lines) do if vim.startswith(line, '> ') then table.insert(sticky_indices, i) - stickies:set(vim.trim(line:sub(3)), true) end end for i = #sticky_indices, 1, -1 do @@ -99,6 +109,20 @@ local function insert_sticky(prompt, config) return table.concat(prompt_lines, '\n') end +local function store_sticky(prompt) + local sticky = {} + local in_code_block = false + for _, line in ipairs(vim.split(prompt, '\n')) do + if line:match('^```') then + in_code_block = not in_code_block + end + if vim.startswith(line, '> ') and not in_code_block then + table.insert(sticky, line:sub(3)) + end + end + state.sticky = sticky +end + --- Update the highlights for chat buffer local function update_highlights() local selection_ns = vim.api.nvim_create_namespace('copilot-chat-selection') @@ -498,11 +522,10 @@ function M.set_source(source_winnr) bufnr = source_bufnr, winnr = source_winnr, cwd = function() - if not vim.api.nvim_win_is_valid(source_winnr) then - return '.' - end - local dir = vim.w[source_winnr].cchat_cwd - if not dir or dir == '' then + local ok, dir = pcall(function() + return vim.w[source_winnr].cchat_cwd + end) + if not ok or not dir or dir == '' then return '.' end return dir @@ -764,6 +787,7 @@ function M.open(config) M.chat:open(config) + -- Add sticky values from provided config when opening the chat local message = M.chat:get_message('user') if message then local prompt = insert_sticky(message.content, config) @@ -772,7 +796,6 @@ function M.open(config) role = 'user', content = '\n' .. prompt, }, true) - M.chat:finish() end end @@ -889,37 +912,30 @@ function M.ask(prompt, config) vim.diagnostic.reset(vim.api.nvim_create_namespace('copilot-chat-diagnostics')) config = vim.tbl_deep_extend('force', M.config, config or {}) - prompt = insert_sticky(prompt, config) - prompt = vim.trim(prompt) + -- Stop previous conversation and open window if not config.headless then if config.clear_chat_on_new_prompt then M.stop(true) elseif client:stop() then finish() end - if not M.chat:focused() then M.open(config) end + else + update_source() + end + + -- Resolve prompt after window is opened + prompt = insert_sticky(prompt, config) + prompt = vim.trim(prompt) + -- Prepare chat + if not config.headless then + store_sticky(prompt) M.chat:start() M.chat:append('\n') - - local sticky = {} - local in_code_block = false - for _, line in ipairs(vim.split(prompt, '\n')) do - if line:match('^```') then - in_code_block = not in_code_block - end - if vim.startswith(line, '> ') and not in_code_block then - table.insert(sticky, line:sub(3)) - end - end - - state.sticky = sticky - else - update_source() end -- Resolve prompt references From fc93d1c535bf9538a0a036f118b1034930ee5eb9 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 1 Aug 2025 01:24:08 +0200 Subject: [PATCH 44/75] fix(chat): properly reset modifiable after modifying it (#1234) Signed-off-by: Tomas Slusny --- lua/CopilotChat/ui/chat.lua | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index e2cf7503..049973f8 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -439,6 +439,7 @@ function Chat:add_message(message, replace) local section = current_message.section if section then + local modifiable = vim.bo[self.bufnr].modifiable vim.bo[self.bufnr].modifiable = true vim.api.nvim_buf_set_lines( self.bufnr, @@ -447,7 +448,7 @@ function Chat:add_message(message, replace) false, vim.split(message.content, '\n') ) - vim.bo[self.bufnr].modifiable = false + vim.bo[self.bufnr].modifiable = modifiable self:append('') end else @@ -474,9 +475,10 @@ function Chat:remove_message(role) end -- Remove the section from the buffer + local modifiable = vim.bo[self.bufnr].modifiable vim.bo[self.bufnr].modifiable = true vim.api.nvim_buf_set_lines(self.bufnr, section.start_line - 2, section.end_line + 1, false, {}) - vim.bo[self.bufnr].modifiable = false + vim.bo[self.bufnr].modifiable = modifiable -- Remove the message from the messages list for i, msg in ipairs(self.messages) do @@ -505,9 +507,10 @@ function Chat:append(str) local last_line, last_column, _ = self:last() + local modifiable = vim.bo[self.bufnr].modifiable vim.bo[self.bufnr].modifiable = true vim.api.nvim_buf_set_text(self.bufnr, last_line, last_column, last_line, last_column, vim.split(str, '\n')) - vim.bo[self.bufnr].modifiable = false + vim.bo[self.bufnr].modifiable = modifiable if should_follow_cursor then self:follow() @@ -520,9 +523,11 @@ function Chat:clear() self.token_count = nil self.token_max_count = nil self.messages = {} + + local modifiable = vim.bo[self.bufnr].modifiable vim.bo[self.bufnr].modifiable = true vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, false, {}) - vim.bo[self.bufnr].modifiable = false + vim.bo[self.bufnr].modifiable = modifiable end --- Create the chat window buffer. From dec3127e4f373875d7fd50854e221ed8dc0e061f Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 1 Aug 2025 01:27:05 +0200 Subject: [PATCH 45/75] fix(utils): remove temp file after curl request is done (#1235) Closes #1194 Signed-off-by: Tomas Slusny --- lua/CopilotChat/utils.lua | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index ae3e6313..0b42fffb 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -398,8 +398,16 @@ M.curl_post = async.wrap(function(url, opts, callback) args = vim.tbl_deep_extend('force', M.curl_args, args) args = vim.tbl_deep_extend('force', args, opts or {}) + local temp_file_path = nil + args.callback = function(response) log.debug('POST response:', url, response) + if temp_file_path then + local ok, err = pcall(os.remove, temp_file_path) + if not ok then + log.debug('Failed to remove temp file:', temp_file_path, err) + end + end if response and not vim.startswith(tostring(response.status), '20') then callback(response, response.body) return @@ -430,7 +438,8 @@ M.curl_post = async.wrap(function(url, opts, callback) ['Content-Type'] = 'application/json', }) - args.body = M.temp_file(vim.json.encode(args.body)) + temp_file_path = M.temp_file(vim.json.encode(args.body)) + args.body = temp_file_path end curl.post(url, args) From 425ff0c48906a94ca522f6d2e98e4b39057e4fd4 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 1 Aug 2025 01:30:59 +0200 Subject: [PATCH 46/75] fix(chat): highlight keywords only in user messages (#1236) Signed-off-by: Tomas Slusny --- lua/CopilotChat/ui/chat.lua | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 049973f8..06b35d64 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -669,6 +669,22 @@ function Chat:render() end end + -- Keywords + if current_message and current_message.role == 'user' then + -- FIXME: This is not optimal, but i cant figure out how to do it better as treesitter keeps overriding it + local patterns = { + '()#?#[^ ]+()', + '()@[^ ]+()', + '()%$[^ ]+()', + '()/[^ ]+()', + } + for _, pattern in ipairs(patterns) do + for s, e in line:gmatch(pattern) do + vim.api.nvim_buf_add_highlight(self.bufnr, highlight_ns, 'CopilotChatKeyword', l - 1, s - 1, e - 1) + end + end + end + -- If last line, finish last message if l == #lines and current_message then current_message.section.end_line = l @@ -695,20 +711,6 @@ function Chat:render() end end end - - -- Highlight keywords - -- FIXME: This is not optimal, but i cant figure out how to do it better as treesitter keeps overriding it - local patterns = { - '()#?#[^ ]+()', - '()@[^ ]+()', - '()%$[^ ]+()', - '()/[^ ]+()', - } - for _, pattern in ipairs(patterns) do - for s, e in line:gmatch(pattern) do - vim.api.nvim_buf_add_highlight(self.bufnr, highlight_ns, 'CopilotChatKeyword', l - 1, s - 1, e - 1) - end - end end -- Replace self.messages with new_messages (preserving tool_calls, etc.) From 1a17534c17e6ae9f5417df08b8c0eec434c47875 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 1 Aug 2025 01:45:30 +0200 Subject: [PATCH 47/75] fix(chat): show messages in overlay (#1237) message can be triggered from other places other than chat so just show chat overlay instead Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 3 +-- lua/CopilotChat/ui/chat.lua | 15 ++++++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 0fda4bad..dd9fb70c 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -1213,8 +1213,7 @@ function M.setup(config) M.chat:delete() end M.chat = require('CopilotChat.ui.chat')( - M.config.headers, - M.config.separator, + M.config, utils.key_to_info('show_help', M.config.mappings.show_help), function(bufnr) for name, _ in pairs(M.config.mappings) do diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 06b35d64..5605ee16 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -68,18 +68,18 @@ end ---@field private separator string ---@field private spinner CopilotChat.ui.spinner.Spinner ---@field private chat_overlay CopilotChat.ui.overlay.Overlay -local Chat = class(function(self, headers, separator, help, on_buf_create) +local Chat = class(function(self, config, help, on_buf_create) Overlay.init(self, 'copilot-chat', help, on_buf_create) self.winnr = nil - self.config = {} + self.config = config self.token_count = nil self.token_max_count = nil self.messages = {} self.layout = nil - self.headers = headers or {} - self.separator = separator + self.headers = config.headers + self.separator = config.separator self.spinner = Spinner() self.chat_overlay = Overlay('copilot-overlay', 'q to close', function(bufnr) @@ -97,7 +97,12 @@ local Chat = class(function(self, headers, separator, help, on_buf_create) notify.listen(notify.MESSAGE, function(msg) utils.schedule_main() - self:append('\n' .. msg .. '\n') + + if not self:visible() then + self:open(self.config) + end + + self:overlay({ text = msg }) end) end, Overlay) From 7c82936f2126b106af1b1bf0f9ae4d42dd45fcad Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 1 Aug 2025 02:00:10 +0200 Subject: [PATCH 48/75] fix(prompt): be more specific when definining what is resource (#1238) Signed-off-by: Tomas Slusny --- lua/CopilotChat/config/prompts.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/CopilotChat/config/prompts.lua b/lua/CopilotChat/config/prompts.lua index 2b56bdda..8bb5efd9 100644 --- a/lua/CopilotChat/config/prompts.lua +++ b/lua/CopilotChat/config/prompts.lua @@ -29,7 +29,7 @@ If tools are explicitly defined in your system context: - Execute actions directly when you indicate you'll do so, without asking for permission. - Only use tools that exist and use proper invocation procedures - no multi_tool_use.parallel. - Before using tools to retrieve information, check if it's already available in context: - 1. Content shared via "#:" references or headers + 1. Resources shared via "# " headers and referenced via "##" links 2. Code blocks with file path labels 3. Other contextual sharing like selected text or conversation history - If you don't have explicit tool definitions in your system context, assume NO tools are available and clearly state this limitation when asked. NEVER pretend to retrieve content you cannot access. From 718f48b120f98081a556df0caadd916511490ecb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 1 Aug 2025 00:00:28 +0000 Subject: [PATCH 49/75] chore(doc): auto generate docs --- doc/CopilotChat.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index ff6f1ec8..bde68614 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 July 31 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 01 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 02cf9e52634b3e3d45beb2c4e5bbc17da28aef64 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 1 Aug 2025 02:24:01 +0200 Subject: [PATCH 50/75] feat(health): add temp dir writable check (#1239) Signed-off-by: Tomas Slusny --- lua/CopilotChat/config/mappings.lua | 1 - lua/CopilotChat/health.lua | 19 +++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/lua/CopilotChat/config/mappings.lua b/lua/CopilotChat/config/mappings.lua index 73cb25a0..3bd339cc 100644 --- a/lua/CopilotChat/config/mappings.lua +++ b/lua/CopilotChat/config/mappings.lua @@ -450,7 +450,6 @@ return { utils.schedule_main() table.insert(lines, '**Logs**: `' .. copilot.config.log_path .. '`') table.insert(lines, '**History**: `' .. copilot.config.history_path .. '`') - table.insert(lines, '**Temp Files**: `' .. vim.fn.fnamemodify(os.tmpname(), ':h') .. '`') table.insert(lines, '') for provider, infolines in pairs(infos) do diff --git a/lua/CopilotChat/health.lua b/lua/CopilotChat/health.lua index a75416ee..acfc5bd2 100644 --- a/lua/CopilotChat/health.lua +++ b/lua/CopilotChat/health.lua @@ -55,6 +55,25 @@ function M.check() error('setup: not called, required for plugin to work. See `:h CopilotChat-installation`.') end + start('CopilotChat.nvim [filesystem]') + + local testfile = os.tmpname() + local f = io.open(testfile, 'w') + local writable = false + if f then + f:write('test') + f:close() + writable = true + end + if writable then + ok('temp dir: writable (' .. testfile .. ')') + os.remove(testfile) + else + local stat = vim.loop.fs_stat(vim.fn.fnamemodify(testfile, ':h')) + local perms = stat and string.format('%o', stat.mode % 512) or 'unknown' + error('temp dir: not writable. Permissions: ' .. perms .. ' (dir: ' .. vim.fn.fnamemodify(testfile, ':h') .. ')') + end + start('CopilotChat.nvim [commands]') local curl_version = run_command('curl', '--version') From 01d38b27ea2183302c743dac09b27611d09d7591 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 1 Aug 2025 02:41:03 +0200 Subject: [PATCH 51/75] feat(providers): prioritize gh clie auth if available for github models (#1240) Also add healthchecks Signed-off-by: Tomas Slusny --- README.md | 2 -- lua/CopilotChat/config/providers.lua | 35 ++++++++++++++++++++++++---- lua/CopilotChat/health.lua | 11 ++++++--- 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 19e790c6..333b8495 100644 --- a/README.md +++ b/README.md @@ -40,8 +40,6 @@ CopilotChat.nvim is a Neovim plugin that brings GitHub Copilot Chat capabilities ## Optional Dependencies -- [copilot.vim](https://github.com/github/copilot.vim) - For `:Copilot setup` authorization, otherwise in-built method i used - - [tiktoken_core](https://github.com/gptlang/lua-tiktoken) - For accurate token counting - Arch Linux: Install [`luajit-tiktoken-bin`](https://aur.archlinux.org/packages/luajit-tiktoken-bin) or [`lua51-tiktoken-bin`](https://aur.archlinux.org/packages/lua51-tiktoken-bin) from AUR - Via luarocks: `sudo luarocks install --lua-version 5.1 tiktoken_core` diff --git a/lua/CopilotChat/config/providers.lua b/lua/CopilotChat/config/providers.lua index 5b35a2b4..d2ac7976 100644 --- a/lua/CopilotChat/config/providers.lua +++ b/lua/CopilotChat/config/providers.lua @@ -99,7 +99,7 @@ end --- Get the github copilot oauth cached token (gu_ token) ---@return string -local function get_github_token(tag) +local function get_github_copilot_token(tag) local function config_path() local config = vim.fs.normalize('$XDG_CONFIG_HOME') if config and vim.uv.fs_stat(config) then @@ -157,6 +157,33 @@ local function get_github_token(tag) return github_device_flow(tag, 'Iv1.b507a08c87ecfe98', '') end +local function get_github_models_token(tag) + local token = get_token(tag) + if token then + return token + end + + -- loading token from the environment only in GitHub Codespaces + local codespaces = os.getenv('CODESPACES') + token = os.getenv('GITHUB_TOKEN') + if token and codespaces then + return set_token(tag, token, false) + end + + -- loading token from gh cli if available + if vim.fn.executable('gh') == 0 then + local result = utils.system({ 'gh', 'auth', 'token', '-h', 'github.com' }) + if result and result.code == 0 and result.stdout then + local gh_token = vim.trim(result.stdout) + if gh_token ~= '' and not gh_token:find('no oauth token') then + return set_token(tag, gh_token, false) + end + end + end + + return github_device_flow(tag, '178c6fc778ccc68e1d6a', 'read:user copilot') +end + ---@class CopilotChat.config.providers.Options ---@field model CopilotChat.client.Model ---@field temperature number? @@ -188,7 +215,7 @@ M.copilot = { local response, err = utils.curl_get('https://api.github.com/copilot_internal/v2/token', { json_response = true, headers = { - ['Authorization'] = 'Token ' .. get_github_token('github_copilot'), + ['Authorization'] = 'Token ' .. get_github_copilot_token('github_copilot'), }, }) @@ -209,7 +236,7 @@ M.copilot = { local response, err = utils.curl_get('https://api.github.com/copilot_internal/user', { json_response = true, headers = { - ['Authorization'] = 'Token ' .. get_github_token('github_copilot'), + ['Authorization'] = 'Token ' .. get_github_copilot_token('github_copilot'), }, }) @@ -430,7 +457,7 @@ M.github_models = { get_headers = function() return { - ['Authorization'] = 'Bearer ' .. github_device_flow('github_models', 'Ov23liqtJusaUH38tIoK', 'read:user copilot'), + ['Authorization'] = 'Bearer ' .. get_github_models_token('github_models'), } end, diff --git a/lua/CopilotChat/health.lua b/lua/CopilotChat/health.lua index acfc5bd2..1c8bc3b4 100644 --- a/lua/CopilotChat/health.lua +++ b/lua/CopilotChat/health.lua @@ -55,8 +55,6 @@ function M.check() error('setup: not called, required for plugin to work. See `:h CopilotChat-installation`.') end - start('CopilotChat.nvim [filesystem]') - local testfile = os.tmpname() local f = io.open(testfile, 'w') local writable = false @@ -104,6 +102,13 @@ function M.check() ok('lynx: ' .. lynx_version) end + local gh_version = run_command('gh', '--version') + if gh_version == false then + warn('gh: missing, optional for improved GitHub authorization. See "https://cli.github.com/".') + else + ok('gh: ' .. gh_version) + end + start('CopilotChat.nvim [dependencies]') if lualib_installed('plenary') then @@ -118,7 +123,7 @@ function M.check() ok('copilot: ' .. (has_copilot and 'copilot.lua' or 'copilot.vim')) else warn( - 'copilot: missing, optional for improved auth implementation. Install "github/copilot.vim" or "zbirenbaum/copilot.lua" plugins.' + 'copilot: missing, optional for improved Copilot authorization. Install "github/copilot.vim" or "zbirenbaum/copilot.lua" plugins.' ) end From 3264dd25ef9d82862c88fe57c1ed2fcacab37c1c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 1 Aug 2025 00:41:24 +0000 Subject: [PATCH 52/75] chore(doc): auto generate docs --- doc/CopilotChat.txt | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index bde68614..4400881d 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -62,16 +62,12 @@ capabilities directly into your editor. It provides: OPTIONAL DEPENDENCIES *CopilotChat-optional-dependencies* -- copilot.vim - For `:Copilot setup` - authorization, otherwise in-built method i used -- tiktoken_core - For accurate token - counting +- tiktoken_core - For accurate token counting - Arch Linux: Install `luajit-tiktoken-bin` or `lua51-tiktoken-bin` from AUR - Via luarocks: `sudo luarocks install --lua-version 5.1 tiktoken_core` - Manual: Download from lua-tiktoken releases and save as `tiktoken_core.so` in your Lua path - git - For git diff context features -- ripgrep - For improved search - performance +- ripgrep - For improved search performance - lynx - For improved URL context features From ea4168476a0fdbd5bf40a4a769d6c1dc998929eb Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 1 Aug 2025 03:05:39 +0200 Subject: [PATCH 53/75] feat(mappings): use C-Space as default completion trigger instead of (#1241) To prevent conflicts with copilot.vim and copilot.lua by default, confusing people Signed-off-by: Tomas Slusny --- lua/CopilotChat/config/mappings.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/CopilotChat/config/mappings.lua b/lua/CopilotChat/config/mappings.lua index 3bd339cc..aaaa0ea6 100644 --- a/lua/CopilotChat/config/mappings.lua +++ b/lua/CopilotChat/config/mappings.lua @@ -137,7 +137,7 @@ end ---@field show_help CopilotChat.config.mapping|false|nil return { complete = { - insert = '', + insert = '', callback = function() copilot.trigger_complete() end, From 653111bbbbc8fa6f8aa8a9bf5f820c6f926c9b89 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 1 Aug 2025 03:08:30 +0200 Subject: [PATCH 54/75] chore: update readme and do not show empty choices for completion (#1242) Signed-off-by: Tomas Slusny --- README.md | 32 ++++++++++++++++---------------- lua/CopilotChat/functions.lua | 4 +++- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 333b8495..d9d5d433 100644 --- a/README.md +++ b/README.md @@ -149,22 +149,22 @@ Commands are used to control the chat interface: Default mappings in the chat interface: -| Insert | Normal | Action | -| ------- | ------- | ------------------------------------------ | -| `` | - | Trigger/accept completion menu for tokens | -| `` | `q` | Close the chat window | -| `` | `` | Reset and clear the chat window | -| `` | `` | Submit the current prompt | -| - | `grr` | Toggle sticky prompt for line under cursor | -| - | `grx` | Clear all sticky prompts in prompt | -| `` | `` | Accept nearest diff | -| - | `gj` | Jump to section of nearest diff | -| - | `gqa` | Add all answers from chat to quickfix list | -| - | `gqd` | Add all diffs from chat to quickfix list | -| - | `gy` | Yank nearest diff to register | -| - | `gd` | Show diff between source and nearest diff | -| - | `gc` | Show info about current chat | -| - | `gh` | Show help message | +| Insert | Normal | Action | +| ----------- | ------- | ------------------------------------------ | +| `` | - | Trigger/accept completion menu for tokens | +| `` | `q` | Close the chat window | +| `` | `` | Reset and clear the chat window | +| `` | `` | Submit the current prompt | +| - | `grr` | Toggle sticky prompt for line under cursor | +| - | `grx` | Clear all sticky prompts in prompt | +| `` | `` | Accept nearest diff | +| - | `gj` | Jump to section of nearest diff | +| - | `gqa` | Add all answers from chat to quickfix list | +| - | `gqd` | Add all diffs from chat to quickfix list | +| - | `gy` | Yank nearest diff to register | +| - | `gd` | Show diff between source and nearest diff | +| - | `gc` | Show info about current chat | +| - | `gh` | Show help message | The mappings can be customized by setting the `mappings` table in your configuration. Each mapping can have: diff --git a/lua/CopilotChat/functions.lua b/lua/CopilotChat/functions.lua index 04c5b103..782510ea 100644 --- a/lua/CopilotChat/functions.lua +++ b/lua/CopilotChat/functions.lua @@ -211,7 +211,9 @@ function M.enter_input(schema, source) if cfg.enum then local choices = type(cfg.enum) == 'table' and cfg.enum or cfg.enum(source) local choice - if #choices == 1 then + if #choices == 0 then + choice = nil + elseif #choices == 1 then choice = choices[1] else choice = utils.select(choices, { From cbea42c9cf3bab433b02674b696cd7d540eb1f52 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 1 Aug 2025 01:08:46 +0000 Subject: [PATCH 55/75] chore(doc): auto generate docs --- doc/CopilotChat.txt | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 4400881d..716fbd92 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -183,22 +183,22 @@ KEY MAPPINGS *CopilotChat-key-mappings* Default mappings in the chat interface: - Insert Normal Action - -------- -------- -------------------------------------------- - - Trigger/accept completion menu for tokens - q Close the chat window - Reset and clear the chat window - Submit the current prompt - - grr Toggle sticky prompt for line under cursor - - grx Clear all sticky prompts in prompt - Accept nearest diff - - gj Jump to section of nearest diff - - gqa Add all answers from chat to quickfix list - - gqd Add all diffs from chat to quickfix list - - gy Yank nearest diff to register - - gd Show diff between source and nearest diff - - gc Show info about current chat - - gh Show help message + Insert Normal Action + ----------- -------- -------------------------------------------- + - Trigger/accept completion menu for tokens + q Close the chat window + Reset and clear the chat window + Submit the current prompt + - grr Toggle sticky prompt for line under cursor + - grx Clear all sticky prompts in prompt + Accept nearest diff + - gj Jump to section of nearest diff + - gqa Add all answers from chat to quickfix list + - gqd Add all diffs from chat to quickfix list + - gy Yank nearest diff to register + - gd Show diff between source and nearest diff + - gc Show info about current chat + - gh Show help message The mappings can be customized by setting the `mappings` table in your configuration. Each mapping can have: From f7a3228f155d0533197ac79b0e08582e504d0399 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 1 Aug 2025 10:36:25 +0200 Subject: [PATCH 56/75] fix(functions): properly filter tool schema from functions (#1243) Signed-off-by: Tomas Slusny --- lua/CopilotChat/functions.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lua/CopilotChat/functions.lua b/lua/CopilotChat/functions.lua index 782510ea..6e936a3d 100644 --- a/lua/CopilotChat/functions.lua +++ b/lua/CopilotChat/functions.lua @@ -44,12 +44,12 @@ local function sorted_propnames(schema) return prop_names end -local function filter_schema(tbl) +local function filter_schema(tbl, root) if type(tbl) ~= 'table' then return tbl end - if utils.empty(tbl.properties) then + if root and utils.empty(tbl.properties) then return nil end @@ -139,7 +139,7 @@ function M.parse_schema(tool) end if schema then - schema = filter_schema(schema) + schema = filter_schema(schema, true) end return schema From d1d155e50193e28a3ec00f8e21d6f11445f96ea1 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 1 Aug 2025 11:41:43 +0200 Subject: [PATCH 57/75] fix(chat): properly replace all message data when replacing message (#1244) Signed-off-by: Tomas Slusny --- lua/CopilotChat/ui/chat.lua | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lua/CopilotChat/ui/chat.lua b/lua/CopilotChat/ui/chat.lua index 5605ee16..fe70feff 100644 --- a/lua/CopilotChat/ui/chat.lua +++ b/lua/CopilotChat/ui/chat.lua @@ -440,7 +440,11 @@ function Chat:add_message(message, replace) elseif replace and current_message then -- Replace the content of the current message self:render() - current_message.content = message.content + + for k, v in pairs(message) do + current_message[k] = v + end + local section = current_message.section if section then From c3d00484c42065a883db0fb859c686e277012d6c Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 1 Aug 2025 11:54:00 +0200 Subject: [PATCH 58/75] fix(chat): do not allow sending empty prompt (#1245) Closes #1189 Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index dd9fb70c..1c25eda5 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -991,6 +991,14 @@ function M.ask(prompt, config) end end + if utils.empty(prompt) and utils.empty(resolved_tools) then + if not config.headless then + M.chat:remove_message('user') + finish() + end + return + end + local ask_ok, ask_response = pcall(client.ask, client, prompt, { headless = config.headless, history = M.chat.messages, From ced388c97b313ea235809824ed501970b155e59f Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 1 Aug 2025 12:46:36 +0200 Subject: [PATCH 59/75] feat(prompts): add configurable response language (#1246) Closes #1086 Signed-off-by: Tomas Slusny --- README.md | 8 ++++---- lua/CopilotChat/config.lua | 2 ++ lua/CopilotChat/config/prompts.lua | 1 + lua/CopilotChat/init.lua | 1 + 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d9d5d433..3355f1b7 100644 --- a/README.md +++ b/README.md @@ -171,14 +171,13 @@ The mappings can be customized by setting the `mappings` table in your configura - `normal`: Key for normal mode - `insert`: Key for insert mode -For example, to change the submit prompt mapping or show_diff full diff option: +For example, to change the complete mapping to Tab or show_diff full diff option: ```lua { mappings = { - submit_prompt = { - normal = 's', - insert = '' + complete = { + insert = '' } show_diff = { full_diff = true @@ -444,6 +443,7 @@ Below are all available configuration options with their default values: model = 'gpt-4.1', -- Default model to use, see ':CopilotChatModels' for available models (can be specified manually in prompt via $). tools = nil, -- Default tool or array of tools (or groups) to share with LLM (can be specified manually in prompt via @). sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat (can be specified manually in prompt via >). + language = 'English', -- Default language to use for answers resource_processing = false, -- Enable intelligent resource processing (skips unnecessary resources to save tokens) diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 8809fdc6..30ad2bd8 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -18,6 +18,7 @@ ---@field model string? ---@field tools string|table|nil ---@field sticky string|table|nil +---@field language string? ---@field resource_processing boolean? ---@field temperature number? ---@field headless boolean? @@ -58,6 +59,7 @@ return { model = 'gpt-4.1', -- Default model to use, see ':CopilotChatModels' for available models (can be specified manually in prompt via $). tools = nil, -- Default tool or array of tools (or groups) to share with LLM (can be specified manually in prompt via @). sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat (can be specified manually in prompt via >). + language = 'English', -- Default language to use for answers resource_processing = false, -- Enable intelligent resource processing (skips unnecessary resources to save tokens) diff --git a/lua/CopilotChat/config/prompts.lua b/lua/CopilotChat/config/prompts.lua index 8bb5efd9..8764f914 100644 --- a/lua/CopilotChat/config/prompts.lua +++ b/lua/CopilotChat/config/prompts.lua @@ -2,6 +2,7 @@ local COPILOT_BASE = [[ When asked for your name, you must respond with "GitHub Copilot". Follow the user's requirements carefully & to the letter. Keep your answers short and impersonal. +Always answer in {LANGUAGE} unless explicitly asked otherwise. The user works in editor called Neovim which has these core concepts: - Buffer: An in-memory text content that may be associated with a file diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 1c25eda5..67848dd8 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -473,6 +473,7 @@ function M.resolve_prompt(prompt, config) if config.system_prompt then config.system_prompt = config.system_prompt:gsub('{OS_NAME}', jit.os) + config.system_prompt = config.system_prompt:gsub('{LANGUAGE}', config.language) if state.source then config.system_prompt = config.system_prompt:gsub('{DIR}', state.source.cwd()) end From aa5e50ee15cfdb0e8cb66ecab9f928ed0fc148ee Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 1 Aug 2025 10:46:52 +0000 Subject: [PATCH 60/75] chore(doc): auto generate docs --- doc/CopilotChat.txt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 716fbd92..8ecf90b9 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -205,14 +205,14 @@ configuration. Each mapping can have: - `normal`: Key for normal mode - `insert`: Key for insert mode -For example, to change the submit prompt mapping or show_diff full diff option: +For example, to change the complete mapping to Tab or show_diff full diff +option: >lua { mappings = { - submit_prompt = { - normal = 's', - insert = '' + complete = { + insert = '' } show_diff = { full_diff = true @@ -515,6 +515,7 @@ Below are all available configuration options with their default values: model = 'gpt-4.1', -- Default model to use, see ':CopilotChatModels' for available models (can be specified manually in prompt via $). tools = nil, -- Default tool or array of tools (or groups) to share with LLM (can be specified manually in prompt via @). sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat (can be specified manually in prompt via >). + language = 'English', -- Default language to use for answers resource_processing = false, -- Enable intelligent resource processing (skips unnecessary resources to save tokens) From ed28296abb59fe083855d6f70371fbccbc939a92 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 1 Aug 2025 18:29:20 +0200 Subject: [PATCH 61/75] docs: properly use functions instead of tools in README (#1248) Signed-off-by: Tomas Slusny --- README.md | 42 +++++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 3355f1b7..9917c928 100644 --- a/README.md +++ b/README.md @@ -286,24 +286,24 @@ For supported models, see: ## Functions -Functions provide additional information and behaviour to the chat. -Tools can be organized into groups by setting the `group` property. Tools assigned to a group are not automatically made available to the LLM - they must be explicitly activated. -To use grouped tools in your prompt, include `@group_name` in your message. This allows the LLM to access and use all tools in that group during the current interaction. -Add tools using `#tool_name[:input]` syntax: - -| Function | Input Support | Description | -| ------------- | ------------- | ------------------------------------------------------ | -| `buffer` | βœ“ (name) | Retrieves content from a specific buffer | -| `buffers` | βœ“ (scope) | Fetches content from multiple buffers (listed/visible) | -| `diagnostics` | βœ“ (scope) | Collects code diagnostics (errors, warnings) | -| `file` | βœ“ (path) | Reads content from a specified file path | -| `gitdiff` | βœ“ (sha) | Retrieves git diff information (unstaged/staged/sha) | -| `gitstatus` | - | Retrieves git status information | -| `glob` | βœ“ (pattern) | Lists filenames matching a pattern in workspace | -| `grep` | βœ“ (pattern) | Searches for a pattern across files in workspace | -| `quickfix` | - | Includes content of files in quickfix list | -| `register` | βœ“ (register) | Provides access to specified Vim register | -| `url` | βœ“ (url) | Fetches content from a specified URL | +Functions provide additional information and behaviour to the chat. +Functions can be organized into groups by setting the `group` property. +Functions can be made available to the LLM with `@group_name` or `@function_name` syntax. LLM will then be able to use them in responses as tool calls. +If function has URI, they can also be used directly in prompt with `#function_name[:input]` syntax for providing context as resources. + +| Function | Input Support | URI | Description | +| ------------- | ------------- | --- | ------------------------------------------------------ | +| `buffer` | βœ“ (name) | βœ“ | Retrieves content from a specific buffer | +| `buffers` | βœ“ (scope) | βœ“ | Fetches content from multiple buffers (listed/visible) | +| `diagnostics` | βœ“ (scope) | βœ“ | Collects code diagnostics (errors, warnings) | +| `file` | βœ“ (path) | βœ“ | Reads content from a specified file path | +| `gitdiff` | βœ“ (sha) | βœ“ | Retrieves git diff information (unstaged/staged/sha) | +| `gitstatus` | - | βœ“ | Retrieves git status information | +| `glob` | βœ“ (pattern) | βœ“ | Lists filenames matching a pattern in workspace | +| `grep` | βœ“ (pattern) | βœ“ | Searches for a pattern across files in workspace | +| `quickfix` | - | βœ“ | Includes content of files in quickfix list | +| `register` | βœ“ (register) | βœ“ | Provides access to specified Vim register | +| `url` | βœ“ (url) | βœ“ | Fetches content from a specified URL | Examples: @@ -318,6 +318,9 @@ Examples: > #quickfix > #register:+ > #url:https://example.com +> @glob +> @grep +> @file ``` Define your own functions in the configuration with input handling and schema: @@ -563,7 +566,7 @@ local chat = require("CopilotChat") chat.ask(prompt, config) -- Ask a question with optional config chat.response() -- Get the last response text chat.resolve_prompt() -- Resolve prompt references -chat.resolve_tools() -- Resolve tools that are available for automatic use by LLM +chat.resolve_functions() -- Resolve functions that are available for automatic use by LLM (WARN: async, requires plenary.async.run) chat.resolve_model() -- Resolve model from prompt (WARN: async, requires plenary.async.run) -- Window Management @@ -619,6 +622,7 @@ window:add_sticky(sticky) -- Add sticky prompt to chat mess -- Content Management window:append(text) -- Append text to chat window window:clear() -- Clear chat window content +window:start() -- Start writing to chat window window:finish() -- Finish writing to chat window -- Navigation From 40a88e13dac6d15427eeac951b7d2274613971ff Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 1 Aug 2025 16:29:43 +0000 Subject: [PATCH 62/75] chore(doc): auto generate docs --- doc/CopilotChat.txt | 57 ++++++++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 8ecf90b9..1e5a908f 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -329,42 +329,43 @@ For supported models, see: FUNCTIONS *CopilotChat-functions* -Functions provide additional information and behaviour to the chat. Tools can -be organized into groups by setting the `group` property. Tools assigned to a -group are not automatically made available to the LLM - they must be explicitly -activated. To use grouped tools in your prompt, include `@group_name` in your -message. This allows the LLM to access and use all tools in that group during -the current interaction. Add tools using `#tool_name[:input]` syntax: +Functions provide additional information and behaviour to the chat. Functions +can be organized into groups by setting the `group` property. Functions can be +made available to the LLM with `@group_name` or `@function_name` syntax. LLM +will then be able to use them in responses as tool calls. If function has URI, +they can also be used directly in prompt with `#function_name[:input]` syntax +for providing context as resources. - -------------------------------------------------------------------------- - Function Input Description - Support - ------------- ------------ ----------------------------------------------- - buffer βœ“ (name) Retrieves content from a specific buffer + ------------------------------------------------------------------------------- + Function Input URI Description + Support + ------------- ------------ ----- ---------------------------------------------- + buffer βœ“ (name) βœ“ Retrieves content from a specific buffer - buffers βœ“ (scope) Fetches content from multiple buffers - (listed/visible) + buffers βœ“ (scope) βœ“ Fetches content from multiple buffers + (listed/visible) - diagnostics βœ“ (scope) Collects code diagnostics (errors, warnings) + diagnostics βœ“ (scope) βœ“ Collects code diagnostics (errors, warnings) - file βœ“ (path) Reads content from a specified file path + file βœ“ (path) βœ“ Reads content from a specified file path - gitdiff βœ“ (sha) Retrieves git diff information - (unstaged/staged/sha) + gitdiff βœ“ (sha) βœ“ Retrieves git diff information + (unstaged/staged/sha) - gitstatus - Retrieves git status information + gitstatus - βœ“ Retrieves git status information - glob βœ“ (pattern) Lists filenames matching a pattern in workspace + glob βœ“ (pattern) βœ“ Lists filenames matching a pattern in + workspace - grep βœ“ (pattern) Searches for a pattern across files in - workspace + grep βœ“ (pattern) βœ“ Searches for a pattern across files in + workspace - quickfix - Includes content of files in quickfix list + quickfix - βœ“ Includes content of files in quickfix list - register βœ“ (register) Provides access to specified Vim register + register βœ“ (register) βœ“ Provides access to specified Vim register - url βœ“ (url) Fetches content from a specified URL - -------------------------------------------------------------------------- + url βœ“ (url) βœ“ Fetches content from a specified URL + ------------------------------------------------------------------------------- Examples: >markdown @@ -378,6 +379,9 @@ Examples: > #quickfix > #register:+ > #url:https://example.com + > @glob + > @grep + > @file < Define your own functions in the configuration with input handling and schema: @@ -640,7 +644,7 @@ CORE *CopilotChat-core* chat.ask(prompt, config) -- Ask a question with optional config chat.response() -- Get the last response text chat.resolve_prompt() -- Resolve prompt references - chat.resolve_tools() -- Resolve tools that are available for automatic use by LLM + chat.resolve_functions() -- Resolve functions that are available for automatic use by LLM (WARN: async, requires plenary.async.run) chat.resolve_model() -- Resolve model from prompt (WARN: async, requires plenary.async.run) -- Window Management @@ -697,6 +701,7 @@ You can also access the chat window UI methods through the `chat.chat` object: -- Content Management window:append(text) -- Append text to chat window window:clear() -- Clear chat window content + window:start() -- Start writing to chat window window:finish() -- Finish writing to chat window -- Navigation From 9fd068f5d6a0ca00fc739a98f29125cb577b2dfa Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Fri, 1 Aug 2025 23:19:27 +0200 Subject: [PATCH 63/75] fix(files): use also plenary filetype on top of vim.filetype.match (#1250) I thought its fine to go back to vim.filetype.match but its as unreliable as ever. Closes #1249 Signed-off-by: Tomas Slusny --- lua/CopilotChat/utils.lua | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lua/CopilotChat/utils.lua b/lua/CopilotChat/utils.lua index 0b42fffb..251238ca 100644 --- a/lua/CopilotChat/utils.lua +++ b/lua/CopilotChat/utils.lua @@ -214,7 +214,17 @@ end ---@param filename string The file name ---@return string|nil function M.filetype(filename) - return vim.filetype.match({ filename = filename }) + local filetype = require('plenary.filetype') + + local ft = filetype.detect(filename, { + fs_access = false, + }) + + if ft == '' or not ft then + return vim.filetype.match({ filename = filename }) + end + + return ft end --- Get the mimetype from filetype From 3509cf0971c59ba79fbcd618d82910f8567a7929 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 2 Aug 2025 14:16:56 +0200 Subject: [PATCH 64/75] fix(functions): change neovim://buffer to just buffer:// to avoid conflicts (#1252) also make quickfix function properly support buffer resources Signed-off-by: Tomas Slusny --- lua/CopilotChat/config/functions.lua | 30 +++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/lua/CopilotChat/config/functions.lua b/lua/CopilotChat/config/functions.lua index 65363811..25c3f6c5 100644 --- a/lua/CopilotChat/config/functions.lua +++ b/lua/CopilotChat/config/functions.lua @@ -118,7 +118,7 @@ return { buffer = { group = 'copilot', - uri = 'neovim://buffer/{name}', + uri = 'buffer://{name}', description = 'Retrieves content from a specific buffer. Useful for discussing or analyzing code from a particular file that is currently loaded.', schema = { @@ -162,7 +162,7 @@ return { end return { { - uri = 'neovim://buffer/' .. name, + uri = 'buffer://' .. name, mimetype = mimetype, data = data, }, @@ -172,7 +172,7 @@ return { buffers = { group = 'copilot', - uri = 'neovim://buffers/{scope}', + uri = 'buffers://{scope}', description = 'Fetches content from multiple buffers. Helps with discussing or analyzing code across multiple files simultaneously.', schema = { @@ -204,7 +204,7 @@ return { return nil end return { - uri = 'neovim://buffer/' .. name, + uri = 'buffer://' .. name, mimetype = mimetype, data = data, } @@ -229,23 +229,35 @@ return { return {} end - local unique_files = {} + local file_to_bufnr = {} for _, item in ipairs(items) do local filename = item.filename or vim.api.nvim_buf_get_name(item.bufnr) if filename then - unique_files[filename] = true + if item.bufnr and utils.buf_valid(item.bufnr) then + file_to_bufnr[filename] = item.bufnr + else + file_to_bufnr[filename] = false + end end end return vim - .iter(vim.tbl_keys(unique_files)) + .iter(vim.tbl_keys(file_to_bufnr)) :map(function(file) - local data, mimetype = resources.get_file(file) + local bufnr = file_to_bufnr[file] + local data, mimetype, uri + if bufnr and bufnr ~= false then + data, mimetype = resources.get_buffer(bufnr) + uri = 'buffer://' .. file + else + data, mimetype = resources.get_file(file) + uri = 'file://' .. file + end if not data then return nil end return { - uri = 'file://' .. file, + uri = uri, mimetype = mimetype, data = data, } From eec50b885165da1e404b9effcc0f4d52e5f66f3b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 2 Aug 2025 12:17:17 +0000 Subject: [PATCH 65/75] chore(doc): auto generate docs --- doc/CopilotChat.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 1e5a908f..9888219d 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -1,4 +1,4 @@ -*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 01 +*CopilotChat.txt* For NVIM v0.8.0 Last change: 2025 August 02 ============================================================================== Table of Contents *CopilotChat-table-of-contents* From 4cd53a48f04fe45331e4845fabd32a81e46f4d2f Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 2 Aug 2025 15:36:02 +0200 Subject: [PATCH 66/75] docs(README): improve readme (#1253) * docs(README): improve readme Signed-off-by: Tomas Slusny * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * update readme Signed-off-by: Tomas Slusny * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * update again Signed-off-by: Tomas Slusny * more readme updates Signed-off-by: Tomas Slusny * more updates Signed-off-by: Tomas Slusny --------- Signed-off-by: Tomas Slusny Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- README.md | 488 ++++++++++++++++++------------------------------------ 1 file changed, 162 insertions(+), 326 deletions(-) diff --git a/README.md b/README.md index 9917c928..b0ea7e5e 100644 --- a/README.md +++ b/README.md @@ -16,23 +16,24 @@ https://github.com/user-attachments/assets/8cad5643-63b2-4641-a5c4-68bc313f20e6 -CopilotChat.nvim is a Neovim plugin that brings GitHub Copilot Chat capabilities directly into your editor. It provides: +CopilotChat.nvim brings GitHub Copilot Chat capabilities directly into Neovim with a focus on transparency and user control. -- πŸ€– GitHub Copilot Chat integration with official model support (GPT-4o, Claude 3.7 Sonnet, Gemini 2.0 Flash, and more) -- πŸ’» Rich workspace context powered by smart embeddings system -- πŸ”’ Explicit data sharing - only sends what you specifically request, either as resource or selection (by default visual selection) -- πŸ”Œ Modular provider architecture supporting both official and custom LLM backends (Ollama, Gemini, Mistral.ai and more) -- πŸ“ Interactive chat UI with completion, diffs and quickfix integration -- 🎯 Powerful prompt system with composable templates and sticky prompts -- πŸ”„ Extensible function calling system for granular workspace understanding (buffers, files, git diffs, URLs, and more) -- ⚑ Efficient token usage with tiktoken token counting and history management +- πŸ€– **Multiple AI Models** - GitHub Copilot (GPT-4o, Claude 3.7 Sonnet, Gemini 2.0 Flash) + custom providers (Ollama, Mistral.ai) +- πŸ”§ **Tool Calling** - LLM can use workspace functions (file reading, git operations, search) with your explicit approval +- πŸ”’ **Explicit Control** - Only shares what you specifically request - no background data collection +- πŸ“ **Interactive Chat** - Rich UI with completion, diffs, and quickfix integration +- 🎯 **Smart Prompts** - Composable templates and sticky prompts for consistent context +- ⚑ **Efficient** - Smart token usage with tiktoken counting and history management +- πŸ”Œ **Extensible** - [Custom functions](https://github.com/CopilotC-Nvim/CopilotChat.nvim/discussions/categories/functions) and [providers](https://github.com/CopilotC-Nvim/CopilotChat.nvim/discussions/categories/providers), plus integrations like [mcphub.nvim](https://github.com/ravitemer/mcphub.nvim) -# Requirements +# Installation + +## Requirements -- [Neovim 0.10.0+](https://neovim.io/) - Older versions are not officially supported -- [curl](https://curl.se/) - Version 8.0.0+ recommended for best compatibility +- [Neovim 0.10.0+](https://neovim.io/) +- [curl 8.0.0+](https://curl.se/) - [Copilot chat in the IDE](https://github.com/settings/copilot) enabled in GitHub settings -- [plenary.nvim](https://github.com/nvim-lua/plenary.nvim) - Plugin dependency +- [plenary.nvim](https://github.com/nvim-lua/plenary.nvim) > [!WARNING] > For Neovim < 0.11.0, add `noinsert` or `noselect` to your `completeopt` otherwise chat autocompletion will not work. @@ -44,7 +45,6 @@ CopilotChat.nvim is a Neovim plugin that brings GitHub Copilot Chat capabilities - Arch Linux: Install [`luajit-tiktoken-bin`](https://aur.archlinux.org/packages/luajit-tiktoken-bin) or [`lua51-tiktoken-bin`](https://aur.archlinux.org/packages/lua51-tiktoken-bin) from AUR - Via luarocks: `sudo luarocks install --lua-version 5.1 tiktoken_core` - Manual: Download from [lua-tiktoken releases](https://github.com/gptlang/lua-tiktoken/releases) and save as `tiktoken_core.so` in your Lua path - - [git](https://git-scm.com/) - For git diff context features - [ripgrep](https://github.com/BurntSushi/ripgrep) - For improved search performance - [lynx](https://lynx.invisible-island.net/) - For improved URL context features @@ -58,14 +58,6 @@ For various plugin pickers to work correctly, you need to replace `vim.ui.select - [snacks.picker](https://github.com/folke/snacks.nvim/blob/main/docs/picker.md#%EF%B8%8F-config) - enable `ui_select` config - [mini.pick](https://github.com/echasnovski/mini.pick/blob/main/lua/mini/pick.lua#L1229) - set `vim.ui.select = require('mini.pick').ui_select` -Plugin features that use picker: - -- `:CopilotChatPrompts` - for selecting prompts -- `:CopilotChatModels` - for selecting models -- `#:` - for selecting function input - -# Installation - ## [lazy.nvim](https://github.com/folke/lazy.nvim) ```lua @@ -73,23 +65,18 @@ return { { "CopilotC-Nvim/CopilotChat.nvim", dependencies = { - { "nvim-lua/plenary.nvim", branch = "master" }, -- for curl, log and async functions + { "nvim-lua/plenary.nvim", branch = "master" }, }, build = "make tiktoken", opts = { -- See Configuration section for options }, - -- See Commands section for default commands if you want to lazy load on them }, } ``` -See [@jellydn](https://github.com/jellydn) for [configuration](https://github.com/jellydn/lazy-nvim-ide/blob/main/lua/plugins/extras/copilot-chat-v2.lua) - ## [vim-plug](https://github.com/junegunn/vim-plug) -Similar to the lazy setup, you can use the following configuration: - ```vim call plug#begin() Plug 'nvim-lua/plenary.nvim' @@ -97,40 +84,41 @@ Plug 'CopilotC-Nvim/CopilotChat.nvim' call plug#end() lua << EOF -require("CopilotChat").setup { - -- See Configuration section for options -} +require("CopilotChat").setup() EOF ``` -## Manual +# Core Concepts -1. Put the files in the right place +- **Resources** (`#`) - Add specific content (files, git diffs, URLs) to your prompt +- **Tools** (`@`) - Give LLM access to functions it can call with your approval +- **Sticky Prompts** (`> `) - Persist context across single chat session +- **Models** (`$`) - Specify which AI model to use for the chat +- **Prompts** (`/PromptName`) - Use predefined prompt templates for common tasks -``` -mkdir -p ~/.config/nvim/pack/copilotchat/start -cd ~/.config/nvim/pack/copilotchat/start +## Examples -git clone https://github.com/nvim-lua/plenary.nvim -git clone https://github.com/CopilotC-Nvim/CopilotChat.nvim -``` +```markdown +# Add specific file to context -2. Add to your configuration (e.g. `~/.config/nvim/init.lua`) +#file:src/main.lua -```lua -require("CopilotChat").setup { - -- See Configuration section for options -} +# Give LLM access to workspace tools + +@copilot What files are in this project? + +# Sticky prompt that persists + +> #buffer:current +> You are a helpful coding assistant ``` -See [@deathbeam](https://github.com/deathbeam) for [configuration](https://github.com/deathbeam/dotfiles/blob/master/nvim/.config/nvim/lua/config/copilot.lua) +When you use `@copilot`, the LLM can call functions like `glob`, `file`, `gitdiff` etc. You'll see the proposed function call and can approve/reject it before execution. -# Features +# Usage ## Commands -Commands are used to control the chat interface: - | Command | Description | | -------------------------- | ----------------------------- | | `:CopilotChat ?` | Open chat with optional input | @@ -147,8 +135,6 @@ Commands are used to control the chat interface: ## Key Mappings -Default mappings in the chat interface: - | Insert | Normal | Action | | ----------- | ------- | ------------------------------------------ | | `` | - | Trigger/accept completion menu for tokens | @@ -166,73 +152,125 @@ Default mappings in the chat interface: | - | `gc` | Show info about current chat | | - | `gh` | Show help message | -The mappings can be customized by setting the `mappings` table in your configuration. Each mapping can have: +## Built-in Functions + +| Function | Description | Example Usage | +| ------------- | ------------------------------------------------ | ---------------------- | +| `buffer` | Retrieves content from a specific buffer | `#buffer` | +| `buffers` | Fetches content from multiple buffers | `#buffers:visible` | +| `diagnostics` | Collects code diagnostics (errors, warnings) | `#diagnostics:current` | +| `file` | Reads content from a specified file path | `#file:path/to/file` | +| `gitdiff` | Retrieves git diff information | `#gitdiff:staged` | +| `gitstatus` | Retrieves git status information | `#gitstatus` | +| `glob` | Lists filenames matching a pattern in workspace | `#glob:**/*.lua` | +| `grep` | Searches for a pattern across files in workspace | `#grep:TODO` | +| `quickfix` | Includes content of files in quickfix list | `#quickfix` | +| `register` | Provides access to specified Vim register | `#register:+` | +| `url` | Fetches content from a specified URL | `#url:https://...` | + +## Predefined Prompts + +Use with `:CopilotChat` or reference with `/PromptName`: + +- `/Explain` - Explain selected code +- `/Review` - Code review +- `/Fix` - Fix bugs +- `/Optimize` - Performance improvements +- `/Docs` - Add documentation +- `/Tests` - Generate tests +- `/Commit` - Commit message + +# Configuration -- `normal`: Key for normal mode -- `insert`: Key for insert mode +For all available configuration options, see [`lua/CopilotChat/config.lua`](lua/CopilotChat/config.lua). -For example, to change the complete mapping to Tab or show_diff full diff option: +## Quick Setup + +Most users only need to configure a few options: ```lua { - mappings = { - complete = { - insert = '' - } - show_diff = { - full_diff = true - } - } + model = 'gpt-4.1', -- AI model to use + temperature = 0.1, -- Lower = focused, higher = creative + window = { + layout = 'vertical', -- 'vertical', 'horizontal', 'float' + width = 0.5, -- 50% of screen width + }, + auto_insert_mode = true, -- Enter insert mode when opening } ``` -## Prompts +## Window & Appearance -### Predefined Prompts +```lua +{ + window = { + layout = 'float', + width = 80, -- Fixed width in columns + height = 20, -- Fixed height in rows + border = 'rounded', -- 'single', 'double', 'rounded', 'solid' + title = 'πŸ€– AI Assistant', + zindex = 100, -- Ensure window stays on top + }, -Predefined prompt templates for common tasks. Reference them with `/PromptName` in chat, use `:CopilotChat` or `:CopilotChatPrompts` to select them: + headers = { + user = 'πŸ‘€ You: ', + assistant = 'πŸ€– Copilot: ', + tool = 'πŸ”§ Tool: ', + }, + separator = '━━', + show_folds = false, -- Disable folding for cleaner look +} +``` -| Prompt | Description | -| ---------- | ------------------------------------------------ | -| `Explain` | Write an explanation for the selected code | -| `Review` | Review the selected code | -| `Fix` | Rewrite the code with bug fixes | -| `Optimize` | Optimize code for performance and readability | -| `Docs` | Add documentation comments to the code | -| `Tests` | Generate tests for the code | -| `Commit` | Write commit message using commitizen convention | +## Buffer Behavior -Define your own prompts in the configuration: +```lua +-- Auto-command to customize chat buffer behavior +vim.api.nvim_create_autocmd('BufEnter', { + pattern = 'copilot-*', + callback = function() + vim.opt_local.relativenumber = false + vim.opt_local.number = false + vim.opt_local.conceallevel = 0 + end, +}) +``` + +## Highlights + +You can customize colors by setting highlight groups in your config: ```lua -{ - prompts = { - MyCustomPrompt = { - prompt = 'Explain how it works.', - system_prompt = 'You are very good at explaining stuff', - mapping = 'ccmc', - description = 'My custom prompt description', - } - } -} +-- In your colorscheme or init.lua +vim.api.nvim_set_hl(0, 'CopilotChatHeader', { fg = '#7C3AED', bold = true }) +vim.api.nvim_set_hl(0, 'CopilotChatSeparator', { fg = '#374151' }) +vim.api.nvim_set_hl(0, 'CopilotChatKeyword', { fg = '#10B981', italic = true }) ``` -### System Prompts +Types of copilot highlights: -System prompts define the AI model's behavior. Reference them with `/PROMPT_NAME` in chat: +- `CopilotChatHeader` - Header highlight in chat buffer +- `CopilotChatSeparator` - Separator highlight in chat buffer +- `CopilotChatStatus` - Status and spinner in chat buffer +- `CopilotChatHelp` - Help messages in chat buffer (help, references) +- `CopilotChatSelection` - Selection highlight in source buffer +- `CopilotChatKeyword` - Keyword highlight in chat buffer (e.g. prompts, tools) +- `CopilotChatAnnotation` - Annotation highlight in chat buffer (file headers, tool call headers, tool call body) -| Prompt | Description | -| ---------------------- | ------------------------------------------ | -| `COPILOT_BASE` | All prompts should be built on top of this | -| `COPILOT_INSTRUCTIONS` | Base instructions | -| `COPILOT_EXPLAIN` | Adds coding tutor behavior | -| `COPILOT_REVIEW` | Adds code review behavior with diagnostics | +## Prompts -Define your own system prompts in the configuration (similar to `prompts`): +Define your own prompts in the configuration: ```lua { prompts = { + MyCustomPrompt = { + prompt = 'Explain how it works.', + system_prompt = 'You are very good at explaining stuff', + mapping = 'ccmc', + description = 'My custom prompt description', + }, Yarrr = { system_prompt = 'You are fascinated by pirates, so please respond in pirate speak.', }, @@ -243,86 +281,8 @@ Define your own system prompts in the configuration (similar to `prompts`): } ``` -### Sticky Prompts - -Sticky prompts persist across chat sessions. They're useful for maintaining model or resource selection. They work as follows: - -1. Prefix text with `> ` using markdown blockquote syntax -2. The prompt will be copied at the start of every new chat prompt -3. Edit sticky prompts freely while maintaining the `> ` prefix - -Examples: - -```markdown -> #glob:`*.lua` -> List all files in the workspace - -> @models Using Mistral-small -> What is 1 + 11 -``` - -You can also set default sticky prompts in the configuration: - -```lua -{ - sticky = { - '#glob:*.lua', - } -} -``` - -## Models - -You can control which AI model to use in three ways: - -1. List available models with `:CopilotChatModels` -2. Set model in prompt with `$model_name` -3. Configure default model via `model` config key - -For supported models, see: - -- [Copilot Chat Models](https://docs.github.com/en/copilot/using-github-copilot/ai-models/changing-the-ai-model-for-copilot-chat#ai-models-for-copilot-chat) -- [GitHub Marketplace Models](https://github.com/marketplace/models) (experimental, limited usage) - ## Functions -Functions provide additional information and behaviour to the chat. -Functions can be organized into groups by setting the `group` property. -Functions can be made available to the LLM with `@group_name` or `@function_name` syntax. LLM will then be able to use them in responses as tool calls. -If function has URI, they can also be used directly in prompt with `#function_name[:input]` syntax for providing context as resources. - -| Function | Input Support | URI | Description | -| ------------- | ------------- | --- | ------------------------------------------------------ | -| `buffer` | βœ“ (name) | βœ“ | Retrieves content from a specific buffer | -| `buffers` | βœ“ (scope) | βœ“ | Fetches content from multiple buffers (listed/visible) | -| `diagnostics` | βœ“ (scope) | βœ“ | Collects code diagnostics (errors, warnings) | -| `file` | βœ“ (path) | βœ“ | Reads content from a specified file path | -| `gitdiff` | βœ“ (sha) | βœ“ | Retrieves git diff information (unstaged/staged/sha) | -| `gitstatus` | - | βœ“ | Retrieves git status information | -| `glob` | βœ“ (pattern) | βœ“ | Lists filenames matching a pattern in workspace | -| `grep` | βœ“ (pattern) | βœ“ | Searches for a pattern across files in workspace | -| `quickfix` | - | βœ“ | Includes content of files in quickfix list | -| `register` | βœ“ (register) | βœ“ | Provides access to specified Vim register | -| `url` | βœ“ (url) | βœ“ | Fetches content from a specified URL | - -Examples: - -```markdown -> #buffer:init.lua -> #buffers:visible -> #diagnostics:current -> #file:path/to/file.js -> #git:staged -> #glob:`**/*.lua` -> #grep:`function setup` -> #quickfix -> #register:+ -> #url:https://example.com -> @glob -> @grep -> @file -``` - Define your own functions in the configuration with input handling and schema: ```lua @@ -356,47 +316,46 @@ Define your own functions in the configuration with input handling and schema: } ``` -### External Functions - -For external functions implementations, see the [discussion page](https://github.com/CopilotC-Nvim/CopilotChat.nvim/discussions/categories/functions). - ## Selections -Selections determine the source content for chat interactions. - -Available selections are located in `local select = require("CopilotChat.select")`: - -| Selection | Description | -| --------- | ------------------------------------------------------ | -| `visual` | Current visual selection | -| `buffer` | Current buffer content | -| `line` | Current line content | -| `unnamed` | Unnamed register (last deleted/changed/yanked content) | - -You can set a default selection in the configuration: +Control what content is automatically included: ```lua { - -- Uses visual selection or falls back to buffer + -- Use visual selection, fallback to current line selection = function(source) - return select.visual(source) or select.buffer(source) - end + return require('CopilotChat.select').visual(source) or + require('CopilotChat.select').line(source) + end, } ``` -## Providers +**Available selections:** -Providers are modules that implement integration with different AI providers. +- `require('CopilotChat.select').visual` - Current visual selection +- `require('CopilotChat.select').buffer` - Entire buffer content +- `require('CopilotChat.select').line` - Current line content +- `require('CopilotChat.select').unnamed` - Unnamed register (last deleted/changed/yanked) -### Built-in Providers +## Providers -- `copilot` - Default GitHub Copilot provider used for chat -- `github_models` - Provider for GitHub Marketplace models (disabled by default, enable it via `providers.github_models.disabled = false`) -- `copilot_embeddings` - Provider for Copilot embeddings, not standalone, used by `copilot` and `github_models` providers +Add custom AI providers: -### Provider Interface +```lua +{ + providers = { + my_provider = { + get_url = function(opts) return "https://api.example.com/chat" end, + get_headers = function() return { ["Authorization"] = "Bearer " .. api_key } end, + get_models = function() return { { id = "gpt-4.1", name = "GPT-4.1 model" } } end, + prepare_input = require('CopilotChat.config.providers').copilot.prepare_input, + prepare_output = require('CopilotChat.config.providers').copilot.prepare_output, + } + } +} +``` -Custom providers can implement these methods: +**Provider Interface:** ```lua { @@ -426,134 +385,11 @@ Custom providers can implement these methods: } ``` -### External Providers - -For external providers (Ollama, LM Studio, Mistral.ai), see the [providers discussion page](https://github.com/CopilotC-Nvim/CopilotChat.nvim/discussions/categories/providers). - -# Configuration - -## Default Configuration - -Below are all available configuration options with their default values: - -```lua -{ - - -- Shared config starts here (can be passed to functions at runtime and configured via setup function) - - system_prompt = 'COPILOT_INSTRUCTIONS', -- System prompt to use (can be specified manually in prompt via /). - - model = 'gpt-4.1', -- Default model to use, see ':CopilotChatModels' for available models (can be specified manually in prompt via $). - tools = nil, -- Default tool or array of tools (or groups) to share with LLM (can be specified manually in prompt via @). - sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat (can be specified manually in prompt via >). - language = 'English', -- Default language to use for answers - - resource_processing = false, -- Enable intelligent resource processing (skips unnecessary resources to save tokens) +**Built-in providers:** - temperature = 0.1, -- Result temperature - headless = false, -- Do not write to chat buffer and use history (useful for using custom processing) - callback = nil, -- Function called when full response is received - remember_as_sticky = true, -- Remember config as sticky prompts when asking questions - - -- default selection - -- see select.lua for implementation - selection = require('CopilotChat.select').visual, - - -- default window options - window = { - layout = 'vertical', -- 'vertical', 'horizontal', 'float', 'replace', or a function that returns the layout - width = 0.5, -- fractional width of parent, or absolute width in columns when > 1 - height = 0.5, -- fractional height of parent, or absolute height in rows when > 1 - -- Options below only apply to floating windows - relative = 'editor', -- 'editor', 'win', 'cursor', 'mouse' - border = 'single', -- 'none', single', 'double', 'rounded', 'solid', 'shadow' - row = nil, -- row position of the window, default is centered - col = nil, -- column position of the window, default is centered - title = 'Copilot Chat', -- title of chat window - footer = nil, -- footer of chat window - zindex = 1, -- determines if window is on top or below other floating windows - blend = 0, -- window blend (transparency), 0-100, 0 is opaque, 100 is fully transparent - }, - - show_help = true, -- Shows help message as virtual lines when waiting for user input - show_folds = true, -- Shows folds for sections in chat - highlight_selection = true, -- Highlight selection - highlight_headers = true, -- Highlight headers in chat - auto_follow_cursor = true, -- Auto-follow cursor in chat - auto_insert_mode = false, -- Automatically enter insert mode when opening window and on new prompt - insert_at_end = false, -- Move cursor to end of buffer when inserting text - clear_chat_on_new_prompt = false, -- Clears chat on every new prompt - - -- Static config starts here (can be configured only via setup function) - - debug = false, -- Enable debug logging (same as 'log_level = 'debug') - log_level = 'info', -- Log level to use, 'trace', 'debug', 'info', 'warn', 'error', 'fatal' - proxy = nil, -- [protocol://]host[:port] Use this proxy - allow_insecure = false, -- Allow insecure server connections - - chat_autocomplete = true, -- Enable chat autocompletion (when disabled, requires manual `mappings.complete` trigger) - - log_path = vim.fn.stdpath('state') .. '/CopilotChat.log', -- Default path to log file - history_path = vim.fn.stdpath('data') .. '/copilotchat_history', -- Default path to stored history - - headers = { - user = '## User ', -- Header to use for user questions - assistant = '## Copilot ', -- Header to use for AI answers - tool = '## Tool ', -- Header to use for tool calls - }, - - separator = '───', -- Separator to use in chat - - -- default providers - -- see config/providers.lua for implementation - providers = require('CopilotChat.config.providers'), - - -- default functions - -- see config/functions.lua for implementation - functions = require('CopilotChat.config.functions'), - - -- default prompts - -- see config/prompts.lua for implementation - prompts = require('CopilotChat.config.prompts'), - - -- default mappings - -- see config/mappings.lua for implementation - mappings = require('CopilotChat.config.mappings'), -} -``` - -## Customizing Buffers - -Types of copilot buffers: - -- `copilot-chat` - Main chat buffer -- `copilot-overlay` - Overlay buffers (e.g. help, info, diff) - -You can set local options for plugin buffers like this: - -```lua -vim.api.nvim_create_autocmd('BufEnter', { - pattern = 'copilot-*', - callback = function() - -- Set buffer-local options - vim.opt_local.relativenumber = false - vim.opt_local.number = false - vim.opt_local.conceallevel = 0 - end -}) -``` - -## Customizing Highlights - -Types of copilot highlights: - -- `CopilotChatHeader` - Header highlight in chat buffer -- `CopilotChatSeparator` - Separator highlight in chat buffer -- `CopilotChatStatus` - Status and spinner in chat buffer -- `CopilotChatHelp` - Help messages in chat buffer (help, references) -- `CopilotChatSelection` - Selection highlight in source buffer -- `CopilotChatKeyword` - Keyword highlight in chat buffer (e.g. prompts, tools) -- `CopilotChatAnnotation` - Annotation highlight in chat buffer (file headers, tool call headers, tool call body) +- `copilot` - GitHub Copilot (default) +- `github_models` - GitHub Marketplace models (disabled by default) +- `copilot_embeddings` - Copilot embeddings provider # API Reference From 5ee40855485b5e1e661b74ecd57e37801594ae4a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 2 Aug 2025 13:36:23 +0000 Subject: [PATCH 67/75] chore(doc): auto generate docs --- doc/CopilotChat.txt | 571 +++++++++++++++----------------------------- 1 file changed, 198 insertions(+), 373 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 9888219d..743276dd 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -3,25 +3,28 @@ ============================================================================== Table of Contents *CopilotChat-table-of-contents* -1. Requirements |CopilotChat-requirements| +1. Installation |CopilotChat-installation| + - Requirements |CopilotChat-requirements| - Optional Dependencies |CopilotChat-optional-dependencies| - Integration with pickers |CopilotChat-integration-with-pickers| -2. Installation |CopilotChat-installation| - lazy.nvim |CopilotChat-lazy.nvim| - vim-plug |CopilotChat-vim-plug| - - Manual |CopilotChat-manual| -3. Features |CopilotChat-features| +2. Core Concepts |CopilotChat-core-concepts| + - Examples |CopilotChat-examples| +3. Usage |CopilotChat-usage| - Commands |CopilotChat-commands| - Key Mappings |CopilotChat-key-mappings| + - Built-in Functions |CopilotChat-built-in-functions| + - Predefined Prompts |CopilotChat-predefined-prompts| +4. Configuration |CopilotChat-configuration| + - Quick Setup |CopilotChat-quick-setup| + - Window & Appearance |CopilotChat-window-&-appearance| + - Buffer Behavior |CopilotChat-buffer-behavior| + - Highlights |CopilotChat-highlights| - Prompts |CopilotChat-prompts| - - Models |CopilotChat-models| - Functions |CopilotChat-functions| - Selections |CopilotChat-selections| - Providers |CopilotChat-providers| -4. Configuration |CopilotChat-configuration| - - Default Configuration |CopilotChat-default-configuration| - - Customizing Buffers |CopilotChat-customizing-buffers| - - Customizing Highlights |CopilotChat-customizing-highlights| 5. API Reference |CopilotChat-api-reference| - Core |CopilotChat-core| - Chat Window |CopilotChat-chat-window| @@ -33,26 +36,28 @@ Table of Contents *CopilotChat-table-of-contents* 8. Stargazers |CopilotChat-stargazers| 9. Links |CopilotChat-links| -CopilotChat.nvim is a Neovim plugin that brings GitHub Copilot Chat -capabilities directly into your editor. It provides: +CopilotChat.nvim brings GitHub Copilot Chat capabilities directly into Neovim +with a focus on transparency and user control. -- πŸ€– GitHub Copilot Chat integration with official model support (GPT-4o, Claude 3.7 Sonnet, Gemini 2.0 Flash, and more) -- πŸ’» Rich workspace context powered by smart embeddings system -- πŸ”’ Explicit data sharing - only sends what you specifically request, either as resource or selection (by default visual selection) -- πŸ”Œ Modular provider architecture supporting both official and custom LLM backends (Ollama, Gemini, Mistral.ai and more) -- πŸ“ Interactive chat UI with completion, diffs and quickfix integration -- 🎯 Powerful prompt system with composable templates and sticky prompts -- πŸ”„ Extensible function calling system for granular workspace understanding (buffers, files, git diffs, URLs, and more) -- ⚑ Efficient token usage with tiktoken token counting and history management +- πŸ€– **Multiple AI Models** - GitHub Copilot (GPT-4o, Claude 3.7 Sonnet, Gemini 2.0 Flash) + custom providers (Ollama, Mistral.ai) +- πŸ”§ **Tool Calling** - LLM can use workspace functions (file reading, git operations, search) with your explicit approval +- πŸ”’ **Explicit Control** - Only shares what you specifically request - no background data collection +- πŸ“ **Interactive Chat** - Rich UI with completion, diffs, and quickfix integration +- 🎯 **Smart Prompts** - Composable templates and sticky prompts for consistent context +- ⚑ **Efficient** - Smart token usage with tiktoken counting and history management +- πŸ”Œ **Extensible** - Custom functions and providers , plus integrations like mcphub.nvim ============================================================================== -1. Requirements *CopilotChat-requirements* +1. Installation *CopilotChat-installation* + -- Neovim 0.10.0+ - Older versions are not officially supported -- curl - Version 8.0.0+ recommended for best compatibility +REQUIREMENTS *CopilotChat-requirements* + +- Neovim 0.10.0+ +- curl 8.0.0+ - Copilot chat in the IDE enabled in GitHub settings -- plenary.nvim - Plugin dependency +- plenary.nvim [!WARNING] For Neovim < 0.11.0, add `noinsert` or `noselect` to your @@ -82,16 +87,6 @@ very basic). Here are some examples: - snacks.picker - enable `ui_select` config - mini.pick - set `vim.ui.select = require('mini.pick').ui_select` -Plugin features that use picker: - -- `:CopilotChatPrompts` - for selecting prompts -- `:CopilotChatModels` - for selecting models -- `#:` - for selecting function input - - -============================================================================== -2. Installation *CopilotChat-installation* - LAZY.NVIM *CopilotChat-lazy.nvim* @@ -100,25 +95,19 @@ LAZY.NVIM *CopilotChat-lazy.nvim* { "CopilotC-Nvim/CopilotChat.nvim", dependencies = { - { "nvim-lua/plenary.nvim", branch = "master" }, -- for curl, log and async functions + { "nvim-lua/plenary.nvim", branch = "master" }, }, build = "make tiktoken", opts = { -- See Configuration section for options }, - -- See Commands section for default commands if you want to lazy load on them }, } < -See @jellydn for configuration - - VIM-PLUG *CopilotChat-vim-plug* -Similar to the lazy setup, you can use the following configuration: - >vim call plug#begin() Plug 'nvim-lua/plenary.nvim' @@ -126,45 +115,49 @@ Similar to the lazy setup, you can use the following configuration: call plug#end() lua << EOF - require("CopilotChat").setup { - -- See Configuration section for options - } + require("CopilotChat").setup() EOF < -MANUAL *CopilotChat-manual* +============================================================================== +2. Core Concepts *CopilotChat-core-concepts* -1. Put the files in the right place +- **Resources** (`#`) - Add specific content (files, git diffs, URLs) to your prompt +- **Tools** (`@`) - Give LLM access to functions it can call with your approval +- **Sticky Prompts** (`> `) - Persist context across single chat session +- **Models** (`$`) - Specify which AI model to use for the chat +- **Prompts** (`/PromptName`) - Use predefined prompt templates for common tasks -> - mkdir -p ~/.config/nvim/pack/copilotchat/start - cd ~/.config/nvim/pack/copilotchat/start - - git clone https://github.com/nvim-lua/plenary.nvim - git clone https://github.com/CopilotC-Nvim/CopilotChat.nvim -< -1. Add to your configuration (e.g.Β `~/.config/nvim/init.lua`) +EXAMPLES *CopilotChat-examples* ->lua - require("CopilotChat").setup { - -- See Configuration section for options - } +>markdown + # Add specific file to context + + #file:src/main.lua + + # Give LLM access to workspace tools + + @copilot What files are in this project? + + # Sticky prompt that persists + + > #buffer:current + > You are a helpful coding assistant < -See @deathbeam for configuration - +When you use `@copilot`, the LLM can call functions like `glob`, `file`, +`gitdiff` etc. You’ll see the proposed function call and can approve/reject +it before execution. ============================================================================== -3. Features *CopilotChat-features* +3. Usage *CopilotChat-usage* COMMANDS *CopilotChat-commands* -Commands are used to control the chat interface: - Command Description -------------------------- ------------------------------- :CopilotChat ? Open chat with optional input @@ -181,8 +174,6 @@ Commands are used to control the chat interface: KEY MAPPINGS *CopilotChat-key-mappings* -Default mappings in the chat interface: - Insert Normal Action ----------- -------- -------------------------------------------- - Trigger/accept completion menu for tokens @@ -199,190 +190,162 @@ Default mappings in the chat interface: - gd Show diff between source and nearest diff - gc Show info about current chat - gh Show help message -The mappings can be customized by setting the `mappings` table in your -configuration. Each mapping can have: -- `normal`: Key for normal mode -- `insert`: Key for insert mode +BUILT-IN FUNCTIONS *CopilotChat-built-in-functions* -For example, to change the complete mapping to Tab or show_diff full diff -option: + ------------------------------------------------------------------------------ + Function Description Example Usage + ------------- ----------------------------------------- ---------------------- + buffer Retrieves content from a specific buffer #buffer ->lua - { - mappings = { - complete = { - insert = '' - } - show_diff = { - full_diff = true - } - } - } -< + buffers Fetches content from multiple buffers #buffers:visible + diagnostics Collects code diagnostics (errors, #diagnostics:current + warnings) -PROMPTS *CopilotChat-prompts* + file Reads content from a specified file path #file:path/to/file + gitdiff Retrieves git diff information #gitdiff:staged -PREDEFINED PROMPTS ~ + gitstatus Retrieves git status information #gitstatus -Predefined prompt templates for common tasks. Reference them with `/PromptName` -in chat, use `:CopilotChat` or `:CopilotChatPrompts` to select -them: + glob Lists filenames matching a pattern in #glob:**/*.lua + workspace - Prompt Description - ---------- -------------------------------------------------- - Explain Write an explanation for the selected code - Review Review the selected code - Fix Rewrite the code with bug fixes - Optimize Optimize code for performance and readability - Docs Add documentation comments to the code - Tests Generate tests for the code - Commit Write commit message using commitizen convention -Define your own prompts in the configuration: + grep Searches for a pattern across files in #grep:TODO + workspace ->lua - { - prompts = { - MyCustomPrompt = { - prompt = 'Explain how it works.', - system_prompt = 'You are very good at explaining stuff', - mapping = 'ccmc', - description = 'My custom prompt description', - } - } - } -< + quickfix Includes content of files in quickfix #quickfix + list + register Provides access to specified Vim register #register:+ -SYSTEM PROMPTS ~ + url Fetches content from a specified URL #url:https://... + ------------------------------------------------------------------------------ -System prompts define the AI model’s behavior. Reference them with -`/PROMPT_NAME` in chat: +PREDEFINED PROMPTS *CopilotChat-predefined-prompts* - Prompt Description - ---------------------- -------------------------------------------- - COPILOT_BASE All prompts should be built on top of this - COPILOT_INSTRUCTIONS Base instructions - COPILOT_EXPLAIN Adds coding tutor behavior - COPILOT_REVIEW Adds code review behavior with diagnostics -Define your own system prompts in the configuration (similar to `prompts`): +Use with `:CopilotChat` or reference with `/PromptName`: ->lua - { - prompts = { - Yarrr = { - system_prompt = 'You are fascinated by pirates, so please respond in pirate speak.', - }, - NiceInstructions = { - system_prompt = 'You are a nice coding tutor, so please respond in a friendly and helpful manner.' .. require('CopilotChat.config.prompts').COPILOT_BASE.system_prompt, - } - } - } -< +- `/Explain` - Explain selected code +- `/Review` - Code review +- `/Fix` - Fix bugs +- `/Optimize` - Performance improvements +- `/Docs` - Add documentation +- `/Tests` - Generate tests +- `/Commit` - Commit message -STICKY PROMPTS ~ - -Sticky prompts persist across chat sessions. They’re useful for maintaining -model or resource selection. They work as follows: +============================================================================== +4. Configuration *CopilotChat-configuration* -1. Prefix text with `>` using markdown blockquote syntax -2. The prompt will be copied at the start of every new chat prompt -3. Edit sticky prompts freely while maintaining the `>` prefix +For all available configuration options, see `lua/CopilotChat/config.lua` +. -Examples: ->markdown - > #glob:`*.lua` - > List all files in the workspace - - > @models Using Mistral-small - > What is 1 + 11 -< +QUICK SETUP *CopilotChat-quick-setup* -You can also set default sticky prompts in the configuration: +Most users only need to configure a few options: >lua { - sticky = { - '#glob:*.lua', - } + model = 'gpt-4.1', -- AI model to use + temperature = 0.1, -- Lower = focused, higher = creative + window = { + layout = 'vertical', -- 'vertical', 'horizontal', 'float' + width = 0.5, -- 50% of screen width + }, + auto_insert_mode = true, -- Enter insert mode when opening } < -MODELS *CopilotChat-models* +WINDOW & APPEARANCE *CopilotChat-window-&-appearance* -You can control which AI model to use in three ways: - -1. List available models with `:CopilotChatModels` -2. Set model in prompt with `$model_name` -3. Configure default model via `model` config key - -For supported models, see: - -- Copilot Chat Models -- GitHub Marketplace Models (experimental, limited usage) +>lua + { + window = { + layout = 'float', + width = 80, -- Fixed width in columns + height = 20, -- Fixed height in rows + border = 'rounded', -- 'single', 'double', 'rounded', 'solid' + title = 'πŸ€– AI Assistant', + zindex = 100, -- Ensure window stays on top + }, + + headers = { + user = 'πŸ‘€ You: ', + assistant = 'πŸ€– Copilot: ', + tool = 'πŸ”§ Tool: ', + }, + separator = '━━', + show_folds = false, -- Disable folding for cleaner look + } +< -FUNCTIONS *CopilotChat-functions* +BUFFER BEHAVIOR *CopilotChat-buffer-behavior* -Functions provide additional information and behaviour to the chat. Functions -can be organized into groups by setting the `group` property. Functions can be -made available to the LLM with `@group_name` or `@function_name` syntax. LLM -will then be able to use them in responses as tool calls. If function has URI, -they can also be used directly in prompt with `#function_name[:input]` syntax -for providing context as resources. +>lua + -- Auto-command to customize chat buffer behavior + vim.api.nvim_create_autocmd('BufEnter', { + pattern = 'copilot-*', + callback = function() + vim.opt_local.relativenumber = false + vim.opt_local.number = false + vim.opt_local.conceallevel = 0 + end, + }) +< - ------------------------------------------------------------------------------- - Function Input URI Description - Support - ------------- ------------ ----- ---------------------------------------------- - buffer βœ“ (name) βœ“ Retrieves content from a specific buffer - buffers βœ“ (scope) βœ“ Fetches content from multiple buffers - (listed/visible) +HIGHLIGHTS *CopilotChat-highlights* - diagnostics βœ“ (scope) βœ“ Collects code diagnostics (errors, warnings) +You can customize colors by setting highlight groups in your config: - file βœ“ (path) βœ“ Reads content from a specified file path +>lua + -- In your colorscheme or init.lua + vim.api.nvim_set_hl(0, 'CopilotChatHeader', { fg = '#7C3AED', bold = true }) + vim.api.nvim_set_hl(0, 'CopilotChatSeparator', { fg = '#374151' }) + vim.api.nvim_set_hl(0, 'CopilotChatKeyword', { fg = '#10B981', italic = true }) +< - gitdiff βœ“ (sha) βœ“ Retrieves git diff information - (unstaged/staged/sha) +Types of copilot highlights: - gitstatus - βœ“ Retrieves git status information +- `CopilotChatHeader` - Header highlight in chat buffer +- `CopilotChatSeparator` - Separator highlight in chat buffer +- `CopilotChatStatus` - Status and spinner in chat buffer +- `CopilotChatHelp` - Help messages in chat buffer (help, references) +- `CopilotChatSelection` - Selection highlight in source buffer +- `CopilotChatKeyword` - Keyword highlight in chat buffer (e.g.Β prompts, tools) +- `CopilotChatAnnotation` - Annotation highlight in chat buffer (file headers, tool call headers, tool call body) - glob βœ“ (pattern) βœ“ Lists filenames matching a pattern in - workspace - grep βœ“ (pattern) βœ“ Searches for a pattern across files in - workspace +PROMPTS *CopilotChat-prompts* - quickfix - βœ“ Includes content of files in quickfix list +Define your own prompts in the configuration: - register βœ“ (register) βœ“ Provides access to specified Vim register +>lua + { + prompts = { + MyCustomPrompt = { + prompt = 'Explain how it works.', + system_prompt = 'You are very good at explaining stuff', + mapping = 'ccmc', + description = 'My custom prompt description', + }, + Yarrr = { + system_prompt = 'You are fascinated by pirates, so please respond in pirate speak.', + }, + NiceInstructions = { + system_prompt = 'You are a nice coding tutor, so please respond in a friendly and helpful manner.' .. require('CopilotChat.config.prompts').COPILOT_BASE.system_prompt, + } + } + } +< - url βœ“ (url) βœ“ Fetches content from a specified URL - ------------------------------------------------------------------------------- -Examples: ->markdown - > #buffer:init.lua - > #buffers:visible - > #diagnostics:current - > #file:path/to/file.js - > #git:staged - > #glob:`**/*.lua` - > #grep:`function setup` - > #quickfix - > #register:+ - > #url:https://example.com - > @glob - > @grep - > @file -< +FUNCTIONS *CopilotChat-functions* Define your own functions in the configuration with input handling and schema: @@ -418,52 +381,47 @@ Define your own functions in the configuration with input handling and schema: < -EXTERNAL FUNCTIONS ~ - -For external functions implementations, see the discussion page -. - - SELECTIONS *CopilotChat-selections* -Selections determine the source content for chat interactions. - -Available selections are located in `local select = -require("CopilotChat.select")`: - - Selection Description - ----------- -------------------------------------------------------- - visual Current visual selection - buffer Current buffer content - line Current line content - unnamed Unnamed register (last deleted/changed/yanked content) -You can set a default selection in the configuration: +Control what content is automatically included: >lua { - -- Uses visual selection or falls back to buffer + -- Use visual selection, fallback to current line selection = function(source) - return select.visual(source) or select.buffer(source) - end + return require('CopilotChat.select').visual(source) or + require('CopilotChat.select').line(source) + end, } < +**Available selections:** -PROVIDERS *CopilotChat-providers* - -Providers are modules that implement integration with different AI providers. - +- `require('CopilotChat.select').visual` - Current visual selection +- `require('CopilotChat.select').buffer` - Entire buffer content +- `require('CopilotChat.select').line` - Current line content +- `require('CopilotChat.select').unnamed` - Unnamed register (last deleted/changed/yanked) -BUILT-IN PROVIDERS ~ -- `copilot` - Default GitHub Copilot provider used for chat -- `github_models` - Provider for GitHub Marketplace models (disabled by default, enable it via `providers.github_models.disabled = false`) -- `copilot_embeddings` - Provider for Copilot embeddings, not standalone, used by `copilot` and `github_models` providers +PROVIDERS *CopilotChat-providers* +Add custom AI providers: -PROVIDER INTERFACE ~ +>lua + { + providers = { + my_provider = { + get_url = function(opts) return "https://api.example.com/chat" end, + get_headers = function() return { ["Authorization"] = "Bearer " .. api_key } end, + get_models = function() return { { id = "gpt-4.1", name = "GPT-4.1 model" } } end, + prepare_input = require('CopilotChat.config.providers').copilot.prepare_input, + prepare_output = require('CopilotChat.config.providers').copilot.prepare_output, + } + } + } +< -Custom providers can implement these methods: +**Provider Interface:** >lua { @@ -493,142 +451,11 @@ Custom providers can implement these methods: } < +**Built-in providers:** -EXTERNAL PROVIDERS ~ - -For external providers (Ollama, LM Studio, Mistral.ai), see the providers -discussion page -. - - -============================================================================== -4. Configuration *CopilotChat-configuration* - - -DEFAULT CONFIGURATION *CopilotChat-default-configuration* - -Below are all available configuration options with their default values: - ->lua - { - - -- Shared config starts here (can be passed to functions at runtime and configured via setup function) - - system_prompt = 'COPILOT_INSTRUCTIONS', -- System prompt to use (can be specified manually in prompt via /). - - model = 'gpt-4.1', -- Default model to use, see ':CopilotChatModels' for available models (can be specified manually in prompt via $). - tools = nil, -- Default tool or array of tools (or groups) to share with LLM (can be specified manually in prompt via @). - sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat (can be specified manually in prompt via >). - language = 'English', -- Default language to use for answers - - resource_processing = false, -- Enable intelligent resource processing (skips unnecessary resources to save tokens) - - temperature = 0.1, -- Result temperature - headless = false, -- Do not write to chat buffer and use history (useful for using custom processing) - callback = nil, -- Function called when full response is received - remember_as_sticky = true, -- Remember config as sticky prompts when asking questions - - -- default selection - -- see select.lua for implementation - selection = require('CopilotChat.select').visual, - - -- default window options - window = { - layout = 'vertical', -- 'vertical', 'horizontal', 'float', 'replace', or a function that returns the layout - width = 0.5, -- fractional width of parent, or absolute width in columns when > 1 - height = 0.5, -- fractional height of parent, or absolute height in rows when > 1 - -- Options below only apply to floating windows - relative = 'editor', -- 'editor', 'win', 'cursor', 'mouse' - border = 'single', -- 'none', single', 'double', 'rounded', 'solid', 'shadow' - row = nil, -- row position of the window, default is centered - col = nil, -- column position of the window, default is centered - title = 'Copilot Chat', -- title of chat window - footer = nil, -- footer of chat window - zindex = 1, -- determines if window is on top or below other floating windows - blend = 0, -- window blend (transparency), 0-100, 0 is opaque, 100 is fully transparent - }, - - show_help = true, -- Shows help message as virtual lines when waiting for user input - show_folds = true, -- Shows folds for sections in chat - highlight_selection = true, -- Highlight selection - highlight_headers = true, -- Highlight headers in chat - auto_follow_cursor = true, -- Auto-follow cursor in chat - auto_insert_mode = false, -- Automatically enter insert mode when opening window and on new prompt - insert_at_end = false, -- Move cursor to end of buffer when inserting text - clear_chat_on_new_prompt = false, -- Clears chat on every new prompt - - -- Static config starts here (can be configured only via setup function) - - debug = false, -- Enable debug logging (same as 'log_level = 'debug') - log_level = 'info', -- Log level to use, 'trace', 'debug', 'info', 'warn', 'error', 'fatal' - proxy = nil, -- [protocol://]host[:port] Use this proxy - allow_insecure = false, -- Allow insecure server connections - - chat_autocomplete = true, -- Enable chat autocompletion (when disabled, requires manual `mappings.complete` trigger) - - log_path = vim.fn.stdpath('state') .. '/CopilotChat.log', -- Default path to log file - history_path = vim.fn.stdpath('data') .. '/copilotchat_history', -- Default path to stored history - - headers = { - user = '## User ', -- Header to use for user questions - assistant = '## Copilot ', -- Header to use for AI answers - tool = '## Tool ', -- Header to use for tool calls - }, - - separator = '───', -- Separator to use in chat - - -- default providers - -- see config/providers.lua for implementation - providers = require('CopilotChat.config.providers'), - - -- default functions - -- see config/functions.lua for implementation - functions = require('CopilotChat.config.functions'), - - -- default prompts - -- see config/prompts.lua for implementation - prompts = require('CopilotChat.config.prompts'), - - -- default mappings - -- see config/mappings.lua for implementation - mappings = require('CopilotChat.config.mappings'), - } -< - - -CUSTOMIZING BUFFERS *CopilotChat-customizing-buffers* - -Types of copilot buffers: - -- `copilot-chat` - Main chat buffer -- `copilot-overlay` - Overlay buffers (e.g.Β help, info, diff) - -You can set local options for plugin buffers like this: - ->lua - vim.api.nvim_create_autocmd('BufEnter', { - pattern = 'copilot-*', - callback = function() - -- Set buffer-local options - vim.opt_local.relativenumber = false - vim.opt_local.number = false - vim.opt_local.conceallevel = 0 - end - }) -< - - -CUSTOMIZING HIGHLIGHTS *CopilotChat-customizing-highlights* - -Types of copilot highlights: - -- `CopilotChatHeader` - Header highlight in chat buffer -- `CopilotChatSeparator` - Separator highlight in chat buffer -- `CopilotChatStatus` - Status and spinner in chat buffer -- `CopilotChatHelp` - Help messages in chat buffer (help, references) -- `CopilotChatSelection` - Selection highlight in source buffer -- `CopilotChatKeyword` - Keyword highlight in chat buffer (e.g.Β prompts, tools) -- `CopilotChatAnnotation` - Annotation highlight in chat buffer (file headers, tool call headers, tool call body) +- `copilot` - GitHub Copilot (default) +- `github_models` - GitHub Marketplace models (disabled by default) +- `copilot_embeddings` - Copilot embeddings provider ============================================================================== @@ -801,9 +628,7 @@ Contributions of any kind are welcome! ============================================================================== 9. Links *CopilotChat-links* -1. *@jellydn*: -2. *@deathbeam*: -3. *Stargazers over time*: https://starchart.cc/CopilotC-Nvim/CopilotChat.nvim.svg?variant=adaptive +1. *Stargazers over time*: https://starchart.cc/CopilotC-Nvim/CopilotChat.nvim.svg?variant=adaptive Generated by panvimdoc From 8116b2d79bb335b8a7d3f19853b4271cd8ca6b06 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 2 Aug 2025 15:51:32 +0200 Subject: [PATCH 68/75] docs(README): use table for prompts as well (#1254) * docs(README): use table for prompts as well Signed-off-by: Tomas Slusny * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Signed-off-by: Tomas Slusny Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- README.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index b0ea7e5e..22de3b98 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,7 @@ When you use `@copilot`, the LLM can call functions like `glob`, `file`, `gitdif | `:CopilotChatModels` | View/select available models | | `:CopilotChat` | Use specific prompt template | -## Key Mappings +## Chat Key Mappings | Insert | Normal | Action | | ----------- | ------- | ------------------------------------------ | @@ -152,7 +152,7 @@ When you use `@copilot`, the LLM can call functions like `glob`, `file`, `gitdif | - | `gc` | Show info about current chat | | - | `gh` | Show help message | -## Built-in Functions +## Functions | Function | Description | Example Usage | | ------------- | ------------------------------------------------ | ---------------------- | @@ -168,17 +168,17 @@ When you use `@copilot`, the LLM can call functions like `glob`, `file`, `gitdif | `register` | Provides access to specified Vim register | `#register:+` | | `url` | Fetches content from a specified URL | `#url:https://...` | -## Predefined Prompts - -Use with `:CopilotChat` or reference with `/PromptName`: +## Prompts -- `/Explain` - Explain selected code -- `/Review` - Code review -- `/Fix` - Fix bugs -- `/Optimize` - Performance improvements -- `/Docs` - Add documentation -- `/Tests` - Generate tests -- `/Commit` - Commit message +| Prompt | Description | +| ---------- | ---------------------------------------------------------------------- | +| `Explain` | Write detailed explanation of selected code as paragraphs | +| `Review` | Comprehensive code review with line-specific issue reporting | +| `Fix` | Identify problems and rewrite code with fixes and explanation | +| `Optimize` | Improve performance and readability with optimization strategy | +| `Docs` | Add documentation comments to selected code | +| `Tests` | Generate tests for selected code | +| `Commit` | Generate commit message with commitizen convention from staged changes | # Configuration From 4eb9076dbaffdbabd919c3719346047175997c79 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 2 Aug 2025 13:51:52 +0000 Subject: [PATCH 69/75] chore(doc): auto generate docs --- doc/CopilotChat.txt | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index 743276dd..e5596682 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -13,9 +13,9 @@ Table of Contents *CopilotChat-table-of-contents* - Examples |CopilotChat-examples| 3. Usage |CopilotChat-usage| - Commands |CopilotChat-commands| - - Key Mappings |CopilotChat-key-mappings| - - Built-in Functions |CopilotChat-built-in-functions| - - Predefined Prompts |CopilotChat-predefined-prompts| + - Chat Key Mappings |CopilotChat-chat-key-mappings| + - Functions |CopilotChat-functions| + - Prompts |CopilotChat-prompts| 4. Configuration |CopilotChat-configuration| - Quick Setup |CopilotChat-quick-setup| - Window & Appearance |CopilotChat-window-&-appearance| @@ -172,7 +172,7 @@ COMMANDS *CopilotChat-commands* :CopilotChatModels View/select available models :CopilotChat Use specific prompt template -KEY MAPPINGS *CopilotChat-key-mappings* +CHAT KEY MAPPINGS *CopilotChat-chat-key-mappings* Insert Normal Action ----------- -------- -------------------------------------------- @@ -191,7 +191,7 @@ KEY MAPPINGS *CopilotChat-key-mappings* - gc Show info about current chat - gh Show help message -BUILT-IN FUNCTIONS *CopilotChat-built-in-functions* +FUNCTIONS *CopilotChat-functions* ------------------------------------------------------------------------------ Function Description Example Usage @@ -223,18 +223,26 @@ BUILT-IN FUNCTIONS *CopilotChat-built-in-functions* url Fetches content from a specified URL #url:https://... ------------------------------------------------------------------------------ -PREDEFINED PROMPTS *CopilotChat-predefined-prompts* +PROMPTS *CopilotChat-prompts* + + ------------------------------------------------------------------------- + Prompt Description + ---------- -------------------------------------------------------------- + Explain Write detailed explanation of selected code as paragraphs + + Review Comprehensive code review with line-specific issue reporting + + Fix Identify problems and rewrite code with fixes and explanation + + Optimize Improve performance and readability with optimization strategy -Use with `:CopilotChat` or reference with `/PromptName`: + Docs Add documentation comments to selected code -- `/Explain` - Explain selected code -- `/Review` - Code review -- `/Fix` - Fix bugs -- `/Optimize` - Performance improvements -- `/Docs` - Add documentation -- `/Tests` - Generate tests -- `/Commit` - Commit message + Tests Generate tests for selected code + Commit Generate commit message with commitizen convention from staged + changes + ------------------------------------------------------------------------- ============================================================================== 4. Configuration *CopilotChat-configuration* From e1dc20c2e8b85f2133e4629203444825e87f15ea Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 2 Aug 2025 16:11:47 +0200 Subject: [PATCH 70/75] docs(README): Fix duplicate headers (#1256) Closes #1255 Signed-off-by: Tomas Slusny --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 22de3b98..d70e31a4 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,9 @@ When you use `@copilot`, the LLM can call functions like `glob`, `file`, `gitdif | - | `gc` | Show info about current chat | | - | `gh` | Show help message | -## Functions +## Predefined Functions + +All predefined functions belong to the `copilot` group. | Function | Description | Example Usage | | ------------- | ------------------------------------------------ | ---------------------- | @@ -168,7 +170,7 @@ When you use `@copilot`, the LLM can call functions like `glob`, `file`, `gitdif | `register` | Provides access to specified Vim register | `#register:+` | | `url` | Fetches content from a specified URL | `#url:https://...` | -## Prompts +## Predefined Prompts | Prompt | Description | | ---------- | ---------------------------------------------------------------------- | From 560d61c4da98da53cf35bdb2359ff6f76b3c12b4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 2 Aug 2025 14:12:05 +0000 Subject: [PATCH 71/75] chore(doc): auto generate docs --- doc/CopilotChat.txt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/doc/CopilotChat.txt b/doc/CopilotChat.txt index e5596682..c23f2808 100644 --- a/doc/CopilotChat.txt +++ b/doc/CopilotChat.txt @@ -14,8 +14,8 @@ Table of Contents *CopilotChat-table-of-contents* 3. Usage |CopilotChat-usage| - Commands |CopilotChat-commands| - Chat Key Mappings |CopilotChat-chat-key-mappings| - - Functions |CopilotChat-functions| - - Prompts |CopilotChat-prompts| + - Predefined Functions |CopilotChat-predefined-functions| + - Predefined Prompts |CopilotChat-predefined-prompts| 4. Configuration |CopilotChat-configuration| - Quick Setup |CopilotChat-quick-setup| - Window & Appearance |CopilotChat-window-&-appearance| @@ -191,7 +191,9 @@ CHAT KEY MAPPINGS *CopilotChat-chat-key-mappings* - gc Show info about current chat - gh Show help message -FUNCTIONS *CopilotChat-functions* +PREDEFINED FUNCTIONS *CopilotChat-predefined-functions* + +All predefined functions belong to the `copilot` group. ------------------------------------------------------------------------------ Function Description Example Usage @@ -223,7 +225,7 @@ FUNCTIONS *CopilotChat-functions* url Fetches content from a specified URL #url:https://... ------------------------------------------------------------------------------ -PROMPTS *CopilotChat-prompts* +PREDEFINED PROMPTS *CopilotChat-predefined-prompts* ------------------------------------------------------------------------- Prompt Description From 4d2586be38a6dbb07fec5d5f3d3335e973ea0ae1 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 2 Aug 2025 18:19:02 +0200 Subject: [PATCH 72/75] fix(functions): properly allow skipping handling for tools (#1257) Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 67848dd8..163287b5 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -973,23 +973,40 @@ function M.ask(prompt, config) if not config.headless then utils.schedule_main() + -- Remove any tool calls that we did not handle + local assistant_message = M.chat:get_message('assistant') + if assistant_message and assistant_message.tool_calls then + local handled_ids = {} + for _, tool in ipairs(resolved_tools) do + handled_ids[tool.id] = true + end + + assistant_message.tool_calls = vim + .iter(assistant_message.tool_calls) + :filter(function(tool_call) + return handled_ids[tool_call.id] + end) + :totable() + end + if not utils.empty(resolved_tools) then + -- If we are handling tools, replace user message with tool results M.chat:remove_message('user') + for _, tool in ipairs(resolved_tools) do + M.chat:add_message({ + id = tool.id, + role = 'tool', + tool_call_id = tool.id, + content = '\n' .. tool.result .. '\n', + }) + end else + -- Otherwise just replace the user message with resolved prompt M.chat:add_message({ role = 'user', content = '\n' .. prompt .. '\n', }, true) end - - for _, tool in ipairs(resolved_tools) do - M.chat:add_message({ - id = tool.id, - role = 'tool', - tool_call_id = tool.id, - content = '\n' .. tool.result .. '\n', - }) - end end if utils.empty(prompt) and utils.empty(resolved_tools) then From bad83db89bb3d813be62dd1b2767406ac3c96e4c Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 2 Aug 2025 18:30:10 +0200 Subject: [PATCH 73/75] fix(chat): handle empty prompt and tools before ask (#1258) Move the check for empty prompt and resolved tools before making the ask call. This ensures that unnecessary user messages are removed and the finish callback is called early, preventing redundant processing. Also remove assistant message if all tool calls are handled. Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 163287b5..d2dbee0c 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -969,10 +969,17 @@ function M.ask(prompt, config) end prompt = vim.trim(prompt) + utils.schedule_main() - if not config.headless then - utils.schedule_main() + if utils.empty(prompt) and utils.empty(resolved_tools) then + if not config.headless then + M.chat:remove_message('user') + finish() + end + return + end + if not config.headless then -- Remove any tool calls that we did not handle local assistant_message = M.chat:get_message('assistant') if assistant_message and assistant_message.tool_calls then @@ -987,6 +994,10 @@ function M.ask(prompt, config) return handled_ids[tool_call.id] end) :totable() + + if utils.empty(assistant_message.tool_calls) then + M.chat:remove_message('assistant') + end end if not utils.empty(resolved_tools) then @@ -1009,14 +1020,6 @@ function M.ask(prompt, config) end end - if utils.empty(prompt) and utils.empty(resolved_tools) then - if not config.headless then - M.chat:remove_message('user') - finish() - end - return - end - local ask_ok, ask_response = pcall(client.ask, client, prompt, { headless = config.headless, history = M.chat.messages, From 936426a500d2f0da25f7d3f065e07450ac851c66 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 2 Aug 2025 18:51:36 +0200 Subject: [PATCH 74/75] fix(chat): handle skipped tool calls with explicit error result (#1259) Previously, unhandled tool calls were simply removed from the assistant message, which could lead to confusion or lack of feedback. Now, when a tool call is skipped, an explicit error result is added to the resolved tools, indicating that the user skipped the function call. This improves transparency and makes the handling of skipped tool calls clearer. Signed-off-by: Tomas Slusny --- lua/CopilotChat/init.lua | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index d2dbee0c..4e81c934 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -971,16 +971,7 @@ function M.ask(prompt, config) prompt = vim.trim(prompt) utils.schedule_main() - if utils.empty(prompt) and utils.empty(resolved_tools) then - if not config.headless then - M.chat:remove_message('user') - finish() - end - return - end - if not config.headless then - -- Remove any tool calls that we did not handle local assistant_message = M.chat:get_message('assistant') if assistant_message and assistant_message.tool_calls then local handled_ids = {} @@ -988,15 +979,15 @@ function M.ask(prompt, config) handled_ids[tool.id] = true end - assistant_message.tool_calls = vim - .iter(assistant_message.tool_calls) - :filter(function(tool_call) - return handled_ids[tool_call.id] - end) - :totable() - - if utils.empty(assistant_message.tool_calls) then - M.chat:remove_message('assistant') + -- If we skipped any tool calls, send that as result + for _, tool_call in ipairs(assistant_message.tool_calls) do + if not handled_ids[tool_call.id] then + table.insert(resolved_tools, { + id = tool_call.id, + result = string.format(BLOCK_OUTPUT_FORMAT, 'error', 'User skipped this function call.'), + }) + handled_ids[tool_call.id] = true + end end end @@ -1020,6 +1011,14 @@ function M.ask(prompt, config) end end + if utils.empty(prompt) and utils.empty(resolved_tools) then + if not config.headless then + M.chat:remove_message('user') + finish() + end + return + end + local ask_ok, ask_response = pcall(client.ask, client, prompt, { headless = config.headless, history = M.chat.messages, From 95d4d7ade6b0ae8acee77f291228109dc37602cc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 3 Aug 2025 06:34:55 +0200 Subject: [PATCH 75/75] chore(main): release 4.0.0 (#1191) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- CHANGELOG.md | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++ version.txt | 2 +- 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10dde051..12e31352 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,77 @@ # Changelog +## [4.0.0](https://github.com/CopilotC-Nvim/CopilotChat.nvim/compare/v3.12.2...v4.0.0) (2025-08-02) + + +### ⚠ BREAKING CHANGES + +* **mappings:** use C-Space as default completion trigger instead of Tab +* **providers:** github_models provider is now disabled by default, enable with `providers.github_models.disabled = false` +* **resources:** intelligent resource processing is now disabled by default, use config.resource_processing: true to reenable +* **context:** Multiple breaking changes due to big refactor: + - The context API has changed from callback-based input handling to schema-based definitions. + - config.contexts renamed to config.tools + - config.context removed, use config.sticky + - diagnostics moved to separate tool call, selection and buffer calls no longer include them by default + - gi renamed to gc, now also includes selection + - filenames renamed to glob + - files removed (use glob together with tool calling instead, or buffers/quickfix) + - copilot extension agents removed, tools + mcp servers can replace this feature and maintaining them was pain, they can still be implemented via custom providers anyway + - actions and integrations action removed as they were deprecated for a while + - config.questionHeader, config.answerHeader moved to config.headers.user/config.headers.assistant + +### Features + +* add Windows_NT support in Makefile and dynamic library loading ([#1190](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1190)) ([7559fd2](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/7559fd25928f8f3cf311ff25b95bdc5f9ec736d7)) +* **context:** switch from contexts to function calling ([057b8e4](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/057b8e46d955748b1426e7b174d7af3e58f5191b)), closes [#1045](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1045) [#1090](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1090) [#1096](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1096) [#526](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/526) +* display group as kind when listing resources ([#1215](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1215)) ([450fcec](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/450fcecf2f71d0469e9c98f5967252092714ed03)) +* **functions:** automatically parse schema from url templates ([#1220](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1220)) ([950fdb6](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/950fdb6ab56754929d4db91c73139b33e645deec)) +* **health:** add temp dir writable check ([#1239](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1239)) ([02cf9e5](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/02cf9e52634b3e3d45beb2c4e5bbc17da28aef64)) +* **mappings:** use C-Space as default completion trigger instead of Tab ([ea41684](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/ea4168476a0fdbd5bf40a4a769d6c1dc998929eb)) +* **prompts:** add configurable response language ([#1246](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1246)) ([ced388c](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/ced388c97b313ea235809824ed501970b155e59f)), closes [#1086](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1086) +* **providers:** add info output to panel for copilot with stats ([#1229](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1229)) ([1713ce6](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/1713ce6c8ec700a7833236a8dadfae8a0742b14d)) +* **providers:** new github models api, in-built authorization without copilot.vim dep ([#1218](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1218)) ([9c4501e](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/9c4501e7ae92020f2d9b828086016ee70e7fa52c)), closes [#1140](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1140) +* **providers:** prioritize gh clie auth if available for github models ([#1240](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1240)) ([01d38b2](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/01d38b27ea2183302c743dac09b27611d09d7591)) +* **resources:** add option to enable resource processing ([#1202](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1202)) ([6ac77aa](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/6ac77aaa68a0ce7fe3c8c41622ab1986f8f6d2c7)) +* **ui:** add window.blend option for controllin float transparency ([#1227](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1227)) ([a01bbd6](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/a01bbd6779f4bee23c29ebcfe0d2f5fa5664b5bf)), closes [#1126](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1126) +* **ui:** highlight copilotchat keywords ([#1225](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1225)) ([8071a69](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/8071a6979b5569ce03f7f4d7192814da4c2d4e0b)) +* **ui:** improve chat responsiveness by starting spinner early ([#1205](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1205)) ([9d9b280](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/9d9b2809e1240f9525752ae145799b88d22cd7af)) + + +### Bug Fixes + +* add back sticky loading on opening window ([#1210](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1210)) ([1d6911f](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/1d6911fef13952c9b56347485f090baeff77a7e4)) +* **chat:** do not allow sending empty prompt ([#1245](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1245)) ([c3d0048](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/c3d00484c42065a883db0fb859c686e277012d6c)), closes [#1189](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1189) +* **chat:** handle empty prompt and tools before ask ([#1258](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1258)) ([bad83db](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/bad83db89bb3d813be62dd1b2767406ac3c96e4c)) +* **chat:** handle skipped tool calls with explicit error result ([#1259](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1259)) ([936426a](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/936426a500d2f0da25f7d3f065e07450ac851c66)) +* **chat:** highlight keywords only in user messages ([#1236](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1236)) ([425ff0c](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/425ff0c48906a94ca522f6d2e98e4b39057e4fd4)) +* **chat:** improve how sticky prompts are stored and parsed ([#1233](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1233)) ([82be513](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/82be513c07a27f55860d55144c54040d1c93cf2a)) +* **chat:** properly replace all message data when replacing message ([#1244](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1244)) ([d1d155e](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/d1d155e50193e28a3ec00f8e21d6f11445f96ea1)) +* **chat:** properly reset modifiable after modifying it ([#1234](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1234)) ([fc93d1c](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/fc93d1c535bf9538a0a036f118b1034930ee5eb9)) +* **chat:** show messages in overlay ([#1237](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1237)) ([1a17534](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/1a17534c17e6ae9f5417df08b8c0eec434c47875)) +* check for explicit uri input properly ([#1214](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1214)) ([b738fb4](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/b738fb40de3a4bcbb835b8ff6ab2d171acc5d2dd)) +* **files:** use also plenary filetype on top of vim.filetype.match ([#1250](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1250)) ([9fd068f](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/9fd068f5d6a0ca00fc739a98f29125cb577b2dfa)), closes [#1249](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1249) +* **functions:** change neovim://buffer to just buffer:// to avoid conflicts ([#1252](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1252)) ([3509cf0](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/3509cf0971c59ba79fbcd618d82910f8567a7929)) +* **functions:** if enum returns only 1 choice auto accept it ([#1209](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1209)) ([e632470](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/e632470171cd82a95c2675360120833c159e7ae0)) +* **functions:** if schema.properties is empty, do not send schema ([#1211](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1211)) ([8a5cda1](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/8a5cda1d90c4d4756dda39cfd748e52cbcde5a99)) +* **functions:** properly allow skipping handling for tools ([#1257](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1257)) ([4d2586b](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/4d2586be38a6dbb07fec5d5f3d3335e973ea0ae1)) +* **functions:** properly escape percent signs in uri inputs ([#1212](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1212)) ([d905917](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/d905917a025e4c056db28b3082dd474475bad8cd)) +* **functions:** properly filter tool schema from functions ([#1243](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1243)) ([f7a3228](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/f7a3228f155d0533197ac79b0e08582e504d0399)) +* **functions:** properly handle multiple tool calls at once ([#1198](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1198)) ([dd06166](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/dd0616661505a3c4892ddcdb9517b720a74e59b8)) +* **functions:** properly resolve defaults for diagnostics ([#1201](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1201)) ([946069a](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/946069a03946ce35619cbacc3a6757819d096ac5)), closes [#1200](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1200) +* **functions:** properly send prompt as 3rd function resolve param ([#1221](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1221)) ([c03bd1d](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/c03bd1df78b276aa5be2f173c2a31ad273164f15)) +* **functions:** use vim.filetype.match for non bulk file reads ([#1226](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1226)) ([b124b94](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/b124b94264140a5d352512b38b7a46d85ee59b24)), closes [#1181](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1181) +* **healthcheck:** chance copilot.vim dependency to optional ([#1219](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1219)) ([d9f4e29](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/d9f4e29c3b46b827443b1832209d22d05c1a69af)) +* **prompt:** be more specific when definining what is resource ([#1238](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1238)) ([7c82936](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/7c82936f2126b106af1b1bf0f9ae4d42dd45fcad)) +* properly validate source window when retrieving cwd ([#1231](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1231)) ([f53069c](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/f53069c595a3b12bbe8b9b711917f9ef33c22a0a)), closes [#1230](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1230) +* **providers:** do not save copilot.vim token ([#1223](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1223)) ([294bcb6](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/294bcb620ff66183e142cd8a43a7c77d5bc77a16)) +* **quickfix:** use new chat messages instead of old chat sections for populating qf ([#1199](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1199)) ([e0df6d1](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/e0df6d1242af29b6262b0eb3e4248568c57c4b3e)) +* **ui:** do not allow empty separator ([#1224](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1224)) ([67ed258](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/67ed258c6ccc0a9bfbb6dfcbe3d5e19e22888e73)) +* **ui:** fix check for auto follow cursor ([#1222](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1222)) ([1f96d53](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/1f96d53c3f10f176ca25065a23e610d7b4a72b99)) +* update sticky reference for commit messages ([#1207](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1207)) ([dab5089](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/dab50896c7e1e80142dd297e6fc75590735b3e9c)) +* update to latest lua actions and update README ([#1196](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1196)) ([b4b7f9c](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/b4b7f9c2bb34d43b18dbbe0a889881630e217bc3)) +* **utils:** remove temp file after curl request is done ([#1235](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1235)) ([dec3127](https://github.com/CopilotC-Nvim/CopilotChat.nvim/commit/dec3127e4f373875d7fd50854e221ed8dc0e061f)), closes [#1194](https://github.com/CopilotC-Nvim/CopilotChat.nvim/issues/1194) + ## [3.12.2](https://github.com/CopilotC-Nvim/CopilotChat.nvim/compare/v3.12.1...v3.12.2) (2025-07-09) diff --git a/version.txt b/version.txt index 8531a3b7..fcdb2e10 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -3.12.2 +4.0.0