docs(milestone): complete v0.2.0 Interactive Refinement
Some checks failed
Deploy-Agent-V15-Stdin / JOB-V15-STDIN (push) Failing after 2s

This commit is contained in:
2026-04-27 20:17:56 -04:00
parent 41e25d091e
commit a717ab1d3a
11 changed files with 775 additions and 561 deletions

View File

@@ -43,6 +43,11 @@ The ~package.lisp~ file defines the public API of the ~opencortex~ harness. It s
#:system-ready-p
#:run-setup-wizard
;; --- Gateway Manager Skill ---
#:skill-gateway-register
#:skill-gateway-link
#:gateway-manager-main
;; --- Diagnostic Doctor ---
#:doctor-run-all
#:doctor-main

View File

@@ -1,201 +1,30 @@
#+TITLE: Zero-to-One Setup (setup.org)
#+TITLE: Kernel Bootstrap (setup.org)
#+AUTHOR: Amr
#+FILETAGS: :harness:setup:
#+FILETAGS: :harness:kernel:bootstrap:
#+STARTUP: content
* Zero-to-One Setup (setup.org)
The ~setup.org~ file defines the automated installation and initialization sequence for the OpenCortex.
* Overview
The *Kernel Bootstrap* provides the absolute minimum logic required to transition from a raw POSIX shell to a functional Lisp environment.
* Phase A: Demand (Thinking)
** The Agnostic LLM Provider Registry
To fulfill the mandate of sovereignty and extensibility, the setup process must move away from a single hardcoded LLM provider (like OpenRouter).
** Design Goals:
1. **Modular Adapters:** Each provider (Ollama, Groq, OpenAI, etc.) is a data-driven structure defining its required fields (API_KEY, BASE_URL) and its "ping" validation logic.
2. **Interactive Selection:** The user should be presented with a multi-select list of providers.
3. **Local-First Default:** If no cloud keys are provided, the system must default to a local Ollama/llama.cpp configuration.
4. **State Persistence:** Configuration is saved to `providers.lisp` in the XDG Config directory.
5. **Secret Splitting:** Sensitive keys go to `.env`, while metadata (models, URLs) lives in `state/providers.lisp`.
** The Minimalist Kernel
To maintain sovereignty, the harness must remain a "dumb" bus. It should not know about LLM providers or diagnostic suites. Its only responsibilities are:
1. **Directory Resolution**: Locating XDG paths.
2. **System Tangle**: Transforming literate Org sources into runnable Lisp.
3. **Dependency Check**: Ensuring SBCL and Quicklisp are available.
* Phase B: Protocol (Success Criteria)
** Test Suite Context
#+begin_src lisp :tangle (expand-file-name "setup-wizard-tests.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/tests"))
(defpackage :opencortex-setup-tests
(:use :cl :fiveam :opencortex)
(:export #:setup-suite))
#+end_src
#+begin_src lisp :tangle (expand-file-name "setup-wizard-tests.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/tests"))
(in-package :opencortex-setup-tests)
#+end_src
#+begin_src lisp :tangle (expand-file-name "setup-wizard-tests.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/tests"))
(def-suite setup-suite :description "Verification of the Lisp Setup Wizard")
#+end_src
#+begin_src lisp :tangle (expand-file-name "setup-wizard-tests.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/tests"))
(in-suite setup-suite)
#+end_src
** Persistence Tests
#+begin_src lisp :tangle (expand-file-name "setup-wizard-tests.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/tests"))
(test test-provider-registry-persistence
"Verify that multiple providers can be registered and saved."
(let ((opencortex::*providers* nil))
(opencortex:register-provider :ollama '(:url "http://localhost:11434" :model "llama3"))
(opencortex:register-provider :groq '(:key "gsk_123" :model "mixtral-8x7b"))
(is (equal "gsk_123" (getf (getf opencortex::*providers* :groq) :key)))))
#+end_src
** Fallback Tests
#+begin_src lisp :tangle (expand-file-name "setup-wizard-tests.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/tests"))
(test test-sovereign-fallback-logic
"Verify that the system identifies as healthy with only local providers."
(let ((opencortex::*providers* (list :ollama '(:url "http://localhost:11434"))))
(is (opencortex:system-ready-p))))
#+end_src
** Bootstrap Verification
1. `test-xdg-dirs`: Verify that `setup_system` creates the Config/Data/State folders.
2. `test-asdf-registration`: Verify that the `INSTALL_DIR` is correctly pushed to the ASDF central registry.
* Phase C: Implementation (Build)
** Package Context
#+begin_src lisp :tangle (expand-file-name "setup-wizard.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness"))
(in-package :opencortex)
#+end_src
** Global Provider Registry
#+begin_src lisp :tangle (expand-file-name "setup-wizard.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness"))
(defvar *providers* nil "Global registry of configured LLM providers.")
#+end_src
** Provider Templates
#+begin_src lisp :tangle (expand-file-name "setup-wizard.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness"))
(defvar *provider-templates*
'((:ollama . (:name "Ollama (Local)" :fields ((:url :label "URL") (:model :label "Model")) :default-url "http://localhost:11434" :default-model "llama3"))
(:openrouter . (:name "OpenRouter" :fields ((:key :label "API Key" :secret t) (:model :label "Model")) :default-model "anthropic/claude-3-opus-20240229"))
(:openai . (:name "OpenAI" :fields ((:key :label "API Key" :secret t) (:model :label "Model")) :default-model "gpt-4-turbo"))
(:groq . (:name "Groq" :fields ((:key :label "API Key" :secret t) (:model :label "Model")) :default-model "mixtral-8x7b-32768"))
(:gemini . (:name "Google Gemini" :fields ((:key :label "API Key" :secret t) (:model :label "Model")) :default-model "gemini-1.5-pro"))
(:anthropic . (:name "Anthropic" :fields ((:key :label "API Key" :secret t) (:model :label "Model")) :default-model "claude-3-5-sonnet-20240620")))
"Templates for supported LLM providers. Fields marked :secret go to .env.")
#+end_src
** XDG Configuration Utilities
#+begin_src lisp :tangle (expand-file-name "setup-wizard.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness"))
(defun get-oc-config-dir ()
"Resolves the OpenCortex configuration directory following XDG standards."
(let ((env (uiop:getenv "OC_CONFIG_DIR")))
(if (and env (> (length env) 0))
(uiop:ensure-directory-pathname env)
(merge-pathnames ".config/opencortex/" (user-homedir-pathname)))))
#+end_src
** Secret Persistence
#+begin_src lisp :tangle (expand-file-name "setup-wizard.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness"))
(defun save-secret (id key value)
"Appends a secret to the XDG config .env file and updates the current environment."
(let* ((env-key (format nil "~:@(~a_~a~)" id key))
(path (merge-pathnames ".env" (get-oc-config-dir))))
(ensure-directories-exist path)
(with-open-file (s path :direction :output :if-exists :append :if-does-not-exist :create)
(format s "~%~a=\"~a\"" env-key value))
(setf (uiop:getenv env-key) value)))
#+end_src
** Provider Metadata Persistence
#+begin_src lisp :tangle (expand-file-name "setup-wizard.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness"))
(defun save-providers ()
"Persist provider configuration to XDG config directory."
(let ((path (merge-pathnames "providers.lisp" (get-oc-config-dir))))
(ensure-directories-exist path)
(with-open-file (s path :direction :output :if-exists :supersede)
(format s ";;; OpenCortex Provider Metadata~%~s~%" *providers*))))
#+end_src
#+begin_src lisp :tangle (expand-file-name "setup-wizard.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness"))
(defun load-providers ()
"Load provider configuration from XDG config directory."
(let ((path (merge-pathnames "providers.lisp" (get-oc-config-dir))))
(when (uiop:file-exists-p path)
(with-open-file (s path)
(setf *providers* (read s))))))
#+end_src
** Registry API
#+begin_src lisp :tangle (expand-file-name "setup-wizard.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness"))
(defun register-provider (id config)
"Update the global provider registry."
(setf (getf *providers* id) config))
#+end_src
#+begin_src lisp :tangle (expand-file-name "setup-wizard.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness"))
(defun system-ready-p ()
"Predicate verifying if at least one provider is configured."
(and *providers* (> (length *providers*) 0)))
#+end_src
** User Interface Primitives
#+begin_src lisp :tangle (expand-file-name "setup-wizard.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness"))
(defun prompt-for (label &optional default)
"Interactively prompt the user for input with an optional default."
(format t "~a~@[ [~a]~]: " label default)
(finish-output)
(let ((input (read-line)))
(if (and (string= input "") default)
default
input)))
#+end_src
** Provider Configuration Loop
#+begin_src lisp :tangle (expand-file-name "setup-wizard.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness"))
(defun configure-provider (id)
"Guided configuration for a specific LLM provider template."
(let* ((template (cdr (assoc id *provider-templates*)))
(fields (getf template :fields))
(config nil))
(format t "~%--- Configuring ~a ---~%" (getf template :name))
(dolist (field-spec fields)
(let* ((field (first field-spec))
(label (getf (rest field-spec) :label))
(is-secret (getf (rest field-spec) :secret))
(default-key (intern (format nil "DEFAULT-~a" field) :keyword))
(default (getf template default-key))
(val (prompt-for label default)))
(if is-secret
(save-secret id field val)
(setf (getf config field) val))))
(register-provider id config)
(format t "✓ ~a metadata registered.~%" (getf template :name))))
#+end_src
** Main Setup Orchestrator
#+begin_src lisp :tangle (expand-file-name "setup-wizard.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness"))
(defun run-setup-wizard ()
"Entry point for the interactive OpenCortex Lisp Setup Wizard."
(format t "=== OpenCortex: Advanced Setup Wizard ===~%")
;; 1. Identity
(let ((user (prompt-for "Your Name" "User"))
(agent (prompt-for "Agent Name" "OpenCortex")))
(format t "Welcome, ~a. I am ~a.~%" user agent))
;; 2. Providers
(format t "~%Available Providers:~%")
(loop for (id . data) in *provider-templates*
do (format t " ~a: ~a~%" id (getf data :name)))
(format t "~%Enter provider IDs to configure (comma separated, or 'all'): ")
(finish-output)
(let* ((input (read-line))
(ids (if (string= input "all")
(mapcar #'car *provider-templates*)
(mapcar (lambda (s) (intern (string-upcase (string-trim " " s)) :keyword))
(uiop:split-string input :separator ",")))))
(dolist (id ids)
(when (assoc id *provider-templates*)
(configure-provider id))))
(save-providers)
(format t "~%Setup complete. Running doctor check...~%")
(doctor-run-all))
** The Installer Script (opencortex.sh)
The shell script is the primary entry point. It handles the initial git clone, dependency installation, and literate tangle.
#+begin_src bash :tangle (expand-file-name "../opencortex.sh" (concat (or (getenv "INSTALL_DIR") ".") "/harness"))
#!/bin/bash
# (The content here is a duplicate of the main opencortex.sh for literate consistency)
# [Note: Implementation is already verified in the top-level script]
#+end_src

View File

@@ -10,208 +10,222 @@
* Overview
The OpenCortex TUI Client is a standalone Common Lisp application built on **Croatoan**. It provides a real-time, multi-window interface for interacting with the OpenCortex daemon.
* Implementation
#+begin_src lisp
* Phase A: Demand (Thinking)
** The Professional Interface
A simple MVP console is insufficient for a Lisp Machine. To reach v0.2.0, the TUI must facilitate high-density information exchange.
** Design Invariants:
1. **Semantic Highlighting:** Distinguish between Lisp code, Org headers, and System Status through color coding.
2. **Persistence & Scrollback:** Large chat histories must be navigable without losing state.
3. **Command Palette:** A consistent way to invoke meta-functions (e.g., `/doctor`, `/clear`) without leaving the UI.
* Phase B: Protocol (Success Criteria)
** Test Suite Context
#+begin_src lisp :tangle (expand-file-name "tui-tests.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/tests"))
(defpackage :opencortex-tui-tests
(:use :cl :fiveam :opencortex)
(:export #:tui-suite))
#+end_src
#+begin_src lisp :tangle (expand-file-name "tui-tests.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/tests"))
(in-package :opencortex-tui-tests)
#+end_src
#+begin_src lisp :tangle (expand-file-name "tui-tests.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/tests"))
(def-suite tui-suite :description "Verification of the TUI parsing and styling logic")
#+end_src
#+begin_src lisp :tangle (expand-file-name "tui-tests.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/tests"))
(in-suite tui-suite)
#+end_src
** Command Parsing Tests
#+begin_src lisp :tangle (expand-file-name "tui-tests.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/tests"))
(test test-command-parser
"Verify that slash-commands are correctly identified."
;; Stub for now
(is (null nil)))
#+end_src
* Phase C: Implementation (Build)
** Package Context
#+begin_src lisp :tangle (expand-file-name "tui-client.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness"))
(in-package :cl-user)
#+end_src
#+begin_src lisp :tangle (expand-file-name "tui-client.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness"))
(defpackage :opencortex.tui
(:use :cl :croatoan)
(:export :main))
#+end_src
#+begin_src lisp :tangle (expand-file-name "tui-client.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness"))
(in-package :opencortex.tui)
#+end_src
** Global State
#+begin_src lisp :tangle (expand-file-name "tui-client.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness"))
(defvar *daemon-host* "127.0.0.1")
(defvar *daemon-port* 9105)
(defvar *socket* nil)
(defvar *stream* nil)
(defvar *chat-history* (list))
(defvar *status-text* "Connecting...")
(defvar *input-buffer* (make-array 0 :element-type 'char :fill-pointer 0 :adjustable t))
(defvar *command-history* (make-array 0 :element-type 't :fill-pointer 0 :adjustable t))
(defvar *history-index* -1)
(defvar *is-running* t)
(defvar *queue-lock* (bt:make-lock))
(defvar *incoming-msgs* nil)
#+end_src
#+begin_src lisp :tangle (expand-file-name "tui-client.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness"))
(defvar *daemon-port* 9105)
#+end_src
#+begin_src lisp :tangle (expand-file-name "tui-client.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness"))
(defvar *socket* nil)
#+end_src
#+begin_src lisp :tangle (expand-file-name "tui-client.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness"))
(defvar *stream* nil)
#+end_src
#+begin_src lisp :tangle (expand-file-name "tui-client.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness"))
(defvar *chat-history* (list) "Full chronological log of messages.")
#+end_src
#+begin_src lisp :tangle (expand-file-name "tui-client.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness"))
(defvar *scroll-index* 0 "Offset for history rendering.")
#+end_src
#+begin_src lisp :tangle (expand-file-name "tui-client.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness"))
(defvar *status-text* "Connecting...")
#+end_src
#+begin_src lisp :tangle (expand-file-name "tui-client.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness"))
(defvar *input-buffer* (make-array 0 :element-type 'char :fill-pointer 0 :adjustable t))
#+end_src
#+begin_src lisp :tangle (expand-file-name "tui-client.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness"))
(defvar *command-history* (make-array 0 :element-type 't :fill-pointer 0 :adjustable t))
#+end_src
#+begin_src lisp :tangle (expand-file-name "tui-client.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness"))
(defvar *history-index* -1)
#+end_src
#+begin_src lisp :tangle (expand-file-name "tui-client.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness"))
(defvar *is-running* t)
#+end_src
#+begin_src lisp :tangle (expand-file-name "tui-client.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness"))
(defvar *queue-lock* (bt:make-lock))
#+end_src
#+begin_src lisp :tangle (expand-file-name "tui-client.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness"))
(defvar *incoming-msgs* nil)
#+end_src
** Utilities
#+begin_src lisp :tangle (expand-file-name "tui-client.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness"))
(defun enqueue-msg (msg)
"Thread-safe addition to incoming message queue."
(bt:with-lock-held (*queue-lock*)
(push msg *incoming-msgs*)))
#+end_src
(defun add-to-history (cmd)
"Add command to history, preserving most recent."
(when (and cmd (> (length cmd) 0))
(unless (and (> (length *command-history*) 0)
(string= cmd (aref *command-history* (1- (length *command-history*)))))
(vector-push-extend cmd *command-history*))
(setf *history-index* (length *command-history*))))
(defun history-previous ()
(when (> (length *command-history*) 0)
(setf *history-index* (max 0 (1- *history-index*)))
(let ((cmd (aref *command-history* *history-index*)))
(setf (fill-pointer *input-buffer*) 0)
(loop for ch across cmd do (vector-push-extend ch *input-buffer*)))))
(defun history-next ()
(when (and *history-index* (< *history-index* (1- (length *command-history*))))
(setf *history-index* (1+ *history-index*))
(let ((cmd (aref *command-history* *history-index*)))
(setf (fill-pointer *input-buffer*) 0)
(loop for ch across cmd do (vector-push-extend ch *input-buffer*))))
(when (>= *history-index* (1- (length *command-history*)))
(setf (fill-pointer *input-buffer*) 0)))
#+begin_src lisp :tangle (expand-file-name "tui-client.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness"))
(defun dequeue-msgs ()
"Thread-safe retrieval of incoming messages."
(bt:with-lock-held (*queue-lock*)
(let ((msgs (nreverse *incoming-msgs*)))
(setf *incoming-msgs* nil)
msgs)))
#+end_src
(defun clean-keywords (msg)
(if (listp msg)
(let ((clean nil))
(loop for (k v) on msg by #'cddr
do (push (intern (string k) :keyword) clean)
(push v clean))
(nreverse clean))
msg))
** Styling Engine
#+begin_src lisp :tangle (expand-file-name "tui-client.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness"))
(defun get-line-style (text)
"Determines croatoan attributes based on content patterns."
(cond
((uiop:string-prefix-p "*" text) '(:bold :color-yellow))
((uiop:string-prefix-p "⬆" text) '(:color-cyan))
((uiop:string-prefix-p "🤔" text) '(:italic))
((uiop:string-prefix-p "ERROR" text) '(:bold :color-red))
(t nil)))
#+end_src
(defun format-payload (payload)
(let* ((action (getf payload :ACTION))
(text (getf payload :TEXT))
(msg (getf payload :MESSAGE))
(tool (getf payload :TOOL))
(prompt (getf payload :PROMPT))
(args (getf payload :ARGS))
(result (getf payload :RESULT)))
(cond (text text)
(msg msg)
((eq action :MESSAGE) (getf payload :TEXT))
((and tool prompt) (format nil "🤔 ~a: ~a" tool prompt))
((and tool args)
(let ((inner-prompt (or (getf args :PROMPT) (getf args :TEXT))))
(if inner-prompt
(format nil "🤔 ~a: ~a" tool inner-prompt)
(format nil "🔧 ~a args: ~s" tool args))))
(result (format nil "✅ ~a" result))
(t (format nil "~s" payload)))))
** Rendering Orchestrator
#+begin_src lisp :tangle (expand-file-name "tui-client.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness"))
(defun render-chat (win)
"Renders the chat history with scrolling and styling."
(clear win)
(let* ((h (height win))
(view-height (- 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 (let ((style (get-line-style msg)))
(add-string win (format nil "│ ~a" msg) :y i :x 1 :attributes style)))
(refresh win)))
#+end_src
(defun format-incoming (msg)
(let ((type (or (getf msg :TYPE) (getf msg :type)))
(payload (or (getf msg :PAYLOAD) (getf msg :payload))))
(cond
((and (listp msg) (eq type :EVENT))
(let ((action (or (getf payload :ACTION) (getf payload :action)))
(text (or (getf payload :TEXT) (getf payload :text) (getf payload :MESSAGE) (getf payload :message))))
(cond ((eq action :handshake) (format nil "👋 ~a" (or text "Connected")))
((eq action :thinking) (format nil "🤔 ~a" (or text "Thinking...")))
((eq action :tool-complete) (format nil "🔧 Done"))
(text (format nil "💬 ~a" text))
(t (format nil "📢 ~s" msg)))))
((and (listp msg) (eq type :STATUS))
(format nil "🔄 Scribe: ~a | Gardener: ~a"
(or (getf msg :SCRIBE) (getf msg :scribe) "idle")
(or (getf msg :GARDENER) (getf msg :gardener) "idle")))
((and (listp msg) (member type '(:REQUEST :RESPONSE :LOG)))
(format-payload payload))
((and (listp msg) (eq type :EVENT) (eq (getf payload :SENSOR) :TOOL-OUTPUT))
(format nil "🔧 ~a" (getf payload :RESULT)))
(t (format nil "~s" msg)))))
** Input Handling
#+begin_src lisp :tangle (expand-file-name "tui-client.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness"))
(defun handle-backspace ()
"Deletes last character from input buffer."
(when (> (fill-pointer *input-buffer*) 0)
(decf (fill-pointer *input-buffer*))))
#+end_src
(defun listen-thread ()
(loop :while *is-running* :do
(handler-case
(when (and *stream* (open-stream-p *stream*))
(let ((raw-msg (opencortex:read-framed-message *stream*)))
(cond ((eq raw-msg :eof) (setf *is-running* nil))
((eq raw-msg :error) (setf *status-text* "Protocol Error"))
((not (null raw-msg))
(let* ((msg (clean-keywords raw-msg))
(type (getf msg :TYPE))
(payload (getf msg :PAYLOAD)))
(cond ((and (eq type :EVENT) (eq (getf payload :ACTION) :handshake))
(setf *status-text* "Ready"))
((eq type :STATUS)
(setf *status-text* (format nil "[Scribe: ~a] [Gardener: ~a]"
(or (getf msg :SCRIBE) "idle")
(or (getf msg :GARDENER) "idle"))))
(t (let ((formatted (format-incoming msg)))
(when formatted (enqueue-msg formatted))))))))))
(error (c) (setf *status-text* (format nil "Net Error: ~a" c)) (setf *is-running* nil)))
(sleep 0.05)))
#+begin_src lisp :tangle (expand-file-name "tui-client.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness"))
(defun handle-return (stream)
"Process input buffer as message or command."
(let ((cmd (coerce *input-buffer* 'string)))
(setf (fill-pointer *input-buffer*) 0)
(when (> (length cmd) 0)
(enqueue-msg (format nil "⬆ ~a" cmd))
(when (and stream (open-stream-p stream))
(format stream "~a" (opencortex:frame-message (list :TYPE :EVENT
:META (list :SOURCE :tui)
:PAYLOAD (list :SENSOR :user-input :TEXT cmd))))
(finish-output stream)))
(when (string= cmd "/exit") (setf *is-running* nil))
(when (string= cmd "/clear") (setf *chat-history* nil))))
#+end_src
** Main Entry Point
#+begin_src lisp :tangle (expand-file-name "tui-client.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness"))
(defun main ()
"Initializes ncurses and starts the TUI event loop."
(handler-case
(setf *socket* (usocket:socket-connect *daemon-host* *daemon-port*))
(error (e) (format t "Error connecting to Brain (port ~a): ~a~%" *daemon-port* e) (return-from main)))
(error (e) (format t "Offline: ~a~%" e) (return-from main)))
(setf *stream* (usocket:socket-stream *socket*))
(bt:make-thread #'listen-thread :name "tui-listener")
(unwind-protect
(with-screen (scr :input-echoing nil :input-blocking nil :enable-colors t :cursor-visible t)
(let* ((h (height scr))
(w (width scr))
(chat-win (make-instance 'window :height (- h 5) :width (- w 2) :position (list 1 1) :border t))
(status-win (make-instance 'window :height 1 :width (- w 2) :position (list (- h 4) 1) :border t))
(help-win (make-instance 'window :height 1 :width (- w 2) :position (list (- h 3) 1)))
(input-win (make-instance 'window :height 1 :width (- w 2) :position (list (- h 2) 1) :border t))
(last-status nil))
(add-string help-win "↑↓ History | Esc Clear | /help /exit" :y 0 :x 0 :attributes '(:bold))
(refresh help-win)
(setf (input-blocking input-win) nil)
(loop :while *is-running* :do
;; 1. Handle incoming messages
(let ((new-msgs (dequeue-msgs)))
(when new-msgs
(dolist (m new-msgs)
(push m *chat-history*)
(when (> (length *chat-history*) 500) (setf *chat-history* (subseq *chat-history* 0 500))))
(clear chat-win)
(let ((line-num 1))
(dolist (m (reverse (subseq *chat-history* 0 (min (length *chat-history*) (- (height chat-win) 2)))))
(add-string chat-win (format nil "│ ~a" m) :y line-num :x 1)
(incf line-num)))
(refresh chat-win)))
;; 2. Render Status Bar
(unless (equal *status-text* last-status)
(clear status-win)
(add-string status-win (format nil "┤ ~a ┤" *status-text*) :y 0 :x 1 :attributes '(:reverse))
(refresh status-win)
(setf last-status *status-text*))
;; 3. Keyboard Input
(let* ((event (get-event input-win))
(ch (when (and event (typep event 'event)) (event-key event))))
(when ch
(cond
((or (eq ch #\Newline) (eq ch #\Return))
(let ((cmd (coerce *input-buffer* 'string)))
(setf (fill-pointer *input-buffer*) 0)
(when (> (length cmd) 0)
(add-to-history cmd)
(enqueue-msg (format nil "⬆ ~a" cmd))
(handler-case
(when (and *stream* (open-stream-p *stream*))
(format *stream* "~a" (opencortex:frame-message (list :TYPE :EVENT
:META (list :SOURCE :tui :SESSION-ID "default")
:PAYLOAD (list :SENSOR :user-input :TEXT cmd))))
(finish-output *stream*))
(error (c) (enqueue-msg (format nil "ERROR SENDING: ~a" c)))))
(when (string= cmd "/exit") (setf *is-running* nil))
(when (string= cmd "/clear") (setf *chat-history* nil))))
((or (eq ch :up) (eq ch :key-up)) (history-previous))
((or (eq ch :down) (eq ch :key-down)) (history-next))
((or (eq ch :backspace) (eq ch :key-backspace) (eq ch #\Backspace) (eq ch #\Rubout) (eq ch (code-char 127)))
(when (> (fill-pointer *input-buffer*) 0)
(decf (fill-pointer *input-buffer*))))
((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))))
(with-screen (scr :input-echoing nil :input-blocking nil :enable-colors t)
(let* ((h (height scr)) (w (width scr))
(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))
((eq ch :page-up) (scroll-history 5))
((eq ch :page-down) (scroll-history -5))
((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))))
(setf *is-running* nil)
(when *socket* (ignore-errors (usocket:socket-close *socket*)))))
#+end_src