From 2df6f0e2935202c6d10c98426e8c07648804125a Mon Sep 17 00:00:00 2001 From: Anirudha Kulkarni Date: Fri, 3 Jul 2026 15:15:20 +0530 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20generic=20command=20palette=20?= =?UTF-8?q?=E2=80=94=20AGENTS.md=20steering,=20scoped=20review,=20sessions?= =?UTF-8?q?,=20model=20override?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds three commands and one behavior change, all surfacing Codex/byteask capabilities that already existed in the CLI but weren't reachable from the plugin: - ByteAskReview now prompts for scope (uncommitted / base branch / commit / whole repo) instead of always reviewing everything. - ByteAskAgents opens or scaffolds the nearest AGENTS.md, the primary lever for steering the agent's behavior in a repo. - ByteAskSessions surfaces archive/unarchive/delete alongside the existing resume/fork, plus a "browse all" entry. - ByteAskModel sets a transient model/-c override for future invocations without editing Lua config. Updates the CI load-test and AGENTS.md docs to the resulting 11-command set. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/ci.yml | 2 +- AGENTS.md | 11 +-- README.md | 24 +++++- lua/byteask/commands.lua | 169 ++++++++++++++++++++++++++++++++++++++- lua/byteask/config.lua | 17 ++++ lua/byteask/init.lua | 29 ++++++- lua/byteask/util.lua | 35 +++++++- 7 files changed, 268 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9cb8dc8..309e83c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,5 +37,5 @@ jobs: run: | nvim --headless --clean --cmd "set rtp+=$(pwd)" \ -c "lua require('byteask').setup({})" \ - -c "lua for _,n in ipairs({'ByteAsk','ByteAskToggle','ByteAskExec','ByteAskReview','ByteAskApply','ByteAskResume','ByteAskFork','ByteAskFixDiagnostics'}) do assert(vim.fn.exists(':'..n)==2, n..' not registered') end; print('ALL_COMMANDS_OK')" \ + -c "lua for _,n in ipairs({'ByteAsk','ByteAskToggle','ByteAskExec','ByteAskReview','ByteAskApply','ByteAskResume','ByteAskFork','ByteAskFixDiagnostics','ByteAskAgents','ByteAskSessions','ByteAskModel'}) do assert(vim.fn.exists(':'..n)==2, n..' not registered') end; print('ALL_COMMANDS_OK')" \ -c "qa!" diff --git a/AGENTS.md b/AGENTS.md index 91bfdcc..7ff4a38 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -35,9 +35,10 @@ byteask doctor # health check ``` Commands: `:ByteAsk [prompt]`, `:ByteAskExec {instr}` (range-aware — visual -selection becomes context), `:ByteAskReview`, `:ByteAskApply`, -`:ByteAskFixDiagnostics`, `:ByteAskResume[!]`, `:ByteAskFork[!]`. Full list and -config options in `README.md`. +selection becomes context), `:ByteAskReview` (scoped: uncommitted/base/commit/ +repo), `:ByteAskApply`, `:ByteAskFixDiagnostics`, `:ByteAskResume[!]`, +`:ByteAskFork[!]`, `:ByteAskAgents`, `:ByteAskSessions`, `:ByteAskModel[!]`. +Full list and config options in `README.md`. ## If you're cutting a release @@ -67,7 +68,7 @@ gh release view vX.Y.Z --repo ByteAsk/ByteAsk.nvim `.github/workflows/ci.yml` runs three jobs: `stylua --check .` (formatting), `luacheck lua/ plugin/ --globals vim` (lint), and a headless Neovim load-test -that asserts all 8 `:ByteAsk*` commands register. Run these locally before +that asserts all 11 `:ByteAsk*` commands register. Run these locally before pushing to avoid a red CI: ```bash @@ -75,7 +76,7 @@ stylua . # auto-fix formatting luacheck lua/ plugin/ --globals vim # lint (brew install luarocks; luarocks install luacheck) nvim --headless --clean --cmd "set rtp+=$(pwd)" \ -c "lua require('byteask').setup({})" \ - -c "lua for _,n in ipairs({'ByteAsk','ByteAskToggle','ByteAskExec','ByteAskReview','ByteAskApply','ByteAskResume','ByteAskFork','ByteAskFixDiagnostics'}) do assert(vim.fn.exists(':'..n)==2, n..' not registered') end; print('ALL_COMMANDS_OK')" \ + -c "lua for _,n in ipairs({'ByteAsk','ByteAskToggle','ByteAskExec','ByteAskReview','ByteAskApply','ByteAskResume','ByteAskFork','ByteAskFixDiagnostics','ByteAskAgents','ByteAskSessions','ByteAskModel'}) do assert(vim.fn.exists(':'..n)==2, n..' not registered') end; print('ALL_COMMANDS_OK')" \ -c "qa!" ``` diff --git a/README.md b/README.md index f49e428..6e441fa 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,12 @@ This plugin drives the `byteask` CLI from inside Neovim: scratch buffer and can apply the produced diff straight to your working tree. - **Context sends** — pipe the current selection, file, or LSP diagnostics into a one-shot `exec` (e.g. "fix these compiler errors"). -- **Session control** — resume/fork previous sessions. +- **Scoped review** — uncommitted changes, a base branch, a specific commit, or the + whole repo, matching what `byteask review` actually supports. +- **AGENTS.md steering** — jump straight to (or scaffold) the file ByteAsk reads for + repo-local instructions. +- **Full session lifecycle** — resume/fork, plus archive/unarchive/delete. +- **Per-invocation model/config overrides** without editing your Lua config. - `:checkhealth byteask` and a lualine statusline component. > Inspired by [`codex.nvim`](https://github.com/johnseth97/codex.nvim), rebuilt for @@ -40,7 +45,10 @@ This plugin drives the `byteask` CLI from inside Neovim: { 'bx', ':ByteAskExec ', mode = 'n', desc = 'ByteAsk exec' }, { 'bx', ":'<,'>ByteAskExec ", mode = 'v', desc = 'ByteAsk exec (selection)' }, { 'bd', function() require('byteask').fix_diagnostics() end, desc = 'ByteAsk: fix diagnostics' }, - { 'br', function() require('byteask').review() end, desc = 'ByteAsk review' }, + { 'br', function() require('byteask').review() end, desc = 'ByteAsk review (scoped)' }, + { 'ba', function() require('byteask').agents() end, desc = 'ByteAsk: open/create AGENTS.md' }, + { 'bs', function() require('byteask').sessions() end, desc = 'ByteAsk: sessions (archive/unarchive/delete)' }, + { 'bm', function() require('byteask').model() end, desc = 'ByteAsk: set model/config override' }, }, opts = { layout = 'float', -- 'float' | 'panel' | 'tab' @@ -69,10 +77,13 @@ use { | `:ByteAskToggle` | Toggle the ByteAsk window (session survives when hidden) | | `:ByteAskExec {instr}` | Headless `exec`. In visual mode, sends the selection as context | | `:ByteAskFixDiagnostics` | Send current-buffer LSP/compiler diagnostics to `exec` for a fix | -| `:ByteAskReview` | Run `byteask review` on the repository | +| `:ByteAskReview` | Run `byteask review`, scoped via a prompt: uncommitted / base branch / commit / whole repo | | `:ByteAskApply` | Apply the latest agent diff to the working tree (`byteask apply`) | | `:ByteAskResume[!]` | Resume a session (picker; `!` = most recent) | | `:ByteAskFork[!]` | Fork a session (picker; `!` = most recent) | +| `:ByteAskAgents` | Open the nearest `AGENTS.md` (walks up from the current file), or offer to create one | +| `:ByteAskSessions` | Browse all sessions, or archive/unarchive/delete one by id or name | +| `:ByteAskModel[!]` | Set a model / `-c` override for future invocations (`!` clears it) | Inside the ByteAsk window, `` hides it (the session keeps running in the background — reopen with `:ByteAsk`). @@ -120,6 +131,13 @@ require('lualine').setup({ `jobstart`, streaming stdout/stderr into the `byteask://output` scratch buffer. - `resume`/`fork` re-launch the TUI with the corresponding subcommand (model/config overrides are omitted so the restored session keeps its own recorded settings). +- `:ByteAskAgents` never spawns `byteask` — it just opens/creates the file on disk; + ByteAsk picks it up itself on the next turn. +- `:ByteAskSessions`'s "browse all" delegates to `byteask resume --all`'s own picker; + archive/unarchive/delete shell the matching subcommand directly. +- `:ByteAskModel` stores a transient override (session-lifetime, not persisted to + disk) that `apply_common_flags` layers on top of your static `setup()` config for + every subsequent invocation, until cleared with `:ByteAskModel!`. ## Roadmap diff --git a/lua/byteask/commands.lua b/lua/byteask/commands.lua index 0cd6ca7..3e0f8f3 100644 --- a/lua/byteask/commands.lua +++ b/lua/byteask/commands.lua @@ -153,12 +153,175 @@ function M.exec(prompt, extra_context) run_headless(argv, { title = 'exec', apply_after = config.options.auto_apply }) end ---- `byteask review` against the current repository. -function M.review() - local argv = util.build_subcommand({ 'review' }) +local function run_review(flags) + local argv = util.build_subcommand({ 'review' }, flags) run_headless(argv, { title = 'review' }) end +--- `byteask review`, scoped to match what the CLI actually supports +--- (`--uncommitted` / `--base ` / `--commit ` / whole repo). +--- With no scope, prompts via vim.ui.select. +---@param scope string|nil one of 'uncommitted' | 'base' | 'commit' | 'repo' +function M.review(scope) + if not scope then + vim.ui.select( + { 'Uncommitted changes', 'Against base branch', 'Specific commit', 'Whole repo' }, + { prompt = 'ByteAsk review scope:' }, + function(choice) + if not choice then + return + end + local scope_of = { + ['Uncommitted changes'] = 'uncommitted', + ['Against base branch'] = 'base', + ['Specific commit'] = 'commit', + ['Whole repo'] = 'repo', + } + M.review(scope_of[choice]) + end + ) + return + end + + if scope == 'uncommitted' then + run_review({ '--uncommitted' }) + elseif scope == 'base' then + vim.ui.input({ prompt = 'Base branch: ', default = 'main' }, function(branch) + if branch and branch ~= '' then + run_review({ '--base', branch }) + end + end) + elseif scope == 'commit' then + vim.ui.input({ prompt = 'Commit SHA: ' }, function(sha) + if sha and sha ~= '' then + run_review({ '--commit', sha }) + end + end) + else + run_review({}) + end +end + +--- Locate (or offer to create) the nearest AGENTS.md, walking up from the +--- current buffer's directory. Pure editor action: no byteask process is +--- involved, since AGENTS.md is picked up by the agent on its next turn. +function M.agents() + local abs = vim.api.nvim_buf_get_name(0) + local start_dir = (abs and abs ~= '') and vim.fn.fnamemodify(abs, ':p:h') or vim.loop.cwd() + local found = util.find_upward(start_dir, 'AGENTS.md') + if found then + vim.cmd('edit ' .. vim.fn.fnameescape(found)) + return + end + + local target = vim.loop.cwd() .. '/AGENTS.md' + vim.ui.select({ 'Yes', 'No' }, { + prompt = 'No AGENTS.md found. Create one at ' .. vim.fn.fnamemodify(target, ':.') .. '?', + }, function(choice) + if choice ~= 'Yes' then + return + end + vim.fn.writefile({ + '# AGENTS.md', + '', + '', + '', + '', + }, target) + vim.cmd('edit ' .. vim.fn.fnameescape(target)) + end) +end + +--- Session lifecycle beyond resume/fork: browse all sessions (delegates to +--- byteask's own picker), or archive/unarchive/delete by id or name. +function M.sessions() + vim.ui.select( + { 'Browse all sessions', 'Archive a session', 'Unarchive a session', 'Delete a session' }, + { prompt = 'ByteAsk sessions:' }, + function(choice) + if not choice then + return + end + if choice == 'Browse all sessions' then + local argv = util.base_cmd() + argv[#argv + 1] = 'resume' + argv[#argv + 1] = '--all' + require('byteask.terminal').open_argv(argv) + return + end + + local subcommand = ({ + ['Archive a session'] = 'archive', + ['Unarchive a session'] = 'unarchive', + ['Delete a session'] = 'delete', + })[choice] + + vim.ui.input({ prompt = 'Session id or name to ' .. subcommand .. ': ' }, function(id) + if not id or id == '' then + return + end + local argv = util.base_cmd() + argv[#argv + 1] = subcommand + argv[#argv + 1] = id + vim.fn.jobstart(argv, { + cwd = vim.loop.cwd(), + on_exit = function(_, code) + vim.schedule(function() + if code == 0 then + vim.notify('ByteAsk: ' .. subcommand .. 'd session ' .. id .. '.', vim.log.levels.INFO) + else + vim.notify('ByteAsk ' .. subcommand .. ' failed (code ' .. code .. ').', vim.log.levels.ERROR) + end + end) + end, + }) + end) + end + ) +end + +--- Interactively set (or, with clear=true, reset) the model / -c overrides +--- used by every subsequent invocation. Persists for the nvim session; falls +--- back to config.lua defaults when left blank or cleared. +---@param clear boolean|nil +function M.model(clear) + if clear then + config.clear_override() + vim.notify('ByteAsk: cleared model/config overrides.', vim.log.levels.INFO) + return + end + + local current_model = config.override.model or config.options.model or '' + vim.ui.input({ + prompt = 'ByteAsk model (blank = default' + .. (current_model ~= '' and (', current: ' .. current_model) or '') + .. '): ', + default = current_model, + }, function(model) + if model == nil then + return -- cancelled + end + vim.ui.input({ + prompt = 'ByteAsk -c overrides, comma-separated key=value (blank = none): ', + }, function(cfg_str) + if cfg_str == nil then + return -- cancelled + end + local cfg = {} + if cfg_str ~= '' then + for pair in cfg_str:gmatch('[^,]+') do + local key, value = pair:match('^%s*([%w_%.%-]+)%s*=%s*(.-)%s*$') + if key and value then + cfg[key] = value + end + end + end + config.set_override({ model = model ~= '' and model or nil, config = cfg }) + vim.notify('ByteAsk: model/config override set (use :ByteAskModel! to clear).', vim.log.levels.INFO) + end) + end) +end + --- Ask ByteAsk to fix the current buffer's diagnostics via exec. function M.fix_diagnostics() local diags = util.diagnostics_block(0) diff --git a/lua/byteask/config.lua b/lua/byteask/config.lua index bd4c1bb..328776a 100644 --- a/lua/byteask/config.lua +++ b/lua/byteask/config.lua @@ -68,4 +68,21 @@ function M.setup(user) return M.options end +-- Transient model/-c override, set via :ByteAskModel and consulted by +-- util.apply_common_flags on top of M.options. Session-lifetime, not +-- persisted; cleared with :ByteAskModel!. +M.override = { model = nil, config = nil } + +--- Merge fields into the active override (nil model/config leaves that field +--- as-is; pass an explicit {} for config to clear just the config overrides). +---@param o table { model = string|nil, config = table|nil } +function M.set_override(o) + M.override = vim.tbl_deep_extend('force', M.override, o or {}) +end + +--- Clear the transient override, falling back to M.options again. +function M.clear_override() + M.override = { model = nil, config = nil } +end + return M diff --git a/lua/byteask/init.lua b/lua/byteask/init.lua index 525a1e7..eac5057 100644 --- a/lua/byteask/init.lua +++ b/lua/byteask/init.lua @@ -26,8 +26,8 @@ end M.exec = function(prompt, ctx) commands.exec(prompt, ctx) end -M.review = function() - commands.review() +M.review = function(scope) + commands.review(scope) end M.apply = function() commands.apply() @@ -41,6 +41,15 @@ end M.fork = function(last) commands.fork(last) end +M.agents = function() + commands.agents() +end +M.sessions = function() + commands.sessions() +end +M.model = function(clear) + commands.model(clear) +end -- ── Statusline helper (lualine-friendly) ─────────────────────────────────── function M.statusline() @@ -95,8 +104,8 @@ function M._register_commands() end, { nargs = '*', range = true, desc = 'Run byteask exec (headless); range = send selection as context' }) cmd('ByteAskReview', function() - commands.review() - end, { desc = 'Run byteask review on the repository' }) + commands.review() -- no scope: prompts (uncommitted / base branch / commit / whole repo) + end, { desc = 'Run byteask review, scoped via a prompt' }) cmd('ByteAskApply', function() commands.apply() end, { desc = 'Apply the latest ByteAsk diff to the working tree' }) @@ -111,6 +120,18 @@ function M._register_commands() cmd('ByteAskFork', function(a) commands.fork(a.bang) end, { bang = true, desc = 'Fork a ByteAsk session (! = most recent)' }) + + cmd('ByteAskAgents', function() + commands.agents() + end, { desc = 'Open (or create) the nearest AGENTS.md to steer ByteAsk' }) + + cmd('ByteAskSessions', function() + commands.sessions() + end, { desc = 'Browse, archive, unarchive, or delete ByteAsk sessions' }) + + cmd('ByteAskModel', function(a) + commands.model(a.bang) -- :ByteAskModel! clears the override + end, { bang = true, desc = 'Set (or ! = clear) a model/-c override for future ByteAsk invocations' }) end local function register_keymaps() diff --git a/lua/byteask/util.lua b/lua/byteask/util.lua index f08544a..2c43c81 100644 --- a/lua/byteask/util.lua +++ b/lua/byteask/util.lua @@ -36,15 +36,23 @@ function M.is_installed() end --- Append the shared flags (model, -c overrides, extra args) to an argv list. +--- The transient override set via :ByteAskModel (config.override) wins over +--- the static M.options defaults: its model replaces opts.model when set, +--- and its config keys are layered on top of opts.config. ---@param argv string[] ---@return string[] argv (mutated + returned) function M.apply_common_flags(argv) local opts = config.options - if opts.model and opts.model ~= '' then + local override = config.override or {} + + local model = override.model or opts.model + if model and model ~= '' then argv[#argv + 1] = '-m' - argv[#argv + 1] = opts.model + argv[#argv + 1] = model end - for key, value in pairs(opts.config or {}) do + + local cfg = vim.tbl_extend('force', opts.config or {}, override.config or {}) + for key, value in pairs(cfg) do argv[#argv + 1] = '-c' -- Values are parsed as TOML by ByteAsk; strings pass through as literals. argv[#argv + 1] = string.format('%s=%s', key, tostring(value)) @@ -108,6 +116,27 @@ function M.visual_selection() return table.concat(lines, '\n') end +--- Walk upward from `start_dir` looking for `filename`, stopping at the +--- filesystem root. Used to locate the nearest AGENTS.md the way ByteAsk +--- itself resolves it (deeper files take precedence over shallower ones). +---@param start_dir string +---@param filename string +---@return string|nil absolute path if found +function M.find_upward(start_dir, filename) + local dir = vim.fn.fnamemodify(start_dir, ':p:h') + while true do + local candidate = dir .. '/' .. filename + if vim.fn.filereadable(candidate) == 1 then + return candidate + end + local parent = vim.fn.fnamemodify(dir, ':h') + if parent == dir then + return nil -- reached the filesystem root + end + dir = parent + end +end + --- Buffer diagnostics rendered as a compact, model-friendly block. ---@param bufnr integer|nil ---@return string From 75b90d9f06c4e587b16b88bb2d6144e77d8adc54 Mon Sep 17 00:00:00 2001 From: Anirudha Kulkarni Date: Fri, 3 Jul 2026 15:45:52 +0530 Subject: [PATCH 2/2] fix: :ByteAskModel override couldn't be cleared once set MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Self-review before merge caught a real bug: config.set_override used vim.tbl_deep_extend('force', ...) to merge the new override into the old one. Since a nil table field is simply absent (not an explicit unset), blanking the model field or the -c field in a second :ByteAskModel call silently kept the old value — directly contradicting the prompt text ("blank = default" / "blank = none"). Only :ByteAskModel! could clear it. Fixes: - set_override now replaces the override wholesale instead of merging, so a blank field actually clears it. - The model is committed as soon as the first prompt resolves, so cancelling the second (optional) -c prompt can no longer discard a model change that was already typed; the existing config carries over untouched in that case. - The -c prompt now prefills with the currently-set overrides (so replacing wholesale doesn't surprise a user who only meant to add one key). - Malformed -c entries (missing '=') now warn instead of silently vanishing. - Session archive/unarchive/delete failures now include the CLI's stderr instead of just an exit code, and the manual argv construction there gets the same "why no common flags" comment M.apply already documents. Co-Authored-By: Claude Sonnet 5 --- lua/byteask/commands.lua | 45 ++++++++++++++++++++++++++++++++++++---- lua/byteask/config.lua | 17 +++++++++------ 2 files changed, 52 insertions(+), 10 deletions(-) diff --git a/lua/byteask/commands.lua b/lua/byteask/commands.lua index 3e0f8f3..269fafb 100644 --- a/lua/byteask/commands.lua +++ b/lua/byteask/commands.lua @@ -260,17 +260,33 @@ function M.sessions() if not id or id == '' then return end + -- No common flags: archive/unarchive/delete only take a session id, + -- same precedent as M.apply ("No common flags: apply only performs a + -- git-apply and rejects model/-c flags") — so this builds off + -- base_cmd(), not build_subcommand(). local argv = util.base_cmd() argv[#argv + 1] = subcommand argv[#argv + 1] = id + local stderr_lines = {} vim.fn.jobstart(argv, { cwd = vim.loop.cwd(), + on_stderr = function(_, data) + for _, line in ipairs(data or {}) do + if line ~= '' then + stderr_lines[#stderr_lines + 1] = line + end + end + end, on_exit = function(_, code) vim.schedule(function() if code == 0 then vim.notify('ByteAsk: ' .. subcommand .. 'd session ' .. id .. '.', vim.log.levels.INFO) else - vim.notify('ByteAsk ' .. subcommand .. ' failed (code ' .. code .. ').', vim.log.levels.ERROR) + local detail = #stderr_lines > 0 and (': ' .. table.concat(stderr_lines, ' ')) or '' + vim.notify( + 'ByteAsk ' .. subcommand .. ' failed (code ' .. code .. ')' .. detail .. '.', + vim.log.levels.ERROR + ) end end) end, @@ -299,13 +315,27 @@ function M.model(clear) default = current_model, }, function(model) if model == nil then - return -- cancelled + return -- cancelled: leave the override untouched + end + + -- Commit the model right away so cancelling the (optional) -c prompt + -- below can't silently discard it; the existing config override carries + -- over unless the next prompt explicitly replaces it. + config.set_override({ model = model ~= '' and model or nil, config = config.override.config }) + + local current_cfg = {} + for key, value in pairs(config.override.config or {}) do + current_cfg[#current_cfg + 1] = string.format('%s=%s', key, tostring(value)) end + table.sort(current_cfg) + vim.ui.input({ prompt = 'ByteAsk -c overrides, comma-separated key=value (blank = none): ', + default = table.concat(current_cfg, ','), }, function(cfg_str) if cfg_str == nil then - return -- cancelled + vim.notify('ByteAsk: model override set; config overrides left unchanged.', vim.log.levels.INFO) + return -- cancelled: keep the config override as it was end local cfg = {} if cfg_str ~= '' then @@ -313,10 +343,17 @@ function M.model(clear) local key, value = pair:match('^%s*([%w_%.%-]+)%s*=%s*(.-)%s*$') if key and value then cfg[key] = value + else + vim.notify( + "ByteAsk: ignoring malformed -c entry '" .. pair .. "' (expected key=value).", + vim.log.levels.WARN + ) end end end - config.set_override({ model = model ~= '' and model or nil, config = cfg }) + -- Replaces the config override wholesale (blank clears it entirely) — + -- see config.set_override's docstring for why this can't be a merge. + config.set_override({ model = config.override.model, config = cfg }) vim.notify('ByteAsk: model/config override set (use :ByteAskModel! to clear).', vim.log.levels.INFO) end) end) diff --git a/lua/byteask/config.lua b/lua/byteask/config.lua index 328776a..1a7980e 100644 --- a/lua/byteask/config.lua +++ b/lua/byteask/config.lua @@ -71,18 +71,23 @@ end -- Transient model/-c override, set via :ByteAskModel and consulted by -- util.apply_common_flags on top of M.options. Session-lifetime, not -- persisted; cleared with :ByteAskModel!. -M.override = { model = nil, config = nil } - ---- Merge fields into the active override (nil model/config leaves that field ---- as-is; pass an explicit {} for config to clear just the config overrides). +M.override = { model = nil, config = {} } + +--- Replace the active override wholesale — NOT a merge. `model = nil` means +--- "no model override" and `config = {}`/nil means "no config overrides": +--- each :ByteAskModel invocation fully restates the desired override, so a +--- blank field genuinely clears it. (A deep-merge here would make a blank +--- field unable to ever clear a previously-set value, since a nil table +--- field is simply absent from the table rather than an explicit unset.) ---@param o table { model = string|nil, config = table|nil } function M.set_override(o) - M.override = vim.tbl_deep_extend('force', M.override, o or {}) + o = o or {} + M.override = { model = o.model, config = o.config or {} } end --- Clear the transient override, falling back to M.options again. function M.clear_override() - M.override = { model = nil, config = nil } + M.override = { model = nil, config = {} } end return M