TUI: /resume <id> resumes a stored session into the current tab

ober

3ca89b067b983e7512753e4e5b5f8857564ffe2d

diff --git a/src/jcode/ui/tui.ss b/src/jcode/ui/tui.ss
index 7de6c71..b03cf46 100644
--- a/src/jcode/ui/tui.ss
+++ b/src/jcode/ui/tui.ss
@@ -562,6 +562,61 @@
   "Toggle between PLAN and BUILD."
   (set-mode! state (if (eq? (current-mode) 'plan) 'build 'plan)))
 
+;; ---- /resume ----
+
+(def (messages->display-blocks msgs)
+  ;; Stored LLM messages -> displayable transcript blocks. Tool calls,
+  ;; tool results and the system prompt are elided -- they are noise
+  ;; when re-opening a conversation; the trace holds the details.
+  (let loop ((ms msgs) (acc '()))
+    (if (null? ms)
+      (reverse acc)
+      (let* ((m (car ms))
+             (role (message-role m))
+             (content (message-content m))
+             (text (if (string? content) content "")))
+        (loop (cdr ms)
+              (cond
+                ((string=? (string-trim text) "") acc)
+                ((equal? role "user")      (cons (msg-block-user text) acc))
+                ((equal? role "assistant") (cons (msg-block-assistant text) acc))
+                (else acc)))))))
+
+(def (find-session-by-prefix id)
+  ;; Exact id first; otherwise a UNIQUE prefix match so you can /resume
+  ;; from the first few chars shown by /sessions.
+  (or (session-load id)
+      (let ((hits (filter
+                    (lambda (s) (string-prefix? id (session-id s)))
+                    (session-list))))
+        (and (= (length hits) 1)
+             (session-load (session-id (car hits)))))))
+
+(def (resume-session! state id)
+  (let ((sess (find-session-by-prefix id)))
+    (cond
+      ((not sess)
+       (add-message! state
+         (msg-block-system
+           (format "Session not found (or ambiguous prefix): ~a -- see /sessions" id))))
+      ((app-state-agent-busy? state)
+       (add-message! state
+         (msg-block-system
+           "Agent is busy -- Esc to interrupt first, or /side and /resume there.")))
+      (else
+       (app-state-session-id-set! state (session-id sess))
+       (app-state-stream-buf-set! state "")
+       (app-state-messages-set! state
+         (append
+           (messages->display-blocks
+             (session-get-messages (session-id sess)))
+           (list (msg-block-system
+                   (format "Resumed session ~a -- ~a"
+                           (session-id sess) (session-title sess))))))
+       (app-state-scroll-offset-set! state 0)
+       (reflow-all! state)
+       (app-state-dirty?-set! state #t)))))
+
 (def (handle-slash-command! state text)
   (let ((cmd (string-trim (substring text 1 (string-length text)))))
     (cond
@@ -598,6 +653,7 @@
                "  /gemini [prompt]    Open/focus a Gemini CLI tab (sessioned)"
                "  /opencode [prompt]  Open/focus an opencode tab (sessioned)"
                "  /grok [prompt]      Open/focus a Grok CLI tab (sessioned)"
+               "  /resume <id>        Resume a stored session in this tab (/sessions lists)"
                "  /side [prompt]      Open a parallel jcode conversation (own tab)"
                "  /tab N              Switch to tab N (also Alt-1..Alt-9)"
                "  /tabs               List open tabs"
@@ -712,6 +768,12 @@
                  (map (lambda (s) (format "  ~a  ~a" (session-id s) (session-title s)))
                       sessions)
                  "\n"))))))
+      ((equal? cmd "resume")
+       (add-message! state
+         (msg-block-system "Usage: /resume <session-id>  (ids from /sessions; a unique prefix works)")))
+      ((string-prefix? "resume " cmd)
+       (resume-session! state
+         (string-trim (substring cmd 7 (string-length cmd)))))
       ((equal? cmd "save")
        (handle-save! state ""))
       ((string-prefix? "save " cmd)