Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions lua/opencode/protocols/v2/observation/events.lua
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,19 @@ session_handlers['session.usage.updated'] = function(observation, _, data)
observation:read().session.tokens = tokens
end

session_handlers['session.revert.staged'] = function(observation, _, data)
if type(data.revert) ~= 'table' then
return boundary.diagnostic(observation, 'session', 'session.revert.staged is missing revert')
end
observation:read().session.revert = vim.deepcopy(data.revert)
return true
end

session_handlers['session.revert.cleared'] = function(observation)
observation:read().session.revert = nil
return true
end

---@param observation OpencodeV2Observation
session_handlers['session.deleted'] = function(observation)
observation:read().sync.session = lifecycle.sync_error('session_deleted', 'session was deleted')
Expand Down
15 changes: 14 additions & 1 deletion lua/opencode/protocols/v2/observation/messages.lua
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,13 @@ end
handlers['session.tool.success'] = finish_tool
handlers['session.tool.failed'] = finish_tool

---@return string 'refetch'
local function invalidate_messages()
return 'refetch'
end
handlers['session.revert.committed'] = invalidate_messages
handlers['session.compaction.ended'] = invalidate_messages

---@param observation OpencodeV2Observation
---@param event OpencodeV2Event
---@return boolean changed
Expand All @@ -391,7 +398,13 @@ function M.ingest_event(observation, event)
if not handler or event.data.sessionID ~= observation._session_id then
return false
end
if not handler(observation, event, event.data) then
local result = handler(observation, event, event.data)
if result == 'refetch' then
observation:read().sync.messages = { state = 'stale' }
observation:_start_resource('messages')
return true
end
if not result then
return false
end
observation:read().sync.messages = { state = 'current' }
Expand Down
8 changes: 8 additions & 0 deletions lua/opencode/services/agent_model.lua
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,14 @@ function M.apply_server_selection(model)
end
end

---@param agent? string
function M.apply_server_agent(agent)
if type(agent) ~= 'string' or agent == '' or state.current_mode == agent then
return
end
state.model.set_mode(agent)
end

---Pull the authoritative session model from the server so state converges on
---panel open; live changes arrive via session.model.selected events.
---@return Promise|nil nil when there is no ready connection or active session
Expand Down
15 changes: 12 additions & 3 deletions lua/opencode/services/session_runtime.lua
Original file line number Diff line number Diff line change
Expand Up @@ -122,21 +122,30 @@ function M.setup_done_thinking_events(subscribe)
})
end

---@param enable boolean|nil
local function setup_model_sync_events(enable)
vim.api.nvim_create_augroup('opencode_model_sync', { clear = true })
if not enable then
return
end
vim.api.nvim_create_autocmd('User', {
group = 'opencode_model_sync',
pattern = 'OpencodeEvent:session.model.selected',
pattern = {
'OpencodeEvent:session.model.selected',
'OpencodeEvent:session.agent.selected',
},
callback = function(args)
local props = args.data and args.data.event and args.data.event.properties
local event = args.data and args.data.event
local props = event and event.properties
local session = state.active_session
if not (props and props.sessionID and session and props.sessionID == session.id) then
return
end
agent_model.apply_server_selection(props.model)
if event.type == 'session.agent.selected' then
agent_model.apply_server_agent(props.agent)
else
agent_model.apply_server_selection(props.model)
end
end,
})
end
Expand Down
53 changes: 53 additions & 0 deletions tests/unit/protocol_v2_observation_runtime_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1372,6 +1372,29 @@ describe('V2 protocol Observation runtime', function()
stop()
end)

it('follows revert staging and clearing made in another client', function()
local value = connection()
local streams = install_operations(value)
local observed = value:observe({ id = 'ses-main' })
local notifications = 0
local stop = observed:watch({ 'session' }, function()
notifications = notifications + 1
end)
flush(function()
return observed:read().sync.session.state == 'current'
end)
local before = notifications

emit(streams[1], event('ses-main', 'session.revert.staged', { revert = { messageID = 'msg-1', files = {} } }))
assert.equals('msg-1', observed:read().session.revert.messageID)
assert.equals(before + 1, notifications)

emit(streams[1], event('ses-main', 'session.revert.cleared', {}))
assert.is_nil(observed:read().session.revert)
assert.equals(before + 2, notifications)
stop()
end)

it('validates permission and form replies before calling their operations', function()
local value = connection()
local calls = {}
Expand Down Expand Up @@ -1439,4 +1462,34 @@ describe('V2 protocol Observation runtime', function()
assert.equals(operations, value.operations)
stop()
end)

it('re-pulls the messages snapshot when the server rewrites the message set', function()
local pulls = 0
local value = connection()
local streams = install_operations(value, {
list_messages = function()
pulls = pulls + 1
return resolved({ data = {}, cursor = {} })
end,
})
local observed = value:observe({ id = 'ses-a' })
local stop = observed:watch({ 'messages' }, function() end)
flush(function()
return observed:read().sync.messages.state == 'current'
end)
pulls = 0

emit(streams[1], event('ses-a', 'session.revert.committed', {}))
flush(function()
return observed:read().sync.messages.state == 'current'
end)
assert.is_true(pulls >= 1)

emit(streams[1], event('ses-a', 'session.compaction.ended', {}))
flush(function()
return observed:read().sync.messages.state == 'current'
end)
assert.is_true(pulls >= 2)
stop()
end)
end)
21 changes: 21 additions & 0 deletions tests/unit/services_agent_model_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -314,4 +314,25 @@ describe('opencode.services.agent_model', function()
assert.equal('anthropic/claude-3-opus', state.current_model)
end)
end)

describe('apply_server_agent', function()
it('applies a server agent selection to the current mode', function()
state.store.set('current_mode', 'build')
agent_model.apply_server_agent('plan')
assert.equal('plan', state.current_mode)
end)

it('keeps the local mode when the server selection matches', function()
state.store.set('current_mode', 'build')
agent_model.apply_server_agent('build')
assert.equal('build', state.current_mode)
end)

it('ignores invalid agent refs', function()
state.store.set('current_mode', 'build')
agent_model.apply_server_agent(nil)
agent_model.apply_server_agent('')
assert.equal('build', state.current_mode)
end)
end)
end)
Loading