v0.10.1: architectural cleanup — full-frame redraw, explicit bg everywhere, :bg-input fallback

- redraw: always draws all three views (status/chat/input) when any
  dirty flag is set. Dirty flags only gate frame rendering, not
  which parts render. Fixes disappearing input/history.
- Added :bg-input to all 13 presets with #2e2e2e (dark) / #d4d4d4
  (light-amber). theme-load fills missing keys from current preset
  defaults for backward compatibility.
- Removed unused *sidebar-panels* defvar and obsolete contract docs.
- Renamed dim-bg → dim-fg (foreground color, not background).
- All draw-text calls in sidebar and dialog minibuffer now pass
  explicit bg-panel, preventing background leaks.
- render-styled (markdown renderer) passes explicit (theme-color :bg).
- Fix h shadowing in view-chat scroll loop (h → mh).
This commit is contained in:
2026-05-16 09:03:59 -04:00
parent 0a0478f502
commit 2189745f40
3 changed files with 112 additions and 114 deletions

View File

@@ -8,21 +8,19 @@
** Contract
1. (view-status win): renders the status bar with connection info,
msg count, scroll offset, rule counter, focus map (v0.4.0), and
timestamp. Two lines: line 1 (status + rules), line 2 (focus + time).
2. (view-chat win h): renders the scrolled chat message list. Takes
window and available height. Messages are color-coded: green (user),
white (agent), yellow (system).
3. (view-input win): renders the input line with cursor and typing
indicator.
4. (redraw sw cw ch iw): dispatches redraws based on ~(st :dirty)~
flags (status, chat, input). Minimizes terminal writes.
5. (char-width ch): returns the terminal column width of character CH.
1. (view-status fb w h): no-op. Status bar is a clean black line.
2. (view-chat fb w h): renders scrolled chat messages. User messages
get amber left border (│), agent messages no border, streaming
agent gets grey left border. Gate traces/tool calls use ╎ prefix.
3. (view-input fb w h): renders light grey input box (h-7 to h-4),
prompt at h-6, right-aligned lowercase hint at h-2.
4. (redraw fb w h): wraps view-status/chat/input in begin-sync/end-sync,
dispatches per dirty flags, fills global :bg first.
5. (char-width ch): returns terminal column width of character CH.
ASCII < 128 = 1. CJK, fullwidth, emoji = 2. Combining marks = 0.
Tab = 8. Used by word-wrap for accurate line counting (v0.7.0).
6. (view-status win): v0.7.0 — timestamp right-aligned at (- chat-w 12)
on line 2, focus info at :x 1. No overlap.
6. (sidebar-visible-p w): returns T if sidebar should show given width W
and current :sidebar-mode (:auto >120, :visible always, :hidden never).
** Status Bar
@@ -92,11 +90,11 @@ Returns a list of strings, one per line."
(defun view-chat (fb w h)
(let* ((w (or (and (numberp w) (> w 0) w) 80))
(h (or (and (numberp h) (> h 0) h) 24))
(hpad 2)
(sidebar-w (if (sidebar-visible-p w) (or (st :sidebar-width) 42) 0))
(chat-w (- w sidebar-w))
(msgs (st :messages)) (total (length msgs))
(max-lines (- h 4)) (is-search (st :search-mode))
(hpad 2)
(sidebar-w (if (sidebar-visible-p w) (or (st :sidebar-width) 42) 0))
(chat-w (- w sidebar-w))
(msgs (st :messages)) (total (length msgs))
(max-lines (- h 7)) (is-search (st :search-mode))
(bordered-w (- chat-w (* 2 hpad) 2))
(unbordered-w (- chat-w (* 2 hpad)))
(y 0))
@@ -113,7 +111,7 @@ Returns a list of strings, one per line."
(content (getf msg :content))
(cs (if is-search (search-highlight content (st :search-query)) content))
(pairs nil)
(dim-bg (theme-color :dim))
(dim-fg (theme-color :dim))
(user-bdr (theme-color :user-border))
(user-fg (theme-color :user-fg))
(agent-fg (theme-color :agent-fg))
@@ -124,7 +122,7 @@ Returns a list of strings, one per line."
(push (list "│" user-bdr l user-fg) pairs)))
(:agent
(let* ((streaming (getf msg :streaming))
(bdr-color (if streaming dim-bg nil))
(bdr-color (if streaming dim-fg nil))
(bdr-str (if streaming "│" ""))
(wrap-w (if streaming bordered-w unbordered-w))
(nodes (cl-tty.markdown:parse-blocks cs))
@@ -138,18 +136,18 @@ Returns a list of strings, one per line."
(let ((gt (getf msg :gate-trace)))
(when (and gt (eq role :agent))
(if (member i (st :collapsed-gates))
(push (list "╎" dim-bg (format nil "Gate trace: ~a gates" (length gt)) dim-bg) pairs)
(push (list "╎" dim-fg (format nil "Gate trace: ~a gates" (length gt)) dim-fg) pairs)
(dolist (entry (passepartout::gate-trace-lines gt))
(let ((ec (theme-color (getf (cdr entry) :fgcolor))))
(dolist (l (cl-tty.box:word-wrap (car entry) bordered-w))
(push (list "╎" dim-bg l ec) pairs)))))))
(push (list "╎" dim-fg l ec) pairs)))))))
;; Tool calls
(let ((tc (getf msg :tool-calls)))
(when tc
(if (member i (st :collapsed-tools))
(let* ((n (or (getf (first tc) :name) "tool"))
(d (or (getf (first tc) :duration) 0.0)))
(push (list "╎" dim-bg (format nil "~a … ~,1fs" n d) (theme-color :tool-done)) pairs))
(push (list "╎" dim-fg (format nil "~a … ~,1fs" n d) (theme-color :tool-done)) pairs))
(dolist (call tc)
(let* ((name (or (getf call :name) "tool"))
(dur (or (getf call :duration) 0.0))
@@ -169,16 +167,16 @@ Returns a list of strings, one per line."
(let ((msg-count 0) (lines-remaining max-lines))
(loop for i from (1- total) downto 0
while (> lines-remaining 0)
do (let ((h (aref msg-heights i)))
(if (<= h lines-remaining)
(progn (decf lines-remaining h) (incf msg-count))
do (let ((mh (aref msg-heights i)))
(if (<= mh lines-remaining)
(progn (decf lines-remaining mh) (incf msg-count))
(setf lines-remaining 0))))
(let* ((scroll-skip (st :scroll-offset))
(start (max 0 (- total msg-count scroll-skip))))
(loop for i from start below total while (< y (- h 4))
do (let ((pairs (aref msg-lines i)))
(dolist (pair pairs)
(when (>= y (- h 4)) (return))
(loop for i from start below total while (< y (- h 7))
do (let ((pairs (aref msg-lines i)))
(dolist (pair pairs)
(when (>= y (- h 7)) (return))
(destructuring-bind (bstr bcolor tstr tcolor) pair
(let ((has-border (and bstr (> (length bstr) 0))))
(when has-border
@@ -201,19 +199,16 @@ Returns a list of strings, one per line."
(pos (or (st :cursor-pos) 0))
(display-start (max 0 (- pos (1- prompt-w))))
(visible (subseq text display-start (min (length text) (+ display-start prompt-w))))
(bg-p (theme-color :bg-panel))
(sep-c (theme-color :separator))
(input-fg (theme-color :input-fg))
(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)))))
(bg-i (theme-color :bg-input))
(input-fg (theme-color :input-fg))
(hint-fg (theme-color :hint)))
;; Light grey input panel: h-7 to h-4 (4 rows), indented by hpad
(cl-tty.backend:draw-rect fb hpad (- h 7) inner-w 4 :bg bg-i)
;; Prompt at h-6, second row at h-5 (placeholder for expansion)
(cl-tty.backend:draw-text fb hpad (- h 6) (format nil"> ~a" visible) input-fg nil)
;; Hint — lowercase, right-aligned at h-2
(let ((hint "ctrl+p | /help"))
(cl-tty.backend:draw-text fb (- chat-w (length hint) 2) (- h 2) hint hint-fg (theme-color :bg)))))
#+end_src
** Sidebar
@@ -275,14 +270,15 @@ Returns a list of strings, one per line."
(defun redraw (fb w h)
(setq w (or (and (numberp w) (> w 0) w) 80)
h (or (and (numberp h) (> h 0) h) 24))
(destructuring-bind (sd cd id) (st :dirty)
;; Fill global background
(when (or (first (st :dirty)) (second (st :dirty)) (third (st :dirty)))
(cl-tty.backend:begin-sync fb)
(cl-tty.backend:draw-rect fb 0 0 w h :bg (theme-color :bg))
(when sd (view-status fb w h))
(when cd (view-chat fb w h))
(when id (view-input fb w h))
(view-status fb w h)
(view-chat fb w h)
(view-input fb w h)
(when (sidebar-visible-p w)
(view-sidebar fb w h))
(cl-tty.backend:end-sync fb)
(setf (st :dirty) (list nil nil nil))))
#+END_SRC
@@ -380,7 +376,7 @@ dead code.
(cl-tty.backend:draw-text fb x y text
(cond (url (passepartout.channel-tui:theme-color :accent))
(t (passepartout.channel-tui:theme-color (or (getf attrs :role) :agent-fg))))
nil
(passepartout.channel-tui:theme-color :bg)
:bold bold)
(incf x (length text))))
y)
@@ -628,9 +624,10 @@ and current sidebar mode."
(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."
"v0.10.0: theme has new :bg, :bg-panel, :bg-element, :bg-input, :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* :bg-input))
(is (getf passepartout.channel-tui::*tui-theme* :text-muted)))
#+END_SRC