diff options
| author | t <t@tjp.lol> | 2026-10-27 16:25:43 -0600 |
|---|---|---|
| committer | t <t@tjp.lol> | 2026-10-27 16:25:47 -0600 |
| commit | 009d5705a59bcf86f3579a78e25c8b3fcd4cd476 (patch) | |
| tree | c6496041305da98deac9e40531fbeec0874268f1 /init.lua | |
Add panto-rsvp: RSVP reading mode for assistant text
Standalone opt-in extension that plays the latest assistant text block one
word at a time above the input, using the core mount, input-mode and
highlight("markdown") APIs settled in pantograph's rsvp-extension-plan.
Registers /rsvp; users bind it via "ctrl+s" = "/rsvp" under [keys].
Player and config logic are pure Lua with an assert-based self-check run
against a host/timer stub, since the core APIs land separately.
Diffstat (limited to 'init.lua')
| -rw-r--r-- | init.lua | 67 |
1 files changed, 67 insertions, 0 deletions
diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..532152a --- /dev/null +++ b/init.lua @@ -0,0 +1,67 @@ +local config = require("rsvp.config") +local Player = require("rsvp.player").Player + +local function activate() + local panto = require("panto") + local ext = panto.ext + local settings, err = config.read(ext.config and ext.config.rsvp) + if not settings then error("panto-rsvp: " .. err) end + local uv = require("luv") + + local player = Player.new { + settings = settings, + highlight = function(body) return ext.highlight(body, "markdown") end, + width = panto.text.width, + truncate = panto.text.truncate, + style = function() return ext.style() end, + new_timer = function() return uv.new_timer() end, + mount = function(component, placement) return ext.mount(component, placement) end, + input_mode = function(spec) return ext.input_mode(spec) end, + } + + local function subscribe(name, handler) + ext.on(name, handler) + end + + subscribe("session_ready", function() + local ok, text = pcall(function() + local latest + for _, message in ipairs(ext.agent:conversation():messages()) do + if message.role == "assistant" then + for _, block in ipairs(message.blocks or {}) do + if block.type == "text" then latest = block.text or "" end + end + end + end + return latest or "" + end) + player:snapshot_source(ok and text or "") + end) + subscribe("render_assistant_text", function(event) + player:set_source(event.text or "", false, event.index) + end) + subscribe("render_assistant_text_delta", function(event) + if event.text ~= nil then player:update_source(event.text, false, event.index) end + end) + subscribe("render_assistant_text_complete", function(event) + player:update_source(event.text or "", true, event.index) + end) + subscribe("suspend", function() player:suspend() end) + subscribe("turn_start", function() player:cancel("turn") end) + subscribe("turn_interrupt", function() player:cancel("turn") end) + subscribe("session_end", function() player:cancel("session_end") end) + + ext.register_command { + name = "rsvp", + description = "Read the latest assistant text one paragraph at a time.", + handler = function(arguments) + if arguments and arguments ~= "" then return "Usage: /rsvp" end + local ok, why = player:activate() + if not ok then + return "RSVP unavailable" .. (why and (": " .. tostring(why)) or "") + end + end, + } +end + +return { name = "rsvp", activate = activate } |
