Skip to content

Conversation

@sudo-tee
Copy link
Owner

@sudo-tee sudo-tee commented Dec 5, 2025

This PR is removing the :wait on the critical paths in favor of a coroutine similar to how promises work in js.

This should resolve #137 and #133

@sudo-tee
Copy link
Owner Author

sudo-tee commented Dec 5, 2025

There is still some issues, like test and context not loading properly, but it's a start

Copy link
Contributor

Copilot AI left a comment

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 improves performance by making the panel opening process asynchronous. Instead of blocking while initializing the session and server, the UI is created immediately and displays "Loading..." while initialization happens in the background via vim.schedule(). This is achieved by introducing an is_opening state flag and returning promises from the core.open() function to allow proper async flow control.

Key changes:

  • Refactored core.open() to be asynchronous using vim.schedule() and promises
  • Added is_opening state flag to track initialization status and display "Loading..." in the UI
  • Updated API functions to chain promises with :and_then() for proper async orchestration

Reviewed changes

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

File Description
lua/opencode/state.lua Adds is_opening boolean field to track panel opening state
lua/opencode/ui/topbar.lua Displays "Loading..." when is_opening is true and subscribes to state changes
lua/opencode/core.lua Refactors M.open() to be async with promises, moves session initialization into vim.schedule(), and adds error logging
lua/opencode/api.lua Updates API functions to return promises, chains core.open() with :and_then(), removes redundant local config declarations, and changes default focus from 'output' to 'input' in M.run() functions
Comments suppressed due to low confidence (1)

lua/opencode/core.lua:186

  • The send_message function doesn't return the promise from api_client:create_message(). This breaks the promise chain in API functions like M.run() and M.run_new_session() which expect to chain this with :and_then().

Add return before the state.api_client:create_message() call to properly propagate the promise:

return state.api_client
  :create_message(session_id, params)
  :and_then(function(response)
    -- ...
  end)
  :catch(function(err)
    -- ...
  end)
  state.api_client
    :create_message(session_id, params)
    :and_then(function(response)
      if not response or not response.info or not response.parts then
        -- fall back to full render. incremental render is handled
        -- event manager
        vim.notify('Invalid response from session: ' .. vim.inspect(response), vim.log.levels.ERROR)
        ui.render_output()
        return
      end

      local received_message_count = vim.deepcopy(state.user_message_count)
      received_message_count[response.info.sessionID] = (received_message_count[response.info.sessionID] ~= nil)
          and (received_message_count[response.info.sessionID] - 1)
        or 0
      state.user_message_count = received_message_count

      M.after_run(prompt)
    end)
    :catch(function(err)
      vim.notify('Error sending message to session: ' .. vim.inspect(err), vim.log.levels.ERROR)
      M.cancel()
    end)

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

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@cameronr
Copy link
Collaborator

cameronr commented Dec 5, 2025

Hmm, the UI panels open faster but then I still get a noticeable blocking pause after the panels appear:

Kapture 2025-12-04 at 20 45 58

I think the underlying issues is the Promise:wait in ensure_server:

function M.ensure_server()
if state.opencode_server and state.opencode_server:is_running() then
return state.opencode_server
end
local promise = Promise.new()
state.opencode_server = opencode_server.new()
state.opencode_server:spawn({
on_ready = function(_, base_url)
promise:resolve(state.opencode_server)
end,
on_error = function(err)
promise:reject(err)
end,
on_exit = function(exit_opts)
promise:reject('Server exited')
end,
})
return promise:wait()
end

Instead of doing :wait there, maybe we can just return the promise? Also, since ensure_server sets state.opencode_server, we don't need to return it from that function.

@sudo-tee
Copy link
Owner Author

sudo-tee commented Dec 5, 2025

Hmm, the UI panels open faster but then I still get a noticeable blocking pause after the panels appear:

Instead of doing :wait there, maybe we can just return the promise? Also, since ensure_server sets state.opencode_server, we don't need to return it from that function.

I will see what I can do. But even then until the server is loaded you cannot display most of the UI elements.

@sudo-tee sudo-tee force-pushed the perf/panel-open-speed branch from e36bb46 to 2bfcb8f Compare December 5, 2025 14:07
@sudo-tee
Copy link
Owner Author

sudo-tee commented Dec 5, 2025

@cameronr

