TUI: /resume <id> resumes a stored session into the current tab
ober
3ca89b067b983e7512753e4e5b5f8857564ffe2d
--- 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)