Skip to content

Commit f2d3d9e

Browse files
authored
feat: experimental inline buffer chat (#142)
This PR is the begining of a very experimental inline buffer chats aimed a quick edits. You can start a quick chat with the default keymap <leader>o/ and enter a small edit to make. This is still experimental and would like to gather feedbacks
1 parent f128f20 commit f2d3d9e

26 files changed

Lines changed: 2759 additions & 552 deletions

README.md

Lines changed: 145 additions & 76 deletions
Large diffs are not rendered by default.

lua/opencode/api.lua

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ local util = require('opencode.util')
33
local session = require('opencode.session')
44
local config_file = require('opencode.config_file')
55
local state = require('opencode.state')
6+
local quick_chat = require('opencode.quick_chat')
67

78
local input_window = require('opencode.ui.input_window')
89
local ui = require('opencode.ui.ui')
@@ -59,6 +60,8 @@ M.toggle = Promise.async(function(new_session)
5960
end
6061
end)
6162

63+
---@param new_session boolean?
64+
---@return nil
6265
function M.toggle_focus(new_session)
6366
if not ui.is_opencode_focused() then
6467
local focus = state.last_focused_opencode_window or 'input' ---@cast focus 'input' | 'output'
@@ -107,6 +110,39 @@ function M.select_history()
107110
require('opencode.ui.history_picker').pick()
108111
end
109112

113+
function M.quick_chat(message, range)
114+
if not range then
115+
if vim.fn.mode():match('[vV\022]') then
116+
local visual_range = util.get_visual_range()
117+
if visual_range then
118+
range = {
119+
start = visual_range.start_line,
120+
stop = visual_range.end_line,
121+
}
122+
end
123+
end
124+
end
125+
126+
if type(message) == 'table' then
127+
message = table.concat(message, ' ')
128+
end
129+
130+
if not message or #message == 0 then
131+
local scope = range and ('[selection: ' .. range.start .. '-' .. range.stop .. ']')
132+
or '[line: ' .. tostring(vim.api.nvim_win_get_cursor(0)[1]) .. ']'
133+
vim.ui.input({ prompt = 'Quick Chat Message: ' .. scope, win = { relative = 'cursor' } }, function(input)
134+
if input and input ~= '' then
135+
local prompt, ctx = util.parse_quick_context_args(input)
136+
quick_chat.quick_chat(prompt, { context_config = ctx }, range)
137+
end
138+
end)
139+
return
140+
end
141+
142+
local prompt, ctx = util.parse_quick_context_args(message)
143+
quick_chat.quick_chat(prompt, { context_config = ctx }, range)
144+
end
145+
110146
function M.toggle_pane()
111147
return core.open({ new_session = false, focus = 'output' }):and_then(function()
112148
ui.toggle_pane()
@@ -970,6 +1006,14 @@ M.commands = {
9701006
fn = M.toggle_zoom,
9711007
},
9721008

1009+
quick_chat = {
1010+
desc = 'Quick chat with current buffer or visual selection',
1011+
fn = M.quick_chat,
1012+
range = true, -- Enable range support for visual selections
1013+
nargs = '+', -- Allow multiple arguments
1014+
complete = false, -- No completion for custom messages
1015+
},
1016+
9731017
swap = {
9741018
desc = 'Swap pane position left/right',
9751019
fn = M.swap_position,
@@ -1011,7 +1055,7 @@ M.commands = {
10111055
local title = table.concat(vim.list_slice(args, 2), ' ')
10121056
M.rename_session(state.active_session, title)
10131057
else
1014-
local valid_subcmds = table.concat(M.commands.session.completions, ', ')
1058+
local valid_subcmds = table.concat(M.commands.session.completions or {}, ', ')
10151059
vim.notify('Invalid session subcommand. Use: ' .. valid_subcmds, vim.log.levels.ERROR)
10161060
end
10171061
end,
@@ -1041,7 +1085,7 @@ M.commands = {
10411085
elseif subcmd == 'close' then
10421086
M.diff_close()
10431087
else
1044-
local valid_subcmds = table.concat(M.commands.diff.completions, ', ')
1088+
local valid_subcmds = table.concat(M.commands.diff.completions or {}, ', ')
10451089
vim.notify('Invalid diff subcommand. Use: ' .. valid_subcmds, vim.log.levels.ERROR)
10461090
end
10471091
end,
@@ -1120,7 +1164,7 @@ M.commands = {
11201164
elseif subcmd == 'select' then
11211165
M.select_agent()
11221166
else
1123-
local valid_subcmds = table.concat(M.commands.agent.completions, ', ')
1167+
local valid_subcmds = table.concat(M.commands.agent.completions or {}, ', ')
11241168
vim.notify('Invalid agent subcommand. Use: ' .. valid_subcmds, vim.log.levels.ERROR)
11251169
end
11261170
end,
@@ -1208,7 +1252,7 @@ M.commands = {
12081252
elseif subcmd == 'deny' then
12091253
M.permission_deny()
12101254
else
1211-
local valid_subcmds = table.concat(M.commands.permission.completions, ', ')
1255+
local valid_subcmds = table.concat(M.commands.permission.completions or {}, ', ')
12121256
vim.notify('Invalid permission subcommand. Use: ' .. valid_subcmds, vim.log.levels.ERROR)
12131257
end
12141258
end,
@@ -1309,6 +1353,14 @@ M.legacy_command_map = {
13091353

13101354
function M.route_command(opts)
13111355
local args = vim.split(opts.args or '', '%s+', { trimempty = true })
1356+
local range = nil
1357+
1358+
if opts.range and opts.range > 0 then
1359+
range = {
1360+
start = opts.line1,
1361+
stop = opts.line2,
1362+
}
1363+
end
13121364

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

13211373
if subcmd_def and subcmd_def.fn then
1322-
subcmd_def.fn(vim.list_slice(args, 2))
1374+
subcmd_def.fn(vim.list_slice(args, 2), range)
13231375
else
13241376
vim.notify('Unknown subcommand: ' .. subcommand, vim.log.levels.ERROR)
13251377
end
@@ -1413,6 +1465,7 @@ function M.setup()
14131465
vim.api.nvim_create_user_command('Opencode', M.route_command, {
14141466
desc = 'Opencode.nvim main command with nested subcommands',
14151467
nargs = '*',
1468+
range = true, -- Enable range support
14161469
complete = M.complete_command,
14171470
})
14181471

lua/opencode/config.lua

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ M.defaults = {
4343
['<leader>oPd'] = { 'permission_deny', desc = 'Deny permission' },
4444
['<leader>otr'] = { 'toggle_reasoning_output', desc = 'Toggle reasoning output' },
4545
['<leader>ott'] = { 'toggle_tool_output', desc = 'Toggle tool output' },
46+
['<leader>o/'] = { 'quick_chat', desc = 'Quick chat with current context', mode = { 'n', 'x' } },
4647
},
4748
output_window = {
4849
['<esc>'] = { 'close' },
@@ -94,6 +95,9 @@ M.defaults = {
9495
delete_entry = { '<C-d>', mode = { 'i', 'n' } },
9596
clear_all = { '<C-X>', mode = { 'i', 'n' } },
9697
},
98+
quick_chat = {
99+
cancel = { '<C-c>', mode = { 'i', 'n' } },
100+
},
97101
},
98102
ui = {
99103
position = 'right',
@@ -170,12 +174,14 @@ M.defaults = {
170174
enabled = true,
171175
cursor_data = {
172176
enabled = false,
177+
context_lines = 5, -- Number of lines before and after cursor to include in context
173178
},
174179
diagnostics = {
175180
enabled = true,
176181
info = false,
177182
warning = true,
178183
error = true,
184+
only_closest = false, -- If true, only diagnostics for cursor/selection
179185
},
180186
current_file = {
181187
enabled = true,
@@ -191,11 +197,21 @@ M.defaults = {
191197
agents = {
192198
enabled = true,
193199
},
200+
buffer = {
201+
enabled = false, -- Disable entire buffer context by default, only used in quick chat
202+
},
203+
git_diff = {
204+
enabled = false,
205+
},
194206
},
195207
debug = {
196208
enabled = false,
197209
capture_streamed_events = false,
198210
show_ids = true,
211+
quick_chat = {
212+
keep_session = false,
213+
set_active_session = false,
214+
},
199215
},
200216
prompt_guard = nil,
201217
hooks = {
@@ -204,22 +220,15 @@ M.defaults = {
204220
on_done_thinking = nil,
205221
on_permission_requested = nil,
206222
},
223+
quick_chat = {
224+
default_model = nil,
225+
default_agent = nil,
226+
instructions = nil, -- Use instructions prompt by default
227+
},
207228
}
208229

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

211-
---Get function names from keymap config, used when normalizing legacy config
212-
---@param keymap_config table
213-
local function get_function_names(keymap_config)
214-
local names = {}
215-
for _, config in pairs(keymap_config) do
216-
if type(config) == 'table' and config[1] then
217-
table.insert(names, config[1])
218-
end
219-
end
220-
return names
221-
end
222-
223232
local function update_keymap_prefix(prefix, default_prefix)
224233
if prefix == default_prefix or not prefix then
225234
return

0 commit comments

Comments
 (0)