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

@@ -1016,17 +1016,8 @@ Returns T on success, nil on failure. Does NOT wait or retry."
;; 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 (and (or (first (st :dirty)) (second (st :dirty)) (third (st :dirty))) (unless (st :dialog-stack)
(null (st :dialog-stack))) (redraw be w h))
(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))) (let ((ds (st :dialog-stack)))
(when ds (when ds
(cl-tty.backend:begin-sync be) (cl-tty.backend:begin-sync be)
@@ -1038,42 +1029,42 @@ Returns T on success, nil on failure. Does NOT wait or retry."
(cnt (length filtered)) (cnt (length filtered))
(filter (cl-tty.select:select-filter sel)) (filter (cl-tty.select:select-filter sel))
(mh (min 15 (+ 1 cnt))) (mh (min 15 (+ 1 cnt)))
(top (max 0 (- h 4 mh))) (top (max 0 (- h 7 mh)))
(bg-p (theme-color :bg-panel)) (bg-p (theme-color :bg-panel))
(sep-c (theme-color :separator))) (sep-c (theme-color :separator)))
;; Fill minibuffer area with panel bg ;; 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 :bg bg-p)) (cl-tty.backend:draw-rect be 0 (+ top r) chat-w 1 :bg bg-p))
;; Top separator ;; Top separator
(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) sep-c bg-p)
(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) bg-p)
;; 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~a" (if 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 :text-muted))
(sel-p (theme-color :accent)) (sel-p (theme-color :accent))
(t (theme-color :agent-fg))) (t (theme-color :agent-fg)))
nil :bold sel-p) bg-p :bold sel-p)
(incf y-off)))) (incf y-off))))
;; Filter prompt ;; Filter prompt
(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 :bg bg-p)
(cl-tty.backend:draw-text be 0 (- h 3) (cl-tty.backend:draw-text be 0 (- h 3)
(format nil "> ~a" (or filter "")) (format nil "> ~a" (or filter ""))
(theme-color :input-prompt) nil)) (theme-color :input-prompt) bg-p))
(cl-tty.backend:end-sync be)) (cl-tty.backend:end-sync be))
(sleep 0.1))) (sleep 0.1)))
(progn (disconnect-daemon))))) (progn (disconnect-daemon)))))

View File

