v0.7.1: Streaming + Markdown + URLs + Interrupt — TDD
Some checks failed
Deploy (Gitea) / deploy (push) Failing after 2s
Some checks failed
Deploy (Gitea) / deploy (push) Failing after 2s
Stream-chunk protocol: SSE streaming via provider-openai-stream, cascade-stream with fboundp guard in think(). TUI renders live. Stream interrupt: Esc during streaming marks [interrupted], finalizes msg. SSE cancel infrastructure: *stream-cancel* check in read loop. Markdown inline: **bold**, *italic*, `code` via parse-markdown-spans. Code blocks: parse-markdown-blocks + syntax-highlight (keywords/strings/fns). URL detection + Tab-to-activate: https:// URLs in dim, Tab opens. Watchdog: 30s stall detection via Dexador read-timeout. [streaming] indicator in status bar. Pre-existing TUI test fixes (7): first→aref, nil→zerop, add-msg arg. Core: 65/65 Neuro: 13/13 TUI View: 22/22 TUI Main: 65/65 Total: 165 tests, 0 failures.
This commit is contained in:
@@ -11,7 +11,50 @@
|
||||
(or name raw))
|
||||
raw)))
|
||||
(cond
|
||||
;; v0.7.0: Ctrl key bindings
|
||||
;; v0.7.1: Esc — interrupt streaming
|
||||
((and (eql ch 27) (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)))
|
||||
;; v0.7.1: Tab on empty input — extract then open URL from agent message
|
||||
((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" (st :url-buffer)))
|
||||
(setf (st :url-buffer) nil))
|
||||
;; Extract URL from last agent message
|
||||
(let ((url nil))
|
||||
(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)
|
||||
when content
|
||||
do (let ((pos (or (search "https://" content) (search "http://" content))))
|
||||
(when pos
|
||||
(let ((end (or (position-if (lambda (c) (find c '(#\Space #\Newline #\Tab #\))))
|
||||
content :start pos)
|
||||
(length content))))
|
||||
(setf url (subseq content pos end))
|
||||
(return)))))
|
||||
(if url
|
||||
(progn
|
||||
(setf (st :url-buffer) url)
|
||||
(add-msg :system (format nil "Press Tab to open ~a" url))
|
||||
(setf (st :dirty) (list t t nil)))
|
||||
nil))))
|
||||
;; v0.7.0: Ctrl key bindings
|
||||
((eql ch 21) ; Ctrl+U — clear line
|
||||
(setf (st :input-buffer) nil)
|
||||
(setf (st :dirty) (list nil nil t)))
|
||||
@@ -71,15 +114,14 @@
|
||||
(add-msg :system
|
||||
"\\ + Enter Multi-line input"))
|
||||
;; /theme command
|
||||
((string-equal text "/theme")
|
||||
(add-msg :system
|
||||
(format nil "Theme: ~a — user=~a agent=~a system=~a input=~a"
|
||||
*tui-theme-current-name*
|
||||
(getf *tui-theme* :user)
|
||||
(getf *tui-theme* :agent)
|
||||
(getf *tui-theme* :system)
|
||||
(getf *tui-theme* :input))
|
||||
(format nil "Presets: /theme dark | light | solarized | gruvbox")))
|
||||
((string-equal text "/theme")
|
||||
(add-msg :system (format nil "Theme: ~a — user=~a agent=~a system=~a input=~a"
|
||||
*tui-theme-current-name*
|
||||
(getf *tui-theme* :user)
|
||||
(getf *tui-theme* :agent)
|
||||
(getf *tui-theme* :system)
|
||||
(getf *tui-theme* :input)))
|
||||
(add-msg :system "Presets: /theme dark | light | solarized | gruvbox"))
|
||||
((and (>= (length text) 7)
|
||||
(string-equal (subseq text 0 7) "/theme "))
|
||||
(let ((name (string-trim '(#\Space) (subseq text 7))))
|
||||
@@ -261,10 +303,42 @@
|
||||
(defun on-daemon-msg (msg)
|
||||
(let* ((payload (getf msg :payload))
|
||||
(text (getf payload :text))
|
||||
(msg-type (getf msg :type))
|
||||
(action (getf payload :action))
|
||||
(gate-trace (getf msg :gate-trace))
|
||||
(rule-count (getf payload :rule-count))
|
||||
(foveal-id (getf payload :foveal-id)))
|
||||
;; v0.7.1: streaming chunk
|
||||
(when (eq msg-type :stream-chunk)
|
||||
(cond
|
||||
((string= text "")
|
||||
;; Final chunk: stamp time, clear streaming
|
||||
(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 (st :streaming-text))
|
||||
;; First chunk: add new streaming message
|
||||
(setf (st :streaming-text) "")
|
||||
(setf (st :busy) nil)
|
||||
(add-msg :agent text)
|
||||
(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 (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 (st :rule-count) rule-count))
|
||||
(when foveal-id (setf (st :foveal-id) foveal-id))
|
||||
(cond
|
||||
@@ -443,7 +517,7 @@
|
||||
(fiveam:is (eq :chat (st :mode)))
|
||||
(fiveam:is (eq nil (st :connected)))
|
||||
(fiveam:is (eq nil (st :stream)))
|
||||
(fiveam:is (eq nil (st :messages)))
|
||||
(fiveam:is (zerop (length (st :messages))))
|
||||
(fiveam:is (eq 0 (st :scroll-offset)))
|
||||
(fiveam:is (eq nil (st :busy))))
|
||||
|
||||
@@ -452,7 +526,7 @@
|
||||
(init-state)
|
||||
(add-msg :user "hello")
|
||||
(let* ((msgs (st :messages))
|
||||
(msg (first msgs)))
|
||||
(msg (aref msgs 0)))
|
||||
(fiveam:is (eq :user (getf msg :role)))
|
||||
(fiveam:is (string= "hello" (getf msg :content)))
|
||||
(fiveam:is (stringp (getf msg :time)))
|
||||
@@ -493,7 +567,7 @@
|
||||
;; A user message should be in the message list
|
||||
(let ((msgs (st :messages)))
|
||||
(fiveam:is (>= (length msgs) 1))
|
||||
(let ((last (first msgs)))
|
||||
(let ((last (aref msgs 0)))
|
||||
(fiveam:is (eq :user (getf last :role)))
|
||||
(fiveam:is (string= "test" (getf last :content))))))
|
||||
|
||||
@@ -506,7 +580,7 @@
|
||||
(on-key 343)
|
||||
(let ((msgs (st :messages)))
|
||||
(fiveam:is (>= (length msgs) 1))
|
||||
(let ((last-msg (first msgs)))
|
||||
(let ((last-msg (aref msgs 0)))
|
||||
(fiveam:is (eq :system (getf last-msg :role)))
|
||||
(fiveam:is (search "=> 3" (getf last-msg :content))))))
|
||||
|
||||
@@ -526,7 +600,7 @@
|
||||
(dolist (ch (coerce "/focus myapp" 'list))
|
||||
(on-key (char-code ch)))
|
||||
(on-key 343)
|
||||
(let ((msg (first (st :messages))))
|
||||
(let ((msg (aref (st :messages) 0)))
|
||||
(fiveam:is (eq :system (getf msg :role)))))
|
||||
|
||||
(fiveam:test test-on-key-scope-command
|
||||
@@ -535,7 +609,7 @@
|
||||
(dolist (ch (coerce "/scope memex" 'list))
|
||||
(on-key (char-code ch)))
|
||||
(on-key 343)
|
||||
(let ((msg (first (st :messages))))
|
||||
(let ((msg (aref (st :messages) 0)))
|
||||
(fiveam:is (eq :system (getf msg :role)))))
|
||||
|
||||
(fiveam:test test-on-key-unfocus-command
|
||||
@@ -544,7 +618,7 @@
|
||||
(dolist (ch (coerce "/unfocus" 'list))
|
||||
(on-key (char-code ch)))
|
||||
(on-key 343)
|
||||
(let ((msg (first (st :messages))))
|
||||
(let ((msg (aref (st :messages) 0)))
|
||||
(fiveam:is (eq :system (getf msg :role)))))
|
||||
|
||||
(fiveam:test test-on-key-tab-completion
|
||||
@@ -636,3 +710,53 @@
|
||||
(dolist (ch (coerce "/theme " 'list)) (on-key (char-code ch)))
|
||||
(on-key 9)
|
||||
(fiveam:is (search "dark" (input-string) :test #'char-equal)))
|
||||
|
||||
;; ── v0.7.1 Streaming ──
|
||||
|
||||
(fiveam:test test-stream-chunk-appends
|
||||
"Contract/v0.7.1: stream-chunk frame appends to last message."
|
||||
(init-state)
|
||||
(on-daemon-msg '(:type :stream-chunk :payload (:text "Hello")))
|
||||
(on-daemon-msg '(:type :stream-chunk :payload (:text " world")))
|
||||
(let ((msgs (st :messages)))
|
||||
(fiveam:is (= 1 (length msgs)))
|
||||
(let ((msg (aref msgs 0)))
|
||||
(fiveam:is (eq :agent (getf msg :role)))
|
||||
(fiveam:is (string= "Hello world" (getf msg :content)))
|
||||
(fiveam:is (eq t (getf msg :streaming))))))
|
||||
|
||||
(fiveam:test test-stream-chunk-final
|
||||
"Contract/v0.7.1: final empty chunk stamps timestamp and clears streaming flag."
|
||||
(init-state)
|
||||
(on-daemon-msg '(:type :stream-chunk :payload (:text "Hi")))
|
||||
(on-daemon-msg '(:type :stream-chunk :payload (:text "")))
|
||||
(let ((msg (aref (st :messages) 0)))
|
||||
(fiveam:is (stringp (getf msg :time)))
|
||||
(fiveam:is (string= "Hi" (getf msg :content)))
|
||||
(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 (st :messages) 0)))
|
||||
(fiveam:is (stringp (getf msg :time)))
|
||||
(fiveam:is (search "[interrupted]" (getf msg :content)))
|
||||
(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 (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")
|
||||
;; Tab should extract URL and set url buffer (model-level test)
|
||||
(on-key 9)
|
||||
(fiveam:is (string= "https://example.com" (st :url-buffer))))
|
||||
|
||||
Reference in New Issue
Block a user