Skip to content

Commit c5454d5

Browse files
committed
feat: add cancel keymap for cancelling quick_chat session
1 parent 69dcd13 commit c5454d5

File tree

4 files changed

+131
-28
lines changed

4 files changed

+131
-28
lines changed

lua/opencode/config.lua

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ M.defaults = {
2222
['<leader>ot'] = { 'toggle_focus', desc = 'Toggle focus' },
2323
['<leader>oT'] = { 'timeline', desc = 'Session timeline' },
2424
['<leader>oq'] = { 'close', desc = 'Close Opencode window' },
25-
['<leader>oQ'] = { 'quick_chat', desc = 'Quick chat with current context', mode = { 'n', 'x' } },
2625
['<leader>os'] = { 'select_session', desc = 'Select session' },
2726
['<leader>oR'] = { 'rename_session', desc = 'Rename session' },
2827
['<leader>op'] = { 'configure_provider', desc = 'Configure provider' },
@@ -42,6 +41,7 @@ M.defaults = {
4241
['<leader>oPa'] = { 'permission_accept', desc = 'Accept permission' },
4342
['<leader>oPA'] = { 'permission_accept_all', desc = 'Accept all permissions' },
4443
['<leader>oPd'] = { 'permission_deny', desc = 'Deny permission' },
44+
['<leader>o/'] = { 'quick_chat', desc = 'Quick chat with current context', mode = { 'n', 'x' } },
4545
},
4646
output_window = {
4747
['<esc>'] = { 'close' },
@@ -91,6 +91,9 @@ M.defaults = {
9191
delete_entry = { '<C-d>', mode = { 'i', 'n' } },
9292
clear_all = { '<C-X>', mode = { 'i', 'n' } },
9393
},
94+
quick_chat = {
95+
cancel = { '<C-c>', mode = { 'i', 'n' } },
96+
},
9497
},
9598
ui = {
9699
position = 'right',
@@ -221,18 +224,6 @@ M.defaults = {
221224

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

224-
---Get function names from keymap config, used when normalizing legacy config
225-
---@param keymap_config table
226-
local function get_function_names(keymap_config)
227-
local names = {}
228-
for _, config in pairs(keymap_config) do
229-
if type(config) == 'table' and config[1] then
230-
table.insert(names, config[1])
231-
end
232-
end
233-
return names
234-
end
235-
236227
local function update_keymap_prefix(prefix, default_prefix)
237228
if prefix == default_prefix or not prefix then
238229
return

lua/opencode/quick_chat.lua

Lines changed: 81 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ local M = {}
2020
---@type table<string, OpencodeQuickChatRunningSession>
2121
local running_sessions = {}
2222

23-
--- Creates an ephemeral session title
23+
--- Global keymaps that are active during quick chat sessions
24+
---@type table<string, boolean>
25+
local active_global_keymaps = {}
26+
27+
--- Creates an quicklchat session title
2428
---@param buf integer Buffer handle
2529
---@return string title The session title
2630
local function create_session_title(buf)
@@ -32,6 +36,69 @@ local function create_session_title(buf)
3236
return string.format('[QuickChat] %s:%d (%s)', relative_path, line_num, timestamp)
3337
end
3438

39+
--- Removes global keymaps for quick chat
40+
local function teardown_global_keymaps()
41+
if not next(active_global_keymaps) then
42+
return
43+
end
44+
45+
for key, _ in pairs(active_global_keymaps) do
46+
pcall(vim.keymap.del, { 'n', 'i' }, key)
47+
end
48+
49+
active_global_keymaps = {}
50+
end
51+
52+
--- Cancels all running quick chat sessions
53+
local function cancel_all_quick_chat_sessions()
54+
for session_id, session_info in pairs(running_sessions) do
55+
if state.api_client then
56+
local ok, result = pcall(function()
57+
return state.api_client:abort_session(session_id):wait()
58+
end)
59+
60+
if not ok then
61+
vim.notify('Quick chat abort error: ' .. vim.inspect(result), vim.log.levels.WARN)
62+
end
63+
end
64+
65+
if session_info and session_info.spinner then
66+
session_info.spinner:stop()
67+
end
68+
69+
if config.values.debug.quick_chat and not config.values.debug.quick_chat.keep_session then
70+
state.api_client:delete_session(session_id):catch(function(err)
71+
vim.notify('Error deleting quicklchat session: ' .. vim.inspect(err), vim.log.levels.WARN)
72+
end)
73+
end
74+
75+
running_sessions[session_id] = nil
76+
end
77+
78+
-- Teardown keymaps once at the end
79+
teardown_global_keymaps()
80+
vim.notify('Quick chat cancelled by user', vim.log.levels.WARN)
81+
end
82+
83+
--- Sets up global keymaps for quick chat
84+
local function setup_global_keymaps()
85+
if next(active_global_keymaps) then
86+
return
87+
end
88+
89+
local quick_chat_keymap = config.keymap.quick_chat or {}
90+
if quick_chat_keymap.cancel then
91+
vim.keymap.set(quick_chat_keymap.cancel.mode or { 'n', 'i' }, quick_chat_keymap.cancel[1], function()
92+
cancel_all_quick_chat_sessions()
93+
end, {
94+
desc = quick_chat_keymap.cancel.desc or 'Cancel quick chat session',
95+
silent = true,
96+
})
97+
98+
active_global_keymaps[quick_chat_keymap.cancel[1]] = true
99+
end
100+
end
101+
35102
--- Helper to clean up session info and spinner
36103
---@param session_info table Session tracking info
37104
---@param session_id string Session ID
@@ -43,11 +110,17 @@ local function cleanup_session(session_info, session_id, message)
43110

44111
if config.debug.quick_chat and not config.debug.quick_chat.keep_session then
45112
state.api_client:delete_session(session_id):catch(function(err)
46-
vim.notify('Error deleting ephemeral session: ' .. vim.inspect(err), vim.log.levels.WARN)
113+
vim.notify('Error deleting quicklchat session: ' .. vim.inspect(err), vim.log.levels.WARN)
47114
end)
48115
end
49116

50117
running_sessions[session_id] = nil
118+
119+
-- Check if there are no more running sessions and teardown global keymaps
120+
if not next(running_sessions) then
121+
teardown_global_keymaps()
122+
end
123+
51124
if message then
52125
vim.notify(message, vim.log.levels.WARN)
53126
end
@@ -67,7 +140,7 @@ local function extract_response_text(message)
67140
return vim.trim(response_text)
68141
end
69142

70-
--- Processes response from ephemeral session
143+
--- Processes response from quicklchat session
71144
---@param session_info table Session tracking info
72145
---@param messages OpencodeMessage[] Session messages
73146
---@return boolean success Whether the response was processed successfully
@@ -308,7 +381,7 @@ M.quick_chat = Promise.async(function(message, options, range)
308381
local quick_chat_session = core.create_new_session(title):await()
309382
if not quick_chat_session then
310383
spinner:stop()
311-
return Promise.new():reject('Failed to create ephemeral session')
384+
return Promise.new():reject('Failed to create quicklchat session')
312385
end
313386

314387
if config.debug.quick_chat and config.debug.quick_chat.set_active_session then
@@ -323,6 +396,9 @@ M.quick_chat = Promise.async(function(message, options, range)
323396
timestamp = vim.uv.now(),
324397
}
325398

399+
-- Set up global keymaps for quick chat
400+
setup_global_keymaps()
401+
326402
local context_config = vim.tbl_deep_extend('force', create_context_config(range ~= nil), options.context_config or {})
327403
local context_instance = context.new_instance(context_config)
328404
local params = create_message(message, buf, range, context_instance, options):await()
@@ -371,6 +447,7 @@ function M.setup()
371447
end
372448
end
373449
running_sessions = {}
450+
teardown_global_keymaps()
374451
end,
375452
})
376453
end

lua/opencode/quick_chat/spinner.lua

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,15 @@
11
local config = require('opencode.config')
22
local Timer = require('opencode.ui.timer')
33

4-
---@class Timer
5-
---@field start function
6-
---@field stop function
7-
---@field is_running function
8-
94
---@class CursorSpinner
105
---@field buf integer
116
---@field row integer
127
---@field col integer
138
---@field ns_id integer
149
---@field extmark_id integer|nil
10+
---@field highlight_extmark_id integer|nil
1511
---@field current_frame integer
16-
---@field timer Timer|nil
12+
---@field timer table|nil
1713
---@field active boolean
1814
---@field frames string[]
1915
---@field float_win integer|nil
@@ -28,6 +24,7 @@ function CursorSpinner.new(buf, row, col)
2824
self.col = col
2925
self.ns_id = vim.api.nvim_create_namespace('opencode_quick_chat_spinner')
3026
self.extmark_id = nil
27+
self.highlight_extmark_id = nil
3128
self.current_frame = 1
3229
self.timer = nil
3330
self.active = true
@@ -58,10 +55,21 @@ function CursorSpinner:create_float()
5855
vim.api.nvim_set_option_value('wrap', false, { win = self.float_win })
5956
end
6057

58+
function CursorSpinner:get_cancel_key()
59+
local quick_chat_keymap = config.values.keymap.quick_chat or {}
60+
return quick_chat_keymap.cancel and quick_chat_keymap.cancel[1] or ''
61+
end
62+
6163
function CursorSpinner:get_float_config()
64+
local cancel_key = self:get_cancel_key()
65+
local legend = ' ' .. cancel_key .. ' to cancel'
66+
local spinner_width = 3
67+
local legend_width = #legend
68+
local total_width = spinner_width + legend_width + 1 -- +1 for spacing
69+
6270
return {
6371
relative = 'cursor',
64-
width = 3,
72+
width = total_width,
6573
height = 1,
6674
row = 0,
6775
col = 2, -- 2 columns to the right of cursor
@@ -77,8 +85,25 @@ function CursorSpinner:render()
7785
return
7886
end
7987

80-
local frame = ' ' .. self.frames[self.current_frame] .. ' '
81-
vim.api.nvim_buf_set_lines(self.float_buf, 0, -1, false, { frame })
88+
local spinner_part = ' ' .. self.frames[self.current_frame] .. ' '
89+
90+
local cancel_key = self:get_cancel_key() or ''
91+
local legend_part = cancel_key and ' ' .. cancel_key .. ' to cancel' or ''
92+
93+
local content = spinner_part .. legend_part
94+
95+
vim.api.nvim_buf_set_lines(self.float_buf, 0, -1, false, { content })
96+
97+
if self.highlight_extmark_id and vim.api.nvim_buf_is_valid(self.float_buf) then
98+
pcall(vim.api.nvim_buf_del_extmark, self.float_buf, self.ns_id, self.highlight_extmark_id)
99+
end
100+
101+
if vim.api.nvim_buf_is_valid(self.float_buf) then
102+
self.highlight_extmark_id = vim.api.nvim_buf_set_extmark(self.float_buf, self.ns_id, 0, #spinner_part + 1, {
103+
end_col = #spinner_part + 1 + #cancel_key,
104+
hl_group = 'WarningMsg',
105+
})
106+
end
82107
end
83108

84109
function CursorSpinner:next_frame()
@@ -98,7 +123,9 @@ function CursorSpinner:start_timer()
98123
end,
99124
repeat_timer = true,
100125
})
101-
self.timer:start()
126+
if self.timer then
127+
self.timer:start()
128+
end
102129
end
103130

