Skip to content

Commit e708ba3

Browse files
committed
fix(context): clear prompt attachments and selections reliably
1 parent bafc8e7 commit e708ba3

4 files changed

Lines changed: 200 additions & 6 deletions

File tree

lua/opencode/context/chat_context.lua

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,23 @@ M.context = {
1414
linter_errors = nil,
1515
}
1616

17+
local cleared_selections = {}
18+
local cleared_selections_context = nil
19+
20+
---@param left OpencodeContextSelection|nil
21+
---@param right OpencodeContextSelection|nil
22+
---@return boolean
23+
local function is_same_selection(left, right)
24+
return (
25+
left
26+
and right
27+
and left.file
28+
and right.file
29+
and left.file.path == right.file.path
30+
and left.lines == right.lines
31+
) == true
32+
end
33+
1734
---@param path string
1835
---@param prompt? string
1936
---@return OpencodeMessagePart
@@ -162,6 +179,9 @@ end
162179
-- Global context management functions
163180

164181
function M.add_selection(selection)
182+
cleared_selections = {}
183+
cleared_selections_context = nil
184+
165185
-- Ensure selections is always a table
166186
if not M.context.selections then
167187
M.context.selections = {}
@@ -266,7 +286,22 @@ function M.clear_subagents()
266286
state.context.set_context_updated_at(vim.uv.now())
267287
end
268288

269-
function M.unload_attachments()
289+
---@param selections? OpencodeContextSelection[]
290+
function M.unload_attachments(selections)
291+
cleared_selections = vim.deepcopy(M.context.selections or {})
292+
for _, selection in ipairs(selections or {}) do
293+
local already_cleared = false
294+
for _, cleared_selection in ipairs(cleared_selections) do
295+
if is_same_selection(selection, cleared_selection) then
296+
already_cleared = true
297+
break
298+
end
299+
end
300+
if not already_cleared then
301+
table.insert(cleared_selections, vim.deepcopy(selection))
302+
end
303+
end
304+
cleared_selections_context = M.context
270305
M.context.mentioned_files = {}
271306
M.context.selections = {}
272307
state.context.set_context_updated_at(vim.uv.now())
@@ -367,6 +402,16 @@ end
367402
-- Load function that populates the global context state
368403
-- This is the core loading logic that was originally in the main context module
369404
function M.load()
405+
local selections_cleared_by_send = {}
406+
if cleared_selections_context == M.context then
407+
selections_cleared_by_send = cleared_selections
408+
else
409+
cleared_selections = {}
410+
cleared_selections_context = nil
411+
end
412+
cleared_selections = {}
413+
cleared_selections_context = nil
414+
370415
if not state.active_session and not state.is_opening then
371416
return
372417
end
@@ -412,7 +457,16 @@ function M.load()
412457
local selection_file = base_context.get_current_file_for_selection(buf)
413458
if selection_file then
414459
local selection = base_context.new_selection(selection_file, current_selection.text, current_selection.lines)
415-
M.add_selection(selection)
460+
local was_cleared = false
461+
for _, cleared_selection in ipairs(selections_cleared_by_send) do
462+
if is_same_selection(selection, cleared_selection) then
463+
was_cleared = true
464+
break
465+
end
466+
end
467+
if not was_cleared then
468+
M.add_selection(selection)
469+
end
416470
end
417471
end
418472
end

