From a722f576ac5abcfc19b97a2c110ccf5045ca286f Mon Sep 17 00:00:00 2001 From: Markus Fritsche Date: Sun, 10 May 2026 19:17:27 +0000 Subject: [PATCH] repl + renderer: streaming assistant output (Phase 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit repl.ask_ai now drives broker.chat_stream and pumps each delta into renderer.assistant_delta(delta) as it arrives. renderer.assistant_flush is called when the stream ends to add a trailing newline if missing. The full reassembled response is then handed to executor.extract_cmd_lines for the CMD: confirm-and-execute path (unchanged from Phase 0). renderer.assistant() is kept for non-streaming callers (none in tree right now, but cheap to keep around). assistant_delta/flush share no state with assistant(); they use a module-local stream_buf that tracks the in-progress streamed block. Q12 deferred: incremental CMD: highlighting (cursor-positioning re- render on flush) is not implemented in Phase 1 — deltas emit raw. The §6 CMD: marker is still extractable on the reassembled string post- stream, which is what executor cares about. Renderer's bold+cyan treatment for CMD: lines stays available via M.assistant(). Broker error / SSE-framed api-error path still pops the user turn and restores ctx.pending_exec_output. Order: assistant_flush always runs (even on error) so the cursor lands on a fresh line before the broker- error status renders. Live verification: `Count one to ten` against hossenfelder fast streams deltas through to stdout incrementally; CMD: extraction works on the reassembled string; confirm gate intact. Co-Authored-By: Claude Opus 4.7 (1M context) --- renderer.lua | 29 +++++++++++++++++++++++++++-- repl.lua | 19 ++++++++++++++----- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/renderer.lua b/renderer.lua index 1e1ff67..20d4a43 100644 --- a/renderer.lua +++ b/renderer.lua @@ -1,7 +1,10 @@ -- 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. Syntax --- highlighting hooks land in Phase 6 (was Phase 5 pre-MCP renumber). +-- 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 = {} @@ -48,4 +51,26 @@ 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 diff --git a/repl.lua b/repl.lua index d76b2f8..e398022 100644 --- a/repl.lua +++ b/repl.lua @@ -68,21 +68,30 @@ function M.run(config) end end - -- Send user text to the active model, render the response, and (per - -- §6 + config.shell.confirm_cmd) optionally execute extracted CMD: lines. + -- Send user text to the active model, render the response token-by-token + -- via broker.chat_stream, and (per §6 + config.shell.confirm_cmd) optionally + -- execute extracted CMD: lines on the reassembled full text. local function ask_ai(text) local prev_pending = ctx.pending_exec_output ctx:append_user(text) -- flushes any pending [exec output] as prefix - local resp, err = broker.chat(active_cfg, ctx:to_messages()) - if not resp then + + local parts = {} + local ok, err = broker.chat_stream(active_cfg, ctx:to_messages(), + function(delta) + parts[#parts + 1] = delta + renderer.assistant_delta(delta) + end) + renderer.assistant_flush() + + if not ok then renderer.status("broker error: " .. tostring(err)) table.remove(ctx.turns) -- back out the merged user turn ctx.pending_exec_output = prev_pending -- restore buffered exec output return end + local resp = table.concat(parts) ctx:append({ role = "assistant", content = resp }) status_evictions(ctx:enforce_budget()) - renderer.assistant(resp) for _, cmd in ipairs(executor.extract_cmd_lines(resp)) do local doit