104131
function CursorSpinner:stop()
@@ -108,7 +135,7 @@ function CursorSpinner:stop()
108135

109136
self.active = false
110137

111-
if self.timer then
138+
if self.timer and self.timer.stop then
112139
self.timer:stop()
113140
self.timer = nil
114141
end
@@ -124,6 +151,10 @@ function CursorSpinner:stop()
124151
if self.extmark_id and vim.api.nvim_buf_is_valid(self.buf) then
125152
pcall(vim.api.nvim_buf_del_extmark, self.buf, self.ns_id, self.extmark_id)
126153
end
154+
155+
if self.highlight_extmark_id and vim.api.nvim_buf_is_valid(self.float_buf or self.buf) then
156+
pcall(vim.api.nvim_buf_del_extmark, self.float_buf or self.buf, self.ns_id, self.highlight_extmark_id)
157+
end
127158
end
128159

129160
return CursorSpinner

lua/opencode/types.lua

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
---@field session_picker OpencodeSessionPickerKeymap
8787
---@field timeline_picker OpencodeTimelinePickerKeymap
8888
---@field history_picker OpencodeHistoryPickerKeymap
89+
---@field quick_chat OpencodeQuickChatKeymap
8990

9091
---@class OpencodeSessionPickerKeymap
9192
---@field delete_session OpencodeKeymapEntry
@@ -100,6 +101,9 @@
100101
---@field delete_entry OpencodeKeymapEntry
101102
---@field clear_all OpencodeKeymapEntry
102103

104+
---@class OpencodeQuickChatKeymap
105+
---@field cancel OpencodeKeymapEntry
106+
103107
---@class OpencodeCompletionFileSourcesConfig
104108
---@field enabled boolean
105109
---@field preferred_cli_tool 'server'|'fd'|'fdfind'|'rg'|'git'

0 commit comments

Comments
 (0)