lua/opencode/services/messaging.lua

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ M.send_message = Promise.async(function(prompt, opts)
6565
params.system = opts.system or config.default_system_prompt or nil
6666

6767
local session_id = state.active_session.id
68+
local sent_context = vim.deepcopy(context.get_context())
69+
context.unload_attachments()
6870

6971
local function update_sent_message_count(num)
7072
local sent_message_count = vim.deepcopy(state.user_message_count)
@@ -86,7 +88,7 @@ M.send_message = Promise.async(function(prompt, opts)
8688
return
8789
end
8890

89-
M.after_run(prompt)
91+
M.after_run(prompt, sent_context)
9092
end)
9193
:catch(function(err)
9294
log.notify('Error sending message to session: ' .. vim.inspect(err), vim.log.levels.ERROR)
@@ -97,9 +99,13 @@ M.send_message = Promise.async(function(prompt, opts)
9799
end)
98100

99101
---@param prompt string
100-
function M.after_run(prompt)
101-
context.unload_attachments()
102-
state.session.set_last_sent_context(vim.deepcopy(context.get_context()))
102+
---@param sent_context? OpencodeContext
103+
function M.after_run(prompt, sent_context)
104+
local context_sent = vim.deepcopy(sent_context or context.get_context())
105+
if not sent_context then
106+
context.unload_attachments()
107+
end
108+
state.session.set_last_sent_context(context_sent)
103109
context.delta_context()
104110
require('opencode.history').write(prompt)
105111
vim.g.opencode_abort_count = 0

tests/unit/context_spec.lua

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -899,6 +899,58 @@ describe('ChatContext.load() preserves selections on file switch', function()
899899
ChatContext.get_diagnostics = original_get_diagnostics
900900
end)
901901

902+
it('should not restore a selection cleared after sending', function()
903+
local selection = {
904+
file = { path = '/tmp/bar.lua', name = 'bar.lua', extension = 'lua' },
905+
content = 'sent selection',
906+
lines = '5, 7',
907+
}
908+
ChatContext.add_selection(selection)
909+
910+
local original_get_current_buf = BaseContext.get_current_buf
911+
local original_get_current_file = BaseContext.get_current_file
912+
local original_get_current_file_for_selection = BaseContext.get_current_file_for_selection
913+
local original_get_current_cursor_data = BaseContext.get_current_cursor_data
914+
local original_is_context_enabled = BaseContext.is_context_enabled
915+
local original_get_current_selection = BaseContext.get_current_selection
916+
local original_get_diagnostics = ChatContext.get_diagnostics
917+
918+
BaseContext.get_current_buf = function()
919+
return 2, 2
920+
end
921+
BaseContext.get_current_file = function()
922+
return selection.file
923+
end
924+
BaseContext.get_current_file_for_selection = function()
925+
return selection.file
926+
end
927+
BaseContext.get_current_cursor_data = function()
928+
return nil
929+
end
930+
BaseContext.is_context_enabled = function(context_type)
931+
return context_type == 'selection'
932+
end
933+
BaseContext.get_current_selection = function()
934+
return { text = selection.content, lines = selection.lines }
935+
end
936+
ChatContext.get_diagnostics = function()
937+
return {}
938+
end
939+
940+
ChatContext.unload_attachments()
941+
ChatContext.load()
942+
943+
assert.same({}, ChatContext.context.selections)
944+
945+
BaseContext.get_current_buf = original_get_current_buf
946+
BaseContext.get_current_file = original_get_current_file
947+
BaseContext.get_current_file_for_selection = original_get_current_file_for_selection
948+
BaseContext.get_current_cursor_data = original_get_current_cursor_data
949+
BaseContext.is_context_enabled = original_is_context_enabled
950+
BaseContext.get_current_selection = original_get_current_selection
951+
ChatContext.get_diagnostics = original_get_diagnostics
952+
end)
953+
902954
it('should clear stale current_file when current_file context is disabled', function()
903955
ChatContext.context.current_file = {
904956
path = '/tmp/stale.lua',

tests/unit/services_messaging_spec.lua

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ loaded.services_messaging_spec = true
88
local messaging = require('opencode.services.messaging')
99
local session_runtime = require('opencode.services.session_runtime')
1010
local config_file = require('opencode.config_file')
11+
local context = require('opencode.context')
1112
local state = require('opencode.state')
1213
local Promise = require('opencode.promise')
1314
local stub = require('luassert.stub')
@@ -280,6 +281,16 @@ describe('opencode.services.messaging', function()
280281
state.session.set_active({ id = 'sess1' })
281282
state.session.set_user_message_count({})
282283

284+
local original_context = vim.deepcopy(context.get_context())
285+
context.get_context().mentioned_files = { '/tmp/attached.lua' }
286+
context.get_context().selections = {
287+
{
288+
file = { path = '/tmp/attached.lua', name = 'attached.lua', extension = 'lua' },
289+
content = 'selected',
290+
lines = '1, 2',
291+
},
292+
}
293+
283294
local count_before = state.user_message_count['sess1'] or 0
284295
local count_during = nil
285296

@@ -299,8 +310,79 @@ describe('opencode.services.messaging', function()
299310
assert.equal(0, count_before)
300311
assert.equal(1, count_during)
301312
assert.equal(0, count_after)
313+
assert.same({}, context.get_context().mentioned_files)
314+
assert.same({}, context.get_context().selections)
302315

303316
state.api_client.create_message = orig
304317
session_runtime.cancel = orig_cancel
318+
for key, value in pairs(original_context) do
319+
context.get_context()[key] = value
320+
end
321+
end)
322+
323+
it('clears attachments before the request is sent', function()
324+
state.ui.set_windows({ mock = 'windows' })
325+
state.session.set_active({ id = 'sess1' })
326+
327+
local original_context = vim.deepcopy(context.get_context())
328+
context.get_context().mentioned_files = { '/tmp/attached.lua' }
329+
context.get_context().selections = {
330+
{
331+
file = { path = '/tmp/attached.lua', name = 'attached.lua', extension = 'lua' },
332+
content = 'selected',
333+
lines = '1, 2',
334+
},
335+
}
336+
337+
local observed_context
338+
local original_create_message = state.api_client.create_message
339+
state.api_client.create_message = function(_, _session_id, _params)
340+
observed_context = vim.deepcopy(context.get_context())
341+
return Promise.new():resolve({ info = { id = 'm1' }, parts = {} })
342+
end
343+
344+
messaging.send_message('hello world'):wait()
345+
346+
assert.same({}, observed_context.mentioned_files)
347+
assert.same({}, observed_context.selections)
348+
349+
state.api_client.create_message = original_create_message
350+
for key, value in pairs(original_context) do
351+
context.get_context()[key] = value
352+
end
353+
end)
354+
355+
it('clears sent attachments from the active context', function()
356+
state.session.set_active({ id = 'sess1' })
357+
358+
local original_context = vim.deepcopy(context.get_context())
359+
local sent_context = {
360+
current_file = nil,
361+
cursor_data = nil,
362+
linter_errors = nil,
363+
mentioned_files = { '/tmp/attached.lua' },
364+
mentioned_subagents = {},
365+
selections = {
366+
{
367+
file = { path = '/tmp/attached.lua', name = 'attached.lua', extension = 'lua' },
368+
content = 'selected',
369+
lines = '1, 2',
370+
},
371+
},
372+
}
373+
for key, value in pairs(sent_context) do
374+
context.get_context()[key] = value
375+
end
376+
377+
local delta_stub = stub(context, 'delta_context')
378+
messaging.after_run('hello')
379+
380+
assert.same({}, context.get_context().mentioned_files)
381+
assert.same({}, context.get_context().selections)
382+
383+
delta_stub:revert()
384+
for key, value in pairs(original_context) do
385+
context.get_context()[key] = value
386+
end
305387
end)
306388
end)

0 commit comments

Comments
 (0)