Skip to content

Commit 36d313d

Browse files
authored
fix(agent): prevent hidden agents from switching the active mode (#382)
* fix(agent): prevent hidden agents from becoming the active mode When a slash command uses a custom hidden agent, the plugin was switching the global current_mode to that hidden agent. Since hidden agents are filtered from the mode picker and M-m cycle, the user would get stuck with no way to switch back. The OpenCode TUI handles this by only switching mode when the agent is visible (not hidden). Match that behavior in both code paths that set the mode: - messaging.send_message: check agent against visible agents list before calling set_mode - agent_model.initialize_current_model: skip restoring mode from messages when the message's agent is hidden * fix(agent): switch mode for visible agents when running user commands run_user_command sends commands via api_client:send_command which bypasses messaging.send_message, so the mode was never updated when a command specified a visible agent. Call switch_to_mode before sending the command so visible agents switch the mode (matching TUI behavior), while hidden agents are silently skipped by switch_to_mode's existing validation.
1 parent 71c519b commit 36d313d

7 files changed

Lines changed: 111 additions & 2 deletions

File tree

lua/opencode/commands/handlers/workflow.lua

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ local input_window = require('opencode.ui.input_window')
1111
local ui = require('opencode.ui.ui')
1212
local nvim = vim['api']
1313
local session_runtime = require('opencode.services.session_runtime')
14+
local agent_model = require('opencode.services.agent_model')
1415

1516
local M = {
1617
actions = {},
@@ -243,6 +244,13 @@ M.actions.run_user_command = Promise.async(function(name, args)
243244
local model = command_cfg.model or state.current_model
244245
local agent = command_cfg.agent or state.current_mode
245246

247+
if command_cfg.agent then
248+
local available_agents = config_file.get_opencode_agents():await()
249+
if vim.tbl_contains(available_agents, agent) then
250+
agent_model.switch_to_mode(agent)
251+
end
252+
end
253+
246254
local active_session = get_active_session_or_warn('No active session')
247255
if not active_session then
248256
return

lua/opencode/services/agent_model.lua

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,10 @@ M.initialize_current_model = Promise.async(function(opts)
174174
state.model.set_model(model_str)
175175
end
176176
if msg.info.mode and state.current_mode ~= msg.info.mode then
177-
state.model.set_mode(msg.info.mode)
177+
local available_agents = config_file.get_opencode_agents():await()
178+
if vim.tbl_contains(available_agents, msg.info.mode) then
179+
state.model.set_mode(msg.info.mode)
180+
end
178181
end
179182
return state.current_model
180183
end

lua/opencode/services/messaging.lua

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ local state = require('opencode.state')
22
local context = require('opencode.context')
33
local util = require('opencode.util')
44
local config = require('opencode.config')
5+
local config_file = require('opencode.config_file')
56
local Promise = require('opencode.promise')
67
local log = require('opencode.log')
78
local agent_model = require('opencode.services.agent_model')
@@ -48,7 +49,10 @@ M.send_message = Promise.async(function(prompt, opts)
4849

4950
if opts.agent then
5051
params.agent = opts.agent
51-
state.model.set_mode(opts.agent)
52+
local available_agents = config_file.get_opencode_agents():await()
53+
if vim.tbl_contains(available_agents, opts.agent) then
54+
state.model.set_mode(opts.agent)
55+
end
5256
end
5357

5458
params.parts = context.format_message(prompt, opts.context):await()

tests/unit/api_spec.lua

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,8 @@ describe('opencode.api', function()
463463
stub(api, 'open_input').invokes(function()
464464
return resolved('done')
465465
end)
466+
local config_file = require('opencode.config_file')
467+
stub(config_file, 'get_opencode_agents').returns(resolved({ 'plan', 'build' }))
466468
end)
467469

468470
it('invokes run with correct model and agent', function()

tests/unit/services_agent_model_spec.lua

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,9 @@ describe('opencode.services.agent_model', function()
144144
it('restores the latest session model and mode when explicitly requested', function()
145145
state.model.set_model('openai/gpt-4.1')
146146
state.model.set_mode('plan')
147+
148+
stub(config_file, 'get_opencode_agents').returns(Promise.new():resolve({ 'plan', 'build' }))
149+
147150
state.renderer.set_messages({
148151
{
149152
info = {
@@ -160,5 +163,33 @@ describe('opencode.services.agent_model', function()
160163
assert.equal('anthropic/claude-3-opus', model)
161164
assert.equal('anthropic/claude-3-opus', state.current_model)
162165
assert.equal('build', state.current_mode)
166+
167+
config_file.get_opencode_agents:revert()
168+
end)
169+
170+
it('does not restore mode to a hidden agent from messages', function()
171+
state.model.set_model('openai/gpt-4.1')
172+
state.model.set_mode('build')
173+
174+
stub(config_file, 'get_opencode_agents').returns(Promise.new():resolve({ 'plan', 'build' }))
175+
176+
state.renderer.set_messages({
177+
{
178+
info = {
179+
id = 'm1',
180+
providerID = 'anthropic',
181+
modelID = 'claude-3-opus',
182+
mode = 'hidden-xyz',
183+
},
184+
},
185+
})
186+
187+
local model = agent_model.initialize_current_model({ restore_from_messages = true }):wait()
188+
189+
assert.equal('anthropic/claude-3-opus', model)
190+
assert.equal('anthropic/claude-3-opus', state.current_model)
191+
assert.equal('build', state.current_mode)
192+
193+
config_file.get_opencode_agents:revert()
163194
end)
164195
end)

tests/unit/services_messaging_spec.lua

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ loaded.services_messaging_spec = true
77

88
local messaging = require('opencode.services.messaging')
99
local session_runtime = require('opencode.services.session_runtime')
10+
local config_file = require('opencode.config_file')
1011
local state = require('opencode.state')
1112
local Promise = require('opencode.promise')
1213
local stub = require('luassert.stub')
@@ -52,6 +53,8 @@ describe('opencode.services.messaging', function()
5253
state.ui.set_windows({ mock = 'windows' })
5354
state.session.set_active({ id = 'sess1' })
5455

56+
stub(config_file, 'get_opencode_agents').returns(Promise.new():resolve({ 'plan', 'build' }))
57+
5558
local create_called = false
5659
state.api_client.create_message = function(_, sid, params)
5760
create_called = true
@@ -69,6 +72,59 @@ describe('opencode.services.messaging', function()
6972
assert.equal(state.current_model, 'test/model')
7073
assert.is_true(create_called)
7174
state.api_client.create_message = orig
75+
config_file.get_opencode_agents:revert()
76+
end)
77+
78+
it('does not switch mode when agent is hidden', function()
79+
state.ui.set_windows({ mock = 'windows' })
80+
state.session.set_active({ id = 'sess1' })
81+
state.model.set_mode('build')
82+
83+
stub(config_file, 'get_opencode_agents').returns(Promise.new():resolve({ 'plan', 'build' }))
84+
85+
local captured_params = nil
86+
local orig = state.api_client.create_message
87+
state.api_client.create_message = function(_, sid, params)
88+
captured_params = params
89+
return Promise.new():resolve({ id = 'm1' })
90+
end
91+
92+
messaging.send_message('hello world', { agent = 'hidden-xyz' })
93+
vim.wait(50, function()
94+
return captured_params ~= nil
95+
end)
96+
97+
assert.equal('build', state.current_mode)
98+
assert.equal('hidden-xyz', captured_params.agent)
99+
100+
state.api_client.create_message = orig
101+
config_file.get_opencode_agents:revert()
102+
end)
103+
104+
it('switches mode when agent is visible', function()
105+
state.ui.set_windows({ mock = 'windows' })
106+
state.session.set_active({ id = 'sess1' })
107+
state.model.set_mode('build')
108+
109+
stub(config_file, 'get_opencode_agents').returns(Promise.new():resolve({ 'plan', 'build' }))
110+
111+
local captured_params = nil
112+
local orig = state.api_client.create_message
113+
state.api_client.create_message = function(_, sid, params)
114+
captured_params = params
115+
return Promise.new():resolve({ id = 'm1' })
116+
end
117+
118+
messaging.send_message('hello world', { agent = 'plan' })
119+
vim.wait(50, function()
120+
return captured_params ~= nil
121+
end)
122+
123+
assert.equal('plan', state.current_mode)
124+
assert.equal('plan', captured_params.agent)
125+
126+
state.api_client.create_message = orig
127+
config_file.get_opencode_agents:revert()
72128
end)
73129

74130
it('increments and decrements user_message_count correctly', function()

tests/unit/services_session_runtime_spec.lua

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,9 @@ describe('opencode.services.session_runtime', function()
650650
it('restores the latest session model and mode when explicitly requested', function()
651651
state.model.set_model('openai/gpt-4.1')
652652
state.model.set_mode('plan')
653+
654+
stub(config_file, 'get_opencode_agents').returns(Promise.new():resolve({ 'plan', 'build' }))
655+
653656
state.renderer.set_messages({
654657
{
655658
info = {
@@ -666,6 +669,8 @@ describe('opencode.services.session_runtime', function()
666669
assert.equal('anthropic/claude-3-opus', model)
667670
assert.equal('anthropic/claude-3-opus', state.current_model)
668671
assert.equal('build', state.current_mode)
672+
673+
config_file.get_opencode_agents:revert()
669674
end)
670675
end)
671676
end)

0 commit comments

Comments
 (0)