diff --git a/lisp/channel-tui-main.lisp b/lisp/channel-tui-main.lisp index 15e5fdb..739a00c 100644 --- a/lisp/channel-tui-main.lisp +++ b/lisp/channel-tui-main.lisp @@ -3,68 +3,68 @@ (defun on-key (ch) (cond ;; v0.7.1: Esc — interrupt streaming - ((and (eq ch :escape) (st :streaming-text)) + ((and (eq ch :escape) (getf *state* :streaming-text)) (send-daemon (list :type :event :payload '(:action :cancel-stream))) - (when (> (length (st :messages)) 0) - (let ((idx (1- (length (st :messages))))) - (setf (getf (aref (st :messages) idx) :content) + (when (> (length (getf *state* :messages)) 0) + (let ((idx (1- (length (getf *state* :messages))))) + (setf (getf (aref (getf *state* :messages) idx) :content) (concatenate 'string - (getf (aref (st :messages) idx) :content) + (getf (aref (getf *state* :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 (getf (aref (getf *state* :messages) idx) :streaming) nil) + (setf (getf (aref (getf *state* :messages) idx) :time) (now)))) + (setf (getf *state* :streaming-text) nil) + (setf (getf *state* :busy) nil) + (setf (getf *state* :dirty) (list t t nil))) ;; v0.7.2: Esc — exit search mode - ((and (eq ch :escape) (st :search-mode)) - (setf (st :search-mode) nil - (st :search-matches) nil - (st :search-query) "") - (setf (st :dirty) (list nil t nil)) + ((and (eq ch :escape) (getf *state* :search-mode)) + (setf (getf *state* :search-mode) nil + (getf *state* :search-matches) nil + (getf *state* :search-query) "") + (setf (getf *state* :dirty) (list nil t nil)) (add-msg :system "Search exited")) ;; v0.7.2: search mode — Up/Down navigate matches - ((and (st :search-mode) (eq ch :up)) - (let* ((matches (st :search-matches)) - (idx (st :search-match-idx)) + ((and (getf *state* :search-mode) (eq ch :up)) + (let* ((matches (getf *state* :search-matches)) + (idx (getf *state* :search-match-idx)) (new-idx (max 0 (1- idx)))) - (setf (st :search-match-idx) new-idx) + (setf (getf *state* :search-match-idx) new-idx) (when matches - (setf (st :scroll-offset) (nth new-idx matches)) + (setf (getf *state* :scroll-offset) (nth new-idx matches)) (add-msg :system (format nil "Match ~d/~d" (1+ new-idx) (length matches))) - (setf (st :dirty) (list nil t nil))))) - ((and (st :search-mode) (eq ch :down)) - (let* ((matches (st :search-matches)) - (idx (st :search-match-idx)) + (setf (getf *state* :dirty) (list nil t nil))))) + ((and (getf *state* :search-mode) (eq ch :down)) + (let* ((matches (getf *state* :search-matches)) + (idx (getf *state* :search-match-idx)) (new-idx (min (1- (length matches)) (1+ idx)))) - (setf (st :search-match-idx) new-idx) + (setf (getf *state* :search-match-idx) new-idx) (when matches - (setf (st :scroll-offset) (nth new-idx matches)) + (setf (getf *state* :scroll-offset) (nth new-idx matches)) (add-msg :system (format nil "Match ~d/~d" (1+ new-idx) (length matches))) - (setf (st :dirty) (list nil t nil))))) + (setf (getf *state* :dirty) (list nil t nil))))) ;; v0.7.2: search mode — Enter jumps to current match - ((and (st :search-mode) (eq ch :enter)) - (let ((matches (st :search-matches)) - (idx (st :search-match-idx))) + ((and (getf *state* :search-mode) (eq ch :enter)) + (let ((matches (getf *state* :search-matches)) + (idx (getf *state* :search-match-idx))) (when (and matches (>= (length matches) (1+ idx))) - (setf (st :scroll-offset) (nth idx matches)) - (setf (st :search-mode) nil - (st :search-matches) nil - (st :search-query) "") + (setf (getf *state* :scroll-offset) (nth idx matches)) + (setf (getf *state* :search-mode) nil + (getf *state* :search-matches) nil + (getf *state* :search-query) "") (add-msg :system (format nil "Jumped to match ~d" (1+ idx))) - (setf (st :dirty) (list nil t nil))))) + (setf (getf *state* :dirty) (list nil t nil))))) ;; v0.7.1: Tab on empty input — extract then open URL from agent message ((and (eq ch :tab) - (null (st :input-buffer))) - (if (st :url-buffer) + (null (getf *state* :input-buffer))) + (if (getf *state* :url-buffer) ;; Already extracted — now open it (progn - (add-msg :system (format nil "Opening ~a" (st :url-buffer))) - (setf (st :url-buffer) nil)) + (add-msg :system (format nil "Opening ~a" (getf *state* :url-buffer))) + (setf (getf *state* :url-buffer) nil)) ;; Extract URL from last agent message (let ((url nil)) - (loop for i from (1- (length (st :messages))) downto 0 - for msg = (aref (st :messages) i) + (loop for i from (1- (length (getf *state* :messages))) downto 0 + for msg = (aref (getf *state* :messages) i) for content = (getf msg :content) for role = (getf msg :role) while (eq role :agent) @@ -78,69 +78,69 @@ (return))))) (if url (progn - (setf (st :url-buffer) url) + (setf (getf *state* :url-buffer) url) (add-msg :system (format nil "Press Tab to open ~a" url)) - (setf (st :dirty) (list t t nil))) + (setf (getf *state* :dirty) (list t t nil))) nil)))) ;; v0.7.0: Ctrl key bindings ((eq ch :ctrl-u) - (setf (st :input-buffer) nil) - (setf (st :dirty) (list nil nil t))) + (setf (getf *state* :input-buffer) nil) + (setf (getf *state* :dirty) (list nil nil t))) ((eq ch :ctrl-w) - (let ((buf (st :input-buffer))) + (let ((buf (getf *state* :input-buffer))) (loop while (and buf (char= (first buf) #\Space)) do (pop buf)) (loop while (and buf (char/= (first buf) #\Space)) do (pop buf)) - (setf (st :input-buffer) buf) - (setf (st :dirty) (list nil nil t)))) + (setf (getf *state* :input-buffer) buf) + (setf (getf *state* :dirty) (list nil nil t)))) ((eq ch :ctrl-a) - (setf (st :cursor-pos) 0)) + (setf (getf *state* :cursor-pos) 0)) ((eq ch :ctrl-e) - (setf (st :cursor-pos) (length (st :input-buffer)))) + (setf (getf *state* :cursor-pos) (length (getf *state* :input-buffer)))) ((eq ch :ctrl-l) - (setf (st :dirty) (list t t t))) + (setf (getf *state* :dirty) (list t t t))) ((eq ch :ctrl-d) - (when (or (null (st :input-buffer)) (string= "" (input-string))) + (when (or (null (getf *state* :input-buffer)) (string= "" (input-string))) (add-msg :system "Goodbye. Run /quit or press Ctrl+D again to exit."))) ((eq ch :ctrl-f) (add-msg :system "Use /search to find messages")) ((eq ch :ctrl-g) (let ((gate-idx nil)) - (loop for i from (1- (length (st :messages))) downto 0 - for m = (aref (st :messages) i) + (loop for i from (1- (length (getf *state* :messages))) downto 0 + for m = (aref (getf *state* :messages) i) when (and (getf m :gate-trace) (listp (getf m :gate-trace))) do (setf gate-idx i) (loop-finish)) (if gate-idx - (let ((cg (st :collapsed-gates))) + (let ((cg (getf *state* :collapsed-gates))) (if (member gate-idx cg) - (setf (st :collapsed-gates) (remove gate-idx cg)) - (push gate-idx (st :collapsed-gates))) + (setf (getf *state* :collapsed-gates) (remove gate-idx cg)) + (push gate-idx (getf *state* :collapsed-gates))) (add-msg :system (format nil "Gate trace ~a for msg ~a" - (if (member gate-idx (st :collapsed-gates)) "hidden" "shown") + (if (member gate-idx (getf *state* :collapsed-gates)) "hidden" "shown") gate-idx)) - (setf (st :dirty) (list nil t nil))) + (setf (getf *state* :dirty) (list nil t nil))) (add-msg :system "No gate trace to toggle")))) ((eq ch :ctrl-x) - (setf (st :pending-ctrl-x) t)) - ((and (st :pending-ctrl-x) (eq ch :ctrl-e)) ; Ctrl+X+E — editor - (setf (st :pending-ctrl-x) nil) + (setf (getf *state* :pending-ctrl-x) t)) + ((and (getf *state* :pending-ctrl-x) (eq ch :ctrl-e)) ; Ctrl+X+E — editor + (setf (getf *state* :pending-ctrl-x) nil) (add-msg :system "Opening $EDITOR... save and exit to return.") - (setf (st :dirty) (list t t nil))) - ((and (st :pending-ctrl-x) (not (eq ch :ctrl-e))) ; cancel Ctrl+X - (setf (st :pending-ctrl-x) nil) + (setf (getf *state* :dirty) (list t t nil))) + ((and (getf *state* :pending-ctrl-x) (not (eq ch :ctrl-e))) ; cancel Ctrl+X + (setf (getf *state* :pending-ctrl-x) nil) (on-key ch) (return-from on-key nil)) ;; Enter ((eq ch :enter) ;; Multi-line: if buffer ends with \, strip it and insert newline - (if (and (st :input-buffer) (eql (first (st :input-buffer)) #\\)) - (progn (pop (st :input-buffer)) - (push #\Newline (st :input-buffer)) - (setf (st :dirty) (list nil nil t))) + (if (and (getf *state* :input-buffer) (eql (first (getf *state* :input-buffer)) #\\)) + (progn (pop (getf *state* :input-buffer)) + (push #\Newline (getf *state* :input-buffer)) + (setf (getf *state* :dirty) (list nil nil t))) (let ((text (string-trim '(#\Space #\Tab) (input-string)))) (when (> (length text) 0) - (push text (st :input-history)) - (setf (st :input-hpos) 0) - (setf (st :scroll-offset) 0) + (push text (getf *state* :input-history)) + (setf (getf *state* :input-hpos) 0) + (setf (getf *state* :scroll-offset) 0) (cond ;; v0.7.2: undo/redo ((string-equal text "/undo") @@ -167,7 +167,7 @@ ;; /help command ;; /why command — show last gate trace ((string-equal text "/why") - (let ((msgs (st :messages)) + (let ((msgs (getf *state* :messages)) (found nil)) (loop for i from (1- (length msgs)) downto 0 for m = (aref msgs i) @@ -220,9 +220,9 @@ (add-msg :system "No tags configured. Set TAG_CATEGORIES env var.")))) ;; /context command — section breakdown with token estimates ((string-equal text "/context") - (let* ((msg-count (length (st :messages))) - (focus (or (st :foveal-id) "none")) - (id-tokens (min 200 (floor (+ 150 (length (or (st :focus-scope) ""))) 4))) + (let* ((msg-count (length (getf *state* :messages))) + (focus (or (getf *state* :foveal-id) "none")) + (id-tokens (min 200 (floor (+ 150 (length (or (getf *state* :focus-scope) ""))) 4))) (tool-tokens (if (boundp 'passepartout::*cognitive-tool-registry*) (floor (* (hash-table-count passepartout::*cognitive-tool-registry*) 40) 4) 50)) @@ -269,7 +269,7 @@ (add-msg :system "Memory not available")))) ;; /context dropped — estimate pruned nodes from budget ((string-equal text "/context dropped") - (let* ((msg-count (length (st :messages))) + (let* ((msg-count (length (getf *state* :messages))) (est-total (* msg-count 60)) (budget 8192) (dropped-msgs (if (> est-total budget) @@ -284,7 +284,7 @@ ;; /search command — message search ((and (>= (length text) 8) (string-equal (subseq text 0 8) "/search ")) (let* ((query (string-downcase (string-trim '(#\Space) (subseq text 8)))) - (msgs (st :messages)) + (msgs (getf *state* :messages)) (total (length msgs)) (matches nil)) (loop for i from 0 below total @@ -294,10 +294,10 @@ do (push i matches)) (setf matches (nreverse matches)) ;; Enter search mode - (setf (st :search-mode) t - (st :search-query) query - (st :search-matches) matches - (st :search-match-idx) 0) + (setf (getf *state* :search-mode) t + (getf *state* :search-query) query + (getf *state* :search-matches) matches + (getf *state* :search-match-idx) 0) (if matches (add-msg :system (format nil "Search: ~d matches for '~a' (1/~d) — Up/Down nav, Enter jump, Esc exit" (length matches) query (length matches))) @@ -445,11 +445,11 @@ (uiop:ensure-all-directories-exist (list hist-file)) (with-open-file (out hist-file :direction :output :if-exists :supersede :if-does-not-exist :create) - (dolist (entry (reverse (st :input-history))) + (dolist (entry (reverse (getf *state* :input-history))) (write-line entry out)))) (add-msg :system "* Goodbye *") (send-daemon (list :type :event :payload '(:action :quit))) - (setf (st :running) nil)) + (setf (getf *state* :running) nil)) ;; /reconnect — re-establish daemon connection ((string-equal text "/reconnect") (disconnect-daemon) @@ -457,12 +457,12 @@ ;; Normal message (t (add-msg :user text) - (setf (st :busy) t) + (setf (getf *state* :busy) t) (send-daemon (list :type :event :payload (list :sensor :user-input :text text))))) - (setf (st :input-buffer) nil) - (setf (st :cursor-pos) 0) - (setf (st :dirty) (list t t t)))))) + (setf (getf *state* :input-buffer) nil) + (setf (getf *state* :cursor-pos) 0) + (setf (getf *state* :dirty) (list t t t)))))) ;; Tab — command completion (v0.7.0: extended with subcommand + file paths) ((eq ch :tab) (let ((text (input-string))) @@ -481,8 +481,8 @@ (string-equal n partial :end2 (length partial)))) names))) (when match - (setf (st :input-buffer) (reverse (coerce (concatenate 'string "@" match) 'list))) - (setf (st :dirty) (list nil nil t))))) + (setf (getf *state* :input-buffer) (reverse (coerce (concatenate 'string "@" match) 'list))) + (setf (getf *state* :dirty) (list nil nil t))))) ;; /theme subcommand ((and (>= (length text) 7) (string-equal (subseq text 0 7) "/theme ")) (let* ((partial (string-trim '(#\Space) (subseq text 7))) @@ -490,8 +490,8 @@ (match (if (string= partial "") (first names) (find partial names :test #'string-equal)))) (when match - (setf (st :input-buffer) (reverse (coerce (concatenate 'string "/theme " match) 'list))) - (setf (st :dirty) (list nil nil t))))) + (setf (getf *state* :input-buffer) (reverse (coerce (concatenate 'string "/theme " match) 'list))) + (setf (getf *state* :dirty) (list nil nil t))))) ;; /focus subcommand ((and (>= (length text) 7) (string-equal (subseq text 0 7) "/focus ")) (let* ((partial (string-trim '(#\Space) (subseq text 7))) @@ -506,8 +506,8 @@ (string-equal d partial :end2 (length partial)))) dirs)))) (when match - (setf (st :input-buffer) (reverse (coerce (concatenate 'string "/focus " match) 'list))) - (setf (st :dirty) (list nil nil t))))) + (setf (getf *state* :input-buffer) (reverse (coerce (concatenate 'string "/focus " match) 'list))) + (setf (getf *state* :dirty) (list nil nil t))))) ;; Command prefix / ((and (> (length text) 1) (eql (char text 0) #\/)) (let* ((cmds '("/eval" "/focus" "/scope" "/unfocus" "/help" "/theme" "/reconnect" "/quit")) @@ -515,51 +515,51 @@ (lambda (in cmd) (and (>= (length cmd) (length in)) (string-equal cmd in :end1 (length in))))))) (when match - (setf (st :input-buffer) (reverse (coerce match 'list))) + (setf (getf *state* :input-buffer) (reverse (coerce match 'list))) (when (member match '("/eval" "/focus" "/scope") :test #'string=) - (push #\Space (st :input-buffer))) - (setf (st :dirty) (list nil nil t)))))))) + (push #\Space (getf *state* :input-buffer))) + (setf (getf *state* :dirty) (list nil nil t))))))) ;; Backspace ((eq ch :backspace) (input-delete-char) - (setf (st :dirty) (list nil nil t))) + (setf (getf *state* :dirty) (list nil nil t))) ;; Left arrow ((eq ch :left) - (when (> (or (st :cursor-pos) 0) 0) - (decf (st :cursor-pos)) - (setf (st :dirty) (list nil nil t)))) + (when (> (or (getf *state* :cursor-pos) 0) 0) + (decf (getf *state* :cursor-pos)) + (setf (getf *state* :dirty) (list nil nil t)))) ;; Right arrow ((eq ch :right) - (when (< (or (st :cursor-pos) 0) (length (st :input-buffer))) - (incf (st :cursor-pos)) - (setf (st :dirty) (list nil nil t)))) + (when (< (or (getf *state* :cursor-pos) 0) (length (getf *state* :input-buffer))) + (incf (getf *state* :cursor-pos)) + (setf (getf *state* :dirty) (list nil nil t)))) ;; Up arrow ((eq ch :up) - (let* ((h (st :input-history)) (p (st :input-hpos))) + (let* ((h (getf *state* :input-history)) (p (getf *state* :input-hpos))) (when (and h (< p (1- (length h)))) - (incf (st :input-hpos)) - (setf (st :input-buffer) - (reverse (coerce (nth (st :input-hpos) h) 'list))) - (setf (st :dirty) (list nil nil t))))) + (incf (getf *state* :input-hpos)) + (setf (getf *state* :input-buffer) + (reverse (coerce (nth (getf *state* :input-hpos) h) 'list))) + (setf (getf *state* :dirty) (list nil nil t))))) ;; Down arrow ((eq ch :down) - (when (> (st :input-hpos) 0) - (decf (st :input-hpos)) - (let ((h (st :input-history))) - (setf (st :input-buffer) - (if (and h (< (st :input-hpos) (length h))) - (reverse (coerce (nth (st :input-hpos) h) 'list)) + (when (> (getf *state* :input-hpos) 0) + (decf (getf *state* :input-hpos)) + (let ((h (getf *state* :input-history))) + (setf (getf *state* :input-buffer) + (if (and h (< (getf *state* :input-hpos) (length h))) + (reverse (coerce (nth (getf *state* :input-hpos) h) 'list)) nil)) - (setf (st :dirty) (list nil nil t))))) + (setf (getf *state* :dirty) (list nil nil t))))) ;; PageUp — scroll back by page (10 lines) ((eq ch :ppage) - (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))) + (let ((max-offset (max 0 (- (length (getf *state* :messages)) 1)))) + (setf (getf *state* :scroll-offset) (min max-offset (+ (getf *state* :scroll-offset) 10)))) + (setf (getf *state* :dirty) (list nil t nil))) ;; PageDown — scroll forward by page ((eq ch :npage) - (setf (st :scroll-offset) (max 0 (- (st :scroll-offset) 10))) - (setf (st :dirty) (list nil t nil))) + (setf (getf *state* :scroll-offset) (max 0 (- (getf *state* :scroll-offset) 10))) + (setf (getf *state* :dirty) (list nil t nil))) ;; Printable (t (let ((chr (typecase ch @@ -571,17 +571,17 @@ (t nil)))) (when (and chr (graphic-char-p chr)) (input-insert-char chr) - (setf (st :dirty) (list nil nil t)))))))) + (setf (getf *state* :dirty) (list nil nil t)))))))) ;; v0.7.2 — resolve-hitl-panel: marks panel as resolved after approve/deny (defun resolve-hitl-panel (decision) "Mark the most recent HITL panel message as resolved with DECISION." - (loop for i from (1- (length (st :messages))) downto 0 - for m = (aref (st :messages) i) + (loop for i from (1- (length (getf *state* :messages))) downto 0 + for m = (aref (getf *state* :messages) i) when (and (getf m :panel) (not (getf m :panel-resolved))) do (setf (getf m :panel-resolved) decision) - (setf (aref (st :messages) i) m) - (setf (st :dirty) (list nil t nil)) + (setf (aref (getf *state* :messages) i) m) + (setf (getf *state* :dirty) (list nil t nil)) (loop-finish))) ;; v0.7.2 — self-help-lookup: read USER_MANUAL.org and find matching sections @@ -646,50 +646,50 @@ (add-msg :system (format nil "┌─ Permission Required ─┐~%~a~%Action: ~a~%Respond: /approve HITL-xxxx or /deny HITL-xxxx" hitl-msg explanation) :panel t)) - (setf (st :dirty) (list nil t nil)) + (setf (getf *state* :dirty) (list nil t nil)) (return-from on-daemon-msg nil)) ;; v0.7.1: streaming chunk (when (eq msg-type :stream-chunk) (cond ((string= text "") ;; Final chunk: stamp time, clear streaming - (when (> (length (st :messages)) 0) - (let ((idx (1- (length (st :messages))))) - (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 nil t nil)) + (when (> (length (getf *state* :messages)) 0) + (let ((idx (1- (length (getf *state* :messages))))) + (setf (getf (aref (getf *state* :messages) idx) :streaming) nil) + (setf (getf (aref (getf *state* :messages) idx) :time) (now)))) + (setf (getf *state* :streaming-text) nil) + (setf (getf *state* :busy) nil) + (setf (getf *state* :dirty) (list nil t nil)) (return-from on-daemon-msg nil)) - ((null (st :streaming-text)) + ((null (getf *state* :streaming-text)) ;; First chunk: add new streaming message - (setf (st :streaming-text) "") - (setf (st :busy) nil) + (setf (getf *state* :streaming-text) "") + (setf (getf *state* :busy) nil) (add-msg :agent text) - (let ((idx (1- (length (st :messages))))) - (setf (getf (aref (st :messages) idx) :streaming) t)) - (setf (st :streaming-text) text) - (setf (st :dirty) (list nil t nil)) + (let ((idx (1- (length (getf *state* :messages))))) + (setf (getf (aref (getf *state* :messages) idx) :streaming) t)) + (setf (getf *state* :streaming-text) text) + (setf (getf *state* :dirty) (list nil t nil)) (return-from on-daemon-msg nil)) (t ;; Subsequent chunk: append - (let* ((new-text (concatenate 'string (st :streaming-text) text)) - (idx (1- (length (st :messages))))) - (setf (st :streaming-text) new-text) - (setf (getf (aref (st :messages) idx) :content) new-text) - (setf (st :dirty) (list nil t nil))) + (let* ((new-text (concatenate 'string (getf *state* :streaming-text) text)) + (idx (1- (length (getf *state* :messages))))) + (setf (getf *state* :streaming-text) new-text) + (setf (getf (aref (getf *state* :messages) idx) :content) new-text) + (setf (getf *state* :dirty) (list nil t nil))) (return-from on-daemon-msg nil)))) - (when rule-count (setf (st :rule-count) rule-count)) - (when foveal-id (setf (st :foveal-id) foveal-id)) + (when rule-count (setf (getf *state* :rule-count) rule-count)) + (when foveal-id (setf (getf *state* :foveal-id) foveal-id)) (cond - (text (setf (st :busy) nil) + (text (setf (getf *state* :busy) nil) (add-msg :agent text :gate-trace gate-trace)) ((eq action :handshake) (add-msg :system (format nil "Connected v~a" (getf payload :version)))) (t (add-msg :agent (format nil "~a" msg)))))) (defun send-daemon (msg) - (let ((s (st :stream))) + (let ((s (getf *state* :stream))) (when (and s (open-stream-p s)) (handler-case (progn @@ -717,7 +717,7 @@ (defun reader-loop (s) (let ((consecutive-nils 0)) - (loop while (and (st :running) (open-stream-p s)) + (loop while (and (getf *state* :running) (open-stream-p s)) do (let ((msg (recv-daemon s))) (if msg (progn (queue-event (list :type :daemon :payload msg)) @@ -736,8 +736,8 @@ (with-open-file (in hist-file :direction :input) (loop for line = (read-line in nil nil) while line - do (push line (st :input-history)))) - (setf (st :input-history) (nreverse (st :input-history)))))) + do (push line (getf *state* :input-history)))) + (setf (getf *state* :input-history) (nreverse (getf *state* :input-history)))))) (defun connect-daemon (&optional (host "127.0.0.1") (port 9105)) (add-msg :system "* Connecting to daemon... *") @@ -746,9 +746,9 @@ do (sleep backoff) (handler-case (let ((s (usocket:socket-connect host port :timeout 5))) - (setf (st :stream) (usocket:socket-stream s) - (st :connected) t) - (bt:make-thread (lambda () (reader-loop (st :stream))) + (setf (getf *state* :stream) (usocket:socket-stream s) + (getf *state* :connected) t) + (bt:make-thread (lambda () (reader-loop (getf *state* :stream))) :name "tui-reader") (add-msg :system (format nil "* Connected v~a *" "0.5.0")) (return-from connect-daemon t)) @@ -764,9 +764,9 @@ nil) (defun disconnect-daemon () - (when (st :stream) - (ignore-errors (close (st :stream))) - (setf (st :stream) nil (st :connected) nil) + (when (getf *state* :stream) + (ignore-errors (close (getf *state* :stream))) + (setf (getf *state* :stream) nil (getf *state* :connected) nil) (add-msg :system "* Disconnected *"))) (defun tui-main () @@ -776,7 +776,7 @@ (let* ((swank-port (or (ignore-errors (parse-integer (uiop:getenv "TUI_SWANK_PORT"))) 4006))) - (setf (st :dirty) (list t t t)) + (setf (getf *state* :dirty) (list t t t)) (connect-daemon) (when (> swank-port 0) (handler-case @@ -793,17 +793,17 @@ (let ((prev-fb (cl-tty.rendering:make-framebuffer w h)) (curr-fb (cl-tty.rendering:make-framebuffer w h))) ;; Initial render - (redraw be curr-fb w h) + (redraw curr-fb w h) (cl-tty.rendering:flush-framebuffer prev-fb curr-fb be) (rotatef prev-fb curr-fb) - (loop while (st :running) do + (loop while (getf *state* :running) do (dolist (ev (drain-queue)) (cond ((eq (getf ev :type) :daemon) (on-daemon-msg (getf ev :payload))) ((eq (getf ev :type) :disconnected) - (setf (st :connected) nil - (st :busy) nil) + (setf (getf *state* :connected) nil + (getf *state* :busy) nil) (add-msg :system "* Connection lost — type /reconnect to retry *")))) (multiple-value-bind (type data) (cl-tty.input:read-event be :timeout 0) @@ -812,7 +812,7 @@ (multiple-value-setq (w h) (cl-tty.backend:backend-size be)) (setf prev-fb (cl-tty.rendering:make-framebuffer w h) curr-fb (cl-tty.rendering:make-framebuffer w h)) - (setf (st :dirty) (list t t t))) + (setf (getf *state* :dirty) (list t t t))) (data (let ((ch (typecase data (cl-tty.input:key-event @@ -823,13 +823,13 @@ k))) (t data)))) (on-key ch))))))) - (when (or (first (st :dirty)) (second (st :dirty)) (third (st :dirty))) + (when (or (first (getf *state* :dirty)) (second (getf *state* :dirty)) (third (getf *state* :dirty))) (cl-tty.backend:backend-clear curr-fb) - (redraw be curr-fb w h) + (redraw curr-fb w h) (cl-tty.rendering:flush-framebuffer prev-fb curr-fb be) (rotatef prev-fb curr-fb)) - (sleep 0.1)))) - (disconnect-daemon)))) + (sleep 0.1))) + (disconnect-daemon))) (eval-when (:compile-toplevel :load-toplevel :execute) (ql:quickload :fiveam :silent t)) @@ -846,19 +846,19 @@ (fiveam:test test-init-state "Contract model.1: init-state returns fresh state plist with required keys." (init-state) - (fiveam:is (eq t (st :running))) - (fiveam:is (eq :chat (st :mode))) - (fiveam:is (eq nil (st :connected))) - (fiveam:is (eq nil (st :stream))) - (fiveam:is (zerop (length (st :messages)))) - (fiveam:is (eq 0 (st :scroll-offset))) - (fiveam:is (eq nil (st :busy)))) + (fiveam:is (eq t (getf *state* :running))) + (fiveam:is (eq :chat (getf *state* :mode))) + (fiveam:is (eq nil (getf *state* :connected))) + (fiveam:is (eq nil (getf *state* :stream))) + (fiveam:is (zerop (length (getf *state* :messages)))) + (fiveam:is (eq 0 (getf *state* :scroll-offset))) + (fiveam:is (eq nil (getf *state* :busy)))) (fiveam:test test-add-msg "Contract model.2: add-msg appends a message with role, content, and time." (init-state) (add-msg :user "hello") - (let* ((msgs (st :messages)) + (let* ((msgs (getf *state* :messages)) (msg (aref msgs 0))) (fiveam:is (eq :user (getf msg :role))) (fiveam:is (string= "hello" (getf msg :content))) @@ -868,9 +868,9 @@ (fiveam:test test-add-msg-dirty-flag "Contract model.2: add-msg sets dirty flags for status and chat." (init-state) - (setf (st :dirty) (list nil nil nil)) + (setf (getf *state* :dirty) (list nil nil nil)) (add-msg :system "boot") - (let ((dirty (st :dirty))) + (let ((dirty (getf *state* :dirty))) (fiveam:is (eq t (first dirty))) (fiveam:is (eq t (second dirty))) (fiveam:is (eq nil (third dirty))))) @@ -898,7 +898,7 @@ ;; Input buffer should be cleared (fiveam:is (string= "" (input-string))) ;; A user message should be in the message list - (let ((msgs (st :messages))) + (let ((msgs (getf *state* :messages))) (fiveam:is (>= (length msgs) 1)) (let ((last (aref msgs 0))) (fiveam:is (eq :user (getf last :role))) @@ -911,7 +911,7 @@ (dolist (ch (coerce "/eval (+ 1 2)" 'list)) (on-key (char-code ch))) (on-key 343) - (let ((msgs (st :messages))) + (let ((msgs (getf *state* :messages))) (fiveam:is (>= (length msgs) 1)) (let ((last-msg (aref msgs 0))) (fiveam:is (eq :system (getf last-msg :role))) @@ -933,7 +933,7 @@ (dolist (ch (coerce "/focus myapp" 'list)) (on-key (char-code ch))) (on-key 343) - (let ((msg (aref (st :messages) 0))) + (let ((msg (aref (getf *state* :messages) 0))) (fiveam:is (eq :system (getf msg :role))))) (fiveam:test test-on-key-scope-command @@ -942,7 +942,7 @@ (dolist (ch (coerce "/scope memex" 'list)) (on-key (char-code ch))) (on-key 343) - (let ((msg (aref (st :messages) 0))) + (let ((msg (aref (getf *state* :messages) 0))) (fiveam:is (eq :system (getf msg :role))))) (fiveam:test test-on-key-unfocus-command @@ -951,7 +951,7 @@ (dolist (ch (coerce "/unfocus" 'list)) (on-key (char-code ch))) (on-key 343) - (let ((msg (aref (st :messages) 0))) + (let ((msg (aref (getf *state* :messages) 0))) (fiveam:is (eq :system (getf msg :role))))) (fiveam:test test-on-key-tab-completion @@ -986,22 +986,22 @@ (dolist (ch (coerce "/help" 'list)) (on-key (char-code ch))) (on-key 343) - (let ((msgs (st :messages))) + (let ((msgs (getf *state* :messages))) (fiveam:is (>= (length msgs) 3)) (fiveam:is (some (lambda (m) (search "/eval" (getf m :content))) msgs)))) (fiveam:test test-activity-indicator "Contract model: :busy flag is set on send and cleared on agent response." (init-state) - (fiveam:is (eq nil (st :busy))) + (fiveam:is (eq nil (getf *state* :busy))) ;; Simulate sending a normal message (sets busy) (dolist (ch (coerce "hello" 'list)) (on-key (char-code ch))) (on-key 343) - (fiveam:is (eq t (st :busy))) + (fiveam:is (eq t (getf *state* :busy))) ;; Simulate receiving an agent response (clears busy) (on-daemon-msg '(:type :event :payload (:text "hi back"))) - (fiveam:is (eq nil (st :busy)))) + (fiveam:is (eq nil (getf *state* :busy)))) (fiveam:test test-theme "Contract view: *tui-theme* provides color mappings." @@ -1021,21 +1021,21 @@ (fiveam:test test-on-key-ctrl-l-redraws "Contract 1/v0.7.0: Ctrl+L sets all dirty flags." (init-state) - (setf (st :dirty) (list nil nil nil)) + (setf (getf *state* :dirty) (list nil nil nil)) (on-key 12) ; Ctrl+L - (let ((d (st :dirty))) + (let ((d (getf *state* :dirty))) (fiveam:is (eq t (first d))) (fiveam:is (eq t (second d))))) (fiveam:test test-scroll-notify "Contract/v0.7.0: add-msg sets scroll-notify when scrolled up." (init-state) - (setf (st :scroll-at-bottom) nil) + (setf (getf *state* :scroll-at-bottom) nil) (add-msg :agent "hi") - (fiveam:is (eq t (st :scroll-notify))) - (setf (st :scroll-at-bottom) t (st :scroll-notify) nil) + (fiveam:is (eq t (getf *state* :scroll-notify))) + (setf (getf *state* :scroll-at-bottom) t (getf *state* :scroll-notify) nil) (add-msg :agent "hi2") - (fiveam:is (eq nil (st :scroll-notify)))) + (fiveam:is (eq nil (getf *state* :scroll-notify)))) (fiveam:test test-tab-subcommand "Contract/v0.7.0: Tab completes subcommand for /theme." @@ -1051,7 +1051,7 @@ (init-state) (on-daemon-msg '(:type :stream-chunk :payload (:text "Hello"))) (on-daemon-msg '(:type :stream-chunk :payload (:text " world"))) - (let ((msgs (st :messages))) + (let ((msgs (getf *state* :messages))) (fiveam:is (= 1 (length msgs))) (let ((msg (aref msgs 0))) (fiveam:is (eq :agent (getf msg :role))) @@ -1063,35 +1063,35 @@ (init-state) (on-daemon-msg '(:type :stream-chunk :payload (:text "Hi"))) (on-daemon-msg '(:type :stream-chunk :payload (:text ""))) - (let ((msg (aref (st :messages) 0))) + (let ((msg (aref (getf *state* :messages) 0))) (fiveam:is (stringp (getf msg :time))) (fiveam:is (string= "Hi" (getf msg :content))) - (fiveam:is (null (st :streaming-text))))) + (fiveam:is (null (getf *state* :streaming-text))))) (fiveam:test test-stream-interrupt "Contract/v0.7.1: Esc during streaming appends [interrupted] and finalizes." (init-state) (on-daemon-msg '(:type :stream-chunk :payload (:text "partial"))) (on-key 27) - (let ((msg (aref (st :messages) 0))) + (let ((msg (aref (getf *state* :messages) 0))) (fiveam:is (stringp (getf msg :time))) (fiveam:is (search "[interrupted]" (getf msg :content))) - (fiveam:is (null (st :streaming-text))) - (fiveam:is (null (st :busy))))) + (fiveam:is (null (getf *state* :streaming-text))) + (fiveam:is (null (getf *state* :busy))))) (fiveam:test test-stream-check-skip "Contract/v0.7.1: Esc without active streaming does nothing." (init-state) (on-key 27) - (fiveam:is (null (st :streaming-text))) - (fiveam:is (= 0 (length (st :messages))))) + (fiveam:is (null (getf *state* :streaming-text))) + (fiveam:is (= 0 (length (getf *state* :messages))))) (fiveam:test test-tab-open-url "Contract/v0.7.1: Tab on empty input with URL message extracts URL." (init-state) (add-msg :agent "visit https://example.com for info") (on-key 9) - (fiveam:is (string= "https://example.com" (st :url-buffer)))) + (fiveam:is (string= "https://example.com" (getf *state* :url-buffer)))) ;; ── v0.7.2 HITL Panels ── @@ -1102,7 +1102,7 @@ :payload (:sensor :approval-required :action (:TYPE :REQUEST :PAYLOAD (:TOOL "shell")) :message "rm -rf blocked"))) - (let ((m (aref (st :messages) 0))) + (let ((m (aref (getf *state* :messages) 0))) (fiveam:is (eq :system (getf m :role))) (fiveam:is (getf m :panel)) (fiveam:is (search "rm -rf" (getf m :content))))) @@ -1116,11 +1116,11 @@ (on-key (char-code ch))) (on-key 13) ;; Panel message (index 0) should be marked resolved - (let ((m (aref (st :messages) 0))) + (let ((m (aref (getf *state* :messages) 0))) (fiveam:is (getf m :panel)) (fiveam:is (eq :approved (getf m :panel-resolved)))) ;; Last message should be the approval confirmation - (let ((m (aref (st :messages) (1- (length (st :messages)))))) + (let ((m (aref (getf *state* :messages) (1- (length (getf *state* :messages)))))) (fiveam:is (search "Approved" (getf m :content))))) (fiveam:test test-hitl-panel-after-deny @@ -1131,7 +1131,7 @@ (dolist (ch (coerce "/deny HITL-deny" 'list)) (on-key (char-code ch))) (on-key 13) - (let ((m (aref (st :messages) 0))) + (let ((m (aref (getf *state* :messages) 0))) (fiveam:is (getf m :panel)) (fiveam:is (eq :denied (getf m :panel-resolved))))) @@ -1142,7 +1142,7 @@ (on-key (char-code ch))) (on-key 343) ;; Should add a system message confirming approval, not a user message - (let ((msgs (st :messages))) + (let ((msgs (getf *state* :messages))) (fiveam:is (>= (length msgs) 1)) (let ((m (aref msgs 0))) (fiveam:is (eq :system (getf m :role))) @@ -1154,7 +1154,7 @@ (dolist (ch (coerce "/deny HITL-xyz" 'list)) (on-key (char-code ch))) (on-key 343) - (let ((m (aref (st :messages) 0))) + (let ((m (aref (getf *state* :messages) 0))) (fiveam:is (eq :system (getf m :role))) (fiveam:is (search "Denied" (getf m :content))))) @@ -1166,7 +1166,7 @@ (dolist (ch (coerce "/undo" 'list)) (on-key (char-code ch))) (on-key 343) - (let ((m (aref (st :messages) 0))) + (let ((m (aref (getf *state* :messages) 0))) (fiveam:is (eq :system (getf m :role))) (fiveam:is (search "Undo" (getf m :content))))) @@ -1176,7 +1176,7 @@ (dolist (ch (coerce "/redo" 'list)) (on-key (char-code ch))) (on-key 343) - (let ((m (aref (st :messages) 0))) + (let ((m (aref (getf *state* :messages) 0))) (fiveam:is (eq :system (getf m :role))) (fiveam:is (search "Redo" (getf m :content))))) @@ -1189,7 +1189,7 @@ (dolist (ch (coerce "/why" 'list)) (on-key (char-code ch))) (on-key 13) - (let* ((msgs (st :messages)) + (let* ((msgs (getf *state* :messages)) (m (aref msgs (1- (length msgs))))) (fiveam:is (eq :system (getf m :role))) (fiveam:is (search "[BLOCKED]" (getf m :content))) @@ -1201,7 +1201,7 @@ (dolist (ch (coerce "/why" 'list)) (on-key (char-code ch))) (on-key 13) - (let* ((msgs (st :messages)) + (let* ((msgs (getf *state* :messages)) (m (aref msgs (1- (length msgs))))) (fiveam:is (search "No recent" (getf m :content))))) @@ -1212,11 +1212,11 @@ (init-state) (add-msg :agent "test" :gate-trace '((:gate "shell" :result :passed))) (on-key 7) ;; Ctrl+G — first press hides - (let* ((msgs (st :messages)) + (let* ((msgs (getf *state* :messages)) (m (aref msgs (1- (length msgs))))) (fiveam:is (search "hidden" (getf m :content)))) (on-key 7) ;; second press shows - (let* ((msgs (st :messages)) + (let* ((msgs (getf *state* :messages)) (m (aref msgs (1- (length msgs))))) (fiveam:is (search "shown" (getf m :content))))) @@ -1224,7 +1224,7 @@ "Contract v0.7.2: Ctrl+G with no gate trace shows fallback." (init-state) (on-key 7) - (let ((m (aref (st :messages) 0))) + (let ((m (aref (getf *state* :messages) 0))) (fiveam:is (search "No gate trace" (getf m :content))))) ;; ── v0.7.2 Message Search Mode ── @@ -1237,9 +1237,9 @@ (dolist (ch (coerce "/search hello" 'list)) (on-key (char-code ch))) (on-key 13) - (fiveam:is (eq t (st :search-mode))) - (fiveam:is (string= "hello" (st :search-query))) - (fiveam:is (= 1 (length (st :search-matches))))) + (fiveam:is (eq t (getf *state* :search-mode))) + (fiveam:is (string= "hello" (getf *state* :search-query))) + (fiveam:is (= 1 (length (getf *state* :search-matches))))) (fiveam:test test-search-mode-escape-exits "Contract v0.7.2: Escape exits search mode." @@ -1248,9 +1248,9 @@ (dolist (ch (coerce "/search test" 'list)) (on-key (char-code ch))) (on-key 13) - (fiveam:is (eq t (st :search-mode))) + (fiveam:is (eq t (getf *state* :search-mode))) (on-key 27) ;; Escape - (fiveam:is (null (st :search-mode)))) + (fiveam:is (null (getf *state* :search-mode)))) (fiveam:test test-search-mode-up-down-nav "Contract v0.7.2: Up/Down navigates between search matches." @@ -1261,13 +1261,13 @@ (dolist (ch (coerce "/search hello" 'list)) (on-key (char-code ch))) (on-key 13) - (fiveam:is (= 0 (st :search-match-idx))) + (fiveam:is (= 0 (getf *state* :search-match-idx))) (on-key 258) ;; Down - (fiveam:is (= 1 (st :search-match-idx))) + (fiveam:is (= 1 (getf *state* :search-match-idx))) (on-key 259) ;; Up - (fiveam:is (= 0 (st :search-match-idx))) + (fiveam:is (= 0 (getf *state* :search-match-idx))) (on-key 259) ;; Up (clamped) - (fiveam:is (= 0 (st :search-match-idx)))) + (fiveam:is (= 0 (getf *state* :search-match-idx)))) (fiveam:test test-context-sections "Contract v0.7.2: /context shows section breakdown with IDENTITY, TOOLS, LOGS." @@ -1276,7 +1276,7 @@ (dolist (ch (coerce "/context" 'list)) (on-key (char-code ch))) (on-key 13) - (let ((msgs (st :messages))) + (let ((msgs (getf *state* :messages))) (fiveam:is (some (lambda (m) (search "IDENTITY" (getf m :content))) msgs)) (fiveam:is (some (lambda (m) (search "LOGS" (getf m :content))) msgs)) (fiveam:is (some (lambda (m) (search "TOOLS" (getf m :content))) msgs)))) @@ -1287,21 +1287,21 @@ (dolist (ch (coerce "/help configuration" 'list)) (on-key (char-code ch))) (on-key 13) - (let ((msgs (st :messages))) + (let ((msgs (getf *state* :messages))) (fiveam:is (some (lambda (m) (search ".env" (getf m :content))) msgs)))) (fiveam:test test-pads-page-up "Contract v0.7.2: PageUp scrolls by page size (> 5 lines)." (init-state) (dotimes (i 30) (add-msg :system (format nil "msg ~d" i))) - (setf (st :scroll-offset) 0) + (setf (getf *state* :scroll-offset) 0) (on-key :ppage) - (fiveam:is (> (st :scroll-offset) 5) "Should scroll by more than 5 lines")) + (fiveam:is (> (getf *state* :scroll-offset) 5) "Should scroll by more than 5 lines")) (fiveam:test test-pads-page-down-clamp "Contract v0.7.2: PageDown clamps to 0." (init-state) (dotimes (i 5) (add-msg :system (format nil "msg ~d" i))) - (setf (st :scroll-offset) 3) + (setf (getf *state* :scroll-offset) 3) (on-key :npage) - (fiveam:is (= 0 (st :scroll-offset)))) + (fiveam:is (= 0 (getf *state* :scroll-offset)))) diff --git a/lisp/channel-tui-state.lisp b/lisp/channel-tui-state.lisp index 0f7b3c6..2ff6d6c 100644 --- a/lisp/channel-tui-state.lisp +++ b/lisp/channel-tui-state.lisp @@ -126,6 +126,9 @@ See *tui-theme-presets* for named presets (dark, light, solarized, gruvbox).") :collapsed-gates nil ; v0.7.2 :search-mode nil :search-query "" ; v0.7.2 :search-matches nil :search-match-idx 0 + :sidebar-visible nil ; v0.8.0 + :expand-tool-calls nil ; v0.8.0 + :mcp-count 0 ; v0.8.0 :dirty (list nil nil nil)))) (defun now () diff --git a/lisp/channel-tui-view.lisp b/lisp/channel-tui-view.lisp index affabb8..e801e8a 100644 --- a/lisp/channel-tui-view.lisp +++ b/lisp/channel-tui-view.lisp @@ -1,25 +1,40 @@ (in-package :passepartout.channel-tui) (defun view-status (fb w) - (let ((line1 (format nil - " Passepartout ~a [~a] msgs:~a scroll:~a Rules:~a~a" - (if (st :connected) "● Connected" "○ Disconnected") - (string-upcase (string (st :mode))) - (length (st :messages)) - (if (> (st :scroll-offset) 0) (format nil "~a↑" (st :scroll-offset)) "0") - (or (st :rule-count) 0) - (if (st :streaming-text) " [streaming]" - (if (st :busy) " …thinking" ""))))) - (cl-tty.backend:draw-text fb 1 1 line1 - (theme-color (if (st :connected) :connected :disconnected)) - nil) - ;; Second line: Focus map (left) + timestamp (right-aligned, v0.7.0) + (let ((degraded (and (find-package :passepartout) + (boundp (find-symbol "*SYSTEM-HEALTH*" :passepartout)) + (member (symbol-value (find-symbol "*SYSTEM-HEALTH*" :passepartout)) + '(:degraded :unhealthy)))) + (bg (if degraded :bright-yellow nil))) + ;; Line 1: Connection, mode, msgs, scroll, rules, streaming/busy + (cl-tty.backend:draw-text fb 1 1 + (format nil " Passepartout ~a [~a] msgs:~a scroll:~a Rules:~a~a" + (if (st :connected) "● Connected" "○ Disconnected") + (string-upcase (string (st :mode))) + (length (st :messages)) + (if (> (st :scroll-offset) 0) (format nil "~a↑" (st :scroll-offset)) "0") + (or (st :rule-count) 0) + (if (st :streaming-text) " [streaming]" + (if (st :busy) " …thinking" ""))) + (theme-color (if (st :connected) :connected :disconnected)) bg) + ;; Line 2: Focus + Timestamp (let ((focus-info (or (st :foveal-id) ""))) (when (and focus-info (> (length focus-info) 0)) (cl-tty.backend:draw-text fb 1 2 (format nil " [Focus: ~a]" focus-info) - (theme-color :timestamp) nil))) + (theme-color :timestamp) bg))) (cl-tty.backend:draw-text fb (max 1 (- w 12)) 2 (format nil " ~a" (now)) - (theme-color :timestamp) nil))) + (theme-color :timestamp) bg) + ;; Line 3: Directory, LSP, MCP, commands hint (v0.8.0) + (let* ((cwd (or (uiop:getenv "PWD") (uiop:getcwd))) + (dir (subseq cwd (max 0 (- (length cwd) (- w 45))))) + (lsp-color (if (st :connected) :green :dim)) + (mcp-count (or (st :mcp-count) 0)) + (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 (+ 2 (length dir)) 3 "●" (theme-color lsp-color) bg) + (cl-tty.backend:draw-text fb (+ 5 (length dir)) 3 (format nil " MCP:~d" mcp-count) + (theme-color :dim) bg) + (cl-tty.backend:draw-text fb (- w (length hint) 2) 3 hint (theme-color :timestamp) bg)))) ;; v0.7.2: search-highlight — wrap matching text in **bold** for markdown (defun search-highlight (content query) diff --git a/org/channel-tui-main.org b/org/channel-tui-main.org index 9d0f150..be1706a 100644 --- a/org/channel-tui-main.org +++ b/org/channel-tui-main.org @@ -37,68 +37,68 @@ Event handlers + daemon I/O + main loop. (defun on-key (ch) (cond ;; v0.7.1: Esc — interrupt streaming - ((and (eq ch :escape) (st :streaming-text)) + ((and (eq ch :escape) (getf *state* :streaming-text)) (send-daemon (list :type :event :payload '(:action :cancel-stream))) - (when (> (length (st :messages)) 0) - (let ((idx (1- (length (st :messages))))) - (setf (getf (aref (st :messages) idx) :content) + (when (> (length (getf *state* :messages)) 0) + (let ((idx (1- (length (getf *state* :messages))))) + (setf (getf (aref (getf *state* :messages) idx) :content) (concatenate 'string - (getf (aref (st :messages) idx) :content) + (getf (aref (getf *state* :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 (getf (aref (getf *state* :messages) idx) :streaming) nil) + (setf (getf (aref (getf *state* :messages) idx) :time) (now)))) + (setf (getf *state* :streaming-text) nil) + (setf (getf *state* :busy) nil) + (setf (getf *state* :dirty) (list t t nil))) ;; v0.7.2: Esc — exit search mode - ((and (eq ch :escape) (st :search-mode)) - (setf (st :search-mode) nil - (st :search-matches) nil - (st :search-query) "") - (setf (st :dirty) (list nil t nil)) + ((and (eq ch :escape) (getf *state* :search-mode)) + (setf (getf *state* :search-mode) nil + (getf *state* :search-matches) nil + (getf *state* :search-query) "") + (setf (getf *state* :dirty) (list nil t nil)) (add-msg :system "Search exited")) ;; v0.7.2: search mode — Up/Down navigate matches - ((and (st :search-mode) (eq ch :up)) - (let* ((matches (st :search-matches)) - (idx (st :search-match-idx)) + ((and (getf *state* :search-mode) (eq ch :up)) + (let* ((matches (getf *state* :search-matches)) + (idx (getf *state* :search-match-idx)) (new-idx (max 0 (1- idx)))) - (setf (st :search-match-idx) new-idx) + (setf (getf *state* :search-match-idx) new-idx) (when matches - (setf (st :scroll-offset) (nth new-idx matches)) + (setf (getf *state* :scroll-offset) (nth new-idx matches)) (add-msg :system (format nil "Match ~d/~d" (1+ new-idx) (length matches))) - (setf (st :dirty) (list nil t nil))))) - ((and (st :search-mode) (eq ch :down)) - (let* ((matches (st :search-matches)) - (idx (st :search-match-idx)) + (setf (getf *state* :dirty) (list nil t nil))))) + ((and (getf *state* :search-mode) (eq ch :down)) + (let* ((matches (getf *state* :search-matches)) + (idx (getf *state* :search-match-idx)) (new-idx (min (1- (length matches)) (1+ idx)))) - (setf (st :search-match-idx) new-idx) + (setf (getf *state* :search-match-idx) new-idx) (when matches - (setf (st :scroll-offset) (nth new-idx matches)) + (setf (getf *state* :scroll-offset) (nth new-idx matches)) (add-msg :system (format nil "Match ~d/~d" (1+ new-idx) (length matches))) - (setf (st :dirty) (list nil t nil))))) + (setf (getf *state* :dirty) (list nil t nil))))) ;; v0.7.2: search mode — Enter jumps to current match - ((and (st :search-mode) (eq ch :enter)) - (let ((matches (st :search-matches)) - (idx (st :search-match-idx))) + ((and (getf *state* :search-mode) (eq ch :enter)) + (let ((matches (getf *state* :search-matches)) + (idx (getf *state* :search-match-idx))) (when (and matches (>= (length matches) (1+ idx))) - (setf (st :scroll-offset) (nth idx matches)) - (setf (st :search-mode) nil - (st :search-matches) nil - (st :search-query) "") + (setf (getf *state* :scroll-offset) (nth idx matches)) + (setf (getf *state* :search-mode) nil + (getf *state* :search-matches) nil + (getf *state* :search-query) "") (add-msg :system (format nil "Jumped to match ~d" (1+ idx))) - (setf (st :dirty) (list nil t nil))))) + (setf (getf *state* :dirty) (list nil t nil))))) ;; v0.7.1: Tab on empty input — extract then open URL from agent message ((and (eq ch :tab) - (null (st :input-buffer))) - (if (st :url-buffer) + (null (getf *state* :input-buffer))) + (if (getf *state* :url-buffer) ;; Already extracted — now open it (progn - (add-msg :system (format nil "Opening ~a" (st :url-buffer))) - (setf (st :url-buffer) nil)) + (add-msg :system (format nil "Opening ~a" (getf *state* :url-buffer))) + (setf (getf *state* :url-buffer) nil)) ;; Extract URL from last agent message (let ((url nil)) - (loop for i from (1- (length (st :messages))) downto 0 - for msg = (aref (st :messages) i) + (loop for i from (1- (length (getf *state* :messages))) downto 0 + for msg = (aref (getf *state* :messages) i) for content = (getf msg :content) for role = (getf msg :role) while (eq role :agent) @@ -112,69 +112,69 @@ Event handlers + daemon I/O + main loop. (return))))) (if url (progn - (setf (st :url-buffer) url) + (setf (getf *state* :url-buffer) url) (add-msg :system (format nil "Press Tab to open ~a" url)) - (setf (st :dirty) (list t t nil))) + (setf (getf *state* :dirty) (list t t nil))) nil)))) ;; v0.7.0: Ctrl key bindings ((eq ch :ctrl-u) - (setf (st :input-buffer) nil) - (setf (st :dirty) (list nil nil t))) + (setf (getf *state* :input-buffer) nil) + (setf (getf *state* :dirty) (list nil nil t))) ((eq ch :ctrl-w) - (let ((buf (st :input-buffer))) + (let ((buf (getf *state* :input-buffer))) (loop while (and buf (char= (first buf) #\Space)) do (pop buf)) (loop while (and buf (char/= (first buf) #\Space)) do (pop buf)) - (setf (st :input-buffer) buf) - (setf (st :dirty) (list nil nil t)))) + (setf (getf *state* :input-buffer) buf) + (setf (getf *state* :dirty) (list nil nil t)))) ((eq ch :ctrl-a) - (setf (st :cursor-pos) 0)) + (setf (getf *state* :cursor-pos) 0)) ((eq ch :ctrl-e) - (setf (st :cursor-pos) (length (st :input-buffer)))) + (setf (getf *state* :cursor-pos) (length (getf *state* :input-buffer)))) ((eq ch :ctrl-l) - (setf (st :dirty) (list t t t))) + (setf (getf *state* :dirty) (list t t t))) ((eq ch :ctrl-d) - (when (or (null (st :input-buffer)) (string= "" (input-string))) + (when (or (null (getf *state* :input-buffer)) (string= "" (input-string))) (add-msg :system "Goodbye. Run /quit or press Ctrl+D again to exit."))) ((eq ch :ctrl-f) (add-msg :system "Use /search to find messages")) ((eq ch :ctrl-g) (let ((gate-idx nil)) - (loop for i from (1- (length (st :messages))) downto 0 - for m = (aref (st :messages) i) + (loop for i from (1- (length (getf *state* :messages))) downto 0 + for m = (aref (getf *state* :messages) i) when (and (getf m :gate-trace) (listp (getf m :gate-trace))) do (setf gate-idx i) (loop-finish)) (if gate-idx - (let ((cg (st :collapsed-gates))) + (let ((cg (getf *state* :collapsed-gates))) (if (member gate-idx cg) - (setf (st :collapsed-gates) (remove gate-idx cg)) - (push gate-idx (st :collapsed-gates))) + (setf (getf *state* :collapsed-gates) (remove gate-idx cg)) + (push gate-idx (getf *state* :collapsed-gates))) (add-msg :system (format nil "Gate trace ~a for msg ~a" - (if (member gate-idx (st :collapsed-gates)) "hidden" "shown") + (if (member gate-idx (getf *state* :collapsed-gates)) "hidden" "shown") gate-idx)) - (setf (st :dirty) (list nil t nil))) + (setf (getf *state* :dirty) (list nil t nil))) (add-msg :system "No gate trace to toggle")))) ((eq ch :ctrl-x) - (setf (st :pending-ctrl-x) t)) - ((and (st :pending-ctrl-x) (eq ch :ctrl-e)) ; Ctrl+X+E — editor - (setf (st :pending-ctrl-x) nil) + (setf (getf *state* :pending-ctrl-x) t)) + ((and (getf *state* :pending-ctrl-x) (eq ch :ctrl-e)) ; Ctrl+X+E — editor + (setf (getf *state* :pending-ctrl-x) nil) (add-msg :system "Opening $EDITOR... save and exit to return.") - (setf (st :dirty) (list t t nil))) - ((and (st :pending-ctrl-x) (not (eq ch :ctrl-e))) ; cancel Ctrl+X - (setf (st :pending-ctrl-x) nil) + (setf (getf *state* :dirty) (list t t nil))) + ((and (getf *state* :pending-ctrl-x) (not (eq ch :ctrl-e))) ; cancel Ctrl+X + (setf (getf *state* :pending-ctrl-x) nil) (on-key ch) (return-from on-key nil)) ;; Enter ((eq ch :enter) ;; Multi-line: if buffer ends with \, strip it and insert newline - (if (and (st :input-buffer) (eql (first (st :input-buffer)) #\\)) - (progn (pop (st :input-buffer)) - (push #\Newline (st :input-buffer)) - (setf (st :dirty) (list nil nil t))) + (if (and (getf *state* :input-buffer) (eql (first (getf *state* :input-buffer)) #\\)) + (progn (pop (getf *state* :input-buffer)) + (push #\Newline (getf *state* :input-buffer)) + (setf (getf *state* :dirty) (list nil nil t))) (let ((text (string-trim '(#\Space #\Tab) (input-string)))) (when (> (length text) 0) - (push text (st :input-history)) - (setf (st :input-hpos) 0) - (setf (st :scroll-offset) 0) + (push text (getf *state* :input-history)) + (setf (getf *state* :input-hpos) 0) + (setf (getf *state* :scroll-offset) 0) (cond ;; v0.7.2: undo/redo ((string-equal text "/undo") @@ -201,7 +201,7 @@ Event handlers + daemon I/O + main loop. ;; /help command ;; /why command — show last gate trace ((string-equal text "/why") - (let ((msgs (st :messages)) + (let ((msgs (getf *state* :messages)) (found nil)) (loop for i from (1- (length msgs)) downto 0 for m = (aref msgs i) @@ -254,9 +254,9 @@ Event handlers + daemon I/O + main loop. (add-msg :system "No tags configured. Set TAG_CATEGORIES env var.")))) ;; /context command — section breakdown with token estimates ((string-equal text "/context") - (let* ((msg-count (length (st :messages))) - (focus (or (st :foveal-id) "none")) - (id-tokens (min 200 (floor (+ 150 (length (or (st :focus-scope) ""))) 4))) + (let* ((msg-count (length (getf *state* :messages))) + (focus (or (getf *state* :foveal-id) "none")) + (id-tokens (min 200 (floor (+ 150 (length (or (getf *state* :focus-scope) ""))) 4))) (tool-tokens (if (boundp 'passepartout::*cognitive-tool-registry*) (floor (* (hash-table-count passepartout::*cognitive-tool-registry*) 40) 4) 50)) @@ -303,7 +303,7 @@ Event handlers + daemon I/O + main loop. (add-msg :system "Memory not available")))) ;; /context dropped — estimate pruned nodes from budget ((string-equal text "/context dropped") - (let* ((msg-count (length (st :messages))) + (let* ((msg-count (length (getf *state* :messages))) (est-total (* msg-count 60)) (budget 8192) (dropped-msgs (if (> est-total budget) @@ -318,7 +318,7 @@ Event handlers + daemon I/O + main loop. ;; /search command — message search ((and (>= (length text) 8) (string-equal (subseq text 0 8) "/search ")) (let* ((query (string-downcase (string-trim '(#\Space) (subseq text 8)))) - (msgs (st :messages)) + (msgs (getf *state* :messages)) (total (length msgs)) (matches nil)) (loop for i from 0 below total @@ -328,10 +328,10 @@ Event handlers + daemon I/O + main loop. do (push i matches)) (setf matches (nreverse matches)) ;; Enter search mode - (setf (st :search-mode) t - (st :search-query) query - (st :search-matches) matches - (st :search-match-idx) 0) + (setf (getf *state* :search-mode) t + (getf *state* :search-query) query + (getf *state* :search-matches) matches + (getf *state* :search-match-idx) 0) (if matches (add-msg :system (format nil "Search: ~d matches for '~a' (1/~d) — Up/Down nav, Enter jump, Esc exit" (length matches) query (length matches))) @@ -479,11 +479,11 @@ Event handlers + daemon I/O + main loop. (uiop:ensure-all-directories-exist (list hist-file)) (with-open-file (out hist-file :direction :output :if-exists :supersede :if-does-not-exist :create) - (dolist (entry (reverse (st :input-history))) + (dolist (entry (reverse (getf *state* :input-history))) (write-line entry out)))) (add-msg :system "* Goodbye *") (send-daemon (list :type :event :payload '(:action :quit))) - (setf (st :running) nil)) + (setf (getf *state* :running) nil)) ;; /reconnect — re-establish daemon connection ((string-equal text "/reconnect") (disconnect-daemon) @@ -491,12 +491,12 @@ Event handlers + daemon I/O + main loop. ;; Normal message (t (add-msg :user text) - (setf (st :busy) t) + (setf (getf *state* :busy) t) (send-daemon (list :type :event :payload (list :sensor :user-input :text text))))) - (setf (st :input-buffer) nil) - (setf (st :cursor-pos) 0) - (setf (st :dirty) (list t t t)))))) + (setf (getf *state* :input-buffer) nil) + (setf (getf *state* :cursor-pos) 0) + (setf (getf *state* :dirty) (list t t t)))))) ;; Tab — command completion (v0.7.0: extended with subcommand + file paths) ((eq ch :tab) (let ((text (input-string))) @@ -515,8 +515,8 @@ Event handlers + daemon I/O + main loop. (string-equal n partial :end2 (length partial)))) names))) (when match - (setf (st :input-buffer) (reverse (coerce (concatenate 'string "@" match) 'list))) - (setf (st :dirty) (list nil nil t))))) + (setf (getf *state* :input-buffer) (reverse (coerce (concatenate 'string "@" match) 'list))) + (setf (getf *state* :dirty) (list nil nil t))))) ;; /theme subcommand ((and (>= (length text) 7) (string-equal (subseq text 0 7) "/theme ")) (let* ((partial (string-trim '(#\Space) (subseq text 7))) @@ -524,8 +524,8 @@ Event handlers + daemon I/O + main loop. (match (if (string= partial "") (first names) (find partial names :test #'string-equal)))) (when match - (setf (st :input-buffer) (reverse (coerce (concatenate 'string "/theme " match) 'list))) - (setf (st :dirty) (list nil nil t))))) + (setf (getf *state* :input-buffer) (reverse (coerce (concatenate 'string "/theme " match) 'list))) + (setf (getf *state* :dirty) (list nil nil t))))) ;; /focus subcommand ((and (>= (length text) 7) (string-equal (subseq text 0 7) "/focus ")) (let* ((partial (string-trim '(#\Space) (subseq text 7))) @@ -540,8 +540,8 @@ Event handlers + daemon I/O + main loop. (string-equal d partial :end2 (length partial)))) dirs)))) (when match - (setf (st :input-buffer) (reverse (coerce (concatenate 'string "/focus " match) 'list))) - (setf (st :dirty) (list nil nil t))))) + (setf (getf *state* :input-buffer) (reverse (coerce (concatenate 'string "/focus " match) 'list))) + (setf (getf *state* :dirty) (list nil nil t))))) ;; Command prefix / ((and (> (length text) 1) (eql (char text 0) #\/)) (let* ((cmds '("/eval" "/focus" "/scope" "/unfocus" "/help" "/theme" "/reconnect" "/quit")) @@ -549,51 +549,51 @@ Event handlers + daemon I/O + main loop. (lambda (in cmd) (and (>= (length cmd) (length in)) (string-equal cmd in :end1 (length in))))))) (when match - (setf (st :input-buffer) (reverse (coerce match 'list))) + (setf (getf *state* :input-buffer) (reverse (coerce match 'list))) (when (member match '("/eval" "/focus" "/scope") :test #'string=) - (push #\Space (st :input-buffer))) - (setf (st :dirty) (list nil nil t)))))))) + (push #\Space (getf *state* :input-buffer))) + (setf (getf *state* :dirty) (list nil nil t))))))) ;; Backspace ((eq ch :backspace) (input-delete-char) - (setf (st :dirty) (list nil nil t))) + (setf (getf *state* :dirty) (list nil nil t))) ;; Left arrow ((eq ch :left) - (when (> (or (st :cursor-pos) 0) 0) - (decf (st :cursor-pos)) - (setf (st :dirty) (list nil nil t)))) + (when (> (or (getf *state* :cursor-pos) 0) 0) + (decf (getf *state* :cursor-pos)) + (setf (getf *state* :dirty) (list nil nil t)))) ;; Right arrow ((eq ch :right) - (when (< (or (st :cursor-pos) 0) (length (st :input-buffer))) - (incf (st :cursor-pos)) - (setf (st :dirty) (list nil nil t)))) + (when (< (or (getf *state* :cursor-pos) 0) (length (getf *state* :input-buffer))) + (incf (getf *state* :cursor-pos)) + (setf (getf *state* :dirty) (list nil nil t)))) ;; Up arrow ((eq ch :up) - (let* ((h (st :input-history)) (p (st :input-hpos))) + (let* ((h (getf *state* :input-history)) (p (getf *state* :input-hpos))) (when (and h (< p (1- (length h)))) - (incf (st :input-hpos)) - (setf (st :input-buffer) - (reverse (coerce (nth (st :input-hpos) h) 'list))) - (setf (st :dirty) (list nil nil t))))) + (incf (getf *state* :input-hpos)) + (setf (getf *state* :input-buffer) + (reverse (coerce (nth (getf *state* :input-hpos) h) 'list))) + (setf (getf *state* :dirty) (list nil nil t))))) ;; Down arrow ((eq ch :down) - (when (> (st :input-hpos) 0) - (decf (st :input-hpos)) - (let ((h (st :input-history))) - (setf (st :input-buffer) - (if (and h (< (st :input-hpos) (length h))) - (reverse (coerce (nth (st :input-hpos) h) 'list)) + (when (> (getf *state* :input-hpos) 0) + (decf (getf *state* :input-hpos)) + (let ((h (getf *state* :input-history))) + (setf (getf *state* :input-buffer) + (if (and h (< (getf *state* :input-hpos) (length h))) + (reverse (coerce (nth (getf *state* :input-hpos) h) 'list)) nil)) - (setf (st :dirty) (list nil nil t))))) + (setf (getf *state* :dirty) (list nil nil t))))) ;; PageUp — scroll back by page (10 lines) ((eq ch :ppage) - (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))) + (let ((max-offset (max 0 (- (length (getf *state* :messages)) 1)))) + (setf (getf *state* :scroll-offset) (min max-offset (+ (getf *state* :scroll-offset) 10)))) + (setf (getf *state* :dirty) (list nil t nil))) ;; PageDown — scroll forward by page ((eq ch :npage) - (setf (st :scroll-offset) (max 0 (- (st :scroll-offset) 10))) - (setf (st :dirty) (list nil t nil))) + (setf (getf *state* :scroll-offset) (max 0 (- (getf *state* :scroll-offset) 10))) + (setf (getf *state* :dirty) (list nil t nil))) ;; Printable (t (let ((chr (typecase ch @@ -605,17 +605,17 @@ Event handlers + daemon I/O + main loop. (t nil)))) (when (and chr (graphic-char-p chr)) (input-insert-char chr) - (setf (st :dirty) (list nil nil t)))))))) + (setf (getf *state* :dirty) (list nil nil t)))))))) ;; v0.7.2 — resolve-hitl-panel: marks panel as resolved after approve/deny (defun resolve-hitl-panel (decision) "Mark the most recent HITL panel message as resolved with DECISION." - (loop for i from (1- (length (st :messages))) downto 0 - for m = (aref (st :messages) i) + (loop for i from (1- (length (getf *state* :messages))) downto 0 + for m = (aref (getf *state* :messages) i) when (and (getf m :panel) (not (getf m :panel-resolved))) do (setf (getf m :panel-resolved) decision) - (setf (aref (st :messages) i) m) - (setf (st :dirty) (list nil t nil)) + (setf (aref (getf *state* :messages) i) m) + (setf (getf *state* :dirty) (list nil t nil)) (loop-finish))) ;; v0.7.2 — self-help-lookup: read USER_MANUAL.org and find matching sections @@ -680,43 +680,43 @@ Event handlers + daemon I/O + main loop. (add-msg :system (format nil "┌─ Permission Required ─┐~%~a~%Action: ~a~%Respond: /approve HITL-xxxx or /deny HITL-xxxx" hitl-msg explanation) :panel t)) - (setf (st :dirty) (list nil t nil)) + (setf (getf *state* :dirty) (list nil t nil)) (return-from on-daemon-msg nil)) ;; v0.7.1: streaming chunk (when (eq msg-type :stream-chunk) (cond ((string= text "") ;; Final chunk: stamp time, clear streaming - (when (> (length (st :messages)) 0) - (let ((idx (1- (length (st :messages))))) - (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 nil t nil)) + (when (> (length (getf *state* :messages)) 0) + (let ((idx (1- (length (getf *state* :messages))))) + (setf (getf (aref (getf *state* :messages) idx) :streaming) nil) + (setf (getf (aref (getf *state* :messages) idx) :time) (now)))) + (setf (getf *state* :streaming-text) nil) + (setf (getf *state* :busy) nil) + (setf (getf *state* :dirty) (list nil t nil)) (return-from on-daemon-msg nil)) - ((null (st :streaming-text)) + ((null (getf *state* :streaming-text)) ;; First chunk: add new streaming message - (setf (st :streaming-text) "") - (setf (st :busy) nil) + (setf (getf *state* :streaming-text) "") + (setf (getf *state* :busy) nil) (add-msg :agent text) - (let ((idx (1- (length (st :messages))))) - (setf (getf (aref (st :messages) idx) :streaming) t)) - (setf (st :streaming-text) text) - (setf (st :dirty) (list nil t nil)) + (let ((idx (1- (length (getf *state* :messages))))) + (setf (getf (aref (getf *state* :messages) idx) :streaming) t)) + (setf (getf *state* :streaming-text) text) + (setf (getf *state* :dirty) (list nil t nil)) (return-from on-daemon-msg nil)) (t ;; Subsequent chunk: append - (let* ((new-text (concatenate 'string (st :streaming-text) text)) - (idx (1- (length (st :messages))))) - (setf (st :streaming-text) new-text) - (setf (getf (aref (st :messages) idx) :content) new-text) - (setf (st :dirty) (list nil t nil))) + (let* ((new-text (concatenate 'string (getf *state* :streaming-text) text)) + (idx (1- (length (getf *state* :messages))))) + (setf (getf *state* :streaming-text) new-text) + (setf (getf (aref (getf *state* :messages) idx) :content) new-text) + (setf (getf *state* :dirty) (list nil t nil))) (return-from on-daemon-msg nil)))) - (when rule-count (setf (st :rule-count) rule-count)) - (when foveal-id (setf (st :foveal-id) foveal-id)) + (when rule-count (setf (getf *state* :rule-count) rule-count)) + (when foveal-id (setf (getf *state* :foveal-id) foveal-id)) (cond - (text (setf (st :busy) nil) + (text (setf (getf *state* :busy) nil) (add-msg :agent text :gate-trace gate-trace)) ((eq action :handshake) (add-msg :system (format nil "Connected v~a" (getf payload :version)))) @@ -726,7 +726,7 @@ Event handlers + daemon I/O + main loop. ** Daemon Communication #+BEGIN_SRC lisp :tangle ../lisp/channel-tui-main.lisp (defun send-daemon (msg) - (let ((s (st :stream))) + (let ((s (getf *state* :stream))) (when (and s (open-stream-p s)) (handler-case (progn @@ -754,7 +754,7 @@ Event handlers + daemon I/O + main loop. (defun reader-loop (s) (let ((consecutive-nils 0)) - (loop while (and (st :running) (open-stream-p s)) + (loop while (and (getf *state* :running) (open-stream-p s)) do (let ((msg (recv-daemon s))) (if msg (progn (queue-event (list :type :daemon :payload msg)) @@ -773,8 +773,8 @@ Event handlers + daemon I/O + main loop. (with-open-file (in hist-file :direction :input) (loop for line = (read-line in nil nil) while line - do (push line (st :input-history)))) - (setf (st :input-history) (nreverse (st :input-history)))))) + do (push line (getf *state* :input-history)))) + (setf (getf *state* :input-history) (nreverse (getf *state* :input-history)))))) #+END_SRC ** Connection @@ -786,9 +786,9 @@ Event handlers + daemon I/O + main loop. do (sleep backoff) (handler-case (let ((s (usocket:socket-connect host port :timeout 5))) - (setf (st :stream) (usocket:socket-stream s) - (st :connected) t) - (bt:make-thread (lambda () (reader-loop (st :stream))) + (setf (getf *state* :stream) (usocket:socket-stream s) + (getf *state* :connected) t) + (bt:make-thread (lambda () (reader-loop (getf *state* :stream))) :name "tui-reader") (add-msg :system (format nil "* Connected v~a *" "0.5.0")) (return-from connect-daemon t)) @@ -804,9 +804,9 @@ Event handlers + daemon I/O + main loop. nil) (defun disconnect-daemon () - (when (st :stream) - (ignore-errors (close (st :stream))) - (setf (st :stream) nil (st :connected) nil) + (when (getf *state* :stream) + (ignore-errors (close (getf *state* :stream))) + (setf (getf *state* :stream) nil (getf *state* :connected) nil) (add-msg :system "* Disconnected *"))) #+END_SRC @@ -819,7 +819,7 @@ Event handlers + daemon I/O + main loop. (let* ((swank-port (or (ignore-errors (parse-integer (uiop:getenv "TUI_SWANK_PORT"))) 4006))) - (setf (st :dirty) (list t t t)) + (setf (getf *state* :dirty) (list t t t)) (connect-daemon) (when (> swank-port 0) (handler-case @@ -836,17 +836,17 @@ Event handlers + daemon I/O + main loop. (let ((prev-fb (cl-tty.rendering:make-framebuffer w h)) (curr-fb (cl-tty.rendering:make-framebuffer w h))) ;; Initial render - (redraw be curr-fb w h) + (redraw curr-fb w h) (cl-tty.rendering:flush-framebuffer prev-fb curr-fb be) (rotatef prev-fb curr-fb) - (loop while (st :running) do + (loop while (getf *state* :running) do (dolist (ev (drain-queue)) (cond ((eq (getf ev :type) :daemon) (on-daemon-msg (getf ev :payload))) ((eq (getf ev :type) :disconnected) - (setf (st :connected) nil - (st :busy) nil) + (setf (getf *state* :connected) nil + (getf *state* :busy) nil) (add-msg :system "* Connection lost — type /reconnect to retry *")))) (multiple-value-bind (type data) (cl-tty.input:read-event be :timeout 0) @@ -855,7 +855,7 @@ Event handlers + daemon I/O + main loop. (multiple-value-setq (w h) (cl-tty.backend:backend-size be)) (setf prev-fb (cl-tty.rendering:make-framebuffer w h) curr-fb (cl-tty.rendering:make-framebuffer w h)) - (setf (st :dirty) (list t t t))) + (setf (getf *state* :dirty) (list t t t))) (data (let ((ch (typecase data (cl-tty.input:key-event @@ -866,13 +866,13 @@ Event handlers + daemon I/O + main loop. k))) (t data)))) (on-key ch))))))) - (when (or (first (st :dirty)) (second (st :dirty)) (third (st :dirty))) + (when (or (first (getf *state* :dirty)) (second (getf *state* :dirty)) (third (getf *state* :dirty))) (cl-tty.backend:backend-clear curr-fb) - (redraw be curr-fb w h) + (redraw curr-fb w h) (cl-tty.rendering:flush-framebuffer prev-fb curr-fb be) (rotatef prev-fb curr-fb)) - (sleep 0.1)))) - (disconnect-daemon)))) + (sleep 0.1))) + (disconnect-daemon))) #+END_SRC * Test Suite @@ -892,19 +892,19 @@ Event handlers + daemon I/O + main loop. (fiveam:test test-init-state "Contract model.1: init-state returns fresh state plist with required keys." (init-state) - (fiveam:is (eq t (st :running))) - (fiveam:is (eq :chat (st :mode))) - (fiveam:is (eq nil (st :connected))) - (fiveam:is (eq nil (st :stream))) - (fiveam:is (zerop (length (st :messages)))) - (fiveam:is (eq 0 (st :scroll-offset))) - (fiveam:is (eq nil (st :busy)))) + (fiveam:is (eq t (getf *state* :running))) + (fiveam:is (eq :chat (getf *state* :mode))) + (fiveam:is (eq nil (getf *state* :connected))) + (fiveam:is (eq nil (getf *state* :stream))) + (fiveam:is (zerop (length (getf *state* :messages)))) + (fiveam:is (eq 0 (getf *state* :scroll-offset))) + (fiveam:is (eq nil (getf *state* :busy)))) (fiveam:test test-add-msg "Contract model.2: add-msg appends a message with role, content, and time." (init-state) (add-msg :user "hello") - (let* ((msgs (st :messages)) + (let* ((msgs (getf *state* :messages)) (msg (aref msgs 0))) (fiveam:is (eq :user (getf msg :role))) (fiveam:is (string= "hello" (getf msg :content))) @@ -914,9 +914,9 @@ Event handlers + daemon I/O + main loop. (fiveam:test test-add-msg-dirty-flag "Contract model.2: add-msg sets dirty flags for status and chat." (init-state) - (setf (st :dirty) (list nil nil nil)) + (setf (getf *state* :dirty) (list nil nil nil)) (add-msg :system "boot") - (let ((dirty (st :dirty))) + (let ((dirty (getf *state* :dirty))) (fiveam:is (eq t (first dirty))) (fiveam:is (eq t (second dirty))) (fiveam:is (eq nil (third dirty))))) @@ -944,7 +944,7 @@ Event handlers + daemon I/O + main loop. ;; Input buffer should be cleared (fiveam:is (string= "" (input-string))) ;; A user message should be in the message list - (let ((msgs (st :messages))) + (let ((msgs (getf *state* :messages))) (fiveam:is (>= (length msgs) 1)) (let ((last (aref msgs 0))) (fiveam:is (eq :user (getf last :role))) @@ -957,7 +957,7 @@ Event handlers + daemon I/O + main loop. (dolist (ch (coerce "/eval (+ 1 2)" 'list)) (on-key (char-code ch))) (on-key 343) - (let ((msgs (st :messages))) + (let ((msgs (getf *state* :messages))) (fiveam:is (>= (length msgs) 1)) (let ((last-msg (aref msgs 0))) (fiveam:is (eq :system (getf last-msg :role))) @@ -979,7 +979,7 @@ Event handlers + daemon I/O + main loop. (dolist (ch (coerce "/focus myapp" 'list)) (on-key (char-code ch))) (on-key 343) - (let ((msg (aref (st :messages) 0))) + (let ((msg (aref (getf *state* :messages) 0))) (fiveam:is (eq :system (getf msg :role))))) (fiveam:test test-on-key-scope-command @@ -988,7 +988,7 @@ Event handlers + daemon I/O + main loop. (dolist (ch (coerce "/scope memex" 'list)) (on-key (char-code ch))) (on-key 343) - (let ((msg (aref (st :messages) 0))) + (let ((msg (aref (getf *state* :messages) 0))) (fiveam:is (eq :system (getf msg :role))))) (fiveam:test test-on-key-unfocus-command @@ -997,7 +997,7 @@ Event handlers + daemon I/O + main loop. (dolist (ch (coerce "/unfocus" 'list)) (on-key (char-code ch))) (on-key 343) - (let ((msg (aref (st :messages) 0))) + (let ((msg (aref (getf *state* :messages) 0))) (fiveam:is (eq :system (getf msg :role))))) (fiveam:test test-on-key-tab-completion @@ -1032,22 +1032,22 @@ Event handlers + daemon I/O + main loop. (dolist (ch (coerce "/help" 'list)) (on-key (char-code ch))) (on-key 343) - (let ((msgs (st :messages))) + (let ((msgs (getf *state* :messages))) (fiveam:is (>= (length msgs) 3)) (fiveam:is (some (lambda (m) (search "/eval" (getf m :content))) msgs)))) (fiveam:test test-activity-indicator "Contract model: :busy flag is set on send and cleared on agent response." (init-state) - (fiveam:is (eq nil (st :busy))) + (fiveam:is (eq nil (getf *state* :busy))) ;; Simulate sending a normal message (sets busy) (dolist (ch (coerce "hello" 'list)) (on-key (char-code ch))) (on-key 343) - (fiveam:is (eq t (st :busy))) + (fiveam:is (eq t (getf *state* :busy))) ;; Simulate receiving an agent response (clears busy) (on-daemon-msg '(:type :event :payload (:text "hi back"))) - (fiveam:is (eq nil (st :busy)))) + (fiveam:is (eq nil (getf *state* :busy)))) (fiveam:test test-theme "Contract view: *tui-theme* provides color mappings." @@ -1067,21 +1067,21 @@ Event handlers + daemon I/O + main loop. (fiveam:test test-on-key-ctrl-l-redraws "Contract 1/v0.7.0: Ctrl+L sets all dirty flags." (init-state) - (setf (st :dirty) (list nil nil nil)) + (setf (getf *state* :dirty) (list nil nil nil)) (on-key 12) ; Ctrl+L - (let ((d (st :dirty))) + (let ((d (getf *state* :dirty))) (fiveam:is (eq t (first d))) (fiveam:is (eq t (second d))))) (fiveam:test test-scroll-notify "Contract/v0.7.0: add-msg sets scroll-notify when scrolled up." (init-state) - (setf (st :scroll-at-bottom) nil) + (setf (getf *state* :scroll-at-bottom) nil) (add-msg :agent "hi") - (fiveam:is (eq t (st :scroll-notify))) - (setf (st :scroll-at-bottom) t (st :scroll-notify) nil) + (fiveam:is (eq t (getf *state* :scroll-notify))) + (setf (getf *state* :scroll-at-bottom) t (getf *state* :scroll-notify) nil) (add-msg :agent "hi2") - (fiveam:is (eq nil (st :scroll-notify)))) + (fiveam:is (eq nil (getf *state* :scroll-notify)))) (fiveam:test test-tab-subcommand "Contract/v0.7.0: Tab completes subcommand for /theme." @@ -1097,7 +1097,7 @@ Event handlers + daemon I/O + main loop. (init-state) (on-daemon-msg '(:type :stream-chunk :payload (:text "Hello"))) (on-daemon-msg '(:type :stream-chunk :payload (:text " world"))) - (let ((msgs (st :messages))) + (let ((msgs (getf *state* :messages))) (fiveam:is (= 1 (length msgs))) (let ((msg (aref msgs 0))) (fiveam:is (eq :agent (getf msg :role))) @@ -1109,35 +1109,35 @@ Event handlers + daemon I/O + main loop. (init-state) (on-daemon-msg '(:type :stream-chunk :payload (:text "Hi"))) (on-daemon-msg '(:type :stream-chunk :payload (:text ""))) - (let ((msg (aref (st :messages) 0))) + (let ((msg (aref (getf *state* :messages) 0))) (fiveam:is (stringp (getf msg :time))) (fiveam:is (string= "Hi" (getf msg :content))) - (fiveam:is (null (st :streaming-text))))) + (fiveam:is (null (getf *state* :streaming-text))))) (fiveam:test test-stream-interrupt "Contract/v0.7.1: Esc during streaming appends [interrupted] and finalizes." (init-state) (on-daemon-msg '(:type :stream-chunk :payload (:text "partial"))) (on-key 27) - (let ((msg (aref (st :messages) 0))) + (let ((msg (aref (getf *state* :messages) 0))) (fiveam:is (stringp (getf msg :time))) (fiveam:is (search "[interrupted]" (getf msg :content))) - (fiveam:is (null (st :streaming-text))) - (fiveam:is (null (st :busy))))) + (fiveam:is (null (getf *state* :streaming-text))) + (fiveam:is (null (getf *state* :busy))))) (fiveam:test test-stream-check-skip "Contract/v0.7.1: Esc without active streaming does nothing." (init-state) (on-key 27) - (fiveam:is (null (st :streaming-text))) - (fiveam:is (= 0 (length (st :messages))))) + (fiveam:is (null (getf *state* :streaming-text))) + (fiveam:is (= 0 (length (getf *state* :messages))))) (fiveam:test test-tab-open-url "Contract/v0.7.1: Tab on empty input with URL message extracts URL." (init-state) (add-msg :agent "visit https://example.com for info") (on-key 9) - (fiveam:is (string= "https://example.com" (st :url-buffer)))) + (fiveam:is (string= "https://example.com" (getf *state* :url-buffer)))) ;; ── v0.7.2 HITL Panels ── @@ -1148,7 +1148,7 @@ Event handlers + daemon I/O + main loop. :payload (:sensor :approval-required :action (:TYPE :REQUEST :PAYLOAD (:TOOL "shell")) :message "rm -rf blocked"))) - (let ((m (aref (st :messages) 0))) + (let ((m (aref (getf *state* :messages) 0))) (fiveam:is (eq :system (getf m :role))) (fiveam:is (getf m :panel)) (fiveam:is (search "rm -rf" (getf m :content))))) @@ -1162,11 +1162,11 @@ Event handlers + daemon I/O + main loop. (on-key (char-code ch))) (on-key 13) ;; Panel message (index 0) should be marked resolved - (let ((m (aref (st :messages) 0))) + (let ((m (aref (getf *state* :messages) 0))) (fiveam:is (getf m :panel)) (fiveam:is (eq :approved (getf m :panel-resolved)))) ;; Last message should be the approval confirmation - (let ((m (aref (st :messages) (1- (length (st :messages)))))) + (let ((m (aref (getf *state* :messages) (1- (length (getf *state* :messages)))))) (fiveam:is (search "Approved" (getf m :content))))) (fiveam:test test-hitl-panel-after-deny @@ -1177,7 +1177,7 @@ Event handlers + daemon I/O + main loop. (dolist (ch (coerce "/deny HITL-deny" 'list)) (on-key (char-code ch))) (on-key 13) - (let ((m (aref (st :messages) 0))) + (let ((m (aref (getf *state* :messages) 0))) (fiveam:is (getf m :panel)) (fiveam:is (eq :denied (getf m :panel-resolved))))) @@ -1188,7 +1188,7 @@ Event handlers + daemon I/O + main loop. (on-key (char-code ch))) (on-key 343) ;; Should add a system message confirming approval, not a user message - (let ((msgs (st :messages))) + (let ((msgs (getf *state* :messages))) (fiveam:is (>= (length msgs) 1)) (let ((m (aref msgs 0))) (fiveam:is (eq :system (getf m :role))) @@ -1200,7 +1200,7 @@ Event handlers + daemon I/O + main loop. (dolist (ch (coerce "/deny HITL-xyz" 'list)) (on-key (char-code ch))) (on-key 343) - (let ((m (aref (st :messages) 0))) + (let ((m (aref (getf *state* :messages) 0))) (fiveam:is (eq :system (getf m :role))) (fiveam:is (search "Denied" (getf m :content))))) @@ -1212,7 +1212,7 @@ Event handlers + daemon I/O + main loop. (dolist (ch (coerce "/undo" 'list)) (on-key (char-code ch))) (on-key 343) - (let ((m (aref (st :messages) 0))) + (let ((m (aref (getf *state* :messages) 0))) (fiveam:is (eq :system (getf m :role))) (fiveam:is (search "Undo" (getf m :content))))) @@ -1222,7 +1222,7 @@ Event handlers + daemon I/O + main loop. (dolist (ch (coerce "/redo" 'list)) (on-key (char-code ch))) (on-key 343) - (let ((m (aref (st :messages) 0))) + (let ((m (aref (getf *state* :messages) 0))) (fiveam:is (eq :system (getf m :role))) (fiveam:is (search "Redo" (getf m :content))))) @@ -1235,7 +1235,7 @@ Event handlers + daemon I/O + main loop. (dolist (ch (coerce "/why" 'list)) (on-key (char-code ch))) (on-key 13) - (let* ((msgs (st :messages)) + (let* ((msgs (getf *state* :messages)) (m (aref msgs (1- (length msgs))))) (fiveam:is (eq :system (getf m :role))) (fiveam:is (search "[BLOCKED]" (getf m :content))) @@ -1247,7 +1247,7 @@ Event handlers + daemon I/O + main loop. (dolist (ch (coerce "/why" 'list)) (on-key (char-code ch))) (on-key 13) - (let* ((msgs (st :messages)) + (let* ((msgs (getf *state* :messages)) (m (aref msgs (1- (length msgs))))) (fiveam:is (search "No recent" (getf m :content))))) @@ -1258,11 +1258,11 @@ Event handlers + daemon I/O + main loop. (init-state) (add-msg :agent "test" :gate-trace '((:gate "shell" :result :passed))) (on-key 7) ;; Ctrl+G — first press hides - (let* ((msgs (st :messages)) + (let* ((msgs (getf *state* :messages)) (m (aref msgs (1- (length msgs))))) (fiveam:is (search "hidden" (getf m :content)))) (on-key 7) ;; second press shows - (let* ((msgs (st :messages)) + (let* ((msgs (getf *state* :messages)) (m (aref msgs (1- (length msgs))))) (fiveam:is (search "shown" (getf m :content))))) @@ -1270,7 +1270,7 @@ Event handlers + daemon I/O + main loop. "Contract v0.7.2: Ctrl+G with no gate trace shows fallback." (init-state) (on-key 7) - (let ((m (aref (st :messages) 0))) + (let ((m (aref (getf *state* :messages) 0))) (fiveam:is (search "No gate trace" (getf m :content))))) ;; ── v0.7.2 Message Search Mode ── @@ -1283,9 +1283,9 @@ Event handlers + daemon I/O + main loop. (dolist (ch (coerce "/search hello" 'list)) (on-key (char-code ch))) (on-key 13) - (fiveam:is (eq t (st :search-mode))) - (fiveam:is (string= "hello" (st :search-query))) - (fiveam:is (= 1 (length (st :search-matches))))) + (fiveam:is (eq t (getf *state* :search-mode))) + (fiveam:is (string= "hello" (getf *state* :search-query))) + (fiveam:is (= 1 (length (getf *state* :search-matches))))) (fiveam:test test-search-mode-escape-exits "Contract v0.7.2: Escape exits search mode." @@ -1294,9 +1294,9 @@ Event handlers + daemon I/O + main loop. (dolist (ch (coerce "/search test" 'list)) (on-key (char-code ch))) (on-key 13) - (fiveam:is (eq t (st :search-mode))) + (fiveam:is (eq t (getf *state* :search-mode))) (on-key 27) ;; Escape - (fiveam:is (null (st :search-mode)))) + (fiveam:is (null (getf *state* :search-mode)))) (fiveam:test test-search-mode-up-down-nav "Contract v0.7.2: Up/Down navigates between search matches." @@ -1307,13 +1307,13 @@ Event handlers + daemon I/O + main loop. (dolist (ch (coerce "/search hello" 'list)) (on-key (char-code ch))) (on-key 13) - (fiveam:is (= 0 (st :search-match-idx))) + (fiveam:is (= 0 (getf *state* :search-match-idx))) (on-key 258) ;; Down - (fiveam:is (= 1 (st :search-match-idx))) + (fiveam:is (= 1 (getf *state* :search-match-idx))) (on-key 259) ;; Up - (fiveam:is (= 0 (st :search-match-idx))) + (fiveam:is (= 0 (getf *state* :search-match-idx))) (on-key 259) ;; Up (clamped) - (fiveam:is (= 0 (st :search-match-idx)))) + (fiveam:is (= 0 (getf *state* :search-match-idx)))) (fiveam:test test-context-sections "Contract v0.7.2: /context shows section breakdown with IDENTITY, TOOLS, LOGS." @@ -1322,7 +1322,7 @@ Event handlers + daemon I/O + main loop. (dolist (ch (coerce "/context" 'list)) (on-key (char-code ch))) (on-key 13) - (let ((msgs (st :messages))) + (let ((msgs (getf *state* :messages))) (fiveam:is (some (lambda (m) (search "IDENTITY" (getf m :content))) msgs)) (fiveam:is (some (lambda (m) (search "LOGS" (getf m :content))) msgs)) (fiveam:is (some (lambda (m) (search "TOOLS" (getf m :content))) msgs)))) @@ -1333,22 +1333,22 @@ Event handlers + daemon I/O + main loop. (dolist (ch (coerce "/help configuration" 'list)) (on-key (char-code ch))) (on-key 13) - (let ((msgs (st :messages))) + (let ((msgs (getf *state* :messages))) (fiveam:is (some (lambda (m) (search ".env" (getf m :content))) msgs)))) (fiveam:test test-pads-page-up "Contract v0.7.2: PageUp scrolls by page size (> 5 lines)." (init-state) (dotimes (i 30) (add-msg :system (format nil "msg ~d" i))) - (setf (st :scroll-offset) 0) + (setf (getf *state* :scroll-offset) 0) (on-key :ppage) - (fiveam:is (> (st :scroll-offset) 5) "Should scroll by more than 5 lines")) + (fiveam:is (> (getf *state* :scroll-offset) 5) "Should scroll by more than 5 lines")) (fiveam:test test-pads-page-down-clamp "Contract v0.7.2: PageDown clamps to 0." (init-state) (dotimes (i 5) (add-msg :system (format nil "msg ~d" i))) - (setf (st :scroll-offset) 3) + (setf (getf *state* :scroll-offset) 3) (on-key :npage) - (fiveam:is (= 0 (st :scroll-offset)))) + (fiveam:is (= 0 (getf *state* :scroll-offset)))) #+END_SRC diff --git a/org/channel-tui-state.org b/org/channel-tui-state.org index 7c107da..5e539a9 100644 --- a/org/channel-tui-state.org +++ b/org/channel-tui-state.org @@ -146,6 +146,9 @@ See *tui-theme-presets* for named presets (dark, light, solarized, gruvbox).") :collapsed-gates nil ; v0.7.2 :search-mode nil :search-query "" ; v0.7.2 :search-matches nil :search-match-idx 0 + :sidebar-visible nil ; v0.8.0 + :expand-tool-calls nil ; v0.8.0 + :mcp-count 0 ; v0.8.0 :dirty (list nil nil nil)))) #+END_SRC diff --git a/org/channel-tui-view.org b/org/channel-tui-view.org index 8d0709b..cdadd9b 100644 --- a/org/channel-tui-view.org +++ b/org/channel-tui-view.org @@ -46,25 +46,40 @@ that the TUI actuator attaches to the response plist before transmission. (in-package :passepartout.channel-tui) (defun view-status (fb w) - (let ((line1 (format nil - " Passepartout ~a [~a] msgs:~a scroll:~a Rules:~a~a" - (if (st :connected) "● Connected" "○ Disconnected") - (string-upcase (string (st :mode))) - (length (st :messages)) - (if (> (st :scroll-offset) 0) (format nil "~a↑" (st :scroll-offset)) "0") - (or (st :rule-count) 0) - (if (st :streaming-text) " [streaming]" - (if (st :busy) " …thinking" ""))))) - (cl-tty.backend:draw-text fb 1 1 line1 - (theme-color (if (st :connected) :connected :disconnected)) - nil) - ;; Second line: Focus map (left) + timestamp (right-aligned, v0.7.0) + (let ((degraded (and (find-package :passepartout) + (boundp (find-symbol "*SYSTEM-HEALTH*" :passepartout)) + (member (symbol-value (find-symbol "*SYSTEM-HEALTH*" :passepartout)) + '(:degraded :unhealthy)))) + (bg (if degraded :bright-yellow nil))) + ;; Line 1: Connection, mode, msgs, scroll, rules, streaming/busy + (cl-tty.backend:draw-text fb 1 1 + (format nil " Passepartout ~a [~a] msgs:~a scroll:~a Rules:~a~a" + (if (st :connected) "● Connected" "○ Disconnected") + (string-upcase (string (st :mode))) + (length (st :messages)) + (if (> (st :scroll-offset) 0) (format nil "~a↑" (st :scroll-offset)) "0") + (or (st :rule-count) 0) + (if (st :streaming-text) " [streaming]" + (if (st :busy) " …thinking" ""))) + (theme-color (if (st :connected) :connected :disconnected)) bg) + ;; Line 2: Focus + Timestamp (let ((focus-info (or (st :foveal-id) ""))) (when (and focus-info (> (length focus-info) 0)) (cl-tty.backend:draw-text fb 1 2 (format nil " [Focus: ~a]" focus-info) - (theme-color :timestamp) nil))) + (theme-color :timestamp) bg))) (cl-tty.backend:draw-text fb (max 1 (- w 12)) 2 (format nil " ~a" (now)) - (theme-color :timestamp) nil))) + (theme-color :timestamp) bg) + ;; Line 3: Directory, LSP, MCP, commands hint (v0.8.0) + (let* ((cwd (or (uiop:getenv "PWD") (uiop:getcwd))) + (dir (subseq cwd (max 0 (- (length cwd) (- w 45))))) + (lsp-color (if (st :connected) :green :dim)) + (mcp-count (or (st :mcp-count) 0)) + (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 (+ 2 (length dir)) 3 "●" (theme-color lsp-color) bg) + (cl-tty.backend:draw-text fb (+ 5 (length dir)) 3 (format nil " MCP:~d" mcp-count) + (theme-color :dim) bg) + (cl-tty.backend:draw-text fb (- w (length hint) 2) 3 hint (theme-color :timestamp) bg)))) ;; v0.7.2: search-highlight — wrap matching text in **bold** for markdown (defun search-highlight (content query)