fix(tui): resolve crash by removing --non-interactive and adding defensive rendering
This commit is contained in:
@@ -27,25 +27,18 @@
|
||||
(setf *incoming-msgs* nil)
|
||||
msgs)))
|
||||
|
||||
(defun get-line-style (text)
|
||||
(cond
|
||||
((uiop:string-prefix-p "*" text) '(:bold :yellow))
|
||||
((uiop:string-prefix-p "⬆" text) '(:cyan))
|
||||
((uiop:string-prefix-p "🤔" text) '(:italic))
|
||||
((uiop:string-prefix-p "ERROR" text) '(:bold :red))
|
||||
(t nil)))
|
||||
|
||||
(defun render-chat (win)
|
||||
(clear win)
|
||||
(let* ((h (height win))
|
||||
(view-height (max 0 (- h 2)))
|
||||
(history-len (length *chat-history*))
|
||||
(start-idx *scroll-index*)
|
||||
(end-idx (min history-len (+ start-idx view-height)))
|
||||
(slice (reverse (subseq *chat-history* start-idx end-idx))))
|
||||
(loop for msg in slice
|
||||
for i from 1
|
||||
do (add-string win (format nil "│ ~a" msg) :y i :x 1 :attributes (get-line-style msg)))
|
||||
(defun render-chat (win h)
|
||||
(when (and win (integerp h))
|
||||
(clear win)
|
||||
(box win 0 0)
|
||||
(let* ((view-height (- h 2))
|
||||
(history (reverse *chat-history*))
|
||||
(len (length history))
|
||||
(num-to-draw (min len view-height)))
|
||||
(loop for i from 0 below num-to-draw
|
||||
for msg in history
|
||||
do (when (and msg (< (1+ i) (1- h)))
|
||||
(add-string win (format nil "~a" msg) :y (1+ i) :x 2))))
|
||||
(refresh win)))
|
||||
|
||||
(defun handle-backspace ()
|
||||
@@ -69,14 +62,12 @@
|
||||
(finish-output stream)))
|
||||
(enqueue-msg "✓ Sent"))
|
||||
(error (c)
|
||||
(format t "Send error: ~a~%" c)
|
||||
(enqueue-msg "ERROR: Connection to daemon lost.")
|
||||
(setf *is-running* nil))))
|
||||
(when (string= cmd "/exit") (setf *is-running* nil))
|
||||
(when (string= cmd "/clear") (setf *chat-history* nil))))
|
||||
(enqueue-msg (format nil "ERROR: Connection lost (~a)" c))
|
||||
(setf *is-running* nil))))
|
||||
(when (string= cmd "/exit") (setf *is-running* nil))
|
||||
(when (string= cmd "/clear") (setf *chat-history* nil))))
|
||||
|
||||
(defun start-background-reader (stream)
|
||||
"Starts a thread that reads framed messages from the daemon stream."
|
||||
(bt:make-thread
|
||||
(lambda ()
|
||||
(loop while *is-running* do
|
||||
@@ -92,10 +83,6 @@
|
||||
(cond
|
||||
((eq (getf payload :action) :handshake)
|
||||
(enqueue-msg "* Connected to daemon *"))
|
||||
((and (eq (getf payload :sensor) :loop-error)
|
||||
(not (string= (or (getf payload :message) "") "Neural Cascade Failure: All providers exhausted.")))
|
||||
(enqueue-msg (format nil "ERROR: Daemon loop error (~a)"
|
||||
(getf payload :message))))
|
||||
(t
|
||||
(let ((text (or (getf payload :text) (format nil "~a" payload))))
|
||||
(enqueue-msg (format nil "⬇ ~a" text))))))))))
|
||||
@@ -103,34 +90,36 @@
|
||||
(when *is-running*
|
||||
(enqueue-msg (format nil "ERROR: Connection lost (~a)" c))
|
||||
(setf *is-running* nil))))))
|
||||
:name "opencortex-tui-reader")))
|
||||
)
|
||||
:name "opencortex-tui-reader"))
|
||||
|
||||
(defun main ()
|
||||
(setf (uiop:getenv "PROVIDER_CASCADE") "openrouter,openai")
|
||||
|
||||
(handler-case
|
||||
(setf *socket* (usocket:socket-connect *daemon-host* *daemon-port*))
|
||||
(error (e) (format t "Offline: ~a~%" e) (return-from main)))
|
||||
(setf *stream* (usocket:socket-stream *socket*))
|
||||
|
||||
;; Guard: Croatoan needs a real terminal (TERM env var, real TTY)
|
||||
(unless (uiop:getenv "TERM")
|
||||
(format t "TUI requires a terminal. Set TERM environment variable.~%")
|
||||
(format t "Or use: echo 'your message' | nc localhost 9105~%")
|
||||
(return-from main))
|
||||
|
||||
(unwind-protect
|
||||
(handler-case
|
||||
(with-screen (scr :input-echoing nil :input-blocking nil :enable-colors t)
|
||||
(let* ((h (height scr)) (w (width scr)))
|
||||
(let ((chat-win (make-instance 'window :height (- h 5) :width (- w 2) :position '(1 1) :border t))
|
||||
(input-win (make-instance 'window :height 1 :width (- w 2) :position (list (- h 2) 1) :border t)))
|
||||
(let* ((h (or (height scr) 24))
|
||||
(w (or (width scr) 80))
|
||||
(chat-h (- h 4))
|
||||
(input-y (- h 2)))
|
||||
(let ((chat-win (make-instance 'window :height chat-h :width (- w 2) :y 1 :x 1))
|
||||
(input-win (make-instance 'window :height 1 :width (- w 2) :y input-y :x 1)))
|
||||
(setf (input-blocking input-win) nil)
|
||||
(start-background-reader *stream*)
|
||||
(loop :while *is-running* :do
|
||||
(let ((msgs (dequeue-msgs)))
|
||||
(when msgs
|
||||
(dolist (m msgs) (push m *chat-history*))
|
||||
(render-chat chat-win)))
|
||||
(render-chat chat-win chat-h)))
|
||||
(let* ((ev (get-event input-win))
|
||||
(ch (when (and ev (typep ev 'event)) (event-key ev))))
|
||||
(when ch
|
||||
@@ -141,8 +130,8 @@
|
||||
(clear input-win)
|
||||
(add-string input-win (format nil "▶ ~a" (coerce *input-buffer* 'string)) :y 0 :x 1)
|
||||
(refresh input-win))
|
||||
(sleep 0.02)))))
|
||||
(sleep 0.01)))))
|
||||
(error (c)
|
||||
(format t "TUI Error: ~a~%" c)))
|
||||
(setf *is-running* nil)
|
||||
(when *socket* (ignore-errors (usocket:socket-close *socket*))))
|
||||
(when *socket* (ignore-errors (usocket:socket-close *socket*)))))
|
||||
|
||||
124
harness/tui-client.lisp.tmp
Normal file
124
harness/tui-client.lisp.tmp
Normal file
@@ -0,0 +1,124 @@
|
||||
|
||||
|
||||
(defpackage :opencortex-tui-tests
|
||||
(:use :cl :opencortex)
|
||||
(:export #:tui-suite))
|
||||
|
||||
(in-package :opencortex-tui-tests)
|
||||
|
||||
(eval-when (:compile-toplevel :load-toplevel :execute)
|
||||
(ql:quickload :fiveam :silent t))
|
||||
|
||||
(fiveam:def-suite tui-suite :description "Verification of the TUI parsing and styling logic")
|
||||
(fiveam:in-suite tui-suite)
|
||||
|
||||
(fiveam:test test-tui-connection-drop
|
||||
"Tier 2 Chaos: Verify that handle-return degrades gracefully when the daemon connection is lost."
|
||||
;; Create a closed stream to simulate connection drop
|
||||
(mock-stream (make-string-output-stream)))
|
||||
(close mock-stream)
|
||||
(opencortex.tui::handle-return mock-stream)
|
||||
;; Check if the error was enqueued to history instead of crashing
|
||||
|
||||
|
||||
(in-package :cl-user)
|
||||
(defpackage :opencortex.tui
|
||||
(:use :cl :croatoan :usocket :bordeaux-threads)
|
||||
(:export :main))
|
||||
(in-package :opencortex.tui)
|
||||
|
||||
|
||||
(defun enqueue-msg (msg)
|
||||
"Thread-safe addition to incoming message queue."
|
||||
|
||||
(defun dequeue-msgs ()
|
||||
"Thread-safe retrieval of incoming messages."
|
||||
msgs)))
|
||||
|
||||
(defun get-line-style (text)
|
||||
(cond
|
||||
((uiop:string-prefix-p "⬆" text) '(:cyan))
|
||||
((uiop:string-prefix-p "🤔" text) '(:italic))
|
||||
((uiop:string-prefix-p "ERROR" text) '(:bold :red))
|
||||
(t nil)))
|
||||
|
||||
(defun render-chat (win)
|
||||
(clear win)
|
||||
(view-height (max 0 (- h 2)))
|
||||
(end-idx (min history-len (+ start-idx view-height)))
|
||||
(loop for msg in slice
|
||||
for i from 1
|
||||
do (add-string win (format nil "│ ~a" msg) :y i :x 1 :attributes (get-line-style msg)))
|
||||
(refresh win)))
|
||||
|
||||
(defun handle-backspace ()
|
||||
|
||||
(defun handle-return (stream)
|
||||
(when (> (length cmd) 0)
|
||||
(enqueue-msg (format nil "⬆ ~a" cmd))
|
||||
(handler-case
|
||||
(progn
|
||||
(when (and stream (open-stream-p stream))
|
||||
:META (list :SOURCE :tui)
|
||||
:PAYLOAD (list :SENSOR :user-input :TEXT cmd)))
|
||||
(payload (format nil "~s" msg))
|
||||
(len (length payload)))
|
||||
(format stream "~6,'0x~a" len payload)
|
||||
(finish-output stream)))
|
||||
(enqueue-msg "✓ Sent"))
|
||||
(error (c)
|
||||
(format t "Send error: ~a~%" c)
|
||||
(enqueue-msg "ERROR: Connection to daemon lost.")
|
||||
|
||||
(defun start-background-reader (stream)
|
||||
"Starts a thread that reads framed messages from the daemon stream."
|
||||
(bt:make-thread
|
||||
(lambda ()
|
||||
(handler-case
|
||||
(count (read-sequence len-buf stream)))
|
||||
(when (= count 6)
|
||||
(msg-buf (make-string msg-len)))
|
||||
(read-sequence msg-buf stream)
|
||||
(let ((msg (read-from-string msg-buf)))
|
||||
(let ((payload (getf msg :payload)))
|
||||
(cond
|
||||
((eq (getf payload :action) :handshake)
|
||||
((and (eq (getf payload :sensor) :loop-error)
|
||||
(not (string= (or (getf payload :message) "") "Neural Cascade Failure: All providers exhausted.")))
|
||||
(enqueue-msg (format nil "ERROR: Daemon loop error (~a)"
|
||||
(getf payload :message))))
|
||||
(t
|
||||
(let ((text (or (getf payload :text) (format nil "~a" payload))))
|
||||
(enqueue-msg (format nil "⬇ ~a" text)))))))))
|
||||
(error (c)
|
||||
(enqueue-msg (format nil "ERROR: Connection lost (~a)" c))
|
||||
:name "opencortex-tui-reader")))
|
||||
|
||||
(defun main ()
|
||||
(handler-case
|
||||
(error (e) (format t "Offline: ~a~%" e) (return-from main)))
|
||||
|
||||
;; Guard: Croatoan needs a real terminal (TERM env var, real TTY)
|
||||
(unless (uiop:getenv "TERM")
|
||||
(format t "TUI requires a terminal. Set TERM environment variable.~%")
|
||||
(format t "Or use: echo 'your message' | nc localhost 9105~%")
|
||||
(return-from main))
|
||||
|
||||
(unwind-protect
|
||||
(handler-case
|
||||
(with-screen (scr :input-echoing nil :input-blocking nil :enable-colors t)
|
||||
(let ((chat-win (make-instance 'window :height (- h 5) :width (- w 2) :position '(1 1) :border t))
|
||||
(input-win (make-instance 'window :height 1 :width (- w 2) :position (list (- h 2) 1) :border t)))
|
||||
(setf (input-blocking input-win) nil)
|
||||
(let ((msgs (dequeue-msgs)))
|
||||
(when msgs
|
||||
(render-chat chat-win)))
|
||||
(ch (when (and ev (typep ev 'event)) (event-key ev))))
|
||||
(when ch
|
||||
(cond
|
||||
((or (eq ch :backspace) (eq ch (code-char 127))) (handle-backspace))
|
||||
(clear input-win)
|
||||
(refresh input-win))
|
||||
(sleep 0.02)))))
|
||||
(error (c)
|
||||
(format t "TUI Error: ~a~%" c)))
|
||||
@@ -6,32 +6,6 @@
|
||||
* Overview
|
||||
The OpenCortex TUI Client is a standalone Common Lisp application built on **Croatoan**.
|
||||
|
||||
* Test Suite
|
||||
#+begin_src lisp :tangle ../tests/tui-tests.lisp
|
||||
(defpackage :opencortex-tui-tests
|
||||
(:use :cl :opencortex)
|
||||
(:export #:tui-suite))
|
||||
|
||||
(in-package :opencortex-tui-tests)
|
||||
|
||||
(eval-when (:compile-toplevel :load-toplevel :execute)
|
||||
(ql:quickload :fiveam :silent t))
|
||||
|
||||
(fiveam:def-suite tui-suite :description "Verification of the TUI parsing and styling logic")
|
||||
(fiveam:in-suite tui-suite)
|
||||
|
||||
(fiveam:test test-tui-connection-drop
|
||||
"Tier 2 Chaos: Verify that handle-return degrades gracefully when the daemon connection is lost."
|
||||
(let ((opencortex.tui::*incoming-msgs* nil)
|
||||
(opencortex.tui::*input-buffer* (make-array 5 :element-type 'character :initial-contents "hello" :fill-pointer 5 :adjustable t))
|
||||
;; Create a closed stream to simulate connection drop
|
||||
(mock-stream (make-string-output-stream)))
|
||||
(close mock-stream)
|
||||
(opencortex.tui::handle-return mock-stream)
|
||||
;; Check if the error was enqueued to history instead of crashing
|
||||
(fiveam:is (member "ERROR: Connection to daemon lost." opencortex.tui::*incoming-msgs* :test #'string=))))
|
||||
#+end_src
|
||||
|
||||
* Implementation
|
||||
|
||||
** Package Context
|
||||
@@ -70,29 +44,22 @@ The OpenCortex TUI Client is a standalone Common Lisp application built on **Cro
|
||||
(let ((msgs *incoming-msgs*))
|
||||
(setf *incoming-msgs* nil)
|
||||
msgs)))
|
||||
|
||||
(defun get-line-style (text)
|
||||
(cond
|
||||
((uiop:string-prefix-p "*" text) '(:bold :yellow))
|
||||
((uiop:string-prefix-p "⬆" text) '(:cyan))
|
||||
((uiop:string-prefix-p "🤔" text) '(:italic))
|
||||
((uiop:string-prefix-p "ERROR" text) '(:bold :red))
|
||||
(t nil)))
|
||||
#+end_src
|
||||
|
||||
** Rendering
|
||||
#+begin_src lisp
|
||||
(defun render-chat (win)
|
||||
(clear win)
|
||||
(let* ((h (height win))
|
||||
(view-height (max 0 (- h 2)))
|
||||
(history-len (length *chat-history*))
|
||||
(start-idx *scroll-index*)
|
||||
(end-idx (min history-len (+ start-idx view-height)))
|
||||
(slice (reverse (subseq *chat-history* start-idx end-idx))))
|
||||
(loop for msg in slice
|
||||
for i from 1
|
||||
do (add-string win (format nil "│ ~a" msg) :y i :x 1 :attributes (get-line-style msg)))
|
||||
(defun render-chat (win h)
|
||||
(when (and win (integerp h))
|
||||
(clear win)
|
||||
(box win 0 0)
|
||||
(let* ((view-height (- h 2))
|
||||
(history (reverse *chat-history*))
|
||||
(len (length history))
|
||||
(num-to-draw (min len view-height)))
|
||||
(loop for i from 0 below num-to-draw
|
||||
for msg in history
|
||||
do (when (and msg (< (1+ i) (1- h)))
|
||||
(add-string win (format nil "~a" msg) :y (1+ i) :x 2))))
|
||||
(refresh win)))
|
||||
#+end_src
|
||||
|
||||
@@ -119,17 +86,15 @@ The OpenCortex TUI Client is a standalone Common Lisp application built on **Cro
|
||||
(finish-output stream)))
|
||||
(enqueue-msg "✓ Sent"))
|
||||
(error (c)
|
||||
(format t "Send error: ~a~%" c)
|
||||
(enqueue-msg "ERROR: Connection to daemon lost.")
|
||||
(setf *is-running* nil))))
|
||||
(when (string= cmd "/exit") (setf *is-running* nil))
|
||||
(when (string= cmd "/clear") (setf *chat-history* nil))))
|
||||
(enqueue-msg (format nil "ERROR: Connection lost (~a)" c))
|
||||
(setf *is-running* nil))))
|
||||
(when (string= cmd "/exit") (setf *is-running* nil))
|
||||
(when (string= cmd "/clear") (setf *chat-history* nil))))
|
||||
#+end_src
|
||||
|
||||
** Background Reader
|
||||
#+begin_src lisp
|
||||
(defun start-background-reader (stream)
|
||||
"Starts a thread that reads framed messages from the daemon stream."
|
||||
(bt:make-thread
|
||||
(lambda ()
|
||||
(loop while *is-running* do
|
||||
@@ -145,10 +110,6 @@ The OpenCortex TUI Client is a standalone Common Lisp application built on **Cro
|
||||
(cond
|
||||
((eq (getf payload :action) :handshake)
|
||||
(enqueue-msg "* Connected to daemon *"))
|
||||
((and (eq (getf payload :sensor) :loop-error)
|
||||
(not (string= (or (getf payload :message) "") "Neural Cascade Failure: All providers exhausted.")))
|
||||
(enqueue-msg (format nil "ERROR: Daemon loop error (~a)"
|
||||
(getf payload :message))))
|
||||
(t
|
||||
(let ((text (or (getf payload :text) (format nil "~a" payload))))
|
||||
(enqueue-msg (format nil "⬇ ~a" text))))))))))
|
||||
@@ -156,37 +117,39 @@ The OpenCortex TUI Client is a standalone Common Lisp application built on **Cro
|
||||
(when *is-running*
|
||||
(enqueue-msg (format nil "ERROR: Connection lost (~a)" c))
|
||||
(setf *is-running* nil))))))
|
||||
:name "opencortex-tui-reader")))
|
||||
)
|
||||
:name "opencortex-tui-reader"))
|
||||
#+end_src
|
||||
|
||||
** Main Entry Point
|
||||
#+begin_src lisp
|
||||
(defun main ()
|
||||
(setf (uiop:getenv "PROVIDER_CASCADE") "openrouter,openai")
|
||||
|
||||
(handler-case
|
||||
(setf *socket* (usocket:socket-connect *daemon-host* *daemon-port*))
|
||||
(error (e) (format t "Offline: ~a~%" e) (return-from main)))
|
||||
(setf *stream* (usocket:socket-stream *socket*))
|
||||
|
||||
;; Guard: Croatoan needs a real terminal (TERM env var, real TTY)
|
||||
(unless (uiop:getenv "TERM")
|
||||
(format t "TUI requires a terminal. Set TERM environment variable.~%")
|
||||
(format t "Or use: echo 'your message' | nc localhost 9105~%")
|
||||
(return-from main))
|
||||
|
||||
(unwind-protect
|
||||
(handler-case
|
||||
(with-screen (scr :input-echoing nil :input-blocking nil :enable-colors t)
|
||||
(let* ((h (height scr)) (w (width scr)))
|
||||
(let ((chat-win (make-instance 'window :height (- h 5) :width (- w 2) :position '(1 1) :border t))
|
||||
(input-win (make-instance 'window :height 1 :width (- w 2) :position (list (- h 2) 1) :border t)))
|
||||
(let* ((h (or (height scr) 24))
|
||||
(w (or (width scr) 80))
|
||||
(chat-h (- h 4))
|
||||
(input-y (- h 2)))
|
||||
(let ((chat-win (make-instance 'window :height chat-h :width (- w 2) :y 1 :x 1))
|
||||
(input-win (make-instance 'window :height 1 :width (- w 2) :y input-y :x 1)))
|
||||
(setf (input-blocking input-win) nil)
|
||||
(start-background-reader *stream*)
|
||||
(loop :while *is-running* :do
|
||||
(let ((msgs (dequeue-msgs)))
|
||||
(when msgs
|
||||
(dolist (m msgs) (push m *chat-history*))
|
||||
(render-chat chat-win)))
|
||||
(render-chat chat-win chat-h)))
|
||||
(let* ((ev (get-event input-win))
|
||||
(ch (when (and ev (typep ev 'event)) (event-key ev))))
|
||||
(when ch
|
||||
@@ -197,9 +160,9 @@ The OpenCortex TUI Client is a standalone Common Lisp application built on **Cro
|
||||
(clear input-win)
|
||||
(add-string input-win (format nil "▶ ~a" (coerce *input-buffer* 'string)) :y 0 :x 1)
|
||||
(refresh input-win))
|
||||
(sleep 0.02)))))
|
||||
(sleep 0.01)))))
|
||||
(error (c)
|
||||
(format t "TUI Error: ~a~%" c)))
|
||||
(setf *is-running* nil)
|
||||
(when *socket* (ignore-errors (usocket:socket-close *socket*))))
|
||||
(when *socket* (ignore-errors (usocket:socket-close *socket*)))))
|
||||
#+end_src
|
||||
|
||||
Reference in New Issue
Block a user