Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
4e3bda1
WIP: very early ground break
sudo-tee Dec 11, 2025
bac518f
feat: new buffer context types
sudo-tee Dec 15, 2025
d9b8076
feat: quick chat ask for input if no message
sudo-tee Dec 15, 2025
b70e379
fix: git_diff not returning value
sudo-tee Dec 15, 2025
1ec3cd5
fix: range transformation for selection
sudo-tee Dec 15, 2025
32245ba
fix: promise with nil arguments
sudo-tee Dec 16, 2025
752f595
refactor(context): modularize context gathering and add search/replac…
sudo-tee Dec 16, 2025
a8179c0
refactor(quick_chat): extract search/replace and spinner into separat…
sudo-tee Dec 16, 2025
8e6bac6
feat(search_replace): support empty SEARCH block as insert operation
sudo-tee Dec 16, 2025
dc5e376
refactor(quick_chat): improve prompt structure and formatting
sudo-tee Dec 16, 2025
d5ec089
fix(quick_chat): remove duplicated instructions
sudo-tee Dec 16, 2025
d6ffb06
feat: optimize SEARCH/REPLACE quick chat for concise context and outp…
sudo-tee Dec 17, 2025
f7a70fc
feat(quick_chat): cheat for position of window for snacks
sudo-tee Dec 17, 2025
cdd55e4
feat(quick_chat): update default conf
sudo-tee Dec 17, 2025
bd19f34
feat(ui): use floating window for CursorSpinner
sudo-tee Dec 17, 2025
e76825f
fix(quick_chat): session deletion
sudo-tee Dec 17, 2025
f742006
feat: add cancel keymap for cancelling quick_chat session
sudo-tee Dec 17, 2025
6a08f8c
feat: update prompt and configs
sudo-tee Dec 17, 2025
ca5065d
feat: update docs toi add images of quick chat
sudo-tee Dec 17, 2025
ed7ff03
chore:remove test file
sudo-tee Dec 18, 2025
6478340
chore: fix typo
sudo-tee Dec 18, 2025
7e8eade
fix: typo in readme
sudo-tee Dec 18, 2025
af4e12f
fix(quick_chat): rename default_prompt to instructions
sudo-tee Dec 18, 2025
cbf6c07
fix: typo
sudo-tee Dec 18, 2025
a421014
feat: refactor context system for modularity and quick chat improvements
sudo-tee Dec 19, 2025
22de13c
chore: remove rogue import
sudo-tee Dec 19, 2025
eb54371
chore: remove duplicated line
sudo-tee Dec 19, 2025
ce0a37c
fix(quick_chat_context): don't re-indent selection
sudo-tee Dec 19, 2025
56bfccc
fix(context): don't trim the current line in context
sudo-tee Dec 19, 2025
867e430
fix: typo
sudo-tee Dec 19, 2025
990776c
fix(quick_chat): improve patch block parsing and application logic
sudo-tee Dec 19, 2025
9e31367
feat: narrower cursor context works better
sudo-tee Dec 19, 2025
6609853
fix(api): show selection/line context in Quick Chat input prompt
sudo-tee Dec 22, 2025
ed2e6ae
feat: replace search/replace with raw code generation in quick chat
sudo-tee Dec 22, 2025
7f92287
fix(quick_chat): improve code extraction and raw replace instructions
sudo-tee Dec 22, 2025
1fdfb40
fix: wrong property name in README
sudo-tee Dec 22, 2025
893efbd
chore: remove .values for accessing config
sudo-tee Dec 22, 2025
1477681
fix: message condition
sudo-tee Dec 22, 2025
f612caa
fix: delta context not using current context from state
sudo-tee Dec 22, 2025
cf88ab6
feat(context): add sent_at/sent_at_mtime file context state tracking
sudo-tee Dec 23, 2025
b5d86fd
fix: send message not working in last merge
sudo-tee Dec 23, 2025
08bf3f9
refactor(context): remove unused legacy context management and test f…
sudo-tee Dec 24, 2025
27db2e4
feat: add support for multi-range diagnostics with selection-aware fi…
sudo-tee Dec 24, 2025
ff6c9d1
feat: better errors in promise finally
sudo-tee Dec 24, 2025
c8863b0
perf: debounce context loading to improve performance
sudo-tee Dec 24, 2025
a2305a2
fix: return type
sudo-tee Jan 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
221 changes: 145 additions & 76 deletions README.md

