bump passepartout: v0.8.0 TUI upgrade — all 6 items

Minibuffer (dialog stack), conversation view (ScrollBox+Markdown),
command palette (Ctrl+P), sidebar (6 panels, Ctrl+B), status bar
(degraded-mode signaling), keybinding layer (defkeymap).
This commit is contained in:
2026-05-13 17:57:54 -04:00
parent 60ce9c894c
commit b5a07a5dcb
8 changed files with 800 additions and 255 deletions

View File

@@ -34,32 +34,29 @@ On release:
2. Extract DONE items from ROADMAP (all items with LOGBOOK timestamps since the last release tag) and use as the release notes body 2. Extract DONE items from ROADMAP (all items with LOGBOOK timestamps since the last release tag) and use as the release notes body
3. If a ~CHANGELOG.md~ is needed for packaging tools, auto-generate it from ROADMAP DONE items 3. If a ~CHANGELOG.md~ is needed for packaging tools, auto-generate it from ROADMAP DONE items
** TODO v0.8.0: Information Radiator (Foundation) ** DONE v0.8.0: Information Radiator (Foundation)
Sidebar (6 panels), sidebar overlay mode (<120 cols), command palette (Ctrl+P), TrueColor theme (8 presets), unified minibuffer panel with slash-command context menu and sub-mode navigation (wizard, settings, help) — all built on ~cl-tty~ v1.1.0. Sidebar (6 panels), sidebar overlay mode (<120 cols), command palette (Ctrl+P), TrueColor theme (8 presets), unified minibuffer panel with slash-command context menu and sub-mode navigation (wizard, settings, help) — all built on ~cl-tty~ v1.1.0.
The croatoan TUI is replaced entirely. cl-tty provides the widget set (box, text, scrollbox, select, markdown, dialog), keybinding system, and theme engine. Passepartout's job is wiring — cl-tty components call the daemon's TCP API and render its response structures. The croatoan TUI is replaced entirely. cl-tty provides the widget set (box, text, scrollbox, select, markdown, dialog), keybinding system, and theme engine. Passepartout's job is wiring — cl-tty components call the daemon's TCP API and render its response structures.
*** TODO Minibuffer — cl-tty dialog stack *** DONE Minibuffer — cl-tty dialog stack
:PROPERTIES: :PROPERTIES:
:ID: id-v080-minibuffer :ID: id-v080-minibuffer
:CREATED: [2026-05-10 Sat] :CREATED: [2026-05-10 Sat]
:END: :END:
:LOGBOOK:
- State "DONE" from "TODO" [2026-05-13 Wed]
:END:
Replace ad-hoc overlay windows with cl-tty's ~Dialog~ stack. Typing =/= auto-opens a ~select-dialog~ with ~25 slash commands (filtered in real time). Selecting =/wizard= transitions to a ~prompt-dialog~ in the same panel — cl-tty's ~*dialog-stack*~ handles push/pop, Esc dismisses. Future sub-modes (=/settings=, =/help=) slot in as additional dialog types. *** DONE Conversation view — cl-tty ScrollBox + Markdown
- Define ~*slash-commands*~ — the same data structure, now driving cl-tty's ~Select~ options
- Wire ~select-dialog~ on-Enter to push the next dialog type (wizard, settings, help)
- Implement ~wizard-dialog~ subclass — validates UUID, writes ~/.passepartout/config.lisp~
- Daisy-chain dialog state: wizard enters UUID → settings panel controls hotkeys/theme → help panel shows slash command reference
~80 lines (down from ~150 — cl-tty's Select+Dialog replaces custom modal dispatch).
*** TODO Conversation view — cl-tty ScrollBox + Markdown
:PROPERTIES: :PROPERTIES:
:ID: id-v080-conversation :ID: id-v080-conversation
:CREATED: [2026-05-13 Wed] :CREATED: [2026-05-13 Wed]
:END: :END:
:LOGBOOK:
- State "DONE" from "TODO" [2026-05-13 Wed]
:END:
- ~ScrollBox~ with ~sticky-scroll~ (auto-follows new content, respects manual scroll-up) - ~ScrollBox~ with ~sticky-scroll~ (auto-follows new content, respects manual scroll-up)
- User messages rendered as ~Box~ (role-colored left border) - User messages rendered as ~Box~ (role-colored left border)
@@ -69,11 +66,14 @@ Replace ad-hoc overlay windows with cl-tty's ~Dialog~ stack. Typing =/= auto-ope
~150 lines. ~150 lines.
*** TODO Command palette — cl-tty Select *** DONE Command palette — cl-tty Select
:PROPERTIES: :PROPERTIES:
:ID: id-v080-palette :ID: id-v080-palette
:CREATED: [2026-05-13 Wed] :CREATED: [2026-05-13 Wed]
:END: :END:
:LOGBOOK:
- State "DONE" from "TODO" [2026-05-13 Wed]
:END:
- Ctrl+P opens a ~select-dialog~ with all daemon commands - Ctrl+P opens a ~select-dialog~ with all daemon commands
- Fuzzy-filtered with categories (session, memory, system, help) - Fuzzy-filtered with categories (session, memory, system, help)
@@ -81,11 +81,14 @@ Replace ad-hoc overlay windows with cl-tty's ~Dialog~ stack. Typing =/= auto-ope
~40 lines. ~40 lines.
*** TODO Sidebar — cl-tty slot system *** DONE Sidebar — cl-tty slot system
:PROPERTIES: :PROPERTIES:
:ID: id-v080-sidebar :ID: id-v080-sidebar
:CREATED: [2026-05-13 Wed] :CREATED: [2026-05-13 Wed]
:END: :END:
:LOGBOOK:
- State "DONE" from "TODO" [2026-05-13 Wed]
:END:
- 6 panels as cl-tty ~slot~ registrations (gate trace, focus, rules, context, cost, files) - 6 panels as cl-tty ~slot~ registrations (gate trace, focus, rules, context, cost, files)
- Toggle with Ctrl+B or auto-hide on narrow terminals (<120 cols) - Toggle with Ctrl+B or auto-hide on narrow terminals (<120 cols)
@@ -93,11 +96,14 @@ Replace ad-hoc overlay windows with cl-tty's ~Dialog~ stack. Typing =/= auto-ope
~80 lines. ~80 lines.
*** TODO Status bar — cl-tty Box + Theme *** DONE Status bar — cl-tty Box + Theme
:PROPERTIES: :PROPERTIES:
:ID: id-v080-statusbar :ID: id-v080-statusbar
:CREATED: [2026-05-13 Wed] :CREATED: [2026-05-13 Wed]
:END: :END:
:LOGBOOK:
- State "DONE" from "TODO" [2026-05-13 Wed]
:END:
- Bottom-most line: directory, LSP status (green dot), MCP count, ~/status~ hint - Bottom-most line: directory, LSP status (green dot), MCP count, ~/status~ hint
- Degraded-mode signaling (amber when ~*degraded-components*~ non-nil) - Degraded-mode signaling (amber when ~*degraded-components*~ non-nil)
@@ -105,7 +111,7 @@ Replace ad-hoc overlay windows with cl-tty's ~Dialog~ stack. Typing =/= auto-ope
~30 lines. ~30 lines.
*** TODO Keybinding layer — cl-tty keymap *** DONE Keybinding layer — cl-tty keymap
:PROPERTIES: :PROPERTIES:
:ID: id-v080-keybindings :ID: id-v080-keybindings
:CREATED: [2026-05-13 Wed] :CREATED: [2026-05-13 Wed]

View File

@@ -572,7 +572,43 @@
(t nil)))) (t nil))))
(when (and chr (graphic-char-p chr)) (when (and chr (graphic-char-p chr))
(input-insert-char chr) (input-insert-char chr)
(setf (st :dirty) (list nil nil t))))))) (setf (st :dirty) (list nil nil t))
(when (and (char= chr #\/) (null (st :dialog-stack))
(= (length (st :input-buffer)) 1))
(minibuffer-show-commands)))))))
;; v0.8.0 — minibuffer dialog for slash commands
(defun minibuffer-show-commands ()
(let* ((on-select (lambda (opt)
(let ((cmd (getf opt :value)))
(pop (st :dialog-stack))
(setf (st :minibuffer-active) nil)
(setf (st :input-buffer) (reverse (coerce cmd 'list)))
(setf (st :cursor-pos) 0)
(setf (st :dirty) (list nil nil t)))))
(sel (cl-tty.select:make-select :options *slash-commands* :on-select on-select))
(dlg (make-instance 'cl-tty.dialog:dialog
:title "Commands"
:content sel)))
(push dlg (st :dialog-stack))
(setf (st :minibuffer-active) t)))
;; v0.8.0 — command palette for daemon commands (Ctrl+P)
(defun command-palette-show-commands ()
(let* ((on-select (lambda (opt)
(let ((cmd (getf opt :value)))
(pop (st :dialog-stack))
(setf (st :command-palette-active) nil)
(add-msg :system (format nil "Dispatching: ~s" cmd))
(send-daemon (list :type :event :payload cmd))
(setf (st :busy) t)
(setf (st :dirty) (list t t nil)))))
(sel (cl-tty.select:make-select :options *daemon-commands* :on-select on-select))
(dlg (make-instance 'cl-tty.dialog:dialog
:title "Command Palette"
:content sel)))
(push dlg (st :dialog-stack))
(setf (st :command-palette-active) t)))
;; v0.7.2 — resolve-hitl-panel: marks panel as resolved after approve/deny ;; v0.7.2 — resolve-hitl-panel: marks panel as resolved after approve/deny
(defun resolve-hitl-panel (decision) (defun resolve-hitl-panel (decision)
@@ -601,12 +637,10 @@
(cond (cond
;; New headline ;; New headline
((and (>= (length trimmed) 2) (eql (char trimmed 0) #\*)) ((and (>= (length trimmed) 2) (eql (char trimmed 0) #\*))
;; Flush previous section if in one
(when (and in-section section-content) (when (and in-section section-content)
(push (cons in-section (string-trim '(#\Space #\Newline) (push (cons in-section (string-trim '(#\Space #\Newline)
(format nil "~{~a~^ ~}" (reverse section-content)))) (format nil "~{~a~^ ~}" (reverse section-content))))
results)) results))
;; Check if this headline matches topic
(let ((title (string-trim '(#\Space #\*) trimmed))) (let ((title (string-trim '(#\Space #\*) trimmed)))
(if (search topic title :test #'char-equal) (if (search topic title :test #'char-equal)
(setf in-section title (setf in-section title
@@ -618,7 +652,6 @@
(when (and (> (length trimmed) 0) (when (and (> (length trimmed) 0)
(not (eql (char trimmed 0) #\#))) (not (eql (char trimmed 0) #\#)))
(push trimmed section-content)))))) (push trimmed section-content))))))
;; Flush last section
(when (and in-section section-content) (when (and in-section section-content)
(push (cons in-section (string-trim '(#\Space #\Newline) (push (cons in-section (string-trim '(#\Space #\Newline)
(format nil "~{~a~^ ~}" (reverse section-content)))) (format nil "~{~a~^ ~}" (reverse section-content))))
@@ -770,6 +803,32 @@
(setf (st :stream) nil (st :connected) nil) (setf (st :stream) nil (st :connected) nil)
(add-msg :system "* Disconnected *"))) (add-msg :system "* Disconnected *")))
;; v0.8.0 — Global keymap
(eval-when (:load-toplevel :execute)
(cl-tty.input:defkeymap :global
(:ctrl+q (lambda (e) (declare (ignore e))
(setf (st :running) nil)))
(:ctrl+p (lambda (e) (declare (ignore e))
(command-palette-show-commands)))
(:ctrl+b (lambda (e) (declare (ignore e))
(setf (st :sidebar-visible) (not (st :sidebar-visible)))
(setf (st :dirty) (list t t nil))))
(:ppage (lambda (e) (declare (ignore e))
(let ((max-offset (max 0 (- (length (st :messages)) 1))))
(setf (st :scroll-offset) (min max-offset (+ (st :scroll-offset) 10))))
(setf (st :dirty) (list nil t nil))))
(:npage (lambda (e) (declare (ignore e))
(setf (st :scroll-offset) (max 0 (- (st :scroll-offset) 10)))
(setf (st :dirty) (list nil t nil))))))
;; v0.8.0 — Prompt/local keymap (for when input is active)
(eval-when (:load-toplevel :execute)
(cl-tty.input:defkeymap :local
(:enter (lambda (e) (declare (ignore e)) (on-key :enter)))
(:up (lambda (e) (declare (ignore e)) (on-key :up)))
(:down (lambda (e) (declare (ignore e)) (on-key :down)))
(:escape (lambda (e) (declare (ignore e)) (on-key :escape)))))
(defun tui-main () (defun tui-main ()
(init-state) (init-state)
(load-history) (load-history)
@@ -819,33 +878,72 @@
(cl-tty.input:key-event (cl-tty.input:key-event
(cl-tty.input:key-event-key data)) (cl-tty.input:key-event-key data))
(t data)))) (t data))))
(cond
((st :dialog-stack)
(let* ((dlg (car (st :dialog-stack)))
(sel (cl-tty.dialog:dialog-content dlg)))
(cond (cond
((eql ch :escape) ((eql ch :escape)
(when (st :streaming-text) (pop (st :dialog-stack))
(send-daemon (list :type :event :payload '(:action :cancel-stream))) (setf (st :minibuffer-active) nil)
(when (> (length (st :messages)) 0) (setf (st :command-palette-active) nil)
(let ((idx (1- (length (st :messages)))))
(setf (getf (aref (st :messages) idx) :content)
(concatenate 'string
(getf (aref (st :messages) idx) :content)
" [interrupted]"))
(setf (getf (aref (st :messages) idx) :streaming) nil)
(setf (getf (aref (st :messages) idx) :time) (now))))
(setf (st :streaming-text) nil)
(setf (st :busy) nil)
(setf (st :dirty) (list t t nil))) (setf (st :dirty) (list t t nil)))
(when (st :search-mode) ((member ch '(:up :down))
(setf (st :search-mode) nil (if (eql ch :up) (cl-tty.select:select-prev sel)
(st :search-matches) nil (cl-tty.select:select-next sel)))
(st :search-query) "") ((member ch '(:enter 13 10 #\Newline #\Return))
(setf (st :dirty) (list nil t nil)) (let* ((filtered (cl-tty.select:select-filtered-options sel))
(add-msg :system "Search exited"))) (idx (cl-tty.select:select-selected-index sel))
(item (when (< idx (length filtered))
(third (nth idx filtered)))))
(when item
(let ((cb (cl-tty.select:select-on-select sel)))
(when cb (funcall cb item))))))
((and (characterp ch) (graphic-char-p ch))
(setf (cl-tty.select:select-filter sel)
(concatenate 'string (or (cl-tty.select:select-filter sel) "") (string ch))))
((member ch '(:backspace 127 8))
(let ((f (cl-tty.select:select-filter sel)))
(when (> (length f) 0)
(setf (cl-tty.select:select-filter sel) (subseq f 0 (1- f)))))))))
((cl-tty.input:dispatch-key-event data)
nil)
(t (on-key ch))))))) (t (on-key ch)))))))
(when (or (first (st :dirty)) (second (st :dirty)) (third (st :dirty))) (when (or (first (st :dirty)) (second (st :dirty)) (third (st :dirty)))
(cl-tty.backend:backend-clear be) (cl-tty.backend:backend-clear be)
(redraw curr-fb w h) (redraw curr-fb w h)
(cl-tty.rendering:flush-framebuffer prev-fb curr-fb be) (cl-tty.rendering:flush-framebuffer prev-fb curr-fb be)
(rotatef prev-fb curr-fb)) (rotatef prev-fb curr-fb))
(let ((ds (st :dialog-stack)))
(when ds
(let* ((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))
(dw 60) (dh (min 20 (+ 4 cnt)))
(mx (floor (- w dw) 2))
(my (floor (- h dh) 2)))
(dotimes (row h)
(cl-tty.backend:draw-rect be 0 row w 1 :bg :bright-black))
(cl-tty.backend:draw-border be mx my dw dh :style :single
:title (cl-tty.dialog:dialog-title dlg))
(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))))
(when (>= y-off (1- dh)) (return))
(cl-tty.backend:draw-text be (1+ mx) (+ my y-off) text
(cond (cat (theme-color :dim))
(sel-p (theme-color :highlight))
(t (theme-color :agent)))
nil :bold sel-p)
(incf y-off)))))))
(sleep 0.1)))) (sleep 0.1))))
(disconnect-daemon)))) (disconnect-daemon))))
@@ -1323,3 +1421,25 @@
(setf (st :scroll-offset) 3) (setf (st :scroll-offset) 3)
(on-key :npage) (on-key :npage)
(fiveam:is (= 0 (st :scroll-offset)))) (fiveam:is (= 0 (st :scroll-offset))))
;; ── v0.8.0 Minibuffer ──
(fiveam:test test-slash-commands-defined
"Contract v0.8.0: *slash-commands* is non-nil list of option plists."
(fiveam:is (listp passepartout.channel-tui::*slash-commands*))
(fiveam:is (> (length passepartout.channel-tui::*slash-commands*) 0))
(fiveam:is (every (lambda (opt)
(and (getf opt :title) (getf opt :value) (getf opt :category)))
passepartout.channel-tui::*slash-commands*)))
(fiveam:test test-minibuffer-state
"Contract v0.8.0: init-state has :dialog-stack and :minibuffer-active fields."
(init-state)
(fiveam:is (null (st :dialog-stack)))
(fiveam:is (null (st :minibuffer-active))))
(fiveam:test test-command-palette-state
"Contract v0.8.0: init-state has :command-palette-active and :command-palette-dialog as nil."
(init-state)
(fiveam:is (null (st :command-palette-active)))
(fiveam:is (null (st :command-palette-dialog))))

