Skip to content

Commit 8cb171f

Browse files
feat: Add prompt_guard status indicator to output buffer topbar
- Create new prompt_guard_indicator module to track prompt guard status - Display guard status icon on top left of output buffer when denied - Add 1-second timer to update indicator when output buffer is visible - Add OpencodeGuardDenied highlight group for guard status - Support both light and dark themes with appropriate colors The indicator helps users quickly see if their prompts will be accepted before attempting to send a message to the opencode server.
1 parent 88edc4e commit 8cb171f

File tree

5 files changed

+127
-11
lines changed

5 files changed

+127
-11
lines changed

lua/opencode/ui/highlight.lua

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ function M.setup()
2222
vim.api.nvim_set_hl(0, 'OpencodeContextualActions', { bg = '#90A4AE', fg = '#1976D2', bold = true, default = true })
2323
vim.api.nvim_set_hl(0, 'OpencodeInputLegend', { bg = '#757575', fg = '#424242', bold = false, default = true })
2424
vim.api.nvim_set_hl(0, 'OpencodeHint', { link = 'Comment', default = true })
25+
vim.api.nvim_set_hl(0, 'OpencodeGuardDenied', { fg = '#F44336', bold = true, default = true })
2526
else
2627
vim.api.nvim_set_hl(0, 'OpencodeBorder', { fg = '#616161', default = true })
2728
vim.api.nvim_set_hl(0, 'OpencodeBackground', { link = 'Normal', default = true })
@@ -41,6 +42,7 @@ function M.setup()
4142
vim.api.nvim_set_hl(0, 'OpencodeContextualActions', { bg = '#3b4261', fg = '#61AFEF', bold = true, default = true })
4243
vim.api.nvim_set_hl(0, 'OpencodeInputLegend', { bg = '#616161', fg = '#CCCCCC', bold = false, default = true })
4344
vim.api.nvim_set_hl(0, 'OpencodeHint', { link = 'Comment', default = true })
45+
vim.api.nvim_set_hl(0, 'OpencodeGuardDenied', { fg = '#EF5350', bold = true, default = true })
4446
end
4547
end
4648

lua/opencode/ui/output_window.lua

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
local state = require('opencode.state')
22
local config = require('opencode.config')
3+
local Timer = require('opencode.ui.timer')
4+
local topbar = require('opencode.ui.topbar')
35

46
local M = {}
57
M.namespace = vim.api.nvim_create_namespace('opencode_output')
@@ -155,6 +157,36 @@ function M.setup_keymaps(windows)
155157
end
156158

157159
function M.setup_autocmds(windows, group)
160+
local guard_timer = nil
161+
162+
local function start_guard_timer()
163+
if guard_timer then
164+
return -- Timer already running
165+
end
166+
guard_timer = Timer.new({
167+
interval = 1000, -- 1 second
168+
repeat_timer = true,
169+
on_tick = function()
170+
if state.windows and vim.api.nvim_win_is_valid(state.windows.output_win) then
171+
topbar.render()
172+
return true
173+
end
174+
return false
175+
end,
176+
})
177+
guard_timer:start_and_tick()
178+
end
179+
180+
local function stop_guard_timer()
181+
if guard_timer then
182+
guard_timer:stop()
183+
guard_timer = nil
184+
end
185+
end
186+
187+
-- Start the timer when the window is created (visible) and tick immediately
188+
start_guard_timer()
189+
158190
vim.api.nvim_create_autocmd('WinEnter', {
159191
group = group,
160192
buffer = windows.output_buf,
@@ -175,6 +207,15 @@ function M.setup_autocmds(windows, group)
175207
end,
176208
})
177209

210+
-- Stop the timer when the window is closed
211+
vim.api.nvim_create_autocmd('WinClosed', {
212+
group = group,
213+
pattern = tostring(windows.output_win),
214+
callback = function()
215+
stop_guard_timer()
216+
end,
217+
})
218+
178219
state.subscribe('current_permission', function()
179220
require('opencode.keymap').toggle_permission_keymap(windows.output_buf)
180221
end)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
local M = {}
2+
3+
local state = require('opencode.state')
4+
local config = require('opencode.config')
5+
local util = require('opencode.util')
6+
local icons = require('opencode.ui.icons')
7+
8+
---Get the current prompt guard status
9+
---@return boolean allowed
10+
---@return string|nil error_message
11+
function M.get_status()
12+
return util.check_prompt_allowed(config.prompt_guard)
13+
end
14+
15+
---Check if guard will deny prompts
16+
---@return boolean denied
17+
function M.is_denied()
18+
local allowed, _ = M.get_status()
19+
return not allowed
20+
end
21+
22+
---Get formatted indicator string with highlight (empty if allowed)
23+
---@return string formatted_indicator
24+
function M.get_formatted()
25+
if not M.is_denied() then
26+
-- Prompts are allowed - don't show anything
27+
return ''
28+
end
29+
30+
-- Prompts will be denied - show red indicator
31+
local icon = icons.get('status_off')
32+
return string.format('%%#OpencodeGuardDenied#%s%%*', icon)
33+
end
34+
35+
return M

