fix: TUI flicker, bottom-anchored minibuffer, 13 color presets
Issue 1 — flickering during typing/updating: - Wrap every frame render in DECICM sync (begin-sync/end-sync) so the terminal defers rendering until the entire frame is written - Replace backend-clear (ESC[2J full clear) with draw-rect background fill — eliminates visible blank frame between redraws - These two changes together eliminate all visible tearing/flicker Issue 2 — bottom-anchored minibuffer (Emacs-style): - Replace centered overlay dialog with bottom-anchored minibuffer that expands upward from the input line - Unified command menu: Ctrl+P and / both open the same menu with all 35+ commands (slash + daemon), dispatch by value type - Filter prompt at h-3 (same position as normal input), options listed above, grows up to 15 lines - No full-screen dim backdrop — just clear the minibuffer area Issue 3 — color schemes: - Add 5 new presets: catppuccin, tokyonight, dracula, gemini, mono - Total: 13 presets (up from 8) - Update /theme completion list and help text Also fixed: pre-existing unbalanced paren in tui-main (missing close)
This commit is contained in:
@@ -380,13 +380,13 @@ Event handlers + daemon I/O + main loop.
|
|||||||
(getf *tui-theme* :agent-fg)
|
(getf *tui-theme* :agent-fg)
|
||||||
(getf *tui-theme* :system)
|
(getf *tui-theme* :system)
|
||||||
(getf *tui-theme* :input-fg)))
|
(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)
|
((and (>= (length text) 7)
|
||||||
(string-equal (subseq text 0 7) "/theme "))
|
(string-equal (subseq text 0 7) "/theme "))
|
||||||
(let ((name (string-trim '(#\Space) (subseq text 7))))
|
(let ((name (string-trim '(#\Space) (subseq text 7))))
|
||||||
(if (theme-switch name)
|
(if (theme-switch name)
|
||||||
(add-msg :system (format nil "Theme switched to ~a" 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
|
;; /eval command
|
||||||
((and (>= (length text) 6)
|
((and (>= (length text) 6)
|
||||||
(string-equal (subseq text 0 6) "/eval "))
|
(string-equal (subseq text 0 6) "/eval "))
|
||||||
@@ -474,7 +474,7 @@ Event handlers + daemon I/O + main loop.
|
|||||||
;; /theme subcommand
|
;; /theme subcommand
|
||||||
((and (>= (length text) 7) (string-equal (subseq text 0 7) "/theme "))
|
((and (>= (length text) 7) (string-equal (subseq text 0 7) "/theme "))
|
||||||
(let* ((partial (string-trim '(#\Space) (subseq text 7)))
|
(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)
|
(match (if (string= partial "") (first names)
|
||||||
(find partial names :test #'string-equal))))
|
(find partial names :test #'string-equal))))
|
||||||
(when match
|
(when match
|
||||||
@@ -562,40 +562,30 @@ Event handlers + daemon I/O + main loop.
|
|||||||
(setf (st :dirty) (list nil nil t))
|
(setf (st :dirty) (list nil nil t))
|
||||||
(when (and (char= chr #\/) (null (st :dialog-stack))
|
(when (and (char= chr #\/) (null (st :dialog-stack))
|
||||||
(= (length (st :input-buffer)) 1))
|
(= (length (st :input-buffer)) 1))
|
||||||
(minibuffer-show-commands)))))))
|
(unified-menu-show "/")))))))
|
||||||
|
|
||||||
;; v0.8.0 — minibuffer dialog for slash commands
|
;; v0.9.0 — unified command minibuffer (replaces separate palette and slash menus)
|
||||||
(defun minibuffer-show-commands ()
|
(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* ((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))
|
(pop (st :dialog-stack))
|
||||||
(setf (st :command-palette-active) nil)
|
(let ((val (getf opt :value)))
|
||||||
(let ((action (getf cmd :value)))
|
(cond ((stringp val)
|
||||||
(send-daemon (list :type :event :payload action))
|
;; Slash command — fill input buffer
|
||||||
(add-msg :system (format nil "Sent: ~a" action)))
|
(setf (st :input-buffer) (reverse (coerce val 'list)))
|
||||||
(setf (st :dirty) (list t t nil))))
|
(setf (st :cursor-pos) 0)
|
||||||
(sel (cl-tty.select:make-select :options *daemon-commands*
|
(setf (st :dirty) (list nil nil t)))
|
||||||
:on-select on-select))
|
((listp val)
|
||||||
(dlg (make-instance 'cl-tty.dialog:dialog
|
;; Daemon action — send immediately
|
||||||
:title "Command Palette"
|
(send-daemon (list :type :event :payload val))
|
||||||
:content sel)))
|
(add-msg :system (format nil "Sent: ~a" (getf opt :title)))
|
||||||
(push dlg (st :dialog-stack))
|
(setf (st :dirty) (list t t nil)))))))
|
||||||
(setf (st :command-palette-active) t)))
|
(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
|
;; v0.7.2 — resolve-hitl-panel: marks panel as resolved after approve/deny
|
||||||
(defun resolve-hitl-panel (decision)
|
(defun resolve-hitl-panel (decision)
|
||||||
@@ -805,7 +795,7 @@ Event handlers + daemon I/O + main loop.
|
|||||||
(:ctrl+q (lambda (e) (declare (ignore e))
|
(:ctrl+q (lambda (e) (declare (ignore e))
|
||||||
(setf (st :running) nil)))
|
(setf (st :running) nil)))
|
||||||
(:ctrl+p (lambda (e) (declare (ignore e))
|
(:ctrl+p (lambda (e) (declare (ignore e))
|
||||||
(command-palette-show-commands)))
|
(unified-menu-show)))
|
||||||
(:ctrl+b (lambda (e) (declare (ignore e))
|
(:ctrl+b (lambda (e) (declare (ignore e))
|
||||||
(setf (st :sidebar-visible) (not (st :sidebar-visible)))
|
(setf (st :sidebar-visible) (not (st :sidebar-visible)))
|
||||||
(setf (st :dirty) (list t t nil))))
|
(setf (st :dirty) (list t t nil))))
|
||||||
@@ -955,7 +945,7 @@ Event handlers + daemon I/O + main loop.
|
|||||||
(ch (getf payload :ch)))
|
(ch (getf payload :ch)))
|
||||||
(case ch
|
(case ch
|
||||||
(:CTRL-Q (setf (st :running) nil))
|
(: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)))
|
(:CTRL-B (setf (st :sidebar-visible) (not (st :sidebar-visible)))
|
||||||
(setf (st :dirty) (list t t t)))
|
(setf (st :dirty) (list t t t)))
|
||||||
(:CTRL-L (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)))
|
(let* ((dlg (car (st :dialog-stack)))
|
||||||
(sel (cl-tty.dialog:dialog-content dlg)))
|
(sel (cl-tty.dialog:dialog-content dlg)))
|
||||||
(cond
|
(cond
|
||||||
((eql ch :escape)
|
((eql ch :escape)
|
||||||
(pop (st :dialog-stack))
|
(pop (st :dialog-stack))
|
||||||
(setf (st :minibuffer-active) nil)
|
(setf (st :dirty) (list t t nil)))
|
||||||
(setf (st :command-palette-active) nil)
|
|
||||||
(setf (st :dirty) (list t t nil)))
|
|
||||||
((member ch '(:up :down))
|
((member ch '(:up :down))
|
||||||
(if (eql ch :up)
|
(if (eql ch :up)
|
||||||
(cl-tty.select:select-prev sel)
|
(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)
|
;; 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)
|
(setq w (or (and (numberp w) (> w 0) w) 80)
|
||||||
h (or (and (numberp h) (> h 0) h) 24))
|
h (or (and (numberp h) (> h 0) h) 24))
|
||||||
(when (or (first (st :dirty)) (second (st :dirty)) (third (st :dirty)))
|
(when (or (first (st :dirty)) (second (st :dirty)) (third (st :dirty)))
|
||||||
(let* ((sidebar-w (if (and (st :sidebar-visible) (>= w 60))
|
(let* ((sidebar-w (if (and (st :sidebar-visible) (>= w 60))
|
||||||
(or (st :sidebar-width) 30) 0))
|
(or (st :sidebar-width) 30) 0))
|
||||||
(chat-w (- w sidebar-w)))
|
(chat-w (- w sidebar-w)))
|
||||||
(cl-tty.backend:backend-clear be)
|
(cl-tty.backend:begin-sync be)
|
||||||
(view-status be w h)
|
(cl-tty.backend:draw-rect be 0 0 w h :bg (theme-color :status-bg))
|
||||||
(view-chat be w h)
|
(view-status be w h)
|
||||||
;; Draw separator line above input
|
(view-chat be w h)
|
||||||
(cl-tty.backend:draw-text be 0 (- h 4) (make-string chat-w :initial-element #\─)
|
;; Draw separator line above input
|
||||||
(theme-color :separator) nil)
|
(cl-tty.backend:draw-text be 0 (- h 4) (make-string chat-w :initial-element #\─)
|
||||||
(view-input be w h)
|
(theme-color :separator) nil)
|
||||||
(when (and (st :sidebar-visible) (>= w 60))
|
(view-input be w h)
|
||||||
(view-sidebar be w h))
|
(when (and (st :sidebar-visible) (>= w 60))
|
||||||
(setf (st :dirty) (list nil nil nil))))
|
(view-sidebar be w h))
|
||||||
(let ((ds (st :dialog-stack)))
|
(cl-tty.backend:end-sync be)
|
||||||
(when ds
|
(setf (st :dirty) (list nil nil nil))))
|
||||||
(let* ((dlg (car ds))
|
(let ((ds (st :dialog-stack)))
|
||||||
(sel (cl-tty.dialog:dialog-content dlg))
|
(when ds
|
||||||
(filtered (cl-tty.select:select-filtered-options sel))
|
(cl-tty.backend:begin-sync be)
|
||||||
(sel-idx (cl-tty.select:select-selected-index sel))
|
(let* ((dlg (car ds))
|
||||||
(cnt (length filtered))
|
(sel (cl-tty.dialog:dialog-content dlg))
|
||||||
(dw 60) (dh (min 20 (+ 4 cnt)))
|
(filtered (cl-tty.select:select-filtered-options sel))
|
||||||
(mx (floor (- w dw) 2))
|
(sel-idx (cl-tty.select:select-selected-index sel))
|
||||||
(my 3))
|
(cnt (length filtered))
|
||||||
(dotimes (row h)
|
(filter (cl-tty.select:select-filter sel))
|
||||||
(cl-tty.backend:draw-rect be 0 row w 1 :bg (theme-color :status-bg)))
|
;; Bottom-anchored: filter at h-3, options above
|
||||||
(cl-tty.backend:draw-border be mx my dw dh :style :single
|
(mh (min 15 (+ 1 cnt))) ;; +1 for title
|
||||||
:title (cl-tty.dialog:dialog-title dlg)
|
(top (max 0 (- h 3 mh))))
|
||||||
:fg (theme-color :user-border))
|
;; Clear the full minibuffer area (top to h-2)
|
||||||
(let ((y-off 1))
|
(dotimes (r (min (- h 2 top) h))
|
||||||
(dolist (item filtered)
|
(cl-tty.backend:draw-rect be 0 (+ top r) w 1
|
||||||
(let* ((display-idx (first item))
|
:bg (theme-color :status-bg)))
|
||||||
(option (third item))
|
;; Title line
|
||||||
(title (getf option :title))
|
(cl-tty.backend:draw-text be 0 top
|
||||||
(cat (getf option :category))
|
(cl-tty.dialog:dialog-title dlg)
|
||||||
(sel-p (eql display-idx sel-idx))
|
(theme-color :accent) nil)
|
||||||
(text (if cat (format nil " ~a" title)
|
;; Options
|
||||||
(format nil " ~:[ ~;▸~] ~a" sel-p title))))
|
(let ((y-off 1))
|
||||||
(when (>= y-off (1- dh)) (return))
|
(dolist (item filtered)
|
||||||
(cl-tty.backend:draw-text be (1+ mx) (+ my y-off) text
|
(let* ((display-idx (first item))
|
||||||
(cond (cat (theme-color :dim))
|
(option (third item))
|
||||||
(sel-p (theme-color :accent))
|
(title (getf option :title))
|
||||||
(t (theme-color :agent-fg)))
|
(cat (getf option :category))
|
||||||
nil :bold sel-p)
|
(sel-p (eql display-idx sel-idx))
|
||||||
(incf y-off)))))))
|
(text (if cat (format nil " ~a" title)
|
||||||
(sleep 0.1))
|
(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*))
|
(uiop:terminate-process *cat-proc*))
|
||||||
(add-msg :system (format nil "* cat ~a ended *" (uiop:process-info-pid *cat-proc*))))
|
(add-msg :system (format nil "* cat ~a ended *" (uiop:process-info-pid *cat-proc*))))
|
||||||
(progn (disconnect-daemon)))
|
(progn (disconnect-daemon))))
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
|
||||||
* Test Suite
|
* Test Suite
|
||||||
|
|||||||
@@ -125,8 +125,60 @@ All state mutation flows through event handlers in the controller.
|
|||||||
:dot-connected "#2E8B57" :dot-disconnected "#CC3300"
|
:dot-connected "#2E8B57" :dot-disconnected "#CC3300"
|
||||||
:error "#CC3300"
|
:error "#CC3300"
|
||||||
:tool-running "#CC6600" :tool-done "#2E8B57" :tool-error "#CC3300"
|
:tool-running "#CC6600" :tool-done "#2E8B57" :tool-error "#CC3300"
|
||||||
:separator "#C8B898" :accent "#CC6600" :dim "#8B7355"))
|
:separator "#C8B898" :accent "#CC6600" :dim "#8B7355")
|
||||||
"8 warm theme presets.")
|
: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
|
(defvar *tui-theme-current-name* :amber
|
||||||
"Name of the currently active theme preset.")
|
"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 "List Skills — Available skills" :value (:action :list-skills) :category :system)
|
||||||
(:title "Help — Show daemon help" :value (:action :help) :category :help))
|
(:title "Help — Show daemon help" :value (:action :help) :category :help))
|
||||||
"Daemon commands for the command palette (Ctrl+P).")
|
"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
|
#+END_SRC
|
||||||
|
|
||||||
** Event Queue
|
** Event Queue
|
||||||
|
|||||||
Reference in New Issue
Block a user