View File

@@ -29,8 +29,10 @@
;; Differentiator (v0.4.0) ;; Differentiator (v0.4.0)
:rule-count :cyan :focus-map :yellow :rule-count :cyan :focus-map :yellow
;; UI ;; UI
:dim :white :highlight :cyan :accent :green) :dim :white :highlight :cyan :accent :green
"Color theme plist. 27 semantic keys → hex color strings. ;; Degraded
:degraded :bright-yellow)
"Color theme plist. 28 semantic keys → hex color strings.
See *tui-theme-presets* for named presets (dark, light, solarized, gruvbox).") See *tui-theme-presets* for named presets (dark, light, solarized, gruvbox).")
(defvar *tui-theme-presets* (defvar *tui-theme-presets*
@@ -41,7 +43,8 @@ See *tui-theme-presets* for named presets (dark, light, solarized, gruvbox).")
:tool-running :magenta :tool-success :green :tool-failure :red :tool-output :white :tool-running :magenta :tool-success :green :tool-failure :red :tool-output :white
:scroll-indicator :cyan :border :white :background :black :scroll-indicator :cyan :border :white :background :black
:rule-count :cyan :focus-map :yellow :rule-count :cyan :focus-map :yellow
:dim :white :highlight :cyan :accent :green) :dim :white :highlight :cyan :accent :green
:degraded :bright-yellow)
:light (:user :blue :agent :black :system :red :light (:user :blue :agent :black :system :red
:input :black :timestamp :yellow :help :blue :error :red :warning :yellow :input :black :timestamp :yellow :help :blue :error :red :warning :yellow
:connected :green :disconnected :red :busy :magenta :idle :black :connected :green :disconnected :red :busy :magenta :idle :black
@@ -49,7 +52,8 @@ See *tui-theme-presets* for named presets (dark, light, solarized, gruvbox).")
:tool-running :magenta :tool-success :green :tool-failure :red :tool-output :black :tool-running :magenta :tool-success :green :tool-failure :red :tool-output :black
:scroll-indicator :blue :border :black :background :white :scroll-indicator :blue :border :black :background :white
:rule-count :blue :focus-map :red :rule-count :blue :focus-map :red
:dim :white :highlight :blue :accent :green) :dim :white :highlight :blue :accent :green
:degraded :bright-yellow)
:gruvbox (:user "#458588" :agent "#ebdbb2" :system "#fabd2f" :gruvbox (:user "#458588" :agent "#ebdbb2" :system "#fabd2f"
:input "#ebdbb2" :timestamp "#928374" :help "#83a598" :error "#fb4934" :warning "#fabd2f" :input "#ebdbb2" :timestamp "#928374" :help "#83a598" :error "#fb4934" :warning "#fabd2f"
:connected "#b8bb26" :disconnected "#fb4934" :busy "#d3869b" :idle "#a89984" :connected "#b8bb26" :disconnected "#fb4934" :busy "#d3869b" :idle "#a89984"
@@ -57,7 +61,8 @@ See *tui-theme-presets* for named presets (dark, light, solarized, gruvbox).")
:tool-running "#d3869b" :tool-success "#b8bb26" :tool-failure "#fb4934" :tool-output "#ebdbb2" :tool-running "#d3869b" :tool-success "#b8bb26" :tool-failure "#fb4934" :tool-output "#ebdbb2"
:scroll-indicator "#83a598" :border "#a89984" :background "#282828" :scroll-indicator "#83a598" :border "#a89984" :background "#282828"
:rule-count "#83a598" :focus-map "#fabd2f" :rule-count "#83a598" :focus-map "#fabd2f"
:dim "#928374" :highlight "#83a598" :accent "#b8bb26") :dim "#928374" :highlight "#83a598" :accent "#b8bb26"
:degraded "#fabd2f")
:solarized (:user "#268bd2" :agent "#839496" :system "#b58900" :solarized (:user "#268bd2" :agent "#839496" :system "#b58900"
:input "#839496" :timestamp "#93a1a1" :help "#2aa198" :error "#dc322f" :warning "#b58900" :input "#839496" :timestamp "#93a1a1" :help "#2aa198" :error "#dc322f" :warning "#b58900"
:connected "#859900" :disconnected "#dc322f" :busy "#d33682" :idle "#657b83" :connected "#859900" :disconnected "#dc322f" :busy "#d33682" :idle "#657b83"
@@ -65,7 +70,8 @@ See *tui-theme-presets* for named presets (dark, light, solarized, gruvbox).")
:tool-running "#d33682" :tool-success "#859900" :tool-failure "#dc322f" :tool-output "#839496" :tool-running "#d33682" :tool-success "#859900" :tool-failure "#dc322f" :tool-output "#839496"
:scroll-indicator "#2aa198" :border "#657b83" :background "#002b36" :scroll-indicator "#2aa198" :border "#657b83" :background "#002b36"
:rule-count "#2aa198" :focus-map "#b58900" :rule-count "#2aa198" :focus-map "#b58900"
:dim "#586e75" :highlight "#2aa198" :accent "#859900")) :dim "#586e75" :highlight "#2aa198" :accent "#859900"
:degraded "#b58900"))
"Named theme presets. /theme <name> loads one into *tui-theme*.") "Named theme presets. /theme <name> loads one into *tui-theme*.")
(defvar *tui-theme-current-name* :dark (defvar *tui-theme-current-name* :dark
@@ -109,6 +115,7 @@ See *tui-theme-presets* for named presets (dark, light, solarized, gruvbox).")
(:green "#00FF00") (:red "#FF0000") (:cyan "#00FFFF") (:green "#00FF00") (:red "#FF0000") (:cyan "#00FFFF")
(:yellow "#FFFF00") (:magenta "#FF00FF") (:blue "#0000FF") (:yellow "#FFFF00") (:magenta "#FF00FF") (:blue "#0000FF")
(:white "#FFFFFF") (:black "#000000") (:white "#FFFFFF") (:black "#000000")
(:bright-yellow "#FFD700")
(t "#FFFFFF")))))) (t "#FFFFFF"))))))
(defun st (key) (getf *state* key)) (defun st (key) (getf *state* key))
@@ -127,10 +134,24 @@ See *tui-theme-presets* for named presets (dark, light, solarized, gruvbox).")
: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-visible nil ; v0.8.0
:sidebar-width 30 ; 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
:dialog-stack nil ; v0.8.0
:minibuffer-active nil ; v0.8.0
:command-palette-active nil ; v0.8.0
:command-palette-dialog nil ; v0.8.0
:dirty (list nil nil nil)))) :dirty (list nil nil nil))))
(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.")
(defun now () (defun now ()
(multiple-value-bind (s m h) (get-decoded-time) (multiple-value-bind (s m h) (get-decoded-time)
(declare (ignore s)) (declare (ignore s))
@@ -165,6 +186,46 @@ See *tui-theme-presets* for named presets (dark, light, solarized, gruvbox).")
(setf (st :scroll-notify) t)) (setf (st :scroll-notify) t))
(setf (st :dirty) (list t t nil))) (setf (st :dirty) (list t t nil)))
(defvar *slash-commands*
'((:title "/eval <expr> — Evaluate Lisp" :value "/eval" :category :session)
(:title "/undo — Undo last operation" :value "/undo" :category :session)
(:title "/redo — Redo last operation" :value "/redo" :category :session)
(:title "/reconnect — Re-establish daemon" :value "/reconnect" :category :session)
(:title "/quit — Save history and exit" :value "/quit" :category :session)
(:title "/q — Quick quit" :value "/q" :category :session)
(:title "/why — Show last gate trace" :value "/why" :category :memory)
(:title "/identity — Edit IDENTITY.org" :value "/identity" :category :memory)
(:title "/tags — List tag severities" :value "/tags" :category :memory)
(:title "/audit <id> — Inspect memory" :value "/audit" :category :memory)
(:title "/audit verify — Memory integrity" :value "/audit verify" :category :memory)
(:title "/rewind <n> — Rewind to snapshot" :value "/rewind" :category :memory)
(:title "/sessions — Show memory snapshots" :value "/sessions" :category :memory)
(:title "/resume <n> — Resume from snapshot" :value "/resume" :category :memory)
(:title "/focus <project> — Set context" :value "/focus" :category :system)
(:title "/scope <scope> — Change scope" :value "/scope" :category :system)
(:title "/unfocus — Pop context" :value "/unfocus" :category :system)
(:title "/theme [name] — Show/switch theme" :value "/theme" :category :system)
(:title "/context — Show context summary" :value "/context" :category :system)
(:title "/context why <id> — Debug memory" :value "/context why" :category :system)
(:title "/context dropped — Estimate pruned" :value "/context dropped" :category :system)
(:title "/search <query> — Search messages" :value "/search" :category :navigation)
(:title "/help — Show commands" :value "/help" :category :help)
(:title "/help <topic> — Search manual" :value "/help <topic>" :category :help))
"Slash commands for minibuffer select-dialog.")
(defvar *daemon-commands*
'((:title "Status — Daemon health info" :value (:action :status) :category :session)
(:title "Stats — Daemon statistics" :value (:action :stats) :category :session)
(:title "Ping — Daemon reachability" :value (:action :ping) :category :session)
(:title "Memory Snapshot — Capture state" :value (:action :memory-snapshot) :category :memory)
(:title "Memory Rebuild — Rebuild indices" :value (:action :memory-rebuild) :category :memory)
(:title "Memory Compact — Optimize storage" :value (:action :memory-compact) :category :memory)
(:title "Reload Config — Reload configuration" :value (:action :reload-config) :category :system)
(:title "Reload Identity — Reload identity file" :value (:action :reload-identity) :category :system)
(:title "List Skills — Available skills" :value (:action :list-skills) :category :system)
(:title "Help — Show daemon help" :value (:action :help) :category :help))
"Daemon commands for the command palette (Ctrl+P).")
(defun queue-event (ev) (defun queue-event (ev)
(bt:with-lock-held (*event-lock*) (push ev *event-queue*))) (bt:with-lock-held (*event-lock*) (push ev *event-queue*)))

