From a98539e5ec47979d00684017d60ab147553553bb Mon Sep 17 00:00:00 2001 From: Guillaume BOEHM Date: Fri, 24 Oct 2025 20:11:46 +0200 Subject: [PATCH 1/9] feat: add prompt_guard callback mechanism - Add prompt_guard configuration option (function that returns boolean) - Check guard before sending prompts (ERROR notification if denied) - Check guard before opening buffer first time (WARN notification if denied) - Add util.check_prompt_allowed() helper functions - Guard has no parameters, users can access vim state directly - Proper error handling for guard callback failures --- lua/opencode/config.lua | 1 + lua/opencode/core.lua | 16 ++++++++++++++++ lua/opencode/types.lua | 1 + lua/opencode/util.lua | 26 ++++++++++++++++++++++++++ 4 files changed, 44 insertions(+) diff --git a/lua/opencode/config.lua b/lua/opencode/config.lua index ae556999..0f730489 100644 --- a/lua/opencode/config.lua +++ b/lua/opencode/config.lua @@ -167,6 +167,7 @@ M.defaults = { enabled = false, capture_streamed_events = false, }, + prompt_guard = nil, } M.values = vim.deepcopy(M.defaults) diff --git a/lua/opencode/core.lua b/lua/opencode/core.lua index 6fa4f7d2..27f0db5f 100644 --- a/lua/opencode/core.lua +++ b/lua/opencode/core.lua @@ -48,6 +48,14 @@ function M.open(opts) local are_windows_closed = state.windows == nil if are_windows_closed then + -- Check if opening buffer is allowed + local allowed, err_msg = util.check_prompt_allowed(config.prompt_guard) + + if not allowed then + vim.notify(err_msg or 'Opening opencode buffer denied by prompt_guard', vim.log.levels.WARN) + return + end + state.windows = ui.create_windows() end @@ -81,6 +89,14 @@ end --- @param prompt string The message prompt to send. --- @param opts? SendMessageOpts function M.send_message(prompt, opts) + -- Check if prompt is allowed + local allowed, err_msg = util.check_prompt_allowed(config.prompt_guard) + + if not allowed then + vim.notify(err_msg or 'Prompt denied by prompt_guard', vim.log.levels.ERROR) + return + end + opts = opts or {} opts.context = opts.context or config.context opts.model = opts.model or state.current_model diff --git a/lua/opencode/types.lua b/lua/opencode/types.lua index 299807c1..97aaf7f0 100644 --- a/lua/opencode/types.lua +++ b/lua/opencode/types.lua @@ -141,6 +141,7 @@ ---@field ui OpencodeUIConfig ---@field context OpencodeContextConfig ---@field debug OpencodeDebugConfig +---@field prompt_guard? fun(): boolean ---@class MessagePartState ---@field input TaskToolInput|BashToolInput|FileToolInput|TodoToolInput|GlobToolInput|GrepToolInput|WebFetchToolInput|ListToolInput Input data for the tool diff --git a/lua/opencode/util.lua b/lua/opencode/util.lua index 1a6673d7..df5bf51a 100644 --- a/lua/opencode/util.lua +++ b/lua/opencode/util.lua @@ -360,4 +360,30 @@ function M.parse_dot_args(args_str) return result end +--- Check if prompt is allowed via guard callback +--- @param guard_callback? function +--- @return boolean allowed +--- @return string|nil error_message +function M.check_prompt_allowed(guard_callback) + if not guard_callback then + return true, nil -- No guard = always allowed + end + + if not type(guard_callback) == 'function' then + return false, 'prompt_guard must be a function' + end + + local success, result = pcall(guard_callback) + + if not success then + return false, 'prompt_guard error: ' .. tostring(result) + end + + if type(result) ~= 'boolean' then + return false, 'prompt_guard must return a boolean' + end + + return result, nil +end + return M From 34c3b9f53893c8763d03bf4da54e5e2d791352bb Mon Sep 17 00:00:00 2001 From: Guillaume BOEHM Date: Fri, 24 Oct 2025 20:38:16 +0200 Subject: [PATCH 2/9] docs: add prompt_guard documentation to README - Document prompt_guard configuration option - Explain behavior for prompt sending and UI opening - Add practical examples for common use cases - Include error handling details --- README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/README.md b/README.md index d0cddadd..f385dda7 100644 --- a/README.md +++ b/README.md @@ -240,6 +240,7 @@ require('opencode').setup({ debug = { enabled = false, -- Enable debug messages in the output window }, + prompt_guard = nil, -- Optional function that returns boolean to control when prompts can be sent (see Prompt Guard section) }) ``` @@ -537,6 +538,31 @@ The plugin defines several highlight groups that can be customized to match your - `OpencodeInputLegend`: Highlight for input window legend (default: #CCCCCC background) - `OpencodeHint`: Highlight for hinting messages in input window and token info in output window footer (linked to `Comment`) +## 🛡️ Prompt Guard + +The `prompt_guard` configuration option allows you to control when prompts can be sent to Opencode. This is useful for preventing accidental or unauthorized AI interactions in certain contexts. + +### Configuration + +Set `prompt_guard` to a function that returns a boolean: + +```lua +require('opencode').setup({ + prompt_guard = function() + -- Your custom logic here + -- Return true to allow, false to deny + return true + end, +}) +``` + +### Behavior + +- **Before sending prompts**: The guard is checked before any prompt is sent to the AI. If denied, an ERROR notification is shown and the prompt is not sent. +- **Before opening UI**: The guard is checked when opening the Opencode buffer for the first time. If denied, a WARN notification is shown and the UI is not opened. +- **No parameters**: The guard function receives no parameters. Access vim state directly (e.g., `vim.fn.getcwd()`, `vim.bo.filetype`). +- **Error handling**: If the guard function throws an error or returns a non-boolean value, the prompt is denied with an appropriate error message. + ## 🔧 Setting up Opencode If you're new to opencode: From 88edc4efb97e99ca141aabf09842e6dd1f0b7958 Mon Sep 17 00:00:00 2001 From: Guillaume BOEHM Date: Sun, 26 Oct 2025 16:12:16 +0100 Subject: [PATCH 3/9] feat: prompt_guard: Let the buffer open and warn if prompt_guard will deny prompts --- lua/opencode/core.lua | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lua/opencode/core.lua b/lua/opencode/core.lua index 27f0db5f..302239e8 100644 --- a/lua/opencode/core.lua +++ b/lua/opencode/core.lua @@ -48,12 +48,10 @@ function M.open(opts) local are_windows_closed = state.windows == nil if are_windows_closed then - -- Check if opening buffer is allowed + -- Check if whether prompting will be allowed local allowed, err_msg = util.check_prompt_allowed(config.prompt_guard) - if not allowed then - vim.notify(err_msg or 'Opening opencode buffer denied by prompt_guard', vim.log.levels.WARN) - return + vim.notify(err_msg or 'Prompts will be denied by prompt_guard', vim.log.levels.WARN) end state.windows = ui.create_windows() From 8cb171f31e5e0321665c1d2fbf010e738c6d17b0 Mon Sep 17 00:00:00 2001 From: Guillaume BOEHM Date: Sun, 26 Oct 2025 16:19:56 +0100 Subject: [PATCH 4/9] 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. --- lua/opencode/ui/highlight.lua | 2 + lua/opencode/ui/output_window.lua | 41 ++++++++++++++++ lua/opencode/ui/prompt_guard_indicator.lua | 35 ++++++++++++++ lua/opencode/ui/timer.lua | 6 +++ lua/opencode/ui/topbar.lua | 54 +++++++++++++++++----- 5 files changed, 127 insertions(+), 11 deletions(-) create mode 100644 lua/opencode/ui/prompt_guard_indicator.lua diff --git a/lua/opencode/ui/highlight.lua b/lua/opencode/ui/highlight.lua index da0ab602..f76cab60 100644 --- a/lua/opencode/ui/highlight.lua +++ b/lua/opencode/ui/highlight.lua @@ -22,6 +22,7 @@ function M.setup() vim.api.nvim_set_hl(0, 'OpencodeContextualActions', { bg = '#90A4AE', fg = '#1976D2', bold = true, default = true }) vim.api.nvim_set_hl(0, 'OpencodeInputLegend', { bg = '#757575', fg = '#424242', bold = false, default = true }) vim.api.nvim_set_hl(0, 'OpencodeHint', { link = 'Comment', default = true }) + vim.api.nvim_set_hl(0, 'OpencodeGuardDenied', { fg = '#F44336', bold = true, default = true }) else vim.api.nvim_set_hl(0, 'OpencodeBorder', { fg = '#616161', default = true }) vim.api.nvim_set_hl(0, 'OpencodeBackground', { link = 'Normal', default = true }) @@ -41,6 +42,7 @@ function M.setup() vim.api.nvim_set_hl(0, 'OpencodeContextualActions', { bg = '#3b4261', fg = '#61AFEF', bold = true, default = true }) vim.api.nvim_set_hl(0, 'OpencodeInputLegend', { bg = '#616161', fg = '#CCCCCC', bold = false, default = true }) vim.api.nvim_set_hl(0, 'OpencodeHint', { link = 'Comment', default = true }) + vim.api.nvim_set_hl(0, 'OpencodeGuardDenied', { fg = '#EF5350', bold = true, default = true }) end end diff --git a/lua/opencode/ui/output_window.lua b/lua/opencode/ui/output_window.lua index ccf76277..ab6aa9d4 100644 --- a/lua/opencode/ui/output_window.lua +++ b/lua/opencode/ui/output_window.lua @@ -1,5 +1,7 @@ local state = require('opencode.state') local config = require('opencode.config') +local Timer = require('opencode.ui.timer') +local topbar = require('opencode.ui.topbar') local M = {} M.namespace = vim.api.nvim_create_namespace('opencode_output') @@ -155,6 +157,36 @@ function M.setup_keymaps(windows) end function M.setup_autocmds(windows, group) + local guard_timer = nil + + local function start_guard_timer() + if guard_timer then + return -- Timer already running + end + guard_timer = Timer.new({ + interval = 1000, -- 1 second + repeat_timer = true, + on_tick = function() + if state.windows and vim.api.nvim_win_is_valid(state.windows.output_win) then + topbar.render() + return true + end + return false + end, + }) + guard_timer:start_and_tick() + end + + local function stop_guard_timer() + if guard_timer then + guard_timer:stop() + guard_timer = nil + end + end + + -- Start the timer when the window is created (visible) and tick immediately + start_guard_timer() + vim.api.nvim_create_autocmd('WinEnter', { group = group, buffer = windows.output_buf, @@ -175,6 +207,15 @@ function M.setup_autocmds(windows, group) end, }) + -- Stop the timer when the window is closed + vim.api.nvim_create_autocmd('WinClosed', { + group = group, + pattern = tostring(windows.output_win), + callback = function() + stop_guard_timer() + end, + }) + state.subscribe('current_permission', function() require('opencode.keymap').toggle_permission_keymap(windows.output_buf) end) diff --git a/lua/opencode/ui/prompt_guard_indicator.lua b/lua/opencode/ui/prompt_guard_indicator.lua new file mode 100644 index 00000000..2c988349 --- /dev/null +++ b/lua/opencode/ui/prompt_guard_indicator.lua @@ -0,0 +1,35 @@ +local M = {} + +local state = require('opencode.state') +local config = require('opencode.config') +local util = require('opencode.util') +local icons = require('opencode.ui.icons') + +---Get the current prompt guard status +---@return boolean allowed +---@return string|nil error_message +function M.get_status() + return util.check_prompt_allowed(config.prompt_guard) +end + +---Check if guard will deny prompts +---@return boolean denied +function M.is_denied() + local allowed, _ = M.get_status() + return not allowed +end + +---Get formatted indicator string with highlight (empty if allowed) +---@return string formatted_indicator +function M.get_formatted() + if not M.is_denied() then + -- Prompts are allowed - don't show anything + return '' + end + + -- Prompts will be denied - show red indicator + local icon = icons.get('status_off') + return string.format('%%#OpencodeGuardDenied#%s%%*', icon) +end + +return M diff --git a/lua/opencode/ui/timer.lua b/lua/opencode/ui/timer.lua index cee93bc2..9cbe6052 100644 --- a/lua/opencode/ui/timer.lua +++ b/lua/opencode/ui/timer.lua @@ -48,6 +48,12 @@ function Timer:start() end end +--- Start the timer and immediately execute the callback +function Timer:start_and_tick() + self:start() + self.on_tick(unpack(self.args)) +end + function Timer:stop() if not self._uv_timer then return diff --git a/lua/opencode/ui/topbar.lua b/lua/opencode/ui/topbar.lua index f01bbf6b..94ab1dc1 100644 --- a/lua/opencode/ui/topbar.lua +++ b/lua/opencode/ui/topbar.lua @@ -2,6 +2,8 @@ local M = {} local state = require('opencode.state') local config_file = require('opencode.config_file') +local prompt_guard_indicator = require('opencode.ui.prompt_guard_indicator') +local icons = require('opencode.ui.icons') local LABELS = { NEW_SESSION_TITLE = 'New session', @@ -40,17 +42,45 @@ local function get_mode_highlight() end end -local function create_winbar_text(description, model_info, mode_info, win_width) - local available_width = win_width - 2 -- 2 padding spaces - - -- If total length exceeds available width, truncate description - if #description + 1 + #model_info + #mode_info + 1 > available_width then - local space_for_desc = available_width - (#model_info + #mode_info + 1) - 4 -- -4 for "... " - description = description:sub(1, space_for_desc) .. '... ' +local function create_winbar_text(description, model_info, mode_info, show_guard_indicator, win_width) + -- Calculate how many visible characters we have + -- Format: " [GUARD] description padding model_info MODE " + -- Where [GUARD] is optional (1 char + 1 space = 2 visible chars) + + local guard_prefix = '' + local guard_visible_width = 0 + + if show_guard_indicator then + local guard_icon = icons.get('status_off') + guard_prefix = string.format('%%#OpencodeGuardDenied#%s%%* ', guard_icon) + guard_visible_width = 2 -- icon + space end - - local padding = string.rep(' ', available_width - #description - #model_info - #mode_info - 1) - return string.format(' %s%s%s %s ', description, padding, model_info, get_mode_highlight() .. mode_info .. '%*') + + -- Total available width for all content + local total_width = win_width + + -- Calculate used width: leading space + guard + trailing space + model + mode + local mode_info_str = get_mode_highlight() .. mode_info .. '%*' + local mode_visible_width = #mode_info + local model_visible_width = #model_info + + -- Reserve space: 1 (leading) + guard_visible_width + 1 (space before description) + 1 (space before model) + model + mode + local reserved_width = 1 + guard_visible_width + 1 + 1 + model_visible_width + mode_visible_width + + -- Available width for description and padding + local available_for_desc = total_width - reserved_width + + -- Truncate description if needed + if #description > available_for_desc then + description = description:sub(1, math.max(1, available_for_desc - 4)) .. '...' + end + + -- Calculate padding to right-align model and mode + local desc_and_padding_width = available_for_desc + local padding_width = desc_and_padding_width - #description + local padding = string.rep(' ', math.max(0, padding_width)) + + return string.format(' %s%s%s%s %s', guard_prefix, description, padding, model_info, mode_info_str) end local function update_winbar_highlights(win_id) @@ -96,8 +126,10 @@ function M.render() end -- topbar needs to at least have a value to make sure footer is positioned correctly vim.wo[win].winbar = ' ' + + local show_guard_indicator = prompt_guard_indicator.is_denied() vim.wo[win].winbar = - create_winbar_text(get_session_desc(), format_model_info(), format_mode_info(), vim.api.nvim_win_get_width(win)) + create_winbar_text(get_session_desc(), format_model_info(), format_mode_info(), show_guard_indicator, vim.api.nvim_win_get_width(win)) update_winbar_highlights(win) end) From 8f16faf707961d8792ee88dedbdc3cad55f4b81e Mon Sep 17 00:00:00 2001 From: Guillaume BOEHM Date: Sun, 26 Oct 2025 18:18:49 +0100 Subject: [PATCH 5/9] feat: pass mentioned files to prompt_guard callback The prompt_guard callback now receives the list of mentioned files from the opencode context as a parameter. This allows users to make decisions based on which files are included in the context. Changes: - Updated prompt_guard type signature from `fun(): boolean` to `fun(mentioned_files: string[]): boolean` - Modified util.check_prompt_allowed to accept and pass mentioned_files parameter - Updated prompt_guard_indicator to retrieve mentioned files from context - Updated core.lua toggle and send_message to pass mentioned files to guard callback This enables use cases like denying prompts when certain sensitive files are mentioned, or allowing prompts only when specific files are included. --- lua/opencode/core.lua | 8 ++++++-- lua/opencode/types.lua | 2 +- lua/opencode/ui/prompt_guard_indicator.lua | 4 +++- lua/opencode/util.lua | 6 ++++-- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/lua/opencode/core.lua b/lua/opencode/core.lua index 302239e8..3f75f04a 100644 --- a/lua/opencode/core.lua +++ b/lua/opencode/core.lua @@ -49,7 +49,9 @@ function M.open(opts) if are_windows_closed then -- Check if whether prompting will be allowed - local allowed, err_msg = util.check_prompt_allowed(config.prompt_guard) + local context_module = require('opencode.context') + local mentioned_files = context_module.context.mentioned_files or {} + local allowed, err_msg = util.check_prompt_allowed(config.prompt_guard, mentioned_files) if not allowed then vim.notify(err_msg or 'Prompts will be denied by prompt_guard', vim.log.levels.WARN) end @@ -88,7 +90,9 @@ end --- @param opts? SendMessageOpts function M.send_message(prompt, opts) -- Check if prompt is allowed - local allowed, err_msg = util.check_prompt_allowed(config.prompt_guard) + local context_module = require('opencode.context') + local mentioned_files = context_module.context.mentioned_files or {} + local allowed, err_msg = util.check_prompt_allowed(config.prompt_guard, mentioned_files) if not allowed then vim.notify(err_msg or 'Prompt denied by prompt_guard', vim.log.levels.ERROR) diff --git a/lua/opencode/types.lua b/lua/opencode/types.lua index 97aaf7f0..249a1a6a 100644 --- a/lua/opencode/types.lua +++ b/lua/opencode/types.lua @@ -141,7 +141,7 @@ ---@field ui OpencodeUIConfig ---@field context OpencodeContextConfig ---@field debug OpencodeDebugConfig ----@field prompt_guard? fun(): boolean +---@field prompt_guard? fun(mentioned_files: string[]): boolean ---@class MessagePartState ---@field input TaskToolInput|BashToolInput|FileToolInput|TodoToolInput|GlobToolInput|GrepToolInput|WebFetchToolInput|ListToolInput Input data for the tool diff --git a/lua/opencode/ui/prompt_guard_indicator.lua b/lua/opencode/ui/prompt_guard_indicator.lua index 2c988349..1bb51b49 100644 --- a/lua/opencode/ui/prompt_guard_indicator.lua +++ b/lua/opencode/ui/prompt_guard_indicator.lua @@ -4,12 +4,14 @@ local state = require('opencode.state') local config = require('opencode.config') local util = require('opencode.util') local icons = require('opencode.ui.icons') +local context = require('opencode.context') ---Get the current prompt guard status ---@return boolean allowed ---@return string|nil error_message function M.get_status() - return util.check_prompt_allowed(config.prompt_guard) + local mentioned_files = context.context.mentioned_files or {} + return util.check_prompt_allowed(config.prompt_guard, mentioned_files) end ---Check if guard will deny prompts diff --git a/lua/opencode/util.lua b/lua/opencode/util.lua index df5bf51a..6d894889 100644 --- a/lua/opencode/util.lua +++ b/lua/opencode/util.lua @@ -362,9 +362,10 @@ end --- Check if prompt is allowed via guard callback --- @param guard_callback? function +--- @param mentioned_files? string[] List of mentioned files in the context --- @return boolean allowed --- @return string|nil error_message -function M.check_prompt_allowed(guard_callback) +function M.check_prompt_allowed(guard_callback, mentioned_files) if not guard_callback then return true, nil -- No guard = always allowed end @@ -373,7 +374,8 @@ function M.check_prompt_allowed(guard_callback) return false, 'prompt_guard must be a function' end - local success, result = pcall(guard_callback) + mentioned_files = mentioned_files or {} + local success, result = pcall(guard_callback, mentioned_files) if not success then return false, 'prompt_guard error: ' .. tostring(result) From 517c6569f3c9d5e0290e01067e7c192252f8b727 Mon Sep 17 00:00:00 2001 From: Guillaume BOEHM Date: Tue, 28 Oct 2025 21:37:13 +0100 Subject: [PATCH 6/9] fix: promtp_guard: Use the defined function to get the status icon --- lua/opencode/ui/topbar.lua | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/lua/opencode/ui/topbar.lua b/lua/opencode/ui/topbar.lua index 94ab1dc1..58dabdd1 100644 --- a/lua/opencode/ui/topbar.lua +++ b/lua/opencode/ui/topbar.lua @@ -46,41 +46,39 @@ local function create_winbar_text(description, model_info, mode_info, show_guard -- Calculate how many visible characters we have -- Format: " [GUARD] description padding model_info MODE " -- Where [GUARD] is optional (1 char + 1 space = 2 visible chars) - - local guard_prefix = '' + + local guard_icon = '' local guard_visible_width = 0 - if show_guard_indicator then - local guard_icon = icons.get('status_off') - guard_prefix = string.format('%%#OpencodeGuardDenied#%s%%* ', guard_icon) + guard_icon = prompt_guard_indicator.get_formatted() guard_visible_width = 2 -- icon + space end - + -- Total available width for all content local total_width = win_width - + -- Calculate used width: leading space + guard + trailing space + model + mode local mode_info_str = get_mode_highlight() .. mode_info .. '%*' local mode_visible_width = #mode_info local model_visible_width = #model_info - + -- Reserve space: 1 (leading) + guard_visible_width + 1 (space before description) + 1 (space before model) + model + mode local reserved_width = 1 + guard_visible_width + 1 + 1 + model_visible_width + mode_visible_width - + -- Available width for description and padding local available_for_desc = total_width - reserved_width - + -- Truncate description if needed if #description > available_for_desc then description = description:sub(1, math.max(1, available_for_desc - 4)) .. '...' end - + -- Calculate padding to right-align model and mode local desc_and_padding_width = available_for_desc local padding_width = desc_and_padding_width - #description local padding = string.rep(' ', math.max(0, padding_width)) - - return string.format(' %s%s%s%s %s', guard_prefix, description, padding, model_info, mode_info_str) + + return string.format(' %s%s%s%s %s', guard_icon, description, padding, model_info, mode_info_str) end local function update_winbar_highlights(win_id) From 09c33d51b2578cbf3081f56b3f88c955ba523f5e Mon Sep 17 00:00:00 2001 From: Guillaume BOEHM Date: Tue, 28 Oct 2025 22:17:25 +0100 Subject: [PATCH 7/9] fix: merge error on topbar mode_info --- lua/opencode/ui/topbar.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/opencode/ui/topbar.lua b/lua/opencode/ui/topbar.lua index 58dabdd1..67990637 100644 --- a/lua/opencode/ui/topbar.lua +++ b/lua/opencode/ui/topbar.lua @@ -78,7 +78,7 @@ local function create_winbar_text(description, model_info, mode_info, show_guard local padding_width = desc_and_padding_width - #description local padding = string.rep(' ', math.max(0, padding_width)) - return string.format(' %s%s%s%s %s', guard_icon, description, padding, model_info, mode_info_str) + return string.format(' %s%s%s%s %s', guard_icon, description, padding, model_info, get_mode_highlight() .. mode_info .. '%*') end local function update_winbar_highlights(win_id) From 6c4b7377f7f23a85b0d2948a8f20edc9ab14d757 Mon Sep 17 00:00:00 2001 From: Guillaume BOEHM Date: Wed, 29 Oct 2025 08:09:55 +0100 Subject: [PATCH 8/9] feat: Use proper guard icon for the topbar --- lua/opencode/ui/icons.lua | 3 +++ lua/opencode/ui/prompt_guard_indicator.lua | 2 +- lua/opencode/ui/topbar.lua | 26 +++++++++------------- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/lua/opencode/ui/icons.lua b/lua/opencode/ui/icons.lua index 972b896e..d8d3363d 100644 --- a/lua/opencode/ui/icons.lua +++ b/lua/opencode/ui/icons.lua @@ -25,6 +25,7 @@ local presets = { -- statuses status_on = '🟢', status_off = '⚫', + guard_on = '🚫', -- borders and misc border = '▌', }, @@ -49,6 +50,7 @@ local presets = { -- statuses status_on = ' ', status_off = ' ', + guard_on = '', -- borders and misc border = '▌', }, @@ -73,6 +75,7 @@ local presets = { -- statuses status_on = 'ON', status_off = 'OFF', + guard_on = 'X', -- borders and misc border = '▌', }, diff --git a/lua/opencode/ui/prompt_guard_indicator.lua b/lua/opencode/ui/prompt_guard_indicator.lua index 1bb51b49..4c255260 100644 --- a/lua/opencode/ui/prompt_guard_indicator.lua +++ b/lua/opencode/ui/prompt_guard_indicator.lua @@ -30,7 +30,7 @@ function M.get_formatted() end -- Prompts will be denied - show red indicator - local icon = icons.get('status_off') + local icon = icons.get('guard_on') return string.format('%%#OpencodeGuardDenied#%s%%*', icon) end diff --git a/lua/opencode/ui/topbar.lua b/lua/opencode/ui/topbar.lua index 67990637..4d0bf339 100644 --- a/lua/opencode/ui/topbar.lua +++ b/lua/opencode/ui/topbar.lua @@ -3,7 +3,6 @@ local M = {} local state = require('opencode.state') local config_file = require('opencode.config_file') local prompt_guard_indicator = require('opencode.ui.prompt_guard_indicator') -local icons = require('opencode.ui.icons') local LABELS = { NEW_SESSION_TITLE = 'New session', @@ -44,41 +43,38 @@ end local function create_winbar_text(description, model_info, mode_info, show_guard_indicator, win_width) -- Calculate how many visible characters we have - -- Format: " [GUARD] description padding model_info MODE " - -- Where [GUARD] is optional (1 char + 1 space = 2 visible chars) + -- Format: " [GUARD ]description padding model_info MODE " + -- Where [GUARD ] is optional - local guard_icon = '' + local guard_info = '' local guard_visible_width = 0 if show_guard_indicator then - guard_icon = prompt_guard_indicator.get_formatted() + guard_info = prompt_guard_indicator.get_formatted() guard_visible_width = 2 -- icon + space end - -- Total available width for all content - local total_width = win_width - -- Calculate used width: leading space + guard + trailing space + model + mode local mode_info_str = get_mode_highlight() .. mode_info .. '%*' local mode_visible_width = #mode_info local model_visible_width = #model_info - -- Reserve space: 1 (leading) + guard_visible_width + 1 (space before description) + 1 (space before model) + model + mode - local reserved_width = 1 + guard_visible_width + 1 + 1 + model_visible_width + mode_visible_width + -- Reserve space: 1 (padding) + guard_visible_width (with padding) + model + 1 (space before mode) + mode + 1 (padding) + local reserved_width = 1 + guard_visible_width + model_visible_width + 1 + mode_visible_width + 1 -- Available width for description and padding - local available_for_desc = total_width - reserved_width + local available_for_desc = win_width - reserved_width -- Truncate description if needed if #description > available_for_desc then - description = description:sub(1, math.max(1, available_for_desc - 4)) .. '...' + local space_for_desc = available_for_desc - 4 -- -4 for "... " + description = description:sub(1, space_for_desc) .. '...' end -- Calculate padding to right-align model and mode - local desc_and_padding_width = available_for_desc - local padding_width = desc_and_padding_width - #description + local padding_width = available_for_desc - #description local padding = string.rep(' ', math.max(0, padding_width)) - return string.format(' %s%s%s%s %s', guard_icon, description, padding, model_info, get_mode_highlight() .. mode_info .. '%*') + return string.format(' %s %s%s%s %s ', guard_info, description, padding, model_info, mode_info_str) end local function update_winbar_highlights(win_id) From 40f33a86b44709e305660b9bb995269d6c4f5225 Mon Sep 17 00:00:00 2001 From: Guillaume BOEHM Date: Wed, 29 Oct 2025 08:20:44 +0100 Subject: [PATCH 9/9] fix: Replace prompt guard timer with observer on opencode window focus --- lua/opencode/ui/output_window.lua | 41 ---------------------- lua/opencode/ui/prompt_guard_indicator.lua | 1 - lua/opencode/ui/topbar.lua | 2 ++ 3 files changed, 2 insertions(+), 42 deletions(-) diff --git a/lua/opencode/ui/output_window.lua b/lua/opencode/ui/output_window.lua index ab6aa9d4..ccf76277 100644 --- a/lua/opencode/ui/output_window.lua +++ b/lua/opencode/ui/output_window.lua @@ -1,7 +1,5 @@ local state = require('opencode.state') local config = require('opencode.config') -local Timer = require('opencode.ui.timer') -local topbar = require('opencode.ui.topbar') local M = {} M.namespace = vim.api.nvim_create_namespace('opencode_output') @@ -157,36 +155,6 @@ function M.setup_keymaps(windows) end function M.setup_autocmds(windows, group) - local guard_timer = nil - - local function start_guard_timer() - if guard_timer then - return -- Timer already running - end - guard_timer = Timer.new({ - interval = 1000, -- 1 second - repeat_timer = true, - on_tick = function() - if state.windows and vim.api.nvim_win_is_valid(state.windows.output_win) then - topbar.render() - return true - end - return false - end, - }) - guard_timer:start_and_tick() - end - - local function stop_guard_timer() - if guard_timer then - guard_timer:stop() - guard_timer = nil - end - end - - -- Start the timer when the window is created (visible) and tick immediately - start_guard_timer() - vim.api.nvim_create_autocmd('WinEnter', { group = group, buffer = windows.output_buf, @@ -207,15 +175,6 @@ function M.setup_autocmds(windows, group) end, }) - -- Stop the timer when the window is closed - vim.api.nvim_create_autocmd('WinClosed', { - group = group, - pattern = tostring(windows.output_win), - callback = function() - stop_guard_timer() - end, - }) - state.subscribe('current_permission', function() require('opencode.keymap').toggle_permission_keymap(windows.output_buf) end) diff --git a/lua/opencode/ui/prompt_guard_indicator.lua b/lua/opencode/ui/prompt_guard_indicator.lua index 4c255260..546ab83c 100644 --- a/lua/opencode/ui/prompt_guard_indicator.lua +++ b/lua/opencode/ui/prompt_guard_indicator.lua @@ -1,6 +1,5 @@ local M = {} -local state = require('opencode.state') local config = require('opencode.config') local util = require('opencode.util') local icons = require('opencode.ui.icons') diff --git a/lua/opencode/ui/topbar.lua b/lua/opencode/ui/topbar.lua index 4d0bf339..f7a7bbf0 100644 --- a/lua/opencode/ui/topbar.lua +++ b/lua/opencode/ui/topbar.lua @@ -137,6 +137,7 @@ function M.setup() state.subscribe('current_mode', on_change) state.subscribe('current_model', on_change) state.subscribe('active_session', on_change) + state.subscribe('is_opencode_focused', on_change) M.render() end @@ -144,5 +145,6 @@ function M.close() state.unsubscribe('current_mode', on_change) state.unsubscribe('current_model', on_change) state.unsubscribe('active_session', on_change) + state.unsubscribe('is_opencode_focused', on_change) end return M