Large diffs are not rendered by default.

63 changes: 58 additions & 5 deletions lua/opencode/api.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ local util = require('opencode.util')
local session = require('opencode.session')
local config_file = require('opencode.config_file')
local state = require('opencode.state')
local quick_chat = require('opencode.quick_chat')

local input_window = require('opencode.ui.input_window')
local ui = require('opencode.ui.ui')
Expand Down Expand Up @@ -59,6 +60,8 @@ M.toggle = Promise.async(function(new_session)
end
end)

---@param new_session boolean?
---@return nil
function M.toggle_focus(new_session)
if not ui.is_opencode_focused() then
local focus = state.last_focused_opencode_window or 'input' ---@cast focus 'input' | 'output'
Expand Down Expand Up @@ -107,6 +110,39 @@ function M.select_history()
require('opencode.ui.history_picker').pick()
end

function M.quick_chat(message, range)
if not range then
if vim.fn.mode():match('[vV\022]') then
local visual_range = util.get_visual_range()
if visual_range then
range = {
start = visual_range.start_line,
stop = visual_range.end_line,
}
end
end
end

if type(message) == 'table' then
message = table.concat(message, ' ')
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The quick_chat function in api.lua on lines 126-128 concatenates an array of message parts with table.concat(message, ' '). However, this assumes that message is always a table when it's passed as an array. If a user accidentally passes a string that looks like an array or has unexpected structure, this could fail. Consider adding type validation or documenting that message must be a table when passed as multiple arguments.

Suggested change
message = table.concat(message, ' ')
local parts = {}
for i, v in ipairs(message) do
parts[i] = tostring(v)
end
message = table.concat(parts, ' ')

Copilot uses AI. Check for mistakes.
end

if not message or #message == 0 then
local scope = range and ('[selection: ' .. range.start .. '-' .. range.stop .. ']')
or '[line: ' .. tostring(vim.api.nvim_win_get_cursor(0)[1]) .. ']'
vim.ui.input({ prompt = 'Quick Chat Message: ' .. scope, win = { relative = 'cursor' } }, function(input)
if input and input ~= '' then
local prompt, ctx = util.parse_quick_context_args(input)
quick_chat.quick_chat(prompt, { context_config = ctx }, range)
end
end)
return
end

local prompt, ctx = util.parse_quick_context_args(message)
quick_chat.quick_chat(prompt, { context_config = ctx }, range)
end

