From 79896c5ffd9fc1a2cecc51e9f16f3095b2cb424c Mon Sep 17 00:00:00 2001 From: Amr Gharbeia Date: Wed, 13 May 2026 14:53:27 -0400 Subject: [PATCH] fix: bypass ASDF compile for TUI load, use direct compile-file+load --- lisp/channel-tui-main.lisp | 575 ++++++++++++++++++------------------ org/channel-tui-main.org | 579 +++++++++++++++++++------------------ passepartout | 33 ++- 3 files changed, 626 insertions(+), 561 deletions(-) diff --git a/lisp/channel-tui-main.lisp b/lisp/channel-tui-main.lisp index 05ee26e..5d3917d 100644 --- a/lisp/channel-tui-main.lisp +++ b/lisp/channel-tui-main.lisp @@ -1,70 +1,79 @@ (in-package :passepartout.channel-tui) -(defun on-key (ch) - (cond - ;; v0.7.1: Esc — interrupt streaming - ((and (eq ch :escape) (getf *state* :streaming-text)) +(defun on-key (&rest args) + ;; Normalize: get-char returns raw ncurses integer codes (e.g. 263 for + ;; backspace). Croatoan's code-key + key-name convert them to keywords + ;; so the cond below can use eq. + (let* ((raw (car args)) + (ch (if (and (integerp raw) (> raw 255)) + (let* ((k (code-key raw)) + (name (and k (key-name k)))) + (or name raw)) + raw))) + (cond + ;; v0.7.1: Esc — interrupt streaming + ((and (eql ch 27) (st :streaming-text)) (send-daemon (list :type :event :payload '(:action :cancel-stream))) - (when (> (length (getf *state* :messages)) 0) - (let ((idx (1- (length (getf *state* :messages))))) - (setf (getf (aref (getf *state* :messages) idx) :content) + (when (> (length (st :messages)) 0) + (let ((idx (1- (length (st :messages))))) + (setf (getf (aref (st :messages) idx) :content) (concatenate 'string - (getf (aref (getf *state* :messages) idx) :content) + (getf (aref (st :messages) idx) :content) " [interrupted]")) - (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))) + (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))) ;; v0.7.2: Esc — exit search mode - ((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)) + ((and (eql ch 27) (st :search-mode)) + (setf (st :search-mode) nil + (st :search-matches) nil + (st :search-query) "") + (setf (st :dirty) (list nil t nil)) (add-msg :system "Search exited")) ;; v0.7.2: search mode — Up/Down navigate matches - ((and (getf *state* :search-mode) (eq ch :up)) - (let* ((matches (getf *state* :search-matches)) - (idx (getf *state* :search-match-idx)) + ((and (st :search-mode) (or (eql ch 259) (eq ch :up))) + (let* ((matches (st :search-matches)) + (idx (st :search-match-idx)) (new-idx (max 0 (1- idx)))) - (setf (getf *state* :search-match-idx) new-idx) + (setf (st :search-match-idx) new-idx) (when matches - (setf (getf *state* :scroll-offset) (nth new-idx matches)) + (setf (st :scroll-offset) (nth new-idx matches)) (add-msg :system (format nil "Match ~d/~d" (1+ new-idx) (length matches))) - (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)) + (setf (st :dirty) (list nil t nil))))) + ((and (st :search-mode) (or (eql ch 258) (eq ch :down))) + (let* ((matches (st :search-matches)) + (idx (st :search-match-idx)) (new-idx (min (1- (length matches)) (1+ idx)))) - (setf (getf *state* :search-match-idx) new-idx) + (setf (st :search-match-idx) new-idx) (when matches - (setf (getf *state* :scroll-offset) (nth new-idx matches)) + (setf (st :scroll-offset) (nth new-idx matches)) (add-msg :system (format nil "Match ~d/~d" (1+ new-idx) (length matches))) - (setf (getf *state* :dirty) (list nil t nil))))) + (setf (st :dirty) (list nil t nil))))) ;; v0.7.2: search mode — Enter jumps to current match - ((and (getf *state* :search-mode) (eq ch :enter)) - (let ((matches (getf *state* :search-matches)) - (idx (getf *state* :search-match-idx))) + ((and (st :search-mode) (or (eql ch 13) (eql ch 10) (eq ch :enter))) + (let ((matches (st :search-matches)) + (idx (st :search-match-idx))) (when (and matches (>= (length matches) (1+ idx))) - (setf (getf *state* :scroll-offset) (nth idx matches)) - (setf (getf *state* :search-mode) nil - (getf *state* :search-matches) nil - (getf *state* :search-query) "") + (setf (st :scroll-offset) (nth idx matches)) + (setf (st :search-mode) nil + (st :search-matches) nil + (st :search-query) "") (add-msg :system (format nil "Jumped to match ~d" (1+ idx))) - (setf (getf *state* :dirty) (list nil t nil))))) + (setf (st :dirty) (list nil t nil))))) ;; v0.7.1: Tab on empty input — extract then open URL from agent message - ((and (eq ch :tab) - (null (getf *state* :input-buffer))) - (if (getf *state* :url-buffer) + ((and (or (eql ch 9) (eq ch :tab)) + (null (st :input-buffer))) + (if (st :url-buffer) ;; Already extracted — now open it (progn - (add-msg :system (format nil "Opening ~a" (getf *state* :url-buffer))) - (setf (getf *state* :url-buffer) nil)) + (add-msg :system (format nil "Opening ~a" (st :url-buffer))) + (setf (st :url-buffer) nil)) ;; Extract URL from last agent message (let ((url nil)) - (loop for i from (1- (length (getf *state* :messages))) downto 0 - for msg = (aref (getf *state* :messages) i) + (loop for i from (1- (length (st :messages))) downto 0 + for msg = (aref (st :messages) i) for content = (getf msg :content) for role = (getf msg :role) while (eq role :agent) @@ -78,69 +87,70 @@ (return))))) (if url (progn - (setf (getf *state* :url-buffer) url) + (setf (st :url-buffer) url) (add-msg :system (format nil "Press Tab to open ~a" url)) - (setf (getf *state* :dirty) (list t t nil))) + (setf (st :dirty) (list t t nil))) nil)))) ;; v0.7.0: Ctrl key bindings - ((eq ch :ctrl-u) - (setf (getf *state* :input-buffer) nil) - (setf (getf *state* :dirty) (list nil nil t))) - ((eq ch :ctrl-w) - (let ((buf (getf *state* :input-buffer))) + ((eql ch 21) ; Ctrl+U — clear line + (setf (st :input-buffer) nil) + (setf (st :dirty) (list nil nil t))) + ((eql ch 23) ; Ctrl+W — delete word backward + (let ((buf (st :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 (getf *state* :input-buffer) buf) - (setf (getf *state* :dirty) (list nil nil t)))) - ((eq ch :ctrl-a) - (setf (getf *state* :cursor-pos) 0)) - ((eq ch :ctrl-e) - (setf (getf *state* :cursor-pos) (length (getf *state* :input-buffer)))) - ((eq ch :ctrl-l) - (setf (getf *state* :dirty) (list t t t))) - ((eq ch :ctrl-d) - (when (or (null (getf *state* :input-buffer)) (string= "" (input-string))) + (setf (st :input-buffer) buf) + (setf (st :dirty) (list nil nil t)))) + ((eql ch 1) ; Ctrl+A — home + (setf (st :cursor-pos) 0)) + ((eql ch 5) ; Ctrl+E — end + (setf (st :cursor-pos) (length (st :input-buffer)))) + ((eql ch 12) ; Ctrl+L — redraw + (setf (st :dirty) (list t t t))) + ((eql ch 4) ; Ctrl+D — quit on empty + (when (or (null (st :input-buffer)) (string= "" (input-string))) (add-msg :system "Goodbye. Run /quit or press Ctrl+D again to exit."))) - ((eq ch :ctrl-f) + ((eql ch 6) ; v0.7.2 Ctrl+F — message search (add-msg :system "Use /search to find messages")) - ((eq ch :ctrl-g) + ((eql ch 7) ; v0.7.2 Ctrl+G — toggle gate trace collapse (let ((gate-idx nil)) - (loop for i from (1- (length (getf *state* :messages))) downto 0 - for m = (aref (getf *state* :messages) i) + (loop for i from (1- (length (st :messages))) downto 0 + for m = (aref (st :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 (getf *state* :collapsed-gates))) + (let ((cg (st :collapsed-gates))) (if (member gate-idx cg) - (setf (getf *state* :collapsed-gates) (remove gate-idx cg)) - (push gate-idx (getf *state* :collapsed-gates))) + (setf (st :collapsed-gates) (remove gate-idx cg)) + (push gate-idx (st :collapsed-gates))) (add-msg :system (format nil "Gate trace ~a for msg ~a" - (if (member gate-idx (getf *state* :collapsed-gates)) "hidden" "shown") + (if (member gate-idx (st :collapsed-gates)) "hidden" "shown") gate-idx)) - (setf (getf *state* :dirty) (list nil t nil))) + (setf (st :dirty) (list nil t nil))) (add-msg :system "No gate trace to toggle")))) - ((eq ch :ctrl-x) - (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) + ((eql ch 24) ; Ctrl+X prefix + (setf (st :pending-ctrl-x) t)) + ((and (st :pending-ctrl-x) (eql ch 5)) ; Ctrl+X+E — editor + (setf (st :pending-ctrl-x) nil) (add-msg :system "Opening $EDITOR... save and exit to return.") - (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) + (setf (st :dirty) (list t t nil))) + ((and (st :pending-ctrl-x) (not (eql ch 5))) ; cancel Ctrl+X + (setf (st :pending-ctrl-x) nil) (on-key ch) (return-from on-key nil)) ;; Enter - ((eq ch :enter) + ((or (eq ch :enter) (eql ch 13) (eql ch 10) + (eql ch #\Newline) (eql ch #\Return)) ;; Multi-line: if buffer ends with \, strip it and insert newline - (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))) + (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))) (let ((text (string-trim '(#\Space #\Tab) (input-string)))) (when (> (length text) 0) - (push text (getf *state* :input-history)) - (setf (getf *state* :input-hpos) 0) - (setf (getf *state* :scroll-offset) 0) + (push text (st :input-history)) + (setf (st :input-hpos) 0) + (setf (st :scroll-offset) 0) (cond ;; v0.7.2: undo/redo ((string-equal text "/undo") @@ -167,7 +177,7 @@ ;; /help command ;; /why command — show last gate trace ((string-equal text "/why") - (let ((msgs (getf *state* :messages)) + (let ((msgs (st :messages)) (found nil)) (loop for i from (1- (length msgs)) downto 0 for m = (aref msgs i) @@ -220,9 +230,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 (getf *state* :messages))) - (focus (or (getf *state* :foveal-id) "none")) - (id-tokens (min 200 (floor (+ 150 (length (or (getf *state* :focus-scope) ""))) 4))) + (let* ((msg-count (length (st :messages))) + (focus (or (st :foveal-id) "none")) + (id-tokens (min 200 (floor (+ 150 (length (or (st :focus-scope) ""))) 4))) (tool-tokens (if (boundp 'passepartout::*cognitive-tool-registry*) (floor (* (hash-table-count passepartout::*cognitive-tool-registry*) 40) 4) 50)) @@ -269,7 +279,7 @@ (add-msg :system "Memory not available")))) ;; /context dropped — estimate pruned nodes from budget ((string-equal text "/context dropped") - (let* ((msg-count (length (getf *state* :messages))) + (let* ((msg-count (length (st :messages))) (est-total (* msg-count 60)) (budget 8192) (dropped-msgs (if (> est-total budget) @@ -284,7 +294,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 (getf *state* :messages)) + (msgs (st :messages)) (total (length msgs)) (matches nil)) (loop for i from 0 below total @@ -294,10 +304,10 @@ do (push i matches)) (setf matches (nreverse matches)) ;; Enter search mode - (setf (getf *state* :search-mode) t - (getf *state* :search-query) query - (getf *state* :search-matches) matches - (getf *state* :search-match-idx) 0) + (setf (st :search-mode) t + (st :search-query) query + (st :search-matches) matches + (st :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 +455,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 (getf *state* :input-history))) + (dolist (entry (reverse (st :input-history))) (write-line entry out)))) (add-msg :system "* Goodbye *") (send-daemon (list :type :event :payload '(:action :quit))) - (setf (getf *state* :running) nil)) + (setf (st :running) nil)) ;; /reconnect — re-establish daemon connection ((string-equal text "/reconnect") (disconnect-daemon) @@ -457,14 +467,14 @@ ;; Normal message (t (add-msg :user text) - (setf (getf *state* :busy) t) + (setf (st :busy) t) (send-daemon (list :type :event :payload (list :sensor :user-input :text text))))) - (setf (getf *state* :input-buffer) nil) - (setf (getf *state* :cursor-pos) 0) - (setf (getf *state* :dirty) (list t t t)))))) + (setf (st :input-buffer) nil) + (setf (st :cursor-pos) 0) + (setf (st :dirty) (list t t t)))))) ;; Tab — command completion (v0.7.0: extended with subcommand + file paths) - ((eq ch :tab) + ((or (eql ch 9) (eq ch :tab)) (let ((text (input-string))) (cond ;; @ prefix — file path completion @@ -481,8 +491,8 @@ (string-equal n partial :end2 (length partial)))) names))) (when match - (setf (getf *state* :input-buffer) (reverse (coerce (concatenate 'string "@" match) 'list))) - (setf (getf *state* :dirty) (list nil nil t))))) + (setf (st :input-buffer) (reverse (coerce (concatenate 'string "@" match) 'list))) + (setf (st :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 +500,8 @@ (match (if (string= partial "") (first names) (find partial names :test #'string-equal)))) (when match - (setf (getf *state* :input-buffer) (reverse (coerce (concatenate 'string "/theme " match) 'list))) - (setf (getf *state* :dirty) (list nil nil t))))) + (setf (st :input-buffer) (reverse (coerce (concatenate 'string "/theme " match) 'list))) + (setf (st :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 +516,8 @@ (string-equal d partial :end2 (length partial)))) dirs)))) (when match - (setf (getf *state* :input-buffer) (reverse (coerce (concatenate 'string "/focus " match) 'list))) - (setf (getf *state* :dirty) (list nil nil t))))) + (setf (st :input-buffer) (reverse (coerce (concatenate 'string "/focus " match) 'list))) + (setf (st :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,73 +525,71 @@ (lambda (in cmd) (and (>= (length cmd) (length in)) (string-equal cmd in :end1 (length in))))))) (when match - (setf (getf *state* :input-buffer) (reverse (coerce match 'list))) + (setf (st :input-buffer) (reverse (coerce match 'list))) (when (member match '("/eval" "/focus" "/scope") :test #'string=) - (push #\Space (getf *state* :input-buffer))) - (setf (getf *state* :dirty) (list nil nil t))))))) + (push #\Space (st :input-buffer))) + (setf (st :dirty) (list nil nil t)))))))) ;; Backspace - ((eq ch :backspace) + ((or (eq ch :backspace) (eql ch 127) (eql ch 8) + (eql ch #\Backspace)) (input-delete-char) - (setf (getf *state* :dirty) (list nil nil t))) + (setf (st :dirty) (list nil nil t))) ;; Left arrow - ((eq ch :left) - (when (> (or (getf *state* :cursor-pos) 0) 0) - (decf (getf *state* :cursor-pos)) - (setf (getf *state* :dirty) (list nil nil t)))) + ((or (eq ch :left) (eql ch 260)) + (when (> (or (st :cursor-pos) 0) 0) + (decf (st :cursor-pos)) + (setf (st :dirty) (list nil nil t)))) ;; Right arrow - ((eq ch :right) - (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)))) + ((or (eq ch :right) (eql ch 261)) + (when (< (or (st :cursor-pos) 0) (length (st :input-buffer))) + (incf (st :cursor-pos)) + (setf (st :dirty) (list nil nil t)))) ;; Up arrow - ((eq ch :up) - (let* ((h (getf *state* :input-history)) (p (getf *state* :input-hpos))) + ((or (eq ch :up) (eql ch 259)) + (let* ((h (st :input-history)) (p (st :input-hpos))) (when (and h (< p (1- (length h)))) - (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))))) + (incf (st :input-hpos)) + (setf (st :input-buffer) + (reverse (coerce (nth (st :input-hpos) h) 'list))) + (setf (st :dirty) (list nil nil t))))) ;; Down arrow - ((eq ch :down) - (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)) + ((or (eq ch :down) (eql ch 258)) + (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)) nil)) - (setf (getf *state* :dirty) (list nil nil t))))) + (setf (st :dirty) (list nil nil t))))) ;; PageUp — scroll back by page (10 lines) - ((eq ch :ppage) - (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))) + ((or (eq ch :ppage) (eql ch 339)) + (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))) ;; PageDown — scroll forward by page - ((eq ch :npage) - (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 - (character ch) - ((integer 32 126) (code-char ch)) - (keyword (let ((s (string ch))) - (and (= (length s) 1) - (char-downcase (char s 0))))) - (t nil)))) - (when (and chr (graphic-char-p chr)) - (input-insert-char chr) - (setf (getf *state* :dirty) (list nil nil t)))))))) + ((or (eq ch :npage) (eql ch 338)) + (setf (st :scroll-offset) (max 0 (- (st :scroll-offset) 10))) + (setf (st :dirty) (list nil t nil))) + ;; Printable + (t + (let ((chr (typecase ch + (character ch) + (integer (code-char ch)) + (t nil)))) + (when (and chr (graphic-char-p chr)) + (input-insert-char chr) + (setf (st :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 (getf *state* :messages))) downto 0 - for m = (aref (getf *state* :messages) i) + (loop for i from (1- (length (st :messages))) downto 0 + for m = (aref (st :messages) i) when (and (getf m :panel) (not (getf m :panel-resolved))) do (setf (getf m :panel-resolved) decision) - (setf (aref (getf *state* :messages) i) m) - (setf (getf *state* :dirty) (list nil t nil)) + (setf (aref (st :messages) i) m) + (setf (st :dirty) (list nil t nil)) (loop-finish))) ;; v0.7.2 — self-help-lookup: read USER_MANUAL.org and find matching sections @@ -646,50 +654,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 (getf *state* :dirty) (list nil t nil)) + (setf (st :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 (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)) + (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)) (return-from on-daemon-msg nil)) - ((null (getf *state* :streaming-text)) + ((null (st :streaming-text)) ;; First chunk: add new streaming message - (setf (getf *state* :streaming-text) "") - (setf (getf *state* :busy) nil) + (setf (st :streaming-text) "") + (setf (st :busy) nil) (add-msg :agent text) - (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)) + (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)) (return-from on-daemon-msg nil)) (t ;; Subsequent chunk: append - (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))) + (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))) (return-from on-daemon-msg nil)))) - (when rule-count (setf (getf *state* :rule-count) rule-count)) - (when foveal-id (setf (getf *state* :foveal-id) foveal-id)) + (when rule-count (setf (st :rule-count) rule-count)) + (when foveal-id (setf (st :foveal-id) foveal-id)) (cond - (text (setf (getf *state* :busy) nil) + (text (setf (st :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 (getf *state* :stream))) + (let ((s (st :stream))) (when (and s (open-stream-p s)) (handler-case (progn @@ -717,7 +725,7 @@ (defun reader-loop (s) (let ((consecutive-nils 0)) - (loop while (and (getf *state* :running) (open-stream-p s)) + (loop while (and (st :running) (open-stream-p s)) do (let ((msg (recv-daemon s))) (if msg (progn (queue-event (list :type :daemon :payload msg)) @@ -736,8 +744,8 @@ (with-open-file (in hist-file :direction :input) (loop for line = (read-line in nil nil) while line - do (push line (getf *state* :input-history)))) - (setf (getf *state* :input-history) (nreverse (getf *state* :input-history)))))) + do (push line (st :input-history)))) + (setf (st :input-history) (nreverse (st :input-history)))))) (defun connect-daemon (&optional (host "127.0.0.1") (port 9105)) (add-msg :system "* Connecting to daemon... *") @@ -746,9 +754,9 @@ do (sleep backoff) (handler-case (let ((s (usocket:socket-connect host port :timeout 5))) - (setf (getf *state* :stream) (usocket:socket-stream s) - (getf *state* :connected) t) - (bt:make-thread (lambda () (reader-loop (getf *state* :stream))) + (setf (st :stream) (usocket:socket-stream s) + (st :connected) t) + (bt:make-thread (lambda () (reader-loop (st :stream))) :name "tui-reader") (add-msg :system (format nil "* Connected v~a *" "0.5.0")) (return-from connect-daemon t)) @@ -764,9 +772,9 @@ nil) (defun disconnect-daemon () - (when (getf *state* :stream) - (ignore-errors (close (getf *state* :stream))) - (setf (getf *state* :stream) nil (getf *state* :connected) nil) + (when (st :stream) + (ignore-errors (close (st :stream))) + (setf (st :stream) nil (st :connected) nil) (add-msg :system "* Disconnected *"))) (defun tui-main () @@ -776,7 +784,7 @@ (let* ((swank-port (or (ignore-errors (parse-integer (uiop:getenv "TUI_SWANK_PORT"))) 4006))) - (setf (getf *state* :dirty) (list t t t)) + (setf (st :dirty) (list t t t)) (connect-daemon) (when (> swank-port 0) (handler-case @@ -793,45 +801,60 @@ (let ((prev-fb (cl-tty.rendering:make-framebuffer w h)) (curr-fb (cl-tty.rendering:make-framebuffer w h))) ;; Initial render - (redraw curr-fb w h) + (redraw be curr-fb w h) (cl-tty.rendering:flush-framebuffer prev-fb curr-fb be) (rotatef prev-fb curr-fb) - (loop while (getf *state* :running) do + (loop while (st :running) do (dolist (ev (drain-queue)) (cond ((eq (getf ev :type) :daemon) (on-daemon-msg (getf ev :payload))) ((eq (getf ev :type) :disconnected) - (setf (getf *state* :connected) nil - (getf *state* :busy) nil) + (setf (st :connected) nil + (st :busy) nil) (add-msg :system "* Connection lost — type /reconnect to retry *")))) (multiple-value-bind (type data) (cl-tty.input:read-event be :timeout 0) (cond ((eq type :resize) - (multiple-value-setq (w h) - (or (ignore-errors (cl-tty.backend:backend-size be)) - (values 80 24))) + (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 (getf *state* :dirty) (list t t t))) + (setf (st :dirty) (list t t t))) (data - (let ((ch (typecase data - (cl-tty.input:key-event - (let ((k (cl-tty.input:key-event-key data)) - (ctrl (cl-tty.input:key-event-ctrl data))) - (if ctrl - (intern (format nil "CTRL-~a" k) :keyword) - k))) - (t data)))) - (on-key ch))))))) - (when (or (first (getf *state* :dirty)) (second (getf *state* :dirty)) (third (getf *state* :dirty))) + (let ((ch (typecase data + (cl-tty.input:key-event + (cl-tty.input:key-event-key data)) + (t data)))) + (cond + ((eql ch :escape) + (when (st :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) + (concatenate 'string + (getf (aref (st :messages) idx) :content) + " [interrupted]")) + (setf (getf (aref (st :messages) idx) :streaming) nil) + (setf (getf (aref (st :messages) idx) :time) (now)))) + (setf (st :streaming-text) nil) + (setf (st :busy) nil) + (setf (st :dirty) (list t t nil))) + (when (st :search-mode) + (setf (st :search-mode) nil + (st :search-matches) nil + (st :search-query) "") + (setf (st :dirty) (list nil t nil)) + (add-msg :system "Search exited"))) + (t (on-key ch))))))) + (when (or (first (st :dirty)) (second (st :dirty)) (third (st :dirty))) (cl-tty.backend:backend-clear curr-fb) - (redraw curr-fb w h) + (redraw be 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)) @@ -848,19 +871,19 @@ (fiveam:test test-init-state "Contract model.1: init-state returns fresh state plist with required keys." (init-state) - (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: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: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 (getf *state* :messages)) + (let* ((msgs (st :messages)) (msg (aref msgs 0))) (fiveam:is (eq :user (getf msg :role))) (fiveam:is (string= "hello" (getf msg :content))) @@ -870,9 +893,9 @@ (fiveam:test test-add-msg-dirty-flag "Contract model.2: add-msg sets dirty flags for status and chat." (init-state) - (setf (getf *state* :dirty) (list nil nil nil)) + (setf (st :dirty) (list nil nil nil)) (add-msg :system "boot") - (let ((dirty (getf *state* :dirty))) + (let ((dirty (st :dirty))) (fiveam:is (eq t (first dirty))) (fiveam:is (eq t (second dirty))) (fiveam:is (eq nil (third dirty))))) @@ -900,7 +923,7 @@ ;; Input buffer should be cleared (fiveam:is (string= "" (input-string))) ;; A user message should be in the message list - (let ((msgs (getf *state* :messages))) + (let ((msgs (st :messages))) (fiveam:is (>= (length msgs) 1)) (let ((last (aref msgs 0))) (fiveam:is (eq :user (getf last :role))) @@ -913,7 +936,7 @@ (dolist (ch (coerce "/eval (+ 1 2)" 'list)) (on-key (char-code ch))) (on-key 343) - (let ((msgs (getf *state* :messages))) + (let ((msgs (st :messages))) (fiveam:is (>= (length msgs) 1)) (let ((last-msg (aref msgs 0))) (fiveam:is (eq :system (getf last-msg :role))) @@ -935,7 +958,7 @@ (dolist (ch (coerce "/focus myapp" 'list)) (on-key (char-code ch))) (on-key 343) - (let ((msg (aref (getf *state* :messages) 0))) + (let ((msg (aref (st :messages) 0))) (fiveam:is (eq :system (getf msg :role))))) (fiveam:test test-on-key-scope-command @@ -944,7 +967,7 @@ (dolist (ch (coerce "/scope memex" 'list)) (on-key (char-code ch))) (on-key 343) - (let ((msg (aref (getf *state* :messages) 0))) + (let ((msg (aref (st :messages) 0))) (fiveam:is (eq :system (getf msg :role))))) (fiveam:test test-on-key-unfocus-command @@ -953,7 +976,7 @@ (dolist (ch (coerce "/unfocus" 'list)) (on-key (char-code ch))) (on-key 343) - (let ((msg (aref (getf *state* :messages) 0))) + (let ((msg (aref (st :messages) 0))) (fiveam:is (eq :system (getf msg :role))))) (fiveam:test test-on-key-tab-completion @@ -988,22 +1011,22 @@ (dolist (ch (coerce "/help" 'list)) (on-key (char-code ch))) (on-key 343) - (let ((msgs (getf *state* :messages))) + (let ((msgs (st :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 (getf *state* :busy))) + (fiveam:is (eq nil (st :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 (getf *state* :busy))) + (fiveam:is (eq t (st :busy))) ;; Simulate receiving an agent response (clears busy) (on-daemon-msg '(:type :event :payload (:text "hi back"))) - (fiveam:is (eq nil (getf *state* :busy)))) + (fiveam:is (eq nil (st :busy)))) (fiveam:test test-theme "Contract view: *tui-theme* provides color mappings." @@ -1023,21 +1046,21 @@ (fiveam:test test-on-key-ctrl-l-redraws "Contract 1/v0.7.0: Ctrl+L sets all dirty flags." (init-state) - (setf (getf *state* :dirty) (list nil nil nil)) + (setf (st :dirty) (list nil nil nil)) (on-key 12) ; Ctrl+L - (let ((d (getf *state* :dirty))) + (let ((d (st :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 (getf *state* :scroll-at-bottom) nil) + (setf (st :scroll-at-bottom) nil) (add-msg :agent "hi") - (fiveam:is (eq t (getf *state* :scroll-notify))) - (setf (getf *state* :scroll-at-bottom) t (getf *state* :scroll-notify) nil) + (fiveam:is (eq t (st :scroll-notify))) + (setf (st :scroll-at-bottom) t (st :scroll-notify) nil) (add-msg :agent "hi2") - (fiveam:is (eq nil (getf *state* :scroll-notify)))) + (fiveam:is (eq nil (st :scroll-notify)))) (fiveam:test test-tab-subcommand "Contract/v0.7.0: Tab completes subcommand for /theme." @@ -1053,7 +1076,7 @@ (init-state) (on-daemon-msg '(:type :stream-chunk :payload (:text "Hello"))) (on-daemon-msg '(:type :stream-chunk :payload (:text " world"))) - (let ((msgs (getf *state* :messages))) + (let ((msgs (st :messages))) (fiveam:is (= 1 (length msgs))) (let ((msg (aref msgs 0))) (fiveam:is (eq :agent (getf msg :role))) @@ -1065,35 +1088,35 @@ (init-state) (on-daemon-msg '(:type :stream-chunk :payload (:text "Hi"))) (on-daemon-msg '(:type :stream-chunk :payload (:text ""))) - (let ((msg (aref (getf *state* :messages) 0))) + (let ((msg (aref (st :messages) 0))) (fiveam:is (stringp (getf msg :time))) (fiveam:is (string= "Hi" (getf msg :content))) - (fiveam:is (null (getf *state* :streaming-text))))) + (fiveam:is (null (st :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 (getf *state* :messages) 0))) + (let ((msg (aref (st :messages) 0))) (fiveam:is (stringp (getf msg :time))) (fiveam:is (search "[interrupted]" (getf msg :content))) - (fiveam:is (null (getf *state* :streaming-text))) - (fiveam:is (null (getf *state* :busy))))) + (fiveam:is (null (st :streaming-text))) + (fiveam:is (null (st :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 (getf *state* :streaming-text))) - (fiveam:is (= 0 (length (getf *state* :messages))))) + (fiveam:is (null (st :streaming-text))) + (fiveam:is (= 0 (length (st :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" (getf *state* :url-buffer)))) + (fiveam:is (string= "https://example.com" (st :url-buffer)))) ;; ── v0.7.2 HITL Panels ── @@ -1104,7 +1127,7 @@ :payload (:sensor :approval-required :action (:TYPE :REQUEST :PAYLOAD (:TOOL "shell")) :message "rm -rf blocked"))) - (let ((m (aref (getf *state* :messages) 0))) + (let ((m (aref (st :messages) 0))) (fiveam:is (eq :system (getf m :role))) (fiveam:is (getf m :panel)) (fiveam:is (search "rm -rf" (getf m :content))))) @@ -1118,11 +1141,11 @@ (on-key (char-code ch))) (on-key 13) ;; Panel message (index 0) should be marked resolved - (let ((m (aref (getf *state* :messages) 0))) + (let ((m (aref (st :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 (getf *state* :messages) (1- (length (getf *state* :messages)))))) + (let ((m (aref (st :messages) (1- (length (st :messages)))))) (fiveam:is (search "Approved" (getf m :content))))) (fiveam:test test-hitl-panel-after-deny @@ -1133,7 +1156,7 @@ (dolist (ch (coerce "/deny HITL-deny" 'list)) (on-key (char-code ch))) (on-key 13) - (let ((m (aref (getf *state* :messages) 0))) + (let ((m (aref (st :messages) 0))) (fiveam:is (getf m :panel)) (fiveam:is (eq :denied (getf m :panel-resolved))))) @@ -1144,7 +1167,7 @@ (on-key (char-code ch))) (on-key 343) ;; Should add a system message confirming approval, not a user message - (let ((msgs (getf *state* :messages))) + (let ((msgs (st :messages))) (fiveam:is (>= (length msgs) 1)) (let ((m (aref msgs 0))) (fiveam:is (eq :system (getf m :role))) @@ -1156,7 +1179,7 @@ (dolist (ch (coerce "/deny HITL-xyz" 'list)) (on-key (char-code ch))) (on-key 343) - (let ((m (aref (getf *state* :messages) 0))) + (let ((m (aref (st :messages) 0))) (fiveam:is (eq :system (getf m :role))) (fiveam:is (search "Denied" (getf m :content))))) @@ -1168,7 +1191,7 @@ (dolist (ch (coerce "/undo" 'list)) (on-key (char-code ch))) (on-key 343) - (let ((m (aref (getf *state* :messages) 0))) + (let ((m (aref (st :messages) 0))) (fiveam:is (eq :system (getf m :role))) (fiveam:is (search "Undo" (getf m :content))))) @@ -1178,7 +1201,7 @@ (dolist (ch (coerce "/redo" 'list)) (on-key (char-code ch))) (on-key 343) - (let ((m (aref (getf *state* :messages) 0))) + (let ((m (aref (st :messages) 0))) (fiveam:is (eq :system (getf m :role))) (fiveam:is (search "Redo" (getf m :content))))) @@ -1191,7 +1214,7 @@ (dolist (ch (coerce "/why" 'list)) (on-key (char-code ch))) (on-key 13) - (let* ((msgs (getf *state* :messages)) + (let* ((msgs (st :messages)) (m (aref msgs (1- (length msgs))))) (fiveam:is (eq :system (getf m :role))) (fiveam:is (search "[BLOCKED]" (getf m :content))) @@ -1203,7 +1226,7 @@ (dolist (ch (coerce "/why" 'list)) (on-key (char-code ch))) (on-key 13) - (let* ((msgs (getf *state* :messages)) + (let* ((msgs (st :messages)) (m (aref msgs (1- (length msgs))))) (fiveam:is (search "No recent" (getf m :content))))) @@ -1214,11 +1237,11 @@ (init-state) (add-msg :agent "test" :gate-trace '((:gate "shell" :result :passed))) (on-key 7) ;; Ctrl+G — first press hides - (let* ((msgs (getf *state* :messages)) + (let* ((msgs (st :messages)) (m (aref msgs (1- (length msgs))))) (fiveam:is (search "hidden" (getf m :content)))) (on-key 7) ;; second press shows - (let* ((msgs (getf *state* :messages)) + (let* ((msgs (st :messages)) (m (aref msgs (1- (length msgs))))) (fiveam:is (search "shown" (getf m :content))))) @@ -1226,7 +1249,7 @@ "Contract v0.7.2: Ctrl+G with no gate trace shows fallback." (init-state) (on-key 7) - (let ((m (aref (getf *state* :messages) 0))) + (let ((m (aref (st :messages) 0))) (fiveam:is (search "No gate trace" (getf m :content))))) ;; ── v0.7.2 Message Search Mode ── @@ -1239,9 +1262,9 @@ (dolist (ch (coerce "/search hello" 'list)) (on-key (char-code ch))) (on-key 13) - (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:is (eq t (st :search-mode))) + (fiveam:is (string= "hello" (st :search-query))) + (fiveam:is (= 1 (length (st :search-matches))))) (fiveam:test test-search-mode-escape-exits "Contract v0.7.2: Escape exits search mode." @@ -1250,9 +1273,9 @@ (dolist (ch (coerce "/search test" 'list)) (on-key (char-code ch))) (on-key 13) - (fiveam:is (eq t (getf *state* :search-mode))) + (fiveam:is (eq t (st :search-mode))) (on-key 27) ;; Escape - (fiveam:is (null (getf *state* :search-mode)))) + (fiveam:is (null (st :search-mode)))) (fiveam:test test-search-mode-up-down-nav "Contract v0.7.2: Up/Down navigates between search matches." @@ -1263,13 +1286,13 @@ (dolist (ch (coerce "/search hello" 'list)) (on-key (char-code ch))) (on-key 13) - (fiveam:is (= 0 (getf *state* :search-match-idx))) + (fiveam:is (= 0 (st :search-match-idx))) (on-key 258) ;; Down - (fiveam:is (= 1 (getf *state* :search-match-idx))) + (fiveam:is (= 1 (st :search-match-idx))) (on-key 259) ;; Up - (fiveam:is (= 0 (getf *state* :search-match-idx))) + (fiveam:is (= 0 (st :search-match-idx))) (on-key 259) ;; Up (clamped) - (fiveam:is (= 0 (getf *state* :search-match-idx)))) + (fiveam:is (= 0 (st :search-match-idx)))) (fiveam:test test-context-sections "Contract v0.7.2: /context shows section breakdown with IDENTITY, TOOLS, LOGS." @@ -1278,7 +1301,7 @@ (dolist (ch (coerce "/context" 'list)) (on-key (char-code ch))) (on-key 13) - (let ((msgs (getf *state* :messages))) + (let ((msgs (st :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)))) @@ -1289,21 +1312,21 @@ (dolist (ch (coerce "/help configuration" 'list)) (on-key (char-code ch))) (on-key 13) - (let ((msgs (getf *state* :messages))) + (let ((msgs (st :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 (getf *state* :scroll-offset) 0) + (setf (st :scroll-offset) 0) (on-key :ppage) - (fiveam:is (> (getf *state* :scroll-offset) 5) "Should scroll by more than 5 lines")) + (fiveam:is (> (st :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 (getf *state* :scroll-offset) 3) + (setf (st :scroll-offset) 3) (on-key :npage) - (fiveam:is (= 0 (getf *state* :scroll-offset)))) + (fiveam:is (= 0 (st :scroll-offset)))) diff --git a/org/channel-tui-main.org b/org/channel-tui-main.org index 165f38c..20f6d5e 100644 --- a/org/channel-tui-main.org +++ b/org/channel-tui-main.org @@ -27,78 +27,87 @@ Event handlers + daemon I/O + main loop. 3. (send-daemon msg): serializes and sends a message to the daemon over the framed TCP protocol. 4. (tui-main): the main loop — connects to daemon, initializes - cl-tty terminal and framebuffer, optionally starts Swank REPL, runs - render/input event loop at ~10fps. + Croatoan windows, optionally starts Swank REPL, runs + render/input event loop at ~30fps. ** Event Handlers #+BEGIN_SRC lisp :tangle ../lisp/channel-tui-main.lisp (in-package :passepartout.channel-tui) -(defun on-key (ch) - (cond - ;; v0.7.1: Esc — interrupt streaming - ((and (eq ch :escape) (getf *state* :streaming-text)) +(defun on-key (&rest args) + ;; Normalize: get-char returns raw ncurses integer codes (e.g. 263 for + ;; backspace). Croatoan's code-key + key-name convert them to keywords + ;; so the cond below can use eq. + (let* ((raw (car args)) + (ch (if (and (integerp raw) (> raw 255)) + (let* ((k (code-key raw)) + (name (and k (key-name k)))) + (or name raw)) + raw))) + (cond + ;; v0.7.1: Esc — interrupt streaming + ((and (eql ch 27) (st :streaming-text)) (send-daemon (list :type :event :payload '(:action :cancel-stream))) - (when (> (length (getf *state* :messages)) 0) - (let ((idx (1- (length (getf *state* :messages))))) - (setf (getf (aref (getf *state* :messages) idx) :content) + (when (> (length (st :messages)) 0) + (let ((idx (1- (length (st :messages))))) + (setf (getf (aref (st :messages) idx) :content) (concatenate 'string - (getf (aref (getf *state* :messages) idx) :content) + (getf (aref (st :messages) idx) :content) " [interrupted]")) - (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))) + (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))) ;; v0.7.2: Esc — exit search mode - ((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)) + ((and (eql ch 27) (st :search-mode)) + (setf (st :search-mode) nil + (st :search-matches) nil + (st :search-query) "") + (setf (st :dirty) (list nil t nil)) (add-msg :system "Search exited")) ;; v0.7.2: search mode — Up/Down navigate matches - ((and (getf *state* :search-mode) (eq ch :up)) - (let* ((matches (getf *state* :search-matches)) - (idx (getf *state* :search-match-idx)) + ((and (st :search-mode) (or (eql ch 259) (eq ch :up))) + (let* ((matches (st :search-matches)) + (idx (st :search-match-idx)) (new-idx (max 0 (1- idx)))) - (setf (getf *state* :search-match-idx) new-idx) + (setf (st :search-match-idx) new-idx) (when matches - (setf (getf *state* :scroll-offset) (nth new-idx matches)) + (setf (st :scroll-offset) (nth new-idx matches)) (add-msg :system (format nil "Match ~d/~d" (1+ new-idx) (length matches))) - (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)) + (setf (st :dirty) (list nil t nil))))) + ((and (st :search-mode) (or (eql ch 258) (eq ch :down))) + (let* ((matches (st :search-matches)) + (idx (st :search-match-idx)) (new-idx (min (1- (length matches)) (1+ idx)))) - (setf (getf *state* :search-match-idx) new-idx) + (setf (st :search-match-idx) new-idx) (when matches - (setf (getf *state* :scroll-offset) (nth new-idx matches)) + (setf (st :scroll-offset) (nth new-idx matches)) (add-msg :system (format nil "Match ~d/~d" (1+ new-idx) (length matches))) - (setf (getf *state* :dirty) (list nil t nil))))) + (setf (st :dirty) (list nil t nil))))) ;; v0.7.2: search mode — Enter jumps to current match - ((and (getf *state* :search-mode) (eq ch :enter)) - (let ((matches (getf *state* :search-matches)) - (idx (getf *state* :search-match-idx))) + ((and (st :search-mode) (or (eql ch 13) (eql ch 10) (eq ch :enter))) + (let ((matches (st :search-matches)) + (idx (st :search-match-idx))) (when (and matches (>= (length matches) (1+ idx))) - (setf (getf *state* :scroll-offset) (nth idx matches)) - (setf (getf *state* :search-mode) nil - (getf *state* :search-matches) nil - (getf *state* :search-query) "") + (setf (st :scroll-offset) (nth idx matches)) + (setf (st :search-mode) nil + (st :search-matches) nil + (st :search-query) "") (add-msg :system (format nil "Jumped to match ~d" (1+ idx))) - (setf (getf *state* :dirty) (list nil t nil))))) + (setf (st :dirty) (list nil t nil))))) ;; v0.7.1: Tab on empty input — extract then open URL from agent message - ((and (eq ch :tab) - (null (getf *state* :input-buffer))) - (if (getf *state* :url-buffer) + ((and (or (eql ch 9) (eq ch :tab)) + (null (st :input-buffer))) + (if (st :url-buffer) ;; Already extracted — now open it (progn - (add-msg :system (format nil "Opening ~a" (getf *state* :url-buffer))) - (setf (getf *state* :url-buffer) nil)) + (add-msg :system (format nil "Opening ~a" (st :url-buffer))) + (setf (st :url-buffer) nil)) ;; Extract URL from last agent message (let ((url nil)) - (loop for i from (1- (length (getf *state* :messages))) downto 0 - for msg = (aref (getf *state* :messages) i) + (loop for i from (1- (length (st :messages))) downto 0 + for msg = (aref (st :messages) i) for content = (getf msg :content) for role = (getf msg :role) while (eq role :agent) @@ -112,69 +121,70 @@ Event handlers + daemon I/O + main loop. (return))))) (if url (progn - (setf (getf *state* :url-buffer) url) + (setf (st :url-buffer) url) (add-msg :system (format nil "Press Tab to open ~a" url)) - (setf (getf *state* :dirty) (list t t nil))) + (setf (st :dirty) (list t t nil))) nil)))) ;; v0.7.0: Ctrl key bindings - ((eq ch :ctrl-u) - (setf (getf *state* :input-buffer) nil) - (setf (getf *state* :dirty) (list nil nil t))) - ((eq ch :ctrl-w) - (let ((buf (getf *state* :input-buffer))) + ((eql ch 21) ; Ctrl+U — clear line + (setf (st :input-buffer) nil) + (setf (st :dirty) (list nil nil t))) + ((eql ch 23) ; Ctrl+W — delete word backward + (let ((buf (st :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 (getf *state* :input-buffer) buf) - (setf (getf *state* :dirty) (list nil nil t)))) - ((eq ch :ctrl-a) - (setf (getf *state* :cursor-pos) 0)) - ((eq ch :ctrl-e) - (setf (getf *state* :cursor-pos) (length (getf *state* :input-buffer)))) - ((eq ch :ctrl-l) - (setf (getf *state* :dirty) (list t t t))) - ((eq ch :ctrl-d) - (when (or (null (getf *state* :input-buffer)) (string= "" (input-string))) + (setf (st :input-buffer) buf) + (setf (st :dirty) (list nil nil t)))) + ((eql ch 1) ; Ctrl+A — home + (setf (st :cursor-pos) 0)) + ((eql ch 5) ; Ctrl+E — end + (setf (st :cursor-pos) (length (st :input-buffer)))) + ((eql ch 12) ; Ctrl+L — redraw + (setf (st :dirty) (list t t t))) + ((eql ch 4) ; Ctrl+D — quit on empty + (when (or (null (st :input-buffer)) (string= "" (input-string))) (add-msg :system "Goodbye. Run /quit or press Ctrl+D again to exit."))) - ((eq ch :ctrl-f) + ((eql ch 6) ; v0.7.2 Ctrl+F — message search (add-msg :system "Use /search to find messages")) - ((eq ch :ctrl-g) + ((eql ch 7) ; v0.7.2 Ctrl+G — toggle gate trace collapse (let ((gate-idx nil)) - (loop for i from (1- (length (getf *state* :messages))) downto 0 - for m = (aref (getf *state* :messages) i) + (loop for i from (1- (length (st :messages))) downto 0 + for m = (aref (st :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 (getf *state* :collapsed-gates))) + (let ((cg (st :collapsed-gates))) (if (member gate-idx cg) - (setf (getf *state* :collapsed-gates) (remove gate-idx cg)) - (push gate-idx (getf *state* :collapsed-gates))) + (setf (st :collapsed-gates) (remove gate-idx cg)) + (push gate-idx (st :collapsed-gates))) (add-msg :system (format nil "Gate trace ~a for msg ~a" - (if (member gate-idx (getf *state* :collapsed-gates)) "hidden" "shown") + (if (member gate-idx (st :collapsed-gates)) "hidden" "shown") gate-idx)) - (setf (getf *state* :dirty) (list nil t nil))) + (setf (st :dirty) (list nil t nil))) (add-msg :system "No gate trace to toggle")))) - ((eq ch :ctrl-x) - (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) + ((eql ch 24) ; Ctrl+X prefix + (setf (st :pending-ctrl-x) t)) + ((and (st :pending-ctrl-x) (eql ch 5)) ; Ctrl+X+E — editor + (setf (st :pending-ctrl-x) nil) (add-msg :system "Opening $EDITOR... save and exit to return.") - (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) + (setf (st :dirty) (list t t nil))) + ((and (st :pending-ctrl-x) (not (eql ch 5))) ; cancel Ctrl+X + (setf (st :pending-ctrl-x) nil) (on-key ch) (return-from on-key nil)) ;; Enter - ((eq ch :enter) + ((or (eq ch :enter) (eql ch 13) (eql ch 10) + (eql ch #\Newline) (eql ch #\Return)) ;; Multi-line: if buffer ends with \, strip it and insert newline - (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))) + (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))) (let ((text (string-trim '(#\Space #\Tab) (input-string)))) (when (> (length text) 0) - (push text (getf *state* :input-history)) - (setf (getf *state* :input-hpos) 0) - (setf (getf *state* :scroll-offset) 0) + (push text (st :input-history)) + (setf (st :input-hpos) 0) + (setf (st :scroll-offset) 0) (cond ;; v0.7.2: undo/redo ((string-equal text "/undo") @@ -201,7 +211,7 @@ Event handlers + daemon I/O + main loop. ;; /help command ;; /why command — show last gate trace ((string-equal text "/why") - (let ((msgs (getf *state* :messages)) + (let ((msgs (st :messages)) (found nil)) (loop for i from (1- (length msgs)) downto 0 for m = (aref msgs i) @@ -254,9 +264,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 (getf *state* :messages))) - (focus (or (getf *state* :foveal-id) "none")) - (id-tokens (min 200 (floor (+ 150 (length (or (getf *state* :focus-scope) ""))) 4))) + (let* ((msg-count (length (st :messages))) + (focus (or (st :foveal-id) "none")) + (id-tokens (min 200 (floor (+ 150 (length (or (st :focus-scope) ""))) 4))) (tool-tokens (if (boundp 'passepartout::*cognitive-tool-registry*) (floor (* (hash-table-count passepartout::*cognitive-tool-registry*) 40) 4) 50)) @@ -303,7 +313,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 (getf *state* :messages))) + (let* ((msg-count (length (st :messages))) (est-total (* msg-count 60)) (budget 8192) (dropped-msgs (if (> est-total budget) @@ -318,7 +328,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 (getf *state* :messages)) + (msgs (st :messages)) (total (length msgs)) (matches nil)) (loop for i from 0 below total @@ -328,10 +338,10 @@ Event handlers + daemon I/O + main loop. do (push i matches)) (setf matches (nreverse matches)) ;; Enter search mode - (setf (getf *state* :search-mode) t - (getf *state* :search-query) query - (getf *state* :search-matches) matches - (getf *state* :search-match-idx) 0) + (setf (st :search-mode) t + (st :search-query) query + (st :search-matches) matches + (st :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 +489,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 (getf *state* :input-history))) + (dolist (entry (reverse (st :input-history))) (write-line entry out)))) (add-msg :system "* Goodbye *") (send-daemon (list :type :event :payload '(:action :quit))) - (setf (getf *state* :running) nil)) + (setf (st :running) nil)) ;; /reconnect — re-establish daemon connection ((string-equal text "/reconnect") (disconnect-daemon) @@ -491,14 +501,14 @@ Event handlers + daemon I/O + main loop. ;; Normal message (t (add-msg :user text) - (setf (getf *state* :busy) t) + (setf (st :busy) t) (send-daemon (list :type :event :payload (list :sensor :user-input :text text))))) - (setf (getf *state* :input-buffer) nil) - (setf (getf *state* :cursor-pos) 0) - (setf (getf *state* :dirty) (list t t t)))))) + (setf (st :input-buffer) nil) + (setf (st :cursor-pos) 0) + (setf (st :dirty) (list t t t)))))) ;; Tab — command completion (v0.7.0: extended with subcommand + file paths) - ((eq ch :tab) + ((or (eql ch 9) (eq ch :tab)) (let ((text (input-string))) (cond ;; @ prefix — file path completion @@ -515,8 +525,8 @@ Event handlers + daemon I/O + main loop. (string-equal n partial :end2 (length partial)))) names))) (when match - (setf (getf *state* :input-buffer) (reverse (coerce (concatenate 'string "@" match) 'list))) - (setf (getf *state* :dirty) (list nil nil t))))) + (setf (st :input-buffer) (reverse (coerce (concatenate 'string "@" match) 'list))) + (setf (st :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 +534,8 @@ Event handlers + daemon I/O + main loop. (match (if (string= partial "") (first names) (find partial names :test #'string-equal)))) (when match - (setf (getf *state* :input-buffer) (reverse (coerce (concatenate 'string "/theme " match) 'list))) - (setf (getf *state* :dirty) (list nil nil t))))) + (setf (st :input-buffer) (reverse (coerce (concatenate 'string "/theme " match) 'list))) + (setf (st :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 +550,8 @@ Event handlers + daemon I/O + main loop. (string-equal d partial :end2 (length partial)))) dirs)))) (when match - (setf (getf *state* :input-buffer) (reverse (coerce (concatenate 'string "/focus " match) 'list))) - (setf (getf *state* :dirty) (list nil nil t))))) + (setf (st :input-buffer) (reverse (coerce (concatenate 'string "/focus " match) 'list))) + (setf (st :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,73 +559,71 @@ 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 (getf *state* :input-buffer) (reverse (coerce match 'list))) + (setf (st :input-buffer) (reverse (coerce match 'list))) (when (member match '("/eval" "/focus" "/scope") :test #'string=) - (push #\Space (getf *state* :input-buffer))) - (setf (getf *state* :dirty) (list nil nil t))))))) + (push #\Space (st :input-buffer))) + (setf (st :dirty) (list nil nil t)))))))) ;; Backspace - ((eq ch :backspace) + ((or (eq ch :backspace) (eql ch 127) (eql ch 8) + (eql ch #\Backspace)) (input-delete-char) - (setf (getf *state* :dirty) (list nil nil t))) + (setf (st :dirty) (list nil nil t))) ;; Left arrow - ((eq ch :left) - (when (> (or (getf *state* :cursor-pos) 0) 0) - (decf (getf *state* :cursor-pos)) - (setf (getf *state* :dirty) (list nil nil t)))) + ((or (eq ch :left) (eql ch 260)) + (when (> (or (st :cursor-pos) 0) 0) + (decf (st :cursor-pos)) + (setf (st :dirty) (list nil nil t)))) ;; Right arrow - ((eq ch :right) - (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)))) + ((or (eq ch :right) (eql ch 261)) + (when (< (or (st :cursor-pos) 0) (length (st :input-buffer))) + (incf (st :cursor-pos)) + (setf (st :dirty) (list nil nil t)))) ;; Up arrow - ((eq ch :up) - (let* ((h (getf *state* :input-history)) (p (getf *state* :input-hpos))) + ((or (eq ch :up) (eql ch 259)) + (let* ((h (st :input-history)) (p (st :input-hpos))) (when (and h (< p (1- (length h)))) - (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))))) + (incf (st :input-hpos)) + (setf (st :input-buffer) + (reverse (coerce (nth (st :input-hpos) h) 'list))) + (setf (st :dirty) (list nil nil t))))) ;; Down arrow - ((eq ch :down) - (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)) + ((or (eq ch :down) (eql ch 258)) + (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)) nil)) - (setf (getf *state* :dirty) (list nil nil t))))) + (setf (st :dirty) (list nil nil t))))) ;; PageUp — scroll back by page (10 lines) - ((eq ch :ppage) - (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))) + ((or (eq ch :ppage) (eql ch 339)) + (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))) ;; PageDown — scroll forward by page - ((eq ch :npage) - (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 - (character ch) - ((integer 32 126) (code-char ch)) - (keyword (let ((s (string ch))) - (and (= (length s) 1) - (char-downcase (char s 0))))) - (t nil)))) - (when (and chr (graphic-char-p chr)) - (input-insert-char chr) - (setf (getf *state* :dirty) (list nil nil t)))))))) + ((or (eq ch :npage) (eql ch 338)) + (setf (st :scroll-offset) (max 0 (- (st :scroll-offset) 10))) + (setf (st :dirty) (list nil t nil))) + ;; Printable + (t + (let ((chr (typecase ch + (character ch) + (integer (code-char ch)) + (t nil)))) + (when (and chr (graphic-char-p chr)) + (input-insert-char chr) + (setf (st :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 (getf *state* :messages))) downto 0 - for m = (aref (getf *state* :messages) i) + (loop for i from (1- (length (st :messages))) downto 0 + for m = (aref (st :messages) i) when (and (getf m :panel) (not (getf m :panel-resolved))) do (setf (getf m :panel-resolved) decision) - (setf (aref (getf *state* :messages) i) m) - (setf (getf *state* :dirty) (list nil t nil)) + (setf (aref (st :messages) i) m) + (setf (st :dirty) (list nil t nil)) (loop-finish))) ;; v0.7.2 — self-help-lookup: read USER_MANUAL.org and find matching sections @@ -680,43 +688,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 (getf *state* :dirty) (list nil t nil)) + (setf (st :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 (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)) + (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)) (return-from on-daemon-msg nil)) - ((null (getf *state* :streaming-text)) + ((null (st :streaming-text)) ;; First chunk: add new streaming message - (setf (getf *state* :streaming-text) "") - (setf (getf *state* :busy) nil) + (setf (st :streaming-text) "") + (setf (st :busy) nil) (add-msg :agent text) - (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)) + (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)) (return-from on-daemon-msg nil)) (t ;; Subsequent chunk: append - (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))) + (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))) (return-from on-daemon-msg nil)))) - (when rule-count (setf (getf *state* :rule-count) rule-count)) - (when foveal-id (setf (getf *state* :foveal-id) foveal-id)) + (when rule-count (setf (st :rule-count) rule-count)) + (when foveal-id (setf (st :foveal-id) foveal-id)) (cond - (text (setf (getf *state* :busy) nil) + (text (setf (st :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 +734,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 (getf *state* :stream))) + (let ((s (st :stream))) (when (and s (open-stream-p s)) (handler-case (progn @@ -754,7 +762,7 @@ Event handlers + daemon I/O + main loop. (defun reader-loop (s) (let ((consecutive-nils 0)) - (loop while (and (getf *state* :running) (open-stream-p s)) + (loop while (and (st :running) (open-stream-p s)) do (let ((msg (recv-daemon s))) (if msg (progn (queue-event (list :type :daemon :payload msg)) @@ -773,8 +781,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 (getf *state* :input-history)))) - (setf (getf *state* :input-history) (nreverse (getf *state* :input-history)))))) + do (push line (st :input-history)))) + (setf (st :input-history) (nreverse (st :input-history)))))) #+END_SRC ** Connection @@ -786,9 +794,9 @@ Event handlers + daemon I/O + main loop. do (sleep backoff) (handler-case (let ((s (usocket:socket-connect host port :timeout 5))) - (setf (getf *state* :stream) (usocket:socket-stream s) - (getf *state* :connected) t) - (bt:make-thread (lambda () (reader-loop (getf *state* :stream))) + (setf (st :stream) (usocket:socket-stream s) + (st :connected) t) + (bt:make-thread (lambda () (reader-loop (st :stream))) :name "tui-reader") (add-msg :system (format nil "* Connected v~a *" "0.5.0")) (return-from connect-daemon t)) @@ -804,9 +812,9 @@ Event handlers + daemon I/O + main loop. nil) (defun disconnect-daemon () - (when (getf *state* :stream) - (ignore-errors (close (getf *state* :stream))) - (setf (getf *state* :stream) nil (getf *state* :connected) nil) + (when (st :stream) + (ignore-errors (close (st :stream))) + (setf (st :stream) nil (st :connected) nil) (add-msg :system "* Disconnected *"))) #+END_SRC @@ -819,7 +827,7 @@ Event handlers + daemon I/O + main loop. (let* ((swank-port (or (ignore-errors (parse-integer (uiop:getenv "TUI_SWANK_PORT"))) 4006))) - (setf (getf *state* :dirty) (list t t t)) + (setf (st :dirty) (list t t t)) (connect-daemon) (when (> swank-port 0) (handler-case @@ -836,45 +844,60 @@ 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 curr-fb w h) + (redraw be curr-fb w h) (cl-tty.rendering:flush-framebuffer prev-fb curr-fb be) (rotatef prev-fb curr-fb) - (loop while (getf *state* :running) do + (loop while (st :running) do (dolist (ev (drain-queue)) (cond ((eq (getf ev :type) :daemon) (on-daemon-msg (getf ev :payload))) ((eq (getf ev :type) :disconnected) - (setf (getf *state* :connected) nil - (getf *state* :busy) nil) + (setf (st :connected) nil + (st :busy) nil) (add-msg :system "* Connection lost — type /reconnect to retry *")))) (multiple-value-bind (type data) (cl-tty.input:read-event be :timeout 0) (cond ((eq type :resize) - (multiple-value-setq (w h) - (or (ignore-errors (cl-tty.backend:backend-size be)) - (values 80 24))) + (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 (getf *state* :dirty) (list t t t))) + (setf (st :dirty) (list t t t))) (data - (let ((ch (typecase data - (cl-tty.input:key-event - (let ((k (cl-tty.input:key-event-key data)) - (ctrl (cl-tty.input:key-event-ctrl data))) - (if ctrl - (intern (format nil "CTRL-~a" k) :keyword) - k))) - (t data)))) - (on-key ch))))))) - (when (or (first (getf *state* :dirty)) (second (getf *state* :dirty)) (third (getf *state* :dirty))) + (let ((ch (typecase data + (cl-tty.input:key-event + (cl-tty.input:key-event-key data)) + (t data)))) + (cond + ((eql ch :escape) + (when (st :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) + (concatenate 'string + (getf (aref (st :messages) idx) :content) + " [interrupted]")) + (setf (getf (aref (st :messages) idx) :streaming) nil) + (setf (getf (aref (st :messages) idx) :time) (now)))) + (setf (st :streaming-text) nil) + (setf (st :busy) nil) + (setf (st :dirty) (list t t nil))) + (when (st :search-mode) + (setf (st :search-mode) nil + (st :search-matches) nil + (st :search-query) "") + (setf (st :dirty) (list nil t nil)) + (add-msg :system "Search exited"))) + (t (on-key ch))))))) + (when (or (first (st :dirty)) (second (st :dirty)) (third (st :dirty))) (cl-tty.backend:backend-clear curr-fb) - (redraw curr-fb w h) + (redraw be 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 @@ -894,19 +917,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 (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: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: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 (getf *state* :messages)) + (let* ((msgs (st :messages)) (msg (aref msgs 0))) (fiveam:is (eq :user (getf msg :role))) (fiveam:is (string= "hello" (getf msg :content))) @@ -916,9 +939,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 (getf *state* :dirty) (list nil nil nil)) + (setf (st :dirty) (list nil nil nil)) (add-msg :system "boot") - (let ((dirty (getf *state* :dirty))) + (let ((dirty (st :dirty))) (fiveam:is (eq t (first dirty))) (fiveam:is (eq t (second dirty))) (fiveam:is (eq nil (third dirty))))) @@ -946,7 +969,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 (getf *state* :messages))) + (let ((msgs (st :messages))) (fiveam:is (>= (length msgs) 1)) (let ((last (aref msgs 0))) (fiveam:is (eq :user (getf last :role))) @@ -959,7 +982,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 (getf *state* :messages))) + (let ((msgs (st :messages))) (fiveam:is (>= (length msgs) 1)) (let ((last-msg (aref msgs 0))) (fiveam:is (eq :system (getf last-msg :role))) @@ -981,7 +1004,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 (getf *state* :messages) 0))) + (let ((msg (aref (st :messages) 0))) (fiveam:is (eq :system (getf msg :role))))) (fiveam:test test-on-key-scope-command @@ -990,7 +1013,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 (getf *state* :messages) 0))) + (let ((msg (aref (st :messages) 0))) (fiveam:is (eq :system (getf msg :role))))) (fiveam:test test-on-key-unfocus-command @@ -999,7 +1022,7 @@ Event handlers + daemon I/O + main loop. (dolist (ch (coerce "/unfocus" 'list)) (on-key (char-code ch))) (on-key 343) - (let ((msg (aref (getf *state* :messages) 0))) + (let ((msg (aref (st :messages) 0))) (fiveam:is (eq :system (getf msg :role))))) (fiveam:test test-on-key-tab-completion @@ -1034,22 +1057,22 @@ Event handlers + daemon I/O + main loop. (dolist (ch (coerce "/help" 'list)) (on-key (char-code ch))) (on-key 343) - (let ((msgs (getf *state* :messages))) + (let ((msgs (st :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 (getf *state* :busy))) + (fiveam:is (eq nil (st :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 (getf *state* :busy))) + (fiveam:is (eq t (st :busy))) ;; Simulate receiving an agent response (clears busy) (on-daemon-msg '(:type :event :payload (:text "hi back"))) - (fiveam:is (eq nil (getf *state* :busy)))) + (fiveam:is (eq nil (st :busy)))) (fiveam:test test-theme "Contract view: *tui-theme* provides color mappings." @@ -1069,21 +1092,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 (getf *state* :dirty) (list nil nil nil)) + (setf (st :dirty) (list nil nil nil)) (on-key 12) ; Ctrl+L - (let ((d (getf *state* :dirty))) + (let ((d (st :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 (getf *state* :scroll-at-bottom) nil) + (setf (st :scroll-at-bottom) nil) (add-msg :agent "hi") - (fiveam:is (eq t (getf *state* :scroll-notify))) - (setf (getf *state* :scroll-at-bottom) t (getf *state* :scroll-notify) nil) + (fiveam:is (eq t (st :scroll-notify))) + (setf (st :scroll-at-bottom) t (st :scroll-notify) nil) (add-msg :agent "hi2") - (fiveam:is (eq nil (getf *state* :scroll-notify)))) + (fiveam:is (eq nil (st :scroll-notify)))) (fiveam:test test-tab-subcommand "Contract/v0.7.0: Tab completes subcommand for /theme." @@ -1099,7 +1122,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 (getf *state* :messages))) + (let ((msgs (st :messages))) (fiveam:is (= 1 (length msgs))) (let ((msg (aref msgs 0))) (fiveam:is (eq :agent (getf msg :role))) @@ -1111,35 +1134,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 (getf *state* :messages) 0))) + (let ((msg (aref (st :messages) 0))) (fiveam:is (stringp (getf msg :time))) (fiveam:is (string= "Hi" (getf msg :content))) - (fiveam:is (null (getf *state* :streaming-text))))) + (fiveam:is (null (st :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 (getf *state* :messages) 0))) + (let ((msg (aref (st :messages) 0))) (fiveam:is (stringp (getf msg :time))) (fiveam:is (search "[interrupted]" (getf msg :content))) - (fiveam:is (null (getf *state* :streaming-text))) - (fiveam:is (null (getf *state* :busy))))) + (fiveam:is (null (st :streaming-text))) + (fiveam:is (null (st :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 (getf *state* :streaming-text))) - (fiveam:is (= 0 (length (getf *state* :messages))))) + (fiveam:is (null (st :streaming-text))) + (fiveam:is (= 0 (length (st :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" (getf *state* :url-buffer)))) + (fiveam:is (string= "https://example.com" (st :url-buffer)))) ;; ── v0.7.2 HITL Panels ── @@ -1150,7 +1173,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 (getf *state* :messages) 0))) + (let ((m (aref (st :messages) 0))) (fiveam:is (eq :system (getf m :role))) (fiveam:is (getf m :panel)) (fiveam:is (search "rm -rf" (getf m :content))))) @@ -1164,11 +1187,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 (getf *state* :messages) 0))) + (let ((m (aref (st :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 (getf *state* :messages) (1- (length (getf *state* :messages)))))) + (let ((m (aref (st :messages) (1- (length (st :messages)))))) (fiveam:is (search "Approved" (getf m :content))))) (fiveam:test test-hitl-panel-after-deny @@ -1179,7 +1202,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 (getf *state* :messages) 0))) + (let ((m (aref (st :messages) 0))) (fiveam:is (getf m :panel)) (fiveam:is (eq :denied (getf m :panel-resolved))))) @@ -1190,7 +1213,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 (getf *state* :messages))) + (let ((msgs (st :messages))) (fiveam:is (>= (length msgs) 1)) (let ((m (aref msgs 0))) (fiveam:is (eq :system (getf m :role))) @@ -1202,7 +1225,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 (getf *state* :messages) 0))) + (let ((m (aref (st :messages) 0))) (fiveam:is (eq :system (getf m :role))) (fiveam:is (search "Denied" (getf m :content))))) @@ -1214,7 +1237,7 @@ Event handlers + daemon I/O + main loop. (dolist (ch (coerce "/undo" 'list)) (on-key (char-code ch))) (on-key 343) - (let ((m (aref (getf *state* :messages) 0))) + (let ((m (aref (st :messages) 0))) (fiveam:is (eq :system (getf m :role))) (fiveam:is (search "Undo" (getf m :content))))) @@ -1224,7 +1247,7 @@ Event handlers + daemon I/O + main loop. (dolist (ch (coerce "/redo" 'list)) (on-key (char-code ch))) (on-key 343) - (let ((m (aref (getf *state* :messages) 0))) + (let ((m (aref (st :messages) 0))) (fiveam:is (eq :system (getf m :role))) (fiveam:is (search "Redo" (getf m :content))))) @@ -1237,7 +1260,7 @@ Event handlers + daemon I/O + main loop. (dolist (ch (coerce "/why" 'list)) (on-key (char-code ch))) (on-key 13) - (let* ((msgs (getf *state* :messages)) + (let* ((msgs (st :messages)) (m (aref msgs (1- (length msgs))))) (fiveam:is (eq :system (getf m :role))) (fiveam:is (search "[BLOCKED]" (getf m :content))) @@ -1249,7 +1272,7 @@ Event handlers + daemon I/O + main loop. (dolist (ch (coerce "/why" 'list)) (on-key (char-code ch))) (on-key 13) - (let* ((msgs (getf *state* :messages)) + (let* ((msgs (st :messages)) (m (aref msgs (1- (length msgs))))) (fiveam:is (search "No recent" (getf m :content))))) @@ -1260,11 +1283,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 (getf *state* :messages)) + (let* ((msgs (st :messages)) (m (aref msgs (1- (length msgs))))) (fiveam:is (search "hidden" (getf m :content)))) (on-key 7) ;; second press shows - (let* ((msgs (getf *state* :messages)) + (let* ((msgs (st :messages)) (m (aref msgs (1- (length msgs))))) (fiveam:is (search "shown" (getf m :content))))) @@ -1272,7 +1295,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 (getf *state* :messages) 0))) + (let ((m (aref (st :messages) 0))) (fiveam:is (search "No gate trace" (getf m :content))))) ;; ── v0.7.2 Message Search Mode ── @@ -1285,9 +1308,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 (getf *state* :search-mode))) - (fiveam:is (string= "hello" (getf *state* :search-query))) - (fiveam:is (= 1 (length (getf *state* :search-matches))))) + (fiveam:is (eq t (st :search-mode))) + (fiveam:is (string= "hello" (st :search-query))) + (fiveam:is (= 1 (length (st :search-matches))))) (fiveam:test test-search-mode-escape-exits "Contract v0.7.2: Escape exits search mode." @@ -1296,9 +1319,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 (getf *state* :search-mode))) + (fiveam:is (eq t (st :search-mode))) (on-key 27) ;; Escape - (fiveam:is (null (getf *state* :search-mode)))) + (fiveam:is (null (st :search-mode)))) (fiveam:test test-search-mode-up-down-nav "Contract v0.7.2: Up/Down navigates between search matches." @@ -1309,13 +1332,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 (getf *state* :search-match-idx))) + (fiveam:is (= 0 (st :search-match-idx))) (on-key 258) ;; Down - (fiveam:is (= 1 (getf *state* :search-match-idx))) + (fiveam:is (= 1 (st :search-match-idx))) (on-key 259) ;; Up - (fiveam:is (= 0 (getf *state* :search-match-idx))) + (fiveam:is (= 0 (st :search-match-idx))) (on-key 259) ;; Up (clamped) - (fiveam:is (= 0 (getf *state* :search-match-idx)))) + (fiveam:is (= 0 (st :search-match-idx)))) (fiveam:test test-context-sections "Contract v0.7.2: /context shows section breakdown with IDENTITY, TOOLS, LOGS." @@ -1324,7 +1347,7 @@ Event handlers + daemon I/O + main loop. (dolist (ch (coerce "/context" 'list)) (on-key (char-code ch))) (on-key 13) - (let ((msgs (getf *state* :messages))) + (let ((msgs (st :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)))) @@ -1335,22 +1358,22 @@ Event handlers + daemon I/O + main loop. (dolist (ch (coerce "/help configuration" 'list)) (on-key (char-code ch))) (on-key 13) - (let ((msgs (getf *state* :messages))) + (let ((msgs (st :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 (getf *state* :scroll-offset) 0) + (setf (st :scroll-offset) 0) (on-key :ppage) - (fiveam:is (> (getf *state* :scroll-offset) 5) "Should scroll by more than 5 lines")) + (fiveam:is (> (st :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 (getf *state* :scroll-offset) 3) + (setf (st :scroll-offset) 3) (on-key :npage) - (fiveam:is (= 0 (getf *state* :scroll-offset)))) + (fiveam:is (= 0 (st :scroll-offset)))) #+END_SRC diff --git a/passepartout b/passepartout index 5d3ae16..6a79aa1 100755 --- a/passepartout +++ b/passepartout @@ -381,13 +381,32 @@ case "$COMMAND" in echo "Starting daemon first..." $0 daemon fi - exec sbcl \ - --eval '(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))' \ - --eval '(declaim (optimize (debug 3) (speed 0) (safety 3)))' \ - --eval "(push (truename \"$PASSEPARTOUT_DATA_DIR/\") asdf:*central-registry*)" \ - --eval '(ql:quickload :passepartout/tui)' \ - --eval '(in-package :passepartout)' \ - --eval '(handler-bind ((error (lambda (c) (ignore-errors (with-open-file (f (merge-pathnames ".cache/passepartout/tui-crash.log" (user-homedir-pathname)) :direction :output :if-exists :supersede :if-does-not-exist :create) (format f "CRASH: ~a~%~%" c) (sb-debug:print-backtrace :count 50 :stream f) (finish-output f))) (format t "~%=== TUI CRASH ===~%CRASH: ~a~%" c) (format t "Full backtrace saved to ~~/.cache/passepartout/tui-crash.log~~%") (sleep 3) (finish-output) (uiop:quit 1)))) (passepartout.channel-tui:tui-main))' + # Build TUI load script with proper paths + cat > /tmp/tui-load.lisp << LISPEOF +(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname))) +(declaim (optimize (debug 3) (speed 0) (safety 3))) +(push (truename "$PASSEPARTOUT_DATA_DIR/") asdf:*central-registry*) +(ql:quickload :cl-tty :silent t) +(ql:quickload :passepartout :silent t) +(let ((dir (pathname (format nil "~a/lisp/" (truename "$PASSEPARTOUT_DATA_DIR"))))) + (dolist (f '("channel-tui-state" "channel-tui-view" "channel-tui-main")) + (let* ((src (merge-pathnames (format nil "~a.lisp" f) dir)) + (fasl (merge-pathnames (format nil "~a.fasl" f) dir))) + (when (or (not (probe-file fasl)) + (< (file-write-date fasl) (file-write-date src))) + (compile-file src :output-file fasl :verbose nil :print nil)) + (load fasl :verbose nil :print nil)))) +(in-package :passepartout) +(handler-bind ((error (lambda (c) (ignore-errors + (with-open-file (f (merge-pathnames ".cache/passepartout/tui-crash.log" (user-homedir-pathname)) + :direction :output :if-exists :supersede :if-does-not-exist :create) + (format f "CRASH: ~a~%~%" c) (sb-debug:print-backtrace :count 50 :stream f) (finish-output f))) + (format t "~%=== TUI CRASH ===~%CRASH: ~a~%" c) + (format t "Full backtrace saved to ~/.cache/passepartout/tui-crash.log~%") + (sleep 3) (finish-output) (uiop:quit 1)))) + (passepartout.channel-tui:tui-main)) +LISPEOF + exec sbcl --noinform --load /tmp/tui-load.lisp ;; gateway) SUBCMD=$1; PLATFORM=$2; TOKEN=$3