Add debug REPL, fix http-get-json hang on plain HTTP, fix ESC in TUI

ober

17fac527850db5693b05af071930a4ce29ec36f4

diff --git a/build-binary.ss b/build-binary.ss
index 31d73d0..eb3e703 100644
--- a/build-binary.ss
+++ b/build-binary.ss
@@ -108,6 +108,7 @@
     "lib/jcode/core/message"
     "lib/jcode/core/agent"
     "lib/jcode/core/plugin"
+    "lib/jcode/core/debug-repl"
     "lib/jcode/provider/provider"
     "lib/jcode/tool/registry"
     "lib/jcode/tool/file"
diff --git a/lib/jcode/core/debug-repl.sls b/lib/jcode/core/debug-repl.sls
new file mode 100644
index 0000000..aee7242
--- /dev/null
+++ b/lib/jcode/core/debug-repl.sls
@@ -0,0 +1,215 @@
+#!chezscheme
+;;; Generated by jerbuild — DO NOT EDIT
+;;; Source: src/jcode/core/debug-repl.ss
+
+(library (jcode core debug-repl)
+  (export start-jcode-repl! stop-jcode-repl! jcode-repl-port)
+  (import
+    (except (chezscheme) make-hash-table hash-table? iota \x31;+ \x31;-
+      getenv path-extension path-absolute? thread? make-mutex
+      mutex? mutex-name)
+    (jcode core log)
+    (jerboa core)
+    (jerboa runtime))
+  (def logger (make-logger "debug-repl"))
+  (def _libc-loaded
+       (guard (e [#t #f]) (load-shared-object #f)))
+  (def c-socket
+       (foreign-procedure "socket" (int int int) int))
+  (def c-bind (foreign-procedure "bind" (int void* int) int))
+  (def c-listen (foreign-procedure "listen" (int int) int))
+  (def c-accept
+       (foreign-procedure "accept" (int void* void*) int))
+  (def c-close (foreign-procedure "close" (int) int))
+  (def c-setsockopt
+       (foreign-procedure "setsockopt"
+         (int int int void* int)
+         int))
+  (def c-htons
+       (foreign-procedure "htons" (unsigned-short) unsigned-short))
+  (def c-getsockname
+       (foreign-procedure "getsockname" (int void* void*) int))
+  (def c-fcntl (foreign-procedure "fcntl" (int int int) int))
+  (def c-errno-location
+       (let ([mt (symbol->string (machine-type))])
+         (if (and (>= (string-length mt) 3)
+                  (string=?
+                    (substring
+                      mt
+                      (- (string-length mt) 3)
+                      (string-length mt))
+                    "osx"))
+             (foreign-procedure "__error" () void*)
+             (foreign-procedure "__errno_location" () void*))))
+  (def (get-errno) (foreign-ref 'int (c-errno-location) 0))
+  (def AF_INET 2)
+  (def SOCK_STREAM 1)
+  (def SOL_SOCKET 65535)
+  (def SO_REUSEADDR 4)
+  (def SOCKADDR_IN_SIZE 16)
+  (def F_GETFL 3)
+  (def F_SETFL 4)
+  (def O_NONBLOCK 4)
+  (def EINTR 4)
+  (def EAGAIN 35)
+  (def *retry-delay* (make-time 'time-duration 10000000 0))
+  (def (set-nonblocking! fd)
+       (let ([flags (c-fcntl fd F_GETFL 0)])
+         (c-fcntl fd F_SETFL (bitwise-ior flags O_NONBLOCK))))
+  (def (make-sockaddr-in port)
+       (let ([buf (foreign-alloc SOCKADDR_IN_SIZE)])
+         (let lp ([i 0])
+           (when (< i SOCKADDR_IN_SIZE)
+             (foreign-set! 'unsigned-8 buf i 0)
+             (lp (+ i 1))))
+         (foreign-set! 'unsigned-8 buf 0 SOCKADDR_IN_SIZE)
+         (foreign-set! 'unsigned-8 buf 1 AF_INET)
+         (foreign-set! 'unsigned-short buf 2 (c-htons port))
+         (foreign-set! 'unsigned-8 buf 4 127)
+         (foreign-set! 'unsigned-8 buf 5 0)
+         (foreign-set! 'unsigned-8 buf 6 0)
+         (foreign-set! 'unsigned-8 buf 7 1)
+         buf))
+  (def (sockaddr-in-port buf)
+       (let ([hi (foreign-ref 'unsigned-8 buf 2)]
+             [lo (foreign-ref 'unsigned-8 buf 3)])
+         (+ (* hi 256) lo)))
+  (def *server-fd* #f)
+  (def *server-port* #f)
+  (def *server-running* #f)
+  (def *repl-port-file*
+       (string-append
+         (or (getenv "HOME") ".")
+         "/.jcode-repl-port"))
+  (def (write-port-file! port-num)
+       (when (file-exists? *repl-port-file*)
+         (delete-file *repl-port-file*))
+       (call-with-output-file
+         *repl-port-file*
+         (lambda (p) (display port-num p) (newline p))))
+  (def (delete-port-file!)
+       (when (file-exists? *repl-port-file*)
+         (guard (e [#t (void)]) (delete-file *repl-port-file*))))
+  (def (client-repl fd)
+       (let ([ip (open-fd-input-port
+                   fd
+                   (buffer-mode block)
+                   (make-transcoder
+                     (utf-8-codec)
+                     (eol-style none)
+                     (error-handling-mode replace)))]
+             [op (open-fd-output-port
+                   fd
+                   (buffer-mode line)
+                   (make-transcoder
+                     (utf-8-codec)
+                     (eol-style lf)
+                     (error-handling-mode replace)))])
+         (dynamic-wind
+           void
+           (lambda ()
+             (let loop ()
+               (guard (e [#t (void)])
+                 (display "jcode> " op)
+                 (flush-output-port op)
+                 (let ([expr (guard (e
+                                      [#t
+                                       (display
+                                         (format
+                                           #f
+                                           "read error: ~a\n"
+                                           (if (message-condition? e)
+                                               (condition-message e)
+                                               e))
+                                         op)
+                                       (flush-output-port op)
+                                       'continue])
+                               (read ip))])
+                   (cond
+                     [(eof-object? expr) (void)]
+                     [(eq? expr 'continue) (loop)]
+                     [else
+                      (guard (e
+                               [#t
+                                (display
+                                  (format
+                                    #f
+                                    "error: ~a\n"
+                                    (if (message-condition? e)
+                                        (condition-message e)
+                                        e))
+                                  op)
+                                (flush-output-port op)])
+                        (call-with-values
+                          (lambda () (eval expr (interaction-environment)))
+                          (lambda results
+                            (for-each
+                              (lambda (v)
+                                (unless (eq? v (void))
+                                  (write v op)
+                                  (newline op)))
+                              results)
+                            (flush-output-port op))))
+                      (loop)])))))
+           (lambda ()
+             (guard (e [#t (void)]) (close-port ip))
+             (guard (e [#t (void)]) (close-port op))))))
+  (def (accept-loop server-fd)
+       (let loop ()
+         (when *server-running*
+           (let ([client-fd (c-accept server-fd 0 0)])
+             (cond
+               [(>= client-fd 0)
+                (fork-thread (lambda () (client-repl client-fd)))
+                (loop)]
+               [(let ([e (get-errno)]) (or (= e EINTR) (= e EAGAIN)))
+                (sleep *retry-delay*)
+                (loop)]
+               [else
+                (when *server-running* (sleep *retry-delay*) (loop))])))))
+  (def (start-jcode-repl! . args)
+       "Start the debug REPL. Pass a port number or 0 for auto-assign."
+       (stop-jcode-repl!)
+       (let ([port (if (pair? args) (car args) 0)])
+         (let ([fd (c-socket AF_INET SOCK_STREAM 0)])
+           (when (< fd 0) (error 'start-jcode-repl! "socket() failed"))
+           (let ([one (foreign-alloc 4)])
+             (foreign-set! 'int one 0 1)
+             (c-setsockopt fd SOL_SOCKET SO_REUSEADDR one 4)
+             (foreign-free one))
+           (let ([addr (make-sockaddr-in port)])
+             (let ([rc (c-bind fd addr SOCKADDR_IN_SIZE)])
+               (foreign-free addr)
+               (when (< rc 0)
+                 (c-close fd)
+                 (error 'start-jcode-repl! "bind() failed" port))))
+           (when (< (c-listen fd 4) 0)
+             (c-close fd)
+             (error 'start-jcode-repl! "listen() failed"))
+           (set-nonblocking! fd)
+           (let ([buf (foreign-alloc SOCKADDR_IN_SIZE)]
+                 [len (foreign-alloc 4)])
+             (foreign-set! 'int len 0 SOCKADDR_IN_SIZE)
+             (c-getsockname fd buf len)
+             (let ([actual-port (sockaddr-in-port buf)])
+               (foreign-free buf)
+               (foreign-free len)
+               (set! *server-fd* fd)
+               (set! *server-port* actual-port)
+               (set! *server-running* #t)
+               (write-port-file! actual-port)
+               (fork-thread (lambda () (accept-loop fd)))
+               (log-info logger "started" `((port . ,actual-port)))
+               actual-port)))))
+  (def (stop-jcode-repl!)
+       "Stop the debug REPL if running."
+       (when *server-running*
+         (set! *server-running* #f)
+         (when *server-fd*
+           (guard (e [#t (void)]) (c-close *server-fd*))
+           (set! *server-fd* #f)
+           (set! *server-port* #f))
+         (delete-port-file!)))
+  (def (jcode-repl-port)
+       "Return the port the debug REPL is listening on, or #f."
+       *server-port*))
diff --git a/lib/jcode/provider/provider.sls b/lib/jcode/provider/provider.sls
index 9ed1b0b..0921331 100644
--- a/lib/jcode/provider/provider.sls
+++ b/lib/jcode/provider/provider.sls
@@ -1187,17 +1187,12 @@
                      (let* ([status-line (port-read-line in)]
                             [status (parse-http-status status-line)]
                             [resp-headers (port-read-headers in)]
-                            [te (header-value
+                            [cl (header-value
                                   resp-headers
-                                  "transfer-encoding")]
-                            [raw (port-read-all-bytes in)]
-                            [body-bv (if (and te
-                                              (string-contains
-                                                te
-                                                "chunked"))
-                                         (decode-chunked-bytes raw)
-                                         raw)]
-                            [body (utf8->string body-bv)])
+                                  "content-length")]
+                            [body (if cl
+                                      (get-string-n in (string->number cl))
+                                      (port-read-all in))])
                        (values status body)))
                    (lambda () (close-port in) (close-port out))))))))
   (def (openai-list-models provider)
diff --git a/lib/jcode/ui/cli.sls b/lib/jcode/ui/cli.sls
index b0979e9..fbc0d28 100644
--- a/lib/jcode/ui/cli.sls
+++ b/lib/jcode/ui/cli.sls
@@ -11,11 +11,11 @@
    (std misc string) (std misc thread) (std text json)
    (jcode core config) (jcode core log) (jcode core session)
    (jcode core message) (jcode core agent)
-   (jcode tool registry) (jcode tool file) (jcode tool bash)
-   (jcode tool web) (jcode tool batch) (jcode tool git)
-   (jcode mcp client) (jcode tool lsp) (jcode core plugin)
-   (jcode ui tui) (jcode ui serve) (jerboa core)
-   (jerboa runtime))
+   (jcode core debug-repl) (jcode tool registry)
+   (jcode tool file) (jcode tool bash) (jcode tool web)
+   (jcode tool batch) (jcode tool git) (jcode mcp client)
+   (jcode tool lsp) (jcode core plugin) (jcode ui tui)
+   (jcode ui serve) (jerboa core) (jerboa runtime))
   (def logger (make-logger "cli"))
   (def *version* "0.1.0")
   (def *cli-mcp-disabled* #f)
@@ -36,6 +36,13 @@
          (load-config)
          (session-init-db)
          (init-tools)
+         (let ([repl-opt (assoc '\x2D;-repl-port opts)])
+           (when repl-opt
+             (let ([port (start-jcode-repl! (cdr repl-opt))])
+               (printf
+                 "Debug REPL on port ~a (nc 127.0.0.1 ~a)~n"
+                 port
+                 port))))
          (let ([p-opt (assoc '\x2D;-provider opts)]
                [m-opt (assoc '\x2D;-model opts)])
            (when p-opt (current-provider-override (cdr p-opt)))
@@ -73,6 +80,14 @@
             (loop (cdr args) (cons '(\x2D;-no-mcp . #t) opts))]
            [(equal? (car args) "--verbose")
             (loop (cdr args) (cons '(\x2D;-verbose . #t) opts))]
+           [(and (equal? (car args) "--repl-port") (pair? (cdr args)))
+            (loop
+              (cddr args)
+              (cons
+                (cons '\x2D;-repl-port (string->number (cadr args)))
+                opts))]
+           [(equal? (car args) "--repl")
+            (loop (cdr args) (cons '(\x2D;-repl-port . 0) opts))]
            [(and (equal? (car args) "--model") (pair? (cdr args)))
             (loop
               (cddr args)
@@ -101,7 +116,7 @@
          (thread-join! t-plugins)))
   (def (display-help)
        (display
-         "jcode - Portable AI coding agent\n\nUSAGE:\n    jcode [OPTIONS] [PROMPT]\n    jcode [COMMAND]\n\nOPTIONS:\n    -h, --help       Show this help message\n    -v, --version    Show version\n    -d, --debug      Enable debug logging\n    -m, --model      Model to use (default: claude-sonnet-4-20250514)\n    -p, --provider   Provider to use (default: anthropic)\n    --tui            Launch terminal UI mode\n    --no-tui         Force line-mode REPL (default)\n    --no-mcp         Skip MCP server initialization\n    --verbose        Log TUI events to ~/jcode.log\n\nCOMMANDS:\n    session list     List all sessions\n    session resume   Resume a previous session\n    config           Show or edit configuration\n    serve            Start JSONL server (stdio mode)\n    serve --port N   Start TCP server on 127.0.0.1:N\n    serve --rotate-token   Generate new auth token and exit\n    serve --show-token     Display current auth token and exit\n\nEXAMPLES:\n    jcode                           Start interactive session\n    jcode \"Read main.ss\"           One-shot query\n    jcode session list              List sessions\n    jcode serve --port 8321         Start TCP server for Android app\n"))
+         "jcode - Portable AI coding agent\n\nUSAGE:\n    jcode [OPTIONS] [PROMPT]\n    jcode [COMMAND]\n\nOPTIONS:\n    -h, --help       Show this help message\n    -v, --version    Show version\n    -d, --debug      Enable debug logging\n    -m, --model      Model to use (default: claude-sonnet-4-20250514)\n    -p, --provider   Provider to use (default: anthropic)\n    --tui            Launch terminal UI mode\n    --no-tui         Force line-mode REPL (default)\n    --no-mcp         Skip MCP server initialization\n    --repl           Start debug REPL on auto-assigned port\n    --repl-port N    Start debug REPL on specific port\n    --verbose        Log TUI events to ~/jcode.log\n\nCOMMANDS:\n    session list     List all sessions\n    session resume   Resume a previous session\n    config           Show or edit configuration\n    serve            Start JSONL server (stdio mode)\n    serve --port N   Start TCP server on 127.0.0.1:N\n    serve --rotate-token   Generate new auth token and exit\n    serve --show-token     Display current auth token and exit\n\nEXAMPLES:\n    jcode                           Start interactive session\n    jcode \"Read main.ss\"           One-shot query\n    jcode session list              List sessions\n    jcode serve --port 8321         Start TCP server for Android app\n"))
   (def (interactive-mode opts)
        (printf "jcode ~a~n" *version*)
        (printf
diff --git a/lib/jcode/ui/tui.sls b/lib/jcode/ui/tui.sls
index e65582a..25bf5c3 100644
--- a/lib/jcode/ui/tui.sls
+++ b/lib/jcode/ui/tui.sls
@@ -15,11 +15,12 @@
    (jcode ui tui-input) (jcode ui tui-sidebar)
    (jcode ui tui-dialog) (jcode core config)
    (jcode core models) (jcode core agent)
-   (jcode provider provider) (jcode core session)
-   (jcode core log) (jcode tool registry) (jcode tool file)
-   (jcode tool bash) (jcode tool web) (jcode tool batch)
-   (jcode tool git) (jcode mcp client) (jcode tool lsp)
-   (jcode core plugin) (jerboa core) (jerboa runtime))
+   (jcode core debug-repl) (jcode provider provider)
+   (jcode core session) (jcode core log) (jcode tool registry)
+   (jcode tool file) (jcode tool bash) (jcode tool web)
+   (jcode tool batch) (jcode tool git) (jcode mcp client)
+   (jcode tool lsp) (jcode core plugin) (jerboa core)
+   (jerboa runtime))
   (def logger (make-logger "tui"))
   (def *version* "0.1.0")
   (def *tui-log-port* (make-parameter #f))
@@ -98,13 +99,14 @@
        (- (app-state-width state) (app-state-sidebar-width state)))
   (def (tui-main args) (load-config) (session-init-db)
        (init-tools-for-tui) (apply-tui-overrides! args)
+       (let ([repl-port (start-jcode-repl!)]) (void))
        (let ([verbose? (and (member "--verbose" args) #t)])
          (when verbose? (open-tui-log!))
          (tui-log "tui-main: starting, args=~a" args)
          (redirect-stderr-for-tui! verbose?))
        (with-tui
          (tui-log "tui-main: tb-init done")
-         (let ([in-mode (bitwise-ior TB_INPUT_ALT TB_INPUT_MOUSE)])
+         (let ([in-mode (bitwise-ior TB_INPUT_ESC TB_INPUT_MOUSE)])
            (tui-log
              "tui-main: setting input-mode=~a output-mode=~a"
              in-mode
@@ -138,6 +140,7 @@
              "tui-main: entering event loop, main-thread=~a"
              (*main-thread*))
            (event-loop state)
+           (stop-jcode-repl!)
            (restore-stderr!)
            (close-tui-log!))))
   (def (init-tools-for-tui) (init-file-tools) (init-bash-tool)
@@ -588,7 +591,11 @@
        (let* ([cur-p (or (current-provider-override)
                          (config-provider))]
               [cur-m (or (current-model-override) (config-model))]
-              [models (provider-models cur-p)]
+              [models (if (equal? cur-p "ollama")
+                          (try (let ([prov (make-provider "ollama" #f #f)])
+                                 (provider-list-models prov))
+                               (catch (e) (provider-models cur-p)))
+                          (provider-models cur-p))]
               [items (map (lambda (m)
                             (cons
                               (format
diff --git a/src/jcode/core/debug-repl.ss b/src/jcode/core/debug-repl.ss
new file mode 100644
index 0000000..5c936e3
--- /dev/null
+++ b/src/jcode/core/debug-repl.ss
@@ -0,0 +1,246 @@
+;;; jcode debug REPL — TCP REPL for inspecting a running jcode instance
+;;;
+;;; Starts a TCP REPL on 127.0.0.1 that you can connect to with:
+;;;   rlwrap nc 127.0.0.1 $(cat ~/.jcode-repl-port)
+;;;
+;;; The REPL evaluates in (interaction-environment), so you can import
+;;; any module and inspect live state:
+;;;   (import (jcode provider provider))
+;;;   (provider-list-models (make-provider "ollama" #f #f))
+;;;
+;;; API:
+;;;   (start-jcode-repl!)             -> port-number (auto-assign)
+;;;   (start-jcode-repl! port)        -> port-number
+;;;   (stop-jcode-repl!)              -> void
+;;;   (jcode-repl-port)               -> port-number or #f
+
+(export start-jcode-repl!
+        stop-jcode-repl!
+        jcode-repl-port)
+
+(import :jcode/core/log)
+
+(def logger (make-logger "debug-repl"))
+
+;; ---- FFI (socket primitives) ----
+
+;; Ensure libc symbols are visible to foreign-procedure lookups.
+;; In a static binary the symbols are already linked; (load-shared-object #f)
+;; tells Chez to search the running process image.
+(def _libc-loaded
+  (guard (e [#t #f])
+    (load-shared-object #f)))
+
+(def c-socket      (foreign-procedure "socket" (int int int) int))
+(def c-bind        (foreign-procedure "bind" (int void* int) int))
+(def c-listen      (foreign-procedure "listen" (int int) int))
+(def c-accept      (foreign-procedure "accept" (int void* void*) int))
+(def c-close       (foreign-procedure "close" (int) int))
+(def c-setsockopt  (foreign-procedure "setsockopt" (int int int void* int) int))
+(def c-htons       (foreign-procedure "htons" (unsigned-short) unsigned-short))
+(def c-getsockname (foreign-procedure "getsockname" (int void* void*) int))
+(def c-fcntl       (foreign-procedure "fcntl" (int int int) int))
+
+(def c-errno-location
+  (let ((mt (symbol->string (machine-type))))
+    (if (and (>= (string-length mt) 3)
+             (string=? (substring mt (- (string-length mt) 3) (string-length mt)) "osx"))
+      (foreign-procedure "__error" () void*)
+      (foreign-procedure "__errno_location" () void*))))
+
+(def (get-errno) (foreign-ref 'int (c-errno-location) 0))
+
+;; ---- Constants ----
+
+(def AF_INET 2)
+(def SOCK_STREAM 1)
+(def SOL_SOCKET #xffff)   ;; macOS/FreeBSD; Linux would be 1
+(def SO_REUSEADDR 4)      ;; macOS/FreeBSD; Linux would be 2
+(def SOCKADDR_IN_SIZE 16)
+(def F_GETFL 3)
+(def F_SETFL 4)
+(def O_NONBLOCK #x4)      ;; macOS; Linux would be #x800
+(def EINTR 4)
+(def EAGAIN 35)            ;; macOS; Linux would be 11
+(def *retry-delay* (make-time 'time-duration 10000000 0))  ;; 10ms
+
+;; ---- Helpers ----
+
+(def (set-nonblocking! fd)
+  (let ((flags (c-fcntl fd F_GETFL 0)))
+    (c-fcntl fd F_SETFL (bitwise-ior flags O_NONBLOCK))))
+
+(def (make-sockaddr-in port)
+  ;; Bind to 127.0.0.1 only (not 0.0.0.0)
+  (let ((buf (foreign-alloc SOCKADDR_IN_SIZE)))
+    (let lp ((i 0))
+      (when (< i SOCKADDR_IN_SIZE)
+        (foreign-set! 'unsigned-8 buf i 0)
+        (lp (+ i 1))))
+    ;; macOS sockaddr_in: sin_len(1) + sin_family(1) + sin_port(2) + sin_addr(4)
+    (foreign-set! 'unsigned-8 buf 0 SOCKADDR_IN_SIZE)  ;; sin_len
+    (foreign-set! 'unsigned-8 buf 1 AF_INET)            ;; sin_family
+    (foreign-set! 'unsigned-short buf 2 (c-htons port))
+    ;; 127.0.0.1 = bytes 127, 0, 0, 1 at offset 4
+    (foreign-set! 'unsigned-8 buf 4 127)
+    (foreign-set! 'unsigned-8 buf 5 0)
+    (foreign-set! 'unsigned-8 buf 6 0)
+    (foreign-set! 'unsigned-8 buf 7 1)
+    buf))
+
+(def (sockaddr-in-port buf)
+  (let ((hi (foreign-ref 'unsigned-8 buf 2))
+        (lo (foreign-ref 'unsigned-8 buf 3)))
+    (+ (* hi 256) lo)))
+
+;; ---- State ----
+
+(def *server-fd* #f)
+(def *server-port* #f)
+(def *server-running* #f)
+
+(def *repl-port-file*
+  (string-append (or (getenv "HOME") ".") "/.jcode-repl-port"))
+
+;; ---- Port file ----
+
+(def (write-port-file! port-num)
+  (when (file-exists? *repl-port-file*)
+    (delete-file *repl-port-file*))
+  (call-with-output-file *repl-port-file*
+    (lambda (p)
+      (display port-num p)
+      (newline p))))
+
+(def (delete-port-file!)
+  (when (file-exists? *repl-port-file*)
+    (guard (e [#t (void)])
+      (delete-file *repl-port-file*))))
+
+;; ---- Client REPL ----
+
+(def (client-repl fd)
+  (let ((ip (open-fd-input-port fd (buffer-mode block)
+              (make-transcoder (utf-8-codec) (eol-style none)
+                (error-handling-mode replace))))
+        (op (open-fd-output-port fd (buffer-mode line)
+              (make-transcoder (utf-8-codec) (eol-style lf)
+                (error-handling-mode replace)))))
+    (dynamic-wind
+      void
+      (lambda ()
+        (let loop ()
+          (guard (e [#t (void)])  ;; I/O error — stop
+            (display "jcode> " op)
+            (flush-output-port op)
+            (let ((expr (guard (e [#t
+                                   (display (format #f "read error: ~a\n"
+                                     (if (message-condition? e)
+                                       (condition-message e)
+                                       e))
+                                     op)
+                                   (flush-output-port op)
+                                   'continue])
+                          (read ip))))
+              (cond
+                ((eof-object? expr) (void))  ;; client disconnected
+                ((eq? expr 'continue) (loop))
+                (else
+                 (guard (e [#t
+                            (display (format #f "error: ~a\n"
+                              (if (message-condition? e)
+                                (condition-message e)
+                                e))
+                              op)
+                            (flush-output-port op)])
+                   (call-with-values
+                     (lambda () (eval expr (interaction-environment)))
+                     (lambda results
+                       (for-each
+                         (lambda (v)
+                           (unless (eq? v (void))
+                             (write v op)
+                             (newline op)))
+                         results)
+                       (flush-output-port op))))
+                 (loop)))))))
+      (lambda ()
+        (guard (e [#t (void)]) (close-port ip))
+        (guard (e [#t (void)]) (close-port op))))))
+
+;; ---- Accept loop ----
+
+(def (accept-loop server-fd)
+  (let loop ()
+    (when *server-running*
+      (let ((client-fd (c-accept server-fd 0 0)))
+        (cond
+          ((>= client-fd 0)
+           (fork-thread (lambda () (client-repl client-fd)))
+           (loop))
+          ((let ((e (get-errno))) (or (= e EINTR) (= e EAGAIN)))
+           (sleep *retry-delay*)
+           (loop))
+          (else
+           (when *server-running*
+             (sleep *retry-delay*)
+             (loop))))))))
+
+;; ---- Public API ----
+
+(def (start-jcode-repl! . args)
+  "Start the debug REPL. Pass a port number or 0 for auto-assign."
+  (stop-jcode-repl!)
+  (let ((port (if (pair? args) (car args) 0)))
+    (let ((fd (c-socket AF_INET SOCK_STREAM 0)))
+      (when (< fd 0)
+        (error 'start-jcode-repl! "socket() failed"))
+      ;; SO_REUSEADDR
+      (let ((one (foreign-alloc 4)))
+        (foreign-set! 'int one 0 1)
+        (c-setsockopt fd SOL_SOCKET SO_REUSEADDR one 4)
+        (foreign-free one))
+      ;; Bind to 127.0.0.1
+      (let ((addr (make-sockaddr-in port)))
+        (let ((rc (c-bind fd addr SOCKADDR_IN_SIZE)))
+          (foreign-free addr)
+          (when (< rc 0)
+            (c-close fd)
+            (error 'start-jcode-repl! "bind() failed" port))))
+      ;; Listen
+      (when (< (c-listen fd 4) 0)
+        (c-close fd)
+        (error 'start-jcode-repl! "listen() failed"))
+      ;; Non-blocking accept
+      (set-nonblocking! fd)
+      ;; Get actual port
+      (let ((buf (foreign-alloc SOCKADDR_IN_SIZE))
+            (len (foreign-alloc 4)))
+        (foreign-set! 'int len 0 SOCKADDR_IN_SIZE)
+        (c-getsockname fd buf len)
+        (let ((actual-port (sockaddr-in-port buf)))
+          (foreign-free buf)
+          (foreign-free len)
+          (set! *server-fd* fd)
+          (set! *server-port* actual-port)
+          (set! *server-running* #t)
+          (write-port-file! actual-port)
+          (fork-thread (lambda () (accept-loop fd)))
+          (log-info logger "started"
+            `((port . ,actual-port)))
+          actual-port)))))
+
+(def (stop-jcode-repl!)
+  "Stop the debug REPL if running."
+  (when *server-running*
+    (set! *server-running* #f)
+    (when *server-fd*
+      (guard (e [#t (void)])
+        (c-close *server-fd*))
+      (set! *server-fd* #f)
+      (set! *server-port* #f))
+    (delete-port-file!)))
+
+(def (jcode-repl-port)
+  "Return the port the debug REPL is listening on, or #f."
+  *server-port*)
diff --git a/src/jcode/provider/provider.ss b/src/jcode/provider/provider.ss
index ecbcde1..bf45e62 100644
--- a/src/jcode/provider/provider.ss
+++ b/src/jcode/provider/provider.ss
@@ -973,12 +973,10 @@
               (let* ((status-line (port-read-line in))
                      (status (parse-http-status status-line))
                      (resp-headers (port-read-headers in))
-                     (te (header-value resp-headers "transfer-encoding"))
-                     (raw (port-read-all-bytes in))
-                     (body-bv (if (and te (string-contains te "chunked"))
-                                (decode-chunked-bytes raw)
-                                raw))
-                     (body (utf8->string body-bv)))
+                     (cl (header-value resp-headers "content-length"))
+                     (body (if cl
+                             (get-string-n in (string->number cl))
+                             (port-read-all in))))
                 (values status body)))
             (lambda ()
               (close-port in)
diff --git a/src/jcode/ui/cli.ss b/src/jcode/ui/cli.ss
index 84be37c..3204a00 100644
--- a/src/jcode/ui/cli.ss
+++ b/src/jcode/ui/cli.ss
@@ -10,6 +10,7 @@
         :jcode/core/session
         :jcode/core/message
         :jcode/core/agent
+        :jcode/core/debug-repl
         :jcode/tool/registry
         :jcode/tool/file
         :jcode/tool/bash
@@ -47,6 +48,11 @@
     (load-config)
     (session-init-db)
     (init-tools)
+    ;; Start debug REPL if requested (--repl for auto-port, --repl-port N)
+    (let ((repl-opt (assoc '--repl-port opts)))
+      (when repl-opt
+        (let ((port (start-jcode-repl! (cdr repl-opt))))
+          (printf "Debug REPL on port ~a (nc 127.0.0.1 ~a)~n" port port))))
     ;; Apply CLI overrides to agent parameters
     (let ((p-opt (assoc '--provider opts))
           (m-opt (assoc '--model opts)))
@@ -78,6 +84,9 @@
       ((equal? (car args) "--no-tui")  (loop (cdr args) (cons '(--no-tui . #t) opts)))
       ((equal? (car args) "--no-mcp")  (loop (cdr args) (cons '(--no-mcp . #t) opts)))
       ((equal? (car args) "--verbose") (loop (cdr args) (cons '(--verbose . #t) opts)))
+      ((and (equal? (car args) "--repl-port") (pair? (cdr args)))
+       (loop (cddr args) (cons (cons '--repl-port (string->number (cadr args))) opts)))
+      ((equal? (car args) "--repl") (loop (cdr args) (cons '(--repl-port . 0) opts)))
       ((and (equal? (car args) "--model") (pair? (cdr args)))
        (loop (cddr args) (cons (cons '--model (cadr args)) opts)))
       ((and (equal? (car args) "-m") (pair? (cdr args)))
@@ -122,6 +131,8 @@ OPTIONS:
     --tui            Launch terminal UI mode
     --no-tui         Force line-mode REPL (default)
     --no-mcp         Skip MCP server initialization
+    --repl           Start debug REPL on auto-assigned port
+    --repl-port N    Start debug REPL on specific port
     --verbose        Log TUI events to ~/jcode.log
 
 COMMANDS:
diff --git a/src/jcode/ui/tui.ss b/src/jcode/ui/tui.ss
index fa94fff..763f3b5 100644
--- a/src/jcode/ui/tui.ss
+++ b/src/jcode/ui/tui.ss
@@ -19,6 +19,7 @@
         :jcode/core/config
         :jcode/core/models
         :jcode/core/agent
+        :jcode/core/debug-repl
         :jcode/provider/provider
         :jcode/core/session
         :jcode/core/log
@@ -176,13 +177,16 @@
   (session-init-db)
   (init-tools-for-tui)
   (apply-tui-overrides! args)
+  ;; Start debug REPL (auto-port) — connect with: rlwrap nc 127.0.0.1 $(cat ~/.jcode-repl-port)
+  (let ((repl-port (start-jcode-repl!)))
+    (void))
   (let ((verbose? (and (member "--verbose" args) #t)))
     (when verbose? (open-tui-log!))
     (tui-log "tui-main: starting, args=~a" args)
     (redirect-stderr-for-tui! verbose?))
   (with-tui
     (tui-log "tui-main: tb-init done")
-    (let ((in-mode (bitwise-ior TB_INPUT_ALT TB_INPUT_MOUSE)))
+    (let ((in-mode (bitwise-ior TB_INPUT_ESC TB_INPUT_MOUSE)))
       (tui-log "tui-main: setting input-mode=~a output-mode=~a" in-mode TB_OUTPUT_TRUECOLOR)
       (tb-set-input-mode! in-mode)
       (tb-set-output-mode! TB_OUTPUT_TRUECOLOR))
@@ -204,6 +208,7 @@
       (*main-thread* (current-thread))
       (tui-log "tui-main: entering event loop, main-thread=~a" (*main-thread*))
       (event-loop state)
+      (stop-jcode-repl!)
       (restore-stderr!)
       (close-tui-log!))))
 
@@ -635,7 +640,14 @@
   "Show a popup list of models for the current provider."
   (let* ((cur-p (or (current-provider-override) (config-provider)))
          (cur-m (or (current-model-override) (config-model)))
-         (models (provider-models cur-p))
+         (models
+           ;; For Ollama, live-query the local API for installed models
+           (if (equal? cur-p "ollama")
+             (try
+               (let ((prov (make-provider "ollama" #f #f)))
+                 (provider-list-models prov))
+               (catch (e) (provider-models cur-p)))
+             (provider-models cur-p)))
          (items (map (lambda (m)
                        (cons (format "~a~a  (~a)"
                                (if (equal? (car m) cur-m) "● " "  ")