diff --git a/harness/tui-client.lisp b/harness/tui-client.lisp index 8811dd6..0bca04f 100644 --- a/harness/tui-client.lisp +++ b/harness/tui-client.lisp @@ -75,33 +75,73 @@ (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 + (handler-case + (let* ((len-buf (make-string 6)) + (count (read-sequence len-buf stream))) + (when (= count 6) + (let* ((msg-len (parse-integer len-buf :radix 16)) + (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) + (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))))))))) + (error (c) + (when *is-running* + (enqueue-msg (format nil "ERROR: Connection lost (~a)" c)) + (setf *is-running* nil)))))) + :name "opencortex-tui-reader")) + (defun main () (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 - (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))) - (setf (input-blocking input-win) nil) - (loop :while *is-running* :do - (let ((msgs (dequeue-msgs))) - (when msgs - (dolist (m msgs) (push m *chat-history*)) - (render-chat chat-win))) - (let* ((ev (get-event input-win)) - (ch (when (and ev (typep ev 'event)) (event-key ev)))) - (when ch - (cond - ((or (eq ch #\Newline) (eq ch #\Return)) (handle-return *stream*)) - ((or (eq ch :backspace) (eq ch (code-char 127))) (handle-backspace)) - ((characterp ch) (vector-push-extend ch *input-buffer*)))) - (clear input-win) - (add-string input-win (format nil "▶ ~a" (coerce *input-buffer* 'string)) :y 0 :x 1) - (refresh input-win)) - (sleep 0.02))))) + (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))) + (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))) + (let* ((ev (get-event input-win)) + (ch (when (and ev (typep ev 'event)) (event-key ev)))) + (when ch + (cond + ((or (eq ch #\Newline) (eq ch #\Return)) (handle-return *stream*)) + ((or (eq ch :backspace) (eq ch (code-char 127))) (handle-backspace)) + ((characterp ch) (vector-push-extend ch *input-buffer*)))) + (clear input-win) + (add-string input-win (format nil "▶ ~a" (coerce *input-buffer* 'string)) :y 0 :x 1) + (refresh input-win)) + (sleep 0.02))))) + (error (c) + (format t "TUI Error: ~a~%" c))) (setf *is-running* nil) (when *socket* (ignore-errors (usocket:socket-close *socket*))))) diff --git a/harness/tui-client.org b/harness/tui-client.org index ae528b7..45163e7 100644 --- a/harness/tui-client.org +++ b/harness/tui-client.org @@ -126,6 +126,39 @@ The OpenCortex TUI Client is a standalone Common Lisp application built on **Cro (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 + (handler-case + (let* ((len-buf (make-string 6)) + (count (read-sequence len-buf stream))) + (when (= count 6) + (let* ((msg-len (parse-integer len-buf :radix 16)) + (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) + (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))))))))) + (error (c) + (when *is-running* + (enqueue-msg (format nil "ERROR: Connection lost (~a)" c)) + (setf *is-running* nil)))))) + :name "opencortex-tui-reader")) +#+end_src + ** Main Entry Point #+begin_src lisp (defun main () @@ -134,28 +167,38 @@ The OpenCortex TUI Client is a standalone Common Lisp application built on **Cro (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 - (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))) - (setf (input-blocking input-win) nil) - (loop :while *is-running* :do - (let ((msgs (dequeue-msgs))) - (when msgs - (dolist (m msgs) (push m *chat-history*)) - (render-chat chat-win))) - (let* ((ev (get-event input-win)) - (ch (when (and ev (typep ev 'event)) (event-key ev)))) - (when ch - (cond - ((or (eq ch #\Newline) (eq ch #\Return)) (handle-return *stream*)) - ((or (eq ch :backspace) (eq ch (code-char 127))) (handle-backspace)) - ((characterp ch) (vector-push-extend ch *input-buffer*)))) - (clear input-win) - (add-string input-win (format nil "▶ ~a" (coerce *input-buffer* 'string)) :y 0 :x 1) - (refresh input-win)) - (sleep 0.02))))) + (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))) + (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))) + (let* ((ev (get-event input-win)) + (ch (when (and ev (typep ev 'event)) (event-key ev)))) + (when ch + (cond + ((or (eq ch #\Newline) (eq ch #\Return)) (handle-return *stream*)) + ((or (eq ch :backspace) (eq ch (code-char 127))) (handle-backspace)) + ((characterp ch) (vector-push-extend ch *input-buffer*)))) + (clear input-win) + (add-string input-win (format nil "▶ ~a" (coerce *input-buffer* 'string)) :y 0 :x 1) + (refresh input-win)) + (sleep 0.02))))) + (error (c) + (format t "TUI Error: ~a~%" c))) (setf *is-running* nil) (when *socket* (ignore-errors (usocket:socket-close *socket*))))) #+end_src