c736d0e129
Phase 2 commit #4 per docs/PHASE2.md §12. Adds M.tool_call_begin(name, args) and M.tool_call_end(content, is_error) for visual parity with the existing exec_begin/exec_end frame. Visual cadence: ─── tool: <name (cyan)> ─── <args, dim, truncated at 200 chars; omitted if empty/"{}"> <content> ─── ok ─── (dim, success) ─── error ─── (red status word inside dim rule, on is_error=true) Same rule glyph (━) and ANSI palette as the exec frame so the user reads tool dispatch and shell dispatch the same way. Smoke-tested all five shapes: success with args / empty args / error / long args truncated / empty content. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
111 lines
3.9 KiB
Lua
111 lines
3.9 KiB
Lua
-- renderer.lua — output formatting and ANSI sequences.
|
|
-- Phase 0: assistant text plain-printed with `CMD: ` lines highlighted;
|
|
-- exec output framed with the exit code on the closing rule.
|
|
-- Phase 1: assistant_delta + assistant_flush for streaming render. CMD:
|
|
-- highlighting in streaming mode is deferred (Q12); deltas print raw, the
|
|
-- §6 substrate `CMD: ` line is still extractable by executor afterwards.
|
|
-- Syntax highlighting hooks land in Phase 6 (was Phase 5 pre-MCP renumber).
|
|
|
|
local M = {}
|
|
|
|
local A = {
|
|
reset = "\27[0m",
|
|
bold = "\27[1m",
|
|
dim = "\27[2m",
|
|
cyan = "\27[36m",
|
|
red = "\27[31m",
|
|
}
|
|
|
|
local function emit(...) io.write(...); io.flush() end
|
|
|
|
-- Print assistant response text. Lines beginning with `CMD: ` (per the §3
|
|
-- substrate-locked extraction marker) are emitted bold+cyan so the user
|
|
-- can spot the suggestion without scanning prose.
|
|
function M.assistant(text)
|
|
for line in ((text or "") .. "\n"):gmatch("([^\n]*)\n") do
|
|
if line:sub(1, 5) == "CMD: " then
|
|
emit(A.bold, A.cyan, line, A.reset, "\n")
|
|
else
|
|
emit(line, "\n")
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Phase 1: executor.exec streams output live to stdout (PTY multiplex), so
|
|
-- the frame is split — exec_begin before the spawn, exec_end after wait().
|
|
-- The body is not re-rendered here; live output lands directly between the
|
|
-- two rules.
|
|
function M.exec_begin()
|
|
emit(A.dim, "─── exec output ───", A.reset, "\n")
|
|
end
|
|
|
|
function M.exec_end(exit_code)
|
|
if exit_code and exit_code ~= 0 then
|
|
emit(A.dim, "─── exit ", A.reset,
|
|
A.red, tostring(exit_code), A.reset,
|
|
A.dim, " ───", A.reset, "\n")
|
|
else
|
|
emit(A.dim, "─── exit 0 ───", A.reset, "\n")
|
|
end
|
|
end
|
|
|
|
-- Single-line dim status (e.g. §8 eviction notice, model switch confirms).
|
|
function M.status(line)
|
|
emit(A.dim, "[aish] ", tostring(line), A.reset, "\n")
|
|
end
|
|
|
|
-- Streaming assistant output. Phase 1: deltas are written raw — the §6 CMD:
|
|
-- highlighting from M.assistant() is not applied incrementally because
|
|
-- mid-line cursor manipulation isn't worth the complexity for Phase 1.
|
|
-- Q12 (PHASE1.md §10) tracks the upgrade. The full assistant text is still
|
|
-- captured by repl.lua and CMD: extraction works against the reassembled
|
|
-- string after the stream ends.
|
|
|
|
local stream_buf = nil -- non-nil while a stream is in progress
|
|
|
|
function M.assistant_delta(chunk)
|
|
if not chunk or chunk == "" then return end
|
|
if stream_buf == nil then stream_buf = "" end
|
|
stream_buf = stream_buf .. chunk
|
|
emit(chunk)
|
|
end
|
|
|
|
function M.assistant_flush()
|
|
if stream_buf == nil then return end
|
|
if not stream_buf:match("\n$") then emit("\n") end
|
|
stream_buf = nil
|
|
end
|
|
|
|
-- Phase 2: MCP tool-call frame. Visual parity with the exec_begin/exec_end
|
|
-- frame so the user reads tool dispatch and shell dispatch the same way.
|
|
-- tool_call_begin renders the top rule + (optionally) the args as a dim
|
|
-- preview; tool_call_end renders the result content followed by a status
|
|
-- rule. Status is "ok" (dim) by default; "error" (red) if is_error is true.
|
|
-- See docs/PHASE2.md §3 renderer.lua row + §4 Tool invocation.
|
|
|
|
function M.tool_call_begin(name, args)
|
|
emit(A.dim, "─── tool: ", A.reset,
|
|
A.cyan, name, A.reset,
|
|
A.dim, " ───", A.reset, "\n")
|
|
if args and args ~= "" and args ~= "{}" then
|
|
local shown = (#args <= 200) and args or (args:sub(1, 197) .. "...")
|
|
emit(A.dim, shown, A.reset, "\n")
|
|
end
|
|
end
|
|
|
|
function M.tool_call_end(content, is_error)
|
|
if content and content ~= "" then
|
|
emit(content)
|
|
if not content:match("\n$") then emit("\n") end
|
|
end
|
|
if is_error then
|
|
emit(A.dim, "─── ", A.reset,
|
|
A.red, "error", A.reset,
|
|
A.dim, " ───", A.reset, "\n")
|
|
else
|
|
emit(A.dim, "─── ok ───", A.reset, "\n")
|
|
end
|
|
end
|
|
|
|
return M
|