wip: unified minibuffer panel, v0.9.1 Emacs dev env in ROADMAP

- Unified minibuffer slash-command panel (panel-based wizard, settings,
  help sub-mode stack) — channel-tui state/view changes
- ROADMAP: v0.8.0 broken into atomic DONE items, v0.9.1 added with
  Emacs major mode + M-x command surface TODOs
- Semver discipline from v0.7.1 onward (X.Y.Z)
This commit is contained in:
2026-05-13 09:17:30 -04:00
parent a8901d9675
commit 00211cf685
5 changed files with 485 additions and 231 deletions

View File

@@ -34,11 +34,32 @@ 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
3. If a ~CHANGELOG.md~ is needed for packaging tools, auto-generate it from ROADMAP DONE items
** DONE v0.8.0: Information Radiator (Foundation)
** TODO v0.8.0: Information Radiator (Foundation)
Sidebar (6 panels), sidebar overlay mode (<120 cols), command palette (Ctrl+P), TrueColor theme expansion (8 presets).
Sidebar (6 panels), sidebar overlay mode (<120 cols), command palette (Ctrl+P), TrueColor theme expansion (8 presets), unified minibuffer panel with slash-command context menu and sub-mode navigation (wizard, settings, help).
For the full DONE items, see ~CHANGELOG.org~.
*** DONE Unified minibuffer slash-command panel
:PROPERTIES:
:ID: id-v080-minibuffer
:CREATED: [2026-05-10 Sat]
:END:
:LOGBOOK:
- State "DONE" from "TODO" [2026-05-10 Sat]
:END:
Replace ad-hoc overlay windows with a single bottom-anchored panel. Typing =/= as the first character opens a command context menu (~25 slash commands, filtered in real time as the user types). Navigating to =/wizard= and pressing Enter transitions the panel into the setup wizard — same panel, same position, sub-mode stack. Esc returns to the command list. Future sub-modes (=/settings=, =/help=) slot into the same architecture.
- Add ~:minibuffer-mode~ and ~:minibuffer-selected-idx~ state fields to ~init-state~
- Extract ~*slash-commands*~ data structure (~25 commands, each with description) from the ~on-key~ Enter handler
- Add ~view-minibuffer~ that dispatches on ~:minibuffer-mode~ to ~view-slash-menu~, ~view-wizard-in-panel~
- Add ~minibuffer-handle-key~ which dispatches to ~slash-menu-handle-key~ or ~wizard-handle-key~
- TUI event loop: replace separate wizard key handlers with unified modal dispatch block
- ~on-key~: auto-open slash-menu when =/= typed as first character
- ~wizard-start~ / ~wizard-cancel~: set ~:minibuffer-mode~ instead of ~:wizard-visible~
- Merge the wizard overlay (centered, 60x14) into the bottom-anchored panel
- Remove ~:wizard-visible~ state field
~150 lines.
** v0.9.0: Eval Harness — Safety Net First
@@ -58,6 +79,74 @@ Every subsequent release ships with automated regression protection. The eval ha
- Task suite grows with codebase: every bug fix adds a regression task
~200 lines.
** v0.9.1: Emacs Development Environment — A Functional UI
The croatoan TUI is on life support — enough to see output and type commands, but every render feature (markdown, tool visualization, mouse, adaptive layout) requires custom ncurses code destined for the trash at v2.0.0. Emacs is the v2.0.0 bridge: the same major mode, sidebar, and M-x commands survive from now through Phase III.
*** TODO Emacs major mode
:PROPERTIES:
:ID: id-v091-emacs-mode
:CREATED: [2026-05-10 Sat]
:END:
- ~passepartout-mode~ major mode for the conversation buffer
- Message rendering as Org headlines: role prefix (~:user:~, ~:agent:~, ~:system:~), universal timestamp, content in body. Gate trace as property drawer under agent message headlines
- Streaming insertion: LLM response chunks arrive from the daemon and insert incrementally into the buffer (~insert~ as chunks arrive, update on each frame)
- Read-only protection on agent response regions (editable regions only in user input area)
- Keybindings: ~C-c C-c~ send current input, ~C-c C-k~ interrupt/stop, ~C-c C-a~ approve HITL, ~C-c C-d~ deny HITL
- ~100 lines elisp
*** TODO M-x command surface
:PROPERTIES:
:ID: id-v091-emacs-commands
:CREATED: [2026-05-10 Sat]
:END:
Replace croatoan's ~/~ slash-command panel with Emacs ~M-x~ dispatch. Each command is a ~defun passeparate-<action>~ with interactive completion:
- ~passepartout-focus~ — completing-read over node IDs from the daemon's focus response. Sets the foveal context
- ~passepartout-eval~ — eval a Lisp form in the daemon. Read from minibuffer, send as framed plist, insert result
- ~passepartout-theme~ — completing-read over available themes. Sends ~/theme <name>~ to daemon
- ~passepartout-export~ — export session. Prefix arg selects format (~C-u M-x passeparate-export~)
- ~passepartout-sidebar~ — toggle the sidebar buffer visibility
- ~passepartout-config~ — completing-read over config keys. Sets env var, triggers daemon reload
- ~passepartout-coach~ — run self-diagnosis. Inserts coaching report as new message
- ~passepartout-agenda~ — run Org agenda query. Inserts results as new message
- ~passepartout-quit~ — close connection and kill buffer
Each command is a thin wrapper around ~passepartout-send~ (the existing TCP bridge from v0.4.0): construct the correct plist, send it, and insert the response. Completing-read, history, and docstrings are free. ~80 lines elisp.
*** TODO Sidebar buffer
:PROPERTIES:
:ID: id-v091-emacs-sidebar
:CREATED: [2026-05-10 Sat]
:END:
- ~passepartout-sidebar-mode~ — a dedicated side window (right, 42 chars) that updates on each daemon response
- Gate Trace panel — per-gate results from the most recent agent response. Colored by state (green/yellow/red)
- Focus panel — current foveal node ID + related node count
- Rules panel — rule counter with session delta. When symbolic engine is active, shows sufficiency score and provenance breakdown
- Context panel — token gauge (percentage bar + color coding)
- Cost panel — session cost updated after each LLM call
- Files panel — modified files list with +/- line counts
- All data already exists in the daemon's response plist (~:rule-count~, ~:foveal-id~, ~:gate-trace~, ~:sufficiency-ratio~). The sidebar is a formatting layer
- Toggle via ~M-x passeparate-sidebar~ or ~C-c C-s~
- ~60 lines elisp
*** TODO Daemon lifecycle
:PROPERTIES:
:ID: id-v091-emacs-lifecycle
:CREATED: [2026-05-10 Sat]
:END:
- ~passepartout-start~ — runs ~passepartout daemon~ in a background process, waits for port 9105, connects
- ~passepartout-stop~ — sends shutdown signal, kills buffer
- ~passepartout-restart~ — stop + start
- ~passepartout-status~ — checks daemon health via ~/status~ command, displays in sidebar
- ~20 lines elisp + bash wrappers
Total: ~260 lines elisp, persisting through v2.0.0+.
** v0.10.0: Phase 0 — Type-Level Gates + Core Integrity (~75 lines)
:PROPERTIES:

