TUI: fix mid-word truncation of assistant text via per-block wrap width

ober

8a80d79b05cde17a4ac296dbea976689f29231b6

diff --git a/src/jcode/ui/tui-message.ss b/src/jcode/ui/tui-message.ss
index d01d91d..d0fde0c 100644
--- a/src/jcode/ui/tui-message.ss
+++ b/src/jcode/ui/tui-message.ss
@@ -10,6 +10,7 @@
   msg-block-content-set! msg-block-tool-status-set!
   msg-block-collapsed?-set!
   msg-block-thinking-set! msg-block-thinking-collapsed?-set!
+  msg-block-wrap-width msg-block-wrap-width-set!
   reflow-message!
   render-msg-block!
   msg-block-user msg-block-assistant msg-block-expert msg-block-tool msg-block-error msg-block-system)
@@ -34,18 +35,19 @@
    collapsed?           ;; #t = show summary only (tools)
    metadata             ;; alist
    thinking             ;; string or #f — extracted <think> content (assistant only)
-   thinking-collapsed?) ;; #t = show "Reasoning" header only; #f = expand thinking text
+   thinking-collapsed?  ;; #t = show "Reasoning" header only; #f = expand thinking text
+   wrap-width)          ;; cols the `lines` were last wrapped at; -1 = never wrapped
   transparent: #t)
 
 ;; ---- Constructors ----
 
 (def (msg-block-user text)
-  (let ((m (make-msg-block 'user text '() 0 #f #f #f '() #f #t)))
+  (let ((m (make-msg-block 'user text '() 0 #f #f #f '() #f #t -1)))
     (reflow-message! m 80)
     m))
 
 (def (msg-block-assistant text)
-  (let ((m (make-msg-block 'assistant text '() 0 #f #f #f '() #f #t)))
+  (let ((m (make-msg-block 'assistant text '() 0 #f #f #f '() #f #t -1)))
     (reflow-message! m 80)
     m))
 
@@ -53,24 +55,24 @@
 ;; block but with a distinct left-border colour so an escalated answer is
 ;; visually distinguishable from the primary model's output.
 (def (msg-block-expert text)
-  (let ((m (make-msg-block 'expert text '() 0 #f #f #f '() #f #t)))
+  (let ((m (make-msg-block 'expert text '() 0 #f #f #f '() #f #t -1)))
     (reflow-message! m 80)
     m))
 
 (def (msg-block-tool name status content . meta)
   (let ((m (make-msg-block 'tool content '() 0 name status #t
              (if (null? meta) '() (car meta))
-             #f #t)))
+             #f #t -1)))
     (reflow-message! m 80)
     m))
 
 (def (msg-block-error text)
-  (let ((m (make-msg-block 'error text '() 0 #f #f #f '() #f #t)))
+  (let ((m (make-msg-block 'error text '() 0 #f #f #f '() #f #t -1)))
     (reflow-message! m 80)
     m))
 
 (def (msg-block-system text)
-  (let ((m (make-msg-block 'system text '() 0 #f #f #f '() #f #t)))
+  (let ((m (make-msg-block 'system text '() 0 #f #f #f '() #f #t -1)))
     (reflow-message! m 80)
     m))
 
@@ -149,7 +151,8 @@
                                width)))
     (let ((wrapped (wrap-segment-lines lines width)))
       (msg-block-lines-set! msg wrapped)
-      (msg-block-height-set! msg (length wrapped)))))
+      (msg-block-height-set! msg (length wrapped))
+      (msg-block-wrap-width-set! msg width))))
 
 (def (render-content role content tool-name tool-status collapsed? metadata thinking thinking-collapsed? width)
   (case role
diff --git a/src/jcode/ui/tui.ss b/src/jcode/ui/tui.ss
index d289797..67a1731 100644
--- a/src/jcode/ui/tui.ss
+++ b/src/jcode/ui/tui.ss
@@ -1654,6 +1654,13 @@
                (skip scroll))
       (when (and (pair? msgs) (>= row y))
         (let* ((msg (car msgs))
+               ;; Invariant: a block's `lines` must be wrapped at the exact
+               ;; width we render it at, or render-msg-block! hard-clips the
+               ;; overflow and loses text. The global *last-reflow-width*
+               ;; guard misses blocks reflowed (mid-stream) at a width that
+               ;; later changed; this per-block check closes that gap.
+               (_   (unless (eqv? (msg-block-wrap-width msg) w)
+                      (reflow-message! msg w)))
                (mh (msg-block-height msg)))
           (if (> skip 0)
             (if (>= skip mh)