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:
2026-05-16 08:02:53 -04:00
parent 3bc1977632
commit 0a0478f502
3 changed files with 394 additions and 332 deletions

View File

@@ -789,7 +789,11 @@ Returns T on success, nil on failure. Does NOT wait or retry."
(:ctrl+p (lambda (e) (declare (ignore e))
(unified-menu-show)))
(: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))))
(:ppage (lambda (e) (declare (ignore e))
(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
(:CTRL-Q (setf (st :running) nil))
(: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)))
(:CTRL-L (setf (st :dirty) (list t t t)))
(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))
(when (and (or (first (st :dirty)) (second (st :dirty)) (third (st :dirty)))
(null (st :dialog-stack)))
(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))))
(cl-tty.backend:begin-sync be)
(cl-tty.backend:draw-rect be 0 0 w h :bg (theme-color :bg))
(view-status be w h)
(view-chat be w h)
(view-input be w h)
(when (sidebar-visible-p w)
(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* ((sidebar-w (if (and (st :sidebar-visible) (>= w 60))
(or (st :sidebar-width) 30) 0))
(chat-w (- w sidebar-w))
(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))
(mh (min 15 (+ 1 cnt))) ;; +1 for title
(top (max 0 (- h 4 mh))))
;; Clear the minibuffer area
(dotimes (r (min (- h 3 top) h))
(cl-tty.backend:draw-rect be 0 (+ top r) chat-w 1
:bg (theme-color :status-bg)))
;; Top border line with title
(cl-tty.backend:draw-text be 0 top
(make-string chat-w :initial-element #\─)
(theme-color :separator) nil)
(cl-tty.backend:draw-text be 1 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-rect be 0 (- h 3) chat-w 1
:bg (theme-color :status-bg))
(cl-tty.backend:draw-text be 0 (- h 3)
(format nil "> ~a" (or filter ""))
(theme-color :input-prompt) nil))
(cl-tty.backend:begin-sync be)
(let* ((chat-w (- w (if (sidebar-visible-p w) (or (st :sidebar-width) 42) 0)))
(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))
(mh (min 15 (+ 1 cnt)))
(top (max 0 (- h 4 mh)))
(bg-p (theme-color :bg-panel))
(sep-c (theme-color :separator)))
;; Fill minibuffer area with panel bg
(dotimes (r (min (- h 3 top) h))
(cl-tty.backend:draw-rect be 0 (+ top r) chat-w 1 :bg bg-p))
;; Top separator
(cl-tty.backend:draw-text be 0 top
(make-string chat-w :initial-element #\─)
sep-c nil)
(cl-tty.backend:draw-text be 1 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~a" (if 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 :text-muted))
(sel-p (theme-color :accent))
(t (theme-color :agent-fg)))
nil :bold sel-p)
(incf y-off))))
;; Filter prompt
(cl-tty.backend:draw-rect be 0 (- h 3) chat-w 1 :bg bg-p)
(cl-tty.backend:draw-text be 0 (- h 3)
(format nil "> ~a" (or filter ""))
(theme-color :input-prompt) nil))
(cl-tty.backend:end-sync be))
(sleep 0.1)))
(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:test test-theme
"Contract view: *tui-theme* provides warm color mappings."
(fiveam:is (string= "#FFB347" (getf *tui-theme* :user-fg)))
(fiveam:is (string= "#E8D5B7" (getf *tui-theme* :agent-fg)))
(fiveam:is (string= "#C8A87C" (getf *tui-theme* :system)))
(fiveam:is (string= "#E8D5B7" (getf *tui-theme* :input-fg)))
"Contract view: *tui-theme* provides color mappings."
(fiveam:is (string= "#fab283" (getf *tui-theme* :user-fg)))
(fiveam:is (string= "#e8e8e8" (getf *tui-theme* :agent-fg)))
(fiveam:is (string= "#808080" (getf *tui-theme* :system)))
(fiveam:is (string= "#e8e8e8" (getf *tui-theme* :input-fg)))
(fiveam:is (string= "#FFFFFF" (theme-color :unknown-role))))
(fiveam:test test-on-key-ctrl-u-clears