Skip to content
Merged
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
66 changes: 66 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,50 @@ Available icon keys (see implementation at lua/opencode/ui/icons.lua lines 7-29)
- `persist_state = true` (default): `toggle()` hides/restores the UI and keeps buffers/session view in memory for fast restore.
- `persist_state = false`: `toggle()` fully tears down UI buffers and recreates them on next open.

Directory-bound sessions can be opened in logical panel tabs without changing
Neovim's cwd:

```lua
require('opencode.api').open_session({
directory = '/path/to/repo/.worktrees/feature',
new = true,
title = 'Feature work',
})
```

Returns a Promise resolving to the session; failures reject. The directory must
exist locally (normal path mapping still applies). Omit `new` to reopen the most
recent root session created in that exact directory, or create one if absent.
Pass `session_id` to open a specific session; `new` and `session_id` are mutually
exclusive. Already-open sessions reuse their logical tab. Completion and new
sessions use the bound directory, including after tab switches. Explicit
directory changes still follow `lock_session_to_directory`; locked bound tabs
keep their directory.

For worktree or other per-project workflows, `lock_session_to_directory` also
accepts a function. It is called on each directory change with
`{ from, to, session }` and returns `true` to keep the active session or `false`
to load the target directory's last session. `from` is the bound directory (or
the directory the session was loaded from) and `to` is the new directory. An
error in the function is reported and falls back to following the cwd. Manual
lock toggles still override the policy per logical tab.

```lua
require('opencode').setup({
lock_session_to_directory = function(change)
local function common_dir(dir)
local r = vim.system({ 'git', '-C', dir, 'rev-parse', '--path-format=absolute', '--git-common-dir' }, { text = true }):wait()
return r.code == 0 and vim.trim(r.stdout) or nil
end
local repo = common_dir(change.from)
return repo ~= nil and repo == common_dir(change.to)
end,
})
```

That example keeps a session across the main checkout and linked worktrees of
the same repository.

Related APIs:

- `require('opencode.api').toggle()` follows the `persist_state` behavior above.
Expand Down Expand Up @@ -1148,6 +1192,28 @@ See the [Opencode Skills Documentation](https://opencode.ai/docs/skills/) for ho

You can run predefined user commands and built-in slash commands from the input window by typing `/`. This opens a command picker where you can select a command to execute. The output of the command will be included in your prompt context.

Local Lua commands can be registered without modifying internal tables:

```lua
require('opencode').register_slash_command({
name = 'worktree',
desc = 'Open a worktree session',
args = true,
fn = function(args)
-- args is a string array, empty when no arguments were supplied.
print(vim.inspect(args))
end,
})
```

Registration works before or after `setup`. Commands appear in completion and
the picker, and execute through command lifecycle hooks (filter by `/worktree`).
Names use letters, digits, underscores, or hyphens, without a leading slash.
Builtin and registered name collisions throw; local commands take precedence
over same-named server commands and skills. Use
`require('opencode').unregister_slash_command('worktree')` to remove one (returns
whether it existed). Callback errors use the normal command error handling.

**Built-in slash commands** include:

- `/share` — Share the current session and get a link
Expand Down
1 change: 1 addition & 0 deletions lua/opencode/api.lua
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ local action_groups = {
},

session = {
open_session = session.open_session,
open_input_new_session = session.open_input_new_session,
navigate_session_tree = session.navigate_session_tree,
share = session.share,
Expand Down
2 changes: 2 additions & 0 deletions lua/opencode/commands/handlers/session.lua
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ local function run_api_action_with_checktime(request_promise, error_prefix, on_s
end)
end

M.actions.open_session = session_runtime.open_session

function M.actions.open_input_new_session()
return session_runtime.open({ new_session = true, focus = 'input', start_insert = true })
end
Expand Down
55 changes: 38 additions & 17 deletions lua/opencode/commands/slash.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ local config_file = require('opencode.config_file')
local commands = require('opencode.commands')
local log = require('opencode.log')
local slash_commands = require('opencode.slash_commands')
local slash_registry = require('opencode.services.slash_registry')

local M = {}

Expand Down Expand Up @@ -76,6 +77,11 @@ local function to_runtime_slash_command(slash_cmd, def)
end

function M.execute_builtin(slash_cmd, args)
local custom = slash_registry.list()[slash_cmd:sub(2)]
if custom then
local parsed = commands.build_parsed_intent(slash_cmd, args or {})
return commands.execute_parsed_intent(parsed, custom.fn)
end
local def = M.get_builtin_command_definitions()[slash_cmd]
if not def then
return
Expand All @@ -96,17 +102,30 @@ M.get_commands = Promise.async(function()
end

local user_commands = config_file.get_user_commands():await()
local custom_commands = slash_registry.list()
for name, def in pairs(custom_commands) do
table.insert(result, {
slash_cmd = '/' .. name,
desc = def.desc,
args = def.args == true,
fn = function(args)
return M.execute_builtin('/' .. name, args)
end,
})
end
if user_commands then
for name, def in pairs(user_commands) do
table.insert(result, {
slash_cmd = '/' .. name,
desc = def.description or 'User command',
fn = function(args)
local cmd_args = vim.list_extend({ name }, args or {})
return dispatch_parsed('command', cmd_args)
end,
args = true,
})
if not custom_commands[name] then
table.insert(result, {
slash_cmd = '/' .. name,
desc = def.description or 'User command',
fn = function(args)
local cmd_args = vim.list_extend({ name }, args or {})
return dispatch_parsed('command', cmd_args)
end,
args = true,
})
end
end
end

Expand All @@ -125,14 +144,16 @@ M.get_commands = Promise.async(function()
end)
if ok and skills then
for _, skill in ipairs(skills) do
table.insert(result, {
slash_cmd = '/' .. skill.name,
desc = skill.description or 'Skill',
fn = function(args)
return dispatch_parsed('skill', vim.list_extend({ skill.name }, args or {}))
end,
args = true,
})
if not custom_commands[skill.name] then
table.insert(result, {
slash_cmd = '/' .. skill.name,
desc = skill.description or 'Skill',
fn = function(args)
return dispatch_parsed('skill', vim.list_extend({ skill.name }, args or {}))
end,
args = true,
})
end
end
end

Expand Down
4 changes: 4 additions & 0 deletions lua/opencode/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ local setup_done = false
local state

local session_runtime = require('opencode.services.session_runtime')
local slash_registry = require('opencode.services.slash_registry')

M.register_slash_command = slash_registry.register
M.unregister_slash_command = slash_registry.unregister

local function on_opencode_server()
require('opencode.ui.permission_window').clear_all()
Expand Down
Loading
Loading