repositories / dotfiles
dotfiles
bugabingas dorkfiles
owned by admin
neovim/lua/bugabinga/pivi/help.lua
Rawlocal rpc = require 'bugabinga.pivi.rpc'
local transcript = require 'bugabinga.pivi.transcript'
local context = require 'bugabinga.pivi.context'
local runtime = require 'bugabinga.pivi.runtime'
local entry = require 'bugabinga.pivi.entry'
local render = require 'bugabinga.pivi.render'
local BUFFER_NAME = 'help'
--- Read-only tools. A help run answers about the editor; it never changes it.
local TOOLS = 'read,grep,find,ls,nvim_help,nvim_state,nvim_lua'
local ORIENTATION = table.concat( {
'You answer one short question about Neovim, about pivi, or about Pi itself.',
'The question carries what the user currently sees in Neovim.',
'Use nvim_help for Neovim and plugin documentation instead of recalling it.',
'Answer directly and briefly, and change nothing.',
}, ' ' )
local build_command = function( options )
options = options or {}
local command = {
options.executable or runtime.executable() or 'pi', '--mode', 'rpc',
'--no-session',
'--tools', TOOLS,
'--append-system-prompt', ORIENTATION,
}
local extension = options.extension ~= false and runtime.extension_path() or nil
if extension then
table.insert( command, '-e' )
table.insert( command, extension )
end
local skill = options.skill ~= false and runtime.skill_path() or nil
if skill then
table.insert( command, '--skill' )
table.insert( command, skill )
end
local chosen = options.model or vim.g.pivi_help_model or vim.g.pivi_model
if chosen then
table.insert( command, '--model' )
table.insert( command, chosen )
end
local thinking = options.thinking or vim.g.pivi_thinking
if thinking then
table.insert( command, '--thinking' )
table.insert( command, thinking )
end
for _, argument in ipairs( options.arguments or {} ) do
table.insert( command, argument )
end
return command
end
--- Builds the question sent to the ephemeral run.
--- The preamble describes the visible editor, not the whole project.
local compose = function( question, options )
if options and options.view == false then return question end
local ok, view = pcall( context.visible )
if not ok or not view then return question end
return context.render_visible( view ) .. question
end
--- Answers a question in an independent ephemeral run.
--- It owns its own process, buffer, and lifetime, so no working session's
--- history, queue, or status is touched.
--- @param question string
--- @param options table? model, arguments, spawn, view, on_done
local ask = function( question, options )
vim.validate( 'question', question, 'string' )
options = options or {}
local bufnr = transcript.ensure( BUFFER_NAME )
transcript.clear( bufnr )
local run = { buffer = bufnr, done = false, answer = '', }
--- Entries here follow the transcript contract: the buffer holds the text as
--- authored, and everything a reader sees around it is decoration.
local begin = function( kind, meta )
if run.entry then entry.close( bufnr, run.entry ) end
run.entry = entry.open( bufnr, kind, meta )
run.kind = kind
render.decorate( bufnr, run.entry )
return run.entry
end
local seal = function()
if not run.entry then return end
entry.close( bufnr, run.entry )
render.decorate( bufnr, run.entry )
run.entry = nil
run.kind = nil
end
begin( 'prompt', { text = question, } )
for _, piece in ipairs( vim.split( question, '\n', { plain = true, } ) ) do
entry.line( bufnr, run.entry, piece )
end
seal()
-- The window must exist before its state can be shown.
transcript.show( bufnr )
local state = function( text ) transcript.set_state( bufnr, BUFFER_NAME, text ) end
state 'thinking'
run.client = rpc.start {
command = build_command( options ),
cwd = options.cwd or vim.uv.cwd(),
env = options.env or runtime.bridge_environment(),
spawn = options.spawn,
on_event = function( event )
if event.type == 'message_update' then
local delta = event.assistantMessageEvent or {}
if delta.type == 'text_delta' and ( delta.delta or '' ) ~= '' then
local text = delta.delta
if run.kind ~= 'answer' then
text = text:gsub( '^%s*\n', '' )
if text ~= '' then
begin( 'answer', {} )
state 'answering'
end
end
if text ~= '' then
run.answer = run.answer .. text
entry.write( bufnr, run.entry, text )
end
end
elseif event.type == 'tool_execution_start' then
local subject = type( event.args ) == 'table' and ( event.args.tag or event.args.path or event.args.pattern ) or nil
local described = tostring( event.toolName )
if type( subject ) == 'string' and subject ~= '' then described = described .. ' ' .. subject end
state( described )
begin( 'tool', { tool = event.toolName, args = event.args, call = event.toolCallId, } )
entry.line( bufnr, run.entry, described )
seal()
elseif event.type == 'message_end' and event.message and event.message.errorMessage then
begin( 'error', { error = event.message.errorMessage, } )
entry.line( bufnr, run.entry, tostring( event.message.errorMessage ) )
seal()
elseif event.type == 'agent_settled' then
run.done = true
seal()
state( nil )
run.client.stop()
if options.on_done then options.on_done( run ) end
end
end,
on_exit = function()
run.done = true
seal()
state( nil )
end,
}
run.client.request( { type = 'prompt', message = compose( question, options ), }, function( response )
if response.success then return end
begin( 'error', { error = response.error, } )
entry.line( bufnr, run.entry, tostring( response.error or 'help run rejected' ) )
seal()
run.done = true
state( nil )
run.client.stop()
if options.on_done then options.on_done( run ) end
end )
return run
end
return {
buffer_name = BUFFER_NAME,
tools = TOOLS,
orientation = ORIENTATION,
build_command = build_command,
compose = compose,
ask = ask,
}