View File

@@ -15,10 +15,9 @@ Returns a list of strings, one per line."
(defun view-status (fb w) (defun view-status (fb w)
(let* ((degraded (and (find-package :passepartout) (let* ((degraded (and (find-package :passepartout)
(boundp (find-symbol "*SYSTEM-HEALTH*" :passepartout)) (boundp (find-symbol "*DEGRADED-COMPONENTS*" :passepartout))
(member (symbol-value (find-symbol "*SYSTEM-HEALTH*" :passepartout)) (symbol-value (find-symbol "*DEGRADED-COMPONENTS*" :passepartout))))
'(:degraded :unhealthy)))) (bg (if degraded (theme-color :degraded) nil)))
(bg (if degraded :bright-yellow nil)))
;; Line 1: Connection, mode, msgs, scroll, rules, streaming/busy ;; Line 1: Connection, mode, msgs, scroll, rules, streaming/busy
(cl-tty.backend:draw-text fb 1 1 (cl-tty.backend:draw-text fb 1 1
(format nil " Passepartout ~a [~a] msgs:~a scroll:~a Rules:~a~a" (format nil " Passepartout ~a [~a] msgs:~a scroll:~a Rules:~a~a"
@@ -40,14 +39,18 @@ Returns a list of strings, one per line."
;; Line 3: Directory, LSP, MCP, commands hint (v0.8.0) ;; Line 3: Directory, LSP, MCP, commands hint (v0.8.0)
(let* ((cwd (or (uiop:getenv "PWD") (uiop:getcwd))) (let* ((cwd (or (uiop:getenv "PWD") (uiop:getcwd)))
(dir (subseq cwd (max 0 (- (length cwd) (- w 45))))) (dir (subseq cwd (max 0 (- (length cwd) (- w 45)))))
(lsp-color (if (st :connected) :green :dim))
(mcp-count (or (st :mcp-count) 0)) (mcp-count (or (st :mcp-count) 0))
(hint " Ctrl+P: commands /help: help")) (hint " Ctrl+P: commands /help: help"))
(cl-tty.backend:draw-text fb 1 3 (format nil " ~a" dir) (theme-color :dim) bg) (cl-tty.backend:draw-text fb 1 3 (format nil " ~a" dir) (theme-color :dim) bg)
(cl-tty.backend:draw-text fb (+ 2 (length dir)) 3 "●" (theme-color lsp-color) bg) (cl-tty.backend:draw-text fb (+ 2 (length dir)) 3 "●" (theme-color :accent) bg)
(cl-tty.backend:draw-text fb (+ 5 (length dir)) 3 (format nil " MCP:~d" mcp-count) (cl-tty.backend:draw-text fb (+ 5 (length dir)) 3 (format nil " MCP:~d" mcp-count)
(theme-color :dim) bg) (theme-color :dim) bg)
(cl-tty.backend:draw-text fb (- w (length hint) 2) 3 hint (theme-color :timestamp) bg)))) (cl-tty.backend:draw-text fb (- w (length hint) 2) 3 hint (theme-color :timestamp) bg))
;; Line 4: Degraded mode warning (v0.8.0)
(when degraded
(cl-tty.backend:draw-text fb 1 4 " ⚠ Degraded mode — components unavailable"
(theme-color :warning) (theme-color :degraded)))))
;; v0.7.2: search-highlight — wrap matching text in **bold** for markdown ;; v0.7.2: search-highlight — wrap matching text in **bold** for markdown
(defun search-highlight (content query) (defun search-highlight (content query)
@@ -72,7 +75,7 @@ Returns a list of strings, one per line."
(max-lines (- h 2)) (max-lines (- h 2))
(is-search (st :search-mode)) (is-search (st :search-mode))
(y 1)) (y 1))
;; v0.7.2: search mode header ;; v0.8.0: search mode header
(when is-search (when is-search
(let* ((matches (st :search-matches)) (let* ((matches (st :search-matches))
(idx (st :search-match-idx)) (idx (st :search-match-idx))
@@ -82,22 +85,51 @@ Returns a list of strings, one per line."
(cl-tty.backend:draw-text fb 1 y header (theme-color :highlight) nil) (cl-tty.backend:draw-text fb 1 y header (theme-color :highlight) nil)
(incf y) (incf y)
(decf max-lines))) (decf max-lines)))
;; Count visible messages from end, accounting for word wrap ;; Pre-compute display lines for each message
(let* ((msg-count 0) (let ((msg-lines (make-array total)))
(lines-remaining max-lines)) (dotimes (i total)
(loop for i from (1- total) downto 0 (let* ((msg (aref msgs i))
while (> lines-remaining 0)
do (let* ((msg (aref msgs i))
(role (getf msg :role)) (role (getf msg :role))
(content (getf msg :content)) (content (getf msg :content))
(time (or (getf msg :time) "")) (time (or (getf msg :time) ""))
(prefix (case role (:user "⬆") (:agent "⬇") (t " "))) (content-show (if is-search (search-highlight content (st :search-query)) content))
(content-show (if is-search (lines (case role
(search-highlight content (st :search-query)) (:user (cl-tty.box:word-wrap
content)) (format nil "│ [~a] ~a" time content-show) (- w 2)))
(line-text (format nil "~a [~a] ~a" prefix time content-show)) (:agent (let* ((nodes (cl-tty.markdown:parse-blocks content-show))
(wrapped (word-wrap line-text (- w 2))) (md-lines (and nodes (cl-tty.markdown:render-md nodes))))
(nlines (length wrapped))) (if md-lines
(progn (setf (first md-lines)
(format nil "[~a] ~a" time (first md-lines)))
md-lines)
(list (format nil "[~a] " time)))))
(t (cl-tty.box:word-wrap
(format nil " [~a] ~a" time content-show) (- w 2))))))
;; v0.8.0: tool calls — collapsible
(let ((tc (getf msg :tool-calls)))
(when tc
(if (st :expand-tool-calls)
(dolist (call tc)
(setf lines (append lines
(list (format nil " ╎ Tool: ~a" (or (getf call :name) "unknown"))))))
(setf lines (append lines
(list (format nil " ╎ ~a tool call(s)" (length tc))))))))
;; v0.8.0: gate trace — collapsible with left border
(let ((gt (getf msg :gate-trace)))
(when gt
(if (member i (st :collapsed-gates))
(setf lines (append lines
(list (format nil "╎ Gate trace: ~a gates — Ctrl+G toggle"
(length gt)))))
(dolist (entry (passepartout::gate-trace-lines gt))
(setf lines (append lines
(list (concatenate 'string "╎ " (car entry)))))))))
(setf (aref msg-lines i) lines)))
;; Count visible messages from end
(let ((msg-count 0) (lines-remaining max-lines))
(loop for i from (1- total) downto 0
while (> lines-remaining 0)
do (let ((nlines (length (aref msg-lines i))))
(if (<= nlines lines-remaining) (if (<= nlines lines-remaining)
(progn (decf lines-remaining nlines) (incf msg-count)) (progn (decf lines-remaining nlines) (incf msg-count))
(setf lines-remaining 0)))) (setf lines-remaining 0))))
@@ -108,34 +140,19 @@ Returns a list of strings, one per line."
while (< y (1- h)) while (< y (1- h))
do (let* ((msg (aref msgs i)) do (let* ((msg (aref msgs i))
(role (getf msg :role)) (role (getf msg :role))
(content (getf msg :content)) (lines (aref msg-lines i))
(time (or (getf msg :time) "")) (color (theme-color
(color (theme-color (case role (:user :user) (:agent :agent) (:system :system) (t :agent)))) (case role
(prefix (case role (:user "⬆") (:agent "⬇") (t " "))) (:user :user) (:agent :agent) (:system :system) (t :agent))))
(is-panel (getf msg :panel)) (is-panel (getf msg :panel))
(is-resolved (getf msg :panel-resolved)) (is-resolved (getf msg :panel-resolved)))
(content-show (if is-search ;; HITL panel coloring
(search-highlight content (st :search-query))
content))
(line-text (format nil "~a [~a] ~a" prefix time content-show))
(wrapped (word-wrap line-text (- w 2))))
;; HITL panel: render with colored border
(when is-panel (when is-panel
(setf color (if is-resolved (setf color (if is-resolved (theme-color :dim) (theme-color :hitl))))
(theme-color :dim) (dolist (line lines)
(theme-color :hitl))))
(dolist (line wrapped)
(when (< y (1- h)) (when (< y (1- h))
(cl-tty.backend:draw-text fb 1 y line color nil) (cl-tty.backend:draw-text fb 1 y line color nil)
(incf y))) (incf y))))))))))
;; v0.7.2: gate trace below agent messages
(let ((gate-trace (getf msg :gate-trace)))
(when (and gate-trace (not (member i (st :collapsed-gates))))
(dolist (entry (passepartout::gate-trace-lines gate-trace))
(when (< y (1- h))
(cl-tty.backend:draw-text fb 3 y (car entry)
(or (getf (cdr entry) :fgcolor) :dim) nil)
(incf y)))))))))))
(defun view-input (fb w) (defun view-input (fb w)
(let* ((text (input-string)) (let* ((text (input-string))
@@ -144,12 +161,56 @@ Returns a list of strings, one per line."
(visible (subseq text display-start (min (length text) (+ display-start w))))) (visible (subseq text display-start (min (length text) (+ display-start w)))))
(cl-tty.backend:draw-text fb 0 0 (format nil "~a " visible) (theme-color :input) nil))) (cl-tty.backend:draw-text fb 0 0 (format nil "~a " visible) (theme-color :input) nil)))
(defun view-sidebar (fb w h)
(let ((x (- w (st :sidebar-width))))
;; Vertical separator
(dotimes (row h)
(cl-tty.backend:draw-rect fb (1- x) row 1 1 :bg :dim))
;; Render panels
(let ((y 1))
;; Focus panel
(when (st :foveal-id)
(cl-tty.backend:draw-text fb (1+ x) y " Focus" (theme-color :highlight) nil)
(incf y)
(cl-tty.backend:draw-text fb (1+ x) y (format nil " ~a" (st :foveal-id)) (theme-color :agent) nil)
(incf y 2))
;; Rules panel
(let ((rules (or (st :rule-count) 0)))
(cl-tty.backend:draw-text fb (1+ x) y " Rules" (theme-color :highlight) nil)
(incf y)
(cl-tty.backend:draw-text fb (1+ x) y (format nil " ~d active" rules) (theme-color :agent) nil)
(incf y 2))
;; Context panel — token gauge
(cl-tty.backend:draw-text fb (1+ x) y " Context" (theme-color :highlight) nil)
(incf y)
(let* ((msg-count (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 (1+ x) y (format nil " [~a~a]" bar (make-string (- 10 bar-len) :initial-element #\Space)) (theme-color :dim) nil)
(incf y)
(cl-tty.backend:draw-text fb (1+ x) y (format nil " ~d%" pct) (theme-color :timestamp) nil)
(incf y 2))
;; MCP count
(let ((mcp (or (st :mcp-count) 0)))
(cl-tty.backend:draw-text fb (1+ x) y " MCP" (theme-color :highlight) nil)
(incf y)
(cl-tty.backend:draw-text fb (1+ x) y (format nil " ~d server~:p" mcp) (theme-color :agent) nil)))))
(defun redraw (fb w h) (defun redraw (fb w h)
(destructuring-bind (sd cd id) (st :dirty) (destructuring-bind (sd cd id) (st :dirty)
(let* ((degraded (and (find-package :passepartout)
(boundp (find-symbol "*DEGRADED-COMPONENTS*" :passepartout))
(symbol-value (find-symbol "*DEGRADED-COMPONENTS*" :passepartout))))
(chat-h (- h (if degraded 6 5))))
(when sd (view-status fb w)) (when sd (view-status fb w))
(when cd (view-chat fb w (- h 5))) (when cd (view-chat fb w chat-h))
(when id (view-input fb w)) (when id (view-input fb w))
(setf (st :dirty) (list nil nil nil)))) (when (and (st :sidebar-visible) (>= w 120))
(view-sidebar fb w h))
(setf (st :dirty) (list nil nil nil)))))
(in-package :passepartout) (in-package :passepartout)
@@ -428,3 +489,24 @@ ASCII < 128 = 1. CJK, fullwidth, emoji = 2. Combining marks = 0. Tab = 8."
(passepartout.channel-tui::init-state) (passepartout.channel-tui::init-state)
(let ((cg (passepartout.channel-tui::st :collapsed-gates))) (let ((cg (passepartout.channel-tui::st :collapsed-gates)))
(is (null cg)))) (is (null cg))))
(test test-sidebar-state
"Contract v0.8.0: init-state includes :sidebar-visible (nil) and :sidebar-width (30)."
(passepartout.channel-tui::init-state)
(is (null (passepartout.channel-tui::st :sidebar-visible)))
(is (= 30 (passepartout.channel-tui::st :sidebar-width))))
(test test-sidebar-not-shown-narrow
"Contract v0.8.0: sidebar is skipped in redraw when terminal width < 120."
(passepartout.channel-tui::init-state)
(setf (passepartout.channel-tui::st :sidebar-visible) t)
;; Simulating redraw logic: should not invoke view-sidebar when w < 120.
;; If view-sidebar were called with a nil fb it would error; this verifies
;; the guard in redraw protects the call.
(let ((fb nil) (w 100) (h 24))
(is (not (and (passepartout.channel-tui::st :sidebar-visible) (>= w 120))))))
(test test-status-bar-tokens
"v0.8.0: status bar uses :degraded and :warning theme tokens."
(is (getf passepartout.channel-tui::*tui-theme* :degraded))
(is (getf passepartout.channel-tui::*tui-theme* :warning)))

View File

@@ -606,7 +606,43 @@ Event handlers + daemon I/O + main loop.
(t nil)))) (t nil))))
(when (and chr (graphic-char-p chr)) (when (and chr (graphic-char-p chr))
(input-insert-char chr) (input-insert-char chr)
(setf (st :dirty) (list nil nil t))))))) (setf (st :dirty) (list nil nil t))
(when (and (char= chr #\/) (null (st :dialog-stack))
(= (length (st :input-buffer)) 1))
(minibuffer-show-commands)))))))
;; v0.8.0 — minibuffer dialog for slash commands
(defun minibuffer-show-commands ()
(let* ((on-select (lambda (opt)
(let ((cmd (getf opt :value)))
(pop (st :dialog-stack))
(setf (st :minibuffer-active) nil)
(setf (st :input-buffer) (reverse (coerce cmd 'list)))
(setf (st :cursor-pos) 0)
(setf (st :dirty) (list nil nil t)))))
(sel (cl-tty.select:make-select :options *slash-commands* :on-select on-select))
(dlg (make-instance 'cl-tty.dialog:dialog
:title "Commands"
:content sel)))
(push dlg (st :dialog-stack))
(setf (st :minibuffer-active) t)))
;; v0.8.0 — command palette for daemon commands (Ctrl+P)
(defun command-palette-show-commands ()
(let* ((on-select (lambda (opt)
(let ((cmd (getf opt :value)))
(pop (st :dialog-stack))
(setf (st :command-palette-active) nil)
(add-msg :system (format nil "Dispatching: ~s" cmd))
(send-daemon (list :type :event :payload cmd))
(setf (st :busy) t)
(setf (st :dirty) (list t t nil)))))
(sel (cl-tty.select:make-select :options *daemon-commands* :on-select on-select))
(dlg (make-instance 'cl-tty.dialog:dialog
:title "Command Palette"
:content sel)))
(push dlg (st :dialog-stack))
(setf (st :command-palette-active) t)))
;; v0.7.2 — resolve-hitl-panel: marks panel as resolved after approve/deny ;; v0.7.2 — resolve-hitl-panel: marks panel as resolved after approve/deny
(defun resolve-hitl-panel (decision) (defun resolve-hitl-panel (decision)
@@ -635,12 +671,10 @@ Event handlers + daemon I/O + main loop.
(cond (cond
;; New headline ;; New headline
((and (>= (length trimmed) 2) (eql (char trimmed 0) #\*)) ((and (>= (length trimmed) 2) (eql (char trimmed 0) #\*))
;; Flush previous section if in one
(when (and in-section section-content) (when (and in-section section-content)
(push (cons in-section (string-trim '(#\Space #\Newline) (push (cons in-section (string-trim '(#\Space #\Newline)
(format nil "~{~a~^ ~}" (reverse section-content)))) (format nil "~{~a~^ ~}" (reverse section-content))))
results)) results))
;; Check if this headline matches topic
(let ((title (string-trim '(#\Space #\*) trimmed))) (let ((title (string-trim '(#\Space #\*) trimmed)))
(if (search topic title :test #'char-equal) (if (search topic title :test #'char-equal)
(setf in-section title (setf in-section title
@@ -652,7 +686,6 @@ Event handlers + daemon I/O + main loop.
(when (and (> (length trimmed) 0) (when (and (> (length trimmed) 0)
(not (eql (char trimmed 0) #\#))) (not (eql (char trimmed 0) #\#)))
(push trimmed section-content)))))) (push trimmed section-content))))))
;; Flush last section
(when (and in-section section-content) (when (and in-section section-content)
(push (cons in-section (string-trim '(#\Space #\Newline) (push (cons in-section (string-trim '(#\Space #\Newline)
(format nil "~{~a~^ ~}" (reverse section-content)))) (format nil "~{~a~^ ~}" (reverse section-content))))
@@ -813,6 +846,33 @@ Event handlers + daemon I/O + main loop.
** Main Loop ** Main Loop
#+BEGIN_SRC lisp :tangle ../lisp/channel-tui-main.lisp #+BEGIN_SRC lisp :tangle ../lisp/channel-tui-main.lisp
;; v0.8.0 — Global keymap
(eval-when (:load-toplevel :execute)
(cl-tty.input:defkeymap :global
(:ctrl+q (lambda (e) (declare (ignore e))
(setf (st :running) nil)))
(:ctrl+p (lambda (e) (declare (ignore e))
(command-palette-show-commands)))
(:ctrl+b (lambda (e) (declare (ignore e))
(setf (st :sidebar-visible) (not (st :sidebar-visible)))
(setf (st :dirty) (list t t nil))))
(:ppage (lambda (e) (declare (ignore e))
(let ((max-offset (max 0 (- (length (st :messages)) 1))))
(setf (st :scroll-offset) (min max-offset (+ (st :scroll-offset) 10))))
(setf (st :dirty) (list nil t nil))))
(:npage (lambda (e) (declare (ignore e))
(setf (st :scroll-offset) (max 0 (- (st :scroll-offset) 10)))
(setf (st :dirty) (list nil t nil))))))
;; v0.8.0 — Prompt/local keymap (for when input is active)
(eval-when (:load-toplevel :execute)
(cl-tty.input:defkeymap :local
(:enter (lambda (e) (declare (ignore e)) (on-key :enter)))
(:up (lambda (e) (declare (ignore e)) (on-key :up)))
(:down (lambda (e) (declare (ignore e)) (on-key :down)))
(:escape (lambda (e) (declare (ignore e)) (on-key :escape)))))
(defun tui-main () (defun tui-main ()
(init-state) (init-state)
(load-history) (load-history)
@@ -862,33 +922,72 @@ Event handlers + daemon I/O + main loop.
(cl-tty.input:key-event (cl-tty.input:key-event
(cl-tty.input:key-event-key data)) (cl-tty.input:key-event-key data))
(t data)))) (t data))))
(cond
((st :dialog-stack)
(let* ((dlg (car (st :dialog-stack)))
(sel (cl-tty.dialog:dialog-content dlg)))
(cond (cond
((eql ch :escape) ((eql ch :escape)
(when (st :streaming-text) (pop (st :dialog-stack))
(send-daemon (list :type :event :payload '(:action :cancel-stream))) (setf (st :minibuffer-active) nil)
(when (> (length (st :messages)) 0) (setf (st :command-palette-active) nil)
(let ((idx (1- (length (st :messages)))))
(setf (getf (aref (st :messages) idx) :content)
(concatenate 'string
(getf (aref (st :messages) idx) :content)
" [interrupted]"))
(setf (getf (aref (st :messages) idx) :streaming) nil)
(setf (getf (aref (st :messages) idx) :time) (now))))
(setf (st :streaming-text) nil)
(setf (st :busy) nil)
(setf (st :dirty) (list t t nil))) (setf (st :dirty) (list t t nil)))
(when (st :search-mode) ((member ch '(:up :down))
(setf (st :search-mode) nil (if (eql ch :up) (cl-tty.select:select-prev sel)
(st :search-matches) nil (cl-tty.select:select-next sel)))
(st :search-query) "") ((member ch '(:enter 13 10 #\Newline #\Return))
(setf (st :dirty) (list nil t nil)) (let* ((filtered (cl-tty.select:select-filtered-options sel))
(add-msg :system "Search exited"))) (idx (cl-tty.select:select-selected-index sel))
(item (when (< idx (length filtered))
(third (nth idx filtered)))))
(when item
(let ((cb (cl-tty.select:select-on-select sel)))
(when cb (funcall cb item))))))
((and (characterp ch) (graphic-char-p ch))
(setf (cl-tty.select:select-filter sel)
(concatenate 'string (or (cl-tty.select:select-filter sel) "") (string ch))))
((member ch '(:backspace 127 8))
(let ((f (cl-tty.select:select-filter sel)))
(when (> (length f) 0)
(setf (cl-tty.select:select-filter sel) (subseq f 0 (1- f)))))))))
((cl-tty.input:dispatch-key-event data)
nil)
(t (on-key ch))))))) (t (on-key ch)))))))
(when (or (first (st :dirty)) (second (st :dirty)) (third (st :dirty))) (when (or (first (st :dirty)) (second (st :dirty)) (third (st :dirty)))
(cl-tty.backend:backend-clear be) (cl-tty.backend:backend-clear be)
(redraw curr-fb w h) (redraw curr-fb w h)
(cl-tty.rendering:flush-framebuffer prev-fb curr-fb be) (cl-tty.rendering:flush-framebuffer prev-fb curr-fb be)
(rotatef prev-fb curr-fb)) (rotatef prev-fb curr-fb))
(let ((ds (st :dialog-stack)))
(when ds
(let* ((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))
(dw 60) (dh (min 20 (+ 4 cnt)))
(mx (floor (- w dw) 2))
(my (floor (- h dh) 2)))
(dotimes (row h)
(cl-tty.backend:draw-rect be 0 row w 1 :bg :bright-black))
(cl-tty.backend:draw-border be mx my dw dh :style :single
:title (cl-tty.dialog:dialog-title dlg))
(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))))
(when (>= y-off (1- dh)) (return))
(cl-tty.backend:draw-text be (1+ mx) (+ my y-off) text
(cond (cat (theme-color :dim))
(sel-p (theme-color :highlight))
(t (theme-color :agent)))
nil :bold sel-p)
(incf y-off)))))))
(sleep 0.1)))) (sleep 0.1))))
(disconnect-daemon)))) (disconnect-daemon))))
#+END_SRC #+END_SRC
@@ -1369,4 +1468,26 @@ Event handlers + daemon I/O + main loop.
(setf (st :scroll-offset) 3) (setf (st :scroll-offset) 3)
(on-key :npage) (on-key :npage)
(fiveam:is (= 0 (st :scroll-offset)))) (fiveam:is (= 0 (st :scroll-offset))))
;; ── v0.8.0 Minibuffer ──
(fiveam:test test-slash-commands-defined
"Contract v0.8.0: *slash-commands* is non-nil list of option plists."
(fiveam:is (listp passepartout.channel-tui::*slash-commands*))
(fiveam:is (> (length passepartout.channel-tui::*slash-commands*) 0))
(fiveam:is (every (lambda (opt)
(and (getf opt :title) (getf opt :value) (getf opt :category)))
passepartout.channel-tui::*slash-commands*)))
(fiveam:test test-minibuffer-state
"Contract v0.8.0: init-state has :dialog-stack and :minibuffer-active fields."
(init-state)
(fiveam:is (null (st :dialog-stack)))
(fiveam:is (null (st :minibuffer-active))))
(fiveam:test test-command-palette-state
"Contract v0.8.0: init-state has :command-palette-active and :command-palette-dialog as nil."
(init-state)
(fiveam:is (null (st :command-palette-active)))
(fiveam:is (null (st :command-palette-dialog))))
#+END_SRC #+END_SRC

