-- 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 -- Frame captured shell-exec output with a top rule + the body + a closing -- rule that carries the exit code (red on non-zero). function M.exec_output(output, exit_code) output = (output or ""):gsub("\n$", "") emit(A.dim, "─── exec output ───", A.reset, "\n") if output ~= "" then emit(output, "\n") end 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