summaryrefslogtreecommitdiff
path: root/panto-report-status/init.lua
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-10-30 15:02:39 -0600
committert <t@tjp.lol>2026-10-30 15:02:45 -0600
commit9e9eb15555d9a657b36ee6ae9ef208c0049588f5 (patch)
tree1a92f1e4b6ce7ba2d6e49ff1e54bfa7919d1072d /panto-report-status/init.lua
initial commit
Diffstat (limited to 'panto-report-status/init.lua')
-rw-r--r--panto-report-status/init.lua125
1 files changed, 125 insertions, 0 deletions
diff --git a/panto-report-status/init.lua b/panto-report-status/init.lua
new file mode 100644
index 0000000..07c6110
--- /dev/null
+++ b/panto-report-status/init.lua
@@ -0,0 +1,125 @@
+local alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
+local function base64(text)
+ local out = {}
+ for i = 1, #text, 3 do
+ local a, b, c = text:byte(i, i + 2)
+ local n = a * 65536 + (b or 0) * 256 + (c or 0)
+ local function digit(shift)
+ local index = math.floor(n / 2 ^ shift) % 64 + 1
+ return alphabet:sub(index, index)
+ end
+ out[#out + 1] = digit(18) .. digit(12) .. (b and digit(6) or "=") .. (c and digit(0) or "=")
+ end
+ return table.concat(out)
+end
+
+-- Names are diagnostic labels, not arbitrary terminal text. Keep messages short,
+-- ASCII and control-free even when a tool or command supplies an unusual name.
+local function label(name)
+ return (name or "?"):gsub("[^ -~]", "?"):sub(1, 27)
+end
+
+local function command_status(name)
+ if name == "compact" then return "compacting" end
+ if name == "!" or name == "!!" then return "shell" end
+ return "cmd:" .. label(name)
+end
+
+return {
+ name = "report-status",
+ activate = function()
+ local uv = require("luv")
+ local function terminal_key()
+ if uv.guess_handle(1) ~= "tty" then return end
+ local stat = uv.fs_fstat(1)
+ if stat then return table.concat({ stat.dev, stat.ino, stat.rdev }, ":") end
+ end
+ local terminal = terminal_key()
+ if not terminal then return end
+ -- Preserve ownership across /new, but don't let a nested panto overwrite
+ -- or clear its parent's status. A different terminal may report normally.
+ local owner = terminal .. "@" .. string.format("%.0f", uv.os_getpid())
+ local inherited = os.getenv("PANTO_REPORT_STATUS_OWNER")
+ if inherited ~= owner and (inherited or ""):match("^(.*)@") == terminal then return end
+ uv.os_setenv("PANTO_REPORT_STATUS_OWNER", owner)
+
+ local panto = require("panto")
+ local last_state, last_status
+ local foreground_working, foreground_status = false, nil
+ local background_work = 0
+ local settled_state = "idle"
+ local ended = false
+
+ local function report(state, status)
+ if state == last_state and status == last_status then return end
+ -- Print mode can restore a redirected stdout after activation. Never
+ -- put control sequences in a pipe, file, or a different terminal.
+ if terminal_key() ~= terminal then return end
+ local sequence = "\27]7501;state=" .. state .. ":app=panto"
+ if status then sequence = sequence .. ":msg=" .. base64(status) end
+ sequence = sequence .. "\27\\"
+ local offset = 0
+ while offset < #sequence do
+ local written, err = uv.fs_write(1, sequence:sub(offset + 1), -1)
+ if not written or written == 0 then
+ error("panto-report-status: terminal write failed: " .. tostring(err or "short write"))
+ end
+ offset = offset + written
+ end
+ last_state, last_status = state, status
+ end
+
+ local function update()
+ if ended then return end
+ if foreground_working or background_work > 0 then
+ report("working", foreground_working and foreground_status or "background")
+ else
+ report(settled_state)
+ end
+ end
+
+ local function foreground(status)
+ foreground_working, foreground_status, settled_state = true, status, "done"
+ update()
+ end
+
+ local function finish(event)
+ if foreground_working then
+ local reason = event and event.reason
+ settled_state = reason == "failed" and "error"
+ or ((reason == "interrupted" or reason == "exit_requested") and "idle" or "done")
+ end
+ foreground_working, foreground_status = false, nil
+ update()
+ end
+
+ panto.ext.on("session_ready", function()
+ foreground_working, foreground_status, background_work, settled_state = false, nil, 0, "idle"
+ update()
+ end)
+ panto.ext.on("turn_start", function() foreground("thinking") end)
+ panto.ext.on("agent_submission", function() foreground("thinking") end)
+ panto.ext.on("render_thinking", function() foreground("thinking") end)
+ panto.ext.on("command_start", function(event) foreground(command_status(event.command_name)) end)
+ panto.ext.on("command_end", finish)
+ panto.ext.on("render_compaction", function()
+ if foreground_working then foreground("compacting") end
+ end)
+ panto.ext.on("render_tool_details", function(event) foreground("tool:" .. label(event.tool_name)) end)
+ panto.ext.on("render_assistant_text", function() foreground("responding") end)
+ panto.ext.on("turn_end", finish)
+ panto.ext.on("background_work_start", function()
+ if not foreground_working and background_work == 0 then settled_state = "done" end
+ background_work = background_work + 1
+ update()
+ end)
+ panto.ext.on("background_work_end", function()
+ background_work = math.max(0, background_work - 1)
+ update()
+ end)
+ panto.ext.on("session_end", function()
+ ended = true
+ report("clear")
+ end)
+ end,
+}