Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions lua/opencode/cli/server.lua
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,19 @@ function M.get_port(launch)
local Promise = require("opencode.promise")

return Promise.new(function(resolve, reject)
-- Check if provider can supply port directly
local provider = require("opencode.config").provider
if provider and provider.get_port then
local provider_port = provider:get_port()
if provider_port then
local ok, _ = pcall(test_port, provider_port)
if ok then
resolve(provider_port)
return
end
end
end

local configured_port = require("opencode.config").opts.port
local find_port_fn = configured_port and function()
return test_port(configured_port)
Expand Down
4 changes: 4 additions & 0 deletions lua/opencode/provider/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@
---Should return `true` if the provider is available,
---else an error string and optional advice (for `vim.health.warn`).
---@field health? fun(): boolean|string, ...string|string[]
---
---Get the port of the `opencode` server started by this provider.
---Returns `nil` if the provider cannot determine the port (falls back to CWD-based discovery).
---@field get_port? fun(self: opencode.Provider): number|nil

---Configure and enable built-in providers.
---@class opencode.provider.Opts
Expand Down
5 changes: 5 additions & 0 deletions lua/opencode/provider/kitty.lua
Original file line number Diff line number Diff line change
Expand Up @@ -152,4 +152,9 @@ function Kitty:stop()
end
end

---@return number|nil
function Kitty:get_port()
return nil
end

return Kitty
5 changes: 5 additions & 0 deletions lua/opencode/provider/snacks.lua
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,9 @@ function Snacks:stop()
end
end

---@return number|nil
function Snacks:get_port()
return nil
end

return Snacks
5 changes: 5 additions & 0 deletions lua/opencode/provider/terminal.lua
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,9 @@ function Terminal:stop()
end
end

---@return number|nil
function Terminal:get_port()
return nil
end

return Terminal
38 changes: 38 additions & 0 deletions lua/opencode/provider/tmux.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
---
---The `tmux` pane ID where `opencode` is running (internal use only).
---@field pane_id? string
---
---Cached port of the `opencode` server (internal use only).
---@field port? number
local Tmux = {}
Tmux.__index = Tmux
Tmux.name = "tmux"
Expand All @@ -20,6 +23,7 @@ function Tmux.new(opts)
local self = setmetatable({}, Tmux)
self.opts = opts or {}
self.pane_id = nil
self.port = nil
return self
end

Expand Down Expand Up @@ -85,7 +89,41 @@ function Tmux:stop()
if pane_id then
vim.fn.system("tmux kill-pane -t " .. pane_id)
self.pane_id = nil
self.port = nil
end
end

---Get the PID of the shell process running in the opencode pane.
---@return number|nil pid
function Tmux:get_pane_process_pid()
local pane_id = self:get_pane_id()
if not pane_id then
return nil
end

local output = vim.fn.system("tmux list-panes -t " .. pane_id .. " -F '#{pane_pid}'")
local pid = tonumber(vim.trim(output))
return pid
end

---Get the port of the opencode server started in this pane.
---Traces from pane PID through descendants to find the listening port.
---Caches the result for subsequent calls.
---@return number|nil port
function Tmux:get_port()
-- Return cached port if pane still exists
if self.port and self:get_pane_id() then
return self.port
end

local pane_pid = self:get_pane_process_pid()
if not pane_pid then
return nil
end

local process = require("opencode.util.process")
self.port = process.get_descendant_listening_port(pane_pid, 3)
return self.port
end

return Tmux
5 changes: 5 additions & 0 deletions lua/opencode/provider/wezterm.lua
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,9 @@ function Wezterm:stop()
end
end

---@return number|nil
function Wezterm:get_port()
return nil
end

return Wezterm
60 changes: 60 additions & 0 deletions lua/opencode/util/process.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---Process utilities for provider implementations.
local M = {}

---Get all descendant PIDs of a process (children, grandchildren, etc.)
---@param pid number The parent process ID
---@param max_depth? number Maximum recursion depth (default 3)
---@return number[] pids List of descendant PIDs
function M.get_descendants(pid, max_depth)
max_depth = max_depth or 3

local function recurse(current_pid, depth)
if depth > max_depth then
return {}
end

local children = {}
local output = vim.fn.system("pgrep -P " .. current_pid .. " 2>/dev/null")
for child_pid in output:gmatch("%d+") do
local child = tonumber(child_pid)
table.insert(children, child)
for _, descendant in ipairs(recurse(child, depth + 1)) do
table.insert(children, descendant)
end
end
return children
end

return recurse(pid, 1)
end

---Find the TCP port a process is listening on.
---@param pid number The process ID
---@return number|nil port The listening port, or nil if not found
function M.get_listening_port(pid)
local lsof_output = vim.fn.system("lsof -w -iTCP -sTCP:LISTEN -P -n -a -p " .. pid .. " 2>/dev/null")
for line in lsof_output:gmatch("[^\r\n]+") do
local port = line:match(":(%d+)%s+%(LISTEN%)")
if port then
return tonumber(port)
end
end
return nil
end

---Find the listening port of any descendant of a process.
---@param pid number The ancestor process ID
---@param max_depth? number Maximum recursion depth (default 3)
---@return number|nil port The listening port, or nil if not found
function M.get_descendant_listening_port(pid, max_depth)
local descendants = M.get_descendants(pid, max_depth)
for _, desc_pid in ipairs(descendants) do
local port = M.get_listening_port(desc_pid)
if port then
return port
end
end
return nil
end

return M