v0.8.0: TUI stabilization, command palette reverse-video highlight, hint bar redesign

- ROADMAP: consolidate all TUI work under v0.8.0 (removed premature
  v0.9.0/v0.10.x labels), restored original v0.9.0 eval harness plan
- channel-tui-view.org: Emacs-style reverse-video cursor (swap fg/bg
  instead of drawing █), hint bar now shows F:focus/MCP:count on left
  and token gauge + keybindings on right, sidebar reorganized to show
  GATE TRACE, RULES + BLOCK COUNT, COST, FILES panels
- channel-tui-main.org: command palette selection now uses reverse-video
  highlight (bg-input fg on input-fg bg, matching cursor style), fixed
  cond order so sel-p is checked before cat (all items had :category
  making sel-p unreachable), added session-cost extraction from daemon
- passepartout: export COLORTERM=truecolor for modern backend detection
This commit is contained in:
2026-05-17 15:37:40 -04:00
parent 2fedbbcb3b
commit e04b12c31c
53 changed files with 12406 additions and 197 deletions

View File

@@ -13,7 +13,7 @@
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 expanding light grey input box,
multi-line word-wrapped prompt, software blinking cursor (█),
multi-line word-wrapped prompt, Emacs-style reverse-video cursor,
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.
@@ -243,57 +243,93 @@ Returns a list of strings, one per line."
(setf cursor-line i
cursor-col (- pos accum)))
(incf accum (1+ len))))
;; 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))))))
;; Hint bar at h-2: F:/MCP: on left, token gauge + keybindings on right
(let* ((focal (or (st :foveal-id) "-"))
(focal-str (format nil "F:~a" focal))
(mcp-str (format nil "MCP:~d" (or (st :mcp-count) 0)))
(left-str (format nil "~a ~a" focal-str mcp-str))
(msg-count (max 1 (length (st :messages))))
(ctx-est (* msg-count 60))
(ctx-limit 8192)
(ctx-pct (min 100 (floor (* 100 ctx-est) ctx-limit)))
(ctx-tok (if (< ctx-est 1000)
(format nil "~d" ctx-est)
(format nil "~dK" (floor ctx-est 1000))))
(ctx-str (format nil "~a (~d%%)" ctx-tok ctx-pct))
(hint-str "ctrl+p | /help")
(ctx-fg (cond ((< ctx-pct 50) (theme-color :tool-done))
((< ctx-pct 80) (theme-color :input-prompt))
(t (theme-color :error))))
(hint-x (- chat-w (length hint-str) 2))
(ctx-x (- hint-x 1 (length ctx-str))))
(cl-tty.backend:draw-text fb hpad (- h 2) left-str hint-fg (theme-color :bg))
(cl-tty.backend:draw-text fb ctx-x (- h 2) ctx-str ctx-fg (theme-color :bg))
(cl-tty.backend:draw-text fb hint-x (- h 2) hint-str hint-fg (theme-color :bg))))))
#+end_src
** Sidebar
#+BEGIN_SRC lisp :tangle /home/user/.local/share/passepartout/lisp/channel-tui-view.lisp
(defun view-sidebar (fb w h)
"Render the right-side sidebar panel."
(let* ((w (or (and (numberp w) (> w 0) w) 80))
(h (or (and (numberp h) (> h 0) h) 24))
(x (- w (or (st :sidebar-width) 42)))
(bg-panel (theme-color :bg-panel))
(y 0))
;; Fill sidebar background (h-1 done separately to avoid scroll)
(cl-tty.backend:draw-rect fb x 0 (- w x) (1- h) :bg bg-panel)
(cl-tty.backend:draw-text fb x (1- h) (make-string (- w x) :initial-element #\Space) nil bg-panel)
;; Focus panel
(cl-tty.backend:draw-text fb (+ x 2) (incf y) "FOCUS" (theme-color :accent) bg-panel)
;; Gate Trace — from latest agent message
(cl-tty.backend:draw-text fb (+ x 2) (incf y) "GATE TRACE" (theme-color :accent) bg-panel)
(incf y)
(cl-tty.backend:draw-text fb (+ x 2) (incf y) (format nil " ~a" (or (st :foveal-id) "none"))
(theme-color :agent-fg) bg-panel)
(incf y 2)
;; Rules panel
(cl-tty.backend:draw-text fb (+ x 2) (incf y) "RULES" (theme-color :accent) bg-panel)
(incf y)
(cl-tty.backend:draw-text fb (+ x 2) (incf y) (format nil " ~d active" (or (st :rule-count) 0))
(theme-color :agent-fg) bg-panel)
(incf y 2)
;; Context panel — token gauge
(cl-tty.backend:draw-text fb (+ x 2) (incf y) "CONTEXT" (theme-color :accent) bg-panel)
(let* ((msg-count (max 1 (length (st :messages))))
(est (* msg-count 60))
(limit 8192)
(pct (min 100 (floor (* 100 est) limit)))
(bar-len (floor pct 10))
(bar (make-string bar-len :initial-element #\#)))
(cl-tty.backend:draw-text fb (+ x 2) (incf y)
(format nil " [~a~a]" bar
(make-string (- 10 bar-len) :initial-element #\Space))
(theme-color :dim) bg-panel)
(incf y)
(cl-tty.backend:draw-text fb (+ x 2) (incf y) (format nil " ~d%" pct)
(theme-color :status-fg) bg-panel)
(let* ((msgs (st :messages))
(last-gt (loop for i from (1- (length msgs)) downto 0
for m = (aref msgs i)
when (getf m :gate-trace)
return (getf m :gate-trace))))
(if last-gt
(dolist (g last-gt)
(let* ((name (getf g :gate))
(result (getf g :result))
(reason (getf g :reason))
(glyph (case result (:passed "✓") (:blocked "✗") (:approval "→") (t "?")))
(color (case result
(:passed (theme-color :tool-done))
(:blocked (theme-color :error))
(:approval (theme-color :input-prompt))
(t (theme-color :dim)))))
(cl-tty.backend:draw-text fb (+ x 2) (incf y) (format nil " ~a ~a" glyph name) color bg-panel)
(when reason
(incf y)
(cl-tty.backend:draw-text fb (+ x 4) (incf y) reason (theme-color :dim) bg-panel))))
(cl-tty.backend:draw-text fb (+ x 2) (incf y) " (none)" (theme-color :dim) bg-panel))
(incf y 2))
;; MCP panel
(cl-tty.backend:draw-text fb (+ x 2) (incf y) "MCP" (theme-color :accent) bg-panel)
;; Rules + Block Count
(let ((blocked (loop for i below (length (st :messages))
for m = (aref (st :messages) i)
sum (loop for g in (getf m :gate-trace)
count (eq (getf g :result) :blocked)))))
(cl-tty.backend:draw-text fb (+ x 2) (incf y) "RULES" (theme-color :accent) bg-panel)
(incf y)
(cl-tty.backend:draw-text fb (+ x 2) (incf y)
(format nil " ~d active" (or (st :rule-count) 0))
(theme-color :agent-fg) bg-panel)
(incf y)
(cl-tty.backend:draw-text fb (+ x 2) (incf y)
(format nil " ~d blocked" blocked)
(if (> blocked 0) (theme-color :error) (theme-color :dim)) bg-panel)
(incf y 2))
;; Cost
(cl-tty.backend:draw-text fb (+ x 2) (incf y) "COST" (theme-color :accent) bg-panel)
(incf y)
(cl-tty.backend:draw-text fb (+ x 2) (incf y) (format nil " ~d server~:p" (or (st :mcp-count) 0))
(theme-color :agent-fg) bg-panel)
;; Version footer at bottom with connection dot
(cl-tty.backend:draw-text fb (+ x 2) (incf y)
(format nil " $~,2f" (or (st :session-cost) 0.0))
(theme-color :status-fg) bg-panel)
(incf y 2)
;; Files (stub)
(cl-tty.backend:draw-text fb (+ x 2) (incf y) "FILES" (theme-color :accent) bg-panel)
(incf y)
(cl-tty.backend:draw-text fb (+ x 2) (incf y) " (not yet)" (theme-color :dim) bg-panel)
(incf y 2)
;; Version footer
(let* ((ver (or (st :daemon-version) ""))
(ver-label (if (> (length ver) 0) (format nil "passepartout ~a" ver) "passepartout"))
(dot (if (st :connected) "●" "○"))
@@ -320,17 +356,27 @@ Returns a list of strings, one per line."
(setf (st :dirty) (list nil nil nil))))
(defun position-cursor (fb w h)
"Draw a solid block cursor at the input insertion point."
"Draw cursor at the input insertion point using reverse video (Emacs-style).
The character under the cursor is redrawn with foreground and background
swapped. If the cursor is past the end of the input string, a reversed
space is drawn."
(let* ((sw (if (sidebar-visible-p w) (or (st :sidebar-width) 42) 0))
(cw (- w sw))
(hpad 2)
(text (input-string))
(text-len (length text))
(pos (or (st :cursor-pos) 0))
(prompt-w (- cw (* 2 hpad) 2))
(display-start (max 0 (- pos (1- prompt-w))))
(cx (+ hpad 2 (- pos display-start)))
(cy (- h 6)))
(cl-tty.backend:draw-text fb cx cy "█" (theme-color :input-prompt) nil)
(cy (- h 6))
(bg-i (theme-color :bg-input))
(input-fg (theme-color :input-fg)))
(if (< pos text-len)
(let ((ch (char text pos)))
(cl-tty.backend:draw-text fb cx cy (string ch) bg-i input-fg))
(cl-tty.backend:draw-text fb cx cy " " bg-i input-fg))
(finish-output (cl-tty.backend::backend-output-stream fb))))
#+END_SRC