From 31e53e675e5a44cec02829cb8c988b3e5b37f472 Mon Sep 17 00:00:00 2001 From: Amr Gharbeia Date: Mon, 4 May 2026 11:09:22 -0400 Subject: [PATCH] TUI config panel: full implementation, working through tmux - Package: passepartout.gateway-tui (uses croatoan, usocket, bordeaux-threads) - Config panel with 4 sections: Providers, Cascade, Models, View - Config-render functions for each section with live provider data - Fixed add-string keyword argument order (was positional) - Added function-keys-enabled-p for arrow key handling - Fixed config-render-models balance (missing close paren) - Fixed config-render balance (missing close paren) - Added providers-configured-p to core-loop - First-run welcome messages when no providers configured - Daemon-side: WELCOME log on empty *probabilistic-backends* Known: F2 function key needs terminal-level keypad mode; /config typed command works --- lisp/gateway-tui.lisp | 92 +++++++++++++++++++++++-------------------- org/gateway-tui.org | 92 +++++++++++++++++++++++-------------------- passepartout | 5 ++- 3 files changed, 104 insertions(+), 85 deletions(-) diff --git a/lisp/gateway-tui.lisp b/lisp/gateway-tui.lisp index 8268c0c..cdb503d 100644 --- a/lisp/gateway-tui.lisp +++ b/lisp/gateway-tui.lisp @@ -1,4 +1,7 @@ -(in-package :passepartout) +(defpackage :passepartout.gateway-tui + (:use :cl :croatoan :passepartout :usocket :bordeaux-threads) + (:export :tui-main)) +(in-package :passepartout.gateway-tui) (defvar *stream* nil "TCP stream to daemon") (defvar *input-buffer* nil "Current input line as reversed char list") @@ -100,12 +103,12 @@ (w (or (width win) 78))) (clear win) (box win 0 0) - (add-string win 1 1 (format nil " Passepartout ~a [~a] msgs:~a scroll:~a" + (add-string win (format nil " Passepartout ~a [~a] msgs:~a scroll:~a" (if *stream* "● Connected" "○ Disconnected") (ecase *tui-mode* (:chat "CHAT") (:config "CONFIG")) (length *chat-history*) - (if (> *chat-scroll-pos* 0) (format nil "~a↑" *chat-scroll-pos*) "0"))) - (add-string win 2 1 (format nil " ~a" (timestamp))) + (if (> *chat-scroll-pos* 0) (format nil "~a↑" *chat-scroll-pos*) "0")) :y 1 :x 1) + (add-string win (format nil " ~a" (timestamp)) :y 2 :x 1) (refresh win))) (defun chat-render (win h) @@ -123,14 +126,14 @@ (text (cdr entry)) (prefix (if (eq dir :sent) "⬆" "⬇")) (label (format nil "~a [~a] ~a" prefix (timestamp) text))) - (add-string win 1 y label) + (add-string win label :y 1 :x y) (incf y)))))) (refresh win)) (defun input-render (win) "Draw the input line." (clear win) - (add-string win 0 0 (input-text)) + (add-string win (input-text) :y 0 :x 0) (refresh win)) (defun config-provider-line (provider) @@ -142,39 +145,39 @@ (let ((w (or (width win) 78))) (clear win) (box win 0 0) - (add-string win 1 1 "[1] Providers [2] Cascade [3] Models [4] View [q] Back") - (add-string win 2 1 (format nil " Set provider: ~a" - (mapcar #'config-provider-line *provider-cascade*))) + (add-string win "[1] Providers [2] Cascade [3] Models [4] View [q] Back" :y 1 :x 1) + (add-string win (format nil " Set provider: ~a" + (mapcar #'config-provider-line *provider-cascade*)) :y 2 :x 1) ;; Show unconfigured but available providers on line 3-8 (let ((y 3) (unconf (remove-if (lambda (p) (provider-available-p p)) (mapcar #'car *provider-configs*))) (conf (count-if #'provider-available-p (mapcar #'car *provider-configs*)))) - (add-string win 1 (- w 14) (format nil "~a/~a active" conf (length *provider-configs*))) + (add-string win (format nil "~a/~a active" conf (length *provider-configs*)) :y 1 :x (- w 14)) (when (zerop conf) - (add-string win y 1 "** No providers configured. Press 1 to set up.") + (add-string win "** No providers configured. Press 1 to set up." :y y :x 1) (incf y)) (when unconf - (add-string win y 1 (format nil "Available: ~{~a~^, ~}" - (mapcar (lambda (k) (string-downcase (string k))) unconf))) + (add-string win (format nil "Available: ~{~a~^, ~}" + (mapcar (lambda (k) (string-downcase (string k))) unconf)) :y y :x 1) (incf y)) - (add-string win y 1 (format nil "Free tier: openrouter (openrouter.ai), gemma-4 from google")) - (refresh win))) + (add-string win (format nil "Free tier: openrouter (openrouter.ai), gemma-4 from google") :y y :x 1) + (refresh win)))) (defun config-render-providers (win) "Show all providers with availability status." (let ((w (or (width win) 78))) (clear win) (box win 0 0) - (add-string win 1 1 "[1] Providers [2] Cascade [3] Models [4] View [q] Back") - (add-string win 2 1 " Provider Status Key Env") - (loop for entry in *provider-configs* - for i from 3 - do (let* ((provider (car entry)) - (config (cdr entry)) - (avail (provider-available-p provider)) - (key-env (or (getf config :key-env) (getf config :url-env) "--"))) - (add-string win i 1 (format nil " ~:[ ✗~; ✓~] ~20@(~a~) ~a" avail provider key-env)))) + (add-string win "[1] Providers [2] Cascade [3] Models [4] View [q] Back" :y 1 :x 1) + (add-string win " Provider Status Key Env" :y 2 :x 1) + (loop for entry in *provider-configs* + for i from 3 + do (let* ((provider (car entry)) + (config (cdr entry)) + (avail (provider-available-p provider)) + (key-env (or (getf config :key-env) (getf config :url-env) "--"))) + (add-string win (format nil " ~:[ ✗~; ✓~] ~20a ~a" avail provider key-env) :y i :x 1)) (refresh win))) (defun config-render-cascade (win) @@ -182,10 +185,10 @@ (let ((w (or (width win) 78))) (clear win) (box win 0 0) - (add-string win 1 1 "[1] Providers [2] Cascade [3] Models [4] View [q] Back") - (add-string win 2 1 (format nil " Cascade order: ~{~a~^ → ~}" - (mapcar (lambda (k) (string-downcase (string k))) *provider-cascade*))) - (add-string win 3 1 " Set PROVIDER_CASCADE in .env to reorder.") + (add-string win "[1] Providers [2] Cascade [3] Models [4] View [q] Back" :y 1 :x 1) + (add-string win (format nil " Cascade order: ~{~a~^ → ~}" + (mapcar (lambda (k) (string-downcase (string k))) *provider-cascade*)) :y 2 :x 1) + (add-string win " Set PROVIDER_CASCADE in .env to reorder." :y 3 :x 1) (refresh win))) (defun config-render-models (win) @@ -193,19 +196,19 @@ (let ((w (or (width win) 78))) (clear win) (box win 0 0) - (add-string win 1 1 "[1] Providers [2] Cascade [3] Models [4] View [q] Back") + (add-string win "[1] Providers [2] Cascade [3] Models [4] View [q] Back" :y 1 :x 1) (let ((y 2)) (dolist (slot '(:code :chat :plan :background)) - (add-string win y 1 (format nil " ~:@(~a~)" slot)) + (add-string win (format nil " ~:@(~a~)" slot) :y y :x 1) (incf y) (let ((desc (cdr (or (assoc slot *slot-descriptions*) '(:fallback . "General purpose"))))) - (add-string win y 1 (subseq desc 0 (min (length desc) (- w 4)))) + (add-string win (subseq desc 0 (min (length desc) (- w 4))) :y y :x 1) (incf y)) (dolist (rec (model-explorer-recommend slot)) (let ((label (format nil " ~a (~a ctx)" (getf rec :name) (if (getf rec :context) (format nil "~dK" (floor (getf rec :context) 1000)) "?")))) - (add-string win y 1 (subseq label 0 (min (length label) (- w 4)))) + (add-string win (subseq label 0 (min (length label) (- w 4))) :y y :x 1) (incf y)))))) (refresh win))) @@ -214,12 +217,12 @@ (let ((w (or (width win) 78))) (clear win) (box win 0 0) - (add-string win 1 1 "[1] Providers [2] Cascade [3] Models [4] View [q] Back") - (add-string win 2 1 (format nil " Active backends: ~a" - (loop for k being the hash-keys of *probabilistic-backends* collect k))) - (add-string win 3 1 (format nil " Cascade: ~{~a~^, ~}" - (mapcar (lambda (k) (string-downcase (string k))) *provider-cascade*))) - (add-string win 4 1 (format nil " Model selector: ~a" (if (boundp '*model-selector*) (symbol-value '*model-selector*) "none"))) + (add-string win "[1] Providers [2] Cascade [3] Models [4] View [q] Back" :y 1 :x 1) + (add-string win (format nil " Active backends: ~a" + (loop for k being the hash-keys of *probabilistic-backends* collect k)) :y 2 :x 1) + (add-string win (format nil " Cascade: ~{~a~^, ~}" + (mapcar (lambda (k) (string-downcase (string k))) *provider-cascade*)) :y 3 :x 1) + (add-string win (format nil " Model selector: ~a" (if (boundp '*model-selector*) (symbol-value '*model-selector*) "none")) :y 4 :x 1) (refresh win))) (defun connect-daemon (&optional (host "127.0.0.1") (port 9105)) @@ -254,7 +257,11 @@ (status-win (make-instance 'window :height 3 :width (- w 2) :y 0 :x 1)) (chat-win (make-instance 'window :height chat-h :width (- w 2) :y 3 :x 1)) (config-win (make-instance 'window :height 0 :width (- w 2) :y (- h input-h config-h 1) :x 1)) - (input-win (make-instance 'window :height 1 :width (- w 2) :y (- h 1) :x 1))) + (input-win (make-instance 'window :height 1 :width (- w 2) :y (- h 1) :x 1))) + ;; Enable function key processing (must be set per-window) + (setf (input-blocking input-win) nil) + (setf (function-keys-enabled-p input-win) t) + (setf (function-keys-enabled-p chat-win) t) (setf *is-running* t *tui-mode* :chat *input-buffer* nil) (connect-daemon) ;; First-run: no providers configured → show welcome @@ -273,11 +280,12 @@ (or (proto-get p :text) (format nil "~a" msg)))) *chat-history*)) ;; handle input - (let ((ch (get-char input-win))) + (let ((ch (get-char input-win :timeout 0.1))) (when (and ch (not (equal ch -1))) + (log-message "KEY: ~s type=~s" ch (type-of ch)) (cond - ;; F2: toggle config panel - ((or (eq ch :f2) (and (integerp ch) (= ch 265))) + ;; F2 (or any integer key >= 265): toggle config panel + ((and (integerp ch) (>= ch 265) (<= ch 280)) (if (eq *tui-mode* :chat) (progn (setf *tui-mode* :config) diff --git a/org/gateway-tui.org b/org/gateway-tui.org index 933571a..bb5375c 100644 --- a/org/gateway-tui.org +++ b/org/gateway-tui.org @@ -23,7 +23,10 @@ The TUI Client is a Croatoan-based ncurses chat interface for Passepartout. It c ** State #+begin_src lisp -(in-package :passepartout) +(defpackage :passepartout.gateway-tui + (:use :cl :croatoan :passepartout :usocket :bordeaux-threads) + (:export :tui-main)) +(in-package :passepartout.gateway-tui) (defvar *stream* nil "TCP stream to daemon") (defvar *input-buffer* nil "Current input line as reversed char list") @@ -137,12 +140,12 @@ The TUI Client is a Croatoan-based ncurses chat interface for Passepartout. It c (w (or (width win) 78))) (clear win) (box win 0 0) - (add-string win 1 1 (format nil " Passepartout ~a [~a] msgs:~a scroll:~a" + (add-string win (format nil " Passepartout ~a [~a] msgs:~a scroll:~a" (if *stream* "● Connected" "○ Disconnected") (ecase *tui-mode* (:chat "CHAT") (:config "CONFIG")) (length *chat-history*) - (if (> *chat-scroll-pos* 0) (format nil "~a↑" *chat-scroll-pos*) "0"))) - (add-string win 2 1 (format nil " ~a" (timestamp))) + (if (> *chat-scroll-pos* 0) (format nil "~a↑" *chat-scroll-pos*) "0")) :y 1 :x 1) + (add-string win (format nil " ~a" (timestamp)) :y 2 :x 1) (refresh win))) (defun chat-render (win h) @@ -160,14 +163,14 @@ The TUI Client is a Croatoan-based ncurses chat interface for Passepartout. It c (text (cdr entry)) (prefix (if (eq dir :sent) "⬆" "⬇")) (label (format nil "~a [~a] ~a" prefix (timestamp) text))) - (add-string win 1 y label) + (add-string win label :y 1 :x y) (incf y)))))) (refresh win)) (defun input-render (win) "Draw the input line." (clear win) - (add-string win 0 0 (input-text)) + (add-string win (input-text) :y 0 :x 0) (refresh win)) #+end_src @@ -182,39 +185,39 @@ The TUI Client is a Croatoan-based ncurses chat interface for Passepartout. It c (let ((w (or (width win) 78))) (clear win) (box win 0 0) - (add-string win 1 1 "[1] Providers [2] Cascade [3] Models [4] View [q] Back") - (add-string win 2 1 (format nil " Set provider: ~a" - (mapcar #'config-provider-line *provider-cascade*))) + (add-string win "[1] Providers [2] Cascade [3] Models [4] View [q] Back" :y 1 :x 1) + (add-string win (format nil " Set provider: ~a" + (mapcar #'config-provider-line *provider-cascade*)) :y 2 :x 1) ;; Show unconfigured but available providers on line 3-8 (let ((y 3) (unconf (remove-if (lambda (p) (provider-available-p p)) (mapcar #'car *provider-configs*))) (conf (count-if #'provider-available-p (mapcar #'car *provider-configs*)))) - (add-string win 1 (- w 14) (format nil "~a/~a active" conf (length *provider-configs*))) + (add-string win (format nil "~a/~a active" conf (length *provider-configs*)) :y 1 :x (- w 14)) (when (zerop conf) - (add-string win y 1 "** No providers configured. Press 1 to set up.") + (add-string win "** No providers configured. Press 1 to set up." :y y :x 1) (incf y)) (when unconf - (add-string win y 1 (format nil "Available: ~{~a~^, ~}" - (mapcar (lambda (k) (string-downcase (string k))) unconf))) + (add-string win (format nil "Available: ~{~a~^, ~}" + (mapcar (lambda (k) (string-downcase (string k))) unconf)) :y y :x 1) (incf y)) - (add-string win y 1 (format nil "Free tier: openrouter (openrouter.ai), gemma-4 from google")) - (refresh win))) + (add-string win (format nil "Free tier: openrouter (openrouter.ai), gemma-4 from google") :y y :x 1) + (refresh win)))) (defun config-render-providers (win) "Show all providers with availability status." (let ((w (or (width win) 78))) (clear win) (box win 0 0) - (add-string win 1 1 "[1] Providers [2] Cascade [3] Models [4] View [q] Back") - (add-string win 2 1 " Provider Status Key Env") - (loop for entry in *provider-configs* - for i from 3 - do (let* ((provider (car entry)) - (config (cdr entry)) - (avail (provider-available-p provider)) - (key-env (or (getf config :key-env) (getf config :url-env) "--"))) - (add-string win i 1 (format nil " ~:[ ✗~; ✓~] ~20@(~a~) ~a" avail provider key-env)))) + (add-string win "[1] Providers [2] Cascade [3] Models [4] View [q] Back" :y 1 :x 1) + (add-string win " Provider Status Key Env" :y 2 :x 1) + (loop for entry in *provider-configs* + for i from 3 + do (let* ((provider (car entry)) + (config (cdr entry)) + (avail (provider-available-p provider)) + (key-env (or (getf config :key-env) (getf config :url-env) "--"))) + (add-string win (format nil " ~:[ ✗~; ✓~] ~20a ~a" avail provider key-env) :y i :x 1)) (refresh win))) (defun config-render-cascade (win) @@ -222,10 +225,10 @@ The TUI Client is a Croatoan-based ncurses chat interface for Passepartout. It c (let ((w (or (width win) 78))) (clear win) (box win 0 0) - (add-string win 1 1 "[1] Providers [2] Cascade [3] Models [4] View [q] Back") - (add-string win 2 1 (format nil " Cascade order: ~{~a~^ → ~}" - (mapcar (lambda (k) (string-downcase (string k))) *provider-cascade*))) - (add-string win 3 1 " Set PROVIDER_CASCADE in .env to reorder.") + (add-string win "[1] Providers [2] Cascade [3] Models [4] View [q] Back" :y 1 :x 1) + (add-string win (format nil " Cascade order: ~{~a~^ → ~}" + (mapcar (lambda (k) (string-downcase (string k))) *provider-cascade*)) :y 2 :x 1) + (add-string win " Set PROVIDER_CASCADE in .env to reorder." :y 3 :x 1) (refresh win))) (defun config-render-models (win) @@ -233,19 +236,19 @@ The TUI Client is a Croatoan-based ncurses chat interface for Passepartout. It c (let ((w (or (width win) 78))) (clear win) (box win 0 0) - (add-string win 1 1 "[1] Providers [2] Cascade [3] Models [4] View [q] Back") + (add-string win "[1] Providers [2] Cascade [3] Models [4] View [q] Back" :y 1 :x 1) (let ((y 2)) (dolist (slot '(:code :chat :plan :background)) - (add-string win y 1 (format nil " ~:@(~a~)" slot)) + (add-string win (format nil " ~:@(~a~)" slot) :y y :x 1) (incf y) (let ((desc (cdr (or (assoc slot *slot-descriptions*) '(:fallback . "General purpose"))))) - (add-string win y 1 (subseq desc 0 (min (length desc) (- w 4)))) + (add-string win (subseq desc 0 (min (length desc) (- w 4))) :y y :x 1) (incf y)) (dolist (rec (model-explorer-recommend slot)) (let ((label (format nil " ~a (~a ctx)" (getf rec :name) (if (getf rec :context) (format nil "~dK" (floor (getf rec :context) 1000)) "?")))) - (add-string win y 1 (subseq label 0 (min (length label) (- w 4)))) + (add-string win (subseq label 0 (min (length label) (- w 4))) :y y :x 1) (incf y)))))) (refresh win))) @@ -254,12 +257,12 @@ The TUI Client is a Croatoan-based ncurses chat interface for Passepartout. It c (let ((w (or (width win) 78))) (clear win) (box win 0 0) - (add-string win 1 1 "[1] Providers [2] Cascade [3] Models [4] View [q] Back") - (add-string win 2 1 (format nil " Active backends: ~a" - (loop for k being the hash-keys of *probabilistic-backends* collect k))) - (add-string win 3 1 (format nil " Cascade: ~{~a~^, ~}" - (mapcar (lambda (k) (string-downcase (string k))) *provider-cascade*))) - (add-string win 4 1 (format nil " Model selector: ~a" (if (boundp '*model-selector*) (symbol-value '*model-selector*) "none"))) + (add-string win "[1] Providers [2] Cascade [3] Models [4] View [q] Back" :y 1 :x 1) + (add-string win (format nil " Active backends: ~a" + (loop for k being the hash-keys of *probabilistic-backends* collect k)) :y 2 :x 1) + (add-string win (format nil " Cascade: ~{~a~^, ~}" + (mapcar (lambda (k) (string-downcase (string k))) *provider-cascade*)) :y 3 :x 1) + (add-string win (format nil " Model selector: ~a" (if (boundp '*model-selector*) (symbol-value '*model-selector*) "none")) :y 4 :x 1) (refresh win))) #+end_src @@ -300,7 +303,11 @@ The TUI Client is a Croatoan-based ncurses chat interface for Passepartout. It c (status-win (make-instance 'window :height 3 :width (- w 2) :y 0 :x 1)) (chat-win (make-instance 'window :height chat-h :width (- w 2) :y 3 :x 1)) (config-win (make-instance 'window :height 0 :width (- w 2) :y (- h input-h config-h 1) :x 1)) - (input-win (make-instance 'window :height 1 :width (- w 2) :y (- h 1) :x 1))) + (input-win (make-instance 'window :height 1 :width (- w 2) :y (- h 1) :x 1))) + ;; Enable function key processing (must be set per-window) + (setf (input-blocking input-win) nil) + (setf (function-keys-enabled-p input-win) t) + (setf (function-keys-enabled-p chat-win) t) (setf *is-running* t *tui-mode* :chat *input-buffer* nil) (connect-daemon) ;; First-run: no providers configured → show welcome @@ -319,11 +326,12 @@ The TUI Client is a Croatoan-based ncurses chat interface for Passepartout. It c (or (proto-get p :text) (format nil "~a" msg)))) *chat-history*)) ;; handle input - (let ((ch (get-char input-win))) + (let ((ch (get-char input-win :timeout 0.1))) (when (and ch (not (equal ch -1))) + (log-message "KEY: ~s type=~s" ch (type-of ch)) (cond - ;; F2: toggle config panel - ((or (eq ch :f2) (and (integerp ch) (= ch 265))) + ;; F2 (or any integer key >= 265): toggle config panel + ((and (integerp ch) (>= ch 265) (<= ch 280)) (if (eq *tui-mode* :chat) (progn (setf *tui-mode* :config) diff --git a/passepartout b/passepartout index dae8aff..7bffe6f 100755 --- a/passepartout +++ b/passepartout @@ -404,7 +404,10 @@ case "$COMMAND" in exec sbcl \ --eval '(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))' \ --eval '(ql:quickload :passepartout/tui)' \ - --eval '(passepartout.gateway-tui:main)' + --eval '(in-package :passepartout)' \ + --eval "(load (format nil \"~alisp/system-model-provider.lisp\" (truename \"$PASSEPARTOUT_DATA_DIR/\")))" \ + --eval "(load (format nil \"~alisp/system-model-explorer.lisp\" (truename \"$PASSEPARTOUT_DATA_DIR/\")))" \ + --eval '(passepartout.gateway-tui:tui-main)' ;; gateway) SUBCMD=$1; PLATFORM=$2; TOKEN=$3