@@ -42,13 +42,14 @@ All state mutation flows through event handlers in the controller.
:bg "#0a0a0a" :bg-panel "#141414" :bg-element "#1e1e1e" :bg "#0a0a0a" :bg-panel "#141414" :bg-element "#1e1e1e"
:text-muted "#808080" :text-muted "#808080"
:dot-connected "#7fd88f" :dot-disconnected "#e06c75" :dot-connected "#7fd88f" :dot-disconnected "#e06c75"
:bg-input "#2e2e2e"
:error "#e06c75" :error "#e06c75"
:tool-running "#fab283" :tool-done "#7fd88f" :tool-error "#e06c75" :tool-running "#fab283" :tool-done "#7fd88f" :tool-error "#e06c75"
:separator "#3c3c3c" :accent "#fab283" :dim "#606060") :separator "#3c3c3c" :accent "#fab283" :dim "#606060")
"Dark-neutral color theme with warm amber accent. Backgrounds are dark grays, "Dark-neutral color theme with warm amber accent. Backgrounds are dark grays,
semantic text colors for context. Keys: :bg (deepest), :bg-panel, :bg-element, 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, :text-muted, :user-fg/bg/border, :agent-header/fg, :system, :input-prompt/fg,
:hint, :status-bg/fg, :dot-connected/disconnected, :error, :tool-*, :hint, :status-bg/fg, :bg-input, :dot-connected/disconnected, :error, :tool-*,
:separator, :accent, :dim.") :separator, :accent, :dim.")
(defvar *tui-theme-presets* (defvar *tui-theme-presets*
@@ -60,6 +61,7 @@ semantic text colors for context. Keys: :bg (deepest), :bg-panel, :bg-element,
:hint "#606060" :hint "#606060"
:status-bg "#141414" :status-fg "#e8e8e8" :status-bg "#141414" :status-fg "#e8e8e8"
:bg "#0a0a0a" :bg-panel "#141414" :bg-element "#1e1e1e" :bg "#0a0a0a" :bg-panel "#141414" :bg-element "#1e1e1e"
:bg-input "#2e2e2e"
:text-muted "#808080" :text-muted "#808080"
:dot-connected "#7fd88f" :dot-disconnected "#e06c75" :dot-connected "#7fd88f" :dot-disconnected "#e06c75"
:error "#e06c75" :error "#e06c75"
@@ -73,6 +75,7 @@ semantic text colors for context. Keys: :bg (deepest), :bg-panel, :bg-element,
:hint "#606060" :hint "#606060"
:status-bg "#141414" :status-fg "#ffd700" :status-bg "#141414" :status-fg "#ffd700"
:bg "#0a0a0a" :bg-panel "#141414" :bg-element "#1e1e1e" :bg "#0a0a0a" :bg-panel "#141414" :bg-element "#1e1e1e"
:bg-input "#2e2e2e"
:text-muted "#808080" :text-muted "#808080"
:dot-connected "#7fd88f" :dot-disconnected "#e06c75" :dot-connected "#7fd88f" :dot-disconnected "#e06c75"
:error "#e06c75" :error "#e06c75"
@@ -86,6 +89,7 @@ semantic text colors for context. Keys: :bg (deepest), :bg-panel, :bg-element,
:hint "#606060" :hint "#606060"
:status-bg "#141414" :status-fg "#d4956a" :status-bg "#141414" :status-fg "#d4956a"
:bg "#0a0a0a" :bg-panel "#141414" :bg-element "#1e1e1e" :bg "#0a0a0a" :bg-panel "#141414" :bg-element "#1e1e1e"
:bg-input "#2e2e2e"
:text-muted "#808080" :text-muted "#808080"
:dot-connected "#6cb85c" :dot-disconnected "#d94a3a" :dot-connected "#6cb85c" :dot-disconnected "#d94a3a"
:error "#d94a3a" :error "#d94a3a"
@@ -99,6 +103,7 @@ semantic text colors for context. Keys: :bg (deepest), :bg-panel, :bg-element,
:hint "#606060" :hint "#606060"
:status-bg "#141414" :status-fg "#b89870" :status-bg "#141414" :status-fg "#b89870"
:bg "#0a0a0a" :bg-panel "#141414" :bg-element "#1e1e1e" :bg "#0a0a0a" :bg-panel "#141414" :bg-element "#1e1e1e"
:bg-input "#2e2e2e"
:text-muted "#808080" :text-muted "#808080"
:dot-connected "#7aac5c" :dot-disconnected "#c84a3a" :dot-connected "#7aac5c" :dot-disconnected "#c84a3a"
:error "#c84a3a" :error "#c84a3a"
@@ -112,6 +117,7 @@ semantic text colors for context. Keys: :bg (deepest), :bg-panel, :bg-element,
:hint "#606060" :hint "#606060"
:status-bg "#141414" :status-fg "#c8a080" :status-bg "#141414" :status-fg "#c8a080"
:bg "#0a0a0a" :bg-panel "#141414" :bg-element "#1e1e1e" :bg "#0a0a0a" :bg-panel "#141414" :bg-element "#1e1e1e"
:bg-input "#2e2e2e"
:text-muted "#808080" :text-muted "#808080"
:dot-connected "#7cb860" :dot-disconnected "#d06050" :dot-connected "#7cb860" :dot-disconnected "#d06050"
:error "#d06050" :error "#d06050"
@@ -125,6 +131,7 @@ semantic text colors for context. Keys: :bg (deepest), :bg-panel, :bg-element,
:hint "#606060" :hint "#606060"
:status-bg "#141414" :status-fg "#cc9966" :status-bg "#141414" :status-fg "#cc9966"
:bg "#0a0a0a" :bg-panel "#141414" :bg-element "#1e1e1e" :bg "#0a0a0a" :bg-panel "#141414" :bg-element "#1e1e1e"
:bg-input "#2e2e2e"
:text-muted "#808080" :text-muted "#808080"
:dot-connected "#7ab85c" :dot-disconnected "#d94a3a" :dot-connected "#7ab85c" :dot-disconnected "#d94a3a"
:error "#d94a3a" :error "#d94a3a"
@@ -138,6 +145,7 @@ semantic text colors for context. Keys: :bg (deepest), :bg-panel, :bg-element,
:hint "#606060" :hint "#606060"
:status-bg "#141414" :status-fg "#c8a070" :status-bg "#141414" :status-fg "#c8a070"
:bg "#0a0a0a" :bg-panel "#141414" :bg-element "#1e1e1e" :bg "#0a0a0a" :bg-panel "#141414" :bg-element "#1e1e1e"
:bg-input "#2e2e2e"
:text-muted "#808080" :text-muted "#808080"
:dot-connected "#7ab85c" :dot-disconnected "#d94a3a" :dot-connected "#7ab85c" :dot-disconnected "#d94a3a"
:error "#d94a3a" :error "#d94a3a"
@@ -150,7 +158,8 @@ semantic text colors for context. Keys: :bg (deepest), :bg-panel, :bg-element,
:input-prompt "#cc6600" :input-fg "#3a2a1a" :input-prompt "#cc6600" :input-fg "#3a2a1a"
:hint "#a0a0a0" :hint "#a0a0a0"
:status-bg "#ebebeb" :status-fg "#3a2a1a" :status-bg "#ebebeb" :status-fg "#3a2a1a"
:bg "#ffffff" :bg-panel "#f5f5f5" :bg-element "#ebebeb" :bg "#ffffff" :bg-panel "#f5f5f5" :bg-element "#ebebeb"
:bg-input "#d4d4d4"
:text-muted "#808080" :text-muted "#808080"
:dot-connected "#2e8b57" :dot-disconnected "#cc3300" :dot-connected "#2e8b57" :dot-disconnected "#cc3300"
:error "#cc3300" :error "#cc3300"
@@ -163,7 +172,8 @@ semantic text colors for context. Keys: :bg (deepest), :bg-panel, :bg-element,
:input-prompt "#fab387" :input-fg "#cdd6f4" :input-prompt "#fab387" :input-fg "#cdd6f4"
:hint "#6c7086" :hint "#6c7086"
:status-bg "#181825" :status-fg "#a6adc8" :status-bg "#181825" :status-fg "#a6adc8"
:bg "#11111b" :bg-panel "#181825" :bg-element "#1e1e2e" :bg "#11111b" :bg-panel "#181825" :bg-element "#1e1e2e"
:bg-input "#2e2e2e"
:text-muted "#6c7086" :text-muted "#6c7086"
:dot-connected "#a6e3a1" :dot-disconnected "#f38ba8" :dot-connected "#a6e3a1" :dot-disconnected "#f38ba8"
:error "#f38ba8" :error "#f38ba8"
@@ -176,7 +186,8 @@ semantic text colors for context. Keys: :bg (deepest), :bg-panel, :bg-element,
:input-prompt "#ff9e64" :input-fg "#a9b1d6" :input-prompt "#ff9e64" :input-fg "#a9b1d6"
:hint "#565f89" :hint "#565f89"
:status-bg "#16161e" :status-fg "#9aa5ce" :status-bg "#16161e" :status-fg "#9aa5ce"
:bg "#0f0f18" :bg-panel "#16161e" :bg-element "#1a1b26" :bg "#0f0f18" :bg-panel "#16161e" :bg-element "#1a1b26"
:bg-input "#2e2e2e"
:text-muted "#565f89" :text-muted "#565f89"
:dot-connected "#9ece6a" :dot-disconnected "#db4b4b" :dot-connected "#9ece6a" :dot-disconnected "#db4b4b"
:error "#db4b4b" :error "#db4b4b"
@@ -189,7 +200,8 @@ semantic text colors for context. Keys: :bg (deepest), :bg-panel, :bg-element,
:input-prompt "#ff9580" :input-fg "#f8f8f2" :input-prompt "#ff9580" :input-fg "#f8f8f2"
:hint "#6272a4" :hint "#6272a4"
:status-bg "#191a24" :status-fg "#e0e0e0" :status-bg "#191a24" :status-fg "#e0e0e0"
:bg "#0f101a" :bg-panel "#191a24" :bg-element "#1e1f2b" :bg "#0f101a" :bg-panel "#191a24" :bg-element "#1e1f2b"
:bg-input "#2e2e2e"
:text-muted "#6272a4" :text-muted "#6272a4"
:dot-connected "#50fa7b" :dot-disconnected "#ff5555" :dot-connected "#50fa7b" :dot-disconnected "#ff5555"
:error "#ff5555" :error "#ff5555"
@@ -202,7 +214,8 @@ semantic text colors for context. Keys: :bg (deepest), :bg-panel, :bg-element,
:input-prompt "#87afff" :input-fg "#ffffff" :input-prompt "#87afff" :input-fg "#ffffff"
:hint "#606060" :hint "#606060"
:status-bg "#141414" :status-fg "#afafaf" :status-bg "#141414" :status-fg "#afafaf"
:bg "#000000" :bg-panel "#141414" :bg-element "#1a1a1a" :bg "#000000" :bg-panel "#141414" :bg-element "#1a1a1a"
:bg-input "#2e2e2e"
:text-muted "#808080" :text-muted "#808080"
:dot-connected "#d7ffd7" :dot-disconnected "#ff87af" :dot-connected "#d7ffd7" :dot-disconnected "#ff87af"
:error "#ff87af" :error "#ff87af"
@@ -215,7 +228,8 @@ semantic text colors for context. Keys: :bg (deepest), :bg-panel, :bg-element,
:input-prompt "#ffffff" :input-fg "#d0d0d0" :input-prompt "#ffffff" :input-fg "#d0d0d0"
:hint "#606060" :hint "#606060"
:status-bg "#141414" :status-fg "#b0b0b0" :status-bg "#141414" :status-fg "#b0b0b0"
:bg "#0a0a0a" :bg-panel "#141414" :bg-element "#1a1a1a" :bg "#0a0a0a" :bg-panel "#141414" :bg-element "#1a1a1a"
:bg-input "#2e2e2e"
:text-muted "#808080" :text-muted "#808080"
:dot-connected "#a0a0a0" :dot-disconnected "#808080" :dot-connected "#a0a0a0" :dot-disconnected "#808080"
:error "#808080" :error "#808080"
@@ -223,7 +237,7 @@ semantic text colors for context. Keys: :bg (deepest), :bg-panel, :bg-element,
:separator "#303030" :accent "#ffffff" :dim "#505050")) :separator "#303030" :accent "#ffffff" :dim "#505050"))
"13 theme presets (amber, gold, terracotta, sepia, nord-warm, "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). Now with dark-neutral backgrounds and new :bg/:bg-panel/:bg-element/:text-muted slots.") gemini, mono). Keys: :bg/:bg-panel/:bg-element/:bg-input/:text-muted.")
(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.")
@@ -240,11 +254,19 @@ gemini, mono). Now with dark-neutral backgrounds and new :bg/:bg-panel/:bg-eleme
t)) t))
(defun theme-load () (defun theme-load ()
"Load persisted theme from disk. Called at startup." "Load persisted theme from disk. Called at startup.
Adds any missing keys with defaults to handle saved themes from older versions."
(let ((path (merge-pathnames ".cache/passepartout/theme.lisp" (let ((path (merge-pathnames ".cache/passepartout/theme.lisp"
(user-homedir-pathname)))) (user-homedir-pathname))))
(when (uiop:file-exists-p path) (when (uiop:file-exists-p path)
(ignore-errors (load path))))) (ignore-errors (load path)))
;; Fill in any missing keys from the default preset
(let ((defaults (getf *tui-theme-presets* *tui-theme-current-name*)))
(when defaults
(dolist (key '(:bg-input :bg-element :text-muted))
(unless (getf *tui-theme* key)
(let ((val (getf defaults key)))
(when val (setf (getf *tui-theme* key) val)))))))))
(defun theme-switch (name) (defun theme-switch (name)
"Switch to a named theme preset. Returns the preset name or nil if not found." "Switch to a named theme preset. Returns the preset name or nil if not found."
@@ -298,18 +320,6 @@ gemini, mono). Now with dark-neutral backgrounds and new :bg/:bg-panel/:bg-eleme
:dirty (list nil nil nil)))) :dirty (list nil nil nil))))
#+END_SRC #+END_SRC
** Sidebar panel definitions
#+BEGIN_SRC lisp :tangle /home/user/.local/share/passepartout/lisp/channel-tui-state.lisp
(defvar *sidebar-panels*
'((:id :gate-trace :title "Gate Trace" :width 28)
(:id :focus :title "Focus" :width 28)
(:id :rules :title "Rules" :width 28)
(:id :context :title "Context" :width 28)
(:id :cost :title "Cost" :width 28)
(:id :files :title "Files" :width 28))
"Sidebar panel definitions for cl-tty slot registrations.")
#+END_SRC
** Helpers ** Helpers
#+BEGIN_SRC lisp :tangle /home/user/.local/share/passepartout/lisp/channel-tui-state.lisp #+BEGIN_SRC lisp :tangle /home/user/.local/share/passepartout/lisp/channel-tui-state.lisp
(defun now () (defun now ()

View File

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