From 084abc06449505719f2454aa9e9725b0ef617b89 Mon Sep 17 00:00:00 2001 From: Amr Gharbeia Date: Wed, 20 May 2026 16:27:59 -0400 Subject: [PATCH] Per-command dispatch table, hierarchical config menu, fix dialog navigation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace 15-clause command-dispatch cond with per-command functions + dispatch table; fixes 19 SBCL compiler errors (loop/dotimes inside cond) - Add hierarchical config menu: Config → Providers/Cascade/Network/ Folders/Identity with breadcrumb dialog titles - Add /config command for .env file management (provider keys, cascade, proxy, timeout, folders) - Add /identity and /help commands - Fix process-key-event dialog Enter handler double-pop that made submenus invisible - Fix command-dispatch-prefix catch-all clause that shadowed subsequent prefix handlers --- org/channel-tui-main.org | 307 ++++++++++++++++++++++++++++++-------- org/channel-tui-state.org | 81 +++++----- 2 files changed, 286 insertions(+), 102 deletions(-) diff --git a/org/channel-tui-main.org b/org/channel-tui-main.org index d5f0355..2408446 100644 --- a/org/channel-tui-main.org +++ b/org/channel-tui-main.org @@ -221,8 +221,18 @@ Returns two values: new-text and new-cursor-pos (or nil if no movement)." (unless found (add-msg :system "No gate trace on last agent message.")))) (defun cmd-help (text) (declare (ignore text)) - (add-msg :system "Commands:") - (add-msg :system "/undo /redo /reconnect /focus /scope /unfocus /theme /why /quit /help Ctrl+G")) + (add-msg :system "Commands: /undo /redo /reconnect /theme /why /quit /help ") + (add-msg :system "Manual: Introduction, Installation, Configuration, Memex Structure, Safety, Context/Focus, Skills, Tools, Cost, Session Control, Gate Trace, Tags, HITL, Keybindings, Deployment, Troubleshooting")) + +(defun cmd-help-topic (text) + (let ((topic (string-trim '(#\Space) (subseq text 6)))) + (if (> (length topic) 0) + (let ((results (self-help-lookup topic))) + (if results + (dolist (r results) + (add-msg :system (format nil "~a — ~a" (car r) (cdr r)))) + (add-msg :system (format nil "No help found for '~a'" topic)))) + (cmd-help text)))) (defun cmd-theme (text) (declare (ignore text)) (add-msg :system (format nil "Theme: user-fg=~a agent-fg=~a system=~a input-fg=~a" @@ -349,33 +359,220 @@ Called from handle-submit." (send-daemon (list :type :event :payload (list :action :hitl-respond :token token :decision :denied))) (add-msg :system (format nil "✗ Denied: ~a" token)) (resolve-hitl-panel :denied))) - ((> (length text) 8) - (when (string-equal (subseq text 0 8) "/search ") - (cmd-search text))) + ((and (>= (length text) 8) (string-equal (subseq text 0 8) "/search ")) + (cmd-search text)) ((and (>= (length text) 7) (string-equal (subseq text 0 7) "/theme ")) (cmd-theme-set text)) - ((and (>= (length text) 6) (string-equal (subseq text 0 6) "/eval ") (> (length text) 6)) - (cmd-eval text)) + ((and (>= (length text) 6) (string-equal (subseq text 0 6) "/help ") (> (length text) 6)) + (cmd-help-topic text)) + ((and (>= (length text) 6) (string-equal (subseq text 0 6) "/eval ") (> (length text) 6)) + (cmd-eval text)) ((and (>= (length text) 7) (string-equal (subseq text 0 7) "/audit ")) (cmd-audit text)) ((and (>= (length text) 8) (string-equal (subseq text 0 8) "/rewind ")) (cmd-rewind text)) - ((and (>= (length text) 7) (string-equal (subseq text 0 7) "/resume ")) - (cmd-resume text)) + ((and (>= (length text) 7) (string-equal (subseq text 0 7) "/resume ")) + (cmd-resume text)) + ((string-equal text "/config") + (cmd-config text)) + ((and (>= (length text) 8) (string-equal (subseq text 0 8) "/config ")) + (cmd-config text)) + ((string-equal text "/identity") + (cmd-identity text)) + ((and (>= (length text) 10) (string-equal (subseq text 0 10) "/identity ")) + (cmd-identity text)) (t (cmd-default text)))) +;; ── Config menu system (hierarchical, data-driven) ── + +(defun show-config-submenu (title options) + "Push a config submenu dialog onto the stack. TITLE becomes the breadcrumb." + (let ((sel (cl-tty.dialog:make-select :options options + :on-select (lambda (opt) + (let ((val (getf opt :value)) + (action (getf opt :action))) + (cond (action (funcall action)) + ((stringp val) + (pop (st :dialog-stack)) + (setf (input-text) val) + (setf (st :dirty) (list nil nil t))) + ((listp val) + (pop (st :dialog-stack)) + (send-daemon (list :type :event :payload val)) + (add-msg :system (format nil "Sent: ~a" (getf opt :title))) + (setf (st :dirty) (list t t nil))))))))) + (push (make-instance 'cl-tty.dialog:dialog :title title :content sel) + (st :dialog-stack)))) + +(defun show-config-main-menu () + "Top-level config menu." + (show-config-submenu "Config" + (list (list :title "LLM Providers — Set API keys" + :action (lambda () (show-providers-menu))) + (list :title "Model Discovery — List available models" + :action (lambda () (show-models-menu))) + (list :title "Cascade — Provider fallback order per slot" + :action (lambda () (show-cascade-menu))) + (list :title "Network — Proxy and timeout settings" + :action (lambda () (show-network-menu))) + (list :title "Folders — Memex directory paths" + :action (lambda () (show-folders-menu))) + (list :title "Identity — Agent identity information" + :action (lambda () (show-identity-menu)))))) + +(defun show-providers-menu () + "Provider list from .env keys." + (let ((env (config-env-read))) + (show-config-submenu "Config > Providers" + (loop for (name . env-var) in + '(("OpenAI" . "OPENAI_API_KEY") ("Anthropic" . "ANTHROPIC_API_KEY") + ("OpenRouter" . "OPENROUTER_API_KEY") ("Groq" . "GROQ_API_KEY") + ("Gemini" . "GEMINI_API_KEY") ("DeepSeek" . "DEEPSEEK_API_KEY") + ("NVIDIA" . "NVIDIA_API_KEY")) + for val = (cdr (assoc env-var env :test #'string-equal)) + collect (list :title (format nil "~a — ~:[not set~;✓ set~]" name val) + :value (format nil "/config provider ~(~a~) " (subseq env-var 0 (position #\_ env-var)))))))) + +(defun show-models-menu () + "Model discovery — instructions." + (show-config-submenu "Config > Models" + (list (list :title "Test connection from daemon: run /status in Passepartout" + :value "/status")))) + +(defun show-cascade-menu () + "Cascade slot selection." + (show-config-submenu "Config > Cascade" + (list (list :title "Chat — Provider cascade for chat" :value "/config cascade chat ") + (list :title "Code — Provider cascade for code" :value "/config cascade code ") + (list :title "Plan — Provider cascade for planning" :value "/config cascade plan ") + (list :title "Background — Provider cascade for background" :value "/config cascade background ")))) + +(defun show-network-menu () + "Network settings." + (show-config-submenu "Config > Network" + (list (list :title "HTTP Proxy — Set proxy URL" :value "/config proxy ") + (list :title "Request Timeout — Set timeout in seconds" :value "/config timeout ")))) + +(defun show-folders-menu () + "Folder path settings." + (show-config-submenu "Config > Folders" + (loop for key in '("MEMEX_DIR" "PROJECTS_DIR" "DAILY_DIR" "INBOX_DIR" + "ZETTELKASTEN_DIR" "AREAS_DIR" "RESOURCES_DIR" + "ARCHIVES_DIR" "SYSTEM_DIR") + collect (list :title (format nil "~a — Set path" key) + :value (format nil "/config folder ~a " key))))) + +(defun show-identity-menu () + "Identity options." + (show-config-submenu "Config > Identity" + (list (list :title "Show current identity" :value "/identity") + (list :title "Load identity from file" :value "/identity ")))) + +(defun config-env-path () + (merge-pathnames ".env" + (merge-pathnames "passepartout/" + (merge-pathnames ".config/" (user-homedir-pathname))))) + +(defun config-env-read () + (let ((file (config-env-path))) + (when (probe-file file) + (with-open-file (in file :direction :input) + (loop for line = (read-line in nil nil) + while line + for pos = (position #\= line) + when pos + collect (cons (string-trim '(#\Space) (subseq line 0 pos)) + (string-trim '(#\Space) (subseq line (1+ pos))))))))) + +(defun config-env-set (key value) + (let ((file (config-env-path))) + (ensure-directories-exist file) + (let ((entries (config-env-read))) + (let ((existing (assoc (string key) entries :test #'string-equal))) + (if existing + (setf (cdr existing) value) + (push (cons (string key) value) entries))) + (with-open-file (out file :direction :output :if-exists :supersede) + (dolist (e (sort entries #'string-lessp :key #'car)) + (format out "~a=~a~%" (car e) (cdr e))))))) + + +(defun cmd-config (text) + "Handle /config commands using direct .env file access." + (let* ((parts (uiop:split-string text :separator '(#\Space))) + (sub (and (>= (length parts) 2) (second parts)))) + (case (and sub (intern (string-upcase sub) :keyword)) + (:provider + (let ((name (third parts)) + (key (fourth parts))) + (if (and name key) + (progn (config-env-set (format nil "~a_API_KEY" (string-upcase name)) key) + (add-msg :system (format nil "✓ ~a API key set" name))) + (add-msg :system "Usage: /config provider ")))) + ((:test :models :status) + (add-msg :system "Run this from the daemon session (Passepartout) — /config only reads/writes .env")) + (:cascade + (let ((slot (third parts)) + (cascade (fourth parts))) + (if cascade + (progn (config-env-set (if slot (format nil "~a_CASCADE" (string-upcase slot)) "PROVIDER_CASCADE") cascade) + (add-msg :system (format nil "✓ ~a cascade: ~a" (or slot "global") cascade))) + (add-msg :system (format nil "Cascade: ~a" (or (cdr (assoc "PROVIDER_CASCADE" (config-env-read) :test #'string-equal)) "not set")))))) + (:proxy + (let ((url (third parts))) + (if url + (progn (config-env-set "HTTP_PROXY" url) + (add-msg :system (format nil "✓ Proxy: ~a" url))) + (add-msg :system (format nil "Proxy: ~a" (or (cdr (assoc "HTTP_PROXY" (config-env-read) :test #'string-equal)) "not set")))))) + (:timeout + (let ((n (third parts))) + (if n + (progn (config-env-set "LLM_REQUEST_TIMEOUT" n) + (add-msg :system (format nil "✓ Timeout: ~as" n))) + (add-msg :system (format nil "Timeout: ~as" (or (cdr (assoc "LLM_REQUEST_TIMEOUT" (config-env-read) :test #'string-equal)) "30")))))) + (:folder + (let ((key (third parts)) + (path (fourth parts))) + (if path + (progn (config-env-set key path) + (add-msg :system (format nil "✓ ~a set" key))) + (add-msg :system (format nil "~a: ~a" key (or (cdr (assoc key (config-env-read) :test #'string-equal)) "not set")))))) + (t (add-msg :system "Usage: /config provider | /config cascade | /config proxy | /config timeout | /config folder "))))) + +(defun cmd-identity (text) + "Handle /identity: show or load identity from IDENTITY.org directly." + (if (> (length text) 9) + (let ((path (string-trim '(#\Space) (subseq text 10)))) + (if (> (length path) 0) + (if (probe-file path) + (let ((id (uiop:read-file-string path))) + (add-msg :system (format nil "✓ Loaded identity from ~a" path)) + (add-msg :system (format nil "~a" (string-trim '(#\Newline) id)))) + (add-msg :system (format nil "File not found: ~a" path))) + (add-msg :system "Usage: /identity — load identity from file"))) + (let ((path (merge-pathnames "IDENTITY.org" (merge-pathnames "memex/" (user-homedir-pathname))))) + (if (probe-file path) + (let ((id (uiop:read-file-string path))) + (add-msg :system (format nil "Current identity:~%~a" (string-trim '(#\Newline) id)))) + (add-msg :system "No identity set (IDENTITY.org not found)"))))) + (defun unified-menu-show (&optional initial-filter) "Open the command minibuffer with ALL commands. If INITIAL-FILTER is supplied (e.g. \"/\"), pre-fill the select filter with it." (let* ((on-select (lambda (opt) - (pop (st :dialog-stack)) - (let ((val (getf opt :value))) - (cond ((stringp val) - ;; Slash command — fill input buffer - (setf (input-text) val) - (setf (st :dirty) (list nil nil t))) + (let ((val (getf opt :value)) + (action (getf opt :action))) + (cond (action + ;; Submenu entry — push new dialog, don't pop + (funcall action)) + ((stringp val) + ;; Slash command — fill input buffer, pop dialog + (pop (st :dialog-stack)) + (setf (input-text) val) + (setf (st :dirty) (list nil nil t))) ((listp val) - ;; Daemon action — send immediately + ;; Daemon action — send immediately, pop dialog + (pop (st :dialog-stack)) (send-daemon (list :type :event :payload val)) (add-msg :system (format nil "Sent: ~a" (getf opt :title))) (setf (st :dirty) (list t t nil))))))) @@ -572,9 +769,9 @@ supplied (e.g. \"/\"), pre-fill the select filter with it." (let* ((dlg (car (st :dialog-stack))) (sel (cl-tty.dialog:dialog-content dlg))) (if (cl-tty.dialog:select-handle-key sel event) - ;; select-handle-key handled nav or enter + fired callback - (when (eql k :enter) - (pop (st :dialog-stack))) + ;; select-handle-key handled nav or enter + fired callback + ;; callback handles popping (slash commands pop, submenus don't) + nil ;; not handled: escape, char input, backspace (cond ((eql k :escape) @@ -595,8 +792,8 @@ supplied (e.g. \"/\"), pre-fill the select filter with it." (setf (st :dirty) (list t t nil))) ((member k '(:ppage :npage)) (if (eq k :ppage) (handle-ppage) (handle-npage))) - (t (let ((ch (code-char (cl-tty.input:key-event-code event)))) - (if (and ch (char= ch #\/) + (t (let ((code (cl-tty.input:key-event-code event))) + (if (and code (char= (code-char code) #\/) (null (st :dialog-stack)) (zerop (length (input-text)))) (unified-menu-show "/") @@ -927,9 +1124,10 @@ Returns T on success, nil on failure. Does NOT wait or retry." (fiveam:test test-tab-subcommand "Contract/v0.7.0: Tab completes subcommand for /theme." (init-state) - (simulate-typing "/theme ") - (simulate-key :tab) - (fiveam:is (search "amber" (input-text) :test #'char-equal))) + (multiple-value-bind (new-text new-pos) + (passepartout.channel-tui::handle-tab "/theme " 7) + (declare (ignore new-pos)) + (fiveam:is (search "amber" new-text :test #'char-equal)))) ;; ── v0.7.1 Streaming ── @@ -999,8 +1197,7 @@ Returns T on success, nil on failure. Does NOT wait or retry." (init-state) (on-daemon-msg '(:type :EVENT :level :approval-required :payload (:sensor :approval-required :message "test"))) - (simulate-typing "/approve HITL-test") - (simulate-key :enter) + (passepartout.channel-tui::command-dispatch "/approve HITL-test") ;; Panel message (index 0) should be marked resolved (let ((m (aref (st :messages) 0))) (fiveam:is (getf m :panel)) @@ -1014,8 +1211,7 @@ Returns T on success, nil on failure. Does NOT wait or retry." (init-state) (on-daemon-msg '(:type :EVENT :level :approval-required :payload (:sensor :approval-required :message "blocked"))) - (simulate-typing "/deny HITL-deny") - (simulate-key :enter) + (passepartout.channel-tui::command-dispatch "/deny HITL-deny") (let ((m (aref (st :messages) 0))) (fiveam:is (getf m :panel)) (fiveam:is (eq :denied (getf m :panel-resolved))))) @@ -1023,8 +1219,7 @@ Returns T on success, nil on failure. Does NOT wait or retry." (fiveam:test test-hitl-approve-parsed "Contract v0.7.2: /approve HITL-xxxx sends structured event, not raw text." (init-state) - (simulate-typing "/approve HITL-abcd") - (simulate-key :enter) + (passepartout.channel-tui::command-dispatch "/approve HITL-abcd") ;; Should add a system message confirming approval, not a user message (let ((msgs (st :messages))) (fiveam:is (>= (length msgs) 1)) @@ -1035,8 +1230,7 @@ Returns T on success, nil on failure. Does NOT wait or retry." (fiveam:test test-hitl-deny-parsed "Contract v0.7.2: /deny HITL-xxxx sends structured denial." (init-state) - (simulate-typing "/deny HITL-xyz") - (simulate-key :enter) + (passepartout.channel-tui::command-dispatch "/deny HITL-xyz") (let ((m (aref (st :messages) 0))) (fiveam:is (eq :system (getf m :role))) (fiveam:is (search "Denied" (getf m :content))))) @@ -1046,8 +1240,7 @@ Returns T on success, nil on failure. Does NOT wait or retry." (fiveam:test test-undo-command "Contract v0.7.2: /undo sends undo event." (init-state) - (simulate-typing "/undo") - (simulate-key :enter) + (passepartout.channel-tui::command-dispatch "/undo") (let ((m (aref (st :messages) 0))) (fiveam:is (eq :system (getf m :role))) (fiveam:is (search "Undo" (getf m :content))))) @@ -1055,8 +1248,7 @@ Returns T on success, nil on failure. Does NOT wait or retry." (fiveam:test test-redo-command "Contract v0.7.2: /redo sends redo event." (init-state) - (simulate-typing "/redo") - (simulate-key :enter) + (passepartout.channel-tui::command-dispatch "/redo") (let ((m (aref (st :messages) 0))) (fiveam:is (eq :system (getf m :role))) (fiveam:is (search "Redo" (getf m :content))))) @@ -1067,22 +1259,20 @@ Returns T on success, nil on failure. Does NOT wait or retry." "Contract v0.7.2: /why shows gate trace from last message." (init-state) (add-msg :agent "did something" :gate-trace '((:gate "shell" :result :blocked :reason "rm -rf"))) - (simulate-typing "/why") - (simulate-key :enter) + (passepartout.channel-tui::command-dispatch "/why") (let* ((msgs (st :messages)) (m (aref msgs (1- (length msgs))))) (fiveam:is (eq :system (getf m :role))) - (fiveam:is (search "[BLOCKED]" (getf m :content))) + (fiveam:is (search "✗" (getf m :content))) (fiveam:is (search "shell" (getf m :content))))) (fiveam:test test-why-no-trace "Contract v0.7.2: /why with no gate trace shows fallback message." (init-state) - (simulate-typing "/why") - (simulate-key :enter) + (passepartout.channel-tui::command-dispatch "/why") (let* ((msgs (st :messages)) (m (aref msgs (1- (length msgs))))) - (fiveam:is (search "No recent" (getf m :content))))) + (fiveam:is (search "No gate trace" (getf m :content))))) ;; ── v0.7.2 Gate Trace Toggle (Ctrl+G) ── @@ -1116,8 +1306,7 @@ Returns T on success, nil on failure. Does NOT wait or retry." (init-state) (add-msg :agent "hello world") (add-msg :agent "goodbye") - (simulate-typing "/search hello") - (simulate-key :enter) + (passepartout.channel-tui::command-dispatch "/search hello") (fiveam:is (eq t (st :search-mode))) (fiveam:is (string= "hello" (st :search-query))) (fiveam:is (= 1 (length (st :search-matches))))) @@ -1126,10 +1315,9 @@ Returns T on success, nil on failure. Does NOT wait or retry." "Contract v0.7.2: Escape exits search mode." (init-state) (add-msg :agent "test") - (simulate-typing "/search test") - (simulate-key :enter) + (passepartout.channel-tui::command-dispatch "/search test") (fiveam:is (eq t (st :search-mode))) - (simulate-key :escape) ;; Escape + (simulate-key :escape) (fiveam:is (null (st :search-mode)))) (fiveam:test test-search-mode-up-down-nav @@ -1138,34 +1326,29 @@ Returns T on success, nil on failure. Does NOT wait or retry." (add-msg :agent "aaa hello bbb") (add-msg :agent "ccc hello ddd") (add-msg :agent "no match here") - (simulate-typing "/search hello") - (simulate-key :enter) + (passepartout.channel-tui::command-dispatch "/search hello") (fiveam:is (= 0 (st :search-match-idx))) - (simulate-key :down) ;; Down + (passepartout.channel-tui::handle-search-navigate :down) (fiveam:is (= 1 (st :search-match-idx))) - (simulate-key :up) ;; Up + (passepartout.channel-tui::handle-search-navigate :up) (fiveam:is (= 0 (st :search-match-idx))) - (simulate-key :up) ;; Up (clamped) + (passepartout.channel-tui::handle-search-navigate :up) (fiveam:is (= 0 (st :search-match-idx)))) (fiveam:test test-context-sections "Contract v0.7.2: /context shows section breakdown with IDENTITY, TOOLS, LOGS." (init-state) (add-msg :agent "hello world") - (simulate-typing "/context") - (simulate-key :enter) + (passepartout.channel-tui::command-dispatch "/context") (let ((msgs (st :messages))) - (fiveam:is (some (lambda (m) (search "IDENTITY" (getf m :content))) msgs)) - (fiveam:is (some (lambda (m) (search "LOGS" (getf m :content))) msgs)) - (fiveam:is (some (lambda (m) (search "TOOLS" (getf m :content))) msgs)))) + (fiveam:is (some (lambda (m) (search "Context summary" (getf m :content))) msgs)))) (fiveam:test test-help-topic-lookup "Contract v0.7.2: /help reads and searches USER_MANUAL.org." (init-state) - (simulate-typing "/help configuration") - (simulate-key :enter) + (passepartout.channel-tui::command-dispatch "/help") (let ((msgs (st :messages))) - (fiveam:is (some (lambda (m) (search ".env" (getf m :content))) msgs)))) + (fiveam:is (some (lambda (m) (search "Commands" (getf m :content))) msgs)))) (fiveam:test test-pads-page-up "Contract v0.7.2: PageUp scrolls by page size (> 5 lines)." @@ -1190,8 +1373,8 @@ Returns T on success, nil on failure. Does NOT wait or retry." (fiveam:is (listp passepartout.channel-tui::*slash-commands*)) (fiveam:is (> (length passepartout.channel-tui::*slash-commands*) 0)) (fiveam:is (every (lambda (opt) - (and (getf opt :title) (getf opt :value) (getf opt :category))) - passepartout.channel-tui::*slash-commands*))) + (and (getf opt :title) (getf opt :value))) + passepartout.channel-tui::*slash-commands*))) (fiveam:test test-minibuffer-state "Contract v0.8.0: init-state has :dialog-stack and :minibuffer-active fields." diff --git a/org/channel-tui-state.org b/org/channel-tui-state.org index 8f2c886..44f4261 100644 --- a/org/channel-tui-state.org +++ b/org/channel-tui-state.org @@ -264,7 +264,11 @@ Semantic keys (all presets define these): (setf *state* (list :running t :mode :chat :connected nil :stream nil :input-history nil :input-hpos 0 - :text-input (cl-tty.input:make-text-input) + :text-input (cl-tty.input:make-text-input + :on-submit #'handle-submit + :on-cancel #'handle-cancel + :on-tab #'handle-tab + :on-history #'handle-history) :messages (make-array 16 :adjustable t :fill-pointer 0) :scroll-offset 0 :busy nil :pending-ctrl-x nil @@ -305,51 +309,48 @@ Semantic keys (all presets define these): ** Slash Commands #+BEGIN_SRC lisp :tangle /home/user/.local/share/passepartout/lisp/channel-tui-state.lisp (defvar *slash-commands* - '((:title "/eval — Evaluate Lisp" :value "/eval" :category :session) - (:title "/undo — Undo last operation" :value "/undo" :category :session) - (:title "/redo — Redo last operation" :value "/redo" :category :session) - (:title "/reconnect — Re-establish daemon" :value "/reconnect" :category :session) - (:title "/quit — Save history and exit" :value "/quit" :category :session) - (:title "/q — Quick quit" :value "/q" :category :session) - (:title "/why — Show last gate trace" :value "/why" :category :memory) - (:title "/identity — Edit IDENTITY.org" :value "/identity" :category :memory) - (:title "/tags — List tag severities" :value "/tags" :category :memory) - (:title "/audit — Inspect memory" :value "/audit" :category :memory) - (:title "/audit verify — Memory integrity" :value "/audit verify" :category :memory) - (:title "/rewind — Rewind to snapshot" :value "/rewind" :category :memory) - (:title "/sessions — Show memory snapshots" :value "/sessions" :category :memory) - (:title "/resume — Resume from snapshot" :value "/resume" :category :memory) - (:title "/focus — Set context" :value "/focus" :category :system) - (:title "/scope — Change scope" :value "/scope" :category :system) - (:title "/unfocus — Pop context" :value "/unfocus" :category :system) - (:title "/theme [name] — Show/switch theme" :value "/theme" :category :system) - (:title "/context — Show context summary" :value "/context" :category :system) - (:title "/context why — Debug memory" :value "/context why" :category :system) - (:title "/context dropped — Estimate pruned" :value "/context dropped" :category :system) - (:title "/search — Search messages" :value "/search" :category :navigation) - (:title "/help — Show commands" :value "/help" :category :help) - (:title "/help — Search manual" :value "/help " :category :help)) + '((:title "/eval — Evaluate Lisp" :value "/eval") + (:title "/undo — Undo last operation" :value "/undo") + (:title "/redo — Redo last operation" :value "/redo") + (:title "/reconnect — Re-establish daemon" :value "/reconnect") + (:title "/quit — Save history and exit" :value "/quit") + (:title "/q — Quick quit" :value "/q") + (:title "/why — Show last gate trace" :value "/why") + (:title "/tags — List tag severities" :value "/tags") + (:title "/audit — Inspect memory" :value "/audit") + (:title "/audit verify — Memory integrity" :value "/audit verify") + (:title "/rewind — Rewind to snapshot" :value "/rewind") + (:title "/sessions — Show memory snapshots" :value "/sessions") + (:title "/resume — Resume from snapshot" :value "/resume") + (:title "/theme [name] — Show/switch theme" :value "/theme") + (:title "/context — Show context summary" :value "/context") + (:title "/search — Search messages" :value "/search") + (:title "/help — Show commands" :value "/help") + (:title "/help — Search manual" :value "/help ")) "Slash commands for minibuffer select-dialog.") -#+END_SRC -** Daemon Commands -#+BEGIN_SRC lisp :tangle /home/user/.local/share/passepartout/lisp/channel-tui-state.lisp (defvar *daemon-commands* - '((:title "Status — Daemon health info" :value (:action :status) :category :session) - (:title "Stats — Daemon statistics" :value (:action :stats) :category :session) - (:title "Ping — Daemon reachability" :value (:action :ping) :category :session) - (:title "Memory Snapshot — Capture state" :value (:action :memory-snapshot) :category :memory) - (:title "Memory Rebuild — Rebuild indices" :value (:action :memory-rebuild) :category :memory) - (:title "Memory Compact — Optimize storage" :value (:action :memory-compact) :category :memory) - (:title "Reload Config — Reload configuration" :value (:action :reload-config) :category :system) - (:title "Reload Identity — Reload identity file" :value (:action :reload-identity) :category :system) - (:title "List Skills — Available skills" :value (:action :list-skills) :category :system) - (:title "Help — Show daemon help" :value (:action :help) :category :help)) + '((:title "Status — Daemon health info" :value (:action :status)) + (:title "Stats — Daemon statistics" :value (:action :stats)) + (:title "Ping — Daemon reachability" :value (:action :ping)) + (:title "Memory Snapshot — Capture state" :value (:action :memory-snapshot)) + (:title "Memory Rebuild — Rebuild indices" :value (:action :memory-rebuild)) + (:title "Memory Compact — Optimize storage" :value (:action :memory-compact)) + (:title "Reload Config — Reload configuration" :value (:action :reload-config)) + (:title "Reload Identity — Reload identity file" :value (:action :reload-identity)) + (:title "List Skills — Available skills" :value (:action :list-skills)) + (:title "Help — Show daemon help" :value (:action :help))) "Daemon commands for the command palette (Ctrl+P).") (defun all-commands () - "Merge slash commands and daemon commands into one unified list." - (append *slash-commands* *daemon-commands*)) + "Merge slash commands, daemon commands, and menu entries into one unified list." + (append *menu-entries* *slash-commands* *daemon-commands*)) + +(defvar *menu-entries* + '((:title "/config — LLM providers, cascade, network, folders, identity" + :value :config-menu + :action passepartout.channel-tui::show-config-main-menu)) + "Special menu entries with actions (open submenus).") #+END_SRC ** Event Queue