Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
7daa6ef
feat: adds code reference picker to navigate file:line references in …
aweis89 Dec 21, 2025
ee47b2b
feat(config): add system prompt for file URI navigation
aweis89 Dec 21, 2025
be83805
fix(reference_picker): clean up display formatting
aweis89 Dec 22, 2025
4fee569
feat(references): parse references on message completion and display …
aweis89 Dec 22, 2025
a76bf27
refactor(references): simplify to only support file:// URIs
aweis89 Dec 22, 2025
14ddd5c
docs(config): clarify system prompt behavior in config.lua
aweis89 Dec 22, 2025
9885d95
feat(config): update system prompt for file referencing in responses
aweis89 Dec 22, 2025
516e91a
feat(ui): enhance file URI parsing to support line ranges
aweis89 Dec 22, 2025
e40731a
fix(core): update guidance for file reference formatting
aweis89 Dec 22, 2025
1612d8f
fix(ui): enhance reference formatting in picker
aweis89 Dec 22, 2025
e93b47c
feat(ui): add preview support to telescope picker
aweis89 Dec 22, 2025
26f9a2e
fix(core): mandate backticks for file references in system prompt
aweis89 Dec 22, 2025
4848e5a
feat(readme): update key bindings and add functionality details
aweis89 Dec 22, 2025
059f818
Merge upstream/main into feat/picker-ai-code-referenes
aweis89 Dec 22, 2025
b522d47
test: regenerate expected snapshots after upstream merge
aweis89 Dec 22, 2025
5144fa7
feat(ui): fix icon merge
aweis89 Dec 22, 2025
afbc637
test: update snapshots after icon fix
aweis89 Dec 22, 2025
fd05c46
feat: add telescope support for code range highlighting
aweis89 Dec 22, 2025
2a65df2
fix: use vim_buffer_vimgrep previewer for range highlighting
aweis89 Dec 22, 2025
7fdcf41
chore: trigger CI rerun
aweis89 Dec 22, 2025
e02416b
feat: add fzf-lua file preview and line navigation support
aweis89 Dec 22, 2025
ddcc145
fix: use nbsp separator for fzf-lua entries (not tab)
aweis89 Dec 22, 2025
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,8 @@ The plugin provides the following actions that can be triggered via keymaps, com
| Close UI windows | `<leader>oq` | `:Opencode close` | `require('opencode.api').close()` |
| Select and load session | `<leader>os` | `:Opencode session select` | `require('opencode.api').select_session()` |
| **Select and load child session** | `<leader>oS` | `:Opencode session select_child` | `require('opencode.api').select_child_session()` |
| Open timeline picker (navigate/undo/redo/fork to message) | - | `:Opencode timeline` | `require('opencode.api').timeline()` |
| Open timeline picker (navigate/undo/redo/fork to message) | `<leader>oT` | `:Opencode timeline` | `require('opencode.api').timeline()` |
| Browse code references from conversation | `gr` (window) | `:Opencode references` / `/references` | `require('opencode.api').references()` |
| Configure provider and model | `<leader>op` | `:Opencode configure provider` | `require('opencode.api').configure_provider()` |
| Open diff view of changes | `<leader>od` | `:Opencode diff open` | `require('opencode.api').diff_open()` |
| Navigate to next file diff | `<leader>o]` | `:Opencode diff next` | `require('opencode.api').diff_next()` |
Expand Down
9 changes: 9 additions & 0 deletions lua/opencode/api.lua
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,10 @@ function M.focus_input()
ui.focus_input({ restore_position = true, start_insert = true })
end

function M.references()
require('opencode.ui.reference_picker').pick()
end

function M.debug_output()
if not config.debug.enabled then
vim.notify('Debugging is not enabled in the config', vim.log.levels.WARN)
Expand Down Expand Up @@ -1228,6 +1232,10 @@ M.commands = {
desc = 'Paste image from clipboard and add to context',
fn = M.paste_image,
},
references = {
desc = 'Browse code references from conversation',
fn = M.references,
},
}

