v0.7.2: message search mode — navigate, highlight, jump — TDD
Search mode activated by /search <query>. State fields: :search-mode, :search-query, :search-matches, :search-match-idx. Up/Down arrows navigate between matches, Enter jumps to current match, Escape exits. search-highlight wraps matching substrings in **bold** for markdown rendering. View-chat shows search header bar with match count and current position. - channel-tui-state: 4 search state fields in init-state - channel-tui-main: modified /search handler, search-mode key handlers (Up/Down/Enter/Escape), 3 new tests (activate, escape, nav) - channel-tui-view: search-highlight fn, search header bar, highlighted content in count+render loops - TUI Main: 97/98 (1 pre-existing flake) View: 29/29
This commit is contained in:
@@ -45,20 +45,57 @@ Event handlers + daemon I/O + main loop.
|
||||
(or name raw))
|
||||
raw)))
|
||||
(cond
|
||||
;; 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: 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.2: Esc — exit search mode
|
||||
((and (eql ch 27) (st :search-mode))
|
||||
(setf (st :search-mode) nil
|
||||
(st :search-matches) nil
|
||||
(st :search-query) "")
|
||||
(setf (st :dirty) (list nil t nil))
|
||||
(add-msg :system "Search exited"))
|
||||
;; v0.7.2: search mode — Up/Down navigate matches
|
||||
((and (st :search-mode) (or (eql ch 259) (eq ch :up)))
|
||||
(let* ((matches (st :search-matches))
|
||||
(idx (st :search-match-idx))
|
||||
(new-idx (max 0 (1- idx))))
|
||||
(setf (st :search-match-idx) new-idx)
|
||||
(when matches
|
||||
(setf (st :scroll-offset) (nth new-idx matches))
|
||||
(add-msg :system (format nil "Match ~d/~d" (1+ new-idx) (length matches)))
|
||||
(setf (st :dirty) (list nil t nil)))))
|
||||
((and (st :search-mode) (or (eql ch 258) (eq ch :down)))
|
||||
(let* ((matches (st :search-matches))
|
||||
(idx (st :search-match-idx))
|
||||
(new-idx (min (1- (length matches)) (1+ idx))))
|
||||
(setf (st :search-match-idx) new-idx)
|
||||
(when matches
|
||||
(setf (st :scroll-offset) (nth new-idx matches))
|
||||
(add-msg :system (format nil "Match ~d/~d" (1+ new-idx) (length matches)))
|
||||
(setf (st :dirty) (list nil t nil)))))
|
||||
;; v0.7.2: search mode — Enter jumps to current match
|
||||
((and (st :search-mode) (or (eql ch 13) (eql ch 10) (eq ch :enter)))
|
||||
(let ((matches (st :search-matches))
|
||||
(idx (st :search-match-idx)))
|
||||
(when (and matches (>= (length matches) (1+ idx)))
|
||||
(setf (st :scroll-offset) (nth idx matches))
|
||||
(setf (st :search-mode) nil
|
||||
(st :search-matches) nil
|
||||
(st :search-query) "")
|
||||
(add-msg :system (format nil "Jumped to match ~d" (1+ idx)))
|
||||
(setf (st :dirty) (list nil 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)))
|
||||
@@ -256,33 +293,27 @@ Event handlers + daemon I/O + main loop.
|
||||
;; /context dropped — pruned nodes
|
||||
((string-equal text "/context dropped")
|
||||
(add-msg :system "Context debugging: dropped nodes view not yet available (v0.8.0)"))
|
||||
;; /search command — message search
|
||||
((and (>= (length text) 8) (string-equal (subseq text 0 8) "/search "))
|
||||
(let* ((query (string-downcase (string-trim '(#\Space) (subseq text 8))))
|
||||
(msgs (st :messages))
|
||||
(total (length msgs))
|
||||
(matches nil))
|
||||
(loop for i from 0 below total
|
||||
for m = (aref msgs i)
|
||||
for content = (getf m :content)
|
||||
when (search query (string-downcase content))
|
||||
do (push (list i content) matches))
|
||||
(setf matches (nreverse matches))
|
||||
(if matches
|
||||
(progn
|
||||
(add-msg :system (format nil "Found ~d matches for '~a':"
|
||||
(length matches) query))
|
||||
(dolist (match matches)
|
||||
(let* ((idx (first match))
|
||||
(content (second match))
|
||||
(pos (search query (string-downcase content)))
|
||||
(preview (if (> (length content) 60)
|
||||
(concatenate 'string
|
||||
(subseq content (max 0 (- pos 20)) (min (length content) (+ pos 40)))
|
||||
"...")
|
||||
content)))
|
||||
(add-msg :system (format nil " #~d: ...~a..." idx preview)))))
|
||||
(add-msg :system (format nil "No matches for '~a'" query)))))
|
||||
;; /search command — message search
|
||||
((and (>= (length text) 8) (string-equal (subseq text 0 8) "/search "))
|
||||
(let* ((query (string-downcase (string-trim '(#\Space) (subseq text 8))))
|
||||
(msgs (st :messages))
|
||||
(total (length msgs))
|
||||
(matches nil))
|
||||
(loop for i from 0 below total
|
||||
for m = (aref msgs i)
|
||||
for content = (getf m :content)
|
||||
when (search query (string-downcase content))
|
||||
do (push i matches))
|
||||
(setf matches (nreverse matches))
|
||||
;; Enter search mode
|
||||
(setf (st :search-mode) t
|
||||
(st :search-query) query
|
||||
(st :search-matches) matches
|
||||
(st :search-match-idx) 0)
|
||||
(if matches
|
||||
(add-msg :system (format nil "Search: ~d matches for '~a' (1/~d) — Up/Down nav, Enter jump, Esc exit"
|
||||
(length matches) query (length matches)))
|
||||
(add-msg :system (format nil "0 matches for '~a'" query)))))
|
||||
;; /rewind command — session rewind
|
||||
((and (>= (length text) 8) (string-equal (subseq text 0 8) "/rewind "))
|
||||
(let* ((n-str (string-trim '(#\Space) (subseq text 8)))
|
||||
@@ -1174,4 +1205,46 @@ Event handlers + daemon I/O + main loop.
|
||||
(on-key 7)
|
||||
(let ((m (aref (st :messages) 0)))
|
||||
(fiveam:is (search "No gate trace" (getf m :content)))))
|
||||
|
||||
;; ── v0.7.2 Message Search Mode ──
|
||||
|
||||
(fiveam:test test-search-mode-activate
|
||||
"Contract v0.7.2: /search enters search mode."
|
||||
(init-state)
|
||||
(add-msg :agent "hello world")
|
||||
(add-msg :agent "goodbye")
|
||||
(dolist (ch (coerce "/search hello" 'list))
|
||||
(on-key (char-code ch)))
|
||||
(on-key 13)
|
||||
(fiveam:is (eq t (st :search-mode)))
|
||||
(fiveam:is (string= "hello" (st :search-query)))
|
||||
(fiveam:is (= 1 (length (st :search-matches)))))
|
||||
|
||||
(fiveam:test test-search-mode-escape-exits
|
||||
"Contract v0.7.2: Escape exits search mode."
|
||||
(init-state)
|
||||
(add-msg :agent "test")
|
||||
(dolist (ch (coerce "/search test" 'list))
|
||||
(on-key (char-code ch)))
|
||||
(on-key 13)
|
||||
(fiveam:is (eq t (st :search-mode)))
|
||||
(on-key 27) ;; Escape
|
||||
(fiveam:is (null (st :search-mode))))
|
||||
|
||||
(fiveam:test test-search-mode-up-down-nav
|
||||
"Contract v0.7.2: Up/Down navigates between search matches."
|
||||
(init-state)
|
||||
(add-msg :agent "aaa hello bbb")
|
||||
(add-msg :agent "ccc hello ddd")
|
||||
(add-msg :agent "no match here")
|
||||
(dolist (ch (coerce "/search hello" 'list))
|
||||
(on-key (char-code ch)))
|
||||
(on-key 13)
|
||||
(fiveam:is (= 0 (st :search-match-idx)))
|
||||
(on-key 258) ;; Down
|
||||
(fiveam:is (= 1 (st :search-match-idx)))
|
||||
(on-key 259) ;; Up
|
||||
(fiveam:is (= 0 (st :search-match-idx)))
|
||||
(on-key 259) ;; Up (clamped)
|
||||
(fiveam:is (= 0 (st :search-match-idx))))
|
||||
#+end_src
|
||||
|
||||
Reference in New Issue
Block a user