Skip to content
Draft
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
43 changes: 28 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,8 @@ require('opencode').setup({
display_model = true, -- Display model name on top winbar
display_context_size = true, -- Display context size in the footer
display_cost = true, -- Display cost in the footer
hide_single_tab = true, -- Hide the panel tab strip when only one session tab exists
notify_on_background_prompt = true, -- Notify when an unfocused session needs a question or permission response
window_highlight = 'Normal:OpencodeBackground,FloatBorder:OpencodeBorder', -- Highlight group for the opencode window
persist_state = true, -- Keep buffers when toggling/closing UI so window state restores quickly
icons = {
Expand All @@ -240,17 +242,20 @@ require('opencode').setup({
},
output = {
filetype = 'opencode_output', -- Filetype assigned to the output buffer (default: 'opencode_output')
compact_assistant_headers = false, -- 'full' (default), 'minimal' (compact if same mode), or 'hidden' (no headers for assistant)
tools = {
show_output = true, -- Show tools output [diffs, cmd output, etc.] (default: true)
show_reasoning_output = true, -- Show reasoning/thinking steps output (default: true)
use_folds = true, -- Use folds for tool output (default: true)
folding_threshold = 25, -- Number of lines to show before folding when show_output is true (default: 25)
fold_exclude = { -- Tools that should never be folded (default: sequential-thinking)
'bash', -- built-in tool name (exact match)
{ server = 'sequential-thinking', tool = 'sequentialthinking' }, -- MCP tool (server + tool match)
},
},
actions = {
open_in_new_tab = false, -- Open inline child-session and fork actions in a new panel tab
},
compact_assistant_headers = false, -- 'full' (default), 'minimal' (compact if same mode), or 'hidden' (no headers for assistant)
tools = {
show_output = true, -- Show tools output [diffs, cmd output, etc.] (default: true)
show_reasoning_output = true, -- Show reasoning/thinking steps output (default: true)
use_folds = true, -- Use folds for tool output (default: true)
folding_threshold = 25, -- Number of lines to show before folding when show_output is true (default: 25)
fold_exclude = { -- Tools that should never be folded (default: sequential-thinking)
'bash', -- built-in tool name (exact match)
{ server = 'sequential-thinking', tool = 'sequentialthinking' }, -- MCP tool (server + tool match)
},
},
rendering = {
markdown_debounce_ms = 250, -- Debounce time for markdown rendering on new data (default: 250ms)
on_data_rendered = nil, -- Called when new data is rendered; set to false to disable default RenderMarkdown/Markview behavior
Expand Down Expand Up @@ -355,7 +360,7 @@ require('opencode').setup({
hooks = {
on_file_edited = nil, -- Called after a file is edited by opencode.
on_session_loaded = nil, -- Called after a session is loaded.
on_done_thinking = nil, -- Called when opencode finishes thinking (all jobs complete).
on_done_thinking = nil, -- Called when a session becomes idle, including sessions started outside Neovim.
on_permission_requested = nil, -- Called when a permission request is issued.
},
quick_chat = {
Expand Down Expand Up @@ -624,8 +629,8 @@ There's 3 main ways on how to change the snacks picker layout
require("opencode").setup({
ui = {
picker = {
---@module "snacks"
---@type snacks.picker.layout.Config | nil
---@module "snacks"
---@type snacks.picker.layout.Config | nil
snacks_layout = {
preset = "custom_layout" -- or builtin snacks, like "select", "default", etc
},
Expand Down Expand Up @@ -653,13 +658,21 @@ There's 3 main ways on how to change the snacks picker layout

The plugin provides the following actions that can be triggered via keymaps, commands, slash commands (typed in the input window), or the Lua API:

Panel tabs are logical tabs inside the Opencode UI. They do not create or switch Neovim tabpages. Each tab keeps its own session state, input buffer, output buffer, and model/context state while reusing the current panel layout.

| Action | Default keymap | Command | API Function |
| ----------------------------------------------------------- | ------------------------------------- | ------------------------------------------- | ---------------------------------------------------------------------- |
| Open opencode. Close if opened | `<leader>og` | `:Opencode` | `require('opencode.api').toggle()` |
| Open input window (current session) | `<leader>oi` | `:Opencode open input` | `require('opencode.api').open_input()` |
| Open input window (new session) | `<leader>oI` | `:Opencode open input_new_session` | `require('opencode.api').open_input_new_session()` |
| Open a new session in a panel tab | `<leader>oN` | `:Opencode tab new [name]` | `require('opencode.api').open_session_tab([name])` |
| Select a panel tab | `<leader>o?` | `:Opencode tab select` | `require('opencode.api').select_session_tab()` |
| Select panel tab by index | `<leader>o1` ... `<leader>o9` | `:Opencode tab select [index]` | `require('opencode.api').select_session_tab(index)` |
| Switch panel tabs | `<leader>o<` / `<leader>o>` | `:Opencode tab previous` / `next` | `require('opencode.api').prev_session_tab()` / `next_session_tab()` |
| Close the current panel tab | `<leader>oQ` | `:Opencode tab close` | `require('opencode.api').close_session_tab()` |
| Open output window | `<leader>oo` | `:Opencode open output` | `require('opencode.api').open_output()` |
| Create and switch to a named session | - | `:Opencode session new <name>` | `:Opencode session new <name>` (user command) |
| Open the selected session in a new panel tab | `<C-t>` (session picker) | - | - |
| Rename current session | `<leader>oR` | `:Opencode session rename <name>` | `:Opencode session rename <name>` (user command) |
| Toggle focus opencode / last window | `<leader>ot` | `:Opencode toggle focus` | `require('opencode.api').toggle_focus()` |
| Close UI windows | `<leader>oq` | `:Opencode close` | `require('opencode.api').close()` |
Expand Down Expand Up @@ -1206,7 +1219,7 @@ You can define custom functions to be called at specific events in Opencode:

- `on_file_edited`: Called after a file is edited by Opencode.
- `on_session_loaded`: Called after a session is loaded.
- `on_done_thinking`: Called when Opencode finishes thinking (all user jobs complete).
- `on_done_thinking`: Called when a session becomes idle, including sessions started outside Neovim.
- `on_permission_requested`: Called when a permission request is issued.

```lua
Expand Down
5 changes: 5 additions & 0 deletions lua/opencode/api.lua
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ local action_groups = {
select_session = session.select_session,
compact_session = session.compact_session,
open_input_new_session_with_title = session.open_input_new_session_with_title,
open_session_tab = session.open_session_tab,
select_session_tab = session.select_session_tab,
next_session_tab = session.next_session_tab,
prev_session_tab = session.prev_session_tab,
close_session_tab = session.close_session_tab,
rename_session = session.rename_session,
undo = session.undo,
copy_message = session.copy_message,
Expand Down
130 changes: 125 additions & 5 deletions lua/opencode/commands/handlers/session.lua
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,22 @@ local M = {
actions = {},
}

local session_subcommands =
{ 'new', 'select', 'navigate', 'compact', 'share', 'unshare', 'agents_init', 'rename', 'toggle_lock' }
local session_subcommands = {
'new',
'tab',
'tabs',
'next_tab',
'prev_tab',
'close_tab',
'select',
'navigate',
'compact',
'share',
'unshare',
'agents_init',
'rename',
'toggle_lock',
}

---@param message string
local function invalid_arguments(message)
Expand Down Expand Up @@ -102,6 +116,31 @@ function M.actions.open_input_new_session_with_title(title)
end)(title)
end

---@param title? string
function M.actions.open_session_tab(title)
return session_runtime.open_session_tab(title)
end

---@param index? string|number
function M.actions.select_session_tab(index)
if index ~= nil then
return session_runtime.switch_session_tab_by_index(index)
end
return require('opencode.ui.session_tab_picker').select()
end

function M.actions.next_session_tab()
return session_runtime.cycle_session_tab(1)
end

function M.actions.prev_session_tab()
return session_runtime.cycle_session_tab(-1)
end

function M.actions.close_session_tab()
return session_runtime.close_session_tab()
end

---@param parent_id? string
---@param scope? 'project' | 'global' defaults to global when session is locked, project otherwise
function M.actions.select_session(parent_id, scope)
Expand Down Expand Up @@ -216,6 +255,9 @@ function M.actions.navigate_session_tree(direction, interaction, wrap, empty_pol
end
return
end
if interaction == 'tab' then
return session_runtime.open_session_in_tab_by_id(direction)
end
if interaction == 'picker' then
return session_runtime.select_session(direction, 'project')
end
Expand Down Expand Up @@ -597,7 +639,8 @@ function M.actions.timeline()
end

---@param message_id? string
function M.actions.fork_session(message_id)
---@param open_in_new_tab? boolean|string
function M.actions.fork_session(message_id, open_in_new_tab)
return with_active_session('No active session to fork', function(state_obj)
local target = message_id and find_message_in_state(state_obj, message_id) or find_last_user_message(state_obj)
if not target then
Expand All @@ -618,7 +661,11 @@ function M.actions.fork_session(message_id)
vim.schedule(function()
if response and response.id then
vim.notify('Session forked successfully. New session ID: ' .. response.id, vim.log.levels.INFO)
session_runtime.switch_session(response.id)
if open_in_new_tab == true or open_in_new_tab == 'tab' then
session_runtime.open_session_in_tab(response)
else
session_runtime.switch_session(response.id)
end
else
vim.notify('Session forked but no new session ID received', vim.log.levels.WARN)
end
Expand Down Expand Up @@ -651,6 +698,21 @@ local session_subcommand_actions = {
end
return M.actions.open_input_new_session()
end,
tab = function(args)
return M.actions.open_session_tab(parse_title(args, 2))
end,
tabs = function(args)
return M.actions.select_session_tab(args[2])
end,
next_tab = function()
return M.actions.next_session_tab()
end,
prev_tab = function()
return M.actions.prev_session_tab()
end,
close_tab = function()
return M.actions.close_session_tab()
end,
rename = function(args)
return M.actions.rename_session(nil, parse_title(args, 2))
end,
Expand Down Expand Up @@ -689,9 +751,43 @@ local session_subcommand_actions = {
end,
}

local tab_subcommands = { 'next', 'new', 'previous', 'select', 'close' }

---@type table<string, fun(args: string[]): any>
local tab_subcommand_actions = {
next = function()
return M.actions.next_session_tab()
end,
new = function(args)
return M.actions.open_session_tab(parse_title(args, 2))
end,
previous = function()
return M.actions.prev_session_tab()
end,
select = function(args)
return M.actions.select_session_tab(args[2])
end,
close = function()
return M.actions.close_session_tab()
end,
}

M.command_defs = {
tab = {
desc = 'Manage Opencode panel tabs',
completions = tab_subcommands,
nested_subcommand = { allow_empty = false },
execute = function(args)
local subcommand = args[1]
local action = tab_subcommand_actions[subcommand]
if not action then
invalid_arguments('Invalid tab subcommand. Use: ' .. table.concat(tab_subcommands, ', '))
end
return action(args)
end,
},
session = {
desc = 'Manage sessions (new/select/navigate/compact/share/unshare/rename/toggle_lock)',
desc = 'Manage sessions and Opencode panel tabs',
completions = session_subcommands,
nested_subcommand = { allow_empty = false },
execute = function(args)
Expand All @@ -705,6 +801,30 @@ M.command_defs = {
},
-- action name aliases for keymap compatibility
open_input_new_session = { desc = 'Open input (new session)', execute = M.actions.open_input_new_session },
open_session_tab = {
desc = 'Open a new session in an Opencode panel tab',
execute = function(args)
return M.actions.open_session_tab(parse_title(args, 1))
end,
},
select_session_tab = {
desc = 'Select an Opencode panel tab',
execute = function(args)
return M.actions.select_session_tab(args[1])
end,
},
next_session_tab = {
desc = 'Switch to the next Opencode panel tab',
execute = M.actions.next_session_tab,
},
prev_session_tab = {
desc = 'Switch to the previous Opencode panel tab',
execute = M.actions.prev_session_tab,
},
close_session_tab = {
desc = 'Close the current Opencode panel tab',
execute = M.actions.close_session_tab,
},
toggle_session_lock = {
desc = 'Toggle session lock (preserve active session across cwd changes)',
execute = function(args)
Expand Down
24 changes: 24 additions & 0 deletions lua/opencode/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,25 @@ M.defaults = {
['<leader>og'] = { 'toggle', desc = 'Toggle Opencode window' },
['<leader>oi'] = { 'open_input', desc = 'Open input window' },
['<leader>oI'] = { 'open_input_new_session', desc = 'Open input (new session)' },
['<leader>oN'] = { 'open_session_tab', desc = 'Open new Opencode session tab' },
['<leader>o<'] = { 'prev_session_tab', desc = 'Previous Opencode session tab' },
['<leader>o>'] = { 'next_session_tab', desc = 'Next Opencode session tab' },
['<leader>o?'] = { 'select_session_tab', desc = 'Select Opencode session tab' },
['<leader>o1'] = { 'select_session_tab', { 1 }, desc = 'Select Opencode session tab 1' },
['<leader>o2'] = { 'select_session_tab', { 2 }, desc = 'Select Opencode session tab 2' },
['<leader>o3'] = { 'select_session_tab', { 3 }, desc = 'Select Opencode session tab 3' },
['<leader>o4'] = { 'select_session_tab', { 4 }, desc = 'Select Opencode session tab 4' },
['<leader>o5'] = { 'select_session_tab', { 5 }, desc = 'Select Opencode session tab 5' },
['<leader>o6'] = { 'select_session_tab', { 6 }, desc = 'Select Opencode session tab 6' },
['<leader>o7'] = { 'select_session_tab', { 7 }, desc = 'Select Opencode session tab 7' },
['<leader>o8'] = { 'select_session_tab', { 8 }, desc = 'Select Opencode session tab 8' },
['<leader>o9'] = { 'select_session_tab', { 9 }, desc = 'Select Opencode session tab 9' },
['<leader>oh'] = { 'select_history', desc = 'Select from history' },
['<leader>oo'] = { 'open_output', desc = 'Open output window' },
['<leader>ot'] = { 'toggle_focus', desc = 'Toggle focus' },
['<leader>oT'] = { 'timeline', desc = 'Session timeline' },
['<leader>oq'] = { 'close', desc = 'Close Opencode window' },
['<leader>oQ'] = { 'close_session_tab', desc = 'Close current Opencode session tab' },
['<leader>os'] = { 'select_session', desc = 'Select session' },
['<leader>oS'] = { 'navigate_session_tree', { 'child', 'picker' }, desc = 'Select child session' },
['<leader>oP'] = { 'navigate_session_tree', { 'parent' }, desc = 'Go to parent session' },
Expand Down Expand Up @@ -116,9 +130,14 @@ M.defaults = {
rename_session = { '<C-r>', desc = 'Rename selected session' },
delete_session = { '<C-d>', desc = 'Delete selected sessions' },
new_session = { '<C-s>', desc = 'Create a new session' },
open_in_tab = { '<C-t>', desc = 'Open selected session in a new panel tab' },
fork_session = { '<C-f>', desc = 'Fork selected session' },
toggle_scope = { '<C-g>', desc = 'Toggle between project/global scope' },
},
session_tab_picker = {
new_tab = { '<C-s>', desc = 'Create a new panel tab' },
close_tab = { '<C-d>', desc = 'Close selected panel tab' },
},
timeline_picker = {
undo = { '<C-u>', mode = { 'i', 'n' }, desc = 'Undo to selected message' },
fork = { '<C-f>', mode = { 'i', 'n' }, desc = 'Fork from selected message' },
Expand Down Expand Up @@ -159,6 +178,8 @@ M.defaults = {
display_model = true,
display_context_size = true,
display_cost = true,
hide_single_tab = true,
notify_on_background_prompt = true,
window_highlight = 'Normal:OpencodeBackground,FloatBorder:OpencodeBorder',
persist_state = true,
icons = {
Expand All @@ -172,6 +193,9 @@ M.defaults = {
filetype = 'opencode_output',
time_format = nil,
compact_assistant_headers = false,
actions = {
open_in_new_tab = false,
},
rendering = {
markdown_debounce_ms = 250,
on_data_rendered = nil,
Expand Down
18 changes: 18 additions & 0 deletions lua/opencode/context.lua
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,24 @@ function M.get_context()
return ChatContext.context
end

---@return OpencodeContext
function M.snapshot()
return vim.deepcopy(ChatContext.context)
end

---@param snapshot OpencodeContext|nil
function M.restore(snapshot)
ChatContext.context = vim.deepcopy(snapshot or {
mentioned_files = {},
selections = {},
mentioned_subagents = {},
current_file = nil,
cursor_data = nil,
linter_errors = nil,
})
state.context.set_context_updated_at(vim.uv.now())
end

--- Formats context for main chat interface (new simplified API)
---@param prompt string The user's instruction/prompt
---@param context_config? OpencodeContextConfig Optional context config
Expand Down
Loading
Loading