lua/opencode/ui/timer.lua

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ function Timer:start()
4848
end
4949
end
5050

51+
--- Start the timer and immediately execute the callback
52+
function Timer:start_and_tick()
53+
self:start()
54+
self.on_tick(unpack(self.args))
55+
end
56+
5157
function Timer:stop()
5258
if not self._uv_timer then
5359
return

lua/opencode/ui/topbar.lua

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ local M = {}
22

33
local state = require('opencode.state')
44
local config_file = require('opencode.config_file')
5+
local prompt_guard_indicator = require('opencode.ui.prompt_guard_indicator')
6+
local icons = require('opencode.ui.icons')
57

68
local LABELS = {
79
NEW_SESSION_TITLE = 'New session',
@@ -40,17 +42,45 @@ local function get_mode_highlight()
4042
end
4143
end
4244

43-
local function create_winbar_text(description, model_info, mode_info, win_width)
44-
local available_width = win_width - 2 -- 2 padding spaces
45-
46-
-- If total length exceeds available width, truncate description
47-
if #description + 1 + #model_info + #mode_info + 1 > available_width then
48-
local space_for_desc = available_width - (#model_info + #mode_info + 1) - 4 -- -4 for "... "
49-
description = description:sub(1, space_for_desc) .. '... '
45+
local function create_winbar_text(description, model_info, mode_info, show_guard_indicator, win_width)
46+
-- Calculate how many visible characters we have
47+
-- Format: " [GUARD] description padding model_info MODE "
48+
-- Where [GUARD] is optional (1 char + 1 space = 2 visible chars)
49+
50+
local guard_prefix = ''
51+
local guard_visible_width = 0
52+
53+
if show_guard_indicator then
54+
local guard_icon = icons.get('status_off')
55+
guard_prefix = string.format('%%#OpencodeGuardDenied#%s%%* ', guard_icon)
56+
guard_visible_width = 2 -- icon + space
5057
end
51-
52-
local padding = string.rep(' ', available_width - #description - #model_info - #mode_info - 1)
53-
return string.format(' %s%s%s %s ', description, padding, model_info, get_mode_highlight() .. mode_info .. '%*')
58+
59+
-- Total available width for all content
60+
local total_width = win_width
61+
62+
-- Calculate used width: leading space + guard + trailing space + model + mode
63+
local mode_info_str = get_mode_highlight() .. mode_info .. '%*'
64+
local mode_visible_width = #mode_info
65+
local model_visible_width = #model_info
66+
67+
-- Reserve space: 1 (leading) + guard_visible_width + 1 (space before description) + 1 (space before model) + model + mode
68+
local reserved_width = 1 + guard_visible_width + 1 + 1 + model_visible_width + mode_visible_width
69+
70+
-- Available width for description and padding
71+
local available_for_desc = total_width - reserved_width
72+
73+
-- Truncate description if needed
74+
if #description > available_for_desc then
75+
description = description:sub(1, math.max(1, available_for_desc - 4)) .. '...'
76+
end
77+
78+
-- Calculate padding to right-align model and mode
79+
local desc_and_padding_width = available_for_desc
80+
local padding_width = desc_and_padding_width - #description
81+
local padding = string.rep(' ', math.max(0, padding_width))
82+
83+
return string.format(' %s%s%s%s %s', guard_prefix, description, padding, model_info, mode_info_str)
5484
end
5585

5686
local function update_winbar_highlights(win_id)
@@ -96,8 +126,10 @@ function M.render()
96126
end
97127
-- topbar needs to at least have a value to make sure footer is positioned correctly
98128
vim.wo[win].winbar = ' '
129+
130+
local show_guard_indicator = prompt_guard_indicator.is_denied()
99131
vim.wo[win].winbar =
100-
create_winbar_text(get_session_desc(), format_model_info(), format_mode_info(), vim.api.nvim_win_get_width(win))
132+
create_winbar_text(get_session_desc(), format_model_info(), format_mode_info(), show_guard_indicator, vim.api.nvim_win_get_width(win))
101133

102134
update_winbar_highlights(win)
103135
end)

0 commit comments

Comments
 (0)