View File

@@ -137,8 +137,14 @@ See *tui-theme-presets* for named presets (dark, light, solarized, gruvbox).")
key)))
(defun theme-color (role)
"Returns the Croatoan color for a semantic role."
(or (getf *tui-theme* role) :white))
"Returns the Croatoan color for a semantic role.
Keyword or hex string values are returned as-is; hex strings are
converted to integers that Croatoan can process."
(let ((val (or (getf *tui-theme* role) :white)))
(if (and (stringp val) (> (length val) 0) (eql (char val 0) #\#))
(handler-case (parse-integer (subseq val 1) :radix 16)
(error () val))
val)))
;; v0.8.0: TrueColor helpers
(defun theme-hex-to-rgb (hex-string)
@@ -182,12 +188,14 @@ See *tui-theme-presets* for named presets (dark, light, solarized, gruvbox).")
:search-mode nil :search-query "" ; v0.7.2
:search-matches nil :search-match-idx 0
:sidebar-visible nil ; v0.8.0
:palette-visible nil :palette-filter nil ; v0.8.0
:palette-selected-idx 0 :palette-items nil ; v0.8.0
:wizard-step 0 :wizard-error nil ; v0.8.0
:wizard-visible nil :wizard-input "" ; v0.8.0
:wizard-provider nil :wizard-api-key nil ; v0.8.0
:wizard-memory nil ; v0.8.0
:minibuffer-mode nil :minibuffer-selected-idx 0 ; v0.8.0
:minibuffer-filter "" ; v0.8.0
:wizard-mode :provider-list ; v0.9.0
:wizard-selected-idx 0 :wizard-input "" ; v0.9.0
:wizard-error nil ; v0.9.0
:wizard-providers nil :wizard-current-provider nil ; v0.9.0
:wizard-cascade '(:fg-prob nil :bg-prob nil :fg-det nil :bg-det nil) ; v0.9.0
:wizard-cascade-slot :fg-prob ; v0.9.0
:dirty (list nil nil nil))))
(defun now ()

View File

@@ -357,7 +357,7 @@ Respects CJK/emoji char widths via char-width."
(when style-bits
(remove-attributes win (get-bitmask style-bits)))
(incf x (length text))))
y)
(1+ y))
(defun parse-markdown-blocks (text)
"Split text at ``` code block boundaries."
@@ -545,100 +545,179 @@ Respects CJK/emoji char widths via char-width."
(refresh win)
(- y 1)))
(defun palette-filter (items query)
"Return items from categorized list whose :name or :desc contains QUERY (case-insensitive)."
(if (or (null query) (string= query ""))
items
(let ((q (string-downcase query)))
(loop for group in items
for category = (getf group :category)
for gitems = (getf group :items)
for filtered = (loop for item in gitems
when (or (search q (string-downcase (getf item :name)))
(search q (string-downcase (or (getf item :desc) ""))))
collect item)
when filtered
collect (list :category category :items filtered)))))
(defun view-minibuffer (win)
"Render the bottom-anchored minibuffer panel. Dispatches on :minibuffer-mode."
(case (st :minibuffer-mode)
(:slash-menu (view-slash-menu win))
(:wizard (view-wizard-in-panel win))
(t nil)))
(defun view-palette (win)
"Render centered command palette overlay with filtered items, selection highlight."
(clear win)
(setf (color-pair win) (list (theme-color :border) (theme-color :background)))
(box win 0 0)
(let* ((w (or (width win) 50))
(h (or (height win) 20))
(y 1)
(query (or (st :palette-filter) ""))
(items (palette-filter (st :palette-items) query))
(selected (st :palette-selected-idx))
(flat-index 0)
(visible-start (max 0 (- selected (floor (- h 6) 2)))))
(add-string win (format nil " Command Palette ") :y y :x 2 :n (- w 4) :fgcolor (theme-color :accent))
(incf y)
(add-string win (format nil " > ~a" (if (> (length query) 0) query "type to filter..."))
:y y :x 2 :n (- w 4) :fgcolor (theme-color :input) :attributes '(:underline t))
(incf y)
(dolist (group items)
(let ((category (getf group :category))
(gitems (getf group :items)))
(when (and gitems (< y (1- h)))
(incf y)
(add-string win (format nil "── ~a ──" category) :y y :x 2 :n (- w 4) :fgcolor (theme-color :dim))
(dolist (item gitems)
(when (< y (1- h))
(incf y)
(let* ((name (getf item :name))
(desc (getf item :desc))
(shortcut (getf item :shortcut))
(is-selected (= flat-index selected))
(fg (if is-selected (theme-color :accent) (theme-color :agent))))
(when is-selected
(add-string win (make-string (- w 4) :initial-element #\Space) :y y :x 2 :n (- w 4)
:fgcolor (theme-color :dim) :bgcolor (theme-color :accent)))
(add-string win (format nil " ~a" name) :y y :x 3 :n (- w 6) :fgcolor fg)
(when (and shortcut (> (- w 6) (+ 4 (length shortcut))))
(add-string win shortcut :y y :x (- w (length shortcut) 3) :n (length shortcut) :fgcolor (theme-color :dim)))
(incf flat-index)))))))
(add-string win (format nil " ↑↓ Navigate Enter Execute Esc Close")
:y (- h 1) :x 2 :n (- w 4) :fgcolor (theme-color :dim))
(refresh win)
(- h 1)))
(defvar *slash-commands* nil) ; forward declaration — defined in channel-tui-main
(defun view-wizard (win)
"Render setup wizard overlay: step title, prompt, input, error, progress."
(defun view-slash-menu (win)
"Render the slash-command menu: filter bar, filtered command list, selection highlight."
(clear win)
(setf (color-pair win) (list (theme-color :border) (theme-color :background)))
(box win 0 0)
(let* ((w (or (width win) 60))
(h (or (height win) 15))
(h (or (height win) 10))
(y 1)
(steps (passepartout.channel-tui::wizard-steps))
(step-idx (st :wizard-step))
(step (when (< step-idx (length steps)) (nth step-idx steps)))
(prompt (getf step :prompt))
(title (getf step :title))
(total (length steps))
(filter (or (st :minibuffer-filter) ""))
(commands passepartout.channel-tui::*slash-commands*)
(filtered (if (or (null filter) (string= filter ""))
(mapcar (lambda (c) (list :index (position c commands) :cmd c)) commands)
(let ((q (string-downcase filter)) (i 0) (r nil))
(dolist (c commands (nreverse r))
(when (or (search q (string-downcase (getf c :name)))
(search q (string-downcase (or (getf c :desc) ""))))
(push (list :index i :cmd c) r))
(incf i)))))
(sel (or (st :minibuffer-selected-idx) 0))
(max-visible (- h 3)))
;; Header: filter bar
(add-string win (format nil " Commands") :y y :x 2 :n (- w 4) :fgcolor (theme-color :accent))
(incf y)
(add-string win (format nil " > ~a_" (if (> (length filter) 0) filter "/"))
:y y :x 2 :n (- w 4) :fgcolor (theme-color :input))
(incf y)
;; Command list
(if filtered
(let* ((start (max 0 (- sel (floor max-visible 2))))
(end (min (length filtered) (+ start max-visible)))
(flat-i 0))
(loop for entry across (subseq (coerce filtered 'vector) start end)
for fi from start
for cmd = (getf entry :cmd)
do (let* ((name (getf cmd :name))
(desc (getf cmd :desc))
(selected (= fi sel))
(fg (if selected (theme-color :highlight) (theme-color :agent))))
(when selected
(add-string win (make-string (- w 4) :initial-element #\Space) :y y :x 2 :n (- w 4)
:fgcolor (theme-color :dim) :bgcolor (theme-color :highlight)))
(let ((prefix (if selected " > " " ")))
(add-string win (format nil "~a~a" prefix name) :y y :x 3 :n (min (- w 6) 25) :fgcolor fg)
(when desc
(add-string win (format nil " — ~a" desc) :y y :x 28 :n (min (- w 30) (length desc)) :fgcolor (theme-color :dim))))
(incf y))))
(progn
(add-string win " (no matching commands)" :y y :x 2 :n (- w 4) :fgcolor (theme-color :dim))
(incf y)))
;; Footer
(add-string win " ↑↓ Navigate Enter Execute Esc Close"
:y (- h 0) :x 2 :n (- w 4) :fgcolor (theme-color :dim))
(refresh win)
(- h 0)))
(defun view-wizard-in-panel (win)
"Render the setup wizard in the bottom-anchored minibuffer panel. Three modes: provider-list, key-entry, cascade-config."
(clear win)
(setf (color-pair win) (list (theme-color :border) (theme-color :background)))
(box win 0 0)
(let* ((w (or (width win) 70))
(h (or (height win) 14))
(y 1)
(mode (st :wizard-mode))
(error-msg (st :wizard-error))
(input (or (st :wizard-input) "")))
(selected-idx (st :wizard-selected-idx))
(providers (passepartout.channel-tui::wizard-provider-list))
(configured (st :wizard-providers)))
(add-string win "Setup Wizard" :y y :x 2 :n (- w 4) :fgcolor (theme-color :accent))
(incf y 2)
(add-string win (format nil "Step ~d/~d" (1+ step-idx) total) :y y :x 2 :n (- w 4) :fgcolor (theme-color :dim))
(case mode
(:provider-list
(let ((count (/ (length configured) 2)))
(add-string win (format nil "Configure Providers~a"
(if (> count 0) (format nil " — ~d configured" count) ""))
:y y :x 2 :n (- w 4) :fgcolor (theme-color :dim))
(incf y)
(when title
(add-string win title :y y :x 3 :n (- w 6) :fgcolor (theme-color :accent))
(incf y))
(when prompt
(add-string win prompt :y y :x 3 :n (- w 6) :fgcolor (theme-color :agent))
(incf y))
(loop for p in providers
for i from 0
do (let* ((meta (passepartout.channel-tui::wizard-provider-meta p))
(name (car meta))
(key (getf configured p))
(prefix (if (= i selected-idx) "> " " "))
(suffix (if key " ✓" ""))
(color (if (= i selected-idx)
(theme-color :highlight)
(theme-color :dim))))
(add-string win (format nil "~a~a~a" prefix name suffix)
:y y :x 3 :n (- w 6) :fgcolor color)
(incf y)))
(incf y)
(add-string win (format nil "> ~a" input) :y y :x 3 :n (- w 6) :fgcolor (theme-color :input))
(add-string win " Done — configure cascade"
:y y :x 3 :n (- w 6)
:fgcolor (if (>= selected-idx (length providers))
(theme-color :highlight)
(theme-color :dim)))
(when (>= selected-idx (length providers))
(add-string win ">" :y y :x 1 :n 2 :fgcolor (theme-color :highlight))))
(:key-entry
(let* ((provider (st :wizard-current-provider))
(meta (passepartout.channel-tui::wizard-provider-meta provider))
(name (car meta))
(url (cadr meta))
(input (or (st :wizard-input) "")))
(add-string win (format nil "API Key: ~a" name) :y y :x 2 :n (- w 4) :fgcolor (theme-color :agent))
(incf y)
(when url
(add-string win (format nil "Get key at: ~a" url) :y y :x 3 :n (- w 6) :fgcolor (theme-color :dim))
(incf y))
(add-string win "Enter your API key." :y y :x 3 :n (- w 6) :fgcolor (theme-color :dim))
(incf y 2)
(add-string win (format nil "Key: > ~a" input) :y y :x 3 :n (- w 6) :fgcolor (theme-color :input))
(incf y)
(when error-msg
(add-string win (format nil "! ~a" error-msg) :y y :x 3 :n (- w 6) :fgcolor (theme-color :error))
(incf y))
(add-string win "Enter=Next Esc=Cancel Bksp=Edit" :y (- h 2) :x 2 :n (- w 4) :fgcolor (theme-color :dim))
(refresh win)
(- h 1)))
(incf y)
(add-string win "Enter=Save Esc=Back Bksp=Edit Ctrl+U=Clear"
:y (- h 0) :x 2 :n (- w 4) :fgcolor (theme-color :dim))
(return-from view-wizard-in-panel)))
(:cascade-config
(let* ((slot (st :wizard-cascade-slot))
(slot-providers (getf (st :wizard-cascade) slot))
(slot-label (cadr (assoc slot passepartout.channel-tui::*wizard-cascade-labels*)))
(count (/ (length configured) 2)))
(add-string win (format nil "Configure Cascade — ~d provider~:p" count)
:y y :x 2 :n (- w 4) :fgcolor (theme-color :dim))
(incf y)
(add-string win (or slot-label "Unknown") :y y :x 2 :n (- w 4) :fgcolor (theme-color :accent))
(incf y)
(let ((shown nil))
(loop for p in providers
for i from 0
do (when (getf configured p)
(let* ((meta (passepartout.channel-tui::wizard-provider-meta p))
(name (car meta))
(in-slot (member p slot-providers))
(prefix (if (= i selected-idx) "> " " "))
(mark (if in-slot " [✓]" " [ ]"))
(color (if (= i selected-idx)
(theme-color :highlight)
(if in-slot (theme-color :gate-passed) (theme-color :dim)))))
(add-string win (format nil "~a~a~a" prefix name mark)
:y y :x 3 :n (- w 6) :fgcolor color)
(incf y)
(push t shown))))
(unless shown
(add-string win " (no providers configured)"
:y y :x 3 :n (- w 6) :fgcolor (theme-color :dim))
(incf y)))
(incf y)
(add-string win (format nil "Cascade: ~{~a~^, ~}"
(or slot-providers '("(none)")))
:y y :x 3 :n (- w 6) :fgcolor (theme-color :dim))))
(when error-msg
(incf y)
(add-string win (format nil "! ~a" error-msg) :y y :x 3 :n (- w 6) :fgcolor (theme-color :error)))
(let ((footer (case mode
(:provider-list "↑↓ Navigate Enter=Select Esc=Back Ctrl+D=Remove")
(:cascade-config "↑↓ Select Enter=Toggle Tab=Next Quadrant Ctrl+S=Save Esc=Back")
(t ""))))
(when footer
(add-string win footer :y (- h 0) :x 2 :n (- w 4) :fgcolor (theme-color :dim))))
(- h 0)))))
(in-package :passepartout-tui-view-tests)
@@ -662,25 +741,20 @@ Respects CJK/emoji char widths via char-width."
(dolist (name names)
(is (getf presets name) (format nil "~a preset should exist" name)))))
(test test-palette-filter-matches-substring
"Contract v0.8.0: palette-filter returns items matching query."
(let* ((items (list (list :category "Session" :items
(list (list :name "/focus" :desc "Set context" :shortcut nil :action nil)
(list :name "/scope" :desc "Change scope" :shortcut nil :action nil)))))
(filtered (passepartout.channel-tui::palette-filter items "focus")))
(is (= 1 (length (getf (first filtered) :items))))
(is (string= "/focus" (getf (first (getf (first filtered) :items)) :name)))))
(test test-minibuffer-init-state-fields
"Contract v0.8.0: init-state includes minibuffer-mode, selected-idx, filter; excludes palette and wizard-visible."
(passepartout.channel-tui::init-state)
(is (null (passepartout.channel-tui::st :minibuffer-mode)))
(is (= 0 (passepartout.channel-tui::st :minibuffer-selected-idx)))
(is (string= "" (passepartout.channel-tui::st :minibuffer-filter)))
(is (null (getf passepartout.channel-tui::*state* :palette-visible)))
(is (null (getf passepartout.channel-tui::*state* :wizard-visible))))
(test test-palette-filter-case-insensitive
"Contract v0.8.0: palette-filter is case-insensitive."
(let* ((items (list (list :category "View" :items
(list (list :name "/theme" :desc "Switch color" :shortcut nil :action nil)))))
(filtered (passepartout.channel-tui::palette-filter items "THEME")))
(is (= 1 (length (getf (first filtered) :items))))))
(test test-palette-filter-no-match-empty
"Contract v0.8.0: palette-filter returns empty categories on no match."
(let* ((items (list (list :category "View" :items
(list (list :name "/theme" :desc "Colors" :shortcut nil :action nil)))))
(filtered (passepartout.channel-tui::palette-filter items "xyznonexistent")))
(is (null (getf (first filtered) :items)))))
(test test-slash-commands-entry-count
"Contract v0.8.0: *slash-commands* has at least 20 entries, each with :name, :desc, :action."
(let ((cmds passepartout.channel-tui::*slash-commands*))
(is (>= (length cmds) 20))
(dolist (c cmds)
(is (stringp (getf c :name)))
(is (stringp (getf c :desc)))
(is (functionp (getf c :action))))))

View File

@@ -229,8 +229,14 @@ See *tui-theme-presets* for named presets (dark, light, solarized, gruvbox).")
key)))
(defun theme-color (role)
"Returns the Croatoan color for a semantic role."
(or (getf *tui-theme* role) :white))
"Returns the Croatoan color for a semantic role.
Keyword or hex string values are returned as-is; hex strings are
converted to integers that Croatoan can process."
(let ((val (or (getf *tui-theme* role) :white)))
(if (and (stringp val) (> (length val) 0) (eql (char val 0) #\#))
(handler-case (parse-integer (subseq val 1) :radix 16)
(error () val))
val)))
;; v0.8.0: TrueColor helpers
(defun theme-hex-to-rgb (hex-string)
@@ -274,12 +280,14 @@ See *tui-theme-presets* for named presets (dark, light, solarized, gruvbox).")
:search-mode nil :search-query "" ; v0.7.2
:search-matches nil :search-match-idx 0
:sidebar-visible nil ; v0.8.0
:palette-visible nil :palette-filter nil ; v0.8.0
:palette-selected-idx 0 :palette-items nil ; v0.8.0
:wizard-step 0 :wizard-error nil ; v0.8.0
:wizard-visible nil :wizard-input "" ; v0.8.0
:wizard-provider nil :wizard-api-key nil ; v0.8.0
:wizard-memory nil ; v0.8.0
:minibuffer-mode nil :minibuffer-selected-idx 0 ; v0.8.0
:minibuffer-filter "" ; v0.8.0
:wizard-mode :provider-list ; v0.9.0
:wizard-selected-idx 0 :wizard-input "" ; v0.9.0
:wizard-error nil ; v0.9.0
:wizard-providers nil :wizard-current-provider nil ; v0.9.0
:wizard-cascade '(:fg-prob nil :bg-prob nil :fg-det nil :bg-det nil) ; v0.9.0
:wizard-cascade-slot :fg-prob ; v0.9.0
:dirty (list nil nil nil))))
#+end_src