function M.toggle_pane()
return core.open({ new_session = false, focus = 'output' }):and_then(function()
ui.toggle_pane()
Expand Down Expand Up @@ -970,6 +1006,14 @@ M.commands = {
fn = M.toggle_zoom,
},

quick_chat = {
desc = 'Quick chat with current buffer or visual selection',
fn = M.quick_chat,
range = true, -- Enable range support for visual selections
nargs = '+', -- Allow multiple arguments
complete = false, -- No completion for custom messages
},

swap = {
desc = 'Swap pane position left/right',
fn = M.swap_position,
Expand Down Expand Up @@ -1011,7 +1055,7 @@ M.commands = {
local title = table.concat(vim.list_slice(args, 2), ' ')
M.rename_session(state.active_session, title)
else
local valid_subcmds = table.concat(M.commands.session.completions, ', ')
local valid_subcmds = table.concat(M.commands.session.completions or {}, ', ')
vim.notify('Invalid session subcommand. Use: ' .. valid_subcmds, vim.log.levels.ERROR)
end
end,
Expand Down Expand Up @@ -1041,7 +1085,7 @@ M.commands = {
elseif subcmd == 'close' then
M.diff_close()
else
local valid_subcmds = table.concat(M.commands.diff.completions, ', ')
local valid_subcmds = table.concat(M.commands.diff.completions or {}, ', ')
vim.notify('Invalid diff subcommand. Use: ' .. valid_subcmds, vim.log.levels.ERROR)
end
end,
Expand Down Expand Up @@ -1120,7 +1164,7 @@ M.commands = {
elseif subcmd == 'select' then
M.select_agent()
else
local valid_subcmds = table.concat(M.commands.agent.completions, ', ')
local valid_subcmds = table.concat(M.commands.agent.completions or {}, ', ')
vim.notify('Invalid agent subcommand. Use: ' .. valid_subcmds, vim.log.levels.ERROR)
end
end,
Expand Down Expand Up @@ -1208,7 +1252,7 @@ M.commands = {
elseif subcmd == 'deny' then
M.permission_deny()
else
local valid_subcmds = table.concat(M.commands.permission.completions, ', ')
local valid_subcmds = table.concat(M.commands.permission.completions or {}, ', ')
vim.notify('Invalid permission subcommand. Use: ' .. valid_subcmds, vim.log.levels.ERROR)
end
end,
Expand Down Expand Up @@ -1309,6 +1353,14 @@ M.legacy_command_map = {

function M.route_command(opts)
local args = vim.split(opts.args or '', '%s+', { trimempty = true })
local range = nil

if opts.range and opts.range > 0 then
range = {
start = opts.line1,
stop = opts.line2,
}
end

if #args == 0 then
M.toggle()
Expand All @@ -1319,7 +1371,7 @@ function M.route_command(opts)
local subcmd_def = M.commands[subcommand]

if subcmd_def and subcmd_def.fn then
subcmd_def.fn(vim.list_slice(args, 2))
subcmd_def.fn(vim.list_slice(args, 2), range)
else
vim.notify('Unknown subcommand: ' .. subcommand, vim.log.levels.ERROR)
end
Expand Down Expand Up @@ -1413,6 +1465,7 @@ function M.setup()
vim.api.nvim_create_user_command('Opencode', M.route_command, {
desc = 'Opencode.nvim main command with nested subcommands',
nargs = '*',
range = true, -- Enable range support
complete = M.complete_command,
})

Expand Down
33 changes: 21 additions & 12 deletions lua/opencode/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ M.defaults = {
['<leader>oPd'] = { 'permission_deny', desc = 'Deny permission' },
['<leader>otr'] = { 'toggle_reasoning_output', desc = 'Toggle reasoning output' },
['<leader>ott'] = { 'toggle_tool_output', desc = 'Toggle tool output' },
['<leader>o/'] = { 'quick_chat', desc = 'Quick chat with current context', mode = { 'n', 'x' } },
},
output_window = {
['<esc>'] = { 'close' },
Expand Down Expand Up @@ -94,6 +95,9 @@ M.defaults = {
delete_entry = { '<C-d>', mode = { 'i', 'n' } },
clear_all = { '<C-X>', mode = { 'i', 'n' } },
},
quick_chat = {
cancel = { '<C-c>', mode = { 'i', 'n' } },
},
},
ui = {
position = 'right',
Expand Down Expand Up @@ -170,12 +174,14 @@ M.defaults = {
enabled = true,
cursor_data = {
enabled = false,
context_lines = 5, -- Number of lines before and after cursor to include in context
},
diagnostics = {
enabled = true,
info = false,
warning = true,
error = true,
only_closest = false, -- If true, only diagnostics for cursor/selection
},
current_file = {
enabled = true,
Expand All @@ -191,11 +197,21 @@ M.defaults = {
agents = {
enabled = true,
},
buffer = {
enabled = false, -- Disable entire buffer context by default, only used in quick chat
},
git_diff = {
enabled = false,
},
},
debug = {
enabled = false,
capture_streamed_events = false,
show_ids = true,
quick_chat = {
keep_session = false,
set_active_session = false,
},
},
prompt_guard = nil,
hooks = {
Expand All @@ -204,22 +220,15 @@ M.defaults = {
on_done_thinking = nil,
on_permission_requested = nil,
},
quick_chat = {
default_model = nil,
default_agent = nil,
instructions = nil, -- Use instructions prompt by default
},
}

M.values = vim.deepcopy(M.defaults)

---Get function names from keymap config, used when normalizing legacy config
---@param keymap_config table
local function get_function_names(keymap_config)
local names = {}
for _, config in pairs(keymap_config) do
if type(config) == 'table' and config[1] then
table.insert(names, config[1])
end
end
return names
end

local function update_keymap_prefix(prefix, default_prefix)
if prefix == default_prefix or not prefix then
return
Expand Down
Loading