From c551786359fc75ffe2828cab0a5fcca1ac05d806 Mon Sep 17 00:00:00 2001 From: jensenojs Date: Tue, 5 May 2026 22:10:48 +0800 Subject: [PATCH 1/3] feat(session): allow session ID as direction in navigate_session_tree When direction is not a known tree direction, treat it as a session ID and switch directly. Task action now passes metadata.sessionId so S key opens the exact child session. --- lua/opencode/commands/handlers/session.lua | 18 +++++++++++++++++- lua/opencode/ui/formatter/tools/task.lua | 18 ++++++++++-------- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/lua/opencode/commands/handlers/session.lua b/lua/opencode/commands/handlers/session.lua index 6c30d11a..4c4d9230 100644 --- a/lua/opencode/commands/handlers/session.lua +++ b/lua/opencode/commands/handlers/session.lua @@ -186,6 +186,18 @@ function M.actions.navigate_session_tree(direction, interaction, wrap, empty_pol return end + -- If direction is not a known navigation direction, treat it as a target session ID + if direction + and not tree_directions[direction] + and direction ~= 'forward' + and direction ~= 'backward' + then + if interaction == 'picker' then + return session_runtime.select_session(direction) + end + return session_runtime.switch_session(direction) + end + local dir = tree_directions[direction] if dir then local target_id = dir.get_target(active) @@ -576,8 +588,12 @@ M.command_defs = { end, }, navigate_session_tree = { - desc = 'Navigate session tree (parent/child/sibling/forward/backward)', + desc = 'Navigate session tree (parent/child/sibling/forward/backward) or switch to a session by ID', execute = function(args) + -- If args[1] is not a known direction, treat it as a session ID + if args[1] and not NAV_DIRECTIONS[args[1]] then + return M.actions.navigate_session_tree(args[1], args[2], args[3], args[4]) + end local direction, interaction, wrap, empty_policy = normalize_navigate_args(args[1], args[2], args[3], args[4]) return M.actions.navigate_session_tree(direction, interaction, wrap, empty_policy) end, diff --git a/lua/opencode/ui/formatter/tools/task.lua b/lua/opencode/ui/formatter/tools/task.lua index 3e852f40..8bb8c8d0 100644 --- a/lua/opencode/ui/formatter/tools/task.lua +++ b/lua/opencode/ui/formatter/tools/task.lua @@ -77,14 +77,16 @@ function M.format(output, part, get_child_parts) end local end_line = output:get_line_count() - output:add_action({ - text = '[S]elect Child Session', - type = 'navigate_session_tree', - args = { 'child', 'picker' }, - key = 'S', - display_line = start_line, - range = { from = start_line + 1, to = end_line + 1 }, - }) + if metadata.sessionId then + output:add_action({ + text = '[S] Open this Session', + type = 'navigate_session_tree', + args = { metadata.sessionId }, + key = 'S', + display_line = start_line, + range = { from = start_line + 1, to = end_line + 1 }, + }) + end end ---@param _ OpencodeMessagePart From fad6fe535cfd39c6a018bd888dd68b106dd932fe Mon Sep 17 00:00:00 2001 From: jensenojs Date: Tue, 5 May 2026 22:27:19 +0800 Subject: [PATCH 2/3] fix(session): validate session ID path and update tests Move session ID detection above tree-navigation active check so invalid inputs get clear feedback via empty_policy=notify. Update tests to match new behavior. --- lua/opencode/commands/handlers/session.lua | 22 +++++++++++++++------- tests/data/explore.expected.json | 4 ++-- tests/unit/commands_handlers_spec.lua | 12 ++++++++---- tests/unit/formatter_spec.lua | 4 ++-- 4 files changed, 27 insertions(+), 15 deletions(-) diff --git a/lua/opencode/commands/handlers/session.lua b/lua/opencode/commands/handlers/session.lua index 4c4d9230..2fc19c42 100644 --- a/lua/opencode/commands/handlers/session.lua +++ b/lua/opencode/commands/handlers/session.lua @@ -180,24 +180,32 @@ local function compute_target_index(current_idx, total, direction, wrap) end function M.actions.navigate_session_tree(direction, interaction, wrap, empty_policy) - local active = state.active_session - if not active then - if empty_policy == 'notify' then vim.notify('No active session', vim.log.levels.WARN) end - return - end - - -- If direction is not a known navigation direction, treat it as a target session ID + -- If direction is not a known navigation direction, treat it as a target session ID. + -- This path runs before the tree lookup so invalid inputs get clear feedback. if direction and not tree_directions[direction] and direction ~= 'forward' and direction ~= 'backward' then + empty_policy = empty_policy or 'notify' + if not state.active_session then + if empty_policy == 'notify' then + vim.notify('No active session to navigate from', vim.log.levels.WARN) + end + return + end if interaction == 'picker' then return session_runtime.select_session(direction) end return session_runtime.switch_session(direction) end + local active = state.active_session + if not active then + if empty_policy == 'notify' then vim.notify('No active session', vim.log.levels.WARN) end + return + end + local dir = tree_directions[direction] if dir then local target_id = dir.get_target(active) diff --git a/tests/data/explore.expected.json b/tests/data/explore.expected.json index 37f5f02e..d1c43dc7 100644 --- a/tests/data/explore.expected.json +++ b/tests/data/explore.expected.json @@ -1188,10 +1188,10 @@ ], "actions": [ { - "args": ["child", "picker"], + "args": ["ses_341f3e676ffez6WUF6zpok7dUZ"], "display_line": 9, "type": "navigate_session_tree", - "text": "[S]elect Child Session", + "text": "[S] Open this Session", "range": { "from": 10, "to": 79 }, "key": "S" } diff --git a/tests/unit/commands_handlers_spec.lua b/tests/unit/commands_handlers_spec.lua index 3fb4fbf7..b7a2f4dc 100644 --- a/tests/unit/commands_handlers_spec.lua +++ b/tests/unit/commands_handlers_spec.lua @@ -517,11 +517,15 @@ describe('opencode.commands.handlers', function() end) -- normalize_navigate_args tests via command_defs - it('normalize_navigate_args rejects invalid direction', function() + it('non-direction string is treated as session ID, notifies when no active session', function() local session_handler = require('opencode.commands.handlers.session') - local ok, err = pcall(session_handler.command_defs.navigate_session_tree.execute, { 'up' }) - assert.is_false(ok) - assert.equal('invalid_arguments', err.code) + local state = require('opencode.state') + local notify_stub = stub(vim, 'notify') + state.session.clear_active() + local ok = pcall(session_handler.command_defs.navigate_session_tree.execute, { 'up' }) + assert.is_true(ok) + assert.stub(notify_stub).was_called() + notify_stub:revert() end) it('normalize_navigate_args rejects invalid interaction', function() diff --git a/tests/unit/formatter_spec.lua b/tests/unit/formatter_spec.lua index bf91a093..830db8c9 100644 --- a/tests/unit/formatter_spec.lua +++ b/tests/unit/formatter_spec.lua @@ -585,9 +585,9 @@ describe('formatter', function() end) assert.are.same({ - text = '[S]elect Child Session', + text = '[S] Open this Session', type = 'navigate_session_tree', - args = { 'child', 'picker' }, + args = { 'ses_child' }, key = 'S', display_line = 1, range = { from = 2, to = 5 }, From b2ee50e868520b8659394d3cdd327f0abc19d1de Mon Sep 17 00:00:00 2001 From: jensenojs Date: Wed, 6 May 2026 23:09:54 +0800 Subject: [PATCH 3/3] remove unnessary comment --- lua/opencode/commands/handlers/session.lua | 29 ++++++++++++---------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/lua/opencode/commands/handlers/session.lua b/lua/opencode/commands/handlers/session.lua index 2fc19c42..34e38754 100644 --- a/lua/opencode/commands/handlers/session.lua +++ b/lua/opencode/commands/handlers/session.lua @@ -180,13 +180,7 @@ local function compute_target_index(current_idx, total, direction, wrap) end function M.actions.navigate_session_tree(direction, interaction, wrap, empty_policy) - -- If direction is not a known navigation direction, treat it as a target session ID. - -- This path runs before the tree lookup so invalid inputs get clear feedback. - if direction - and not tree_directions[direction] - and direction ~= 'forward' - and direction ~= 'backward' - then + if direction and not tree_directions[direction] and direction ~= 'forward' and direction ~= 'backward' then empty_policy = empty_policy or 'notify' if not state.active_session then if empty_policy == 'notify' then @@ -202,7 +196,9 @@ function M.actions.navigate_session_tree(direction, interaction, wrap, empty_pol local active = state.active_session if not active then - if empty_policy == 'notify' then vim.notify('No active session', vim.log.levels.WARN) end + if empty_policy == 'notify' then + vim.notify('No active session', vim.log.levels.WARN) + end return end @@ -210,8 +206,12 @@ function M.actions.navigate_session_tree(direction, interaction, wrap, empty_pol if dir then local target_id = dir.get_target(active) if not target_id then - if direction == 'sibling' then return session_runtime.select_session(nil) end - if empty_policy == 'notify' then vim.notify('No ' .. direction, vim.log.levels.INFO) end + if direction == 'sibling' then + return session_runtime.select_session(nil) + end + if empty_policy == 'notify' then + vim.notify('No ' .. direction, vim.log.levels.INFO) + end return end if interaction == 'picker' or not dir.allow_direct then @@ -224,13 +224,17 @@ function M.actions.navigate_session_tree(direction, interaction, wrap, empty_pol return Promise.async(function() local all_sessions = session_store.get_all_workspace_sessions():await() if not all_sessions or #all_sessions == 0 then - if empty_policy == 'notify' then vim.notify('No sessions', vim.log.levels.INFO) end + if empty_policy == 'notify' then + vim.notify('No sessions', vim.log.levels.INFO) + end return end local current_idx = find_session_index(all_sessions, active.id) if not current_idx then - if empty_policy == 'notify' then vim.notify('Session not in list', vim.log.levels.INFO) end + if empty_policy == 'notify' then + vim.notify('Session not in list', vim.log.levels.INFO) + end return end @@ -598,7 +602,6 @@ M.command_defs = { navigate_session_tree = { desc = 'Navigate session tree (parent/child/sibling/forward/backward) or switch to a session by ID', execute = function(args) - -- If args[1] is not a known direction, treat it as a session ID if args[1] and not NAV_DIRECTIONS[args[1]] then return M.actions.navigate_session_tree(args[1], args[2], args[3], args[4]) end