Skip to content

Commit 3c6995a

Browse files
committed
Merge remote-tracking branch 'upstream'
2 parents 9c58098 + a52636b commit 3c6995a

118 files changed

Lines changed: 12050 additions & 25442 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,7 @@ luac.out
1818
doc/tags
1919

2020
# Test dependencies
21-
deps/
21+
deps/
22+
23+
# Local Claude settings (keep out of repo)
24+
.claude/

README.md

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -168,16 +168,16 @@ require('opencode').setup({
168168
},
169169
input_window = {
170170
['<S-cr>'] = { 'submit_input_prompt', mode = { 'n', 'i' } }, -- Submit prompt (normal mode and insert mode)
171-
['<esc>'] = { 'close' }, -- Close UI windows
172-
['<C-c>'] = { 'cancel' }, -- Cancel opencode request while it is running
171+
['<esc>'] = { 'close', defer_to_completion = true }, -- Close UI windows
172+
['<C-c>'] = { 'cancel', defer_to_completion = true }, -- Cancel opencode request while it is running
173173
['~'] = { 'mention_file', mode = 'i' }, -- Pick a file and add to context. See File Mentions section
174174
['@'] = { 'mention', mode = 'i' }, -- Insert mention (file/agent)
175175
['/'] = { 'slash_commands', mode = 'i' }, -- Pick a command to run in the input window
176176
['#'] = { 'context_items', mode = 'i' }, -- Manage context items (current file, selection, diagnostics, mentioned files)
177177
['<M-v>'] = { 'paste_image', mode = 'i' }, -- Paste image from clipboard as attachment
178-
['<tab>'] = { 'toggle_pane', mode = { 'n', 'i' } }, -- Toggle between input and output panes
179-
['<up>'] = { 'prev_prompt_history', mode = { 'n', 'i' } }, -- Navigate to previous prompt in history
180-
['<down>'] = { 'next_prompt_history', mode = { 'n', 'i' } }, -- Navigate to next prompt in history
178+
['<tab>'] = { 'toggle_pane', mode = { 'n', 'i' }, defer_to_completion = true }, -- Toggle between input and output panes
179+
['<up>'] = { 'prev_prompt_history', mode = { 'n', 'i' }, defer_to_completion = true }, -- Navigate to previous prompt in history
180+
['<down>'] = { 'next_prompt_history', mode = { 'n', 'i' }, defer_to_completion = true }, -- Navigate to next prompt in history
181181
['<M-m>'] = { 'switch_mode' }, -- Switch between modes (build/plan)
182182
['<M-r>'] = { 'cycle_variant', mode = { 'n', 'i' } }, -- Cycle through available model variants
183183
},
@@ -234,6 +234,7 @@ require('opencode').setup({
234234
},
235235
output = {
236236
filetype = 'opencode_output', -- Filetype assigned to the output buffer (default: 'opencode_output')
237+
compact_assistant_headers = false, -- Collapse consecutive assistant headers in the same mode to a right-aligned timestamp only
237238
tools = {
238239
show_output = true, -- Show tools output [diffs, cmd output, etc.] (default: true)
239240
show_reasoning_output = true, -- Show reasoning/thinking steps output (default: true)
@@ -368,6 +369,7 @@ Each keymap entry is a table consising of:
368369
- Or a custom function: `{ function() ... end }`
369370
- An optional mode: `{ 'toggle', mode = { 'n', 'i' } }`
370371
- An optional desc: `{'toggle', desc = 'Toggle Opencode' }`
372+
- An optional defer_to_completion: `{'toggle', defer_to_completion = true }` if true, when completion menu is open, it will defer to the completion keymaps instead of triggering the action
371373

372374
#### Disabling Specific Keymaps
373375

@@ -661,13 +663,14 @@ Example keymap for silent add:
661663

662664
**add_visual_selection_inline** inserts the visually selected code directly into the input buffer as a Markdown code block, prefixed with the file path:
663665

664-
```
666+
````
665667
**`path/to/file.lua`**
666668
667669
```lua
668670
<selected text>
669-
```
670-
```
671+
````
672+
673+
````
671674
672675
The cursor is left in normal mode in the input buffer so you can type your prompt around the inserted snippet.
673676
@@ -692,7 +695,7 @@ Run a prompt in a new session using the Plan agent and disabling current file co
692695
```vim
693696
:Opencode run new_session "Please help me plan a new feature" agent=plan context.current_file.enabled=false
694697
:Opencode run "Fix the bug in the current file" model=github-copilot/claude-sonnet-4
695-
```
698+
````
696699

697700
## 👮 Permissions
698701

lua/opencode/api.lua

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ end
4141

4242
function M.close()
4343
if state.display_route then
44-
state.display_route = nil
44+
state.ui.clear_display_route()
4545
ui.clear_output()
4646
-- need to trigger a re-render here to re-display the session
4747
ui.render_output()
@@ -61,7 +61,7 @@ end
6161

6262
---@return {status: 'closed'|'hidden'|'visible', position: string, windows: OpencodeWindowState|nil, cursor_positions: {input: integer[]|nil, output: integer[]|nil}}
6363
function M.get_window_state()
64-
return state.get_window_state()
64+
return state.ui.get_window_state()
6565
end
6666

6767
---@param hidden OpencodeHiddenBuffers|nil
@@ -82,7 +82,7 @@ end
8282
---@return {focus: 'input'|'output', open_action: 'reuse_visible'|'restore_hidden'|'create_fresh'}
8383
local function build_toggle_open_context(restore_hidden)
8484
if restore_hidden then
85-
local hidden = state.inspect_hidden_buffers()
85+
local hidden = state.ui.inspect_hidden_buffers()
8686
return {
8787
focus = resolve_hidden_focus(hidden),
8888
open_action = 'restore_hidden',
@@ -98,7 +98,7 @@ local function build_toggle_open_context(restore_hidden)
9898
end
9999

100100
M.toggle = Promise.async(function(new_session)
101-
local decision = state.resolve_toggle_decision(config.ui.persist_state, state.display_route ~= nil)
101+
local decision = state.ui.resolve_toggle_decision(config.ui.persist_state, state.display_route ~= nil)
102102
local action = decision.action
103103
local is_new_session = new_session == true
104104

@@ -329,7 +329,7 @@ function M.set_review_breakpoint()
329329
end
330330

331331
function M.prev_history()
332-
if not state.is_visible() then
332+
if not state.ui.is_visible() then
333333
return
334334
end
335335
local prev_prompt = history.prev()
@@ -340,7 +340,7 @@ function M.prev_history()
340340
end
341341

342342
function M.next_history()
343-
if not state.is_visible() then
343+
if not state.ui.is_visible() then
344344
return
345345
end
346346
local next_prompt = history.next()
@@ -390,7 +390,7 @@ M.submit_input_prompt = Promise.async(function()
390390
if state.display_route then
391391
-- we're displaying /help or something similar, need to clear that and refresh
392392
-- the session data before sending the command
393-
state.display_route = nil
393+
state.ui.clear_display_route()
394394
ui.render_output(true)
395395
end
396396

@@ -485,7 +485,7 @@ M.initialize = Promise.async(function()
485485
vim.notify('Invalid model format: ' .. tostring(state.current_model), vim.log.levels.ERROR)
486486
return
487487
end
488-
state.active_session = new_session
488+
state.session.set_active(new_session)
489489
M.open_input()
490490
state.api_client:init_session(state.active_session.id, {
491491
providerID = providerId,
@@ -533,7 +533,7 @@ end)
533533

534534
function M.with_header(lines, show_welcome)
535535
show_welcome = show_welcome or false
536-
state.display_route = '/header'
536+
state.ui.set_display_route('/header')
537537

538538
local msg = {
539539
'## Opencode.nvim',
@@ -558,7 +558,7 @@ function M.with_header(lines, show_welcome)
558558
end
559559

560560
function M.help()
561-
state.display_route = '/help'
561+
state.ui.set_display_route('/help')
562562
M.open_input()
563563
local msg = M.with_header({
564564
'### Available Commands',
@@ -575,7 +575,7 @@ function M.help()
575575
'|--------------|-------------|',
576576
}, false)
577577

578-
if not state.is_visible() or not state.windows.output_win then
578+
if not state.ui.is_visible() or not state.windows.output_win then
579579
return
580580
end
581581

@@ -611,7 +611,7 @@ M.commands_list = Promise.async(function()
611611
return
612612
end
613613

614-
state.display_route = '/commands'
614+
state.ui.set_display_route('/commands')
615615
M.open_input()
616616

617617
local msg = M.with_header({
@@ -859,7 +859,7 @@ M.rename_session = Promise.async(function(current_session, new_title)
859859
local session_obj = session.get_by_id(current_session.id):await()
860860
if session_obj then
861861
session_obj.title = title
862-
state.active_session = vim.deepcopy(session_obj)
862+
state.session.set_active(vim.deepcopy(session_obj))
863863
end
864864
end
865865
promise:resolve(current_session)
@@ -1056,7 +1056,7 @@ M.review = Promise.async(function(args)
10561056
vim.notify('Invalid model format: ' .. tostring(state.current_model), vim.log.levels.ERROR)
10571057
return
10581058
end
1059-
state.active_session = new_session
1059+
state.session.set_active(new_session)
10601060
M.open_input():await()
10611061
state.api_client
10621062
:send_command(state.active_session.id, {
@@ -1204,7 +1204,7 @@ M.commands = {
12041204
vim.notify('Failed to create new session', vim.log.levels.ERROR)
12051205
return
12061206
end
1207-
state.active_session = new_session
1207+
state.session.set_active(new_session)
12081208
M.open_input()
12091209
else
12101210
M.open_input_new_session()

lua/opencode/api_client.lua

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ function OpencodeApiClient:_ensure_base_url()
3434

3535
if not state.opencode_server then
3636
-- this is last resort - try to start the server and could be blocking
37-
state.opencode_server = server_job.ensure_server():wait() --[[@as OpencodeServer]]
37+
state.jobs.set_server(server_job.ensure_server():wait() --[[@as OpencodeServer]])
3838
-- shouldn't normally happen but prevents error in replay tester
3939
if not state.opencode_server then
4040
return false
@@ -532,7 +532,7 @@ local function create_client(base_url)
532532
end
533533
end
534534

535-
state.subscribe('opencode_server', on_server_change)
535+
state.store.subscribe('opencode_server', on_server_change)
536536

537537
return api_client
538538
end

lua/opencode/config.lua

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -74,26 +74,26 @@ M.defaults = {
7474
['<leader>ods'] = { 'debug_session', desc = 'Open raw session debug view' },
7575
},
7676
input_window = {
77-
['<cr>'] = { 'submit_input_prompt', mode = { 'n' }, desc = 'Submit prompt' },
78-
['<S-cr>'] = { 'submit_input_prompt', mode = { 'n', 'i' }, desc = 'Submit prompt' },
79-
['<esc>'] = { 'close', desc = 'Close Opencode windows' },
80-
['<C-c>'] = { 'cancel', desc = 'Cancel running request' },
81-
['~'] = { 'mention_file', mode = 'i', desc = 'Mention file in context' },
82-
['@'] = { 'mention', mode = 'i', desc = 'Open mention picker' },
83-
['/'] = { 'slash_commands', mode = 'i', desc = 'Open slash commands picker' },
84-
['#'] = { 'context_items', mode = 'i', desc = 'Open context items picker' },
85-
['<M-v>'] = { 'paste_image', mode = 'i', desc = 'Paste image from clipboard' },
86-
['<tab>'] = { 'toggle_pane', mode = { 'n' }, desc = 'Toggle input/output panes' },
87-
['<up>'] = { 'prev_prompt_history', mode = { 'n', 'i' }, desc = 'Previous prompt history item' },
88-
['<down>'] = { 'next_prompt_history', mode = { 'n', 'i' }, desc = 'Next prompt history item' },
89-
['<M-m>'] = { 'switch_mode', mode = { 'n', 'i' }, desc = 'Switch agent mode' },
90-
['<M-r>'] = { 'cycle_variant', mode = { 'n', 'i' }, desc = 'Cycle model variants' },
91-
['<M-i>'] = { 'toggle_input', mode = { 'n', 'i' }, desc = 'Toggle input window' },
92-
['gr'] = { 'references', desc = 'Browse code references' },
93-
['<leader>oS'] = { 'select_child_session', desc = 'Select child session' },
94-
['<leader>oD'] = { 'debug_message', desc = 'Open raw message debug view' },
95-
['<leader>oO'] = { 'debug_output', desc = 'Open raw output debug view' },
96-
['<leader>ods'] = { 'debug_session', desc = 'Open raw session debug view' },
77+
['<cr>'] = { 'submit_input_prompt', mode = { 'n' }, desc = 'Submit prompt' },
78+
['<S-cr>'] = { 'submit_input_prompt', mode = { 'n', 'i' }, desc = 'Submit prompt' },
79+
['<esc>'] = { 'close', desc = 'Close Opencode windows', defer_to_completion = true },
80+
['<C-c>'] = { 'cancel', desc = 'Cancel running request' , defer_to_completion = true },
81+
['~'] = { 'mention_file', mode = 'i', desc = 'Mention file in context' },
82+
['@'] = { 'mention', mode = 'i', desc = 'Open mention picker' },
83+
['/'] = { 'slash_commands', mode = 'i', desc = 'Open slash commands picker' },
84+
['#'] = { 'context_items', mode = 'i', desc = 'Open context items picker' },
85+
['<M-v>'] = { 'paste_image', mode = 'i', desc = 'Paste image from clipboard' },
86+
['<tab>'] = { 'toggle_pane', mode = { 'n' }, desc = 'Toggle input/output panes', defer_to_completion = true },
87+
['<up>'] = { 'prev_prompt_history', mode = { 'n', 'i' }, desc = 'Previous prompt history item', defer_to_completion = true },
88+
['<down>'] = { 'next_prompt_history', mode = { 'n', 'i' }, desc = 'Next prompt history item' , defer_to_completion = true },
89+
['<M-m>'] = { 'switch_mode', mode = { 'n', 'i' }, desc = 'Switch agent mode' },
90+
['<M-r>'] = { 'cycle_variant', mode = { 'n', 'i' }, desc = 'Cycle model variants' },
91+
['<M-i>'] = { 'toggle_input', mode = { 'n', 'i' }, desc = 'Toggle input window' },
92+
['gr'] = { 'references', desc = 'Browse code references' },
93+
['<leader>oS'] = { 'select_child_session', desc = 'Select child session' },
94+
['<leader>oD'] = { 'debug_message', desc = 'Open raw message debug view' },
95+
['<leader>oO'] = { 'debug_output', desc = 'Open raw output debug view' },
96+
['<leader>ods'] = { 'debug_session', desc = 'Open raw session debug view' },
9797
},
9898
session_picker = {
9999
rename_session = { '<C-r>', desc = 'Rename selected session' },
@@ -139,9 +139,15 @@ M.defaults = {
139139
},
140140
output = {
141141
filetype = 'opencode_output',
142+
compact_assistant_headers = false,
142143
rendering = {
143144
markdown_debounce_ms = 250,
144145
on_data_rendered = nil,
146+
markdown_on_idle = false,
147+
-- If set to a number, markdown rendering will be deferred while
148+
-- `state.user_message_count[session_id]` is greater than this value.
149+
-- If `nil`, the existing behavior is used (defer while > 0).
150+
markdown_on_idle_threshold = nil,
145151
event_throttle_ms = 40,
146152
event_collapsing = true,
147153
},
@@ -255,6 +261,8 @@ M.defaults = {
255261
enabled = false,
256262
capture_streamed_events = false,
257263
show_ids = true,
264+
highlight_changed_lines = false,
265+
highlight_changed_lines_timeout_ms = 120,
258266
quick_chat = {
259267
keep_session = false,
260268
set_active_session = false,

lua/opencode/context.lua

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,10 @@ local toggleable_context_keys = {
2222
---@param context_key OpencodeToggleableContextKey
2323
---@return table
2424
local function ensure_context_state(context_key)
25-
state.current_context_config = state.current_context_config or {}
26-
local current = state.current_context_config[context_key]
2725
local defaults = vim.tbl_get(config, 'context', context_key) or {}
28-
state.current_context_config[context_key] = vim.tbl_deep_extend('force', {}, defaults, current or {})
29-
return state.current_context_config[context_key]
26+
return state.context.update_current_context_config(function(current_config)
27+
current_config[context_key] = vim.tbl_deep_extend('force', {}, defaults, current_config[context_key] or {})
28+
end)[context_key]
3029
end
3130

3231
M.ChatContext = ChatContext
@@ -117,12 +116,10 @@ end
117116
-- Delegate global state management to ChatContext
118117
function M.add_selection(selection)
119118
ChatContext.add_selection(selection)
120-
state.context_updated_at = vim.uv.now()
121119
end
122120

123121
function M.remove_selection(selection)
124122
ChatContext.remove_selection(selection)
125-
state.context_updated_at = vim.uv.now()
126123
end
127124

128125
function M.clear_selections()
@@ -197,12 +194,7 @@ function M.build_inline_selection_text(range)
197194
end
198195

199196
local filetype = vim.bo[buf].filetype or ''
200-
local text = string.format(
201-
'**`%s`**\n\n```%s\n%s\n```',
202-
file.path,
203-
filetype,
204-
current_selection.text
205-
)
197+
local text = string.format('**`%s`**\n\n```%s\n%s\n```', file.path, filetype, current_selection.text)
206198

207199
return text
208200
end
@@ -222,13 +214,11 @@ function M.add_file(file)
222214

223215
file = vim.fn.fnamemodify(file, ':p')
224216
ChatContext.add_file(file)
225-
state.context_updated_at = vim.uv.now()
226217
end
227218

228219
function M.remove_file(file)
229220
file = vim.fn.fnamemodify(file, ':p')
230221
ChatContext.remove_file(file)
231-
state.context_updated_at = vim.uv.now()
232222
end
233223

234224
function M.clear_files()
@@ -237,12 +227,10 @@ end
237227

238228
function M.add_subagent(subagent)
239229
ChatContext.add_subagent(subagent)
240-
state.context_updated_at = vim.uv.now()
241230
end
242231

243232
function M.remove_subagent(subagent)
244233
ChatContext.remove_subagent(subagent)
245-
state.context_updated_at = vim.uv.now()
246234
end
247235

248236
function M.clear_subagents()
@@ -255,7 +243,6 @@ end
255243

256244
function M.load()
257245
ChatContext.load()
258-
state.context_updated_at = vim.uv.now()
259246
end
260247

261248
-- Context creation with delta logic (delegates to ChatContext)
@@ -354,7 +341,7 @@ function M.setup()
354341
M.load()
355342
end, 200)
356343

357-
state.subscribe({ 'current_code_buf', 'current_context_config', 'is_opencode_focused' }, function()
344+
state.store.subscribe({ 'current_code_buf', 'current_context_config', 'is_opencode_focused' }, function()
358345
debounced_load()
359346
end)
360347

0 commit comments

Comments
 (0)