I pushed a couple of improvements and removed the :wait()

It improved a little bit, there is no jumping of the cursor, but we still have to wait for the session to be loaded. Which in itself is un-avoidable.

Screen.Recording.2025-12-05.091554.mp4

@cameronr
Copy link
Collaborator

cameronr commented Dec 5, 2025

It's improving but I still get a UI freeze. I think the core problem is that Promse:wait uses vim.wait which blocks the neovim UI. I think it's fine if we're not able to fill in our UI elements until the opencode server starts but we shouldn't be blocking the neovim UI while we're waiting for that and I think that blocking is what makes it "feel" slow.

I added a debug.traceback() print to Promise:wait and it's called from these places when opening the panels:

              waiting
stack traceback:
	...am/Dev/neovim-dev/opencode.nvim/lua/opencode/promise.lua:173: in function <...am/Dev/neovim-dev/opencode.nvim/lua/opencode/promise.lua:162>
	[C]: in function 'pcall'
	...ev/neovim-dev/opencode.nvim/lua/opencode/config_file.lua:13: in function 'get_opencode_config'
	...ev/neovim-dev/opencode.nvim/lua/opencode/config_file.lua:76: in function 'get_opencode_agents'
	...s/cam/Dev/neovim-dev/opencode.nvim/lua/opencode/core.lua:373: in function 'ensure_current_mode'
	...s/cam/Dev/neovim-dev/opencode.nvim/lua/opencode/core.lua:84: in function <...s/cam/Dev/neovim-dev/opencode.nvim/lua/opencode/core.lua:81>
	[C]: in function 'pcall'
	...s/cam/Dev/neovim-dev/opencode.nvim/lua/opencode/core.lua:81: in function <...s/cam/Dev/neovim-dev/opencode.nvim/lua/opencode/core.lua:80>
	[C]: in function 'pcall'
	...am/Dev/neovim-dev/opencode.nvim/lua/opencode/promise.lua:81: in function 'cb'
	...am/Dev/neovim-dev/opencode.nvim/lua/opencode/promise.lua:43: in function ''
	vim/_editor.lua: in function <vim/_editor.lua:0>
              waiting
stack traceback:
	...am/Dev/neovim-dev/opencode.nvim/lua/opencode/promise.lua:173: in function <...am/Dev/neovim-dev/opencode.nvim/lua/opencode/promise.lua:162>
	[C]: in function 'pcall'
	...am/Dev/neovim-dev/opencode.nvim/lua/opencode/session.lua:45: in function 'get_all_sessions'
	...am/Dev/neovim-dev/opencode.nvim/lua/opencode/session.lua:83: in function 'get_all_workspace_sessions'
	...am/Dev/neovim-dev/opencode.nvim/lua/opencode/session.lua:109: in function 'get_last_workspace_session'
	...s/cam/Dev/neovim-dev/opencode.nvim/lua/opencode/core.lua:97: in function <...s/cam/Dev/neovim-dev/opencode.nvim/lua/opencode/core.lua:81>
	[C]: in function 'pcall'
	...s/cam/Dev/neovim-dev/opencode.nvim/lua/opencode/core.lua:81: in function <...s/cam/Dev/neovim-dev/opencode.nvim/lua/opencode/core.lua:80>
	[C]: in function 'pcall'
	...am/Dev/neovim-dev/opencode.nvim/lua/opencode/promise.lua:81: in function 'cb'
	...am/Dev/neovim-dev/opencode.nvim/lua/opencode/promise.lua:43: in function ''
	vim/_editor.lua: in function <vim/_editor.lua:0>
              waiting
