Skip to content

Commit 42ae5e2

Browse files
authored
fix(context): keep context state and send behavior in sync (#306)
1 parent 8a66ab1 commit 42ae5e2

6 files changed

Lines changed: 166 additions & 6 deletions

File tree

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -728,6 +728,13 @@ You can quiclkly see the current context items in the context bar at the top of
728728
<img src="https://i.imgur.com/vGgu6br.png" alt="Opencode.nvim context bar" width="90%" />
729729
</div>
730730

731+
For `Current file`, the color indicates whether it will be sent with the next prompt:
732+
733+
- Regular highlight: file is pending and will be included.
734+
- Dimmed/gray highlight: file was already sent and has not changed, so it will be skipped (delta behavior).
735+
736+
If the file content changes, it becomes pending again and will be sent on the next prompt.
737+
731738
### Context Items Completion
732739

733740
You can quickly reference available context items by typing `#` in the input window. This will show a completion menu with all available context items:

lua/opencode/context.lua

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,8 +208,7 @@ function M.clear_subagents()
208208
end
209209

210210
function M.unload_attachments()
211-
ChatContext.clear_files()
212-
ChatContext.clear_selections()
211+
ChatContext.unload_attachments()
213212
end
214213

215214
function M.load()

lua/opencode/context/chat_context.lua

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ end
195195

196196
function M.clear_selections()
197197
M.context.selections = {}
198+
state.context_updated_at = vim.uv.now()
198199
end
199200

200201
function M.add_file(file)
@@ -230,6 +231,7 @@ end
230231

231232
function M.clear_files()
232233
M.context.mentioned_files = {}
234+
state.context_updated_at = vim.uv.now()
233235
end
234236

235237
function M.add_subagent(subagent)
@@ -261,11 +263,13 @@ end
261263

262264
function M.clear_subagents()
263265
M.context.mentioned_subagents = {}
266+
state.context_updated_at = vim.uv.now()
264267
end
265268

266269
function M.unload_attachments()
267270
M.context.mentioned_files = {}
268271
M.context.selections = {}
272+
state.context_updated_at = vim.uv.now()
269273
end
270274

271275
function M.get_mentioned_files()
@@ -336,7 +340,7 @@ function M.should_update_current_file(current_file)
336340
end
337341

338342
if not current_file then
339-
return false, false
343+
return true, false
340344
end
341345

342346
-- Different file name means update needed
@@ -373,6 +377,10 @@ function M.load()
373377
return
374378
end
375379

380+
local prev_current_file = vim.deepcopy(M.context.current_file)
381+
local prev_cursor_data = vim.deepcopy(M.context.cursor_data)
382+
local prev_linter_errors = vim.deepcopy(M.context.linter_errors)
383+
376384
local current_file = base_context.get_current_file(buf)
377385
local cursor_data = base_context.get_current_cursor_data(buf, win)
378386

@@ -389,6 +397,14 @@ function M.load()
389397
M.context.cursor_data = cursor_data
390398
M.context.linter_errors = M.get_diagnostics(buf, nil, nil)
391399

400+
if
401+
not vim.deep_equal(prev_current_file, M.context.current_file)
402+
or not vim.deep_equal(prev_cursor_data, M.context.cursor_data)
403+
or not vim.deep_equal(prev_linter_errors, M.context.linter_errors)
404+
then
405+
state.context_updated_at = vim.uv.now()
406+
end
407+
392408
-- Handle current selection
393409
if base_context.is_context_enabled('selection') then
394410
local current_selection = base_context.get_current_selection()
@@ -484,7 +500,11 @@ M.format_message = Promise.async(function(prompt, opts)
484500
return { parts = parts }
485501
end
486502

487-
if M.context.current_file and not M.context.current_file.sent_at then
503+
if
504+
base_context.is_context_enabled('current_file', context_config)
505+
and M.context.current_file
506+
and not M.context.current_file.sent_at
507+
then
488508
table.insert(parts, format_file_part(M.context.current_file.path))
489509
set_file_sent_timestamps(M.context.current_file)
490510
end

lua/opencode/ui/context_bar.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ end
171171

172172
function M.setup()
173173
state.subscribe(
174-
{ 'current_context_config', 'current_code_buf', 'opencode_focused', 'context_updated_at', 'user_message_count' },
174+
{ 'current_context_config', 'current_code_buf', 'is_opencode_focused', 'context_updated_at', 'user_message_count' },
175175
function()
176176
M.render()
177177
end

tests/unit/context_bar_spec.lua

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,8 @@ describe('opencode.ui.context_bar', function()
299299
assert.is_true(subscription_called)
300300
assert.is_table(captured_keys)
301301

302-
local expected_keys = { 'current_context_config', 'current_code_buf', 'opencode_focused', 'context_updated_at' }
302+
local expected_keys =
303+
{ 'current_context_config', 'current_code_buf', 'is_opencode_focused', 'context_updated_at' }
303304
for _, expected_key in ipairs(expected_keys) do
304305
local found = false
305306
for _, key in ipairs(captured_keys) do

tests/unit/context_spec.lua

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,92 @@ describe('format_message', function()
154154
BaseContext.get_current_selection = original_get_current_selection
155155
BaseContext.get_current_file_for_selection = original_get_current_file_for_selection
156156
end)
157+
158+
it('does not include current_file when disabled even if stale current_file exists', function()
159+
local ChatContext = require('opencode.context.chat_context')
160+
local BaseContext = require('opencode.context.base_context')
161+
162+
local original_get_current_buf = BaseContext.get_current_buf
163+
local original_get_diagnostics = BaseContext.get_diagnostics
164+
165+
ChatContext.context.current_file = {
166+
path = '/tmp/foo.lua',
167+
name = 'foo.lua',
168+
extension = 'lua',
169+
sent_at = nil,
170+
sent_at_mtime = nil,
171+
}
172+
173+
BaseContext.get_current_buf = function()
174+
return 1, 1
175+
end
176+
BaseContext.get_diagnostics = function()
177+
return {}
178+
end
179+
180+
local parts = context
181+
.format_message('follow-up prompt', {
182+
current_file = { enabled = false },
183+
selection = { enabled = false },
184+
diagnostics = { enabled = false },
185+
cursor_data = { enabled = false },
186+
buffer = { enabled = false },
187+
git_diff = { enabled = false },
188+
})
189+
:wait()
190+
191+
local has_file_part = false
192+
for _, part in ipairs(parts) do
193+
if part.type == 'file' then
194+
has_file_part = true
195+
break
196+
end
197+
end
198+
199+
assert.is_false(has_file_part)
200+
assert.is_nil(ChatContext.context.current_file.sent_at)
201+
202+
BaseContext.get_current_buf = original_get_current_buf
203+
BaseContext.get_diagnostics = original_get_diagnostics
204+
end)
205+
end)
206+
207+
describe('context update notifications', function()
208+
local ChatContext
209+
local original_now
210+
211+
before_each(function()
212+
ChatContext = require('opencode.context.chat_context')
213+
ChatContext.context.mentioned_files = { '/tmp/a.lua' }
214+
ChatContext.context.selections = { { file = { path = '/tmp/a.lua' }, lines = '1, 1', content = 'x' } }
215+
ChatContext.context.mentioned_subagents = { 'agent1' }
216+
217+
state.context_updated_at = 0
218+
local tick = 0
219+
original_now = vim.uv.now
220+
vim.uv.now = function()
221+
tick = tick + 1
222+
return tick
223+
end
224+
end)
225+
226+
after_each(function()
227+
vim.uv.now = original_now
228+
end)
229+
230+
it('updates context_updated_at for clear operations and unload_attachments', function()
231+
ChatContext.clear_files()
232+
assert.equal(1, state.context_updated_at)
233+
234+
ChatContext.clear_selections()
235+
assert.equal(2, state.context_updated_at)
236+
237+
ChatContext.clear_subagents()
238+
assert.equal(3, state.context_updated_at)
239+
240+
context.unload_attachments()
241+
assert.equal(4, state.context_updated_at)
242+
end)
157243
end)
158244

159245
describe('delta_context', function()
@@ -812,6 +898,53 @@ describe('ChatContext.load() preserves selections on file switch', function()
812898
BaseContext.new_selection = original_new_selection
813899
ChatContext.get_diagnostics = original_get_diagnostics
814900
end)
901+
902+
it('should clear stale current_file when current_file context is disabled', function()
903+
ChatContext.context.current_file = {
904+
path = '/tmp/stale.lua',
905+
name = 'stale.lua',
906+
extension = 'lua',
907+
sent_at = nil,
908+
sent_at_mtime = nil,
909+
}
910+
911+
local original_get_current_buf = BaseContext.get_current_buf
912+
local original_get_current_file = BaseContext.get_current_file
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 1, 1
920+
end
921+
BaseContext.get_current_file = function()
922+
return nil
923+
end
924+
BaseContext.get_current_cursor_data = function()
925+
return nil
926+
end
927+
BaseContext.is_context_enabled = function()
928+
return false
929+
end
930+
BaseContext.get_current_selection = function()
931+
return nil
932+
end
933+
ChatContext.get_diagnostics = function()
934+
return {}
935+
end
936+
937+
ChatContext.load()
938+
939+
assert.is_nil(ChatContext.context.current_file)
940+
941+
BaseContext.get_current_buf = original_get_current_buf
942+
BaseContext.get_current_file = original_get_current_file
943+
BaseContext.get_current_cursor_data = original_get_current_cursor_data
944+
BaseContext.is_context_enabled = original_is_context_enabled
945+
BaseContext.get_current_selection = original_get_current_selection
946+
ChatContext.get_diagnostics = original_get_diagnostics
947+
end)
815948
end)
816949

817950
describe('add_visual_selection API', function()

0 commit comments

Comments
 (0)