View File

@@ -517,7 +517,7 @@ Respects CJK/emoji char widths via char-width."
(when style-bits
(remove-attributes win (get-bitmask style-bits)))
(incf x (length text))))
y)
(1+ y))
(defun parse-markdown-blocks (text)
"Split text at ``` code block boundaries."
@@ -612,7 +612,7 @@ Respects CJK/emoji char widths via char-width."
(nreverse lines)))
#+end_src
* v0.8.0 — Sidebar + Palette View
* v0.8.0 — Sidebar + Minibuffer View
#+begin_src lisp
(in-package :passepartout.channel-tui)
@@ -711,103 +711,183 @@ Respects CJK/emoji char widths via char-width."
(refresh win)
(- y 1)))
(defun palette-filter (items query)
"Return items from categorized list whose :name or :desc contains QUERY (case-insensitive)."
(if (or (null query) (string= query ""))
items
(let ((q (string-downcase query)))
(loop for group in items
for category = (getf group :category)
for gitems = (getf group :items)
for filtered = (loop for item in gitems
when (or (search q (string-downcase (getf item :name)))
(search q (string-downcase (or (getf item :desc) ""))))
collect item)
when filtered
collect (list :category category :items filtered)))))
(defun view-minibuffer (win)
"Render the bottom-anchored minibuffer panel. Dispatches on :minibuffer-mode."
(case (st :minibuffer-mode)
(:slash-menu (view-slash-menu win))
(:wizard (view-wizard-in-panel win))
(t nil)))
(defun view-palette (win)
"Render centered command palette overlay with filtered items, selection highlight."
(clear win)
(setf (color-pair win) (list (theme-color :border) (theme-color :background)))
(box win 0 0)
(let* ((w (or (width win) 50))
(h (or (height win) 20))
(y 1)
(query (or (st :palette-filter) ""))
(items (palette-filter (st :palette-items) query))
(selected (st :palette-selected-idx))
(flat-index 0)
(visible-start (max 0 (- selected (floor (- h 6) 2)))))
(add-string win (format nil " Command Palette ") :y y :x 2 :n (- w 4) :fgcolor (theme-color :accent))
(incf y)
(add-string win (format nil " > ~a" (if (> (length query) 0) query "type to filter..."))
:y y :x 2 :n (- w 4) :fgcolor (theme-color :input) :attributes '(:underline t))
(incf y)
(dolist (group items)
(let ((category (getf group :category))
(gitems (getf group :items)))
(when (and gitems (< y (1- h)))
(incf y)
(add-string win (format nil "── ~a ──" category) :y y :x 2 :n (- w 4) :fgcolor (theme-color :dim))
(dolist (item gitems)
(when (< y (1- h))
(incf y)
(let* ((name (getf item :name))
(desc (getf item :desc))
(shortcut (getf item :shortcut))
(is-selected (= flat-index selected))
(fg (if is-selected (theme-color :accent) (theme-color :agent))))
(when is-selected
(add-string win (make-string (- w 4) :initial-element #\Space) :y y :x 2 :n (- w 4)
:fgcolor (theme-color :dim) :bgcolor (theme-color :accent)))
(add-string win (format nil " ~a" name) :y y :x 3 :n (- w 6) :fgcolor fg)
(when (and shortcut (> (- w 6) (+ 4 (length shortcut))))
(add-string win shortcut :y y :x (- w (length shortcut) 3) :n (length shortcut) :fgcolor (theme-color :dim)))
(incf flat-index)))))))
(add-string win (format nil " ↑↓ Navigate Enter Execute Esc Close")
:y (- h 1) :x 2 :n (- w 4) :fgcolor (theme-color :dim))
(refresh win)
(- h 1)))
(defvar *slash-commands* nil) ; forward declaration — defined in channel-tui-main
(defun view-wizard (win)
"Render setup wizard overlay: step title, prompt, input, error, progress."
(defun view-slash-menu (win)
"Render the slash-command menu: filter bar, filtered command list, selection highlight."
(clear win)
(setf (color-pair win) (list (theme-color :border) (theme-color :background)))
(box win 0 0)
(let* ((w (or (width win) 60))
(h (or (height win) 15))
(h (or (height win) 10))
(y 1)
(steps (passepartout.channel-tui::wizard-steps))
(step-idx (st :wizard-step))
(step (when (< step-idx (length steps)) (nth step-idx steps)))
(prompt (getf step :prompt))
(title (getf step :title))
(total (length steps))
(filter (or (st :minibuffer-filter) ""))
(commands passepartout.channel-tui::*slash-commands*)
(filtered (if (or (null filter) (string= filter ""))
(mapcar (lambda (c) (list :index (position c commands) :cmd c)) commands)
(let ((q (string-downcase filter)) (i 0) (r nil))
(dolist (c commands (nreverse r))
(when (or (search q (string-downcase (getf c :name)))
(search q (string-downcase (or (getf c :desc) ""))))
(push (list :index i :cmd c) r))
(incf i)))))
(sel (or (st :minibuffer-selected-idx) 0))
(max-visible (- h 3)))
;; Header: filter bar
(add-string win (format nil " Commands") :y y :x 2 :n (- w 4) :fgcolor (theme-color :accent))
(incf y)
(add-string win (format nil " > ~a_" (if (> (length filter) 0) filter "/"))
:y y :x 2 :n (- w 4) :fgcolor (theme-color :input))
(incf y)
;; Command list
(if filtered
(let* ((start (max 0 (- sel (floor max-visible 2))))
(end (min (length filtered) (+ start max-visible)))
(flat-i 0))
(loop for entry across (subseq (coerce filtered 'vector) start end)
for fi from start
for cmd = (getf entry :cmd)
do (let* ((name (getf cmd :name))
(desc (getf cmd :desc))
(selected (= fi sel))
(fg (if selected (theme-color :highlight) (theme-color :agent))))
(when selected
(add-string win (make-string (- w 4) :initial-element #\Space) :y y :x 2 :n (- w 4)
:fgcolor (theme-color :dim) :bgcolor (theme-color :highlight)))
(let ((prefix (if selected " > " " ")))
(add-string win (format nil "~a~a" prefix name) :y y :x 3 :n (min (- w 6) 25) :fgcolor fg)
(when desc
(add-string win (format nil " — ~a" desc) :y y :x 28 :n (min (- w 30) (length desc)) :fgcolor (theme-color :dim))))
(incf y))))
(progn
(add-string win " (no matching commands)" :y y :x 2 :n (- w 4) :fgcolor (theme-color :dim))
(incf y)))
;; Footer
(add-string win " ↑↓ Navigate Enter Execute Esc Close"
:y (- h 0) :x 2 :n (- w 4) :fgcolor (theme-color :dim))
(refresh win)
(- h 0)))
(defun view-wizard-in-panel (win)
"Render the setup wizard in the bottom-anchored minibuffer panel. Three modes: provider-list, key-entry, cascade-config."
(clear win)
(setf (color-pair win) (list (theme-color :border) (theme-color :background)))
(box win 0 0)
(let* ((w (or (width win) 70))
(h (or (height win) 14))
(y 1)
(mode (st :wizard-mode))
(error-msg (st :wizard-error))
(input (or (st :wizard-input) "")))
(selected-idx (st :wizard-selected-idx))
(providers (passepartout.channel-tui::wizard-provider-list))
(configured (st :wizard-providers)))
(add-string win "Setup Wizard" :y y :x 2 :n (- w 4) :fgcolor (theme-color :accent))
(incf y 2)
(add-string win (format nil "Step ~d/~d" (1+ step-idx) total) :y y :x 2 :n (- w 4) :fgcolor (theme-color :dim))
(case mode
(:provider-list
(let ((count (/ (length configured) 2)))
(add-string win (format nil "Configure Providers~a"
(if (> count 0) (format nil " — ~d configured" count) ""))
:y y :x 2 :n (- w 4) :fgcolor (theme-color :dim))
(incf y)
(when title
(add-string win title :y y :x 3 :n (- w 6) :fgcolor (theme-color :accent))
(incf y))
(when prompt
(add-string win prompt :y y :x 3 :n (- w 6) :fgcolor (theme-color :agent))
(incf y))
(loop for p in providers
for i from 0
do (let* ((meta (passepartout.channel-tui::wizard-provider-meta p))
(name (car meta))
(key (getf configured p))
(prefix (if (= i selected-idx) "> " " "))
(suffix (if key " ✓" ""))
(color (if (= i selected-idx)
(theme-color :highlight)
(theme-color :dim))))
(add-string win (format nil "~a~a~a" prefix name suffix)
:y y :x 3 :n (- w 6) :fgcolor color)
(incf y)))
(incf y)
(add-string win (format nil "> ~a" input) :y y :x 3 :n (- w 6) :fgcolor (theme-color :input))
(add-string win " Done — configure cascade"
:y y :x 3 :n (- w 6)
:fgcolor (if (>= selected-idx (length providers))
(theme-color :highlight)
(theme-color :dim)))
(when (>= selected-idx (length providers))
(add-string win ">" :y y :x 1 :n 2 :fgcolor (theme-color :highlight))))
(:key-entry
(let* ((provider (st :wizard-current-provider))
(meta (passepartout.channel-tui::wizard-provider-meta provider))
(name (car meta))
(url (cadr meta))
(input (or (st :wizard-input) "")))
(add-string win (format nil "API Key: ~a" name) :y y :x 2 :n (- w 4) :fgcolor (theme-color :agent))
(incf y)
(when url
(add-string win (format nil "Get key at: ~a" url) :y y :x 3 :n (- w 6) :fgcolor (theme-color :dim))
(incf y))
(add-string win "Enter your API key." :y y :x 3 :n (- w 6) :fgcolor (theme-color :dim))
(incf y 2)
(add-string win (format nil "Key: > ~a" input) :y y :x 3 :n (- w 6) :fgcolor (theme-color :input))
(incf y)
(when error-msg
(add-string win (format nil "! ~a" error-msg) :y y :x 3 :n (- w 6) :fgcolor (theme-color :error))
(incf y))
(add-string win "Enter=Next Esc=Cancel Bksp=Edit" :y (- h 2) :x 2 :n (- w 4) :fgcolor (theme-color :dim))
(refresh win)
(- h 1)))
(incf y)
(add-string win "Enter=Save Esc=Back Bksp=Edit Ctrl+U=Clear"
:y (- h 0) :x 2 :n (- w 4) :fgcolor (theme-color :dim))
(return-from view-wizard-in-panel)))
(:cascade-config
(let* ((slot (st :wizard-cascade-slot))
(slot-providers (getf (st :wizard-cascade) slot))
(slot-label (cadr (assoc slot passepartout.channel-tui::*wizard-cascade-labels*)))
(count (/ (length configured) 2)))
(add-string win (format nil "Configure Cascade — ~d provider~:p" count)
:y y :x 2 :n (- w 4) :fgcolor (theme-color :dim))
(incf y)
(add-string win (or slot-label "Unknown") :y y :x 2 :n (- w 4) :fgcolor (theme-color :accent))
(incf y)
(let ((shown nil))
(loop for p in providers
for i from 0
do (when (getf configured p)
(let* ((meta (passepartout.channel-tui::wizard-provider-meta p))
(name (car meta))
(in-slot (member p slot-providers))
(prefix (if (= i selected-idx) "> " " "))
(mark (if in-slot " [✓]" " [ ]"))
(color (if (= i selected-idx)
(theme-color :highlight)
(if in-slot (theme-color :gate-passed) (theme-color :dim)))))
(add-string win (format nil "~a~a~a" prefix name mark)
:y y :x 3 :n (- w 6) :fgcolor color)
(incf y)
(push t shown))))
(unless shown
(add-string win " (no providers configured)"
:y y :x 3 :n (- w 6) :fgcolor (theme-color :dim))
(incf y)))
(incf y)
(add-string win (format nil "Cascade: ~{~a~^, ~}"
(or slot-providers '("(none)")))
:y y :x 3 :n (- w 6) :fgcolor (theme-color :dim))))
(when error-msg
(incf y)
(add-string win (format nil "! ~a" error-msg) :y y :x 3 :n (- w 6) :fgcolor (theme-color :error)))
(let ((footer (case mode
(:provider-list "↑↓ Navigate Enter=Select Esc=Back Ctrl+D=Remove")
(:cascade-config "↑↓ Select Enter=Toggle Tab=Next Quadrant Ctrl+S=Save Esc=Back")
(t ""))))
(when footer
(add-string win footer :y (- h 0) :x 2 :n (- w 4) :fgcolor (theme-color :dim))))
(- h 0)))))
#+end_src
* v0.8.0 Tests — Sidebar View
* v0.8.0 Tests — Sidebar View + Minibuffer View
#+begin_src lisp
(in-package :passepartout-tui-view-tests)
@@ -831,26 +911,21 @@ Respects CJK/emoji char widths via char-width."
(dolist (name names)
(is (getf presets name) (format nil "~a preset should exist" name)))))
(test test-palette-filter-matches-substring
"Contract v0.8.0: palette-filter returns items matching query."
(let* ((items (list (list :category "Session" :items
(list (list :name "/focus" :desc "Set context" :shortcut nil :action nil)
(list :name "/scope" :desc "Change scope" :shortcut nil :action nil)))))
(filtered (passepartout.channel-tui::palette-filter items "focus")))
(is (= 1 (length (getf (first filtered) :items))))
(is (string= "/focus" (getf (first (getf (first filtered) :items)) :name)))))
(test test-minibuffer-init-state-fields
"Contract v0.8.0: init-state includes minibuffer-mode, selected-idx, filter; excludes palette and wizard-visible."
(passepartout.channel-tui::init-state)
(is (null (passepartout.channel-tui::st :minibuffer-mode)))
(is (= 0 (passepartout.channel-tui::st :minibuffer-selected-idx)))
(is (string= "" (passepartout.channel-tui::st :minibuffer-filter)))
(is (null (getf passepartout.channel-tui::*state* :palette-visible)))
(is (null (getf passepartout.channel-tui::*state* :wizard-visible))))
(test test-palette-filter-case-insensitive
"Contract v0.8.0: palette-filter is case-insensitive."
(let* ((items (list (list :category "View" :items
(list (list :name "/theme" :desc "Switch color" :shortcut nil :action nil)))))
(filtered (passepartout.channel-tui::palette-filter items "THEME")))
(is (= 1 (length (getf (first filtered) :items))))))
(test test-palette-filter-no-match-empty
"Contract v0.8.0: palette-filter returns empty categories on no match."
(let* ((items (list (list :category "View" :items
(list (list :name "/theme" :desc "Colors" :shortcut nil :action nil)))))
(filtered (passepartout.channel-tui::palette-filter items "xyznonexistent")))
(is (null (getf (first filtered) :items)))))
(test test-slash-commands-entry-count
"Contract v0.8.0: *slash-commands* has at least 19 entries, each with :name, :desc, :action."
(let ((cmds passepartout.channel-tui::*slash-commands*))
(is (>= (length cmds) 19))
(dolist (c cmds)
(is (stringp (getf c :name)))
(is (stringp (getf c :desc)))
(is (functionp (getf c :action))))))
#+end_src