Skip to content

Refactor state management into smaller slices - #321

Merged
sudo-tee merged 13 commits into
mainfrom
refactor/state-management
Mar 18, 2026
Merged

Refactor state management into smaller slices#321
sudo-tee merged 13 commits into
mainfrom
refactor/state-management

Conversation

@sudo-tee

Copy link
Copy Markdown
Owner

This pull request refactors the codebase to centralize and encapsulate state management by replacing direct access to state variables with method calls on new or existing state submodules (such as state.session, state.model, state.ui, and state.jobs). This improves code maintainability, readability, and reduces the risk of inconsistent state updates.

@sudo-teesudo-tee mentioned this pull request Mar 13, 2026
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.
@sudo-tee
sudo-teeforce-pushed the refactor/state-management branch from 865ed29 to 726772bCompareMarch 13, 2026 17:18

This comment was marked as outdated.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors opencode.state into a protected, domain-based state store (UI/session/jobs/model/renderer/context) and updates the core UI/runtime code plus tests to use the new mutation APIs instead of direct state writes.

Changes:

  • Introduces a new protected state store (opencode.state.store) and domain-specific state mutation modules (state.ui, state.session, state.jobs, state.model, state.renderer, state.context).
  • Updates core runtime modules (UI, renderer, core, server job, context) to mutate state exclusively via domain setters.
  • Updates unit/replay/manual tests to align with the new state mutation APIs and adds a direct-write error assertion.

Reviewed changes

Copilot reviewed 50 out of 50 changed files in this pull request and generated 5 comments.

Show a summary per file
FileDescription
tests/unit/zoom_spec.luaUpdates zoom/window state mutations to use state.ui setters.
tests/unit/state_spec.luaReworks observable-state tests to use domain setters and adds direct-write error test.
tests/unit/snapshot_spec.luaSwitches active-session state writes to state.session setters.
tests/unit/session_spec.luaUses state.jobs.set_api_client instead of direct state.api_client writes.
tests/unit/server_job_spec.luaMigrates server state manipulation to state.jobs APIs.
tests/unit/render_state_spec.luaUses state.renderer.set_messages rather than direct state.messages writes.
tests/unit/persist_state_spec.luaRefactors multiple state writes to use state.* domains and store.set for generic keys.
tests/unit/permission_integration_spec.luaMoves message/permission/session setup to state.renderer + state.session.
tests/unit/loading_animation_spec.luaUses state.session setters for active session state.
tests/unit/input_window_spec.luaUpdates window/input-content/display-route state mutations to state.ui APIs.
tests/unit/hooks_spec.luaMigrates active session, user_message_count, and pending permissions mutations to new domains.
tests/unit/event_manager_spec.luaUses state.jobs for server state changes.
tests/unit/dialog_spec.luaUpdates window state setup/teardown to state.ui.
tests/unit/cursor_tracking_spec.luaMoves cursor + window state operations to state.ui and uses store.set for cursor persistence keys.
tests/unit/core_spec.luaUpdates tests to use domain setters and store.set when restoring state keys.
tests/unit/context_spec.luaMigrates context/session/opening mutations to state.context/state.session/state.ui.
tests/unit/context_bar_spec.luaUses state.ui.set_windows for context bar rendering tests.
tests/unit/config_file_spec.luaMigrates api client setup to state.jobs.set_api_client.
tests/unit/api_spec.luaUses state.session for active session and state.jobs for api client; uses state.model for current_model.
tests/unit/api_client_spec.luaUses state.context.set_current_cwd instead of direct writes.
tests/replay/renderer_spec.luaMigrates replay tests to state.session and state.renderer.
tests/minimal/init.luaStubs Tree-sitter start in minimal test init to avoid side effects.
tests/manual/renderer_replay.luaUpdates manual replay script to use state.session and state.jobs APIs.
tests/helpers.luaUses state.model.set_mode and state.ui.set_windows in test setup.
lua/opencode/variant_picker.luaWrites selected variant via state.model.set_variant.
lua/opencode/ui/ui.luaMigrates UI state reads/writes (cursors, windows, hidden snapshots, zoom) to state.ui APIs.
lua/opencode/ui/session_picker.luaUses state.session.set_active when deleting current session.
lua/opencode/ui/renderer.luaMigrates renderer-related state to state.renderer and model/mode to state.model.
lua/opencode/ui/output_window.luaUses state.ui cursor/window focus and saved-options setters.
lua/opencode/ui/input_window.luaUses state.ui cursor/window focus and input-content setters.
lua/opencode/ui/autocmds.luaMigrates last-code-window/current-buf and cwd/panel-focused updates to state.ui/state.context.
lua/opencode/state/ui.luaNew UI state domain module (windows, focus, cursors, hidden snapshots, toggle decision logic).
lua/opencode/state/store.luaNew protected store implementation with subscriptions/emits and guarded writes.
lua/opencode/state/session.luaNew session state domain module (active session, restore points, last sent context, counts).
lua/opencode/state/renderer.luaNew renderer state domain module (messages, permissions, stats).
lua/opencode/state/model.luaNew model state domain module (mode/model/variant + overrides).
lua/opencode/state/jobs.luaNew jobs state domain module (job count, server, api client, event manager, CLI version).
lua/opencode/state/init.luaNew opencode.state entry point exposing store + domains and forbidding direct writes.
lua/opencode/state/context.luaNew context state domain module (context config, timestamps, cwd).
lua/opencode/state.luaThin wrapper re-exporting opencode.state.init.
lua/opencode/snapshot.luaUpdates restore-point state writes to state.session APIs.
lua/opencode/server_job.luaMigrates job count + server state mutations to state.jobs.
lua/opencode/quick_chat.luaUses state.session.set_active for debug quick-chat session wiring.
lua/opencode/image_handler.luaUpdates context timestamp mutation to state.context.
lua/opencode/event_manager.luaUses state.jobs.set_event_manager during setup.
lua/opencode/core.luaMigrates many state writes (visibility checks, open/close, model/mode, session, context config, job/server) to domains.
lua/opencode/context/chat_context.luaUpdates context timestamp writes to state.context.
lua/opencode/context.luaSwitches current_context_config updates to state.context setters (and refactors ensure logic).
lua/opencode/api.luaUses state.ui for window state/visibility/toggle decisions and state.session for active session writes.
lua/opencode/api_client.luaUses state.jobs.set_server when ensuring server as fallback.
Comments suppressed due to low confidence (1)

