-
Notifications
You must be signed in to change notification settings - Fork 39
Currently splitting implementation via new PRs - feat(ui): preserve buffers on toggle #229
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Currently splitting implementation via new PRs - feat(ui): preserve buffers on toggle #229
Conversation
cf1203c to
6ab8fd6
Compare
|
great addition! this is so useful, imo it would make for a sensible default. there is still |
|
This sems like a useful feature. As I will have to maintain this in the future, I would like it if you add a couple of tests for this. As well as updating the README file Thanks a lot for the PR |
|
Sorry, I found some more subtle issues. I'm glad you reviewed so carefully, including that it should probably be the default behavior. I will resolve all the above issues as soon as possible. |
Add `persist_state` configuration option (default: true) that preserves buffers and cursor positions when toggling the UI off. This enables faster restore and maintains scroll position across toggle operations.
6ab8fd6 to
ec6711e
Compare
|
I am quite satisfied with my local behavior, (my nvim configuration will switch when the position is both left and center, plus I want to retain the cursor position when toggling, so although the code is relatively complex, I also spent quite a while calibrating through actual testing. I might record a video hhh) The overall code writing and cleanup were all done by kimi2.5. There may be some unnecessary comments or still overdesigned parts here. I will carefully review my PR tomorrow and clean up that junk (it's late night here now, I have to sleep). I pushed the code up first in case someone might want to try it out early. @sudo-tee @disrupted update : iShot_2026-02-06_14.55.30.mp4 |
|
at 0:13 in the video it looks like the cursor jumped up a little when re-opening the output buffer. do you know why that's happening? |
|
Thanks for the PR, I will play with it a little bit in the coming days as welle a redo a finale review. |
When the cursor bounces back from the editing window to the opencode output window, if it wasn't at the bottom before (for example, if I was looking at messages in the middle), I want it to reappear at the corresponding position. Actually, it's not just the cursor jumping up; it seems to be one of my autojump commands that centers the entire window, although it does create a bit of visual noise. |
|
Need a more detailed performance evaluation and bottleneck analysis (in progress) |
|
I played a little bit with it this morning, It progressing great, I like that I don't have to wait for a full render if I toggle the panel. I find one issue where if you move the cursor in the outpout window withoud scrolling and close/reopen, the cursor will be set at the bottom instead of restoring to the last position. There is also some tests failiong in the branch. |
I'm sorry—I've actually been running local tests in my off hours these past few days. What you mentioned is a known issue to me, but at the same time there's a more serious problem: using end-to-end load testing I can reproduce a severe performance issue. Restoring a cached buffer requires reattaching the window and setting the filetype, which triggers Treesitter to reparse all existing content in full. That cost negates the benefits of avoiding re-rendering. I can't come up with a complete, well-reasoned conclusion right now, and I don't want to treat this project with a restless attitude. I'll need to look through the code myself again tomorrow — I can't keep relying on GPT-5.3 and Kimi2.5. At first I implemented this feature almost entirely by vibe coding; when reviewing the code I looked at the data structures and added a mock to create a performance comparison test, which deceptively showed a 40% performance improvement. But when I later compared latency end-to-end in a real opencode session (looped 100 times, comparing mean and std), the results were terrible (I really should've picked up on this script from the start). I was able to identify some plausible issues, like duplicate subscriptions (possibly a legacy problem) or treesitter doing full parses that took too long... The final optimizations weren't particularly satisfying, and since I didn't have deeper control over the code during that time, it was pretty bad. |
|
Performance testing details are as follows, along with my originally intended commit message:
However, this commit has too many changes. For maintainability, I'd like to split it into several smaller PRs. Is that possible? (I'll assume you've agreed (lol @sudo-tee I found I don't like that visual noise either, so I also added an option to control whether this thing can be turned on. @disrupted |
|
If you are able to split in multiple PR . I will agree. It will be easier |
| group = group, | ||
| buffer = windows.input_buf, | ||
| callback = function() | ||
| -- Save cursor position before leaving |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If possible, I'd like to keep comments only for explaining complex snippet.
AI tends to add comments for abvious thinks like this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
will do
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I left a couple of little comments to improve readability.
Some tests are still failing in ther branch
A part from this it seems good
| function M.create_split_windows(input_buf, output_buf) | ||
| if state.windows then | ||
| M.close_windows(state.windows) | ||
| if state.windows and state.windows.input_win and vim.api.nvim_win_is_valid(state.windows.input_win) then |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a function in each window module calles M.mounted().
You might want to leverage it instead, as it checks the same conditions as this
| return nil | ||
| end | ||
|
|
||
| local input_cursor = get_window_cursor(windows and windows.input_win) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you could simplify the nil checks with this
local input_cursor = get_window_cursor(windows and windows.input_win) or state.get_cursor_position('input')
local output_cursor = get_window_cursor(windows and windows.output_win) or state.get_cursor_position('output')| end | ||
|
|
||
| local windows_open = state.windows ~= nil | ||
| and (is_valid_win(state.windows.input_win) or is_valid_win(state.windows.output_win)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could use both input_window.mounted() and input_window.mounted() here

Background
This PR improves UI toggle behavior by introducing
ui.persist_state(default:true) and refining window and buffer lifecycle handling.Goal: when toggling UI off and on, preserve user context (buffers and cursor or scroll intent) without breaking close semantics.
Behavior Changes
Add
ui.persist_state(defaulttrue):togglenow hides windows and preserves buffers when enabled.Normalize window state semantics:
visible: input or output window is currently visiblehidden: windows are closed but restorable hidden buffers existclosed: no visible windows and no restorable hidden buffersFix toggle open-state detection:
visible(notclosed).Make
get_window_state()read-only:Restore policy after hidden reopen:
Explicit hidden-buffer lifecycle:
Type and LSP cleanup:
gdnavigation targeting real implementations.Test Coverage
Added and updated behavior-focused tests:
tests/unit/persist_state_spec.luaget_window_state()purity (no mutation)persist_state=falsefull-close pathtests/unit/core_spec.luaReviewer Guide
Suggested review order:
lua/opencode/state.lua: state model plus hidden snapshot lifecyclelua/opencode/ui/ui.lua: close and create window lifecycle splitlua/opencode/core.lua: reopen render gating behaviorlua/opencode/api.lua: toggle semantics plus getter purityRisks / Compatibility
persist_state=true).persist_state=false.visible/hidden/closed), covered by regression tests above.