Skip to content

Commit 2a3203f

Browse files
authored
Refactor state management into smaller slices (#321)
* wip: state-management refactor * feat(state): split state in smaller slices * refactor(state): move UI/window APIs into state.ui Move window state, cursor, visibility, and hidden-buffer helpers into a dedicated state.ui table. Update callers across modules and tests to reference state.ui.*, improving separation between core store logic and UI utilities. * refactor(state): centralize protected state mutations into domain setters * refactor(state): add mutation type annotations for state modules * refactor(state): rename notify to emit * fix: unsubscribe in tests * fix: context setting immutability * refactor(state): use std.RawGet and keyof OpencodeState in annotations * refactor(state): simplify store API and session/jobs mutations * refactor(state): centralize observable API under state.store Replace top-level observable helpers (state.subscribe, state.unsubscribe, state.append, state.emit) * fix: reference_picker * handle split resize safely + use store subscribe (#328) * fix(ui): handle split resize safely Use window-type-aware resize logic in output_window.update_dimensions. - guard missing or invalid output_win before resizing - use nvim_win_set_width for split windows (relative == '') - keep nvim_win_set_config for floating windows - add regression tests for float-focus resize and invalid window This prevents "Cannot split a floating window" on VimResized while preserving existing zoom width behavior. Verified with: - ./run_tests.sh -t tests/unit/zoom_spec.lua * fix(reference-picker): use store subscribe Follow the state observable API migration by switching reference picker setup from state.subscribe(...) to state.store.subscribe(...). This matches the refactor that centralized observable helpers under state.store and fixes startup error: attempt to call field 'subscribe' (a nil value). Verified with: ./run_tests.sh -t tests/unit/reference_picker_spec.lua
1 parent bb31b54 commit 2a3203f

57 files changed

Lines changed: 1657 additions & 1196 deletions

Some content is hidden

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

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/context.lua

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,14 @@ 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]
25+
local current_config = state.current_context_config or {}
26+
local current = current_config[context_key]
27+
local new_config = vim.deepcopy(current_config)
2728
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]
29+
30+
new_config[context_key] = vim.tbl_deep_extend('force', {}, defaults, current or {})
31+
state.context.set_current_context_config(new_config)
32+
return new_config[context_key]
3033
end
3134

3235
M.ChatContext = ChatContext
@@ -117,12 +120,12 @@ end
117120
-- Delegate global state management to ChatContext
118121
function M.add_selection(selection)
119122
ChatContext.add_selection(selection)
120-
state.context_updated_at = vim.uv.now()
123+
state.context.set_context_updated_at(vim.uv.now())
121124
end
122125

123126
function M.remove_selection(selection)
124127
ChatContext.remove_selection(selection)
125-
state.context_updated_at = vim.uv.now()
128+
state.context.set_context_updated_at(vim.uv.now())
126129
end
127130

128131
function M.clear_selections()
@@ -222,13 +225,13 @@ function M.add_file(file)
222225

223226
file = vim.fn.fnamemodify(file, ':p')
224227
ChatContext.add_file(file)
225-
state.context_updated_at = vim.uv.now()
228+
state.context.set_context_updated_at(vim.uv.now())
226229
end
227230

228231
function M.remove_file(file)
229232
file = vim.fn.fnamemodify(file, ':p')
230233
ChatContext.remove_file(file)
231-
state.context_updated_at = vim.uv.now()
234+
state.context.set_context_updated_at(vim.uv.now())
232235
end
233236

234237
function M.clear_files()
@@ -237,12 +240,12 @@ end
237240

238241
function M.add_subagent(subagent)
239242
ChatContext.add_subagent(subagent)
240-
state.context_updated_at = vim.uv.now()
243+
state.context.set_context_updated_at(vim.uv.now())
241244
end
242245

243246
function M.remove_subagent(subagent)
244247
ChatContext.remove_subagent(subagent)
245-
state.context_updated_at = vim.uv.now()
248+
state.context.set_context_updated_at(vim.uv.now())
246249
end
247250

248251
function M.clear_subagents()
@@ -255,7 +258,7 @@ end
255258

