Skip to content

Commit dea057e

Browse files
committed
feat: notify and mark background prompts on session tabs
1 parent 0e92ebf commit dea057e

15 files changed

Lines changed: 498 additions & 5 deletions

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ require('opencode').setup({
229229
display_context_size = true, -- Display context size in the footer
230230
display_cost = true, -- Display cost in the footer
231231
hide_single_tab = false, -- Hide the panel tab strip when only one session tab exists
232+
notify_on_background_prompt = true, -- Notify when an unfocused session needs a question or permission response
232233
window_highlight = 'Normal:OpencodeBackground,FloatBorder:OpencodeBorder', -- Highlight group for the opencode window
233234
persist_state = true, -- Keep buffers when toggling/closing UI so window state restores quickly
234235
icons = {

lua/opencode/config.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ M.defaults = {
178178
display_context_size = true,
179179
display_cost = true,
180180
hide_single_tab = true,
181+
notify_on_background_prompt = true,
181182
window_highlight = 'Normal:OpencodeBackground,FloatBorder:OpencodeBorder',
182183
persist_state = true,
183184
icons = {

lua/opencode/init.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ function M.setup(opts)
6060
require('opencode.ui.completion').setup()
6161
require('opencode.keymap').setup(config.keymap)
6262
require('opencode.event_manager').setup()
63+
require('opencode.ui.session_tab_notifications').setup()
6364
require('opencode.context').setup()
6465
require('opencode.ui.context_bar').setup()
6566
end

lua/opencode/state/session.lua

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@ local M = {}
66

77
---@param session Session|nil
88
function M.set_active(session)
9+
local previous = store.get('active_session')
10+
local previous_id = type(previous) == 'table' and previous.id or nil
11+
local session_id = type(session) == 'table' and session.id or nil
12+
if previous_id ~= session_id then
13+
local runtime = session_tabs.current()
14+
if runtime then
15+
session_tabs.clear_pending_prompts(runtime.id)
16+
end
17+
end
18+
919
local result = store.batch(function()
1020
store.set('restore_points', {})
1121
store.set('last_sent_context', nil)
@@ -17,6 +27,13 @@ function M.set_active(session)
1727
end
1828

1929
function M.clear_active()
30+
if store.get('active_session') then
31+
local runtime = session_tabs.current()
32+
if runtime then
33+
session_tabs.clear_pending_prompts(runtime.id)
34+
end
35+
end
36+
2037
local result = store.batch(function()
2138
store.set('restore_points', {})
2239
store.set('last_sent_context', nil)

lua/opencode/state/session_tabs.lua

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ local store = require('opencode.state.store')
2828
---@field messages OpencodeMessage[]|nil
2929
---@field current_message OpencodeMessage|nil
3030
---@field pending_permissions OpencodePermission[]
31+
---@field pending_prompt_permissions OpencodePermission[]
32+
---@field pending_questions OpencodeQuestionRequest[]
3133
---@field cost number
3234
---@field tokens_count number
3335
---@field user_message_count table<string, number>
@@ -141,6 +143,8 @@ local function default_runtime(id)
141143
messages = nil,
142144
current_message = nil,
143145
pending_permissions = {},
146+
pending_prompt_permissions = {},
147+
pending_questions = {},
144148
cost = 0,
145149
tokens_count = 0,
146150
user_message_count = {},
@@ -215,6 +219,139 @@ function M.get(id)
215219
return runtimes[id]
216220
end
217221

222+
---@param session_id string|nil
223+
---@return OpencodeSessionTabRuntime|nil
224+
function M.find_by_session_id(session_id)
225+
if not session_id or session_id == '' then
226+
return nil
227+
end
228+
229+
for _, runtime in ipairs(M.list()) do
230+
if runtime.active_session and runtime.active_session.id == session_id then
231+
return runtime
232+
end
233+
234+
local render_state = runtime.renderer_context and runtime.renderer_context.render_state
235+
if render_state and render_state.get_task_part_by_child_session then
236+
local ok, task_part = pcall(render_state.get_task_part_by_child_session, render_state, session_id)
237+
if ok and task_part then
238+
return runtime
239+
end
240+
end
241+
end
242+
243+
local current = M.current()
244+
if current and current.active_session and current.active_session.id then
245+
local render_state = require('opencode.ui.renderer.ctx').render_state
246+
if render_state:get_task_part_by_child_session(session_id) then
247+
return current
248+
end
249+
end
250+
end
251+
252+
---@param tab_id string
253+
---@param permission OpencodePermission
254+
function M.add_pending_permission(tab_id, permission)
255+
local runtime = runtimes[tab_id]
256+
if not runtime or not permission or not permission.id then
257+
return
258+
end
259+
260+
for index, existing in ipairs(runtime.pending_prompt_permissions) do
261+
if existing.id == permission.id then
262+
runtime.pending_prompt_permissions[index] = permission
263+
notify_change()
264+
return
265+
end
266+
end
267+
268+
table.insert(runtime.pending_prompt_permissions, permission)
269+
notify_change()
270+
end
271+
272+
---@param tab_id string
273+
---@param permission_id string
274+
function M.remove_pending_permission(tab_id, permission_id)
275+
local runtime = runtimes[tab_id]
276+
if not runtime or not permission_id then
277+
return
278+
end
279+
280+
for index, permission in ipairs(runtime.pending_prompt_permissions) do
281+
if permission.id == permission_id then
282+
table.remove(runtime.pending_prompt_permissions, index)
283+
notify_change()
284+
return
285+
end
286+
end
287+
end
288+
289+
---@param tab_id string
290+
---@param question OpencodeQuestionRequest
291+
function M.add_pending_question(tab_id, question)
292+
local runtime = runtimes[tab_id]
293+
if not runtime or not question or not question.id then
294+
return
295+
end
296+
297+
for index, existing in ipairs(runtime.pending_questions) do
298+
if existing.id == question.id then
299+
runtime.pending_questions[index] = question
300+
notify_change()
301+
return
302+
end
303+
end
304+
305+
table.insert(runtime.pending_questions, question)
306+
notify_change()
307+
end
308+
309+
---@param tab_id string
310+
---@param question_id string
311+
function M.remove_pending_question(tab_id, question_id)
312+
local runtime = runtimes[tab_id]
313+
if not runtime or not question_id then
314+
return
315+
end
316+
317+
for index, question in ipairs(runtime.pending_questions) do
318+
if question.id == question_id then
319+
table.remove(runtime.pending_questions, index)
320+
notify_change()
321+
return
322+
end
323+
end
324+
end
325+
326+
---@param tab_id string
327+
function M.clear_pending_prompts(tab_id)
328+
local runtime = runtimes[tab_id]
329+
if not runtime then
330+
return
331+
end
332+
333+
local active = M.active_id() == tab_id
334+
local has_pending = #runtime.pending_permissions > 0
335+
or #runtime.pending_prompt_permissions > 0
336+
or #runtime.pending_questions > 0
337+
if active then
338+
has_pending = has_pending or #(store.get('pending_permissions') or {}) > 0
339+
end
340+
if not has_pending then
341+
return
342+
end
343+
344+
runtime.pending_permissions = {}
345+
runtime.pending_prompt_permissions = {}
346+
runtime.pending_questions = {}
347+
if active then
348+
store.batch(function()
349+
store.set('pending_permissions', {})
350+
end)
351+
end
352+
notify_change()
353+
end
354+
218355
---@return OpencodeSessionTabRuntime|nil
219356
function M.current()
220357
local runtime = runtimes[store.get('active_session_tab')]
@@ -337,6 +474,8 @@ function M.create(session)
337474
runtime.messages = nil
338475
runtime.current_message = nil
339476
runtime.pending_permissions = {}
477+
runtime.pending_prompt_permissions = {}
478+
runtime.pending_questions = {}
340479
runtime.restore_points = {}
341480
runtime.last_sent_context = nil
342481
runtime.user_message_count = {}

lua/opencode/types.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@
243243
---@field display_context_size boolean
244244
---@field display_cost boolean
245245
---@field hide_single_tab boolean
246+
---@field notify_on_background_prompt boolean
246247
---@field window_highlight string
247248
---@field icons { preset: 'text'|'nerdfonts', overrides: table<string,string> }
248249
---@field loading_animation OpencodeLoadingAnimationConfig

lua/opencode/ui/base_picker.lua

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ local Promise = require('opencode.promise')
6363
---@class BasePicker
6464
local M = {}
6565
local picker = require('opencode.ui.picker')
66+
local icons = require('lua.opencode.ui.icons')
6667

6768
---@param bufnr integer?
6869
---@return PickerPreviewTarget
@@ -135,7 +136,7 @@ local function build_title(base_title, actions, support_multi)
135136
table.insert(legend, action.key[1] .. ' ' .. label)
136137
end
137138
end
138-
return base_title .. (#legend > 0 and '' .. table.concat(legend, '') or '')
139+
return base_title .. (#legend > 0 and icons.get('separator') .. table.concat(legend, icons.get('separator')) or '')
139140
end
140141

141142
---Telescope UI implementation

lua/opencode/ui/highlight.lua

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@ function M.setup()
1313
vim.api.nvim_set_hl(0, 'OpencodeSessionTabIndex', { link = 'Number', default = true })
1414
vim.api.nvim_set_hl(0, 'OpencodeSessionTabSeparator', { link = 'NonText', default = true })
1515
vim.api.nvim_set_hl(0, 'OpencodeSessionTabOverflow', { link = 'Special', bold = true, default = true })
16+
vim.api.nvim_set_hl(
17+
0,
18+
'OpencodeSessionTabPendingPermission',
19+
{ fg = '#9A3412', bg = '#FED7AA', bold = true, default = true }
20+
)
21+
vim.api.nvim_set_hl(
22+
0,
23+
'OpencodeSessionTabPendingQuestion',
24+
{ fg = '#1D4ED8', bg = '#DBEAFE', bold = true, default = true }
25+
)
1626
vim.api.nvim_set_hl(0, 'OpencodeMention', { link = 'Special', default = true })
1727
vim.api.nvim_set_hl(0, 'OpencodeToolBorder', { fg = '#B0BEC5', nocombine = true, default = true })
1828
vim.api.nvim_set_hl(0, 'OpencodeMessageRoleAssistant', { link = 'Special', default = true })
@@ -69,6 +79,16 @@ function M.setup()
6979
vim.api.nvim_set_hl(0, 'OpencodeSessionTabIndex', { link = 'Number', default = true })
7080
vim.api.nvim_set_hl(0, 'OpencodeSessionTabSeparator', { link = 'NonText', default = true })
7181
vim.api.nvim_set_hl(0, 'OpencodeSessionTabOverflow', { link = 'Special', bold = true, default = true })
82+
vim.api.nvim_set_hl(
83+
0,
84+
'OpencodeSessionTabPendingPermission',
85+
{ fg = '#FFD580', bg = '#5A3A00', bold = true, default = true }
86+
)
87+
vim.api.nvim_set_hl(
88+
0,
89+
'OpencodeSessionTabPendingQuestion',
90+
{ fg = '#9CDCFE', bg = '#1E3A5F', bold = true, default = true }
91+
)
7292
vim.api.nvim_set_hl(0, 'OpencodeMention', { link = 'Special', default = true })
7393
vim.api.nvim_set_hl(0, 'OpencodeToolBorder', { fg = '#3b4261', nocombine = true, default = true })
7494
vim.api.nvim_set_hl(0, 'OpencodeRevertBorder', { bg = '#FF9E3B', default = true })

lua/opencode/ui/icons.lua

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ local presets = {
5151
running = '',
5252
checkbox_checked = '',
5353
checkbox_unchecked = '',
54+
separator = '',
5455
},
5556
text = {
5657
-- headers
@@ -98,6 +99,7 @@ local presets = {
9899
running = '> ',
99100
checkbox_checked = '[*]',
100101
checkbox_unchecked = '[ ]',
102+
separator = '|',
101103
},
102104
}
103105

lua/opencode/ui/permission_window.lua

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
local state = require('opencode.state')
2+
local session_tabs = require('opencode.state.session_tabs')
23
local Dialog = require('opencode.ui.dialog')
34
local session_scope = require('opencode.ui.session_scope')
45
local formatter_utils = require('opencode.ui.formatter.utils')
@@ -219,6 +220,10 @@ function M.remove_permission(permission_id)
219220

220221
for i, permission in ipairs(M._permission_queue) do
221222
if permission.id == permission_id then
223+
local runtime = session_tabs.find_by_session_id(permission.sessionID)
224+
if runtime then
225+
session_tabs.remove_pending_permission(runtime.id, permission_id)
226+
end
222227
table.remove(M._permission_queue, i)
223228
break
224229
end
@@ -532,6 +537,10 @@ function M.restore_pending_permissions(session_id)
532537
for _, permission in ipairs(permissions) do
533538
if permission and permission.id then
534539
if session_scope.belongs_to_session(permission, session_id) and not is_resolved_permission(permission) then
540+
local runtime = session_tabs.find_by_session_id(session_id)
541+
if runtime then
542+
session_tabs.add_pending_permission(runtime.id, permission)
543+
end
535544
-- Check if already queued (avoid duplicate)
536545
local already_queued = false
537546
for _, existing in ipairs(M._permission_queue) do

0 commit comments

Comments
 (0)