-- 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 return M