256259
function M.load()
257260
ChatContext.load()
258-
state.context_updated_at = vim.uv.now()
261+
state.context.set_context_updated_at(vim.uv.now())
259262
end
260263

261264
-- Context creation with delta logic (delegates to ChatContext)
@@ -354,7 +357,7 @@ function M.setup()
354357
M.load()
355358
end, 200)
356359

357-
state.subscribe({ 'current_code_buf', 'current_context_config', 'is_opencode_focused' }, function()
360+
state.store.subscribe({ 'current_code_buf', 'current_context_config', 'is_opencode_focused' }, function()
358361
debounced_load()
359362
end)
360363

lua/opencode/context/chat_context.lua

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ function M.add_selection(selection)
175175
end
176176

177177
table.insert(M.context.selections, selection)
178-
state.context_updated_at = vim.uv.now()
178+
state.context.set_context_updated_at(vim.uv.now())
179179
end
180180

181181
function M.remove_selection(selection)
@@ -190,12 +190,12 @@ function M.remove_selection(selection)
190190
break
191191
end
192192
end
193-
state.context_updated_at = vim.uv.now()
193+
state.context.set_context_updated_at(vim.uv.now())
194194
end
195195

196196
function M.clear_selections()
197197
M.context.selections = {}
198-
state.context_updated_at = vim.uv.now()
198+
state.context.set_context_updated_at(vim.uv.now())
199199
end
200200

201201
function M.add_file(file)
@@ -210,7 +210,7 @@ function M.add_file(file)
210210
if not vim.tbl_contains(M.context.mentioned_files, file) then
211211
table.insert(M.context.mentioned_files, file)
212212
end
213-
state.context_updated_at = vim.uv.now()
213+
state.context.set_context_updated_at(vim.uv.now())
214214
end
215215

216216
function M.remove_file(file)
@@ -226,12 +226,12 @@ function M.remove_file(file)
226226
break
227227
end
228228
end
229-
state.context_updated_at = vim.uv.now()
229+
state.context.set_context_updated_at(vim.uv.now())
230230
end
231231

232232
function M.clear_files()
233233
M.context.mentioned_files = {}
234-
state.context_updated_at = vim.uv.now()
234+
state.context.set_context_updated_at(vim.uv.now())
235235
end
236236

237237
function M.add_subagent(subagent)
@@ -243,7 +243,7 @@ function M.add_subagent(subagent)
243243
if not vim.tbl_contains(M.context.mentioned_subagents, subagent) then
244244
table.insert(M.context.mentioned_subagents, subagent)
245245
end
246-
state.context_updated_at = vim.uv.now()
246+
state.context.set_context_updated_at(vim.uv.now())
247247
end
248248

249249
function M.remove_subagent(subagent)
@@ -258,18 +258,18 @@ function M.remove_subagent(subagent)
258258
break
259259
end
260260
end
261-
state.context_updated_at = vim.uv.now()
261+
state.context.set_context_updated_at(vim.uv.now())
262262
end
263263

264264
function M.clear_subagents()
265265
M.context.mentioned_subagents = {}
266-
state.context_updated_at = vim.uv.now()
266+
state.context.set_context_updated_at(vim.uv.now())
267267
end
268268

269269
function M.unload_attachments()
270270
M.context.mentioned_files = {}
271271
M.context.selections = {}
272-
state.context_updated_at = vim.uv.now()
272+
state.context.set_context_updated_at(vim.uv.now())
273273
end
274274

275275
function M.get_mentioned_files()
@@ -402,7 +402,7 @@ function M.load()
402402
or not vim.deep_equal(prev_cursor_data, M.context.cursor_data)
403403
or not vim.deep_equal(prev_linter_errors, M.context.linter_errors)
404404
then
405-
state.context_updated_at = vim.uv.now()
405+
state.context.set_context_updated_at(vim.uv.now())
406406
end
407407

408408
-- Handle current selection
@@ -471,7 +471,7 @@ function M.delta_context(opts)
471471
end
472472
end
473473

474-
state.context_updated_at = vim.uv.now()
474+
state.context.set_context_updated_at(vim.uv.now())
475475
return ctx
476476
end
477477

0 commit comments

Comments
 (0)