Skip to content

Commit b72d46b

Browse files
committed
feat(history_picker): implement a history picker to select old prompts
1 parent 03e5e3a commit b72d46b

File tree

8 files changed

+197
-3
lines changed

8 files changed

+197
-3
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,10 @@ require('opencode').setup({
169169
undo = { '<C-u>', mode = { 'i', 'n' } }, -- Undo to selected message in timeline picker
170170
fork = { '<C-f>', mode = { 'i', 'n' } }, -- Fork from selected message in timeline picker
171171
},
172+
history_picker = {
173+
delete_entry = { '<C-d>', mode = { 'i', 'n' } }, -- Delete selected entry in the history picker
174+
clear_all = { '<C-X>', mode = { 'i', 'n' } }, -- Clear all entries in the history picker
175+
}
172176
},
173177
ui = {
174178
position = 'right', -- 'right' (default) or 'left'. Position of the UI split

lua/opencode/api.lua

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ function M.select_child_session()
9898
core.select_session(state.active_session and state.active_session.id or nil)
9999
end
100100

101+
function M.select_history()
102+
require('opencode.ui.history_picker').pick()
103+
end
104+
101105
function M.toggle_pane()
102106
if not state.windows then
103107
core.open({ new_session = false, focus = 'output' })
@@ -1174,6 +1178,11 @@ M.commands = {
11741178
fn = M.help,
11751179
},
11761180

1181+
history = {
1182+
desc = 'Select from prompt history',
1183+
fn = M.select_history,
1184+
},
1185+
11771186
mcp = {
11781187
desc = 'Show MCP server configuration',
11791188
fn = M.mcp,
@@ -1224,6 +1233,7 @@ M.slash_commands_map = {
12241233
['/child-sessions'] = { fn = M.select_child_session, desc = 'Select child session' },
12251234
['/command-list'] = { fn = M.commands_list, desc = 'Show user-defined commands' },
12261235
['/compact'] = { fn = M.compact_session, desc = 'Compact current session' },
1236+
['/history'] = { fn = M.select_history, desc = 'Select from history' },
12271237
['/mcp'] = { fn = M.mcp, desc = 'Show MCP server configuration' },
12281238
['/models'] = { fn = M.configure_provider, desc = 'Switch provider/model' },
12291239
['/new'] = { fn = M.open_input_new_session, desc = 'Create new session' },

lua/opencode/config.lua

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ M.defaults = {
1717
['<leader>og'] = { 'toggle', desc = 'Toggle Opencode window' },
1818
['<leader>oi'] = { 'open_input', desc = 'Open input window' },
1919
['<leader>oI'] = { 'open_input_new_session', desc = 'Open input (new session)' },
20+
['<leader>oh'] = { 'select_history', desc = 'Select from history' },
2021
['<leader>oo'] = { 'open_output', desc = 'Open output window' },
2122
['<leader>ot'] = { 'toggle_focus', desc = 'Toggle focus' },
2223
['<leader>oT'] = { 'timeline', desc = 'Session timeline' },
@@ -85,6 +86,10 @@ M.defaults = {
8586
undo = { '<C-u>', mode = { 'i', 'n' } },
8687
fork = { '<C-f>', mode = { 'i', 'n' } },
8788
},
89+
history_picker = {
90+
delete_entry = { '<C-d>', mode = { 'i', 'n' } },
91+
clear_all = { '<C-X>', mode = { 'i', 'n' } },
92+
},
8893
},
8994
ui = {
9095
position = 'right',
@@ -185,7 +190,7 @@ M.defaults = {
185190
debug = {
186191
enabled = false,
187192
capture_streamed_events = false,
188-
show_ids = false,
193+
show_ids = true,
189194
},
190195
prompt_guard = nil,
191196
hooks = {

lua/opencode/history.lua

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,63 @@ M.next = function()
9797
return M.read()[M.index]
9898
end
9999

100+
---Delete specific entries from history by their indices
101+
---@param indices number[] Array of 1-based indices to delete
102+
M.delete = function(indices)
103+
if not indices or #indices == 0 then
104+
return false
105+
end
106+
107+
local history = M.read()
108+
if #history == 0 then
109+
return false
110+
end
111+
112+
-- Sort indices in descending order to avoid index shifting issues
113+
local sorted_indices = {}
114+
for _, idx in ipairs(indices) do
115+
if idx > 0 and idx <= #history then
116+
table.insert(sorted_indices, idx)
117+
end
118+
end
119+
table.sort(sorted_indices, function(a, b)
120+
return a > b
121+
end)
122+
123+
for _, idx in ipairs(sorted_indices) do
124+
table.remove(history, idx)
125+
end
126+
127+
return M._write_history(history)
128+
end
129+
130+
---Clear all history entries
131+
M.clear = function()
132+
return M._write_history({})
133+
end
134+
135+
---Internal function to write history array to file
136+
---@param history_array table Array of history entries to write
137+
---@return boolean success Whether the write operation succeeded
138+
M._write_history = function(history_array)
139+
local file = io.open(get_history_file().filename, 'w')
140+
if not file then
141+
return false
142+
end
143+
144+
for i = #history_array, 1, -1 do
145+
local entry = history_array[i]
146+
if entry and entry ~= '' then
147+
local escaped_entry = entry:gsub('\n', '\\n')
148+
file:write(escaped_entry .. '\n')
149+
end
150+
end
151+
152+
file:close()
153+
154+
cached_history = nil
155+
156+
return true
157+
end
158+
100159
return M

lua/opencode/types.lua

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
---@field permission OpencodeKeymapPermission
8585
---@field session_picker OpencodeSessionPickerKeymap
8686
---@field timeline_picker OpencodeTimelinePickerKeymap
87+
---@field history_picker OpencodeHistoryPickerKeymap
8788

8889
---@class OpencodeSessionPickerKeymap
8990
---@field delete_session OpencodeKeymapEntry
@@ -94,6 +95,10 @@
9495
---@field undo OpencodeKeymapEntry
9596
---@field fork OpencodeKeymapEntry
9697

98+
---@class OpencodeHistoryPickerKeymap
99+
---@field delete_entry OpencodeKeymapEntry
100+
---@field clear_all OpencodeKeymapEntry
101+
97102
---@class OpencodeCompletionFileSourcesConfig
98103
---@field enabled boolean
99104
---@field preferred_cli_tool 'server'|'fd'|'fdfind'|'rg'|'git'

lua/opencode/ui/base_picker.lua

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -459,8 +459,8 @@ function M.create_picker_item(text, time, debug_text, width)
459459
function item:to_formatted_text()
460460
return {
461461
{ self.content },
462-
self.time_text and { ' ' .. self.time_text, 'OpencodePickerTime' } or nil,
463-
self.debug_text and { ' ' .. self.debug_text, 'OpencodeDebugText' } or nil,
462+
self.time_text and { ' ' .. self.time_text, 'OpencodePickerTime' } or { '' },
463+
self.debug_text and { ' ' .. self.debug_text, 'OpencodeDebugText' } or { '' },
464464
}
465465
end
466466

lua/opencode/ui/history_picker.lua

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
local M = {}
2+
local config = require('opencode.config')
3+
local base_picker = require('opencode.ui.base_picker')
4+
local util = require('opencode.util')
5+
local history = require('opencode.history')
6+
7+
---Format history entries for history picker
8+
---@param item table History item with text content
9+
---@param width number Picker width
10+
---@return PickerItem
11+
local function format_history_item(item, width)
12+
local entry = item.content or item.text or ''
13+
14+
return base_picker.create_picker_item(entry:gsub('\n', ''), nil, 'ID: ' .. item.id, width)
15+
end
16+
17+
function M.pick(callback)
18+
local history_entries = history.read()
19+
20+
if #history_entries == 0 then
21+
vim.notify('No history entries found', vim.log.levels.INFO)
22+
return false
23+
end
24+
25+
local history_items = {}
26+
for i, entry in ipairs(history_entries) do
27+
table.insert(history_items, { id = i, text = entry, content = entry })
28+
end
29+
30+
local actions = {
31+
delete = {
32+
key = config.keymap.history_picker.delete_entry,
33+
label = 'delete',
34+
multi_selection = true,
35+
fn = function(selected, opts)
36+
local entries_to_delete = type(selected) == 'table' and selected.id == nil and selected or { selected }
37+
38+
local indices_to_remove = {}
39+
for _, entry_to_delete in ipairs(entries_to_delete) do
40+
local idx = util.find_index_of(opts.items, function(item)
41+
return item.id == entry_to_delete.id
42+
end)
43+
if idx > 0 then
44+
table.insert(indices_to_remove, idx)
45+
end
46+
end
47+
48+
table.sort(indices_to_remove, function(a, b)
49+
return a > b
50+
end)
51+
52+
local success = history.delete(indices_to_remove)
53+
if success then
54+
for _, idx in ipairs(indices_to_remove) do
55+
table.remove(opts.items, idx)
56+
end
57+
vim.notify('Deleted ' .. #entries_to_delete .. ' history entry(s)', vim.log.levels.INFO)
58+
else
59+
vim.notify('Failed to delete history entries', vim.log.levels.ERROR)
60+
end
61+
62+
return opts.items
63+
end,
64+
reload = true,
65+
},
66+
clear_all = {
67+
key = config.keymap.history_picker.clear_all,
68+
label = 'clear all',
69+
fn = function(_, opts)
70+
local success = history.clear()
71+
if success then
72+
opts.items = {}
73+
vim.notify('Cleared all history entries', vim.log.levels.INFO)
74+
else
75+
vim.notify('Failed to clear history entries', vim.log.levels.ERROR)
76+
end
77+
78+
return opts.items
79+
end,
80+
reload = true,
81+
},
82+
}
83+
84+
return base_picker.pick({
85+
items = history_items,
86+
format_fn = format_history_item,
87+
actions = actions,
88+
callback = function(selected_item)
89+
if selected_item and callback then
90+
callback(selected_item.content or selected_item.text)
91+
elseif selected_item then
92+
local input_window = require('opencode.ui.input_window')
93+
local state = require('opencode.state')
94+
local windows = state.windows
95+
if not input_window.mounted(windows) then
96+
require('opencode.core').open({ focus_input = true })
97+
end
98+
---@cast windows { input_win: integer, input_buf: integer }
99+
100+
input_window.set_content(selected_item.content or selected_item.text)
101+
require('opencode.ui.mention').restore_mentions(windows.input_buf)
102+
input_window.focus_input()
103+
end
104+
end,
105+
title = 'Select History Entry',
106+
width = config.ui.picker_width or 100,
107+
})
108+
end
109+
110+
return M

lua/opencode/ui/input_window.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ function M.create_window(windows)
2323
windows.input_win = vim.api.nvim_open_win(windows.input_buf, true, M._build_input_win_config())
2424
end
2525

26+
---@return_cast windows { input_win: integer, input_buf: integer }
2627
function M.mounted(windows)
2728
windows = windows or state.windows
2829
if

0 commit comments

Comments
 (0)