TUI: responsiveness during streaming, full completion list, reply timing
ober
40d5406ead5bdd1e819ac5935423b5bc3e9c9006
--- a/src/jcode/ui/tui-ffi.ss +++ b/src/jcode/ui/tui-ffi.ss @@ -115,7 +115,11 @@ (define-tb c-tb-printf "jcode_tb_printf" (int int unsigned-32 unsigned-32 string string) int) (define-tb c-tb-set-input "jcode_tb_set_input_mode" (int) int) (define-tb c-tb-set-output "jcode_tb_set_output_mode" (int) int) -(define-tb c-tb-poll "jcode_tb_poll_event" () int) +;; __collect_safe: blocks indefinitely waiting for input. +(def c-tb-poll + (if (and _shim-loaded (foreign-entry? "jcode_tb_poll_event")) + (foreign-procedure __collect_safe "jcode_tb_poll_event" () int) + (lambda args (error 'tui-ffi "termbox shim not loaded" "jcode_tb_poll_event")))) ;; __collect_safe: releases the GC/scheduler lock during the blocking poll, ;; allowing agent worker threads to run while we wait for terminal input. (def c-tb-peek --- a/src/jcode/ui/tui.ss +++ b/src/jcode/ui/tui.ss @@ -330,10 +330,6 @@ (tui-log "event-loop: drained ~a agent events" n) (app-state-dirty?-set! state #t))) - ;; Force redraw during streaming (for spinner) - (when (app-state-agent-busy? state) - (app-state-dirty?-set! state #t)) - ;; Activity screen refreshes every tick: ages advance and background ;; runs keep logging even when this tab's agent is idle. (when (eq? (app-state-view state) 'activity) @@ -360,11 +356,21 @@ (tui-log " -> mouse key=~a x=~a y=~a" (tui-event-key ev) (tui-event-x ev) (tui-event-y ev)) (handle-mouse! state ev))))) - ;; Redraw if dirty - (when (app-state-dirty? state) - (draw-all! state) - (tb-present!) - (app-state-dirty?-set! state #f)) + ;; Redraw. Full repaint only when content actually changed; while + ;; the agent is busy an otherwise-clean frame repaints JUST the + ;; spinner row. The old behavior forced draw-all! (tb-clear + full + ;; transcript repaint) on every 50ms tick during streaming -- the + ;; main thread spent the whole turn painting and keystrokes queued + ;; behind it. Drained events still set dirty, so streamed text and + ;; tool updates repaint normally. + (cond + ((app-state-dirty? state) + (draw-all! state) + (tb-present!) + (app-state-dirty?-set! state #f)) + ((app-state-agent-busy? state) + (draw-spinner! state) + (tb-present!))) ;; Continue unless quitting (unless (app-state-quit? state) @@ -1244,10 +1250,11 @@ (tab-messages-set! t (append (tab-messages t) (list (msg-block-expert notice)))) (tab-stream-buf-set! t notice))) - ((list 'agent-done) + ((list 'agent-done elapsed) (tab-busy?-set! t #f) (tab-unread?-set! t #t) (tab-stream-buf-set! t "") + (timing-stamp-last-reply! (tab-messages t) elapsed) (toast-add! 'info "Tab" (format "side conversation (tab ~a) finished" (car hit))) (app-state-dirty?-set! state #t)) @@ -1268,6 +1275,26 @@ (app-state-dirty?-set! state #t)) (_ (void))))))) +(def (format-elapsed secs) + ;; "40s", or "2m30s" past a minute. + (if (< secs 60) + (format "~as" secs) + (format "~am~as" (quotient secs 60) (modulo secs 60)))) + +(def (timing-stamp-last-reply! msgs elapsed) + ;; Append the turn's wall time to the final reply block, e.g. "40s" + ;; on its own trailing line. Returns the stamped block or #f. + (let loop ((ms (reverse msgs))) + (cond + ((null? ms) #f) + ((streamed-reply-role? (msg-block-role (car ms))) + (let ((m (car ms))) + (msg-block-content-set! m + (string-append (msg-block-content m) + (format "\n\n~a" (format-elapsed elapsed)))) + m)) + (else (loop (cdr ms)))))) + (def (apply-agent-event-body! state ev) (match ev ((list 'stream-token token) @@ -1284,15 +1311,18 @@ (app-state-agent-busy?-set! state #f) (app-state-active-tools-set! state '()) (app-state-dirty?-set! state #t)) - ((list 'agent-done) - (tui-log "apply-agent-event: agent-done") + ((list 'agent-done elapsed) + (tui-log "apply-agent-event: agent-done elapsed=~a" elapsed) ;; Clear busy FIRST: a finalize error must never leave the spinner stuck. (app-state-agent-busy?-set! state #f) (app-state-active-tools-set! state '()) (guard (e (#t (log-debug "tui" (format "agent-done finalize ERROR: ~a" (with-output-to-string (lambda () (display-condition e))))))) - (finalize-last-assistant! state)) + (finalize-last-assistant! state) + ;; Stamp the turn's wall time at the end of the reply ("40s"). + (let ((m (timing-stamp-last-reply! (app-state-messages state) elapsed))) + (when m (reflow-message! m (msg-area-width state))))) (app-state-scroll-offset-set! state 0) (app-state-dirty?-set! state #t)) ((list 'agent-cancelled) @@ -1375,6 +1405,7 @@ ;; when the TTY's output buffer backs up. (let ((gen (tui-run-gen)) (abort (cons #f #f)) ;; per-run abort cell (Esc on THIS tab only) + (t0 (time-second (current-time))) ;; submit time, for reply stamp (err-port (current-error-port)) (log-lvl (current-log-level))) (register-agent-run! s-id gen abort) @@ -1410,7 +1441,8 @@ (tui-log "worker: calling agent-run") (agent-run s-id text) (tui-log "worker: agent-run returned, sending agent-done") - (send-run-event! s-id gen (list 'agent-done)) + (send-run-event! s-id gen + (list 'agent-done (- (time-second (current-time)) t0))) (tui-log "worker: agent-done sent, worker exiting normally")) (catch (e) (let ((msg (err->string e)))