@@ -401,6 +401,9 @@ Must be a positive integer."
401401 (define-key map (kbd " C-c C-m" ) #'eca-chat-select-model )
402402 (define-key map (kbd " C-c C-n" ) #'eca-chat-new )
403403 (define-key map (kbd " C-c C-f" ) #'eca-chat-select )
404+ (define-key map (kbd " C-c C-p" ) #'eca-chat-repeat-prompt )
405+ (define-key map (kbd " C-c C-d" ) #'eca-chat-clear-prompt )
406+ (define-key map (kbd " C-c C-h" ) #'eca-chat-timeline )
404407 (define-key map (kbd " C-c C-a" ) #'eca-chat-tool-call-accept-all )
405408 (define-key map (kbd " C-c C-S-a" ) #'eca-chat-tool-call-accept-next )
406409 (define-key map (kbd " C-c C-s" ) #'eca-chat-tool-call-accept-all-and-remember )
@@ -582,19 +585,19 @@ Must be a positive integer."
582585
583586(defun eca-chat--clear ()
584587 " Clear the chat for SESSION."
585- (erase-buffer )
586- (remove-overlays (point-min ) (point-max ))
587- (insert " \n " )
588- (eca-chat--insert-prompt-string)
589- (eca-chat--refresh-context))
588+ (erase-buffer )
589+ (remove-overlays (point-min ) (point-max ))
590+ (insert " \n " )
591+ (eca-chat--insert-prompt-string)
592+ (eca-chat--refresh-context))
590593
591594(defun eca-chat--stop-prompt (session )
592595 " Stop the running chat prompt for SESSION."
593596 (when eca-chat--chat-loading
594- (eca-api-notify session
595- :method " chat/promptStop"
596- :params (list :chatId eca-chat--id))
597- (eca-chat--set-chat-loading session nil )))
597+ (eca-api-notify session
598+ :method " chat/promptStop"
599+ :params (list :chatId eca-chat--id))
600+ (eca-chat--set-chat-loading session nil )))
598601
599602(defun eca-chat--set-chat-loading (session loading )
600603 " Set the SESSION chat to a loading state if LOADING is non nil.
@@ -721,7 +724,7 @@ the prompt/context line."
721724 (string-blank-p (buffer-substring-no-properties (line-beginning-position ) (point ))))))
722725 (ding ))
723726
724- ; ; in context area trying to remove a context space separator
727+ ; ; in context area trying to remove a context space separator
725728 ((and cur-ov
726729 (overlay-get cur-ov 'eca-chat-context-area )
727730 (and (string= " " (string (char-before (point ))))
@@ -991,7 +994,9 @@ Add a overlay before with OVERLAY-KEY = OVERLAY-VALUE if passed."
991994 (goto-char (1- (point )))
992995 (when overlay-key
993996 (let ((ov (make-overlay (point ) (point ) (current-buffer ))))
994- (overlay-put ov overlay-key overlay-value)))
997+ (overlay-put ov overlay-key overlay-value)
998+ (when (eq overlay-key 'eca-chat--user-message-id )
999+ (overlay-put ov 'eca-chat--timestamp (float-time )))))
9951000 (insert text)
9961001 (point ))))
9971002
@@ -1092,8 +1097,8 @@ If FORCE? decide to CLOSE? or not."
10921097 (content (overlay-get ov-content 'eca-chat--expandable-content-content ))
10931098 (empty-content? (string-empty-p content))
10941099 (close? (if force?
1095- close?
1096- (overlay-get ov-label 'eca-chat--expandable-content-toggle ))))
1100+ close?
1101+ (overlay-get ov-label 'eca-chat--expandable-content-toggle ))))
10971102 (save-excursion
10981103 (goto-char (overlay-start ov-label))
10991104 (if (or close? empty-content?)
@@ -1333,7 +1338,7 @@ If STATIC? return strs with no dynamic values."
13331338Add text property to prompt text to match context."
13341339 (let ((context (get-text-property 0 'eca-chat-completion-item item)))
13351340 (let ((start-pos (save-excursion
1336- (search-backward eca-chat-context-prefix (line-beginning-position ) t )))
1341+ (search-backward eca-chat-context-prefix (line-beginning-position ) t )))
13371342 (end-pos (point )))
13381343 (delete-region start-pos end-pos)
13391344 (insert (eca-chat--context->str context 'static ))))
@@ -1399,10 +1404,10 @@ Add text property to prompt text to match context."
13991404(defun eca-chat--go-to-overlay (ov-key range-min range-max first? )
14001405 " Go to overlay finding from RANGE-MIN to RANGE-MAX if matches OV-KEY."
14011406 (eca-chat--with-current-buffer (eca-chat--get-last-buffer (eca-session))
1402- (let ((get-fn (if first? #'-first #'-last )))
1403- (when-let ((ov (funcall get-fn (-lambda (ov) (overlay-get ov ov-key))
1404- (overlays-in range-min range-max))))
1405- (goto-char (overlay-start ov))))))
1407+ (let ((get-fn (if first? #'-first #'-last )))
1408+ (when-let ((ov (funcall get-fn (-lambda (ov) (overlay-get ov ov-key))
1409+ (overlays-in range-min range-max))))
1410+ (goto-char (overlay-start ov))))))
14061411
14071412(defun eca-chat--cur-position ()
14081413 " Return the start and end positions for current point.
@@ -1413,8 +1418,8 @@ of (LINE . CHARACTER) representing the current selection or cursor position."
14131418 (end-pos (if (use-region-p ) (region-end ) (point )))
14141419 (start-line (line-number-at-pos start-pos))
14151420 (start-char (1+ (progn
1416- (goto-char start-pos)
1417- (current-column ))))
1421+ (goto-char start-pos)
1422+ (current-column ))))
14181423 (end-line (line-number-at-pos end-pos))
14191424 (end-char (1+ (progn
14201425 (goto-char end-pos)
@@ -1724,8 +1729,8 @@ Calls CB with the resulting message."
17241729 (cond
17251730 ((eq action 'metadata )
17261731 '(metadata (category . eca-capf)
1727- (display-sort-function . identity)
1728- (cycle-sort-function . identity)))
1732+ (display-sort-function . identity)
1733+ (cycle-sort-function . identity)))
17291734 ((eq (car-safe action) 'boundaries ) nil )
17301735 (t
17311736 (complete-with-action action (funcall candidates-fn) probe pred))))
@@ -2054,38 +2059,38 @@ Calls CB with the resulting message."
20542059 (unless (buffer-live-p (eca-chat--get-last-buffer session))
20552060 (eca-chat--create-buffer session))
20562061 (eca-chat--with-current-buffer (eca-chat--get-last-buffer session)
2057- (unless (derived-mode-p 'eca-chat-mode )
2058- (eca-chat-mode)
2059- (eca-chat--track-cursor-position-schedule)
2060- (when eca-chat-auto-add-cursor
2061- (eca-chat--add-context (list :type " cursor" )))
2062- (when eca-chat-auto-add-repomap
2063- (eca-chat--add-context (list :type " repoMap" ))))
2064- (unless (eq (current-buffer ) (eca-get (eca--session-chats session) 'empty ))
2065- (setf (eca--session-chats session) (eca-assoc (eca--session-chats session) 'empty (current-buffer ))))
2066- (if (window-live-p (get-buffer-window (buffer-name )))
2067- (eca-chat--select-window)
2068- (eca-chat--pop-window))
2069- (unless (eca--session-last-chat-buffer session)
2070- (setf (eca--session-last-chat-buffer session) (current-buffer ))))
2062+ (unless (derived-mode-p 'eca-chat-mode )
2063+ (eca-chat-mode)
2064+ (eca-chat--track-cursor-position-schedule)
2065+ (when eca-chat-auto-add-cursor
2066+ (eca-chat--add-context (list :type " cursor" )))
2067+ (when eca-chat-auto-add-repomap
2068+ (eca-chat--add-context (list :type " repoMap" ))))
2069+ (unless (eq (current-buffer ) (eca-get (eca--session-chats session) 'empty ))
2070+ (setf (eca--session-chats session) (eca-assoc (eca--session-chats session) 'empty (current-buffer ))))
2071+ (if (window-live-p (get-buffer-window (buffer-name )))
2072+ (eca-chat--select-window)
2073+ (eca-chat--pop-window))
2074+ (unless (eca--session-last-chat-buffer session)
2075+ (setf (eca--session-last-chat-buffer session) (current-buffer ))))
20712076 (eca-chat--track-cursor))
20722077
20732078(defun eca-chat-exit (session )
20742079 " Exit the ECA chat for SESSION."
20752080 (when (buffer-live-p (eca-chat--get-last-buffer session))
20762081 (eca-chat--with-current-buffer (eca-chat--get-last-buffer session)
2077- (setq eca-chat--closed t )
2078- (force-mode-line-update )
2079- (goto-char (point-max ))
2080- (rename-buffer (concat (buffer-name ) " :closed" ) t )
2081- ; ; Keep only the most recently closed chat buffer; kill older ones.
2082- (let ((current (current-buffer )))
2083- (dolist (b (buffer-list ))
2084- (when (and (not (eq b current))
2085- (string-match-p " ^<eca-chat:.*>:closed$" (buffer-name b)))
2086- (kill-buffer b))))
2087- (when-let* ((window (get-buffer-window (eca-chat--get-last-buffer session))))
2088- (quit-window nil window)))))
2082+ (setq eca-chat--closed t )
2083+ (force-mode-line-update )
2084+ (goto-char (point-max ))
2085+ (rename-buffer (concat (buffer-name ) " :closed" ) t )
2086+ ; ; Keep only the most recently closed chat buffer; kill older ones.
2087+ (let ((current (current-buffer )))
2088+ (dolist (b (buffer-list ))
2089+ (when (and (not (eq b current))
2090+ (string-match-p " ^<eca-chat:.*>:closed$" (buffer-name b)))
2091+ (kill-buffer b))))
2092+ (when-let* ((window (get-buffer-window (eca-chat--get-last-buffer session))))
2093+ (quit-window nil window)))))
20892094
20902095;;;### autoload
20912096(defun eca-chat-clear ()
@@ -2403,7 +2408,7 @@ if ARG is current prefix, ask for file, otherwise drop current file."
24032408 (eca-assert-session-running session)
24042409 (eca-chat-open session)
24052410 (eca-chat--with-current-buffer (eca-chat--get-last-buffer session)
2406- (goto-char (point-max )))
2411+ (goto-char (point-max )))
24072412 (let ((buffer (get-buffer-create " *whisper-stdout*" )))
24082413 (with-current-buffer buffer
24092414 (erase-buffer )
@@ -2414,15 +2419,117 @@ if ARG is current prefix, ask for file, otherwise drop current file."
24142419 (line-beginning-position )
24152420 (line-end-position ))))
24162421 (eca-chat--with-current-buffer (eca-chat--get-last-buffer session)
2417- (insert transcription)
2418- (newline )
2419- (eca-chat--key-pressed-return))))
2422+ (insert transcription)
2423+ (newline )
2424+ (eca-chat--key-pressed-return))))
24202425 nil t )
24212426 (whisper-run)
24222427 (eca-info " Recording audio. Press RET when you are done." )
24232428 (while (not (equal ?\r (read-char )))
24242429 (sit-for 0.5 ))
24252430 (whisper-run)))))
24262431
2432+ (defun eca-chat--format-message-for-completion (msg )
2433+ " Format MSG for display in completion interface.
2434+ If MSG has :timestamp, prepends [HH:MM] to the text."
2435+ (let ((timestamp (plist-get msg :timestamp ))
2436+ (text (plist-get msg :text )))
2437+ (if timestamp
2438+ (format " [%s ] %s "
2439+ (format-time-string " %H:%M" timestamp)
2440+ text)
2441+ text)))
2442+
2443+ (defun eca-chat--get-user-messages (&optional buffer )
2444+ " Extract all user messages from the chat BUFFER.
2445+ If BUFFER is nil, use the last chat buffer from current session.
2446+ Returns a list of plists, each containing:
2447+ :text - the message text
2448+ :start - start position in buffer
2449+ :end - end position in buffer
2450+ :id - message ID from overlay
2451+ :line - line number of the message
2452+ :timestamp - timestamp when message was sent
2453+
2454+ Messages are ordered from newest to oldest.
2455+ Returns empty list if session is not running or buffer is not available."
2456+ (when-let* ((session (eca-session))
2457+ (chat-buffer (or buffer (eca-chat--get-last-buffer session)))
2458+ ((buffer-live-p chat-buffer)))
2459+ (with-current-buffer chat-buffer
2460+ (let ((messages '()))
2461+ (dolist (ov (overlays-in (point-min ) (point-max )))
2462+ (when-let* ((msg-id (overlay-get ov 'eca-chat--user-message-id ))
2463+ (start (overlay-start ov))
2464+ (end (save-excursion
2465+ (goto-char start)
2466+ (while (and (not (eobp ))
2467+ (progn (forward-line 1 )
2468+ (eq (get-text-property (point ) 'font-lock-face )
2469+ 'eca-chat-user-messages-face ))))
2470+ (line-end-position 0 )))
2471+ (text (string-trim (buffer-substring-no-properties start end)))
2472+ (timestamp (overlay-get ov 'eca-chat--timestamp )))
2473+ (unless (string-empty-p text)
2474+ (push (list :text text
2475+ :start start
2476+ :end end
2477+ :id msg-id
2478+ :timestamp timestamp
2479+ :line (line-number-at-pos start))
2480+ messages))))
2481+ messages))))
2482+
2483+ (defun eca-chat--select-message-from-completion (prompt )
2484+ " Show completion with user messages using PROMPT.
2485+ Returns selected message plist or nil if no messages or cancelled."
2486+ (when-let ((messages (eca-chat--get-user-messages)))
2487+ (let ((table (make-hash-table :test 'equal )))
2488+ (dolist (msg (reverse messages))
2489+ (puthash (eca-chat--format-message-for-completion msg) msg table))
2490+ (when-let ((choice (completing-read
2491+ prompt
2492+ (lambda (string pred action )
2493+ (if (eq action 'metadata )
2494+ `(metadata (display-sort-function . identity))
2495+ (complete-with-action action (hash-table-keys table) string pred)))
2496+ nil t )))
2497+ (gethash choice table)))))
2498+
2499+ ;;;### autoload
2500+ (defun eca-chat-timeline ()
2501+ " Navigate to a user message via completion."
2502+ (interactive )
2503+ (if-let* ((selected-msg (eca-chat--select-message-from-completion " Timeline: " ))
2504+ (pos (plist-get selected-msg :start ))
2505+ (chat-buffer (eca-chat--get-last-buffer (eca-session))))
2506+ (progn
2507+ (eca-chat--display-buffer chat-buffer)
2508+ (with-current-buffer chat-buffer
2509+ (goto-char pos)
2510+ (recenter )))
2511+ (message " No user messages found " )))
2512+
2513+ ;;;### autoload
2514+ (defun eca-chat-clear-prompt ()
2515+ " Clear the prompt input field in chat."
2516+ (interactive )
2517+ (when-let ((chat-buffer (eca-chat--get-last-buffer (eca-session))))
2518+ (with-current-buffer chat-buffer
2519+ (eca-chat--set-prompt " " ))))
2520+
2521+ ;;;### autoload
2522+ (defun eca-chat-repeat-prompt ()
2523+ " Select a previous message and insert its text into the prompt."
2524+ (interactive )
2525+ (if-let* ((selected-msg (eca-chat--select-message-from-completion " Repeat prompt: " ))
2526+ (text (plist-get selected-msg :text ))
2527+ (chat-buffer (eca-chat--get-last-buffer (eca-session))))
2528+ (progn
2529+ (eca-chat--display-buffer chat-buffer)
2530+ (with-current-buffer chat-buffer
2531+ (eca-chat--set-prompt text)))
2532+ (message " No user messages found " )))
2533+
24272534(provide 'eca-chat )
24282535; ;; eca-chat.el ends here
0 commit comments