Dev Spy

@rebindv0.2.0

See the pointer, window, and color under it while you write a script, record observations and macros, and try AI calls.

Files

Version 0.2.0 (current)

ai.luau

-- The AI console: runs one rb.ai call at a time from the menu, shows the
-- question and the answer on the AI panel, and logs both to a running
-- recording. The prompts come from the settings, or from the clipboard when
-- that is switched on.

local M = {}

local cfg, ai, hud, session
local busy = false
local from_clipboard = false

-- ai is the rb.ai the entry script sees
function M.init(config, rb_ai, view, log)
  cfg, ai, hud, session = config, rb_ai, view, log
end

local function choices()
  local list = {}
  for part in string.gmatch(cfg.ai_choices, "[^,]+") do
    local choice = string.match(part, "^%s*(.-)%s*$")
    if choice ~= "" then
      table.insert(list, choice)
    end
  end
  return list
end

local function prompt(setting)
  if from_clipboard then
    local ok, text = pcall(rb.clipboard.get)
    if ok and text ~= nil and string.match(text, "%S") then
      return text
    end
  end
  return setting
end

local function window()
  return cfg.ai_window ~= "" and cfg.ai_window or nil
end

local function answer(call, a, b, x, y, opts)
  if call == "decide" then
    return {
      answer = { { text = a, tail = string.format("  confidence %.2f", b) } },
      note = { "choices", table.concat(opts.choices, ", ") },
    }, { choice = a, confidence = b }
  elseif call == "find" then
    if a == nil then
      return { answer = { { text = "not found", role = "warning" } } }, {
        point = false,
      }
    end
    hud.marker(a.x, a.y)
    return {
      answer = {
        {
          text = string.format("%d, %d", math.floor(a.x), math.floor(a.y)),
          tail = "  marked for 5 s",
        },
      },
    }, { point = a }
  end
  -- four lines at most; the panel cuts each to its width by character
  local lines, all = {}, string.split(a, "\n")
  for i = 1, math.min(#all, 4) do
    table.insert(lines, { text = all[i] })
  end
  if #all > 4 then
    table.insert(
      lines,
      { text = "+" .. (#all - 4) .. " more lines", role = "dim" }
    )
  end
  if not string.match(a, "%S") then
    lines = { { text = "no text", role = "warning" } }
  end
  local note = opts.at
      and { "where", string.format("400×160 around %d, %d", x, y) }
    or { "where", opts.window }
  return { answer = lines, note = note }, { text = a }
end

-- call is decide, find or read; scope "pointer" reads around the pointer
function M.run(call, scope)
  if busy then
    hud.toast("an AI call is still running", "busy")
    return
  end
  local x, y = rb.system.mouse()
  local question, opts, where
  if call == "decide" then
    question = prompt(cfg.ai_question)
    opts = { choices = choices(), window = window(), screen = window() == nil }
    if #opts.choices < 2 then
      hud.toast(
        "Decide needs two or more choices, separated by commas",
        "not run"
      )
      return
    end
  elseif call == "find" then
    question = prompt(cfg.ai_find)
    opts = { window = window() }
  elseif scope == "pointer" then
    question = prompt(cfg.ai_read)
    opts = { at = { x = x, y = y } }
  else
    question = prompt(cfg.ai_read)
    -- with no title to name, read around the pointer instead
    local title = window() or rb.system.window().title
    opts = title ~= "" and { window = title } or { at = { x = x, y = y } }
  end
  where = opts.window
    or (opts.at and "400×160 around the pointer")
    or "the display under the pointer"

  busy = true
  local view = {
    call = call,
    question = question,
    where = where,
    started = rb.system.time(),
  }
  hud.ai(view)
  rb.run(function()
    local ok, a, b = pcall(ai[call], question, opts)
    busy = false
    view.ms = rb.system.time() - view.started
    local entry =
      { kind = "ai", call = call, question = question, ms = view.ms }
    if not ok then
      view.error = tostring(a)
      entry.error = view.error
    else
      local shown, result = answer(call, a, b, x, y, opts)
      view.answer, view.note = shown.answer, shown.note
      entry.result = result
    end
    if opts.window ~= nil then
      entry.window = opts.window
    end
    view.logged = session.note(entry)
    hud.ai(view)
  end)
end

-- the AI console menu; back reopens the main menu, again this one. Each
-- hint names what the call will use.
function M.items(back, again)
  local read_in = window() or "the active window"
  local asks = from_clipboard and "clipboard" or nil
  return {
    {
      label = "Decide",
      hint = asks or cfg.ai_choices,
      action = function()
        M.run("decide")
      end,
    },
    {
      label = "Find",
      hint = asks or cfg.ai_find,
      action = function()
        M.run("find")
      end,
    },
    {
      label = "Read at pointer",
      hint = asks or "400×160",
      action = function()
        M.run("read", "pointer")
      end,
    },
    {
      label = "Read in window",
      hint = asks or read_in,
      action = function()
        M.run("read", "window")
      end,
    },
    {
      label = "Question from clipboard",
      hint = from_clipboard and "on" or "off",
      action = function()
        from_clipboard = not from_clipboard
        again()
      end,
    },
    { label = "Close AI panel", action = hud.ai_close },
    { label = "Back", action = back },
  }
end

return M