diff --git a/lisp/channel-tui-main.lisp b/lisp/channel-tui-main.lisp index 396636d..4cff6a0 100644 --- a/lisp/channel-tui-main.lisp +++ b/lisp/channel-tui-main.lisp @@ -866,43 +866,49 @@ (cl-tty.backend:with-terminal (be w h) (let ((tty (open "/dev/tty" :direction :input))) ;; Initial render - (cl-tty.backend:backend-clear be) - (view-status be w h) - (view-chat be w h) - (view-input be w h) - (cl-tty.backend:draw-text be 0 (- h 4) (make-string w :initial-element #\─) - (theme-color :separator) nil) - (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 (st :connected) nil - (st :busy) nil) - (add-msg :system "* Connection lost — type /reconnect to retry *")))) - ;; Read key input from /dev/tty (non-blocking) - (let ((raw-ch (read-char-no-hang tty nil nil))) - (when raw-ch - (let ((code (char-code raw-ch))) - (let ((ch (cond - ((= code 13) :enter) - ((= code 10) :enter) - ((= code 27) :escape) - ((= code 9) :tab) - ((or (= code 127) (= code 8)) :backspace) - ((and (>= code 1) (<= code 26)) - (intern (string-upcase (format nil "CTRL-~a" - (code-char (+ #x60 code)))) - :keyword)) - (t raw-ch)))) - (case ch - (:CTRL-Q (setf (st :running) nil)) - (:CTRL-P (command-palette-show-commands)) - (:CTRL-B (setf (st :sidebar-visible) (not (st :sidebar-visible))) - (setf (st :dirty) (list t t nil))) - (:CTRL-L (setf (st :dirty) (list t t t))) - (t (on-key ch))))))) + (cl-tty.backend:backend-clear be) + (view-status be w h) + (view-chat be w h) + (view-input be w h) + (cl-tty.backend:draw-text be 0 (- h 4) (make-string w :initial-element #\─) + (theme-color :separator) nil) + (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 (st :connected) nil + (st :busy) nil) + (add-msg :system "* Connection lost — type /reconnect to retry *")))) + ;; Check for terminal resize (SIGWINCH sets this flag) + (when (boundp 'cl-tty.input::*terminal-resized-p*) + (when cl-tty.input::*terminal-resized-p* + (setf cl-tty.input::*terminal-resized-p* nil) + (multiple-value-setq (w h) (cl-tty.backend:backend-size be)) + (setf (st :dirty) (list t t t)))) + ;; Read key input from /dev/tty (non-blocking) + (let ((raw-ch (read-char-no-hang tty nil nil))) + (when raw-ch + (let ((code (char-code raw-ch))) + (let ((ch (cond + ((= code 13) :enter) + ((= code 10) :enter) + ((= code 27) :escape) + ((= code 9) :tab) + ((or (= code 127) (= code 8)) :backspace) + ((and (>= code 1) (<= code 26)) + (intern (string-upcase (format nil "CTRL-~a" + (code-char (+ #x60 code)))) + :keyword)) + (t raw-ch)))) + (case ch + (:CTRL-Q (setf (st :running) nil)) + (:CTRL-P (command-palette-show-commands)) + (:CTRL-B (setf (st :sidebar-visible) (not (st :sidebar-visible))) + (setf (st :dirty) (list t t nil))) + (:CTRL-L (setf (st :dirty) (list t t t))) + (t (on-key ch))))))) (when (or (first (st :dirty)) (second (st :dirty)) (third (st :dirty))) (cl-tty.backend:backend-clear be) (view-status be w h) @@ -944,29 +950,23 @@ (sel-p (theme-color :accent)) (t (theme-color :agent-fg))) nil :bold sel-p) - (incf y-off))))))) - (sleep 0.1))) + (incf y-off))))))) + (sleep 0.1)))) (close tty)) - (disconnect-daemon)))) + (disconnect-daemon))) -#+passepartout-tests (eval-when (:compile-toplevel :load-toplevel :execute) (ql:quickload :fiveam :silent t)) -#+passepartout-tests (defpackage :passepartout-tui-tests (:use :cl :passepartout :passepartout.channel-tui) (:export #:tui-suite)) -#+passepartout-tests (in-package :passepartout-tui-tests) -#+passepartout-tests (fiveam:def-suite tui-suite :description "Verification of the TUI model and event handling") -#+passepartout-tests (fiveam:in-suite tui-suite) -#+passepartout-tests (fiveam:test test-init-state "Contract model.1: init-state returns fresh state plist with required keys." (init-state) @@ -978,7 +978,6 @@ (fiveam:is (eq 0 (st :scroll-offset))) (fiveam:is (eq nil (st :busy)))) -#+passepartout-tests (fiveam:test test-add-msg "Contract model.2: add-msg appends a message with role, content, and time." (init-state) @@ -990,7 +989,6 @@ (fiveam:is (stringp (getf msg :time))) (fiveam:is (= 5 (length (getf msg :time)))))) -#+passepartout-tests (fiveam:test test-add-msg-dirty-flag "Contract model.2: add-msg sets dirty flags for status and chat." (init-state) @@ -1001,7 +999,6 @@ (fiveam:is (eq t (second dirty))) (fiveam:is (eq nil (third dirty))))) -#+passepartout-tests (fiveam:test test-queue-event-roundtrip "Contract model.3: queue-event + drain-queue preserves events in order." (init-state) @@ -1013,7 +1010,6 @@ (fiveam:is (equal '(:type :daemon :payload (:text "hi")) (second evs))) (fiveam:is (null (drain-queue))))) -#+passepartout-tests (fiveam:test test-on-key-enter-sends-user-message "Contract 1: on-key with Enter extracts input, adds user message, clears buffer." (init-state) @@ -1032,7 +1028,6 @@ (fiveam:is (eq :user (getf last :role))) (fiveam:is (string= "test" (getf last :content)))))) -#+passepartout-tests (fiveam:test test-on-key-eval-command "Contract 1: on-key handles /eval command and displays result." (init-state) @@ -1046,7 +1041,6 @@ (fiveam:is (eq :system (getf last-msg :role))) (fiveam:is (search "=> 3" (getf last-msg :content)))))) -#+passepartout-tests (fiveam:test test-on-key-backspace "Contract 1: on-key with Backspace removes last character from buffer." (init-state) @@ -1057,7 +1051,6 @@ (on-key 263) (fiveam:is (string= "ab" (input-string)))) -#+passepartout-tests (fiveam:test test-on-key-focus-command "Contract 1: /focus command parses project name." (init-state) @@ -1067,7 +1060,6 @@ (let ((msg (aref (st :messages) 0))) (fiveam:is (eq :system (getf msg :role))))) -#+passepartout-tests (fiveam:test test-on-key-scope-command "Contract 1: /scope command with valid argument." (init-state) @@ -1077,7 +1069,6 @@ (let ((msg (aref (st :messages) 0))) (fiveam:is (eq :system (getf msg :role))))) -#+passepartout-tests (fiveam:test test-on-key-unfocus-command "Contract 1: /unfocus command dispatches correctly." (init-state) @@ -1087,7 +1078,6 @@ (let ((msg (aref (st :messages) 0))) (fiveam:is (eq :system (getf msg :role))))) -#+passepartout-tests (fiveam:test test-on-key-tab-completion "Contract 1: Tab completes / commands when input starts with /." (init-state) @@ -1096,7 +1086,6 @@ (on-key 9) (fiveam:is (string= "/eval " (input-string)))) -#+passepartout-tests (fiveam:test test-on-key-tab-no-slash "Contract 1: Tab does nothing when input doesn't start with /." (init-state) @@ -1105,7 +1094,6 @@ (on-key 9) (fiveam:is (string= "hello" (input-string)))) -#+passepartout-tests (fiveam:test test-on-key-multiline "Contract 1: \\ + Enter inserts newline instead of sending." (init-state) @@ -1116,7 +1104,6 @@ (fiveam:is (search "line1" (input-string))) (fiveam:is (search (string #\Newline) (input-string)))) -#+passepartout-tests (fiveam:test test-on-key-help "Contract 1: /help displays command list." (init-state) @@ -1127,7 +1114,6 @@ (fiveam:is (>= (length msgs) 3)) (fiveam:is (some (lambda (m) (search "/eval" (getf m :content))) msgs)))) -#+passepartout-tests (fiveam:test test-activity-indicator "Contract model: :busy flag is set on send and cleared on agent response." (init-state) @@ -1141,7 +1127,6 @@ (on-daemon-msg '(:type :event :payload (:text "hi back"))) (fiveam:is (eq nil (st :busy)))) -#+passepartout-tests (fiveam:test test-theme "Contract view: *tui-theme* provides warm color mappings." (fiveam:is (string= "#FFB347" (getf *tui-theme* :user-fg))) @@ -1150,7 +1135,6 @@ (fiveam:is (string= "#E8D5B7" (getf *tui-theme* :input-fg))) (fiveam:is (string= "#FFFFFF" (theme-color :unknown-role)))) -#+passepartout-tests (fiveam:test test-on-key-ctrl-u-clears "Contract v0.9.0: Ctrl+U (via dispatch-key-event) clears the input buffer." (init-state) @@ -1159,7 +1143,6 @@ (cl-tty.input:make-key-event :key :u :ctrl t :code 21)) (fiveam:is (string= "" (input-string)))) -#+passepartout-tests (fiveam:test test-on-key-ctrl-l-redraws "Contract v0.9.0: Ctrl+L (via dispatch-key-event) sets all dirty flags." (init-state) @@ -1170,7 +1153,6 @@ (fiveam:is (eq t (first d))) (fiveam:is (eq t (second d))))) -#+passepartout-tests (fiveam:test test-scroll-notify "Contract/v0.7.0: add-msg sets scroll-notify when scrolled up." (init-state) @@ -1181,7 +1163,6 @@ (add-msg :agent "hi2") (fiveam:is (eq nil (st :scroll-notify)))) -#+passepartout-tests (fiveam:test test-tab-subcommand "Contract/v0.7.0: Tab completes subcommand for /theme." (init-state) @@ -1191,7 +1172,6 @@ ;; ── v0.7.1 Streaming ── -#+passepartout-tests (fiveam:test test-stream-chunk-appends "Contract/v0.7.1: stream-chunk frame appends to last message." (init-state) @@ -1204,7 +1184,6 @@ (fiveam:is (string= "Hello world" (getf msg :content))) (fiveam:is (eq t (getf msg :streaming)))))) -#+passepartout-tests (fiveam:test test-stream-chunk-final "Contract/v0.7.1: final empty chunk stamps timestamp and clears streaming flag." (init-state) @@ -1215,7 +1194,6 @@ (fiveam:is (string= "Hi" (getf msg :content))) (fiveam:is (null (st :streaming-text))))) -#+passepartout-tests (fiveam:test test-stream-interrupt "Contract/v0.7.1: Esc during streaming appends [interrupted] and finalizes." (init-state) @@ -1227,7 +1205,6 @@ (fiveam:is (null (st :streaming-text))) (fiveam:is (null (st :busy))))) -#+passepartout-tests (fiveam:test test-stream-check-skip "Contract/v0.7.1: Esc without active streaming does nothing." (init-state) @@ -1235,7 +1212,6 @@ (fiveam:is (null (st :streaming-text))) (fiveam:is (= 0 (length (st :messages))))) -#+passepartout-tests (fiveam:test test-tab-open-url "Contract/v0.7.1: Tab on empty input with URL message extracts URL." (init-state) @@ -1245,7 +1221,6 @@ ;; ── v0.7.2 HITL Panels ── -#+passepartout-tests (fiveam:test test-hitl-panel-in-on-daemon-msg "Contract v0.7.2: approval-required messages render as HITL panels." (init-state) @@ -1258,7 +1233,6 @@ (fiveam:is (getf m :panel)) (fiveam:is (search "rm -rf" (getf m :content))))) -#+passepartout-tests (fiveam:test test-hitl-panel-after-approve "Contract v0.7.2: /approve adds confirmation and marks panel resolved." (init-state) @@ -1275,7 +1249,6 @@ (let ((m (aref (st :messages) (1- (length (st :messages)))))) (fiveam:is (search "Approved" (getf m :content))))) -#+passepartout-tests (fiveam:test test-hitl-panel-after-deny "Contract v0.7.2: /deny marks panel as denied." (init-state) @@ -1288,7 +1261,6 @@ (fiveam:is (getf m :panel)) (fiveam:is (eq :denied (getf m :panel-resolved))))) -#+passepartout-tests (fiveam:test test-hitl-approve-parsed "Contract v0.7.2: /approve HITL-xxxx sends structured event, not raw text." (init-state) @@ -1302,7 +1274,6 @@ (fiveam:is (eq :system (getf m :role))) (fiveam:is (search "Approved" (getf m :content)))))) -#+passepartout-tests (fiveam:test test-hitl-deny-parsed "Contract v0.7.2: /deny HITL-xxxx sends structured denial." (init-state) @@ -1315,7 +1286,6 @@ ;; ── v0.7.2 Undo/Redo ── -#+passepartout-tests (fiveam:test test-undo-command "Contract v0.7.2: /undo sends undo event." (init-state) @@ -1326,7 +1296,6 @@ (fiveam:is (eq :system (getf m :role))) (fiveam:is (search "Undo" (getf m :content))))) -#+passepartout-tests (fiveam:test test-redo-command "Contract v0.7.2: /redo sends redo event." (init-state) @@ -1339,7 +1308,6 @@ ;; ── v0.7.2 Self-help ── -#+passepartout-tests (fiveam:test test-why-command "Contract v0.7.2: /why shows gate trace from last message." (init-state) @@ -1353,7 +1321,6 @@ (fiveam:is (search "[BLOCKED]" (getf m :content))) (fiveam:is (search "shell" (getf m :content))))) -#+passepartout-tests (fiveam:test test-why-no-trace "Contract v0.7.2: /why with no gate trace shows fallback message." (init-state) @@ -1366,7 +1333,6 @@ ;; ── v0.7.2 Gate Trace Toggle (Ctrl+G) ── -#+passepartout-tests (fiveam:test test-ctrlg-toggle-gate-trace "Contract v0.9.0: Ctrl+G (via dispatch-key-event) toggles gate-trace collapse state." (init-state) @@ -1382,7 +1348,6 @@ (m (aref msgs (1- (length msgs))))) (fiveam:is (search "shown" (getf m :content))))) -#+passepartout-tests (fiveam:test test-ctrlg-no-gate-trace "Contract v0.9.0: Ctrl+G (via dispatch-key-event) with no gate trace shows fallback." (init-state) @@ -1393,7 +1358,6 @@ ;; ── v0.7.2 Message Search Mode ── -#+passepartout-tests (fiveam:test test-search-mode-activate "Contract v0.7.2: /search enters search mode." (init-state) @@ -1406,7 +1370,6 @@ (fiveam:is (string= "hello" (st :search-query))) (fiveam:is (= 1 (length (st :search-matches))))) -#+passepartout-tests (fiveam:test test-search-mode-escape-exits "Contract v0.7.2: Escape exits search mode." (init-state) @@ -1418,7 +1381,6 @@ (on-key 27) ;; Escape (fiveam:is (null (st :search-mode)))) -#+passepartout-tests (fiveam:test test-search-mode-up-down-nav "Contract v0.7.2: Up/Down navigates between search matches." (init-state) @@ -1436,7 +1398,6 @@ (on-key 259) ;; Up (clamped) (fiveam:is (= 0 (st :search-match-idx)))) -#+passepartout-tests (fiveam:test test-context-sections "Contract v0.7.2: /context shows section breakdown with IDENTITY, TOOLS, LOGS." (init-state) @@ -1449,7 +1410,6 @@ (fiveam:is (some (lambda (m) (search "LOGS" (getf m :content))) msgs)) (fiveam:is (some (lambda (m) (search "TOOLS" (getf m :content))) msgs)))) -#+passepartout-tests (fiveam:test test-help-topic-lookup "Contract v0.7.2: /help reads and searches USER_MANUAL.org." (init-state) @@ -1459,7 +1419,6 @@ (let ((msgs (st :messages))) (fiveam:is (some (lambda (m) (search ".env" (getf m :content))) msgs)))) -#+passepartout-tests (fiveam:test test-pads-page-up "Contract v0.7.2: PageUp scrolls by page size (> 5 lines)." (init-state) @@ -1468,7 +1427,6 @@ (on-key :ppage) (fiveam:is (> (st :scroll-offset) 5) "Should scroll by more than 5 lines")) -#+passepartout-tests (fiveam:test test-pads-page-down-clamp "Contract v0.7.2: PageDown clamps to 0." (init-state) @@ -1479,7 +1437,6 @@ ;; ── v0.8.0 Minibuffer ── -#+passepartout-tests (fiveam:test test-slash-commands-defined "Contract v0.8.0: *slash-commands* is non-nil list of option plists." (fiveam:is (listp passepartout.channel-tui::*slash-commands*)) @@ -1488,14 +1445,12 @@ (and (getf opt :title) (getf opt :value) (getf opt :category))) passepartout.channel-tui::*slash-commands*))) -#+passepartout-tests (fiveam:test test-minibuffer-state "Contract v0.8.0: init-state has :dialog-stack and :minibuffer-active fields." (init-state) (fiveam:is (null (st :dialog-stack))) (fiveam:is (null (st :minibuffer-active)))) -#+passepartout-tests (fiveam:test test-command-palette-state "Contract v0.8.0: init-state has :command-palette-active and :command-palette-dialog as nil." (init-state) diff --git a/org/channel-tui-main.org b/org/channel-tui-main.org index dd24803..995b967 100644 --- a/org/channel-tui-main.org +++ b/org/channel-tui-main.org @@ -925,6 +925,12 @@ Event handlers + daemon I/O + main loop. (setf (st :connected) nil (st :busy) nil) (add-msg :system "* Connection lost — type /reconnect to retry *")))) + ;; Check for terminal resize (SIGWINCH sets this flag) + (when (boundp 'cl-tty.input::*terminal-resized-p*) + (when cl-tty.input::*terminal-resized-p* + (setf cl-tty.input::*terminal-resized-p* nil) + (multiple-value-setq (w h) (cl-tty.backend:backend-size be)) + (setf (st :dirty) (list t t t)))) ;; Read key input from /dev/tty (non-blocking) (let ((raw-ch (read-char-no-hang tty nil nil))) (when raw-ch