M.slash_commands_map = {
Expand All @@ -1245,6 +1253,7 @@ M.slash_commands_map = {
['/sessions'] = { fn = M.select_session, desc = 'Select session' },
['/share'] = { fn = M.share, desc = 'Share current session' },
['/timeline'] = { fn = M.timeline, desc = 'Open timeline picker' },
['/references'] = { fn = M.references, desc = 'Browse code references from conversation' },
['/undo'] = { fn = M.undo, desc = 'Undo last action' },
['/unshare'] = { fn = M.unshare, desc = 'Unshare current session' },
['/rename'] = { fn = M.rename_session, desc = 'Rename current session' },
Expand Down
2 changes: 2 additions & 0 deletions lua/opencode/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ M.defaults = {
['[['] = { 'prev_message' },
['<tab>'] = { 'toggle_pane', mode = { 'n', 'i' } },
['i'] = { 'focus_input' },
['gr'] = { 'references', desc = 'Browse code references' },
['<leader>oS'] = { 'select_child_session' },
['<leader>oD'] = { 'debug_message' },
['<leader>oO'] = { 'debug_output' },
Expand All @@ -69,6 +70,7 @@ M.defaults = {
['<up>'] = { 'prev_prompt_history', mode = { 'n', 'i' } },
['<down>'] = { 'next_prompt_history', mode = { 'n', 'i' } },
['<M-m>'] = { 'switch_mode', mode = { 'n', 'i' } },
['gr'] = { 'references', desc = 'Browse code references' },
['<leader>oS'] = { 'select_child_session' },
['<leader>oD'] = { 'debug_message' },
['<leader>oO'] = { 'debug_output' },
Expand Down
15 changes: 15 additions & 0 deletions lua/opencode/core.lua
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,21 @@ M.send_message = Promise.async(function(prompt, opts)
state.current_mode = opts.agent
end

params.system = [[
# Code References

**CRITICAL: Always use the file:// URI scheme when referencing files in responses AND wrap them in backticks.**

Format: `file://path/to/file.lua`, `file://path/to/file.lua:42`, or `file://path/to/file.lua:42-50`

Examples:
- CORRECT: "The error is in `file://src/services/process.ts:712`"
- INCORRECT: "The error is in file://src/services/process.ts:712"
- INCORRECT: "The error is in src/services/process.ts:712"

This matches the file:// URI format that the reference picker already parses from your responses, enabling automatic navigation.
]]

params.parts = context.format_message(prompt, opts.context)
M.before_run(opts)

Expand Down
1 change: 1 addition & 0 deletions lua/opencode/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ function M.setup(opts)
require('opencode.event_manager').setup()
require('opencode.context').setup()
require('opencode.ui.context_bar').setup()
require('opencode.ui.reference_picker').setup()
end

return M
11 changes: 11 additions & 0 deletions lua/opencode/types.lua
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@
---@class OpencodeMessage
---@field info MessageInfo Metadata about the message
---@field parts OpencodeMessagePart[] Parts that make up the message
---@field references CodeReference[]|nil Parsed file references from text parts (cached)

---@class MessageInfo
---@field id string Unique message identifier
Expand Down Expand Up @@ -488,3 +489,13 @@
---@field messages number Number of messages reverted
---@field tool_calls number Number of tool calls reverted
---@field files table<string, {additions: number, deletions: number}> Summary of file changes reverted

---@class CodeReference
---@field file_path string Relative or absolute file path
---@field line number|nil Line number (1-indexed)
---@field column number|nil Column number (optional)
---@field message_id string ID of the message containing this reference
---@field match_start number Start position of match in original text
---@field match_end number End position of match in original text
---@field file string Absolute file path (for Snacks picker preview)
---@field pos number[]|nil Position as {line, col} for Snacks picker preview
75 changes: 68 additions & 7 deletions lua/opencode/ui/base_picker.lua
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ local Promise = require('opencode.promise')
---@field title string|fun(): string The picker title
---@field width? number Optional width for the picker (defaults to config or current window width)
---@field multi_selection? table<string, boolean> Actions that support multi-selection
---@field preview? "file"|"none"|false Preview mode: "file" for file preview, "none" or false to disable

---@class TelescopeEntry
---@field value any
Expand Down Expand Up @@ -89,14 +90,28 @@ local function telescope_ui(opts)
---@param item any
---@return TelescopeEntry
local function make_entry(item)
return {
local entry = {
value = item,
display = function(entry)
local formatted = opts.format_fn(entry.value):to_formatted_text()
return displayer(formatted)
end,
ordinal = opts.format_fn(item):to_string(),
}

if type(item) == 'table' then
entry.path = item.file or item.file_path or item.path or item.filename
entry.lnum = item.line or item.lnum
entry.col = item.column or item.col
-- Support line ranges for preview highlighting
if item.end_pos and type(item.end_pos) == 'table' and item.end_pos[1] then
entry.lnend = item.end_pos[1]
end
elseif type(item) == 'string' then
entry.path = item
end

return entry
end

local function refresh_picker()
Expand All @@ -111,6 +126,7 @@ local function telescope_ui(opts)
prompt_title = opts.title,
finder = finders.new_table({ results = opts.items, entry_maker = make_entry }),
sorter = conf.generic_sorter({}),
previewer = opts.preview == 'file' and require('telescope.previewers').vim_buffer_vimgrep.new({}) or nil,
layout_config = opts.width and {
width = opts.width + 7, -- extra space for telescope UI
} or nil,
Expand Down Expand Up @@ -193,9 +209,15 @@ local function fzf_ui(opts)
['--multi'] = has_multi_action and true or nil,
},
_headers = { 'actions' },
-- Enable builtin previewer for file preview support
previewer = opts.preview == 'file' and 'builtin' or nil,
fn_fzf_index = function(line)
-- Strip the appended file:line:col info before matching
-- fzf-lua uses nbsp (U+2002 EN SPACE) as separator
local nbsp = '\xe2\x80\x82'
local display_part = line:match('^([^' .. nbsp .. ']+)') or line
for i, item in ipairs(opts.items) do
if opts.format_fn(item):to_string() == line then
if opts.format_fn(item):to_string() == display_part then
return i
end
end
Expand All @@ -207,7 +229,33 @@ local function fzf_ui(opts)
local function create_finder()
return function(fzf_cb)
for _, item in ipairs(opts.items) do
fzf_cb(opts.format_fn(item):to_string())
local line_str = opts.format_fn(item):to_string()

-- For file preview support, append file:line:col format
-- fzf-lua's builtin previewer automatically parses this format
if opts.preview == 'file' and type(item) == 'table' then
local file_path = item.file_path or item.path or item.filename or item.file
local line = item.line or item.lnum
local col = item.column or item.col

if file_path then
-- fzf-lua parses "path:line:col:" format for preview positioning
local pos_info = file_path
if line then
pos_info = pos_info .. ':' .. tostring(line)
if col then
pos_info = pos_info .. ':' .. tostring(col)
end
pos_info = pos_info .. ':'
end
-- Append position info after nbsp separator (fzf-lua standard)
-- nbsp is U+2002 EN SPACE, not regular tab
local nbsp = '\xe2\x80\x82'
line_str = line_str .. nbsp .. pos_info
end
end

fzf_cb(line_str)
end
fzf_cb()
end
Expand Down Expand Up @@ -341,15 +389,23 @@ end
local function snacks_picker_ui(opts)
local Snacks = require('snacks')

-- Determine if preview is enabled
local has_preview = opts.preview == 'file'

-- Choose layout preset based on preview
local layout_preset = has_preview and 'default' or 'select'

local snack_opts = {
title = opts.title,
layout = {
preset = 'select',
preset = layout_preset,
config = function(layout)
local width = opts.width and (opts.width + 3) or nil -- extra space for snacks UI
layout.layout.width = width
layout.layout.max_width = width
layout.layout.min_width = width
if not has_preview then
layout.layout.width = width
layout.layout.max_width = width
layout.layout.min_width = width
end
return layout
end,
},
Expand Down Expand Up @@ -378,6 +434,11 @@ local function snacks_picker_ui(opts)
},
}

-- Add file preview if enabled
if has_preview then
snack_opts.preview = 'file'
end

snack_opts.win = snack_opts.win or {}
snack_opts.win.input = snack_opts.win.input or { keys = {} }

Expand Down
35 changes: 33 additions & 2 deletions lua/opencode/ui/formatter.lua
Original file line number Diff line number Diff line change
Expand Up @@ -466,8 +466,39 @@ end
---@param output Output Output object to write to
---@param text string
function M._format_assistant_message(output, text)
-- output:add_empty_line()
output:add_lines(vim.split(text, '\n'))
local reference_picker = require('opencode.ui.reference_picker')
local references = reference_picker.parse_references(text, '')

-- If no references, just add the text as-is
if #references == 0 then
output:add_lines(vim.split(text, '\n'))
return
end

-- Sort references by match_start position (ascending)
table.sort(references, function(a, b)
return a.match_start < b.match_start
end)

-- Build a new text with icons inserted before each reference
local result = ''
local last_pos = 1
local ref_icon = icons.get('reference')

for _, ref in ipairs(references) do
-- Add text before this reference
result = result .. text:sub(last_pos, ref.match_start - 1)
-- Add the icon and the reference
result = result .. ref_icon .. text:sub(ref.match_start, ref.match_end)
last_pos = ref.match_end + 1
end

-- Add any remaining text after the last reference
if last_pos <= #text then
result = result .. text:sub(last_pos)
end

output:add_lines(vim.split(result, '\n'))
end

---@param output Output Output object to write to
Expand Down
2 changes: 2 additions & 0 deletions lua/opencode/ui/highlight.lua
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ function M.setup()
vim.api.nvim_set_hl(0, 'OpencodeContextSwitchOn', { link = '@label', default = true })
vim.api.nvim_set_hl(0, 'OpencodePickerTime', { link = 'Comment', default = true })
vim.api.nvim_set_hl(0, 'OpencodeDebugText', { link = 'Comment', default = true })
vim.api.nvim_set_hl(0, 'OpencodeReference', { fg = '#1976D2', default = true })
vim.api.nvim_set_hl(0, 'OpencodeReasoningText', { link = 'Comment', default = true })
else
vim.api.nvim_set_hl(0, 'OpencodeBorder', { fg = '#616161', default = true })
Expand Down Expand Up @@ -65,6 +66,7 @@ function M.setup()
vim.api.nvim_set_hl(0, 'OpencodeContextSwitchOn', { link = '@label', default = true })
vim.api.nvim_set_hl(0, 'OpencodePickerTime', { link = 'Comment', default = true })
vim.api.nvim_set_hl(0, 'OpencodeDebugText', { link = 'Comment', default = true })
vim.api.nvim_set_hl(0, 'OpencodeReference', { fg = '#7AA2F7', default = true })
vim.api.nvim_set_hl(0, 'OpencodeReasoningText', { link = 'Comment', default = true })
end
end
Expand Down
2 changes: 2 additions & 0 deletions lua/opencode/ui/icons.lua
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ local presets = {
folder = ' ',
attached_file = '󰌷 ',
agent = '󰚩 ',
reference = ' ',
reasoning = '󰧑 ',
-- statuses
status_on = ' ',
Expand Down Expand Up @@ -61,6 +62,7 @@ local presets = {
folder = '[@]',
attached_file = '@',
agent = '@',
reference = '@',
-- statuses
status_on = 'ON',
status_off = 'OFF',
Expand Down
Loading