Skip to content

Commit 16a03d2

Browse files
committed
feat(session_picker): add rename session action <C-r>
1 parent ac95f5e commit 16a03d2

File tree

11 files changed

+142
-55
lines changed

11 files changed

+142
-55
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,9 @@ require('opencode').setup({
157157
deny = 'd', -- Deny permission request once (only available when there is a pending permission request)
158158
},
159159
session_picker = {
160+
rename_session = { '<C-r>' }, -- Rename selected session in the session picker
160161
delete_session = { '<C-d>' }, -- Delete selected session in the session picker
162+
new_session = { '<C-n>' }, -- Create and switch to a new session in the session picker
161163
},
162164
timeline_picker = {
163165
undo = { '<C-u>', mode = { 'i', 'n' } }, -- Undo to selected message in timeline picker

lua/opencode/config.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ M.defaults = {
7373
deny = 'd',
7474
},
7575
session_picker = {
76+
rename_session = { '<C-r>' },
7677
delete_session = { '<C-d>' },
7778
new_session = { '<C-n>' },
7879
},

lua/opencode/core.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ M._abort_count = 0
1515
function M.select_session(parent_id)
1616
local all_sessions = session.get_all_workspace_sessions() or {}
1717
local filtered_sessions = vim.tbl_filter(function(s)
18-
return s.description ~= '' and s ~= nil and s.parentID == parent_id
18+
return s.title ~= '' and s ~= nil and s.parentID == parent_id
1919
end, all_sessions)
2020

2121
ui.select_session(filtered_sessions, function(selected_session)

lua/opencode/promise.lua

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,4 +194,20 @@ function Promise:is_rejected()
194194
return self._resolved and self._error ~= nil
195195
end
196196

197+
function Promise.is_promise(obj)
198+
return type(obj) == 'table' and type(obj.and_then) == 'function' and type(obj.catch) == 'function'
199+
end
200+
201+
---@generic T
202+
---@param obj T | Promise<T>
203+
---@return Promise<T>
204+
function Promise.wrap(obj)
205+
if Promise.is_promise(obj) then
206+
---@cast obj Promise<T>
207+
return obj
208+
else
209+
return Promise.new():resolve(obj)
210+
end
211+
end
212+
197213
return Promise

lua/opencode/session.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ function M.create_session_object(session_json)
6262
local storage_path = M.get_storage_path()
6363
return {
6464
workspace = session_json.directory,
65-
description = session_json.title or '',
65+
title = session_json.title or '',
6666
modified = session_json.time and session_json.time.updated or os.time(),
6767
id = session_json.id,
6868
parentID = session_json.parentID,

lua/opencode/types.lua

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040

4141
---@class Session
4242
---@field workspace string
43-
---@field description string
43+
---@field title string
4444
---@field modified number
4545
---@field id string
4646
---@field parentID string|nil
@@ -77,6 +77,7 @@
7777
---@class OpencodeSessionPickerKeymap
7878
---@field delete_session OpencodeKeymapEntry
7979
---@field new_session OpencodeKeymapEntry
80+
---@field rename_session OpencodeKeymapEntry
8081

8182
---@class OpencodeTimelinePickerKeymap
8283
---@field undo OpencodeKeymapEntry

lua/opencode/ui/base_picker.lua

Lines changed: 66 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
local config = require('opencode.config')
22
local util = require('opencode.util')
3+
local Promise = require('opencode.promise')
34

45
---@class PickerAction
56
---@field key? OpencodeKeymapEntry|string The key binding for this action
67
---@field label string The display label for this action
7-
---@field fn fun(selected: any, opts: PickerOptions): any[]? The action function
8+
---@field fn fun(selected: any, opts: PickerOptions): any[]|Promise<any[]>? The action function
89
---@field reload? boolean Whether to reload the picker after action
910

1011
---@class PickerOptions
@@ -24,7 +25,7 @@ local util = require('opencode.util')
2425
---@field fn_fzf_index fun(line: string): integer?
2526

2627
---@class FzfAction
27-
---@field fn fun(selected: string[], fzf_opts: FzfLuaOptions): nil
28+
---@field fn fun(selected: string[], fzf_opts: FzfLuaOptions): nil|Promise<nil>
2829
---@field header string
2930
---@field reload boolean
3031

@@ -127,10 +128,12 @@ local function telescope_ui(opts)
127128
local selection = action_state.get_selected_entry()
128129
if selection then
129130
local new_items = action.fn(selection.value, opts)
130-
if action.reload and new_items then
131-
opts.items = new_items
132-
refresh_picker()
133-
end
131+
Promise.wrap(new_items):and_then(function(resolved_items)
132+
if action.reload and resolved_items then
133+
opts.items = resolved_items
134+
refresh_picker()
135+
end
136+
end)
134137
end
135138
end
136139

@@ -152,6 +155,39 @@ end
152155
local function fzf_ui(opts)
153156
local fzf_lua = require('fzf-lua')
154157

158+
local function create_fzf_config()
159+
return {
160+
winopts = opts.width and {
161+
width = opts.width + 8, -- extra space for fzf UI
162+
} or nil,
163+
fzf_opts = { ['--prompt'] = opts.title .. ' > ' },
164+
_headers = { 'actions' },
165+
fn_fzf_index = function(line)
166+
for i, item in ipairs(opts.items) do
167+
if opts.format_fn(item):to_string() == line then
168+
return i
169+
end
170+
end
171+
return nil
172+
end,
173+
}
174+
end
175+
176+
local function create_finder()
177+
return function(fzf_cb)
178+
for _, item in ipairs(opts.items) do
179+
fzf_cb(opts.format_fn(item):to_string())
180+
end
181+
fzf_cb()
182+
end
183+
end
184+
185+
local function refresh_fzf()
186+
vim.schedule(function()
187+
fzf_ui(opts)
188+
end)
189+
end
190+
155191
---@type FzfLuaActions
156192
local actions_config = {
157193
['default'] = function(selected, fzf_opts)
@@ -176,9 +212,13 @@ local function fzf_ui(opts)
176212
local idx = fzf_opts.fn_fzf_index(selected[1] --[[@as string]])
177213
if idx and opts.items[idx] then
178214
local new_items = action.fn(opts.items[idx], opts)
179-
if action.reload and new_items then
180-
opts.items = new_items
181-
end
215+
Promise.wrap(new_items):and_then(function(resolved_items)
216+
if action.reload and resolved_items then
217+
---@cast resolved_items any[]
218+
opts.items = resolved_items
219+
refresh_fzf()
220+
end
221+
end)
182222
end
183223
end,
184224
header = action.label,
@@ -187,27 +227,10 @@ local function fzf_ui(opts)
187227
end
188228
end
189229

190-
fzf_lua.fzf_exec(function(fzf_cb)
191-
for _, item in ipairs(opts.items) do
192-
fzf_cb(opts.format_fn(item):to_string())
193-
end
194-
fzf_cb()
195-
end, {
196-
winopts = opts.width and {
197-
width = opts.width + 8, -- extra space for fzf UI
198-
} or nil,
199-
fzf_opts = { ['--prompt'] = opts.title .. ' > ' },
200-
_headers = { 'actions' },
201-
actions = actions_config,
202-
fn_fzf_index = function(line)
203-
for i, item in ipairs(opts.items) do
204-
if opts.format_fn(item):to_string() == line then
205-
return i
206-
end
207-
end
208-
return nil
209-
end,
210-
})
230+
local fzf_config = create_fzf_config()
231+
fzf_config.actions = actions_config
232+
233+
fzf_lua.fzf_exec(create_finder(), fzf_config)
211234
end
212235

213236
---Mini.pick UI implementation
@@ -230,15 +253,14 @@ local function mini_pick_ui(opts)
230253
local selected = mini_pick.get_picker_matches().current
231254
if selected and selected.item then
232255
local new_items = action.fn(selected.item, opts)
233-
if action.reload and new_items then
234-
opts.items = new_items
235-
---@type MiniPickItem[]
236-
items = vim.tbl_map(function(it)
237-
return { text = opts.format_fn(it):to_string(), item = it }
238-
end, opts.items)
239-
mini_pick.set_picker_items(items)
240-
end
256+
Promise.wrap(new_items):and_then(function(resolved_items)
257+
if action.reload and resolved_items then
258+
opts.items = resolved_items
259+
mini_pick_ui(opts)
260+
end
261+
end)
241262
end
263+
return true
242264
end,
243265
}
244266
end
@@ -318,10 +340,12 @@ local function snacks_picker_ui(opts)
318340
if item then
319341
vim.schedule(function()
320342
local new_items = action.fn(item, opts)
321-
if action.reload and new_items then
322-
opts.items = new_items
323-
_picker:find()
324-
end
343+
Promise.wrap(new_items):and_then(function(resolved_items)
344+
if action.reload and resolved_items then
345+
opts.items = resolved_items
346+
_picker:find()
347+
end
348+
end)
325349
end)
326350
end
327351
end

lua/opencode/ui/session_picker.lua

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,49 @@
11
local M = {}
22
local config = require('opencode.config')
33
local base_picker = require('opencode.ui.base_picker')
4+
local util = require('opencode.util')
45

56
---Format session parts for session picker
67
---@param session Session object
78
---@return PickerItem
89
function format_session_item(session, width)
910
local debug_text = 'ID: ' .. (session.id or 'N/A')
10-
return base_picker.create_picker_item(session.description, session.modified, debug_text, width)
11+
return base_picker.create_picker_item(session.title, session.modified, debug_text, width)
1112
end
1213

1314
function M.pick(sessions, callback)
1415
local actions = {
16+
rename = {
17+
key = config.keymap.session_picker.rename_session,
18+
label = 'rename',
19+
fn = function(selected, opts)
20+
local promise = require('opencode.promise').new()
21+
local state = require('opencode.state')
22+
23+
vim.schedule(function()
24+
vim.ui.input({ prompt = 'New session name: ', default = selected.title or '' }, function(input)
25+
if input and input ~= '' then
26+
state.api_client:update_session(selected.id, { title = input }):catch(function(err)
27+
vim.schedule(function()
28+
vim.notify('Failed to rename session: ' .. vim.inspect(err), vim.log.levels.ERROR)
29+
end)
30+
end)
31+
selected.title = input
32+
local idx = util.find_index_of(opts.items, function(item)
33+
return item.id == selected.id
34+
end)
35+
36+
if idx > 0 then
37+
opts.items[idx].title = input
38+
end
39+
end
40+
promise:resolve(opts.items)
41+
end)
42+
end)
43+
return promise
44+
end,
45+
reload = true,
46+
},
1547
delete = {
1648
key = config.keymap.session_picker.delete_session,
1749
label = 'delete',
@@ -31,7 +63,9 @@ function M.pick(sessions, callback)
3163
end)
3264
end)
3365

34-
local idx = vim.fn.index(opts.items, selected) + 1
66+
local idx = util.find_index_of(opts.items, function(item)
67+
return item.id == selected.id
68+
end)
3569
if idx > 0 then
3670
table.remove(opts.items, idx)
3771
end

lua/opencode/ui/topbar.lua

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ local function get_session_desc()
8080

8181
if state.active_session then
8282
local session = require('opencode.session').get_by_id(state.active_session.id)
83-
if session and session.description ~= '' then
84-
session_desc = session.description
83+
if session and session.title ~= '' then
84+
session_desc = session.title
8585
end
8686
end
8787

lua/opencode/util.lua

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,15 @@ function M.index_of(tbl, value)
111111
return nil
112112
end
113113

114+
function M.find_index_of(tbl, predicate)
115+
for i, v in ipairs(tbl) do
116+
if predicate(v) then
117+
return i
118+
end
119+
end
120+
return nil
121+
end
122+
114123
local _is_git_project = nil
115124
function M.is_git_project()
116125
if _is_git_project ~= nil then

0 commit comments

Comments
 (0)