updates
ober
4b51f602228edad1b63e6558a2f3677e3be467b2
--- a/src/jcode/ui/tui-ffi.ss +++ b/src/jcode/ui/tui-ffi.ss @@ -179,19 +179,32 @@ (def TB_KEY_MOUSE_WHEEL_DOWN (- #xFFFF 28)) ;; Ctrl keys -(def TB_KEY_CTRL_A #x01) (def TB_KEY_CTRL_B #x02) -(def TB_KEY_CTRL_C #x03) (def TB_KEY_CTRL_D #x04) -(def TB_KEY_CTRL_E #x05) (def TB_KEY_CTRL_F #x06) -(def TB_KEY_CTRL_G #x07) (def TB_KEY_CTRL_H #x08) -(def TB_KEY_CTRL_I #x09) (def TB_KEY_CTRL_J #x0A) -(def TB_KEY_CTRL_K #x0B) (def TB_KEY_CTRL_L #x0C) -(def TB_KEY_CTRL_M #x0D) (def TB_KEY_CTRL_N #x0E) -(def TB_KEY_CTRL_O #x0F) (def TB_KEY_CTRL_P #x10) -(def TB_KEY_CTRL_Q #x11) (def TB_KEY_CTRL_R #x12) -(def TB_KEY_CTRL_S #x13) (def TB_KEY_CTRL_T #x14) -(def TB_KEY_CTRL_U #x15) (def TB_KEY_CTRL_V #x16) -(def TB_KEY_CTRL_W #x17) (def TB_KEY_CTRL_X #x18) -(def TB_KEY_CTRL_Y #x19) (def TB_KEY_CTRL_Z #x1A) +(def TB_KEY_CTRL_A #x01) +(def TB_KEY_CTRL_B #x02) +(def TB_KEY_CTRL_C #x03) +(def TB_KEY_CTRL_D #x04) +(def TB_KEY_CTRL_E #x05) +(def TB_KEY_CTRL_F #x06) +(def TB_KEY_CTRL_G #x07) +(def TB_KEY_CTRL_H #x08) +(def TB_KEY_CTRL_I #x09) +(def TB_KEY_CTRL_J #x0A) +(def TB_KEY_CTRL_K #x0B) +(def TB_KEY_CTRL_L #x0C) +(def TB_KEY_CTRL_M #x0D) +(def TB_KEY_CTRL_N #x0E) +(def TB_KEY_CTRL_O #x0F) +(def TB_KEY_CTRL_P #x10) +(def TB_KEY_CTRL_Q #x11) +(def TB_KEY_CTRL_R #x12) +(def TB_KEY_CTRL_S #x13) +(def TB_KEY_CTRL_T #x14) +(def TB_KEY_CTRL_U #x15) +(def TB_KEY_CTRL_V #x16) +(def TB_KEY_CTRL_W #x17) +(def TB_KEY_CTRL_X #x18) +(def TB_KEY_CTRL_Y #x19) +(def TB_KEY_CTRL_Z #x1A) ;; Modifiers (def TB_MOD_ALT #x01) --- a/src/jcode/ui/tui-message.ss +++ b/src/jcode/ui/tui-message.ss @@ -142,7 +142,7 @@ (def (reflow-message! msg width) "Re-render message content into lines for given terminal width." (let ((lines (render-content (msg-block-role msg) - (msg-block-content msg) + (strip-terminal-escapes (msg-block-content msg)) (msg-block-tool-name msg) (msg-block-tool-status msg) (msg-block-collapsed? msg) @@ -155,6 +155,62 @@ (msg-block-height-set! msg (length wrapped)) (msg-block-wrap-width-set! msg width)))) +(def *esc-char* (integer->char #x1b)) +(def *bel-char* (integer->char #x07)) + +(def (ansi-final-byte? ch) + (let ((n (char->integer ch))) + (and (>= n #x40) (<= n #x7e)))) + +(def (skip-csi text pos len) + (let loop ((i pos)) + (cond + ((>= i len) len) + ((ansi-final-byte? (string-ref text i)) (+ i 1)) + (else (loop (+ i 1)))))) + +(def (skip-osc text pos len) + (let loop ((i pos)) + (cond + ((>= i len) len) + ((char=? (string-ref text i) *bel-char*) (+ i 1)) + ((and (char=? (string-ref text i) *esc-char*) + (< (+ i 1) len) + (char=? (string-ref text (+ i 1)) #\\)) + (+ i 2)) + (else (loop (+ i 1)))))) + +(def (strip-terminal-escapes text) + "Remove ANSI/control sequences before rendering text through termbox." + (let ((out (open-output-string)) + (len (string-length text))) + (let loop ((i 0)) + (when (< i len) + (let ((ch (string-ref text i))) + (cond + ((char=? ch *esc-char*) + (let ((next (+ i 1))) + (cond + ((>= next len) + (loop next)) + ((char=? (string-ref text next) #\[) + (loop (skip-csi text (+ next 1) len))) + ((char=? (string-ref text next) #\]) + (loop (skip-osc text (+ next 1) len))) + (else + (loop (min len (+ i 2))))))) + ((or (< (char->integer ch) #x20) + (= (char->integer ch) #x7f)) + (if (or (char=? ch #\newline) (char=? ch #\tab)) + (begin + (write-char ch out) + (loop (+ i 1))) + (loop (+ i 1)))) + (else + (write-char ch out) + (loop (+ i 1))))))) + (get-output-string out))) + (def (render-content role content tool-name tool-status collapsed? metadata thinking thinking-collapsed? width) (case role ((user) (render-user-content content)) --- a/src/jcode/ui/tui-sidebar.ss +++ b/src/jcode/ui/tui-sidebar.ss @@ -49,41 +49,57 @@ (cloop (+ col 1)))) (rloop (+ row 1)))) - ;; Draw divider line on the left edge - (let dloop ((row y)) - (when (< row (+ y height)) - (tb-change-cell! x row (char->integer #\│) dfg bg) - (dloop (+ row 1)))) + (when (> height 0) + (let* ((bottom-row (- (+ y height) 1)) + (content-max-row (max y bottom-row))) + ;; Draw divider line on the left edge. The final row is reserved + ;; for the bottom border so the sidebar closes cleanly above status. + (let dloop ((row y)) + (when (< row bottom-row) + (tb-change-cell! x row (char->integer #\│) dfg bg) + (dloop (+ row 1)))) + + (let* ((cx (+ x 2)) ;; content start x (past divider + padding) + (cw (- width 3)) ;; content width + (max-row content-max-row) + ;; Sessions section + (row (render-section! "Sessions" x y width max-row tfg tbg dfg bg #t)) + (row (render-sessions! sidebar cx row cw max-row fg bg)) + ;; Files section + (row (render-section! "Files Changed" x row width max-row tfg tbg dfg bg #f)) + (row (render-files! sidebar cx row cw max-row fg bg)) + ;; System section (above Tools so CPU/MEM/GPU stay visible) + (row (render-section! "System" x row width max-row tfg tbg dfg bg #f)) + (row (render-system! sysmon memstats cx row cw max-row fg bg)) + ;; Tools section + (row (render-section! "Tools" x row width max-row tfg tbg dfg bg #f)) + (row (render-tools! sidebar cx row cw max-row fg bg)) + ;; Connections section + (row (render-section! "Connections" x row width max-row tfg tbg dfg bg #f))) + (render-connections! sidebar cx row cw max-row fg bg)) + + (render-sidebar-bottom! x bottom-row width dfg bg))))) - (let* ((cx (+ x 2)) ;; content start x (past divider + padding) - (cw (- width 3)) ;; content width - (max-row (+ y height)) - ;; Sessions section - (row (render-section! "Sessions" cx y cw max-row tfg tbg)) - (row (render-sessions! sidebar cx row cw max-row fg bg)) - ;; Files section - (row (+ row 1)) - (row (render-section! "Files Changed" cx row cw max-row tfg tbg)) - (row (render-files! sidebar cx row cw max-row fg bg)) - ;; System section (above Tools so CPU/MEM/GPU stay visible) - (row (+ row 1)) - (row (render-section! "System" cx row cw max-row tfg tbg)) - (row (render-system! sysmon memstats cx row cw max-row fg bg)) - ;; Tools section - (row (+ row 1)) - (row (render-section! "Tools" cx row cw max-row tfg tbg)) - (row (render-tools! sidebar cx row cw max-row fg bg)) - ;; Connections section - (row (+ row 1)) - (row (render-section! "Connections" cx row cw max-row tfg tbg))) - (render-connections! sidebar cx row cw max-row fg bg)))) +(def (render-sidebar-bottom! x row width dfg bg) + (when (> width 0) + (tb-change-cell! x row (char->integer #\└) dfg bg) + (when (> width 1) + (tb-print! (+ x 1) row dfg bg (make-string (- width 1) #\─))))) -(def (render-section! title x row width max-row tfg tbg) +(def (render-section! title x row width max-row tfg tbg dfg bg first?) (when (< row max-row) - (tb-print! x row tfg tbg title) + (tb-change-cell! x row + (char->integer (if first? #\┌ #\├)) + dfg bg) + (when (> width 1) + (tb-print! (+ x 1) row dfg bg (make-string (- width 1) #\─))) (when (< (+ row 1) max-row) - (tb-print! x (+ row 1) (face-fg-attr 'horizontal-rule) tbg - (make-string (min width 20) #\─)))) + (let* ((title-x (+ x 2)) + (title-width (max 1 (- width 3))) + (display-title (if (> (string-length title) title-width) + (truncate-string title title-width) + title))) + (tb-print! title-x (+ row 1) tfg tbg display-title)))) (+ row 2)) (def (render-sessions! sidebar x row width max-row fg bg) --- a/test/run.ss +++ b/test/run.ss @@ -36,6 +36,7 @@ (jcode core verified-run) (jcode proxy convert) (jcode proxy handler) + (jcode ui tui-message) (jcode core slot-worker) (jcode proxy server) (jcode eval scenario) @@ -96,6 +97,12 @@ (with-output-to-string (lambda () (display-condition e)))]) (call-with-values thunk (lambda vals #f)))) +(define (msg-block-plain-text m) + (apply string-append + (map (lambda (line) + (apply string-append (map car line))) + (msg-block-lines m)))) + (define (read-test-http-request in) (let header-loop ([content-length 0]) (let ([line (get-line in)]) @@ -242,6 +249,20 @@ (let ([r (tool-execute "bash" (args "command" "exit 42"))]) (check-pred! "bash non-zero exit shown" r (lambda (s) (str-contains? s "Exit code")))) +;; ── TUI rendering ───────────────────────────────────────────────── + +(section "=== tui rendering ===") + +(let* ([esc (integer->char #x1b)] + [colored (string-append "Error: " + (string esc) "[91m" + (string esc) "[1mboom" + (string esc) "[0m ok")] + [m (msg-block-assistant colored)]) + (check! "assistant strips ansi escapes" + (msg-block-plain-text m) + "Error: boom ok")) + ;; ── Text-form tool call recovery ────────────────────────────────── ;; Local Ollama-hosted GGUF models (fine-tuned Qwen/Hermes etc.) often ;; emit tool calls as text in the content stream instead of via the