Luigit
repositories / dotfiles

dotfiles

bugabingas dorkfiles

owned by admin

neovim/lua/bugabinga/pivi/help.lua

Raw
local 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,
}