diff --git a/org/channel-tui-main.org b/org/channel-tui-main.org index b3129c0..8709dbb 100644 --- a/org/channel-tui-main.org +++ b/org/channel-tui-main.org @@ -380,13 +380,13 @@ Event handlers + daemon I/O + main loop. (getf *tui-theme* :agent-fg) (getf *tui-theme* :system) (getf *tui-theme* :input-fg))) - (add-msg :system "Presets: /theme amber | gold | terracotta | sepia | nord-warm | monokai-warm | gruvbox-warm | light-amber")) + (add-msg :system "Presets: /theme amber | gold | terracotta | sepia | nord-warm | monokai-warm | gruvbox-warm | light-amber | catppuccin | tokyonight | dracula | gemini | mono")) ((and (>= (length text) 7) (string-equal (subseq text 0 7) "/theme ")) (let ((name (string-trim '(#\Space) (subseq text 7)))) (if (theme-switch name) (add-msg :system (format nil "Theme switched to ~a" name)) - (add-msg :system (format nil "Unknown theme '~a'. Try: amber gold terracotta sepia nord-warm monokai-warm gruvbox-warm light-amber" name))))) + (add-msg :system (format nil "Unknown theme '~a'. Try: amber gold terracotta sepia nord-warm monokai-warm gruvbox-warm light-amber catppuccin tokyonight dracula gemini mono" name))))) ;; /eval command ((and (>= (length text) 6) (string-equal (subseq text 0 6) "/eval ")) @@ -474,7 +474,7 @@ Event handlers + daemon I/O + main loop. ;; /theme subcommand ((and (>= (length text) 7) (string-equal (subseq text 0 7) "/theme ")) (let* ((partial (string-trim '(#\Space) (subseq text 7))) - (names '("amber" "gold" "terracotta" "sepia" "nord-warm" "monokai-warm" "gruvbox-warm" "light-amber")) + (names '("amber" "gold" "terracotta" "sepia" "nord-warm" "monokai-warm" "gruvbox-warm" "light-amber" "catppuccin" "tokyonight" "dracula" "gemini" "mono")) (match (if (string= partial "") (first names) (find partial names :test #'string-equal)))) (when match @@ -562,40 +562,30 @@ Event handlers + daemon I/O + main loop. (setf (st :dirty) (list nil nil t)) (when (and (char= chr #\/) (null (st :dialog-stack)) (= (length (st :input-buffer)) 1)) - (minibuffer-show-commands))))))) + (unified-menu-show "/"))))))) -;; v0.8.0 — minibuffer dialog for slash commands -(defun minibuffer-show-commands () +;; v0.9.0 — unified command minibuffer (replaces separate palette and slash menus) +(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) - (let ((cmd (getf opt :value))) - (pop (st :dialog-stack)) - (setf (st :minibuffer-active) nil) - (setf (st :input-buffer) (reverse (coerce cmd 'list))) - (setf (st :cursor-pos) 0) - (setf (st :dirty) (list nil nil t))))) - (sel (cl-tty.select:make-select :options *slash-commands* :on-select on-select)) - (dlg (make-instance 'cl-tty.dialog:dialog - :title "Commands" - :content sel))) - (push dlg (st :dialog-stack)) - (setf (st :minibuffer-active) t))) - -;; v0.8.0 — command palette for daemon commands (Ctrl+P) -(defun command-palette-show-commands () - (let* ((on-select (lambda (cmd) (pop (st :dialog-stack)) - (setf (st :command-palette-active) nil) - (let ((action (getf cmd :value))) - (send-daemon (list :type :event :payload action)) - (add-msg :system (format nil "Sent: ~a" action))) - (setf (st :dirty) (list t t nil)))) - (sel (cl-tty.select:make-select :options *daemon-commands* - :on-select on-select)) - (dlg (make-instance 'cl-tty.dialog:dialog - :title "Command Palette" - :content sel))) - (push dlg (st :dialog-stack)) - (setf (st :command-palette-active) t))) + (let ((val (getf opt :value))) + (cond ((stringp val) + ;; Slash command — fill input buffer + (setf (st :input-buffer) (reverse (coerce val 'list))) + (setf (st :cursor-pos) 0) + (setf (st :dirty) (list nil nil t))) + ((listp val) + ;; Daemon action — send immediately + (send-daemon (list :type :event :payload val)) + (add-msg :system (format nil "Sent: ~a" (getf opt :title))) + (setf (st :dirty) (list t t nil))))))) + (sel (cl-tty.select:make-select :options (all-commands) :on-select on-select))) + (when initial-filter + (setf (cl-tty.select:select-filter sel) initial-filter)) + (let ((dlg (make-instance 'cl-tty.dialog:dialog :title "Commands" :content sel))) + (push dlg (st :dialog-stack))))) ;; v0.7.2 — resolve-hitl-panel: marks panel as resolved after approve/deny (defun resolve-hitl-panel (decision) @@ -805,7 +795,7 @@ Event handlers + daemon I/O + main loop. (:ctrl+q (lambda (e) (declare (ignore e)) (setf (st :running) nil))) (:ctrl+p (lambda (e) (declare (ignore e)) - (command-palette-show-commands))) + (unified-menu-show))) (:ctrl+b (lambda (e) (declare (ignore e)) (setf (st :sidebar-visible) (not (st :sidebar-visible))) (setf (st :dirty) (list t t nil)))) @@ -955,7 +945,7 @@ Event handlers + daemon I/O + main loop. (ch (getf payload :ch))) (case ch (:CTRL-Q (setf (st :running) nil)) - (:CTRL-P (command-palette-show-commands)) + (:CTRL-P (unified-menu-show)) (:CTRL-B (setf (st :sidebar-visible) (not (st :sidebar-visible))) (setf (st :dirty) (list t t t))) (:CTRL-L (setf (st :dirty) (list t t t))) @@ -963,11 +953,9 @@ Event handlers + daemon I/O + main loop. (let* ((dlg (car (st :dialog-stack))) (sel (cl-tty.dialog:dialog-content dlg))) (cond - ((eql ch :escape) - (pop (st :dialog-stack)) - (setf (st :minibuffer-active) nil) - (setf (st :command-palette-active) nil) - (setf (st :dirty) (list t t nil))) + ((eql ch :escape) + (pop (st :dialog-stack)) + (setf (st :dirty) (list t t nil))) ((member ch '(:up :down)) (if (eql ch :up) (cl-tty.select:select-prev sel) @@ -1026,55 +1014,69 @@ Event handlers + daemon I/O + main loop. ;; Guard w and h before render (resize or other code may have set them to nil) (setq w (or (and (numberp w) (> w 0) w) 80) h (or (and (numberp h) (> h 0) h) 24)) - (when (or (first (st :dirty)) (second (st :dirty)) (third (st :dirty))) - (let* ((sidebar-w (if (and (st :sidebar-visible) (>= w 60)) - (or (st :sidebar-width) 30) 0)) - (chat-w (- w sidebar-w))) - (cl-tty.backend:backend-clear be) - (view-status be w h) - (view-chat be w h) - ;; Draw separator line above input - (cl-tty.backend:draw-text be 0 (- h 4) (make-string chat-w :initial-element #\─) - (theme-color :separator) nil) - (view-input be w h) - (when (and (st :sidebar-visible) (>= w 60)) - (view-sidebar be w h)) - (setf (st :dirty) (list nil nil nil)))) - (let ((ds (st :dialog-stack))) - (when ds - (let* ((dlg (car ds)) - (sel (cl-tty.dialog:dialog-content dlg)) - (filtered (cl-tty.select:select-filtered-options sel)) - (sel-idx (cl-tty.select:select-selected-index sel)) - (cnt (length filtered)) - (dw 60) (dh (min 20 (+ 4 cnt))) - (mx (floor (- w dw) 2)) - (my 3)) - (dotimes (row h) - (cl-tty.backend:draw-rect be 0 row w 1 :bg (theme-color :status-bg))) - (cl-tty.backend:draw-border be mx my dw dh :style :single - :title (cl-tty.dialog:dialog-title dlg) - :fg (theme-color :user-border)) - (let ((y-off 1)) - (dolist (item filtered) - (let* ((display-idx (first item)) - (option (third item)) - (title (getf option :title)) - (cat (getf option :category)) - (sel-p (eql display-idx sel-idx)) - (text (if cat (format nil " ~a" title) - (format nil " ~:[ ~;▸~] ~a" sel-p title)))) - (when (>= y-off (1- dh)) (return)) - (cl-tty.backend:draw-text be (1+ mx) (+ my y-off) text - (cond (cat (theme-color :dim)) - (sel-p (theme-color :accent)) - (t (theme-color :agent-fg))) - nil :bold sel-p) - (incf y-off))))))) - (sleep 0.1)) + (when (or (first (st :dirty)) (second (st :dirty)) (third (st :dirty))) + (let* ((sidebar-w (if (and (st :sidebar-visible) (>= w 60)) + (or (st :sidebar-width) 30) 0)) + (chat-w (- w sidebar-w))) + (cl-tty.backend:begin-sync be) + (cl-tty.backend:draw-rect be 0 0 w h :bg (theme-color :status-bg)) + (view-status be w h) + (view-chat be w h) + ;; Draw separator line above input + (cl-tty.backend:draw-text be 0 (- h 4) (make-string chat-w :initial-element #\─) + (theme-color :separator) nil) + (view-input be w h) + (when (and (st :sidebar-visible) (>= w 60)) + (view-sidebar be w h)) + (cl-tty.backend:end-sync be) + (setf (st :dirty) (list nil nil nil)))) + (let ((ds (st :dialog-stack))) + (when ds + (cl-tty.backend:begin-sync be) + (let* ((dlg (car ds)) + (sel (cl-tty.dialog:dialog-content dlg)) + (filtered (cl-tty.select:select-filtered-options sel)) + (sel-idx (cl-tty.select:select-selected-index sel)) + (cnt (length filtered)) + (filter (cl-tty.select:select-filter sel)) + ;; Bottom-anchored: filter at h-3, options above + (mh (min 15 (+ 1 cnt))) ;; +1 for title + (top (max 0 (- h 3 mh)))) + ;; Clear the full minibuffer area (top to h-2) + (dotimes (r (min (- h 2 top) h)) + (cl-tty.backend:draw-rect be 0 (+ top r) w 1 + :bg (theme-color :status-bg))) + ;; Title line + (cl-tty.backend:draw-text be 0 top + (cl-tty.dialog:dialog-title dlg) + (theme-color :accent) nil) + ;; Options + (let ((y-off 1)) + (dolist (item filtered) + (let* ((display-idx (first item)) + (option (third item)) + (title (getf option :title)) + (cat (getf option :category)) + (sel-p (eql display-idx sel-idx)) + (text (if cat (format nil " ~a" title) + (format nil " ~:[ ~;▸~] ~a" sel-p title))) + (row (+ top y-off))) + (when (>= row (1- h)) (return)) + (cl-tty.backend:draw-text be 1 row text + (cond (cat (theme-color :dim)) + (sel-p (theme-color :accent)) + (t (theme-color :agent-fg))) + nil :bold sel-p) + (incf y-off)))) + ;; Filter prompt at h-3 + (cl-tty.backend:draw-text be 0 (- h 3) + (format nil "> ~a" filter) + (theme-color :input-prompt) nil)) + (cl-tty.backend:end-sync be)) + (sleep 0.1)) (uiop:terminate-process *cat-proc*)) (add-msg :system (format nil "* cat ~a ended *" (uiop:process-info-pid *cat-proc*)))) - (progn (disconnect-daemon))) + (progn (disconnect-daemon)))) #+END_SRC * Test Suite diff --git a/org/channel-tui-state.org b/org/channel-tui-state.org index 4e64d89..99ae3e3 100644 --- a/org/channel-tui-state.org +++ b/org/channel-tui-state.org @@ -125,8 +125,60 @@ All state mutation flows through event handlers in the controller. :dot-connected "#2E8B57" :dot-disconnected "#CC3300" :error "#CC3300" :tool-running "#CC6600" :tool-done "#2E8B57" :tool-error "#CC3300" - :separator "#C8B898" :accent "#CC6600" :dim "#8B7355")) - "8 warm theme presets.") + :separator "#C8B898" :accent "#CC6600" :dim "#8B7355") + :catppuccin (:user-fg "#FAB387" :user-bg "#1E1E2E" :user-border "#F5A97F" + :agent-header "#CBA6F7" :agent-fg "#CDD6F4" + :system "#94E2D5" + :input-prompt "#FAB387" :input-fg "#CDD6F4" + :hint "#6C7086" + :status-bg "#181825" :status-fg "#A6ADC8" + :dot-connected "#A6E3A1" :dot-disconnected "#F38BA8" + :error "#F38BA8" + :tool-running "#FAB387" :tool-done "#A6E3A1" :tool-error "#F38BA8" + :separator "#313244" :accent "#FAB387" :dim "#585B70") + :tokyonight (:user-fg "#FF9E64" :user-bg "#1A1B26" :user-border "#F59E4C" + :agent-header "#7AA2F7" :agent-fg "#A9B1D6" + :system "#73DACA" + :input-prompt "#FF9E64" :input-fg "#A9B1D6" + :hint "#565F89" + :status-bg "#16161E" :status-fg "#9AA5CE" + :dot-connected "#9ECE6A" :dot-disconnected "#DB4B4B" + :error "#DB4B4B" + :tool-running "#FF9E64" :tool-done "#9ECE6A" :tool-error "#DB4B4B" + :separator "#292E42" :accent "#FF9E64" :dim "#444B6A") + :dracula (:user-fg "#FF9580" :user-bg "#1E1F2B" :user-border "#FF6E6E" + :agent-header "#BD93F9" :agent-fg "#F8F8F2" + :system "#8BE9FD" + :input-prompt "#FF9580" :input-fg "#F8F8F2" + :hint "#6272A4" + :status-bg "#191A24" :status-fg "#E0E0E0" + :dot-connected "#50FA7B" :dot-disconnected "#FF5555" + :error "#FF5555" + :tool-running "#FF9580" :tool-done "#50FA7B" :tool-error "#FF5555" + :separator "#34354A" :accent "#FF9580" :dim "#5A5B7A") + :gemini (:user-fg "#87AFFF" :user-bg "#000000" :user-border "#5F5F5F" + :agent-header "#D7AFFF" :agent-fg "#FFFFFF" + :system "#87D7D7" + :input-prompt "#87AFFF" :input-fg "#FFFFFF" + :hint "#AFAFAF" + :status-bg "#1A1A1A" :status-fg "#AFAFAF" + :dot-connected "#D7FFD7" :dot-disconnected "#FF87AF" + :error "#FF87AF" + :tool-running "#87AFFF" :tool-done "#D7FFD7" :tool-error "#FF87AF" + :separator "#3A3A3A" :accent "#87AFFF" :dim "#5F5F5F") + :mono (:user-fg "#E0E0E0" :user-bg "#1A1A1A" :user-border "#808080" + :agent-header "#C0C0C0" :agent-fg "#D0D0D0" + :system "#A0A0A0" + :input-prompt "#FFFFFF" :input-fg "#D0D0D0" + :hint "#606060" + :status-bg "#141414" :status-fg "#B0B0B0" + :dot-connected "#A0A0A0" :dot-disconnected "#808080" + :error "#808080" + :tool-running "#E0E0E0" :tool-done "#A0A0A0" :tool-error "#808080" + :separator "#303030" :accent "#FFFFFF" :dim "#505050")) + "13 warm theme presets (amber, gold, terracotta, sepia, nord-warm, +monokai-warm, gruvbox-warm, light-amber, catppuccin, tokyonight, dracula, +gemini, mono).") (defvar *tui-theme-current-name* :amber "Name of the currently active theme preset.") @@ -294,6 +346,10 @@ All state mutation flows through event handlers in the controller. (:title "List Skills — Available skills" :value (:action :list-skills) :category :system) (:title "Help — Show daemon help" :value (:action :help) :category :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*)) #+END_SRC ** Event Queue