Export tool calls in saved TUI sessions

ober

34f8ec84334427317b1c5ed15e92ddba2637a448

diff --git a/src/jcode/ui/tui.ss b/src/jcode/ui/tui.ss
index 29703d8..7495b4b 100644
--- a/src/jcode/ui/tui.ss
+++ b/src/jcode/ui/tui.ss
@@ -1641,8 +1641,49 @@
              (msg-block-system (format "Copied last reply to ~a" path)))))))))
 
 ;; ---- /save [path] ----
-;; Dump the visible message thread to a markdown file. Sessions persist in
-;; ~/.jcode/sessions.db but a plain-text artifact is easier to share/diff.
+;; Dump the message thread to a markdown file. Prefer the persisted session
+;; transcript so tool calls/results are present; the visible TUI intentionally
+;; hides most tool traffic to stay readable.
+(def (tool-call->markdown tc port)
+  (display "- " port)
+  (display (tool-call-name tc) port)
+  (display "\n\n```json\n" port)
+  (display (tool-call-arguments tc) port)
+  (display "\n```\n\n" port))
+
+(def (stored-message->markdown msg port)
+  (let ((role (message-role msg))
+        (content (message-content msg))
+        (tcs (message-tool-calls msg)))
+    (cond
+      ((equal? role "system") (void))
+      ((equal? role "user")
+       (display "## user\n\n" port)
+       (display (or content "") port)
+       (display "\n\n" port))
+      ((equal? role "assistant")
+       (display "## assistant\n\n" port)
+       (when (and content (not (string-empty? (string-trim content))))
+         (display content port)
+         (display "\n\n" port))
+       (when (and tcs (pair? tcs))
+         (display "### tool calls\n\n" port)
+         (for-each (lambda (tc) (tool-call->markdown tc port)) tcs)))
+      ((equal? role "tool")
+       (display "### tool result" port)
+       (when (message-tool-call-id msg)
+         (display " " port)
+         (display (message-tool-call-id msg) port))
+       (display "\n\n```\n" port)
+       (display (or content "") port)
+       (display "\n```\n\n" port))
+      (else
+       (display "### " port)
+       (display role port)
+       (display "\n\n" port)
+       (display (or content "") port)
+       (display "\n\n" port)))))
+
 (def (msg-block->markdown blk port)
   (case (msg-block-role blk)
     ((user)
@@ -1687,9 +1728,15 @@
         (lambda (p)
           (display "# jcode session " p) (display sid p)
           (newline p) (newline p)
-          (for-each
-            (lambda (m) (msg-block->markdown m p))
-            (app-state-messages state))))
+          (let ((saved (and (app-state-session-id state)
+                            (session-load (app-state-session-id state)))))
+            (if saved
+              (for-each
+                (lambda (m) (stored-message->markdown m p))
+                (session-messages saved))
+              (for-each
+                (lambda (m) (msg-block->markdown m p))
+                (app-state-messages state))))))
       (add-message! state
         (msg-block-system (format "Saved session to ~a" path))))))
 
@@ -1912,22 +1959,24 @@
   ;; the next reply and corrupted its leading chars (e.g. "Based on" shown as
   ;; "<U+FFFD>ased on"). Keying reuse on content==stream-buf makes streaming
   ;; robust to any preceding block.
-  (let* ((msgs  (app-state-messages state))
-         (last  (and (pair? msgs) (car (reverse msgs))))
-         (reuse? (and last
-                      (streamed-reply-role? (msg-block-role last))
-                      (equal? (msg-block-content last)
-                              (app-state-stream-buf state)))))
-    (tui-log "tui-stream-token: reuse?=~a token-len=~a" reuse? (string-length token))
-    (unless reuse?
-      (tui-log "tui-stream-token: opening fresh assistant block")
-      (app-state-stream-buf-set! state "")
-      (add-message! state (msg-block-assistant "")))
-    (let ((new-buf (string-append (app-state-stream-buf state) token)))
-      (app-state-stream-buf-set! state new-buf)
-      (update-last-assistant! state new-buf)
-      (app-state-scroll-offset-set! state 0)
-      (app-state-dirty?-set! state #t))))
+  (unless (and (string-empty? (app-state-stream-buf state))
+               (string-empty? (string-trim token)))
+    (let* ((msgs  (app-state-messages state))
+           (last  (and (pair? msgs) (car (reverse msgs))))
+           (reuse? (and last
+                        (streamed-reply-role? (msg-block-role last))
+                        (equal? (msg-block-content last)
+                                (app-state-stream-buf state)))))
+      (tui-log "tui-stream-token: reuse?=~a token-len=~a" reuse? (string-length token))
+      (unless reuse?
+        (tui-log "tui-stream-token: opening fresh assistant block")
+        (app-state-stream-buf-set! state "")
+        (add-message! state (msg-block-assistant "")))
+      (let ((new-buf (string-append (app-state-stream-buf state) token)))
+        (app-state-stream-buf-set! state new-buf)
+        (update-last-assistant! state new-buf)
+        (app-state-scroll-offset-set! state 0)
+        (app-state-dirty?-set! state #t)))))
 
 (def (tui-tool-event! state event name args)
   (case event