fixes
ober
a7a4cf64a1cbd66d4f89ddb9ff3ce7dcd95fb419
--- a/docs/cli.md +++ b/docs/cli.md @@ -43,7 +43,7 @@ Parsed before any subcommand. | Subcommand | Purpose | |---|---| -| `session list` · `session resume <id>` | List or resume saved sessions. | +| `session list` · `session resume <id>` · `session restore <id>` | List saved sessions, or restore one into the TUI. Use `--no-tui` to force the line-mode REPL. | | `config` | Print the resolved provider, model, and key status. | | `keys …` | Manage the encrypted key store (see below). | | `serve …` | Run the JSONL agent server (stdio or TCP). See [remote.md](remote.md). | --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -165,6 +165,7 @@ Local providers (**Ollama**, **MLX**) need no key. See ```bash jcode # interactive line-mode REPL jcode --tui # full terminal UI (see docs/tui.md) +jcode session resume <id> # restore a saved session in the TUI jcode -p "summarise this repo and list the entry points" # one-shot jcode --model claude-opus-4-20250514 # override the model for this run ``` --- a/src/jcode/provider/provider.ss +++ b/src/jcode/provider/provider.ss @@ -120,6 +120,9 @@ (string-contains msg "HTTP read timed out") (string-contains msg "stream read timed out") (string-contains msg "stream closed before a terminal event") + (and (string-contains msg "not permitted on closed port") + (or (string-contains msg "tcp-connection-in") + (string-contains msg "tcp-connection-out"))) (string-contains msg "provider aborted the stream") ;; Some OpenAI-compatible gateways occasionally return HTTP 200 with ;; a truncated or whitespace-only body. No assistant response was @@ -865,6 +868,36 @@ (vector-set! closed? 0 #t) (guard (e [(i/o-error? e) (void)]) (close-port in)) (guard (e [(i/o-error? e) (void)]) (close-port out)))) + (def (raise-stream-timeout!) + (error 'jcode-http-post-stream + (format "stream read timed out after ~as of silence (host: ~a)" + timeout-secs host))) + (def (raise-stream-closed! e) + (error 'jcode-http-post-stream + (format "stream closed before a terminal event (host: ~a): ~a" + host (err->string e)))) + (def (guard-stream-read thunk) + (guard (e [#t + (let ((msg (err->string e))) + (if (string-contains msg "not permitted on closed port") + (if (vector-ref timed-out? 0) + (raise-stream-timeout!) + (raise-stream-closed! e)) + (raise e)))]) + (thunk))) + (def (read-stream-line-or-false) + (check-stream-abort!) + (guard-stream-read + (lambda () + (let ((c (peek-char in))) + (touch!) + (check-stream-abort!) + (if (eof-object? c) + #f + (let ((line (port-read-line in))) + (touch!) + (check-stream-abort!) + line)))))) (fork-thread (lambda () (guard (e [#t (when (tracing?) @@ -891,47 +924,30 @@ (check-stream-abort!) (port-write-string out req) (touch!) - (let* ((status-line (port-read-line in)) + (let* ((status-line (guard-stream-read + (lambda () (port-read-line in)))) (status (parse-http-status status-line)) - (_headers (port-read-headers in))) + (_headers (guard-stream-read + (lambda () (port-read-headers in))))) (touch!) (check-stream-abort!) (unless (= status 200) - (let ((body (read-http-body-lines - (lambda () - (check-stream-abort!) - (let ((c (peek-char in))) - (touch!) - (check-stream-abort!) - (if (eof-object? c) - #f - (let ((line (port-read-line in))) - (touch!) - (check-stream-abort!) - line))))))) + (let ((body (read-http-body-lines read-stream-line-or-false))) (raise-http-error status body))) ;; Read SSE lines until EOF or chunked terminator. - ;; Filter HTTP chunked transfer-encoding size lines — see + ;; Filter HTTP chunked transfer-encoding size lines -- see ;; above (TLS branch) for details. (let loop () - (check-stream-abort!) - (let ((c (peek-char in))) - (touch!) - (check-stream-abort!) - (unless (eof-object? c) - (let ((line (port-read-line in))) - (touch!) - (check-stream-abort!) - (cond - ((equal? line "0") (void)) - ((chunk-size-line? line) (loop)) - (else (line-cb line) - (check-stream-abort!) - (loop))))))) + (let ((line (read-stream-line-or-false))) + (when line + (cond + ((equal? line "0") (void)) + ((chunk-size-line? line) (loop)) + (else (line-cb line) + (check-stream-abort!) + (loop)))))) (when (vector-ref timed-out? 0) - (error 'jcode-http-post-stream - (format "stream read timed out after ~as of silence (host: ~a)" - timeout-secs host))) + (raise-stream-timeout!)) status)) (lambda () (vector-set! done? 0 #t) --- a/src/jcode/ui/cli.ss +++ b/src/jcode/ui/cli.ss @@ -139,6 +139,9 @@ (tui-main args)) ;; Commands and REPL ((null? rest) (interactive-mode opts)) + ((and (session-restore-command? rest) + (not (assoc '--no-tui opts))) + (tui-main args)) ((equal? (car rest) "session") (session-command (cdr rest))) ((equal? (car rest) "config") (config-command (cdr rest))) ((equal? (car rest) "keys") (keys-command (cdr rest))) @@ -218,6 +221,14 @@ (not (member (car rest) '("config" "keys" "proxy" "verified" "relay" "connect")))))) +(def (session-restore-command? rest) + (and (pair? rest) + (equal? (car rest) "session") + (pair? (cdr rest)) + (or (equal? (cadr rest) "resume") + (equal? (cadr rest) "restore")) + (pair? (cddr rest)))) + (def (init-tools . maybe-verified-only) (let ((verified-only? (and (pair? maybe-verified-only) (car maybe-verified-only)))) @@ -259,7 +270,7 @@ OPTIONS: --provider Provider to use (default: anthropic) -p PROMPT One-shot prompt; everything after -p is the prompt --tui Launch terminal UI mode - --no-tui Force line-mode REPL (default) + --no-tui Force line-mode REPL --no-mcp Skip MCP server initialization --no-expert Disable configured expert escalation for this process --repl-port [127.x.x.x:]N @@ -274,7 +285,8 @@ OPTIONS: COMMANDS: session list List all sessions - session resume Resume a previous session + session resume Resume a previous session in the TUI + session restore Alias for session resume config Show or edit configuration keys Manage encrypted API key store (init | list | add | remove | unlock | @@ -1584,7 +1596,8 @@ EXAMPLES: (printf "~a ~a ~a~n" (session-id s) (session-title s) (session-created s))) sessions)))) - ((equal? (car args) "resume") + ((or (equal? (car args) "resume") + (equal? (car args) "restore")) (if (null? (cdr args)) (printf "Usage: jcode session resume <session-id>~n") (let ((session (session-load (cadr args)))) --- a/src/jcode/ui/tui.ss +++ b/src/jcode/ui/tui.ss @@ -246,7 +246,9 @@ (let* ((w (tb-width)) (h (tb-height)) (state (make-fresh-state w h)) - (session (session-create "New session"))) + (resume-id (tui-initial-resume-id args)) + (restored-session (and resume-id (find-session-by-prefix resume-id))) + (session (or restored-session (session-create "New session")))) (tui-log "tui-main: terminal ~ax~a, session=~a" w h (session-id session)) (set! *dbg-state* state) (app-state-session-id-set! state (session-id session)) @@ -254,9 +256,30 @@ (tui-log "tui-main: refresh-mcp-sidebar! (begin)") (refresh-mcp-sidebar! state) (tui-log "tui-main: refresh-mcp-sidebar! (done)") - ;; Welcome message + ;; Welcome / restored transcript (app-state-messages-set! state - (list (msg-block-system (format "jcode ~a — Type your message, /help for commands" *version*)))) + (cond + (restored-session + (append + (messages->display-blocks + (session-get-messages (session-id restored-session))) + (list + (msg-block-system + (format "Resumed session ~a -- ~a" + (session-id restored-session) + (session-title restored-session)))))) + (resume-id + (list + (msg-block-system + (format "Session not found (or ambiguous prefix): ~a -- see /sessions" + resume-id)) + (msg-block-system + (format "jcode ~a -- Type your message, /help for commands" + *version*)))) + (else + (list (msg-block-system + (format "jcode ~a -- Type your message, /help for commands" + *version*)))))) (tui-log "tui-main: reflow-all! (begin)") (reflow-all! state) (tui-log "tui-main: draw-all! (begin)") @@ -272,6 +295,25 @@ (close-tui-log!) (close-trace-log!)))) +(def (tui-initial-resume-id args) + (let loop ((args args)) + (cond + ((null? args) #f) + ((and (equal? (car args) "session") + (pair? (cdr args)) + (or (equal? (cadr args) "resume") + (equal? (cadr args) "restore")) + (pair? (cddr args))) + (caddr args)) + ((member (car args) + '("--tui" "--verbose" "--debug" "-d" "--no-mcp" "--no-expert")) + (loop (cdr args))) + ((and (member (car args) + '("--provider" "--model" "-m" "--repl-port" "--trace")) + (pair? (cdr args))) + (loop (cddr args))) + (else (loop (cdr args)))))) + (def (init-tools-for-tui) ;; Import and init tools — same as cli.ss (init-file-tools) --- a/test/run.ss +++ b/test/run.ss @@ -92,6 +92,10 @@ (guard (e [#t e]) (error 'stream-chat-with-expert "provider aborted the stream before producing a usable tool call"))] + [closed-tcp-port-error + (guard (e [#t e]) + (error 'read-char + "not permitted on closed port #<input port tcp-connection-in>"))] [ordinary-error (guard (e [#t e]) (error 'http-post-json "invalid request body"))]) @@ -101,6 +105,8 @@ (provider-retryable-error? incomplete-stream-error) #t) (check! "provider-aborted model stream is retryable" (provider-retryable-error? provider-aborted-error) #t) + (check! "closed TCP stream port is retryable" + (provider-retryable-error? closed-tcp-port-error) #t) (check! "ordinary provider errors remain terminal" (provider-retryable-error? ordinary-error) #f))