Fix TUI blocking, terminal reset, /mcp command, user msg display
ober
6d1e33d15e159248276e6438a2e51ac568a7991c
--- a/lib/jcode/ui/cli.sls +++ b/lib/jcode/ui/cli.sls @@ -18,6 +18,7 @@ (jerboa runtime)) (def logger (make-logger "cli")) (def *version* "0.1.0") + (def *cli-mcp-disabled* #f) (def (cli-main args) (let ([t (current-time)]) (random-seed @@ -174,7 +175,7 @@ (cond [(equal? cmd "help") (display - "\nCommands:\n /help Show this help\n /model [name] Show or set model\n /provider [name] Show or set provider\n /plan Switch to PLAN mode (read-only)\n /build Switch to BUILD mode (read+write)\n /mode Show current mode\n /tools List available tools\n /clear Start a new session\n /sessions List saved sessions\n /compact Show message count\n /quit Exit\n\nMulti-line: end a line with \\ to continue on the next line.\n\n")] + "\nCommands:\n /help Show this help\n /model [name] Show or set model\n /provider [name] Show or set provider\n /plan Switch to PLAN mode (read-only)\n /build Switch to BUILD mode (read+write)\n /mode Show current mode\n /mcp Toggle MCP tools on/off\n /tools List available tools\n /clear Start a new session\n /sessions List saved sessions\n /compact Show message count\n /quit Exit\n\nMulti-line: end a line with \\ to continue on the next line.\n\n")] [(equal? cmd "model") (printf "Provider: ~a~n" @@ -207,6 +208,19 @@ (printf "Available tools: ~a~n" (string-join (list-tools) ", "))] + [(equal? cmd "mcp") + (let* ([mcp-tools (filter + (lambda (n) (string-prefix? "mcp_" n)) + (list-tools))] + [hide? (not *cli-mcp-disabled*)]) + (set! *cli-mcp-disabled* hide?) + (for-each + (lambda (n) (set-tool-internal! n hide?)) + mcp-tools) + (printf + "MCP tools ~a (~a tools).~n" + (if hide? "disabled" "enabled") + (length mcp-tools)))] [(or (equal? cmd "plan") (equal? cmd "mode plan")) (current-mode 'plan) (printf @@ -342,50 +356,20 @@ (format-elapsed elapsed)))) (flush-output-port (current-output-port)))) (def (one-shot-mode prompt opts) - (let ([t0 (time-second (current-time))] - [term-state (save-terminal-state)]) - (dynamic-wind - (lambda () (set-cbreak-mode!)) - (lambda () - (try (md-reset!) - (let ([watcher (start-escape-watcher!)]) - (parameterize ([current-stream-cb - interruptible-stream-cb] - [current-tool-cb tool-indicator]) - (agent-chat prompt))) - (md-flush!) - (let ([elapsed (- (time-second (current-time)) t0)]) - (printf - "~n\x1B;[2m(~a)\x1B;[0m~n" - (format-elapsed elapsed))) - (mcp-stop-all!) (exit 0) - (catch - (e) - (if (and (message-condition? e) - (string-contains - (condition-message e) - "stream-aborted")) - (begin - (md-flush!) - (let ([elapsed (- (time-second (current-time)) - t0)]) - (printf - "~n\x1B;[2m(interrupted after ~a)\x1B;[0m~n" - (format-elapsed elapsed))) - (mcp-stop-all!) - (exit 0)) - (begin - (log-error - logger - "error" - `((msg . ,(err->string e)))) - (printf "Error: ~a~n" (format-error e)) - (mcp-stop-all!) - (exit 1)))))) - (lambda () - (set-car! *stream-abort* #t) - (restore-terminal-state! term-state) - (drain-stdin!))))) + (let ([t0 (time-second (current-time))]) + (try (md-reset!) + (parameterize ([current-stream-cb md-stream-token] + [current-tool-cb tool-indicator]) + (agent-chat prompt)) + (md-flush!) + (let ([elapsed (- (time-second (current-time)) t0)]) + (printf + "~n\x1B;[2m(~a)\x1B;[0m~n" + (format-elapsed elapsed))) + (mcp-stop-all!) (exit 0) + (catch (e) (log-error logger "error" `((msg . ,(err->string e)))) + (printf "Error: ~a~n" (format-error e)) (mcp-stop-all!) + (exit 1))))) (def *md-line-buf* "") (def *md-in-code-block* #f) (def *backtick* (integer->char 96)) --- a/lib/jcode/ui/tui-dialog.sls +++ b/lib/jcode/ui/tui-dialog.sls @@ -14,8 +14,8 @@ (except (chezscheme) make-hash-table hash-table? iota \x31;+ \x31;- getenv path-extension path-absolute? thread? make-mutex mutex? mutex-name) - (jcode ui tui-ffi) (jcode ui tui-theme) (jerboa core) - (jerboa runtime)) + (jcode ui tui-ffi) (jerboa core) (jerboa runtime) + (jcode ui tui-theme)) (defstruct dialog (title body actions selected result) --- a/lib/jcode/ui/tui-diff.sls +++ b/lib/jcode/ui/tui-diff.sls @@ -8,8 +8,8 @@ (except (chezscheme) make-hash-table hash-table? iota \x31;+ \x31;- getenv path-extension path-absolute? thread? make-mutex mutex? mutex-name) - (std misc string) (jcode ui tui-theme) (jerboa core) - (jerboa runtime)) + (std misc string) (jerboa core) (jerboa runtime) + (jcode ui tui-theme)) (defstruct diff-line (type text) transparent: #t) (def (diff-parse-lines diff-text) "Parse unified diff string into list of diff-line structs." --- a/lib/jcode/ui/tui-ffi.sls +++ b/lib/jcode/ui/tui-ffi.sls @@ -43,11 +43,10 @@ (jerboa core) (jerboa runtime)) (def _shim-loaded - (or (guard (e [list #t #f]) + (or (guard (e [#t #f]) (load-shared-object "jcode_tui_shim.dylib")) - (guard (e [list #t #f]) - (load-shared-object "jcode_tui_shim.so")) - (guard (e [list #t #f]) + (guard (e [#t #f]) (load-shared-object "jcode_tui_shim.so")) + (guard (e [#t #f]) (load-shared-object (path-join (path-directory (car (command-line))) @@ -60,6 +59,16 @@ (foreign-procedure c-name arg-types ret-type) (lambda args (error 'tui-ffi "termbox shim not loaded" c-name))))) + (def c-tb-peek + (if (and _shim-loaded + (foreign-entry? "jcode_tb_peek_event")) + (foreign-procedure __collect_safe "jcode_tb_peek_event" + (int) + int) + (lambda args + (error 'tui-ffi + "termbox shim not loaded" + "jcode_tb_peek_event")))) (defstruct tui-event (type mod key ch w h x y)) (def (read-event!) (make-tui-event (c-tb-ev-type) (c-tb-ev-mod) (c-tb-ev-key) (c-tb-ev-ch) @@ -237,7 +246,6 @@ (int) int) (define-tb c-tb-poll "jcode_tb_poll_event" () int) - (define-tb c-tb-peek "jcode_tb_peek_event" (int) int) (define-tb c-tb-ev-type "jcode_tb_event_type" () int) (define-tb c-tb-ev-mod "jcode_tb_event_mod" () int) (define-tb c-tb-ev-key "jcode_tb_event_key" () int) --- a/lib/jcode/ui/tui-input.sls +++ b/lib/jcode/ui/tui-input.sls @@ -17,8 +17,8 @@ (except (chezscheme) make-hash-table hash-table? iota \x31;+ \x31;- getenv path-extension path-absolute? thread? make-mutex mutex? mutex-name) - (std misc string) (jcode ui tui-ffi) (jcode ui tui-theme) - (jerboa core) (jerboa runtime)) + (std misc string) (jerboa core) (jerboa runtime) + (jcode ui tui-ffi) (jcode ui tui-theme)) (defstruct input-state (text cursor-pos history history-idx completion --- a/lib/jcode/ui/tui-markdown.sls +++ b/lib/jcode/ui/tui-markdown.sls @@ -9,8 +9,8 @@ (except (chezscheme) make-hash-table hash-table? iota \x31;+ \x31;- getenv path-extension path-absolute? thread? make-mutex mutex? mutex-name) - (std misc string) (jcode ui tui-ffi) (jcode ui tui-theme) - (jerboa core) (jerboa runtime)) + (std misc string) (jerboa core) (jerboa runtime) + (jcode ui tui-ffi) (jcode ui tui-theme)) (defstruct md-state (in-code-block? code-lang) --- a/lib/jcode/ui/tui-message.sls +++ b/lib/jcode/ui/tui-message.sls @@ -17,8 +17,8 @@ getenv path-extension path-absolute? thread? make-mutex mutex? mutex-name) (std misc string) (jcode ui tui-ffi) (jcode ui tui-theme) - (jcode ui tui-markdown) (jcode ui tui-diff) (jerboa core) - (jerboa runtime)) + (jcode ui tui-markdown) (jerboa core) (jerboa runtime) + (jcode ui tui-diff)) (defstruct msg-block (role content lines height tool-name tool-status collapsed? @@ -112,14 +112,17 @@ [(system) (render-system-content content)] [else (list (list (cons content 'default)))])) (def (render-user-content content) - (let ([label-line (list (cons "You:" 'user-label))]) - (let ([text-lines (map (lambda (line) - (list - (cons - (string-append " " line) - 'user-text))) - (string-split content #\newline))]) - (cons label-line text-lines)))) + (let ([lines (string-split content #\newline)]) + (if (null? lines) + (list (list (cons "You:" 'user-label))) + (cons + (list + (cons "You: " 'user-label) + (cons (car lines) 'user-text)) + (map (lambda (line) + (list + (cons (string-append " " line) 'user-text))) + (cdr lines)))))) (def (render-assistant-content content) (if (string-empty? content) '() --- a/lib/jcode/ui/tui-sidebar.sls +++ b/lib/jcode/ui/tui-sidebar.sls @@ -14,8 +14,8 @@ (except (chezscheme) make-hash-table hash-table? iota \x31;+ \x31;- getenv path-extension path-absolute? thread? make-mutex mutex? mutex-name) - (jcode ui tui-ffi) (jcode ui tui-theme) (jerboa core) - (jerboa runtime)) + (jcode ui tui-ffi) (jerboa core) (jerboa runtime) + (jcode ui tui-theme)) (defstruct sidebar-state (sessions current-session files tools mcp lsp) --- a/lib/jcode/ui/tui-status.sls +++ b/lib/jcode/ui/tui-status.sls @@ -8,8 +8,8 @@ (except (chezscheme) make-hash-table hash-table? iota \x31;+ \x31;- getenv path-extension path-absolute? thread? make-mutex mutex? mutex-name) - (std misc string) (jcode ui tui-ffi) (jcode ui tui-theme) - (jerboa core) (jerboa runtime)) + (std misc string) (jerboa core) (jerboa runtime) + (jcode ui tui-ffi) (jcode ui tui-theme)) (def (render-status-bar! x y width provider model tokens-in tokens-out cost cwd mode) "Render the status bar at row y across width columns." --- a/lib/jcode/ui/tui.sls +++ b/lib/jcode/ui/tui.sls @@ -24,6 +24,7 @@ (def *version* "0.1.0") (def *tui-log-port* (make-parameter #f)) (def *main-thread* (make-parameter #f)) + (def *mcp-disabled* #f) (def (tui-log fmt . args) (let ([p (*tui-log-port*)]) (when p @@ -339,6 +340,7 @@ " /clear Start new session" " /sessions List sessions" " /search <term> Search session history" + " /mcp Toggle MCP tools on/off (for local models)" " /theme Cycle theme (Ctrl-T)" " /sidebar Toggle sidebar (Ctrl-B)" " /quit Exit" "" @@ -389,6 +391,23 @@ [(equal? cmd "theme") (cycle-theme!) (add-message! state (msg-block-system "Theme cycled."))] + [(equal? cmd "mcp") + (let* ([mcp-tools (filter + (lambda (n) (string-prefix? "mcp_" n)) + (list-tools))] + [hide? (not *mcp-disabled*)]) + (set! *mcp-disabled* hide?) + (for-each + (lambda (n) (set-tool-internal! n hide?)) + mcp-tools) + (add-message! + state + (msg-block-system + (format + "MCP tools ~a (~a tools). Use /mcp to toggle." + (if hide? "disabled" "enabled") + (length mcp-tools)))) + (refresh-mcp-sidebar! state))] [(equal? cmd "sidebar") (app-state-sidebar-visible?-set! state --- a/src/jcode/ui/cli.ss +++ b/src/jcode/ui/cli.ss @@ -26,6 +26,7 @@ (def logger (make-logger "cli")) (def *version* "0.1.0") +(def *cli-mcp-disabled* #f) (def (cli-main args) ;; Seed with ns+s mixed, clamped to 32 bits (random-seed max) @@ -191,7 +192,7 @@ EXAMPLES: (let ((cmd (string-trim (substring input 1 (string-length input))))) (cond ((equal? cmd "help") - (display "\nCommands:\n /help Show this help\n /model [name] Show or set model\n /provider [name] Show or set provider\n /plan Switch to PLAN mode (read-only)\n /build Switch to BUILD mode (read+write)\n /mode Show current mode\n /tools List available tools\n /clear Start a new session\n /sessions List saved sessions\n /compact Show message count\n /quit Exit\n\nMulti-line: end a line with \\ to continue on the next line.\n\n")) + (display "\nCommands:\n /help Show this help\n /model [name] Show or set model\n /provider [name] Show or set provider\n /plan Switch to PLAN mode (read-only)\n /build Switch to BUILD mode (read+write)\n /mode Show current mode\n /mcp Toggle MCP tools on/off\n /tools List available tools\n /clear Start a new session\n /sessions List saved sessions\n /compact Show message count\n /quit Exit\n\nMulti-line: end a line with \\ to continue on the next line.\n\n")) ((equal? cmd "model") (printf "Provider: ~a~n" (or (current-provider-override) (config-provider))) (printf "Model: ~a~n" (or (current-model-override) (config-model))) @@ -209,6 +210,13 @@ EXAMPLES: (printf "Provider set to: ~a (model: ~a)~n" new-provider (current-model-override)))) ((equal? cmd "tools") (printf "Available tools: ~a~n" (string-join (list-tools) ", "))) + ((equal? cmd "mcp") + (let* ((mcp-tools (filter (lambda (n) (string-prefix? "mcp_" n)) (list-tools))) + (hide? (not *cli-mcp-disabled*))) + (set! *cli-mcp-disabled* hide?) + (for-each (lambda (n) (set-tool-internal! n hide?)) mcp-tools) + (printf "MCP tools ~a (~a tools).~n" + (if hide? "disabled" "enabled") (length mcp-tools)))) ((or (equal? cmd "plan") (equal? cmd "mode plan")) (current-mode 'plan) (printf "\x1b;[1;33mMode: PLAN\x1b;[0m (read-only — write tools disabled)~n")) @@ -339,40 +347,22 @@ EXAMPLES: (flush-output-port (current-output-port)))) (def (one-shot-mode prompt opts) - (let ((t0 (time-second (current-time))) - (term-state (save-terminal-state))) - (dynamic-wind - (lambda () (set-cbreak-mode!)) - (lambda () - (try - (md-reset!) - (let ((watcher (start-escape-watcher!))) - (parameterize ((current-stream-cb interruptible-stream-cb) - (current-tool-cb tool-indicator)) - (agent-chat prompt))) - (md-flush!) - (let ((elapsed (- (time-second (current-time)) t0))) - (printf "~n\x1b;[2m(~a)\x1b;[0m~n" (format-elapsed elapsed))) - (mcp-stop-all!) - (exit 0) - (catch (e) - (if (and (message-condition? e) - (string-contains (condition-message e) "stream-aborted")) - (begin - (md-flush!) - (let ((elapsed (- (time-second (current-time)) t0))) - (printf "~n\x1b;[2m(interrupted after ~a)\x1b;[0m~n" (format-elapsed elapsed))) - (mcp-stop-all!) - (exit 0)) - (begin - (log-error logger "error" `((msg . ,(err->string e)))) - (printf "Error: ~a~n" (format-error e)) - (mcp-stop-all!) - (exit 1)))))) - (lambda () - (set-car! *stream-abort* #t) - (restore-terminal-state! term-state) - (drain-stdin!))))) + (let ((t0 (time-second (current-time)))) + (try + (md-reset!) + (parameterize ((current-stream-cb md-stream-token) + (current-tool-cb tool-indicator)) + (agent-chat prompt)) + (md-flush!) + (let ((elapsed (- (time-second (current-time)) t0))) + (printf "~n\x1b;[2m(~a)\x1b;[0m~n" (format-elapsed elapsed))) + (mcp-stop-all!) + (exit 0) + (catch (e) + (log-error logger "error" `((msg . ,(err->string e)))) + (printf "Error: ~a~n" (format-error e)) + (mcp-stop-all!) + (exit 1))))) ;; --- line-buffered markdown renderer --- --- a/src/jcode/ui/tui-dialog.ss +++ b/src/jcode/ui/tui-dialog.ss @@ -14,6 +14,8 @@ show-list-dialog! render-list-dialog!) (import :jcode/ui/tui-ffi + :jerboa/core + :jerboa/runtime :jcode/ui/tui-theme) ;; ---- Dialog struct ---- --- a/src/jcode/ui/tui-diff.ss +++ b/src/jcode/ui/tui-diff.ss @@ -6,6 +6,8 @@ diff-parse-lines) (import :std/misc/string + :jerboa/core + :jerboa/runtime :jcode/ui/tui-theme) ;; ---- Parse diff text into typed lines ---- --- a/src/jcode/ui/tui-ffi.ss +++ b/src/jcode/ui/tui-ffi.ss @@ -54,7 +54,9 @@ ;; Constants — cursor TB_HIDE_CURSOR) -(import :std/os/path) +(import :std/os/path + :jerboa/core + :jerboa/runtime) ;; ---- Load shared library ---- @@ -92,7 +94,12 @@ (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) -(define-tb c-tb-peek "jcode_tb_peek_event" (int) int) +;; __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 + (if (and _shim-loaded (foreign-entry? "jcode_tb_peek_event")) + (foreign-procedure __collect_safe "jcode_tb_peek_event" (int) int) + (lambda args (error 'tui-ffi "termbox shim not loaded" "jcode_tb_peek_event")))) (define-tb c-tb-ev-type "jcode_tb_event_type" () int) (define-tb c-tb-ev-mod "jcode_tb_event_mod" () int) (define-tb c-tb-ev-key "jcode_tb_event_key" () int) --- a/src/jcode/ui/tui-input.ss +++ b/src/jcode/ui/tui-input.ss @@ -16,6 +16,8 @@ *slash-commands*) (import :std/misc/string + :jerboa/core + :jerboa/runtime :jcode/ui/tui-ffi :jcode/ui/tui-theme) --- a/src/jcode/ui/tui-keys.ss +++ b/src/jcode/ui/tui-keys.ss @@ -5,7 +5,9 @@ event->key-name make-keymap) -(import :jcode/ui/tui-ffi) +(import :jcode/ui/tui-ffi + :jerboa/core + :jerboa/runtime) ;; ---- Keymap ---- --- a/src/jcode/ui/tui-markdown.ss +++ b/src/jcode/ui/tui-markdown.ss @@ -6,6 +6,8 @@ md-state-make md-state-in-code-block? md-state-in-code-block?-set!) (import :std/misc/string + :jerboa/core + :jerboa/runtime :jcode/ui/tui-ffi :jcode/ui/tui-theme) --- a/src/jcode/ui/tui-message.ss +++ b/src/jcode/ui/tui-message.ss @@ -16,6 +16,8 @@ :jcode/ui/tui-ffi :jcode/ui/tui-theme :jcode/ui/tui-markdown + :jerboa/core + :jerboa/runtime :jcode/ui/tui-diff) ;; ---- Message block struct ---- @@ -123,11 +125,17 @@ (else (list (list (cons content 'default)))))) (def (render-user-content content) - (let ((label-line (list (cons "You:" 'user-label)))) - (let ((text-lines (map (lambda (line) - (list (cons (string-append " " line) 'user-text))) - (string-split content #\newline)))) - (cons label-line text-lines)))) + (let ((lines (string-split content #\newline))) + (if (null? lines) + (list (list (cons "You:" 'user-label))) + (cons + ;; First line: "You: <text>" on same line + (list (cons "You: " 'user-label) + (cons (car lines) 'user-text)) + ;; Remaining lines indented + (map (lambda (line) + (list (cons (string-append " " line) 'user-text))) + (cdr lines)))))) (def (render-assistant-content content) (if (string-empty? content) --- a/src/jcode/ui/tui-sidebar.ss +++ b/src/jcode/ui/tui-sidebar.ss @@ -11,6 +11,8 @@ render-sidebar!) (import :jcode/ui/tui-ffi + :jerboa/core + :jerboa/runtime :jcode/ui/tui-theme) ;; ---- Sidebar state ---- --- a/src/jcode/ui/tui-status.ss +++ b/src/jcode/ui/tui-status.ss @@ -4,6 +4,8 @@ (export render-status-bar!) (import :std/misc/string + :jerboa/core + :jerboa/runtime :jcode/ui/tui-ffi :jcode/ui/tui-theme) --- a/src/jcode/ui/tui-theme.ss +++ b/src/jcode/ui/tui-theme.ss @@ -9,7 +9,9 @@ ;; Helpers rgb truecolor) -(import :jcode/ui/tui-ffi) +(import :jcode/ui/tui-ffi + :jerboa/core + :jerboa/runtime) ;; ---- Face struct ---- --- a/src/jcode/ui/tui.ss +++ b/src/jcode/ui/tui.ss @@ -30,7 +30,9 @@ :jcode/tool/git :jcode/mcp/client :jcode/tool/lsp - :jcode/core/plugin) + :jcode/core/plugin + :jerboa/core + :jerboa/runtime) (def logger (make-logger "tui")) (def *version* "0.1.0") @@ -46,6 +48,7 @@ ;; app-state directly — Chez hash tables and lists are not thread-safe under ;; preemptive pthreads, so all state mutation must happen on the main thread. (def *main-thread* (make-parameter #f)) +(def *mcp-disabled* #f) (def (tui-log fmt . args) (let ((p (*tui-log-port*))) @@ -438,6 +441,7 @@ " /clear Start new session" " /sessions List sessions" " /search <term> Search session history" + " /mcp Toggle MCP tools on/off (for local models)" " /theme Cycle theme (Ctrl-T)" " /sidebar Toggle sidebar (Ctrl-B)" " /quit Exit" @@ -485,6 +489,16 @@ ((equal? cmd "theme") (cycle-theme!) (add-message! state (msg-block-system "Theme cycled."))) + ((equal? cmd "mcp") + (let* ((mcp-tools (filter (lambda (n) (string-prefix? "mcp_" n)) (list-tools))) + (hide? (not *mcp-disabled*))) + (set! *mcp-disabled* hide?) + (for-each (lambda (n) (set-tool-internal! n hide?)) mcp-tools) + (add-message! state + (msg-block-system + (format "MCP tools ~a (~a tools). Use /mcp to toggle." + (if hide? "disabled" "enabled") (length mcp-tools)))) + (refresh-mcp-sidebar! state))) ((equal? cmd "sidebar") (app-state-sidebar-visible?-set! state (not (app-state-sidebar-visible? state))) (reflow-all! state))