lua/opencode/ui/ui.lua:169

  • hide_visible_windows mutates fields on the state.windows table directly (state.windows.input_win = nil, etc.). With the new protected store approach, mutating nested tables bypasses store.set/change notifications and can leave subscribers observing stale window state. Consider updating windows via a dedicated state.ui helper (or re-setting the whole windows table through state.ui.set_windows(...)) instead of mutating nested fields in place.
 state.ui.stash_hidden_buffers(snapshot)
if state.windows == windows then
state.windows.input_win = nil
state.windows.output_win = nil
state.windows.footer_win = nil
state.windows.output_was_at_bottom = snapshot.output_was_at_bottom
end

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadlua/opencode/state/store.lua Outdated
Comment threadlua/opencode/state/store.lua Outdated
Comment threadlua/opencode/context.lua
Comment threadlua/opencode/state/ui.lua
Comment threadtests/unit/context_spec.lua
@sudo-tee
sudo-teeforce-pushed the refactor/state-management branch 4 times, most recently from 638b05f to dfbbe6bCompareMarch 13, 2026 19:57
@sudo-tee
sudo-teeforce-pushed the refactor/state-management branch 3 times, most recently from 2414691 to 6f6866fCompareMarch 16, 2026 15:02
@sudo-tee
sudo-teeforce-pushed the refactor/state-management branch from 6f6866f to dac4648CompareMarch 16, 2026 17:05
@sudo-tee
sudo-tee marked this pull request as ready for review March 16, 2026 18:56
@sudo-tee
sudo-teeforce-pushed the refactor/state-management branch from ed89809 to 13c630eCompareMarch 17, 2026 13:03
Replace top-level observable helpers (state.subscribe, state.unsubscribe, state.append, state.emit)
@jensenojs

jensenojs commented Mar 17, 2026

Copy link
Copy Markdown
Collaborator

So, after refactoring the PR, it will be considered for direct merging into the main branch, right?

--

Many of my tasks depend on each other. I can only make each commit as readable as possible, but the overall change may be large. Should I just produce a draft? Even so, that could still make the review process painful.

It might sound a bit odd, but could I create a personal branch here with you? I don't know if it would help our local workflow (I would only create one or two at most), and it would make management easier

I don't have much experience collaborating this way, so please handle it however you're most comfortable. (I also never got the hang of GitHub branch permissions.)

@sudo-tee

Copy link
Copy Markdown
OwnerAuthor

Yes once completed it will be merged to main. I'm almost done. I am using this branch daily and it's pretty stable.

@sudo-tee

Copy link
Copy Markdown
OwnerAuthor

As per branch collaboration usually there is a checkbox when creating a PR where you call allow the owner to make changes. I'm mobile at the moment so I can't really check.

I will try to merge this branch today or tomorrow. So you can start from it

sudo-teeand others added 2 commits March 18, 2026 07:15
* 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
---------
Co-authored-by: oujinsai <oujinsai@baidu.com>
@sudo-tee
sudo-tee merged commit 2a3203f into mainMar 18, 2026
10 checks passed
disrupted pushed a commit to disrupted/opencode-native.nvim that referenced this pull request Jul 23, 2026
* 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 (sudo-tee#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
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@sudo-tee@jensenojs