stack traceback:
	...am/Dev/neovim-dev/opencode.nvim/lua/opencode/promise.lua:173: in function <...am/Dev/neovim-dev/opencode.nvim/lua/opencode/promise.lua:162>
	[C]: in function 'pcall'
	...ev/neovim-dev/opencode.nvim/lua/opencode/config_file.lua:31: in function 'get_opencode_project'
	...am/Dev/neovim-dev/opencode.nvim/lua/opencode/session.lua:9: in function 'project_id'
	...am/Dev/neovim-dev/opencode.nvim/lua/opencode/session.lua:27: in function 'get_workspace_session_path'
	...am/Dev/neovim-dev/opencode.nvim/lua/opencode/session.lua:61: in function <...am/Dev/neovim-dev/opencode.nvim/lua/opencode/session.lua:60>
	vim/shared.lua: in function 'get_all_sessions'
	...am/Dev/neovim-dev/opencode.nvim/lua/opencode/session.lua:83: in function 'get_all_workspace_sessions'
	...am/Dev/neovim-dev/opencode.nvim/lua/opencode/session.lua:109: in function 'get_last_workspace_session'
	...s/cam/Dev/neovim-dev/opencode.nvim/lua/opencode/core.lua:97: in function <...s/cam/Dev/neovim-dev/opencode.nvim/lua/opencode/core.lua:81>
	[C]: in function 'pcall'
	...s/cam/Dev/neovim-dev/opencode.nvim/lua/opencode/core.lua:81: in function <...s/cam/Dev/neovim-dev/opencode.nvim/lua/opencode/core.lua:80>
	[C]: in function 'pcall'
	...am/Dev/neovim-dev/opencode.nvim/lua/opencode/promise.lua:81: in function 'cb'
	...am/Dev/neovim-dev/opencode.nvim/lua/opencode/promise.lua:43: in function ''
	vim/_editor.lua: in function <vim/_editor.lua:0>
              waiting
stack traceback:
	...am/Dev/neovim-dev/opencode.nvim/lua/opencode/promise.lua:173: in function <...am/Dev/neovim-dev/opencode.nvim/lua/opencode/promise.lua:162>
	[C]: in function 'pcall'
	...ev/neovim-dev/opencode.nvim/lua/opencode/config_file.lua:48: in function 'get_opencode_providers'
	...ev/neovim-dev/opencode.nvim/lua/opencode/config_file.lua:61: in function 'get_model_info'
	.../Dev/neovim-dev/opencode.nvim/lua/opencode/ui/topbar.lua:19: in function 'format_token_info'
	.../Dev/neovim-dev/opencode.nvim/lua/opencode/ui/topbar.lua:105: in function <.../Dev/neovim-dev/opencode.nvim/lua/opencode/ui/topbar.lua:93>

To fully fix it, we'll have to update those places to not use wait in the common path so the nvim UI is never blocked.

@sudo-tee
Copy link
Owner Author

sudo-tee commented Dec 5, 2025

Good point, I think the "real" way of doing it would be to have a promise based on coroutines.. but it would require a bigger refactor. I will check the usages of :wait and see if it's possible to continue without blocking the ui or to manage the promise.

@sudo-tee
Copy link
Owner Author

sudo-tee commented Dec 5, 2025

@cameronr I had a look at eliminating the :wait on the critical path.

The isssue I have is that promises are spreading and need to be changed everywhere. I gave up after a couple of hours.

If you want to have a jab at it don't hesitate. I will try to find another solution to avoid calling :wait.

@cameronr
Copy link
Collaborator

cameronr commented Dec 6, 2025

Good point, I think the "real" way of doing it would be to have a promise based on coroutines.. but it would require a bigger refactor. I will check the usages of :wait and see if it's possible to continue without blocking the ui or to manage the promise.

Yes, I think you're right. I'll think about it a bit and see if I can come up with anything but I suspect it would involve refactoring most of the api to take call backs rather than executing synchronously.

@sudo-tee sudo-tee force-pushed the perf/panel-open-speed branch from 49dd82c to bc85505 Compare December 8, 2025 12:16
@sudo-tee
Copy link
Owner Author

sudo-tee commented Dec 8, 2025

@cameronr

Just so you know I'm in the middle of the process of converting every promise into a coroutine. It will take a moment, but I think it will improve and cleanup a lot of stuff.

@cameronr
Copy link
Collaborator

cameronr commented Dec 8, 2025

oh great! switching to coroutines was what i was going to explore so that sounds good!

@sudo-tee sudo-tee force-pushed the perf/panel-open-speed branch 3 times, most recently from 9a67f66 to 4c8299a Compare December 9, 2025 12:17
This uses coroutines for managing promises.

The sync :wait method is still used in some not critical parts

where it's not obvious to change to the await method
@sudo-tee sudo-tee force-pushed the perf/panel-open-speed branch from 4c8299a to 96e9f2d Compare December 9, 2025 13:13
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 32 out of 32 changed files in this pull request and generated 11 comments.


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