View File

@@ -49,8 +49,10 @@ All state mutation flows through event handlers in the controller.
;; Differentiator (v0.4.0) ;; Differentiator (v0.4.0)
:rule-count :cyan :focus-map :yellow :rule-count :cyan :focus-map :yellow
;; UI ;; UI
:dim :white :highlight :cyan :accent :green) :dim :white :highlight :cyan :accent :green
"Color theme plist. 27 semantic keys → hex color strings. ;; Degraded
:degraded :bright-yellow)
"Color theme plist. 28 semantic keys → hex color strings.
See *tui-theme-presets* for named presets (dark, light, solarized, gruvbox).") See *tui-theme-presets* for named presets (dark, light, solarized, gruvbox).")
(defvar *tui-theme-presets* (defvar *tui-theme-presets*
@@ -61,7 +63,8 @@ See *tui-theme-presets* for named presets (dark, light, solarized, gruvbox).")
:tool-running :magenta :tool-success :green :tool-failure :red :tool-output :white :tool-running :magenta :tool-success :green :tool-failure :red :tool-output :white
:scroll-indicator :cyan :border :white :background :black :scroll-indicator :cyan :border :white :background :black
:rule-count :cyan :focus-map :yellow :rule-count :cyan :focus-map :yellow
:dim :white :highlight :cyan :accent :green) :dim :white :highlight :cyan :accent :green
:degraded :bright-yellow)
:light (:user :blue :agent :black :system :red :light (:user :blue :agent :black :system :red
:input :black :timestamp :yellow :help :blue :error :red :warning :yellow :input :black :timestamp :yellow :help :blue :error :red :warning :yellow
:connected :green :disconnected :red :busy :magenta :idle :black :connected :green :disconnected :red :busy :magenta :idle :black
@@ -69,7 +72,8 @@ See *tui-theme-presets* for named presets (dark, light, solarized, gruvbox).")
:tool-running :magenta :tool-success :green :tool-failure :red :tool-output :black :tool-running :magenta :tool-success :green :tool-failure :red :tool-output :black
:scroll-indicator :blue :border :black :background :white :scroll-indicator :blue :border :black :background :white
:rule-count :blue :focus-map :red :rule-count :blue :focus-map :red
:dim :white :highlight :blue :accent :green) :dim :white :highlight :blue :accent :green
:degraded :bright-yellow)
:gruvbox (:user "#458588" :agent "#ebdbb2" :system "#fabd2f" :gruvbox (:user "#458588" :agent "#ebdbb2" :system "#fabd2f"
:input "#ebdbb2" :timestamp "#928374" :help "#83a598" :error "#fb4934" :warning "#fabd2f" :input "#ebdbb2" :timestamp "#928374" :help "#83a598" :error "#fb4934" :warning "#fabd2f"
:connected "#b8bb26" :disconnected "#fb4934" :busy "#d3869b" :idle "#a89984" :connected "#b8bb26" :disconnected "#fb4934" :busy "#d3869b" :idle "#a89984"
@@ -77,7 +81,8 @@ See *tui-theme-presets* for named presets (dark, light, solarized, gruvbox).")
:tool-running "#d3869b" :tool-success "#b8bb26" :tool-failure "#fb4934" :tool-output "#ebdbb2" :tool-running "#d3869b" :tool-success "#b8bb26" :tool-failure "#fb4934" :tool-output "#ebdbb2"
:scroll-indicator "#83a598" :border "#a89984" :background "#282828" :scroll-indicator "#83a598" :border "#a89984" :background "#282828"
:rule-count "#83a598" :focus-map "#fabd2f" :rule-count "#83a598" :focus-map "#fabd2f"
:dim "#928374" :highlight "#83a598" :accent "#b8bb26") :dim "#928374" :highlight "#83a598" :accent "#b8bb26"
:degraded "#fabd2f")
:solarized (:user "#268bd2" :agent "#839496" :system "#b58900" :solarized (:user "#268bd2" :agent "#839496" :system "#b58900"
:input "#839496" :timestamp "#93a1a1" :help "#2aa198" :error "#dc322f" :warning "#b58900" :input "#839496" :timestamp "#93a1a1" :help "#2aa198" :error "#dc322f" :warning "#b58900"
:connected "#859900" :disconnected "#dc322f" :busy "#d33682" :idle "#657b83" :connected "#859900" :disconnected "#dc322f" :busy "#d33682" :idle "#657b83"
@@ -85,7 +90,8 @@ See *tui-theme-presets* for named presets (dark, light, solarized, gruvbox).")
:tool-running "#d33682" :tool-success "#859900" :tool-failure "#dc322f" :tool-output "#839496" :tool-running "#d33682" :tool-success "#859900" :tool-failure "#dc322f" :tool-output "#839496"
:scroll-indicator "#2aa198" :border "#657b83" :background "#002b36" :scroll-indicator "#2aa198" :border "#657b83" :background "#002b36"
:rule-count "#2aa198" :focus-map "#b58900" :rule-count "#2aa198" :focus-map "#b58900"
:dim "#586e75" :highlight "#2aa198" :accent "#859900")) :dim "#586e75" :highlight "#2aa198" :accent "#859900"
:degraded "#b58900"))
"Named theme presets. /theme <name> loads one into *tui-theme*.") "Named theme presets. /theme <name> loads one into *tui-theme*.")
(defvar *tui-theme-current-name* :dark (defvar *tui-theme-current-name* :dark
@@ -129,6 +135,7 @@ See *tui-theme-presets* for named presets (dark, light, solarized, gruvbox).")
(:green "#00FF00") (:red "#FF0000") (:cyan "#00FFFF") (:green "#00FF00") (:red "#FF0000") (:cyan "#00FFFF")
(:yellow "#FFFF00") (:magenta "#FF00FF") (:blue "#0000FF") (:yellow "#FFFF00") (:magenta "#FF00FF") (:blue "#0000FF")
(:white "#FFFFFF") (:black "#000000") (:white "#FFFFFF") (:black "#000000")
(:bright-yellow "#FFD700")
(t "#FFFFFF")))))) (t "#FFFFFF"))))))
(defun st (key) (getf *state* key)) (defun st (key) (getf *state* key))
@@ -147,11 +154,28 @@ See *tui-theme-presets* for named presets (dark, light, solarized, gruvbox).")
: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-visible nil ; v0.8.0
:sidebar-width 30 ; 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
:dialog-stack nil ; v0.8.0
:minibuffer-active nil ; v0.8.0
:command-palette-active nil ; v0.8.0
:command-palette-dialog nil ; v0.8.0
:dirty (list nil nil nil)))) :dirty (list nil nil nil))))
#+END_SRC #+END_SRC
** Sidebar panel definitions
#+BEGIN_SRC lisp :tangle ../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 ../lisp/channel-tui-state.lisp #+BEGIN_SRC lisp :tangle ../lisp/channel-tui-state.lisp
(defun now () (defun now ()
@@ -189,6 +213,52 @@ See *tui-theme-presets* for named presets (dark, light, solarized, gruvbox).")
(setf (st :dirty) (list t t nil))) (setf (st :dirty) (list t t nil)))
#+END_SRC #+END_SRC
** Slash Commands
#+BEGIN_SRC lisp :tangle ../lisp/channel-tui-state.lisp
(defvar *slash-commands*
'((:title "/eval <expr> — Evaluate Lisp" :value "/eval" :category :session)
(:title "/undo — Undo last operation" :value "/undo" :category :session)
(:title "/redo — Redo last operation" :value "/redo" :category :session)
(:title "/reconnect — Re-establish daemon" :value "/reconnect" :category :session)
(:title "/quit — Save history and exit" :value "/quit" :category :session)
(:title "/q — Quick quit" :value "/q" :category :session)
(:title "/why — Show last gate trace" :value "/why" :category :memory)
(:title "/identity — Edit IDENTITY.org" :value "/identity" :category :memory)
(:title "/tags — List tag severities" :value "/tags" :category :memory)
(:title "/audit <id> — Inspect memory" :value "/audit" :category :memory)
(:title "/audit verify — Memory integrity" :value "/audit verify" :category :memory)
(:title "/rewind <n> — Rewind to snapshot" :value "/rewind" :category :memory)
(:title "/sessions — Show memory snapshots" :value "/sessions" :category :memory)
(:title "/resume <n> — Resume from snapshot" :value "/resume" :category :memory)
(:title "/focus <project> — Set context" :value "/focus" :category :system)
(:title "/scope <scope> — Change scope" :value "/scope" :category :system)
(:title "/unfocus — Pop context" :value "/unfocus" :category :system)
(:title "/theme [name] — Show/switch theme" :value "/theme" :category :system)
(:title "/context — Show context summary" :value "/context" :category :system)
(:title "/context why <id> — Debug memory" :value "/context why" :category :system)
(:title "/context dropped — Estimate pruned" :value "/context dropped" :category :system)
(:title "/search <query> — Search messages" :value "/search" :category :navigation)
(:title "/help — Show commands" :value "/help" :category :help)
(:title "/help <topic> — Search manual" :value "/help <topic>" :category :help))
"Slash commands for minibuffer select-dialog.")
#+END_SRC
** Daemon Commands
#+BEGIN_SRC lisp :tangle ../lisp/channel-tui-state.lisp
(defvar *daemon-commands*
'((:title "Status — Daemon health info" :value (:action :status) :category :session)
(:title "Stats — Daemon statistics" :value (:action :stats) :category :session)
(:title "Ping — Daemon reachability" :value (:action :ping) :category :session)
(:title "Memory Snapshot — Capture state" :value (:action :memory-snapshot) :category :memory)
(:title "Memory Rebuild — Rebuild indices" :value (:action :memory-rebuild) :category :memory)
(:title "Memory Compact — Optimize storage" :value (:action :memory-compact) :category :memory)
(:title "Reload Config — Reload configuration" :value (:action :reload-config) :category :system)
(:title "Reload Identity — Reload identity file" :value (:action :reload-identity) :category :system)
(:title "List Skills — Available skills" :value (:action :list-skills) :category :system)
(:title "Help — Show daemon help" :value (:action :help) :category :help))
"Daemon commands for the command palette (Ctrl+P).")
#+END_SRC
** Event Queue ** Event Queue
#+BEGIN_SRC lisp :tangle ../lisp/channel-tui-state.lisp #+BEGIN_SRC lisp :tangle ../lisp/channel-tui-state.lisp
(defun queue-event (ev) (defun queue-event (ev)

View File

@@ -60,10 +60,9 @@ Returns a list of strings, one per line."
(defun view-status (fb w) (defun view-status (fb w)
(let* ((degraded (and (find-package :passepartout) (let* ((degraded (and (find-package :passepartout)
(boundp (find-symbol "*SYSTEM-HEALTH*" :passepartout)) (boundp (find-symbol "*DEGRADED-COMPONENTS*" :passepartout))
(member (symbol-value (find-symbol "*SYSTEM-HEALTH*" :passepartout)) (symbol-value (find-symbol "*DEGRADED-COMPONENTS*" :passepartout))))
'(:degraded :unhealthy)))) (bg (if degraded (theme-color :degraded) nil)))
(bg (if degraded :bright-yellow nil)))
;; Line 1: Connection, mode, msgs, scroll, rules, streaming/busy ;; Line 1: Connection, mode, msgs, scroll, rules, streaming/busy
(cl-tty.backend:draw-text fb 1 1 (cl-tty.backend:draw-text fb 1 1
(format nil " Passepartout ~a [~a] msgs:~a scroll:~a Rules:~a~a" (format nil " Passepartout ~a [~a] msgs:~a scroll:~a Rules:~a~a"
@@ -85,14 +84,18 @@ Returns a list of strings, one per line."
;; Line 3: Directory, LSP, MCP, commands hint (v0.8.0) ;; Line 3: Directory, LSP, MCP, commands hint (v0.8.0)
(let* ((cwd (or (uiop:getenv "PWD") (uiop:getcwd))) (let* ((cwd (or (uiop:getenv "PWD") (uiop:getcwd)))
(dir (subseq cwd (max 0 (- (length cwd) (- w 45))))) (dir (subseq cwd (max 0 (- (length cwd) (- w 45)))))
(lsp-color (if (st :connected) :green :dim))
(mcp-count (or (st :mcp-count) 0)) (mcp-count (or (st :mcp-count) 0))
(hint " Ctrl+P: commands /help: help")) (hint " Ctrl+P: commands /help: help"))
(cl-tty.backend:draw-text fb 1 3 (format nil " ~a" dir) (theme-color :dim) bg) (cl-tty.backend:draw-text fb 1 3 (format nil " ~a" dir) (theme-color :dim) bg)
(cl-tty.backend:draw-text fb (+ 2 (length dir)) 3 "●" (theme-color lsp-color) bg) (cl-tty.backend:draw-text fb (+ 2 (length dir)) 3 "●" (theme-color :accent) bg)
(cl-tty.backend:draw-text fb (+ 5 (length dir)) 3 (format nil " MCP:~d" mcp-count) (cl-tty.backend:draw-text fb (+ 5 (length dir)) 3 (format nil " MCP:~d" mcp-count)
(theme-color :dim) bg) (theme-color :dim) bg)
(cl-tty.backend:draw-text fb (- w (length hint) 2) 3 hint (theme-color :timestamp) bg)))) (cl-tty.backend:draw-text fb (- w (length hint) 2) 3 hint (theme-color :timestamp) bg))
;; Line 4: Degraded mode warning (v0.8.0)
(when degraded
(cl-tty.backend:draw-text fb 1 4 " ⚠ Degraded mode — components unavailable"
(theme-color :warning) (theme-color :degraded)))))
;; v0.7.2: search-highlight — wrap matching text in **bold** for markdown ;; v0.7.2: search-highlight — wrap matching text in **bold** for markdown
(defun search-highlight (content query) (defun search-highlight (content query)
@@ -117,7 +120,7 @@ Returns a list of strings, one per line."
(max-lines (- h 2)) (max-lines (- h 2))
(is-search (st :search-mode)) (is-search (st :search-mode))
(y 1)) (y 1))
;; v0.7.2: search mode header ;; v0.8.0: search mode header
(when is-search (when is-search
(let* ((matches (st :search-matches)) (let* ((matches (st :search-matches))
(idx (st :search-match-idx)) (idx (st :search-match-idx))
@@ -127,22 +130,51 @@ Returns a list of strings, one per line."
(cl-tty.backend:draw-text fb 1 y header (theme-color :highlight) nil) (cl-tty.backend:draw-text fb 1 y header (theme-color :highlight) nil)
(incf y) (incf y)
(decf max-lines))) (decf max-lines)))
;; Count visible messages from end, accounting for word wrap ;; Pre-compute display lines for each message
(let* ((msg-count 0) (let ((msg-lines (make-array total)))
(lines-remaining max-lines)) (dotimes (i total)
(loop for i from (1- total) downto 0 (let* ((msg (aref msgs i))
while (> lines-remaining 0)
do (let* ((msg (aref msgs i))
(role (getf msg :role)) (role (getf msg :role))
(content (getf msg :content)) (content (getf msg :content))
(time (or (getf msg :time) "")) (time (or (getf msg :time) ""))
(prefix (case role (:user "⬆") (:agent "⬇") (t " "))) (content-show (if is-search (search-highlight content (st :search-query)) content))
(content-show (if is-search (lines (case role
(search-highlight content (st :search-query)) (:user (cl-tty.box:word-wrap
content)) (format nil "│ [~a] ~a" time content-show) (- w 2)))
(line-text (format nil "~a [~a] ~a" prefix time content-show)) (:agent (let* ((nodes (cl-tty.markdown:parse-blocks content-show))
(wrapped (word-wrap line-text (- w 2))) (md-lines (and nodes (cl-tty.markdown:render-md nodes))))
(nlines (length wrapped))) (if md-lines
(progn (setf (first md-lines)
(format nil "[~a] ~a" time (first md-lines)))
md-lines)
(list (format nil "[~a] " time)))))
(t (cl-tty.box:word-wrap
(format nil " [~a] ~a" time content-show) (- w 2))))))
;; v0.8.0: tool calls — collapsible
(let ((tc (getf msg :tool-calls)))
(when tc
(if (st :expand-tool-calls)
(dolist (call tc)
(setf lines (append lines
(list (format nil " ╎ Tool: ~a" (or (getf call :name) "unknown"))))))
(setf lines (append lines
(list (format nil " ╎ ~a tool call(s)" (length tc))))))))
;; v0.8.0: gate trace — collapsible with left border
(let ((gt (getf msg :gate-trace)))
(when gt
(if (member i (st :collapsed-gates))
(setf lines (append lines
(list (format nil "╎ Gate trace: ~a gates — Ctrl+G toggle"
(length gt)))))
(dolist (entry (passepartout::gate-trace-lines gt))
(setf lines (append lines
(list (concatenate 'string "╎ " (car entry)))))))))
(setf (aref msg-lines i) lines)))
;; Count visible messages from end
(let ((msg-count 0) (lines-remaining max-lines))
(loop for i from (1- total) downto 0
while (> lines-remaining 0)
do (let ((nlines (length (aref msg-lines i))))
(if (<= nlines lines-remaining) (if (<= nlines lines-remaining)
(progn (decf lines-remaining nlines) (incf msg-count)) (progn (decf lines-remaining nlines) (incf msg-count))
(setf lines-remaining 0)))) (setf lines-remaining 0))))
@@ -153,34 +185,19 @@ Returns a list of strings, one per line."
while (< y (1- h)) while (< y (1- h))
do (let* ((msg (aref msgs i)) do (let* ((msg (aref msgs i))
(role (getf msg :role)) (role (getf msg :role))
(content (getf msg :content)) (lines (aref msg-lines i))
(time (or (getf msg :time) "")) (color (theme-color
(color (theme-color (case role (:user :user) (:agent :agent) (:system :system) (t :agent)))) (case role
(prefix (case role (:user "⬆") (:agent "⬇") (t " "))) (:user :user) (:agent :agent) (:system :system) (t :agent))))
(is-panel (getf msg :panel)) (is-panel (getf msg :panel))
(is-resolved (getf msg :panel-resolved)) (is-resolved (getf msg :panel-resolved)))
(content-show (if is-search ;; HITL panel coloring
(search-highlight content (st :search-query))
content))
(line-text (format nil "~a [~a] ~a" prefix time content-show))
(wrapped (word-wrap line-text (- w 2))))
;; HITL panel: render with colored border
(when is-panel (when is-panel
(setf color (if is-resolved (setf color (if is-resolved (theme-color :dim) (theme-color :hitl))))
(theme-color :dim) (dolist (line lines)
(theme-color :hitl))))
(dolist (line wrapped)
(when (< y (1- h)) (when (< y (1- h))
(cl-tty.backend:draw-text fb 1 y line color nil) (cl-tty.backend:draw-text fb 1 y line color nil)
(incf y))) (incf y))))))))))
;; v0.7.2: gate trace below agent messages
(let ((gate-trace (getf msg :gate-trace)))
(when (and gate-trace (not (member i (st :collapsed-gates))))
(dolist (entry (passepartout::gate-trace-lines gate-trace))
(when (< y (1- h))
(cl-tty.backend:draw-text fb 3 y (car entry)
(or (getf (cdr entry) :fgcolor) :dim) nil)
(incf y)))))))))))
#+END_SRC #+END_SRC
** Input Line ** Input Line
@@ -193,14 +210,61 @@ Returns a list of strings, one per line."
(cl-tty.backend:draw-text fb 0 0 (format nil "~a " visible) (theme-color :input) nil))) (cl-tty.backend:draw-text fb 0 0 (format nil "~a " visible) (theme-color :input) nil)))
#+end_src #+end_src
** Sidebar
#+BEGIN_SRC lisp :tangle ../lisp/channel-tui-view.lisp
(defun view-sidebar (fb w h)
(let ((x (- w (st :sidebar-width))))
;; Vertical separator
(dotimes (row h)
(cl-tty.backend:draw-rect fb (1- x) row 1 1 :bg :dim))
;; Render panels
(let ((y 1))
;; Focus panel
(when (st :foveal-id)
(cl-tty.backend:draw-text fb (1+ x) y " Focus" (theme-color :highlight) nil)
(incf y)
(cl-tty.backend:draw-text fb (1+ x) y (format nil " ~a" (st :foveal-id)) (theme-color :agent) nil)
(incf y 2))
;; Rules panel
(let ((rules (or (st :rule-count) 0)))
(cl-tty.backend:draw-text fb (1+ x) y " Rules" (theme-color :highlight) nil)
(incf y)
(cl-tty.backend:draw-text fb (1+ x) y (format nil " ~d active" rules) (theme-color :agent) nil)
(incf y 2))
;; Context panel — token gauge
(cl-tty.backend:draw-text fb (1+ x) y " Context" (theme-color :highlight) nil)
(incf y)
(let* ((msg-count (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 (1+ x) y (format nil " [~a~a]" bar (make-string (- 10 bar-len) :initial-element #\Space)) (theme-color :dim) nil)
(incf y)
(cl-tty.backend:draw-text fb (1+ x) y (format nil " ~d%" pct) (theme-color :timestamp) nil)
(incf y 2))
;; MCP count
(let ((mcp (or (st :mcp-count) 0)))
(cl-tty.backend:draw-text fb (1+ x) y " MCP" (theme-color :highlight) nil)
(incf y)
(cl-tty.backend:draw-text fb (1+ x) y (format nil " ~d server~:p" mcp) (theme-color :agent) nil)))))
#+END_SRC
** Redraw (dirty-flag dispatch) ** Redraw (dirty-flag dispatch)
#+begin_src lisp #+begin_src lisp
(defun redraw (fb w h) (defun redraw (fb w h)
(destructuring-bind (sd cd id) (st :dirty) (destructuring-bind (sd cd id) (st :dirty)
(let* ((degraded (and (find-package :passepartout)
(boundp (find-symbol "*DEGRADED-COMPONENTS*" :passepartout))
(symbol-value (find-symbol "*DEGRADED-COMPONENTS*" :passepartout))))
(chat-h (- h (if degraded 6 5))))
(when sd (view-status fb w)) (when sd (view-status fb w))
(when cd (view-chat fb w (- h 5))) (when cd (view-chat fb w chat-h))
(when id (view-input fb w)) (when id (view-input fb w))
(setf (st :dirty) (list nil nil nil)))) (when (and (st :sidebar-visible) (>= w 120))
(view-sidebar fb w h))
(setf (st :dirty) (list nil nil nil)))))
#+END_SRC #+END_SRC
* Implementation — v0.7.0 additions * Implementation — v0.7.0 additions
@@ -491,4 +555,25 @@ ASCII < 128 = 1. CJK, fullwidth, emoji = 2. Combining marks = 0. Tab = 8."
(passepartout.channel-tui::init-state) (passepartout.channel-tui::init-state)
(let ((cg (passepartout.channel-tui::st :collapsed-gates))) (let ((cg (passepartout.channel-tui::st :collapsed-gates)))
(is (null cg)))) (is (null cg))))
(test test-sidebar-state
"Contract v0.8.0: init-state includes :sidebar-visible (nil) and :sidebar-width (30)."
(passepartout.channel-tui::init-state)
(is (null (passepartout.channel-tui::st :sidebar-visible)))
(is (= 30 (passepartout.channel-tui::st :sidebar-width))))
(test test-sidebar-not-shown-narrow
"Contract v0.8.0: sidebar is skipped in redraw when terminal width < 120."
(passepartout.channel-tui::init-state)
(setf (passepartout.channel-tui::st :sidebar-visible) t)
;; Simulating redraw logic: should not invoke view-sidebar when w < 120.
;; If view-sidebar were called with a nil fb it would error; this verifies
;; the guard in redraw protects the call.
(let ((fb nil) (w 100) (h 24))
(is (not (and (passepartout.channel-tui::st :sidebar-visible) (>= w 120))))))
(test test-status-bar-tokens
"v0.8.0: status bar uses :degraded and :warning theme tokens."
(is (getf passepartout.channel-tui::*tui-theme* :degraded))
(is (getf passepartout.channel-tui::*tui-theme* :warning)))
#+END_SRC #+END_SRC

View File

@@ -1,7 +1,7 @@
(defsystem :passepartout (defsystem :passepartout
:name "Passepartout" :name "Passepartout"
:author "Amr Gharbeia" :author "Amr Gharbeia"
:version "0.7.2" :version "0.4.3"
:license "AGPLv3" :license "AGPLv3"
:description "The Probabilistic-Deterministic Lisp Machine" :description "The Probabilistic-Deterministic Lisp Machine"
:depends-on (:usocket :bordeaux-threads :dexador :uiop :cl-dotenv :cl-ppcre :hunchentoot :ironclad :str :cl-json :uuid) :depends-on (:usocket :bordeaux-threads :dexador :uiop :cl-dotenv :cl-ppcre :hunchentoot :ironclad :str :cl-json :uuid)
@@ -16,7 +16,7 @@
(:file "lisp/core-pipeline"))) (:file "lisp/core-pipeline")))
(defsystem :passepartout/tui (defsystem :passepartout/tui
:depends-on (:passepartout :cl-tty :usocket :bordeaux-threads) :depends-on (:passepartout :cl-tty :croatoan :usocket :bordeaux-threads)
:serial t :serial t
:components ((:file "lisp/channel-tui-state") :components ((:file "lisp/channel-tui-state")
(:file "lisp/channel-tui-view") (:file "lisp/channel-tui-view")