@@ -10,7 +10,7 @@ local M = {
1010 actions = {},
1111}
1212
13- local session_subcommands = { ' new' , ' select' , ' child ' , ' sibling ' , ' parent ' , ' compact' , ' share' , ' unshare' , ' agents_init' , ' rename' }
13+ local session_subcommands = { ' new' , ' select' , ' navigate ' , ' compact' , ' share' , ' unshare' , ' agents_init' , ' rename' }
1414
1515--- @param message string
1616local function invalid_arguments (message )
7070--- @param request_promise Promise<any>
7171--- @param error_prefix string
7272local function run_api_action_with_checktime (request_promise , error_prefix )
73- request_promise
74- :and_then (schedule_checktime )
75- :catch (function (err )
76- notify_error (error_prefix , err )
77- end )
73+ request_promise :and_then (schedule_checktime ):catch (function (err )
74+ notify_error (error_prefix , err )
75+ end )
7876end
7977
8078function M .actions .open_input_new_session ()
@@ -100,28 +98,132 @@ function M.actions.select_session(parent_id)
10098 session_runtime .select_session (parent_id )
10199end
102100
103- function M .actions .select_child_session ()
104- local active = state .active_session
105- session_runtime .select_session (active and active .id or nil )
101+ local NAV_DIRECTIONS = { parent = true , child = true , sibling = true , forward = true , backward = true }
102+ local NAV_INTERACTION_DEFAULTS =
103+ { parent = ' direct' , child = ' picker' , sibling = ' picker' , forward = ' direct' , backward = ' direct' }
104+
105+ --- @return string direction , string interaction , boolean wrap , string empty_policy
106+ --- @diagnostic disable-next-line : missing-return-value
107+ local function normalize_navigate_args (direction , interaction , wrap , empty_policy )
108+ if not NAV_DIRECTIONS [direction ] then
109+ invalid_arguments (' Invalid direction: ' .. tostring (direction ))
110+ end
111+
112+ interaction = interaction or NAV_INTERACTION_DEFAULTS [direction ]
113+ if interaction ~= ' direct' and interaction ~= ' picker' then
114+ invalid_arguments (' Invalid interaction: ' .. tostring (interaction ))
115+ end
116+
117+ if wrap == nil then
118+ wrap = false
119+ end
120+ if type (wrap ) == ' string' then
121+ local coerced = ({ [' true' ] = true , [' false' ] = false })[wrap ]
122+ if coerced == nil then
123+ invalid_arguments (' Invalid wrap: ' .. tostring (wrap ))
124+ end
125+ wrap = coerced
126+ elseif type (wrap ) ~= ' boolean' then
127+ invalid_arguments (' Invalid wrap: ' .. tostring (wrap ))
128+ end
129+
130+ empty_policy = empty_policy or ' notify'
131+ if empty_policy ~= ' notify' and empty_policy ~= ' noop' then
132+ invalid_arguments (' Invalid empty_policy: ' .. tostring (empty_policy ))
133+ end
134+
135+ return direction , interaction , wrap , empty_policy
106136end
107137
108- function M .actions .select_sibling_session ()
109- local active = state .active_session
110- if not active or not active .parentID then
111- vim .notify (' Current session has no parent – showing root sessions' , vim .log .levels .INFO )
112- session_runtime .select_session (nil )
113- return
138+ -- parent: direct switch to parentID; child/sibling: target_id is filter, always picker
139+ local tree_directions = {
140+ parent = {
141+ get_target = function (a )
142+ return a .parentID
143+ end ,
144+ allow_direct = true ,
145+ },
146+ child = {
147+ get_target = function (a )
148+ return a .id
149+ end ,
150+ allow_direct = false ,
151+ },
152+ sibling = {
153+ get_target = function (a )
154+ return a .parentID
155+ end ,
156+ allow_direct = false ,
157+ },
158+ }
159+
160+ local function find_session_index (sessions , session_id )
161+ for i , s in ipairs (sessions ) do
162+ if s .id == session_id then
163+ return i
164+ end
114165 end
115- session_runtime . select_session ( active . parentID )
166+ return nil
116167end
117168
118- function M .actions .select_parent_session ()
169+ local function compute_target_index (current_idx , total , direction , wrap )
170+ local step = direction == ' forward' and - 1 or 1
171+ local target = current_idx + step
172+
173+ if target >= 1 and target <= total then
174+ return target
175+ end
176+ if wrap then
177+ return direction == ' forward' and total or 1
178+ end
179+ return nil
180+ end
181+
182+ function M .actions .navigate_session_tree (direction , interaction , wrap , empty_policy )
119183 local active = state .active_session
120- if not active or not active . parentID then
121- vim .notify (' Current session has no parent ' , vim .log .levels .INFO )
184+ if not active then
185+ if empty_policy == ' notify ' then vim .notify (' No active session ' , vim .log .levels .WARN ) end
122186 return
123187 end
124- session_runtime .switch_session (active .parentID )
188+
189+ local dir = tree_directions [direction ]
190+ if dir then
191+ local target_id = dir .get_target (active )
192+ if not target_id then
193+ if direction == ' sibling' then return session_runtime .select_session (nil ) end
194+ if empty_policy == ' notify' then vim .notify (' No ' .. direction , vim .log .levels .INFO ) end
195+ return
196+ end
197+ if interaction == ' picker' or not dir .allow_direct then
198+ return session_runtime .select_session (target_id )
199+ end
200+ return session_runtime .switch_session (target_id )
201+ end
202+
203+ -- forward / backward: flat navigation by time.updated
204+ return Promise .async (function ()
205+ local all_sessions = session_store .get_all_workspace_sessions ():await ()
206+ if not all_sessions or # all_sessions == 0 then
207+ if empty_policy == ' notify' then vim .notify (' No sessions' , vim .log .levels .INFO ) end
208+ return
209+ end
210+
211+ local current_idx = find_session_index (all_sessions , active .id )
212+ if not current_idx then
213+ if empty_policy == ' notify' then vim .notify (' Session not in list' , vim .log .levels .INFO ) end
214+ return
215+ end
216+
217+ local target_idx = compute_target_index (current_idx , # all_sessions , direction , wrap )
218+ if not target_idx then
219+ if empty_policy == ' notify' then
220+ vim .notify (' At ' .. (direction == ' forward' and ' newest' or ' oldest' ) .. ' session' , vim .log .levels .INFO )
221+ end
222+ return
223+ end
224+
225+ return session_runtime .switch_session (all_sessions [target_idx ].id )
226+ end )()
125227end
126228
127229--- @param current_session ? Session
324426function M .actions .redo ()
325427 return with_active_session (' No active session to redo' , function (state_obj )
326428 local active_session = state_obj .active_session
429+ --- @diagnostic disable-next-line : need-check-nil
327430 if not active_session .revert or active_session .revert .messageID == ' ' then
328431 vim .notify (' Nothing to redo' , vim .log .levels .WARN )
329432 return
@@ -335,11 +438,16 @@ function M.actions.redo()
335438
336439 local next_message_id = find_next_message_for_redo (state_obj )
337440 if not next_message_id then
338- run_api_action_with_checktime (state_obj .api_client :unrevert_messages (active_session .id ), ' Failed to redo message: ' )
441+ --- @diagnostic disable-next-line : need-check-nil
442+ run_api_action_with_checktime (
443+ state_obj .api_client :unrevert_messages (active_session .id ),
444+ ' Failed to redo message: '
445+ )
339446 return
340447 end
341448
342449 run_api_action_with_checktime (
450+ --- @diagnostic disable-next-line : need-check-nil
343451 state_obj .api_client :revert_message (active_session .id , {
344452 messageID = next_message_id ,
345453 }),
@@ -427,14 +535,9 @@ local session_subcommand_actions = {
427535 select = function ()
428536 return M .actions .select_session ()
429537 end ,
430- child = function ()
431- return M .actions .select_child_session ()
432- end ,
433- sibling = function ()
434- return M .actions .select_sibling_session ()
435- end ,
436- parent = function ()
437- return M .actions .select_parent_session ()
538+ navigate = function (args )
539+ local direction , interaction , wrap , empty_policy = normalize_navigate_args (args [2 ], args [3 ], args [4 ], args [5 ])
540+ return M .actions .navigate_session_tree (direction , interaction , wrap , empty_policy )
438541 end ,
439542 compact = function ()
440543 return M .actions .compact_session ()
@@ -452,7 +555,7 @@ local session_subcommand_actions = {
452555
453556M .command_defs = {
454557 session = {
455- desc = ' Manage sessions (new/select/child /compact/share/unshare/rename)' ,
558+ desc = ' Manage sessions (new/select/navigate /compact/share/unshare/rename)' ,
456559 completions = session_subcommands ,
457560 nested_subcommand = { allow_empty = false },
458561 execute = function (args )
@@ -466,11 +569,25 @@ M.command_defs = {
466569 },
467570 -- action name aliases for keymap compatibility
468571 open_input_new_session = { desc = ' Open input (new session)' , execute = M .actions .open_input_new_session },
469- select_session = { desc = ' Select session' , execute = function () return M .actions .select_session () end },
470- select_child_session = { desc = ' Select child session' , execute = M .actions .select_child_session },
471- select_sibling_session = { desc = ' Select sibling session' , execute = M .actions .select_sibling_session },
472- select_parent_session = { desc = ' Go to parent session' , execute = M .actions .select_parent_session },
473- rename_session = { desc = ' Rename session' , execute = function (args ) return M .actions .rename_session (nil , args [1 ]) end },
572+ select_session = {
573+ desc = ' Select session' ,
574+ execute = function ()
575+ return M .actions .select_session ()
576+ end ,
577+ },
578+ navigate_session_tree = {
579+ desc = ' Navigate session tree (parent/child/sibling/forward/backward)' ,
580+ execute = function (args )
581+ local direction , interaction , wrap , empty_policy = normalize_navigate_args (args [1 ], args [2 ], args [3 ], args [4 ])
582+ return M .actions .navigate_session_tree (direction , interaction , wrap , empty_policy )
583+ end ,
584+ },
585+ rename_session = {
586+ desc = ' Rename session' ,
587+ execute = function (args )
588+ return M .actions .rename_session (nil , args [1 ])
589+ end ,
590+ },
474591 undo = {
475592 desc = ' Undo last action' ,
476593 execute = function (args )
0 commit comments