@sudo-tee sudo-tee force-pushed the perf/panel-open-speed branch 9 times, most recently from 0771d43 to 9a207ac Compare December 9, 2025 16:13
@sudo-tee sudo-tee force-pushed the perf/panel-open-speed branch from 9a207ac to e092046 Compare December 9, 2025 16:18
@sudo-tee sudo-tee marked this pull request as ready for review December 9, 2025 16:19
@cameronr
Copy link
Collaborator

cameronr commented Dec 9, 2025

Ooh, exciting! I'll take a look today!

@cameronr
Copy link
Collaborator

cameronr commented Dec 10, 2025

I've started looking into this and you really have made great progress!

I did notice that there's still a small delay when opening the panels and I traced it down to core.opencode_ok. It seems like the opencode app has a noticeable delay now, even when running with just --version:

time opencode --version
1.0.138
opencode --version  0.26s user 0.03s system 50% cpu 0.577 total

Rather than using vim.system():wait which, i believe, is blocking (even in a coroutine context), we could do something like:

-- this could be in Promise or util
local function system_async(cmd, opts)
  local p = Promise.new()

  vim.system(cmd, opts or {}, function(result)
    if result.code == 0 then
      p:resolve(result)
    else
      p:reject(result)
    end
  end)

  return p
end


M.opencode_ok = Promise.async(function()
  if vim.fn.executable('opencode') == 0 then
    vim.notify(
      'opencode command not found - please install and configure opencode before using this plugin',
      vim.log.levels.ERROR
    )
    return false
  end

  vim.notify('alive')
  if not state.opencode_cli_version or state.opencode_cli_version == '' then
    local result = system_async({ 'opencode', '--version' }):await()
    -- can test with something like:
    -- local result = system_async({ 'sleep', '3' }):await()
    local out = (result and result.stdout or ''):gsub('%s+$', '')
    state.opencode_cli_version = out:match('(%d+%%.%d+%%.%d+)') or out
    vim.notify('done: ' .. tostring(state.opencode_cli_version))
  end
  ...
end)

Let me know if it's ok to make that change.

I'll also keep looking at the changes.

@sudo-tee
Copy link
Owner Author

sudo-tee commented Dec 10, 2025

Great catch. Yes I am totally fine with this change

Can you run

'time opencode --version' on your system.

For me I don't see any significant delay

But the change should help anyway

@cameronr
Copy link
Collaborator

cameronr commented Dec 10, 2025

I had already included the time results in my previous comment :) Takes about 270ms:

I've started looking into this and you really have made great progress!

I did notice that there's still a small delay when opening the panels and I traced it down to core.opencode_ok. It seems like the opencode app has a noticeable delay now, even when running with just --version:

time opencode --version
1.0.138
opencode --version  0.26s user 0.03s system 50% cpu 0.577 total

I'll make that change.

Caused by vim.system():wait() delay when checking opencode version.

Fixed by adding Promise.system() which wraps vim.system in a promise
(using vim.system's callback mechanism)
Also fix type in Promise
@cameronr
Copy link
Collaborator

cameronr commented Dec 10, 2025

I added some vim.system sleeps (to both opencode_ok and OpencodeServer:spawn to make sure it's non-blocking and it looks good:

Kapture 2025-12-09 at 17 41 28

I'll review the rest of code by tomorrow night.

@sudo-tee
Copy link
Owner Author

I had already included the time results in my previous comment :) Takes about 270ms:

I've started looking into this and you really have made great progress!

I did notice that there's still a small delay when opening the panels and I traced it down to core.opencode_ok. It seems like the opencode app has a noticeable delay now, even when running with just --version:

time opencode --version
1.0.138
opencode --version  0.26s user 0.03s system 50% cpu 0.577 total

I'll make that change.

I was on mobile and didn't see it 😂

Copy link
Owner Author

Choose a reason for hiding this comment

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

This file is not used yet,

It ended up here by error, but I plan to use it in a near feature for the completion bug.

Copy link
Collaborator

@cameronr cameronr left a comment

Choose a reason for hiding this comment

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

Looks great, only noticed some minor things.

@sudo-tee sudo-tee merged commit 6094564 into main Dec 11, 2025
10 checks passed
Sign up for free to 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.

Slow to open Opencode window when session is really, really long.

3 participants