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
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user