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
1 change: 1 addition & 0 deletions lua/opencode/core.lua
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ M.open = Promise.async(function(opts)
if opts.new_session then
state.active_session = nil
state.last_sent_context = nil
context.unload_attachments()

state.current_model = nil
state.current_mode = nil
Expand Down
6 changes: 3 additions & 3 deletions lua/opencode/ui/completion/context.lua
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ local function add_mentioned_files_items(ctx)
true,
'Select to remove file ' .. filename,
icons.get('file'),
nil,
{ file_path = file },
kind_priority.mentioned_file
)
)
Expand Down Expand Up @@ -277,8 +277,8 @@ local context_source = {
state.current_context_config = context_cfg

if type == 'mentioned_file' then
context.remove_file(item.data.name)
input_win.remove_mention(item.data.name)
local file_path = item.data.additional_data and item.data.additional_data.file_path or item.data.name
context.remove_file(file_path)
elseif type == 'subagent' then
local subagent_name = item.data.name:gsub(' %(agent%)$', '')
context.remove_subagent(subagent_name)
Expand Down
77 changes: 77 additions & 0 deletions lua/opencode/ui/input_window.lua
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ function M.handle_submit()
return
end

if input_content:match('^!') then
M._execute_shell_command(input_content:sub(2))
return
end

local key = config.get_key_for_function('input_window', 'slash_commands') or '/'
if input_content:match('^' .. key) then
M._execute_slash_command(input_content)
Expand All @@ -75,6 +80,76 @@ function M.handle_submit()
require('opencode.core').send_message(input_content)
end

M._execute_shell_command = function(command)
local cmd = command:match('^%s*(.-)%s*$')
if cmd == '' then
return
end

local shell = vim.o.shell
local shell_cmd = { shell, '-c', cmd }

vim.system(shell_cmd, { text = true }, function(result)
vim.schedule(function()
if result.code ~= 0 then
vim.notify('Command failed with exit code ' .. result.code, vim.log.levels.ERROR)
end

local output = result.stdout or ''
if result.stderr and result.stderr ~= '' then
output = output .. '\n' .. result.stderr
end

M._prompt_add_to_context(cmd, output, result.code)
end)
end)
end

M._prompt_add_to_context = function(cmd, output, exit_code)
local output_window = require('opencode.ui.output_window')
if not output_window.mounted() then
return
end

local formatted_output = string.format('$ %s\n%s', cmd, output)
local lines = vim.split(formatted_output, '\n')

output_window.set_lines(lines)

vim.ui.select({ 'Yes', 'No' }, {
prompt = 'Add command + output to context?',
}, function(choice)
if choice == 'Yes' then
local message = string.format('Command: `%s`\nExit code: %d\nOutput:\n```\n%s```', cmd, exit_code, output)
M._append_to_input(message)
end
output_window.clear()
require('opencode.ui.input_window').focus_input()
end)
end

M._append_to_input = function(text)
if not M.mounted() then
return
end

local current_lines = vim.api.nvim_buf_get_lines(state.windows.input_buf, 0, -1, false)
local new_lines = vim.split(text, '\n')

if #current_lines == 1 and current_lines[1] == '' then
vim.api.nvim_buf_set_lines(state.windows.input_buf, 0, -1, false, new_lines)
else
vim.api.nvim_buf_set_lines(state.windows.input_buf, -1, -1, false, { '', '---', '' })
vim.api.nvim_buf_set_lines(state.windows.input_buf, -1, -1, false, new_lines)
end

M.refresh_placeholder(state.windows)
require('opencode.ui.mention').highlight_all_mentions(state.windows.input_buf)

local line_count = vim.api.nvim_buf_line_count(state.windows.input_buf)
vim.api.nvim_win_set_cursor(state.windows.input_win, { line_count, 0 })
end

M._execute_slash_command = function(command)
local slash_commands = require('opencode.api').get_slash_commands():await()
local key = config.get_key_for_function('input_window', 'slash_commands') or '/'
Expand Down Expand Up @@ -160,6 +235,8 @@ function M.refresh_placeholder(windows, input_lines)
vim.api.nvim_buf_set_extmark(windows.input_buf, ns_id, 0, 0, {
virt_text = {
{ 'Type your prompt here... ', 'OpencodeHint' },
{ '!', 'OpencodeInputLegend' },
{ ' shell ', 'OpencodeHint' },
{ slash_key or '/', 'OpencodeInputLegend' },
{ ' commands ', 'OpencodeHint' },
{ mention_key or '@', 'OpencodeInputLegend' },
Expand Down
13 changes: 4 additions & 9 deletions tests/unit/context_completion_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -314,19 +314,12 @@ describe('context completion', function()

it('should remove mentioned file when selected', function()
local remove_file_called = false
local remove_mention_called = false

local context_module = require('opencode.context')
local input_win_module = require('opencode.ui.input_window')

context_module.remove_file = function(name)
remove_file_called = true
assert.are.equal('test.lua', name)
end

input_win_module.remove_mention = function(name)
remove_mention_called = true
assert.are.equal('test.lua', name)
assert.are.equal('/test/file.lua', name)
end

local item = {
Expand All @@ -335,13 +328,15 @@ describe('context completion', function()
type = 'mentioned_file',
name = 'test.lua',
available = true,
additional_data = {
file_path = '/test/file.lua',
},
},
}

source.on_complete(item)

assert.is_true(remove_file_called)
assert.is_true(remove_mention_called)
end)

it('should remove subagent when selected', function()
Expand Down
Loading