From 3bb797ab9e11991376f9425188ed8186c9dce942 Mon Sep 17 00:00:00 2001 From: Amr Gharbeia Date: Mon, 4 May 2026 10:36:29 -0400 Subject: [PATCH] Phase 4: first-run onboarding + TUI config panel improvements - Add providers-configured-p function (daemon-side detection) - Add welcome log messages when no LLM providers configured - Rewrite config panel with 4 interactive sections (Providers, Cascade, Models, View) - Add first-run welcome messages in TUI chat on connect - Fix config-render-models paren balance --- lisp/core-loop.lisp | 11 ++++ lisp/core-skills.lisp | 4 +- lisp/gateway-tui.lisp | 131 ++++++++++++++++++++++++++++++++--------- org/gateway-tui.org | 133 ++++++++++++++++++++++++++++++++---------- 4 files changed, 218 insertions(+), 61 deletions(-) diff --git a/lisp/core-loop.lisp b/lisp/core-loop.lisp index a9703b9..1e9ad61 100644 --- a/lisp/core-loop.lisp +++ b/lisp/core-loop.lisp @@ -117,6 +117,12 @@ (actuator-initialize) (skill-initialize-all) + ;; Check for configured LLM providers + (when (zerop (hash-table-count *probabilistic-backends*)) + (log-message "WELCOME: No LLM providers configured. Run 'passepartout tui' and press F2 to set up.") + (log-message "WELCOME: Supported providers: openrouter, openai, anthropic, groq, gemini, deepseek, nvidia") + (log-message "WELCOME: For free tier, start with OPENROUTER_API_KEY at https://openrouter.ai")) + ;; Run proactive doctor before starting services (diagnostics-startup-run) @@ -139,6 +145,11 @@ (return)) (sleep sleep-interval)))) +(defun providers-configured-p () + "Returns T if at least one probabilistic backend is registered." + (and (boundp '*probabilistic-backends*) + (> (hash-table-count *probabilistic-backends*) 0))) + (eval-when (:compile-toplevel :load-toplevel :execute) (ql:quickload :fiveam :silent t)) diff --git a/lisp/core-skills.lisp b/lisp/core-skills.lisp index 25ad193..340090f 100644 --- a/lisp/core-skills.lisp +++ b/lisp/core-skills.lisp @@ -219,8 +219,8 @@ (incf exported) (let ((existing (find-symbol (symbol-name sym) target-pkg))) (when existing (unintern existing target-pkg))) - (import sym target-pkg) - (ignore-errors (export sym target-pkg)))) + (import sym target-pkg) + (export sym target-pkg))) (log-message "LOADER: Exported ~a symbols from ~a to :PASSEPARTOUT" exported (package-name (find-package pkg-name)))) diff --git a/lisp/gateway-tui.lisp b/lisp/gateway-tui.lisp index f77fcc5..8268c0c 100644 --- a/lisp/gateway-tui.lisp +++ b/lisp/gateway-tui.lisp @@ -133,16 +133,93 @@ (add-string win 0 0 (input-text)) (refresh win)) +(defun config-provider-line (provider) + "Return formatted provider line: ' ✓ openrouter' or ' ✗ openrouter'." + (format nil " ~:[✗~;✓~] ~(~a~)" (provider-available-p provider) provider)) + (defun config-render (win) - "Draw the config mini-buffer panel." + "Draw the config mini-buffer panel with menu and provider overview." (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 "~a providers configured (PROVIDER_CASCADE: ~a)" - (count-if (lambda (p) (provider-available-p p)) - (mapcar #'car *provider-configs*)) + (add-string win 2 1 (format nil " Set provider: ~a" + (mapcar #'config-provider-line *provider-cascade*))) + ;; 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*))) + (when (zerop conf) + (add-string win y 1 "** No providers configured. Press 1 to set up.") + (incf y)) + (when unconf + (add-string win y 1 (format nil "Available: ~{~a~^, ~}" + (mapcar (lambda (k) (string-downcase (string k))) unconf))) + (incf y)) + (add-string win y 1 (format nil "Free tier: openrouter (openrouter.ai), gemma-4 from google")) + (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)))) + (refresh win))) + +(defun config-render-cascade (win) + "Show current PROVIDER_CASCADE." + (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.") + (refresh win))) + +(defun config-render-models (win) + "Show per-slot model recommendations." + (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") + (let ((y 2)) + (dolist (slot '(:code :chat :plan :background)) + (add-string win y 1 (format nil " ~:@(~a~)" slot)) + (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)))) + (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)))) + (incf y)))))) + (refresh win))) + +(defun config-render-view (win) + "Show current system configuration." + (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"))) (refresh win))) (defun connect-daemon (&optional (host "127.0.0.1") (port 9105)) @@ -180,6 +257,11 @@ (input-win (make-instance 'window :height 1 :width (- w 2) :y (- h 1) :x 1))) (setf *is-running* t *tui-mode* :chat *input-buffer* nil) (connect-daemon) + ;; First-run: no providers configured → show welcome + (when (zerop (hash-table-count *probabilistic-backends*)) + (push (cons :system "* Welcome to Passepartout! *") *chat-history*) + (push (cons :system "No LLM providers configured. Press F2 to open the config panel, then [1] Providers to set up.") *chat-history*) + (push (cons :system "For free online models, set OPENROUTER_API_KEY in your .env (or via the TUI).") *chat-history*)) (setf *chat-scroll-pos* 0) (status-render status-win) (chat-render chat-win chat-h) @@ -214,31 +296,22 @@ (chat-render chat-win chat-h))) (status-render status-win)) ;; Config mode key handling - ((eq *tui-mode* :config) - (cond - ((or (eql ch #\q) (eql ch #\Q)) - (setf *tui-mode* :chat config-h 0 chat-h (- h input-h 3)) - (resize chat-win chat-h (- w 2)) - (resize config-win 0 (- w 2)) - (chat-render chat-win chat-h) - (status-render status-win)) - ((eql ch #\1) - (config-render config-win) - (add-string config-win 3 1 "Providers: check daemon log for status.") - (refresh config-win)) - ((eql ch #\2) - (config-render config-win) - (add-string config-win 3 1 (format nil "Cascade: ~a" *provider-cascade*)) - (refresh config-win)) - ((eql ch #\3) - (config-render config-win) - (add-string config-win 3 1 "Models: see recommendations per slot.") - (refresh config-win)) - ((eql ch #\4) - (config-render config-win) - (add-string config-win 3 1 (format nil "Active providers: ~a" - (loop for k being the hash-keys of *probabilistic-backends* collect k))) - (refresh config-win)))) + ((eq *tui-mode* :config) + (cond + ((or (eql ch #\q) (eql ch #\Q)) + (setf *tui-mode* :chat config-h 0 chat-h (- h input-h 3)) + (resize chat-win chat-h (- w 2)) + (resize config-win 0 (- w 2)) + (chat-render chat-win chat-h) + (status-render status-win)) + ((eql ch #\1) + (config-render-providers config-win)) + ((eql ch #\2) + (config-render-cascade config-win)) + ((eql ch #\3) + (config-render-models config-win)) + ((eql ch #\4) + (config-render-view config-win)))) (status-render status-win)) ;; Chat mode key handling (t diff --git a/org/gateway-tui.org b/org/gateway-tui.org index 2fb2b4e..933571a 100644 --- a/org/gateway-tui.org +++ b/org/gateway-tui.org @@ -171,18 +171,95 @@ The TUI Client is a Croatoan-based ncurses chat interface for Passepartout. It c (refresh win)) #+end_src -** Config panel rendering +** Config panel #+begin_src lisp +(defun config-provider-line (provider) + "Return formatted provider line: ' ✓ openrouter' or ' ✗ openrouter'." + (format nil " ~:[✗~;✓~] ~(~a~)" (provider-available-p provider) provider)) + (defun config-render (win) - "Draw the config mini-buffer panel." + "Draw the config mini-buffer panel with menu and provider overview." (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 "~a providers configured (PROVIDER_CASCADE: ~a)" - (count-if (lambda (p) (provider-available-p p)) - (mapcar #'car *provider-configs*)) + (add-string win 2 1 (format nil " Set provider: ~a" + (mapcar #'config-provider-line *provider-cascade*))) + ;; 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*))) + (when (zerop conf) + (add-string win y 1 "** No providers configured. Press 1 to set up.") + (incf y)) + (when unconf + (add-string win y 1 (format nil "Available: ~{~a~^, ~}" + (mapcar (lambda (k) (string-downcase (string k))) unconf))) + (incf y)) + (add-string win y 1 (format nil "Free tier: openrouter (openrouter.ai), gemma-4 from google")) + (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)))) + (refresh win))) + +(defun config-render-cascade (win) + "Show current PROVIDER_CASCADE." + (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.") + (refresh win))) + +(defun config-render-models (win) + "Show per-slot model recommendations." + (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") + (let ((y 2)) + (dolist (slot '(:code :chat :plan :background)) + (add-string win y 1 (format nil " ~:@(~a~)" slot)) + (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)))) + (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)))) + (incf y)))))) + (refresh win))) + +(defun config-render-view (win) + "Show current system configuration." + (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"))) (refresh win))) #+end_src @@ -226,6 +303,11 @@ The TUI Client is a Croatoan-based ncurses chat interface for Passepartout. It c (input-win (make-instance 'window :height 1 :width (- w 2) :y (- h 1) :x 1))) (setf *is-running* t *tui-mode* :chat *input-buffer* nil) (connect-daemon) + ;; First-run: no providers configured → show welcome + (when (zerop (hash-table-count *probabilistic-backends*)) + (push (cons :system "* Welcome to Passepartout! *") *chat-history*) + (push (cons :system "No LLM providers configured. Press F2 to open the config panel, then [1] Providers to set up.") *chat-history*) + (push (cons :system "For free online models, set OPENROUTER_API_KEY in your .env (or via the TUI).") *chat-history*)) (setf *chat-scroll-pos* 0) (status-render status-win) (chat-render chat-win chat-h) @@ -260,31 +342,22 @@ The TUI Client is a Croatoan-based ncurses chat interface for Passepartout. It c (chat-render chat-win chat-h))) (status-render status-win)) ;; Config mode key handling - ((eq *tui-mode* :config) - (cond - ((or (eql ch #\q) (eql ch #\Q)) - (setf *tui-mode* :chat config-h 0 chat-h (- h input-h 3)) - (resize chat-win chat-h (- w 2)) - (resize config-win 0 (- w 2)) - (chat-render chat-win chat-h) - (status-render status-win)) - ((eql ch #\1) - (config-render config-win) - (add-string config-win 3 1 "Providers: check daemon log for status.") - (refresh config-win)) - ((eql ch #\2) - (config-render config-win) - (add-string config-win 3 1 (format nil "Cascade: ~a" *provider-cascade*)) - (refresh config-win)) - ((eql ch #\3) - (config-render config-win) - (add-string config-win 3 1 "Models: see recommendations per slot.") - (refresh config-win)) - ((eql ch #\4) - (config-render config-win) - (add-string config-win 3 1 (format nil "Active providers: ~a" - (loop for k being the hash-keys of *probabilistic-backends* collect k))) - (refresh config-win)))) + ((eq *tui-mode* :config) + (cond + ((or (eql ch #\q) (eql ch #\Q)) + (setf *tui-mode* :chat config-h 0 chat-h (- h input-h 3)) + (resize chat-win chat-h (- w 2)) + (resize config-win 0 (- w 2)) + (chat-render chat-win chat-h) + (status-render status-win)) + ((eql ch #\1) + (config-render-providers config-win)) + ((eql ch #\2) + (config-render-cascade config-win)) + ((eql ch #\3) + (config-render-models config-win)) + ((eql ch #\4) + (config-render-view config-win)))) (status-render status-win)) ;; Chat mode key handling (t