v0.10.0: TUI visual overhaul — dark-neutral theme, left-border messages, sidebar auto-show, cl-tty style-reset
- Theme: near-black (#0a0a0a) backgrounds, dark-grey panels (#141414), warm amber (#fab283) accent only. New keys: :bg, :bg-panel, :bg-element, :text-muted. All 13 presets updated. - Messages: No background fills (sit on global black). User messages get amber left border (│). Agent response has no border (invisible). Streaming agent messages get grey left border. Gate traces and tool calls use grey ╎ prefix. No label lines, no time separators. - Sidebar: :sidebar-mode with :auto/:visible/:hidden. Auto-shows at >120 cols (opencode-style). Width 42 with version + connection dot footer. - Input: 2-char hpad on each side. Grey panel (2 rows: separator + prompt). Hint right-aligned at bottom on black. - Status bar: empty (clean black line). - cl-tty backend: draw-text, draw-rect, draw-link, draw-border now use \e[22;23;24;25;27m (style-only reset) instead of \e[0m (full reset), preserving foreground/background across draw calls. - Fix: all sidebar text draws pass explicit bg-panel background. - Fix: hint at h-1 passes explicit (theme-color :bg). - Fix: sidebar bottom row uses draw-text (no \n) to prevent scroll at h-1.
This commit is contained in:
@@ -789,7 +789,11 @@ Returns T on success, nil on failure. Does NOT wait or retry."
|
|||||||
(:ctrl+p (lambda (e) (declare (ignore e))
|
(:ctrl+p (lambda (e) (declare (ignore e))
|
||||||
(unified-menu-show)))
|
(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-mode)
|
||||||
|
(case (st :sidebar-mode)
|
||||||
|
(:auto :visible)
|
||||||
|
(:visible :hidden)
|
||||||
|
(:hidden :auto)))
|
||||||
(setf (st :dirty) (list t t nil))))
|
(setf (st :dirty) (list t t nil))))
|
||||||
(:ppage (lambda (e) (declare (ignore e))
|
(:ppage (lambda (e) (declare (ignore e))
|
||||||
(let ((max-offset (max 0 (- (length (st :messages)) 1))))
|
(let ((max-offset (max 0 (- (length (st :messages)) 1))))
|
||||||
@@ -929,7 +933,11 @@ Returns T on success, nil on failure. Does NOT wait or retry."
|
|||||||
(case ch
|
(case ch
|
||||||
(:CTRL-Q (setf (st :running) nil))
|
(:CTRL-Q (setf (st :running) nil))
|
||||||
(:CTRL-P (unified-menu-show))
|
(:CTRL-P (unified-menu-show))
|
||||||
(:CTRL-B (setf (st :sidebar-visible) (not (st :sidebar-visible)))
|
(:CTRL-B (setf (st :sidebar-mode)
|
||||||
|
(case (st :sidebar-mode)
|
||||||
|
(:auto :visible)
|
||||||
|
(:visible :hidden)
|
||||||
|
(:hidden :auto)))
|
||||||
(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)))
|
||||||
(t (if (st :dialog-stack)
|
(t (if (st :dialog-stack)
|
||||||
@@ -1010,70 +1018,62 @@ Returns T on success, nil on failure. Does NOT wait or retry."
|
|||||||
h (or (and (numberp h) (> h 0) h) 24))
|
h (or (and (numberp h) (> h 0) h) 24))
|
||||||
(when (and (or (first (st :dirty)) (second (st :dirty)) (third (st :dirty)))
|
(when (and (or (first (st :dirty)) (second (st :dirty)) (third (st :dirty)))
|
||||||
(null (st :dialog-stack)))
|
(null (st :dialog-stack)))
|
||||||
(let* ((sidebar-w (if (and (st :sidebar-visible) (>= w 60))
|
(cl-tty.backend:begin-sync be)
|
||||||
(or (st :sidebar-width) 30) 0))
|
(cl-tty.backend:draw-rect be 0 0 w h :bg (theme-color :bg))
|
||||||
(chat-w (- w sidebar-w)))
|
(view-status be w h)
|
||||||
(cl-tty.backend:begin-sync be)
|
(view-chat be w h)
|
||||||
(cl-tty.backend:draw-rect be 0 0 w h :bg (theme-color :status-bg))
|
(view-input be w h)
|
||||||
(view-status be w h)
|
(when (sidebar-visible-p w)
|
||||||
(view-chat be w h)
|
(view-sidebar be w h))
|
||||||
;; Draw separator line above input
|
(cl-tty.backend:end-sync be)
|
||||||
(cl-tty.backend:draw-text be 0 (- h 4) (make-string chat-w :initial-element #\─)
|
(setf (st :dirty) (list nil nil nil)))
|
||||||
(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)))
|
(let ((ds (st :dialog-stack)))
|
||||||
(when ds
|
(when ds
|
||||||
(cl-tty.backend:begin-sync be)
|
(cl-tty.backend:begin-sync be)
|
||||||
(let* ((sidebar-w (if (and (st :sidebar-visible) (>= w 60))
|
(let* ((chat-w (- w (if (sidebar-visible-p w) (or (st :sidebar-width) 42) 0)))
|
||||||
(or (st :sidebar-width) 30) 0))
|
(dlg (car ds))
|
||||||
(chat-w (- w sidebar-w))
|
(sel (cl-tty.dialog:dialog-content dlg))
|
||||||
(dlg (car ds))
|
(filtered (cl-tty.select:select-filtered-options sel))
|
||||||
(sel (cl-tty.dialog:dialog-content dlg))
|
(sel-idx (cl-tty.select:select-selected-index sel))
|
||||||
(filtered (cl-tty.select:select-filtered-options sel))
|
(cnt (length filtered))
|
||||||
(sel-idx (cl-tty.select:select-selected-index sel))
|
(filter (cl-tty.select:select-filter sel))
|
||||||
(cnt (length filtered))
|
(mh (min 15 (+ 1 cnt)))
|
||||||
(filter (cl-tty.select:select-filter sel))
|
(top (max 0 (- h 4 mh)))
|
||||||
(mh (min 15 (+ 1 cnt))) ;; +1 for title
|
(bg-p (theme-color :bg-panel))
|
||||||
(top (max 0 (- h 4 mh))))
|
(sep-c (theme-color :separator)))
|
||||||
;; Clear the minibuffer area
|
;; Fill minibuffer area with panel bg
|
||||||
(dotimes (r (min (- h 3 top) h))
|
(dotimes (r (min (- h 3 top) h))
|
||||||
(cl-tty.backend:draw-rect be 0 (+ top r) chat-w 1
|
(cl-tty.backend:draw-rect be 0 (+ top r) chat-w 1 :bg bg-p))
|
||||||
:bg (theme-color :status-bg)))
|
;; Top separator
|
||||||
;; Top border line with title
|
(cl-tty.backend:draw-text be 0 top
|
||||||
(cl-tty.backend:draw-text be 0 top
|
(make-string chat-w :initial-element #\─)
|
||||||
(make-string chat-w :initial-element #\─)
|
sep-c nil)
|
||||||
(theme-color :separator) nil)
|
(cl-tty.backend:draw-text be 1 top
|
||||||
(cl-tty.backend:draw-text be 1 top
|
(cl-tty.dialog:dialog-title dlg)
|
||||||
(cl-tty.dialog:dialog-title dlg)
|
(theme-color :accent) nil)
|
||||||
(theme-color :accent) nil)
|
;; Options
|
||||||
;; Options
|
(let ((y-off 1))
|
||||||
(let ((y-off 1))
|
(dolist (item filtered)
|
||||||
(dolist (item filtered)
|
(let* ((display-idx (first item))
|
||||||
(let* ((display-idx (first item))
|
(option (third item))
|
||||||
(option (third item))
|
(title (getf option :title))
|
||||||
(title (getf option :title))
|
(cat (getf option :category))
|
||||||
(cat (getf option :category))
|
(sel-p (eql display-idx sel-idx))
|
||||||
(sel-p (eql display-idx sel-idx))
|
(text (if cat (format nil " ~a" title)
|
||||||
(text (if cat (format nil " ~a" title)
|
(format nil " ~a~a" (if sel-p "▸ " " ") title)))
|
||||||
(format nil " ~:[ ~;▸~] ~a" sel-p title)))
|
(row (+ top y-off)))
|
||||||
(row (+ top y-off)))
|
(when (>= row (1- h)) (return))
|
||||||
(when (>= row (1- h)) (return))
|
(cl-tty.backend:draw-text be 1 row text
|
||||||
(cl-tty.backend:draw-text be 1 row text
|
(cond (cat (theme-color :text-muted))
|
||||||
(cond (cat (theme-color :dim))
|
(sel-p (theme-color :accent))
|
||||||
(sel-p (theme-color :accent))
|
(t (theme-color :agent-fg)))
|
||||||
(t (theme-color :agent-fg)))
|
nil :bold sel-p)
|
||||||
nil :bold sel-p)
|
(incf y-off))))
|
||||||
(incf y-off))))
|
;; Filter prompt
|
||||||
;; Filter prompt at h-3
|
(cl-tty.backend:draw-rect be 0 (- h 3) chat-w 1 :bg bg-p)
|
||||||
(cl-tty.backend:draw-rect be 0 (- h 3) chat-w 1
|
(cl-tty.backend:draw-text be 0 (- h 3)
|
||||||
:bg (theme-color :status-bg))
|
(format nil "> ~a" (or filter ""))
|
||||||
(cl-tty.backend:draw-text be 0 (- h 3)
|
(theme-color :input-prompt) nil))
|
||||||
(format nil "> ~a" (or filter ""))
|
|
||||||
(theme-color :input-prompt) nil))
|
|
||||||
(cl-tty.backend:end-sync be))
|
(cl-tty.backend:end-sync be))
|
||||||
(sleep 0.1)))
|
(sleep 0.1)))
|
||||||
(progn (disconnect-daemon)))))
|
(progn (disconnect-daemon)))))
|
||||||
@@ -1254,11 +1254,11 @@ Returns T on success, nil on failure. Does NOT wait or retry."
|
|||||||
(fiveam:is (eq nil (st :busy))))
|
(fiveam:is (eq nil (st :busy))))
|
||||||
|
|
||||||
(fiveam:test test-theme
|
(fiveam:test test-theme
|
||||||
"Contract view: *tui-theme* provides warm color mappings."
|
"Contract view: *tui-theme* provides color mappings."
|
||||||
(fiveam:is (string= "#FFB347" (getf *tui-theme* :user-fg)))
|
(fiveam:is (string= "#fab283" (getf *tui-theme* :user-fg)))
|
||||||
(fiveam:is (string= "#E8D5B7" (getf *tui-theme* :agent-fg)))
|
(fiveam:is (string= "#e8e8e8" (getf *tui-theme* :agent-fg)))
|
||||||
(fiveam:is (string= "#C8A87C" (getf *tui-theme* :system)))
|
(fiveam:is (string= "#808080" (getf *tui-theme* :system)))
|
||||||
(fiveam:is (string= "#E8D5B7" (getf *tui-theme* :input-fg)))
|
(fiveam:is (string= "#e8e8e8" (getf *tui-theme* :input-fg)))
|
||||||
(fiveam:is (string= "#FFFFFF" (theme-color :unknown-role))))
|
(fiveam:is (string= "#FFFFFF" (theme-color :unknown-role))))
|
||||||
|
|
||||||
(fiveam:test test-on-key-ctrl-u-clears
|
(fiveam:test test-on-key-ctrl-u-clears
|
||||||
|
|||||||
@@ -33,152 +33,197 @@ All state mutation flows through event handlers in the controller.
|
|||||||
(defvar *event-lock* (bt:make-lock "tui-event-lock"))
|
(defvar *event-lock* (bt:make-lock "tui-event-lock"))
|
||||||
|
|
||||||
(defvar *tui-theme*
|
(defvar *tui-theme*
|
||||||
'(:user-fg "#FFB347" :user-bg "#3A2A1A" :user-border "#CC8800"
|
'(:user-fg "#fab283" :user-bg "#1e1e1e" :user-border "#fab283"
|
||||||
:agent-header "#D4956A" :agent-fg "#E8D5B7"
|
:agent-header "#d4956a" :agent-fg "#e8e8e8"
|
||||||
:system "#C8A87C"
|
:system "#808080"
|
||||||
:input-prompt "#FF8C42" :input-fg "#E8D5B7"
|
:input-prompt "#fab283" :input-fg "#e8e8e8"
|
||||||
:hint "#A08060"
|
:hint "#606060"
|
||||||
:status-bg "#2A1F1A" :status-fg "#D4A574"
|
:status-bg "#141414" :status-fg "#e8e8e8"
|
||||||
:dot-connected "#7CCC6C" :dot-disconnected "#E2584A"
|
:bg "#0a0a0a" :bg-panel "#141414" :bg-element "#1e1e1e"
|
||||||
:error "#E2584A"
|
:text-muted "#808080"
|
||||||
:tool-running "#FF8C42" :tool-done "#7CCC6C" :tool-error "#E2584A"
|
:dot-connected "#7fd88f" :dot-disconnected "#e06c75"
|
||||||
:separator "#4A3A2A" :accent "#FFB347" :dim "#8B7355")
|
:error "#e06c75"
|
||||||
"Warm amber/gold color theme. 20 semantic keys → hex color strings.")
|
:tool-running "#fab283" :tool-done "#7fd88f" :tool-error "#e06c75"
|
||||||
|
:separator "#3c3c3c" :accent "#fab283" :dim "#606060")
|
||||||
|
"Dark-neutral color theme with warm amber accent. Backgrounds are dark grays,
|
||||||
|
semantic text colors for context. Keys: :bg (deepest), :bg-panel, :bg-element,
|
||||||
|
:text-muted, :user-fg/bg/border, :agent-header/fg, :system, :input-prompt/fg,
|
||||||
|
:hint, :status-bg/fg, :dot-connected/disconnected, :error, :tool-*,
|
||||||
|
:separator, :accent, :dim.")
|
||||||
|
|
||||||
(defvar *tui-theme-presets*
|
(defvar *tui-theme-presets*
|
||||||
'(:amber (:user-fg "#FFB347" :user-bg "#3A2A1A" :user-border "#CC8800"
|
'(:amber
|
||||||
:agent-header "#D4956A" :agent-fg "#E8D5B7"
|
(:user-fg "#fab283" :user-bg "#1e1e1e" :user-border "#fab283"
|
||||||
:system "#C8A87C"
|
:agent-header "#d4956a" :agent-fg "#e8e8e8"
|
||||||
:input-prompt "#FF8C42" :input-fg "#E8D5B7"
|
:system "#808080"
|
||||||
:hint "#A08060"
|
:input-prompt "#fab283" :input-fg "#e8e8e8"
|
||||||
:status-bg "#2A1F1A" :status-fg "#D4A574"
|
:hint "#606060"
|
||||||
:dot-connected "#7CCC6C" :dot-disconnected "#E2584A"
|
:status-bg "#141414" :status-fg "#e8e8e8"
|
||||||
:error "#E2584A"
|
:bg "#0a0a0a" :bg-panel "#141414" :bg-element "#1e1e1e"
|
||||||
:tool-running "#FF8C42" :tool-done "#7CCC6C" :tool-error "#E2584A"
|
:text-muted "#808080"
|
||||||
:separator "#4A3A2A" :accent "#FFB347" :dim "#8B7355")
|
:dot-connected "#7fd88f" :dot-disconnected "#e06c75"
|
||||||
:gold (:user-fg "#FFD700" :user-bg "#3A3020" :user-border "#DAA520"
|
:error "#e06c75"
|
||||||
:agent-header "#D4A574" :agent-fg "#F0E6D0"
|
:tool-running "#fab283" :tool-done "#7fd88f" :tool-error "#e06c75"
|
||||||
:system "#C8A87C"
|
:separator "#3c3c3c" :accent "#fab283" :dim "#606060")
|
||||||
:input-prompt "#FFA500" :input-fg "#F0E6D0"
|
:gold
|
||||||
:hint "#A08060"
|
(:user-fg "#ffd700" :user-bg "#1e1e1e" :user-border "#ffd700"
|
||||||
:status-bg "#2A1F1A" :status-fg "#DAA520"
|
:agent-header "#d4a574" :agent-fg "#e8e8e8"
|
||||||
:dot-connected "#7CCC6C" :dot-disconnected "#E2584A"
|
:system "#808080"
|
||||||
:error "#E2584A"
|
:input-prompt "#ffd700" :input-fg "#e8e8e8"
|
||||||
:tool-running "#FFA500" :tool-done "#7CCC6C" :tool-error "#E2584A"
|
:hint "#606060"
|
||||||
:separator "#4A3A2A" :accent "#FFD700" :dim "#8B7355")
|
:status-bg "#141414" :status-fg "#ffd700"
|
||||||
:terracotta (:user-fg "#E87A5D" :user-bg "#2D1C15" :user-border "#C0684A"
|
:bg "#0a0a0a" :bg-panel "#141414" :bg-element "#1e1e1e"
|
||||||
:agent-header "#D4956A" :agent-fg "#E0C8B0"
|
:text-muted "#808080"
|
||||||
:system "#A08060"
|
:dot-connected "#7fd88f" :dot-disconnected "#e06c75"
|
||||||
:input-prompt "#E87A5D" :input-fg "#E0C8B0"
|
:error "#e06c75"
|
||||||
:hint "#8B6F5E"
|
:tool-running "#ffd700" :tool-done "#7fd88f" :tool-error "#e06c75"
|
||||||
:status-bg "#1F1410" :status-fg "#D4956A"
|
:separator "#3c3c3c" :accent "#ffd700" :dim "#606060")
|
||||||
:dot-connected "#6CB85C" :dot-disconnected "#D94A3A"
|
:terracotta
|
||||||
:error "#D94A3A"
|
(:user-fg "#e87a5d" :user-bg "#1e1e1e" :user-border "#e87a5d"
|
||||||
:tool-running "#E87A5D" :tool-done "#6CB85C" :tool-error "#D94A3A"
|
:agent-header "#d4956a" :agent-fg "#e0c8b0"
|
||||||
:separator "#3A2820" :accent "#E87A5D" :dim "#7A6050")
|
:system "#808080"
|
||||||
:sepia (:user-fg "#C4A882" :user-bg "#2A2218" :user-border "#A08860"
|
:input-prompt "#e87a5d" :input-fg "#e0c8b0"
|
||||||
:agent-header "#B89870" :agent-fg "#D4C4A8"
|
:hint "#606060"
|
||||||
:system "#9A8A6A"
|
:status-bg "#141414" :status-fg "#d4956a"
|
||||||
:input-prompt "#C4A882" :input-fg "#D4C4A8"
|
:bg "#0a0a0a" :bg-panel "#141414" :bg-element "#1e1e1e"
|
||||||
:hint "#8A7A5E"
|
:text-muted "#808080"
|
||||||
:status-bg "#1E1810" :status-fg "#B89870"
|
:dot-connected "#6cb85c" :dot-disconnected "#d94a3a"
|
||||||
:dot-connected "#7AAC5C" :dot-disconnected "#C84A3A"
|
:error "#d94a3a"
|
||||||
:error "#C84A3A"
|
:tool-running "#e87a5d" :tool-done "#6cb85c" :tool-error "#d94a3a"
|
||||||
:tool-running "#C4A882" :tool-done "#7AAC5C" :tool-error "#C84A3A"
|
:separator "#3c3c3c" :accent "#e87a5d" :dim "#606060")
|
||||||
:separator "#3A3020" :accent "#C4A882" :dim "#7A6A50")
|
:sepia
|
||||||
:nord-warm (:user-fg "#D4A574" :user-bg "#2A2220" :user-border "#B8885A"
|
(:user-fg "#c4a882" :user-bg "#1e1e1e" :user-border "#c4a882"
|
||||||
:agent-header "#C49870" :agent-fg "#E0D0C0"
|
:agent-header "#b89870" :agent-fg "#d4c4a8"
|
||||||
:system "#A89080"
|
:system "#808080"
|
||||||
:input-prompt "#D08770" :input-fg "#E0D0C0"
|
:input-prompt "#c4a882" :input-fg "#d4c4a8"
|
||||||
:hint "#908070"
|
:hint "#606060"
|
||||||
:status-bg "#1E1A18" :status-fg "#C8A080"
|
:status-bg "#141414" :status-fg "#b89870"
|
||||||
:dot-connected "#7CB860" :dot-disconnected "#D06050"
|
:bg "#0a0a0a" :bg-panel "#141414" :bg-element "#1e1e1e"
|
||||||
:error "#D06050"
|
:text-muted "#808080"
|
||||||
:tool-running "#D08770" :tool-done "#7CB860" :tool-error "#D06050"
|
:dot-connected "#7aac5c" :dot-disconnected "#c84a3a"
|
||||||
:separator "#3A3030" :accent "#D4A574" :dim "#807060")
|
:error "#c84a3a"
|
||||||
:monokai-warm (:user-fg "#E6B87D" :user-bg "#1E1A16" :user-border "#CC9966"
|
:tool-running "#c4a882" :tool-done "#7aac5c" :tool-error "#c84a3a"
|
||||||
:agent-header "#D4A06A" :agent-fg "#D8C8B0"
|
:separator "#3c3c3c" :accent "#c4a882" :dim "#606060")
|
||||||
:system "#A89070"
|
:nord-warm
|
||||||
:input-prompt "#E6B87D" :input-fg "#D8C8B0"
|
(:user-fg "#d4a574" :user-bg "#1e1e1e" :user-border "#d4a574"
|
||||||
:hint "#8A7A5E"
|
:agent-header "#c49870" :agent-fg "#e0d0c0"
|
||||||
:status-bg "#141210" :status-fg "#CC9966"
|
:system "#808080"
|
||||||
:dot-connected "#7AB85C" :dot-disconnected "#D94A3A"
|
:input-prompt "#d08770" :input-fg "#e0d0c0"
|
||||||
:error "#D94A3A"
|
:hint "#606060"
|
||||||
:tool-running "#E6B87D" :tool-done "#7AB85C" :tool-error "#D94A3A"
|
:status-bg "#141414" :status-fg "#c8a080"
|
||||||
:separator "#2E2820" :accent "#E6B87D" :dim "#7A6A50")
|
:bg "#0a0a0a" :bg-panel "#141414" :bg-element "#1e1e1e"
|
||||||
:gruvbox-warm (:user-fg "#D8A657" :user-bg "#1D1A16" :user-border "#B8884A"
|
:text-muted "#808080"
|
||||||
:agent-header "#C8A070" :agent-fg "#E0C8A8"
|
:dot-connected "#7cb860" :dot-disconnected "#d06050"
|
||||||
:system "#A89070"
|
:error "#d06050"
|
||||||
:input-prompt "#D8A657" :input-fg "#E0C8A8"
|
:tool-running "#d08770" :tool-done "#7cb860" :tool-error "#d06050"
|
||||||
:hint "#8A7A5E"
|
:separator "#3c3c3c" :accent "#d4a574" :dim "#606060")
|
||||||
:status-bg "#141210" :status-fg "#C8A070"
|
:monokai-warm
|
||||||
:dot-connected "#7AB85C" :dot-disconnected "#D94A3A"
|
(:user-fg "#e6b87d" :user-bg "#1e1e1e" :user-border "#e6b87d"
|
||||||
:error "#D94A3A"
|
:agent-header "#d4a06a" :agent-fg "#d8c8b0"
|
||||||
:tool-running "#D8A657" :tool-done "#7AB85C" :tool-error "#D94A3A"
|
:system "#808080"
|
||||||
:separator "#2E2820" :accent "#D8A657" :dim "#7A6A50")
|
:input-prompt "#e6b87d" :input-fg "#d8c8b0"
|
||||||
:light-amber (:user-fg "#CC6600" :user-bg "#FFF5E6" :user-border "#CC8800"
|
:hint "#606060"
|
||||||
:agent-header "#8B6914" :agent-fg "#3A2A1A"
|
:status-bg "#141414" :status-fg "#cc9966"
|
||||||
:system "#6B5B3E"
|
:bg "#0a0a0a" :bg-panel "#141414" :bg-element "#1e1e1e"
|
||||||
:input-prompt "#CC6600" :input-fg "#3A2A1A"
|
:text-muted "#808080"
|
||||||
:hint "#8B7355"
|
:dot-connected "#7ab85c" :dot-disconnected "#d94a3a"
|
||||||
:status-bg "#E8D5B7" :status-fg "#3A2A1A"
|
:error "#d94a3a"
|
||||||
:dot-connected "#2E8B57" :dot-disconnected "#CC3300"
|
:tool-running "#e6b87d" :tool-done "#7ab85c" :tool-error "#d94a3a"
|
||||||
:error "#CC3300"
|
:separator "#3c3c3c" :accent "#e6b87d" :dim "#606060")
|
||||||
:tool-running "#CC6600" :tool-done "#2E8B57" :tool-error "#CC3300"
|
:gruvbox-warm
|
||||||
:separator "#C8B898" :accent "#CC6600" :dim "#8B7355")
|
(:user-fg "#d8a657" :user-bg "#1e1e1e" :user-border "#d8a657"
|
||||||
:catppuccin (:user-fg "#FAB387" :user-bg "#1E1E2E" :user-border "#F5A97F"
|
:agent-header "#c8a070" :agent-fg "#e0c8a8"
|
||||||
:agent-header "#CBA6F7" :agent-fg "#CDD6F4"
|
:system "#808080"
|
||||||
:system "#94E2D5"
|
:input-prompt "#d8a657" :input-fg "#e0c8a8"
|
||||||
:input-prompt "#FAB387" :input-fg "#CDD6F4"
|
:hint "#606060"
|
||||||
:hint "#6C7086"
|
:status-bg "#141414" :status-fg "#c8a070"
|
||||||
:status-bg "#181825" :status-fg "#A6ADC8"
|
:bg "#0a0a0a" :bg-panel "#141414" :bg-element "#1e1e1e"
|
||||||
:dot-connected "#A6E3A1" :dot-disconnected "#F38BA8"
|
:text-muted "#808080"
|
||||||
:error "#F38BA8"
|
:dot-connected "#7ab85c" :dot-disconnected "#d94a3a"
|
||||||
:tool-running "#FAB387" :tool-done "#A6E3A1" :tool-error "#F38BA8"
|
:error "#d94a3a"
|
||||||
:separator "#313244" :accent "#FAB387" :dim "#585B70")
|
:tool-running "#d8a657" :tool-done "#7ab85c" :tool-error "#d94a3a"
|
||||||
:tokyonight (:user-fg "#FF9E64" :user-bg "#1A1B26" :user-border "#F59E4C"
|
:separator "#3c3c3c" :accent "#d8a657" :dim "#606060")
|
||||||
:agent-header "#7AA2F7" :agent-fg "#A9B1D6"
|
:light-amber
|
||||||
:system "#73DACA"
|
(:user-fg "#cc6600" :user-bg "#f5f5f5" :user-border "#cc6600"
|
||||||
:input-prompt "#FF9E64" :input-fg "#A9B1D6"
|
:agent-header "#8b6914" :agent-fg "#3a2a1a"
|
||||||
:hint "#565F89"
|
:system "#808080"
|
||||||
:status-bg "#16161E" :status-fg "#9AA5CE"
|
:input-prompt "#cc6600" :input-fg "#3a2a1a"
|
||||||
:dot-connected "#9ECE6A" :dot-disconnected "#DB4B4B"
|
:hint "#a0a0a0"
|
||||||
:error "#DB4B4B"
|
:status-bg "#ebebeb" :status-fg "#3a2a1a"
|
||||||
:tool-running "#FF9E64" :tool-done "#9ECE6A" :tool-error "#DB4B4B"
|
:bg "#ffffff" :bg-panel "#f5f5f5" :bg-element "#ebebeb"
|
||||||
:separator "#292E42" :accent "#FF9E64" :dim "#444B6A")
|
:text-muted "#808080"
|
||||||
:dracula (:user-fg "#FF9580" :user-bg "#1E1F2B" :user-border "#FF6E6E"
|
:dot-connected "#2e8b57" :dot-disconnected "#cc3300"
|
||||||
:agent-header "#BD93F9" :agent-fg "#F8F8F2"
|
:error "#cc3300"
|
||||||
:system "#8BE9FD"
|
:tool-running "#cc6600" :tool-done "#2e8b57" :tool-error "#cc3300"
|
||||||
:input-prompt "#FF9580" :input-fg "#F8F8F2"
|
:separator "#d4d4d4" :accent "#cc6600" :dim "#a0a0a0")
|
||||||
:hint "#6272A4"
|
:catppuccin
|
||||||
:status-bg "#191A24" :status-fg "#E0E0E0"
|
(:user-fg "#fab387" :user-bg "#1e1e2e" :user-border "#fab387"
|
||||||
:dot-connected "#50FA7B" :dot-disconnected "#FF5555"
|
:agent-header "#cba6f7" :agent-fg "#cdd6f4"
|
||||||
:error "#FF5555"
|
:system "#808080"
|
||||||
:tool-running "#FF9580" :tool-done "#50FA7B" :tool-error "#FF5555"
|
:input-prompt "#fab387" :input-fg "#cdd6f4"
|
||||||
:separator "#34354A" :accent "#FF9580" :dim "#5A5B7A")
|
:hint "#6c7086"
|
||||||
:gemini (:user-fg "#87AFFF" :user-bg "#000000" :user-border "#5F5F5F"
|
:status-bg "#181825" :status-fg "#a6adc8"
|
||||||
:agent-header "#D7AFFF" :agent-fg "#FFFFFF"
|
:bg "#11111b" :bg-panel "#181825" :bg-element "#1e1e2e"
|
||||||
:system "#87D7D7"
|
:text-muted "#6c7086"
|
||||||
:input-prompt "#87AFFF" :input-fg "#FFFFFF"
|
:dot-connected "#a6e3a1" :dot-disconnected "#f38ba8"
|
||||||
:hint "#AFAFAF"
|
:error "#f38ba8"
|
||||||
:status-bg "#1A1A1A" :status-fg "#AFAFAF"
|
:tool-running "#fab387" :tool-done "#a6e3a1" :tool-error "#f38ba8"
|
||||||
:dot-connected "#D7FFD7" :dot-disconnected "#FF87AF"
|
:separator "#313244" :accent "#fab387" :dim "#585b70")
|
||||||
:error "#FF87AF"
|
:tokyonight
|
||||||
:tool-running "#87AFFF" :tool-done "#D7FFD7" :tool-error "#FF87AF"
|
(:user-fg "#ff9e64" :user-bg "#1a1b26" :user-border "#ff9e64"
|
||||||
:separator "#3A3A3A" :accent "#87AFFF" :dim "#5F5F5F")
|
:agent-header "#7aa2f7" :agent-fg "#a9b1d6"
|
||||||
:mono (:user-fg "#E0E0E0" :user-bg "#1A1A1A" :user-border "#808080"
|
:system "#808080"
|
||||||
:agent-header "#C0C0C0" :agent-fg "#D0D0D0"
|
:input-prompt "#ff9e64" :input-fg "#a9b1d6"
|
||||||
:system "#A0A0A0"
|
:hint "#565f89"
|
||||||
:input-prompt "#FFFFFF" :input-fg "#D0D0D0"
|
:status-bg "#16161e" :status-fg "#9aa5ce"
|
||||||
:hint "#606060"
|
:bg "#0f0f18" :bg-panel "#16161e" :bg-element "#1a1b26"
|
||||||
:status-bg "#141414" :status-fg "#B0B0B0"
|
:text-muted "#565f89"
|
||||||
:dot-connected "#A0A0A0" :dot-disconnected "#808080"
|
:dot-connected "#9ece6a" :dot-disconnected "#db4b4b"
|
||||||
:error "#808080"
|
:error "#db4b4b"
|
||||||
:tool-running "#E0E0E0" :tool-done "#A0A0A0" :tool-error "#808080"
|
:tool-running "#ff9e64" :tool-done "#9ece6a" :tool-error "#db4b4b"
|
||||||
:separator "#303030" :accent "#FFFFFF" :dim "#505050"))
|
:separator "#292e42" :accent "#ff9e64" :dim "#444b6a")
|
||||||
"13 warm theme presets (amber, gold, terracotta, sepia, nord-warm,
|
:dracula
|
||||||
|
(:user-fg "#ff9580" :user-bg "#1e1f2b" :user-border "#ff9580"
|
||||||
|
:agent-header "#bd93f9" :agent-fg "#f8f8f2"
|
||||||
|
:system "#808080"
|
||||||
|
:input-prompt "#ff9580" :input-fg "#f8f8f2"
|
||||||
|
:hint "#6272a4"
|
||||||
|
:status-bg "#191a24" :status-fg "#e0e0e0"
|
||||||
|
:bg "#0f101a" :bg-panel "#191a24" :bg-element "#1e1f2b"
|
||||||
|
:text-muted "#6272a4"
|
||||||
|
: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 "#1a1a1a" :user-border "#87afff"
|
||||||
|
:agent-header "#d7afff" :agent-fg "#ffffff"
|
||||||
|
:system "#808080"
|
||||||
|
:input-prompt "#87afff" :input-fg "#ffffff"
|
||||||
|
:hint "#606060"
|
||||||
|
:status-bg "#141414" :status-fg "#afafaf"
|
||||||
|
:bg "#000000" :bg-panel "#141414" :bg-element "#1a1a1a"
|
||||||
|
:text-muted "#808080"
|
||||||
|
: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 "#808080"
|
||||||
|
:input-prompt "#ffffff" :input-fg "#d0d0d0"
|
||||||
|
:hint "#606060"
|
||||||
|
:status-bg "#141414" :status-fg "#b0b0b0"
|
||||||
|
:bg "#0a0a0a" :bg-panel "#141414" :bg-element "#1a1a1a"
|
||||||
|
:text-muted "#808080"
|
||||||
|
:dot-connected "#a0a0a0" :dot-disconnected "#808080"
|
||||||
|
:error "#808080"
|
||||||
|
:tool-running "#e0e0e0" :tool-done "#a0a0a0" :tool-error "#808080"
|
||||||
|
:separator "#303030" :accent "#ffffff" :dim "#505050"))
|
||||||
|
"13 theme presets (amber, gold, terracotta, sepia, nord-warm,
|
||||||
monokai-warm, gruvbox-warm, light-amber, catppuccin, tokyonight, dracula,
|
monokai-warm, gruvbox-warm, light-amber, catppuccin, tokyonight, dracula,
|
||||||
gemini, mono).")
|
gemini, mono). Now with dark-neutral backgrounds and new :bg/:bg-panel/:bg-element/:text-muted slots.")
|
||||||
|
|
||||||
(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.")
|
||||||
@@ -239,8 +284,8 @@ gemini, mono).")
|
|||||||
:collapsed-gates nil ; v0.7.2
|
:collapsed-gates nil ; v0.7.2
|
||||||
:search-mode nil :search-query "" ; v0.7.2
|
:search-mode nil :search-query "" ; v0.7.2
|
||||||
:search-matches nil :search-match-idx 0
|
:search-matches nil :search-match-idx 0
|
||||||
:sidebar-visible nil ; v0.8.0
|
:sidebar-mode :auto ; v0.8.0: :auto/:visible/:hidden
|
||||||
:sidebar-width 30 ; v0.8.0
|
:sidebar-width 42 ; v0.8.0
|
||||||
:expand-tool-calls nil ; v0.8.0
|
:expand-tool-calls nil ; v0.8.0
|
||||||
:mcp-count 0 ; v0.8.0
|
:mcp-count 0 ; v0.8.0
|
||||||
:kill-ring nil ; v0.9.0
|
:kill-ring nil ; v0.9.0
|
||||||
|
|||||||
@@ -45,6 +45,13 @@ that the TUI actuator attaches to the response plist before transmission.
|
|||||||
#+BEGIN_SRC lisp :tangle /home/user/.local/share/passepartout/lisp/channel-tui-view.lisp
|
#+BEGIN_SRC lisp :tangle /home/user/.local/share/passepartout/lisp/channel-tui-view.lisp
|
||||||
(in-package :passepartout.channel-tui)
|
(in-package :passepartout.channel-tui)
|
||||||
|
|
||||||
|
(defun sidebar-visible-p (w)
|
||||||
|
"Compute whether sidebar should be shown given terminal width W
|
||||||
|
and current sidebar mode (:auto/:visible/:hidden)."
|
||||||
|
(let ((mode (st :sidebar-mode)))
|
||||||
|
(or (eq mode :visible)
|
||||||
|
(and (eq mode :auto) (> w 120)))))
|
||||||
|
|
||||||
(defun word-wrap (text width)
|
(defun word-wrap (text width)
|
||||||
"Wrap TEXT to at most WIDTH columns. Splits on word boundaries.
|
"Wrap TEXT to at most WIDTH columns. Splits on word boundaries.
|
||||||
Returns a list of strings, one per line."
|
Returns a list of strings, one per line."
|
||||||
@@ -59,24 +66,10 @@ Returns a list of strings, one per line."
|
|||||||
(nreverse lines)))
|
(nreverse lines)))
|
||||||
|
|
||||||
(defun view-status (fb w h)
|
(defun view-status (fb w h)
|
||||||
(let* ((w (or (and (numberp w) (> w 0) w) 80))
|
(declare (ignore fb w h))
|
||||||
(h (or (and (numberp h) (> h 0) h) 24))
|
;; Status bar is now a clean black line — blends with global :bg.
|
||||||
(sidebar-w (if (and (st :sidebar-visible) (>= w 60)) (or (st :sidebar-width) 30) 0))
|
;; No clock, no dot, no text. Everything clean.
|
||||||
(chat-w (- w sidebar-w))
|
)
|
||||||
(bg (theme-color :status-bg))
|
|
||||||
(fg (theme-color :status-fg))
|
|
||||||
(ver (st :daemon-version))
|
|
||||||
(ver-str (if ver (format nil " v~a" ver) ""))
|
|
||||||
(left (format nil " ~a ~a~a msgs:~d Rules:~a"
|
|
||||||
(if (st :connected) "●" "○")
|
|
||||||
(or (st :foveal-id) "passepartout")
|
|
||||||
ver-str
|
|
||||||
(length (st :messages))
|
|
||||||
(or (st :rule-count) 0)))
|
|
||||||
(right (format nil "$~,2f ~a" (or (st :session-cost) 0.0) (now))))
|
|
||||||
(cl-tty.backend:draw-rect fb 0 (- h 1) chat-w 1 :bg bg)
|
|
||||||
(cl-tty.backend:draw-text fb 1 (- h 1) left fg nil)
|
|
||||||
(cl-tty.backend:draw-text fb (- chat-w (length right) 2) (- h 1) right fg nil)))
|
|
||||||
|
|
||||||
|
|
||||||
;; v0.7.2: search-highlight — wrap matching text in **bold** for markdown
|
;; v0.7.2: search-highlight — wrap matching text in **bold** for markdown
|
||||||
@@ -99,68 +92,64 @@ Returns a list of strings, one per line."
|
|||||||
(defun view-chat (fb w h)
|
(defun view-chat (fb w h)
|
||||||
(let* ((w (or (and (numberp w) (> w 0) w) 80))
|
(let* ((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))
|
||||||
(sidebar-w (if (and (st :sidebar-visible) (>= w 60))
|
(hpad 2)
|
||||||
(or (st :sidebar-width) 30) 0))
|
(sidebar-w (if (sidebar-visible-p w) (or (st :sidebar-width) 42) 0))
|
||||||
(chat-w (- w sidebar-w))
|
(chat-w (- w sidebar-w))
|
||||||
(msgs (st :messages)) (total (length msgs))
|
(msgs (st :messages)) (total (length msgs))
|
||||||
(max-lines (- h 4)) (is-search (st :search-mode)) (y 0))
|
(max-lines (- h 4)) (is-search (st :search-mode))
|
||||||
|
(bordered-w (- chat-w (* 2 hpad) 2))
|
||||||
|
(unbordered-w (- chat-w (* 2 hpad)))
|
||||||
|
(y 0))
|
||||||
(when is-search
|
(when is-search
|
||||||
(let* ((matches (st :search-matches)) (idx (st :search-match-idx))
|
(let* ((matches (st :search-matches)) (idx (st :search-match-idx))
|
||||||
(query (st :search-query))
|
(query (st :search-query))
|
||||||
(hdr (format nil "Search: ~d matches for '~a' (~d/~d) — Esc to exit"
|
(hdr (format nil "Search: ~d matches for '~a' (~d/~d) — Esc to exit"
|
||||||
(length matches) query (1+ idx) (length matches))))
|
(length matches) query (1+ idx) (length matches))))
|
||||||
(cl-tty.backend:draw-text fb 1 y hdr (theme-color :accent) nil)
|
(cl-tty.backend:draw-text fb hpad y hdr (theme-color :accent) nil)
|
||||||
(incf y) (decf max-lines)))
|
(incf y) (decf max-lines)))
|
||||||
(let ((msg-lines (make-array total)) (msg-heights (make-array total)))
|
(let ((msg-lines (make-array total)) (msg-heights (make-array total)))
|
||||||
(dotimes (i total)
|
(dotimes (i total)
|
||||||
(let* ((msg (aref msgs i)) (role (getf msg :role))
|
(let* ((msg (aref msgs i)) (role (getf msg :role))
|
||||||
(content (getf msg :content)) (time (or (getf msg :time) ""))
|
(content (getf msg :content))
|
||||||
(cs (if is-search (search-highlight content (st :search-query)) content))
|
(cs (if is-search (search-highlight content (st :search-query)) content))
|
||||||
(pairs nil))
|
(pairs nil)
|
||||||
|
(dim-bg (theme-color :dim))
|
||||||
|
(user-bdr (theme-color :user-border))
|
||||||
|
(user-fg (theme-color :user-fg))
|
||||||
|
(agent-fg (theme-color :agent-fg))
|
||||||
|
(system-fg (theme-color :system)))
|
||||||
(case role
|
(case role
|
||||||
(:user
|
(:user
|
||||||
(let* ((top (format nil "┌─ you ~a ─" time))
|
(dolist (l (cl-tty.box:word-wrap cs bordered-w))
|
||||||
(top-str (format nil "~a~a┐" top
|
(push (list "│" user-bdr l user-fg) pairs)))
|
||||||
(make-string (max 0 (- chat-w (length top) 1)) :initial-element #\─)))
|
|
||||||
(body (cl-tty.box:word-wrap cs (- chat-w 4)))
|
|
||||||
(pad (- chat-w 3))
|
|
||||||
(bot (format nil "└~a┘" (make-string (max 0 pad) :initial-element #\─)))
|
|
||||||
(bdr (theme-color :user-border)))
|
|
||||||
(push (list top-str bdr) pairs)
|
|
||||||
(dolist (l body)
|
|
||||||
(push (list (format nil "│ ~a~a│" l
|
|
||||||
(make-string (max 0 (- pad (length l))) :initial-element #\Space))
|
|
||||||
(theme-color :user-fg) (theme-color :user-bg)) pairs))
|
|
||||||
(push (list bot bdr) pairs)))
|
|
||||||
(:agent
|
(:agent
|
||||||
(let* ((hdr (format nil "── passepartout ~a " time))
|
(let* ((streaming (getf msg :streaming))
|
||||||
(hdr-str (format nil "~a~a" hdr
|
(bdr-color (if streaming dim-bg nil))
|
||||||
(make-string (max 0 (- chat-w (length hdr))) :initial-element #\─)))
|
(bdr-str (if streaming "│" ""))
|
||||||
|
(wrap-w (if streaming bordered-w unbordered-w))
|
||||||
(nodes (cl-tty.markdown:parse-blocks cs))
|
(nodes (cl-tty.markdown:parse-blocks cs))
|
||||||
(raw-body (or (and nodes (cl-tty.markdown:render-md nodes)) (list "")))
|
(raw-body (or (and nodes (cl-tty.markdown:render-md nodes)) (list "")))
|
||||||
(body (mapcan (lambda (l) (cl-tty.box:word-wrap l (- chat-w 2))) raw-body)))
|
(body (mapcan (lambda (l) (cl-tty.box:word-wrap l wrap-w)) raw-body)))
|
||||||
(push (list hdr-str (theme-color :agent-header)) pairs)
|
(dolist (l body)
|
||||||
(dolist (l body) (push (list l (theme-color :agent-fg)) pairs))))
|
(push (list bdr-str bdr-color l agent-fg) pairs))))
|
||||||
(t (dolist (l (cl-tty.box:word-wrap cs (- chat-w 2)))
|
(t (dolist (l (cl-tty.box:word-wrap cs unbordered-w))
|
||||||
(push (list l (theme-color :system)) pairs))))
|
(push (list "" nil l system-fg) pairs))))
|
||||||
|
;; Gate trace
|
||||||
(let ((gt (getf msg :gate-trace)))
|
(let ((gt (getf msg :gate-trace)))
|
||||||
(when (and gt (eq role :agent))
|
(when (and gt (eq role :agent))
|
||||||
(if (member i (st :collapsed-gates))
|
(if (member i (st :collapsed-gates))
|
||||||
(push (list (format nil "╎ Gate trace: ~a gates" (length gt))
|
(push (list "╎" dim-bg (format nil "Gate trace: ~a gates" (length gt)) dim-bg) pairs)
|
||||||
(theme-color :dim)) pairs)
|
|
||||||
(dolist (entry (passepartout::gate-trace-lines gt))
|
(dolist (entry (passepartout::gate-trace-lines gt))
|
||||||
(push (list (concatenate 'string "╎ " (car entry))
|
(let ((ec (theme-color (getf (cdr entry) :fgcolor))))
|
||||||
(theme-color (getf (cdr entry) :fgcolor))) pairs)))))
|
(dolist (l (cl-tty.box:word-wrap (car entry) bordered-w))
|
||||||
|
(push (list "╎" dim-bg l ec) pairs)))))))
|
||||||
|
;; Tool calls
|
||||||
(let ((tc (getf msg :tool-calls)))
|
(let ((tc (getf msg :tool-calls)))
|
||||||
(when tc
|
(when tc
|
||||||
(if (member i (st :collapsed-tools))
|
(if (member i (st :collapsed-tools))
|
||||||
(let* ((n (or (getf (first tc) :name) "tool"))
|
(let* ((n (or (getf (first tc) :name) "tool"))
|
||||||
(d (or (getf (first tc) :duration) 0.0))
|
(d (or (getf (first tc) :duration) 0.0)))
|
||||||
(extra (reduce #'+ tc :key
|
(push (list "╎" dim-bg (format nil "~a … ~,1fs" n d) (theme-color :tool-done)) pairs))
|
||||||
(lambda (c) (length (cl-tty.box:word-wrap
|
|
||||||
(or (getf c :output) "") (- chat-w 6)))))))
|
|
||||||
(push (list (format nil "┌─ ~a ──── ~,1fs ── [+~d more] ────────┐" n d extra)
|
|
||||||
(theme-color :tool-done)) pairs))
|
|
||||||
(dolist (call tc)
|
(dolist (call tc)
|
||||||
(let* ((name (or (getf call :name) "tool"))
|
(let* ((name (or (getf call :name) "tool"))
|
||||||
(dur (or (getf call :duration) 0.0))
|
(dur (or (getf call :duration) 0.0))
|
||||||
@@ -171,29 +160,10 @@ Returns a list of strings, one per line."
|
|||||||
((eq st :error) :tool-error)
|
((eq st :error) :tool-error)
|
||||||
(t :tool-done))))
|
(t :tool-done))))
|
||||||
(pfx (cond ((eq st :error) "✗") ((eq st :running) "●") (t "✓")))
|
(pfx (cond ((eq st :error) "✗") ((eq st :running) "●") (t "✓")))
|
||||||
(ol (when out (cl-tty.box:word-wrap out (- chat-w 6))))
|
(ol (when out (cl-tty.box:word-wrap out bordered-w))))
|
||||||
(top (format nil "┌─ ~a ──── ~,1fs ─" name dur))
|
(push (list "╎" bc (format nil "~a ~a ~,1fs" pfx name dur) bc) pairs)
|
||||||
(top-str (format nil "~a~a┐" top
|
|
||||||
(make-string (max 0 (- chat-w (length top) 1)) :initial-element #\─)))
|
|
||||||
(bot (format nil "└~a┘" (make-string (max 0 (- chat-w 2)) :initial-element #\─))))
|
|
||||||
(push (list top-str bc) pairs)
|
|
||||||
(dolist (l ol)
|
(dolist (l ol)
|
||||||
(push (list (format nil "│ ~a ~a~a│" pfx l
|
(push (list "╎" bc l bc) pairs)))))))
|
||||||
(make-string (max 0 (- chat-w (length pfx) (length l) 4))
|
|
||||||
:initial-element #\Space)) bc) pairs))
|
|
||||||
(push (list bot bc) pairs))))))
|
|
||||||
(when (> i 0)
|
|
||||||
(let ((pt (or (getf (aref msgs (1- i)) :time) "")))
|
|
||||||
(flet ((h (s) (if (> (length s) 0) (subseq s 0 (or (position #\: s) 0)) "")))
|
|
||||||
(let ((ph (h pt)) (ch (h time)))
|
|
||||||
(when (and (> (length ch) 0) (string/= ch ph))
|
|
||||||
(let* ((pad (max 0 (floor (- chat-w (length time) 2) 2)))
|
|
||||||
(rpad (- chat-w (length time) 2 pad)))
|
|
||||||
(push (list (format nil "~a ~a ~a"
|
|
||||||
(make-string pad :initial-element #\─)
|
|
||||||
time
|
|
||||||
(make-string rpad :initial-element #\─))
|
|
||||||
(theme-color :separator)) pairs)))))))
|
|
||||||
(setf (aref msg-lines i) (nreverse pairs))
|
(setf (aref msg-lines i) (nreverse pairs))
|
||||||
(setf (aref msg-heights i) (length pairs))))
|
(setf (aref msg-heights i) (length pairs))))
|
||||||
(let ((msg-count 0) (lines-remaining max-lines))
|
(let ((msg-count 0) (lines-remaining max-lines))
|
||||||
@@ -209,9 +179,11 @@ Returns a list of strings, one per line."
|
|||||||
do (let ((pairs (aref msg-lines i)))
|
do (let ((pairs (aref msg-lines i)))
|
||||||
(dolist (pair pairs)
|
(dolist (pair pairs)
|
||||||
(when (>= y (- h 4)) (return))
|
(when (>= y (- h 4)) (return))
|
||||||
(destructuring-bind (text color &optional bg) pair
|
(destructuring-bind (bstr bcolor tstr tcolor) pair
|
||||||
(when bg (cl-tty.backend:draw-text fb 0 y (make-string w :initial-element #\Space) nil bg))
|
(let ((has-border (and bstr (> (length bstr) 0))))
|
||||||
(cl-tty.backend:draw-text fb 0 y text color nil))
|
(when has-border
|
||||||
|
(cl-tty.backend:draw-text fb hpad y bstr bcolor nil))
|
||||||
|
(cl-tty.backend:draw-text fb (+ hpad (if has-border 2 0)) y tstr tcolor nil)))
|
||||||
(incf y)))))))))
|
(incf y)))))))))
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
|
||||||
@@ -220,64 +192,82 @@ Returns a list of strings, one per line."
|
|||||||
(defun view-input (fb w h)
|
(defun view-input (fb w h)
|
||||||
(let* ((w (or (and (numberp w) (> w 0) w) 80))
|
(let* ((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))
|
||||||
(sidebar-w (if (and (st :sidebar-visible) (>= w 60)) (or (st :sidebar-width) 30) 0))
|
(hpad 2)
|
||||||
|
(sidebar-w (if (sidebar-visible-p w) (or (st :sidebar-width) 42) 0))
|
||||||
(chat-w (- w sidebar-w))
|
(chat-w (- w sidebar-w))
|
||||||
(prompt-w (- chat-w 2)) ; leave room for "> "
|
(inner-w (- chat-w (* 2 hpad)))
|
||||||
|
(prompt-w (- inner-w 2))
|
||||||
(text (input-string))
|
(text (input-string))
|
||||||
(pos (or (st :cursor-pos) 0))
|
(pos (or (st :cursor-pos) 0))
|
||||||
(display-start (max 0 (- pos (1- prompt-w))))
|
(display-start (max 0 (- pos (1- prompt-w))))
|
||||||
(visible (subseq text display-start (min (length text) (+ display-start prompt-w))))
|
(visible (subseq text display-start (min (length text) (+ display-start prompt-w))))
|
||||||
(hint " Ctrl+P palette | Up/Dn history | Tab complete")
|
(bg-p (theme-color :bg-panel))
|
||||||
(hint (if (> (length hint) chat-w) (subseq hint 0 chat-w) hint)))
|
(sep-c (theme-color :separator))
|
||||||
(cl-tty.backend:draw-text fb 0 (- h 2) hint (theme-color :hint) nil)
|
(input-fg (theme-color :input-fg))
|
||||||
(cl-tty.backend:draw-text fb 0 (- h 3) (format nil "> ~a" visible) (theme-color :input-fg) nil)))
|
(hint-fg (theme-color :hint)))
|
||||||
|
;; Fill the 2-line input area (separator + prompt) with panel bg, indented by hpad
|
||||||
|
(cl-tty.backend:draw-rect fb hpad (- h 4) inner-w 2 :bg bg-p)
|
||||||
|
;; Separator line within the panel
|
||||||
|
(cl-tty.backend:draw-text fb hpad (- h 4) (make-string inner-w :initial-element #\─) sep-c nil)
|
||||||
|
;; Input line
|
||||||
|
(cl-tty.backend:draw-text fb hpad (- h 3) (format nil"> ~a" visible) input-fg nil)
|
||||||
|
;; Hint line — right-aligned on black background at the very bottom
|
||||||
|
(let ((hint "Ctrl+P | /help"))
|
||||||
|
(cl-tty.backend:draw-text fb (- chat-w (length hint) 2) (- h 1) hint hint-fg (theme-color :bg)))))
|
||||||
#+end_src
|
#+end_src
|
||||||
|
|
||||||
** Sidebar
|
** Sidebar
|
||||||
#+BEGIN_SRC lisp :tangle /home/user/.local/share/passepartout/lisp/channel-tui-view.lisp
|
#+BEGIN_SRC lisp :tangle /home/user/.local/share/passepartout/lisp/channel-tui-view.lisp
|
||||||
(defun view-sidebar (fb w h)
|
(defun view-sidebar (fb w h)
|
||||||
"Render the right-side sidebar panel with warm colors."
|
"Render the right-side sidebar panel."
|
||||||
(let* ((w (or (and (numberp w) (> w 0) w) 80))
|
(let* ((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))
|
||||||
(x (- w (or (st :sidebar-width) 30)))
|
(x (- w (or (st :sidebar-width) 42)))
|
||||||
|
(bg-panel (theme-color :bg-panel))
|
||||||
(y 0))
|
(y 0))
|
||||||
;; Vertical separator
|
;; Fill sidebar background (h-1 done separately to avoid scroll)
|
||||||
(dotimes (row h)
|
(cl-tty.backend:draw-rect fb x 0 (- w x) (1- h) :bg bg-panel)
|
||||||
(cl-tty.backend:draw-text fb (1- x) row " " nil (theme-color :separator)))
|
(cl-tty.backend:draw-text fb x (1- h) (make-string (- w x) :initial-element #\Space) nil bg-panel)
|
||||||
;; Focus panel
|
;; Focus panel
|
||||||
(cl-tty.backend:draw-text fb (1+ x) (incf y) " FOCUS" (theme-color :accent) nil)
|
(cl-tty.backend:draw-text fb (+ x 2) (incf y) "FOCUS" (theme-color :accent) bg-panel)
|
||||||
(incf y)
|
(incf y)
|
||||||
(cl-tty.backend:draw-text fb (1+ x) (incf y) (format nil " ~a" (or (st :foveal-id) "none"))
|
(cl-tty.backend:draw-text fb (+ x 2) (incf y) (format nil " ~a" (or (st :foveal-id) "none"))
|
||||||
(theme-color :agent-fg) nil)
|
(theme-color :agent-fg) bg-panel)
|
||||||
(incf y 2)
|
(incf y 2)
|
||||||
;; Rules panel
|
;; Rules panel
|
||||||
(cl-tty.backend:draw-text fb (1+ x) (incf y) " RULES" (theme-color :accent) nil)
|
(cl-tty.backend:draw-text fb (+ x 2) (incf y) "RULES" (theme-color :accent) bg-panel)
|
||||||
(incf y)
|
(incf y)
|
||||||
(cl-tty.backend:draw-text fb (1+ x) (incf y) (format nil " ~d active" (or (st :rule-count) 0))
|
(cl-tty.backend:draw-text fb (+ x 2) (incf y) (format nil " ~d active" (or (st :rule-count) 0))
|
||||||
(theme-color :agent-fg) nil)
|
(theme-color :agent-fg) bg-panel)
|
||||||
(incf y 2)
|
(incf y 2)
|
||||||
;; Context panel — token gauge
|
;; Context panel — token gauge
|
||||||
(cl-tty.backend:draw-text fb (1+ x) (incf y) " CONTEXT" (theme-color :accent) nil)
|
(cl-tty.backend:draw-text fb (+ x 2) (incf y) "CONTEXT" (theme-color :accent) bg-panel)
|
||||||
(incf y)
|
|
||||||
(let* ((msg-count (max 1 (length (st :messages))))
|
(let* ((msg-count (max 1 (length (st :messages))))
|
||||||
(est (* msg-count 60))
|
(est (* msg-count 60))
|
||||||
(limit 8192)
|
(limit 8192)
|
||||||
(pct (min 100 (floor (* 100 est) limit)))
|
(pct (min 100 (floor (* 100 est) limit)))
|
||||||
(bar-len (floor pct 10))
|
(bar-len (floor pct 10))
|
||||||
(bar (make-string bar-len :initial-element #\#)))
|
(bar (make-string bar-len :initial-element #\#)))
|
||||||
(cl-tty.backend:draw-text fb (1+ x) (incf y)
|
(cl-tty.backend:draw-text fb (+ x 2) (incf y)
|
||||||
(format nil " [~a~a]" bar
|
(format nil " [~a~a]" bar
|
||||||
(make-string (- 10 bar-len) :initial-element #\Space))
|
(make-string (- 10 bar-len) :initial-element #\Space))
|
||||||
(theme-color :dim) nil)
|
(theme-color :dim) bg-panel)
|
||||||
(incf y)
|
(incf y)
|
||||||
(cl-tty.backend:draw-text fb (1+ x) (incf y) (format nil " ~d%" pct)
|
(cl-tty.backend:draw-text fb (+ x 2) (incf y) (format nil " ~d%" pct)
|
||||||
(theme-color :status-fg) nil)
|
(theme-color :status-fg) bg-panel)
|
||||||
(incf y 2))
|
(incf y 2))
|
||||||
;; MCP panel
|
;; MCP panel
|
||||||
(cl-tty.backend:draw-text fb (1+ x) (incf y) " MCP" (theme-color :accent) nil)
|
(cl-tty.backend:draw-text fb (+ x 2) (incf y) "MCP" (theme-color :accent) bg-panel)
|
||||||
(incf y)
|
(incf y)
|
||||||
(cl-tty.backend:draw-text fb (1+ x) (incf y) (format nil " ~d server~:p" (or (st :mcp-count) 0))
|
(cl-tty.backend:draw-text fb (+ x 2) (incf y) (format nil " ~d server~:p" (or (st :mcp-count) 0))
|
||||||
(theme-color :agent-fg) nil)))
|
(theme-color :agent-fg) bg-panel)
|
||||||
|
;; Version footer at bottom with connection dot
|
||||||
|
(let* ((ver (or (st :daemon-version) ""))
|
||||||
|
(ver-label (if (> (length ver) 0) (format nil "passepartout ~a" ver) "passepartout"))
|
||||||
|
(dot (if (st :connected) "●" "○"))
|
||||||
|
(dot-color (if (st :connected) (theme-color :dot-connected) (theme-color :dot-disconnected))))
|
||||||
|
(cl-tty.backend:draw-text fb (+ x 2) (- h 2) dot dot-color bg-panel)
|
||||||
|
(cl-tty.backend:draw-text fb (+ x 4) (- h 2) ver-label (theme-color :text-muted) bg-panel))))
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
|
||||||
** Redraw (dirty-flag dispatch)
|
** Redraw (dirty-flag dispatch)
|
||||||
@@ -286,10 +276,12 @@ Returns a list of strings, one per line."
|
|||||||
(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))
|
||||||
(destructuring-bind (sd cd id) (st :dirty)
|
(destructuring-bind (sd cd id) (st :dirty)
|
||||||
|
;; Fill global background
|
||||||
|
(cl-tty.backend:draw-rect fb 0 0 w h :bg (theme-color :bg))
|
||||||
(when sd (view-status fb w h))
|
(when sd (view-status fb w h))
|
||||||
(when cd (view-chat fb w h))
|
(when cd (view-chat fb w h))
|
||||||
(when id (view-input fb w h))
|
(when id (view-input fb w h))
|
||||||
(when (and (st :sidebar-visible) (>= w 60))
|
(when (sidebar-visible-p w)
|
||||||
(view-sidebar fb w h))
|
(view-sidebar fb w h))
|
||||||
(setf (st :dirty) (list nil nil nil))))
|
(setf (st :dirty) (list nil nil nil))))
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
@@ -597,23 +589,48 @@ dead code.
|
|||||||
(is (null cg))))
|
(is (null cg))))
|
||||||
|
|
||||||
(test test-sidebar-state
|
(test test-sidebar-state
|
||||||
"Contract v0.8.0: init-state includes :sidebar-visible (nil) and :sidebar-width (30)."
|
"Contract v0.8.0: init-state includes :sidebar-mode (:auto) and :sidebar-width (42)."
|
||||||
(passepartout.channel-tui::init-state)
|
(passepartout.channel-tui::init-state)
|
||||||
(is (null (passepartout.channel-tui::st :sidebar-visible)))
|
(is (eq :auto (passepartout.channel-tui::st :sidebar-mode)))
|
||||||
(is (= 30 (passepartout.channel-tui::st :sidebar-width))))
|
(is (= 42 (passepartout.channel-tui::st :sidebar-width))))
|
||||||
|
|
||||||
(test test-sidebar-not-shown-narrow
|
(defun sidebar-visible-p (w)
|
||||||
"Contract v0.8.0: sidebar is skipped in redraw when terminal width < 120."
|
"Compute whether sidebar should be shown given terminal width W
|
||||||
|
and current sidebar mode."
|
||||||
|
(let ((mode (passepartout.channel-tui::st :sidebar-mode)))
|
||||||
|
(or (eq mode :visible)
|
||||||
|
(and (eq mode :auto) (> w 120)))))
|
||||||
|
|
||||||
|
(test test-sidebar-auto-wide
|
||||||
|
"Contract v0.8.0: sidebar auto-shows when terminal > 120 cols."
|
||||||
(passepartout.channel-tui::init-state)
|
(passepartout.channel-tui::init-state)
|
||||||
(setf (passepartout.channel-tui::st :sidebar-visible) t)
|
(setf (passepartout.channel-tui::st :sidebar-mode) :auto)
|
||||||
;; Redraw guard: view-sidebar is only called when w >= 60. This
|
(is (sidebar-visible-p 140))
|
||||||
;; verifies the guard expression evaluates to nil at w=100 when
|
(is (not (sidebar-visible-p 100))))
|
||||||
;; sidebar-visible is set but width is below 120 threshold.
|
|
||||||
(let ((w 100))
|
(test test-sidebar-visible-mode
|
||||||
(is (not (and (passepartout.channel-tui::st :sidebar-visible) (>= w 60))))))
|
"Contract v0.8.0: :visible mode shows sidebar regardless of width."
|
||||||
|
(passepartout.channel-tui::init-state)
|
||||||
|
(setf (passepartout.channel-tui::st :sidebar-mode) :visible)
|
||||||
|
(is (sidebar-visible-p 40))
|
||||||
|
(is (sidebar-visible-p 140)))
|
||||||
|
|
||||||
|
(test test-sidebar-hidden-mode
|
||||||
|
"Contract v0.8.0: :hidden mode hides sidebar regardless of width."
|
||||||
|
(passepartout.channel-tui::init-state)
|
||||||
|
(setf (passepartout.channel-tui::st :sidebar-mode) :hidden)
|
||||||
|
(is (not (sidebar-visible-p 140)))
|
||||||
|
(is (not (sidebar-visible-p 40))))
|
||||||
|
|
||||||
(test test-status-bar-tokens
|
(test test-status-bar-tokens
|
||||||
"v0.9.0: status bar uses :status-fg and :status-bg theme tokens."
|
"v0.9.0: status bar uses :status-fg and :status-bg theme tokens."
|
||||||
(is (getf passepartout.channel-tui::*tui-theme* :status-fg))
|
(is (getf passepartout.channel-tui::*tui-theme* :status-fg))
|
||||||
(is (getf passepartout.channel-tui::*tui-theme* :status-bg)))
|
(is (getf passepartout.channel-tui::*tui-theme* :status-bg)))
|
||||||
|
|
||||||
|
(test test-new-theme-keys
|
||||||
|
"v0.10.0: theme has new :bg, :bg-panel, :bg-element, :text-muted keys."
|
||||||
|
(is (getf passepartout.channel-tui::*tui-theme* :bg))
|
||||||
|
(is (getf passepartout.channel-tui::*tui-theme* :bg-panel))
|
||||||
|
(is (getf passepartout.channel-tui::*tui-theme* :bg-element))
|
||||||
|
(is (getf passepartout.channel-tui::*tui-theme* :text-muted)))
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
|||||||
Reference in New Issue
Block a user