From 9be171b756ff927bdcac7f730d7979519de67f6d Mon Sep 17 00:00:00 2001 From: CsBigDataHub Date: Fri, 12 Sep 2025 16:15:30 -0400 Subject: [PATCH 1/6] introduce timer to fix performace issues, fix #38 --- eca-chat.el | 123 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 79 insertions(+), 44 deletions(-) diff --git a/eca-chat.el b/eca-chat.el index ea249a0..f0c655d 100644 --- a/eca-chat.el +++ b/eca-chat.el @@ -147,6 +147,22 @@ Must be a valid model supported by server, check `eca-chat-select-model`." (const :tag "Merge-style Smerge" smerge)) :group 'eca) +(defcustom eca-chat-tool-prepare-throttle 'smart + "Throttle strategy for handling `toolCallPrepare` events. +Possible values: `all` or `smart` (default)." + :type '(choice (const :tag "Process all updates" all) + (const :tag "Smart throttle" smart)) + :group 'eca) + +(defcustom eca-chat-tool-prepare-update-interval 5 + "When `smart`, process every Nth `toolCallPrepare` update. +Must be a positive integer." + :type 'integer + :group 'eca) + +(defvar-local eca-chat--tool-prepare-counters (make-hash-table :test 'equal) + "Buffer-local hash table mapping toolCall ID to seen `toolCallPrepare` counts.") + ;; Faces @@ -1345,13 +1361,28 @@ string." (format "Preparing tool: %s" name))) (label (concat (propertize summary 'font-lock-face 'eca-chat-mcp-tool-call-label-face) - " " eca-chat-mcp-tool-call-loading-symbol))) - (if (eca-chat--get-expandable-content id) - (eca-chat--update-expandable-content id label argsText t) - (eca-chat--add-expandable-content id label - (eca-chat--content-table - `(("Tool" . ,name) - ("Arguments" . ,argsText))))))) + " " eca-chat-mcp-tool-call-loading-symbol)) + (current-count (gethash id eca-chat--tool-prepare-counters 0)) + (should-process + (pcase eca-chat-tool-prepare-throttle + ('all t) + ('smart + (or (= current-count 0) + (and (integerp eca-chat-tool-prepare-update-interval) + (> eca-chat-tool-prepare-update-interval 0) + (= (mod current-count eca-chat-tool-prepare-update-interval) 0)))) + (_ t)))) + ;; update counter and only perform the expensive UI updates when + ;; `should-process' is true. This reduces churn for high-frequency + ;; streaming `toolCallPrepare' events. + (puthash id (1+ current-count) eca-chat--tool-prepare-counters) + (when should-process + (if (eca-chat--get-expandable-content id) + (eca-chat--update-expandable-content id label argsText t) + (eca-chat--add-expandable-content id label + (eca-chat--content-table + `(("Tool" . ,name) + ("Arguments" . ,argsText)))))))) ("toolCallRun" (let* ((id (plist-get content :id)) @@ -1442,45 +1473,47 @@ string." `(("Tool" . ,name) ("Arguments" . ,(plist-get content :arguments)))))))) - ("toolCalled" - (let* ((id (plist-get content :id)) - (name (plist-get content :name)) - (summary (or (plist-get content :summary) - (format "Called tool: %s" name))) - (outputs (plist-get content :outputs)) - (output-text (if outputs + ("toolCalled" + (let* ((id (plist-get content :id)) + (name (plist-get content :name)) + (summary (or (plist-get content :summary) + (format "Called tool: %s" name))) + (outputs (plist-get content :outputs)) + (output-text (if outputs (mapconcat (lambda (o) (or (plist-get o :text) "")) outputs "\n") "")) - (details (plist-get content :details)) - (status (if (plist-get content :error) - eca-chat-mcp-tool-call-error-symbol - eca-chat-mcp-tool-call-success-symbol))) - (if (and (stringp (plist-get details :type)) - (string= "fileChange" (plist-get details :type))) - (let* ((path (plist-get details :path)) - (diff (plist-get details :diff)) - (view-btn - (concat " " - (eca-buttonize - (propertize "view_diff" 'font-lock-face 'eca-chat-diff-view-face) - `(lambda () - (interactive) - (eca-chat--show-diff ,path ,diff)))))) - (eca-chat--update-expandable-content - id - (concat (propertize summary 'font-lock-face 'eca-chat-mcp-tool-call-label-face) - " " status - (eca-chat--file-change-details-label details) - view-btn) - (concat "Tool: `" name "`\n" - (eca-chat--file-change-diff path diff roots)))) - (eca-chat--update-expandable-content - id - (concat (propertize summary 'font-lock-face 'eca-chat-mcp-tool-call-label-face) - " " status) - (eca-chat--content-table - `(("Tool" . ,name) - ("Output" . ,output-text))))))) + (details (plist-get content :details)) + (status (if (plist-get content :error) + eca-chat-mcp-tool-call-error-symbol + eca-chat-mcp-tool-call-success-symbol))) + ;; Cleanup counters for this tool-call id to avoid unbounded growth + (remhash id eca-chat--tool-prepare-counters) + (if (and (stringp (plist-get details :type)) + (string= "fileChange" (plist-get details :type))) + (let* ((path (plist-get details :path)) + (diff (plist-get details :diff)) + (view-btn + (concat " " + (eca-buttonize + (propertize "view_diff" 'font-lock-face 'eca-chat-diff-view-face) + `(lambda () + (interactive) + (eca-chat--show-diff ,path ,diff)))))) + (eca-chat--update-expandable-content + id + (concat (propertize summary 'font-lock-face 'eca-chat-mcp-tool-call-label-face) + " " status + (eca-chat--file-change-details-label details) + view-btn) + (concat "Tool: `" name "`\n" + (eca-chat--file-change-diff path diff roots)))) + (eca-chat--update-expandable-content + id + (concat (propertize summary 'font-lock-face 'eca-chat-mcp-tool-call-label-face) + " " status) + (eca-chat--content-table + `(("Tool" . ,name) + ("Output" . ,output-text))))))) ("progress" (pcase (plist-get content :state) @@ -1591,6 +1624,8 @@ string." (setq-local eca-chat--session-cost nil) (setq-local eca-chat--empty t) (setq-local eca-chat--track-context t) + ;; Reset per-buffer tool prepare counters to avoid leaking across sessions + (setq-local eca-chat--tool-prepare-counters (make-hash-table :test 'equal)) (eca-chat--clear (eca-session)))) ;;;###autoload From f0ad6570cd84adff3ceae6870eccd3bf9ab157b6 Mon Sep 17 00:00:00 2001 From: CsBigDataHub Date: Fri, 12 Sep 2025 16:16:55 -0400 Subject: [PATCH 2/6] format eca-chat.el --- eca-chat.el | 78 ++++++++++++++++++++++++++--------------------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/eca-chat.el b/eca-chat.el index f0c655d..8c88664 100644 --- a/eca-chat.el +++ b/eca-chat.el @@ -1473,47 +1473,47 @@ string." `(("Tool" . ,name) ("Arguments" . ,(plist-get content :arguments)))))))) - ("toolCalled" - (let* ((id (plist-get content :id)) - (name (plist-get content :name)) - (summary (or (plist-get content :summary) - (format "Called tool: %s" name))) - (outputs (plist-get content :outputs)) - (output-text (if outputs + ("toolCalled" + (let* ((id (plist-get content :id)) + (name (plist-get content :name)) + (summary (or (plist-get content :summary) + (format "Called tool: %s" name))) + (outputs (plist-get content :outputs)) + (output-text (if outputs (mapconcat (lambda (o) (or (plist-get o :text) "")) outputs "\n") "")) - (details (plist-get content :details)) - (status (if (plist-get content :error) - eca-chat-mcp-tool-call-error-symbol - eca-chat-mcp-tool-call-success-symbol))) - ;; Cleanup counters for this tool-call id to avoid unbounded growth - (remhash id eca-chat--tool-prepare-counters) - (if (and (stringp (plist-get details :type)) - (string= "fileChange" (plist-get details :type))) - (let* ((path (plist-get details :path)) - (diff (plist-get details :diff)) - (view-btn - (concat " " - (eca-buttonize - (propertize "view_diff" 'font-lock-face 'eca-chat-diff-view-face) - `(lambda () - (interactive) - (eca-chat--show-diff ,path ,diff)))))) - (eca-chat--update-expandable-content - id - (concat (propertize summary 'font-lock-face 'eca-chat-mcp-tool-call-label-face) - " " status - (eca-chat--file-change-details-label details) - view-btn) - (concat "Tool: `" name "`\n" - (eca-chat--file-change-diff path diff roots)))) - (eca-chat--update-expandable-content - id - (concat (propertize summary 'font-lock-face 'eca-chat-mcp-tool-call-label-face) - " " status) - (eca-chat--content-table - `(("Tool" . ,name) - ("Output" . ,output-text))))))) + (details (plist-get content :details)) + (status (if (plist-get content :error) + eca-chat-mcp-tool-call-error-symbol + eca-chat-mcp-tool-call-success-symbol))) + ;; Cleanup counters for this tool-call id to avoid unbounded growth + (remhash id eca-chat--tool-prepare-counters) + (if (and (stringp (plist-get details :type)) + (string= "fileChange" (plist-get details :type))) + (let* ((path (plist-get details :path)) + (diff (plist-get details :diff)) + (view-btn + (concat " " + (eca-buttonize + (propertize "view_diff" 'font-lock-face 'eca-chat-diff-view-face) + `(lambda () + (interactive) + (eca-chat--show-diff ,path ,diff)))))) + (eca-chat--update-expandable-content + id + (concat (propertize summary 'font-lock-face 'eca-chat-mcp-tool-call-label-face) + " " status + (eca-chat--file-change-details-label details) + view-btn) + (concat "Tool: `" name "`\n" + (eca-chat--file-change-diff path diff roots)))) + (eca-chat--update-expandable-content + id + (concat (propertize summary 'font-lock-face 'eca-chat-mcp-tool-call-label-face) + " " status) + (eca-chat--content-table + `(("Tool" . ,name) + ("Output" . ,output-text))))))) ("progress" (pcase (plist-get content :state) From 4b709f79bdbb1bb404349baacd0df8ff08e8a880 Mon Sep 17 00:00:00 2001 From: "hadoopchetan@gmail.com" Date: Sun, 14 Sep 2025 13:53:21 -0400 Subject: [PATCH 3/6] feat(eca-chat): add caching for tool call prepare streaming Introduce buffer-local caches for accumulating tool arguments and metadata during streaming toolCallPrepare messages. This reduces UI churn by throttling updates based on the new `eca-chat-tool-prepare-throttle` options ('first-last' and improved 'smart'). Add heuristic `eca-chat--detect-final-tool-prepare-p` to identify complete JSON-like argument blocks. Update toolCallRun, toolCallRunning, toolCalled, and toolCallRejected to use cached data when available. Clean up caches on completion or rejection. Initialize new caches in `eca-chat-mode` and apply minor formatting fixes for readability. --- eca-chat.el | 861 +++++++++++++++++++++++++++------------------------- 1 file changed, 446 insertions(+), 415 deletions(-) diff --git a/eca-chat.el b/eca-chat.el index f889b9c..d9f4198 100644 --- a/eca-chat.el +++ b/eca-chat.el @@ -161,7 +161,13 @@ Must be a positive integer." :group 'eca) (defvar-local eca-chat--tool-prepare-counters (make-hash-table :test 'equal) - "Buffer-local hash table mapping toolCall ID to seen `toolCallPrepare` counts.") + "Hash table mapping toolCall ID to message count.") + +(defvar-local eca-chat--tool-prepare-content-cache (make-hash-table :test 'equal) + "Hash table mapping toolCall ID to accumulated argument text.") + +(defvar-local eca-chat--tool-prepare-metadata-cache (make-hash-table :test 'equal) + "Hash table mapping toolCall ID to tool metadata (name, summary, etc).") ;; Faces @@ -365,16 +371,17 @@ Must be a positive integer." (defun eca-chat--spinner-start (session) "Start modeline spinner for SESSION." (setq eca-chat--spinner-timer - (eca-chat--with-current-buffer (eca-chat--get-buffer session) - (run-with-timer - 0 - 0.5 - (lambda () - (when eca-chat--spinner-timer - (if (eq 3 (length eca-chat--spinner-string)) - (setq eca-chat--spinner-string ".") - (setq eca-chat--spinner-string (concat eca-chat--spinner-string "."))) - (force-mode-line-update))))))) + (eca-chat--with-current-buffer + (eca-chat--get-buffer session) + (run-with-timer + 0 + 0.5 + (lambda () + (when eca-chat--spinner-timer + (if (eq 3 (length eca-chat--spinner-string)) + (setq eca-chat--spinner-string ".") + (setq eca-chat--spinner-string (concat eca-chat--spinner-string "."))) + (force-mode-line-update))))))) (defun eca-chat--spinner-stop () "Stop modeline spinner." @@ -434,21 +441,23 @@ Must be a positive integer." (defun eca-chat--clear (session) "Clear the chat for SESSION." - (eca-chat--with-current-buffer (eca-chat--get-buffer session) - (erase-buffer) - (remove-overlays (point-min) (point-max)) - (insert "\n") - (eca-chat--insert-prompt-string) - (eca-chat--refresh-context))) + (eca-chat--with-current-buffer + (eca-chat--get-buffer session) + (erase-buffer) + (remove-overlays (point-min) (point-max)) + (insert "\n") + (eca-chat--insert-prompt-string) + (eca-chat--refresh-context))) (defun eca-chat--stop-prompt (session) "Stop the running chat prompt for SESSION." (when eca-chat--chat-loading - (eca-chat--with-current-buffer (eca-chat--get-buffer session) - (eca-api-notify session - :method "chat/promptStop" - :params (list :chatId eca-chat--id)) - (eca-chat--set-chat-loading session nil)))) + (eca-chat--with-current-buffer + (eca-chat--get-buffer session) + (eca-api-notify session + :method "chat/promptStop" + :params (list :chatId eca-chat--id)) + (eca-chat--set-chat-loading session nil)))) (defun eca-chat--set-chat-loading (session loading) "Set the SESSION chat to a loading state if LOADING is non nil. @@ -505,18 +514,19 @@ Otherwise to a not loading state." (defun eca-chat--key-pressed-tab () "Expand tool call if point is at expandable content, or use default behavior." (interactive) - (eca-chat--with-current-buffer (eca-chat--get-buffer (eca-session)) - (cond - ;; expandable toggle - ((eca-chat--expandable-content-at-point) - (eca-chat--expandable-content-toggle (overlay-get (eca-chat--expandable-content-at-point) 'eca-chat--expandable-content-id))) + (eca-chat--with-current-buffer + (eca-chat--get-buffer (eca-session)) + (cond + ;; expandable toggle + ((eca-chat--expandable-content-at-point) + (eca-chat--expandable-content-toggle (overlay-get (eca-chat--expandable-content-at-point) 'eca-chat--expandable-content-id))) - ;; context completion - ((and (eca-chat--prompt-context-field-ov) - (eolp)) - (completion-at-point)) + ;; context completion + ((and (eca-chat--prompt-context-field-ov) + (eolp)) + (completion-at-point)) - (t t)))) + (t t)))) (defun eca-chat--prompt-field-ov () "Return the overlay for the prompt field." @@ -588,54 +598,56 @@ the prompt/context line." (defun eca-chat--send-prompt (session prompt) "Send PROMPT to server for SESSION." - (eca-chat--with-current-buffer (eca-chat--get-buffer session) - (let* ((prompt-start (eca-chat--prompt-field-start-point)) - (refined-contexts (-map #'eca-chat--refine-context eca-chat--context))) - (when (seq-empty-p eca-chat--history) (eca-chat--clear session)) - (add-to-list 'eca-chat--history prompt) - (setq eca-chat--history-index -1) - (goto-char prompt-start) - (delete-region (point) (point-max)) - (eca-chat--set-chat-loading session t) - (eca-api-request-async - session - :method "chat/prompt" - :params (list :message prompt - :request-id (cl-incf eca-chat--last-request-id) - :chatId eca-chat--id - :model (eca-chat--model session) - :behavior (eca-chat--behavior session) - :contexts (vconcat refined-contexts)) - :success-callback (-lambda (res) - (setq-local eca-chat--id (plist-get res :chatId))))))) + (eca-chat--with-current-buffer + (eca-chat--get-buffer session) + (let* ((prompt-start (eca-chat--prompt-field-start-point)) + (refined-contexts (-map #'eca-chat--refine-context eca-chat--context))) + (when (seq-empty-p eca-chat--history) (eca-chat--clear session)) + (add-to-list 'eca-chat--history prompt) + (setq eca-chat--history-index -1) + (goto-char prompt-start) + (delete-region (point) (point-max)) + (eca-chat--set-chat-loading session t) + (eca-api-request-async + session + :method "chat/prompt" + :params (list :message prompt + :request-id (cl-incf eca-chat--last-request-id) + :chatId eca-chat--id + :model (eca-chat--model session) + :behavior (eca-chat--behavior session) + :contexts (vconcat refined-contexts)) + :success-callback (-lambda (res) + (setq-local eca-chat--id (plist-get res :chatId))))))) (defun eca-chat--key-pressed-return () "Send the current prompt to eca process if in prompt." (interactive) - (eca-chat--with-current-buffer (eca-chat--get-buffer (eca-session)) - (let* ((prompt-start (eca-chat--prompt-field-start-point)) - (session (eca-session)) - (prompt (save-excursion - (goto-char prompt-start) - (string-trim (buffer-substring-no-properties (point) (point-max)))))) - (cond - ;; check prompt - ((and (not (string-empty-p prompt)) - (not eca-chat--chat-loading)) - (eca-chat--send-prompt session prompt)) - - ;; check it's an actionable text - ((-some->> (thing-at-point 'symbol) (get-text-property 0 'eca-button-on-action)) - (-some->> (thing-at-point 'symbol) - (get-text-property 0 'eca-button-on-action) - (funcall))) - - ;; check is inside a expandable text - ((eca-chat--expandable-content-at-point) - (let ((ov (eca-chat--expandable-content-at-point))) - (eca-chat--expandable-content-toggle (overlay-get ov 'eca-chat--expandable-content-id)))) - - (t nil))))) + (eca-chat--with-current-buffer + (eca-chat--get-buffer (eca-session)) + (let* ((prompt-start (eca-chat--prompt-field-start-point)) + (session (eca-session)) + (prompt (save-excursion + (goto-char prompt-start) + (string-trim (buffer-substring-no-properties (point) (point-max)))))) + (cond + ;; check prompt + ((and (not (string-empty-p prompt)) + (not eca-chat--chat-loading)) + (eca-chat--send-prompt session prompt)) + + ;; check it's an actionable text + ((-some->> (thing-at-point 'symbol) (get-text-property 0 'eca-button-on-action)) + (-some->> (thing-at-point 'symbol) + (get-text-property 0 'eca-button-on-action) + (funcall))) + + ;; check is inside a expandable text + ((eca-chat--expandable-content-at-point) + (let ((ov (eca-chat--expandable-content-at-point))) + (eca-chat--expandable-content-toggle (overlay-get ov 'eca-chat--expandable-content-id)))) + + (t nil))))) (defun eca-chat--point-at-new-context-p () "Return non-nil if point is at the context area." @@ -1061,11 +1073,12 @@ If FORCE? decide to OPEN? or not." (defun eca-chat--go-to-overlay (ov-key range-min range-max first?) "Go to overlay finding from RANGE-MIN to RANGE-MAX if matches OV-KEY." - (eca-chat--with-current-buffer (eca-chat--get-buffer (eca-session)) - (let ((get-fn (if first? #'-first #'-last))) - (when-let ((ov (funcall get-fn (-lambda (ov) (overlay-get ov ov-key)) - (overlays-in range-min range-max)))) - (goto-char (overlay-start ov)))))) + (eca-chat--with-current-buffer + (eca-chat--get-buffer (eca-session)) + (let ((get-fn (if first? #'-first #'-last))) + (when-let ((ov (funcall get-fn (-lambda (ov) (overlay-get ov ov-key)) + (overlays-in range-min range-max)))) + (goto-char (overlay-start ov)))))) (defun eca-chat--cur-position () "Return the start and end positions for current point. @@ -1096,10 +1109,10 @@ of (LINE . CHARACTER) representing the current selection or cursor position." ((start-line . start-character) start) ((end-line . end-character) end)) (eca-chat--with-current-buffer buffer - (setq eca-chat--cursor-context - (list :path path - :position (list :start (list :line start-line :character start-character) - :end (list :line end-line :character end-character)))))))))))) + (setq eca-chat--cursor-context + (list :path path + :position (list :start (list :line start-line :character start-character) + :end (list :line end-line :character end-character)))))))))))) (defun eca-chat--track-cursor-position-schedule () "Debounce `eca-chat--track-cursor' via an idle timer." @@ -1111,6 +1124,13 @@ of (LINE . CHARACTER) representing the current selection or cursor position." (declare-function evil-delete-backward-word "evil" ()) (declare-function evil-delete-back-to-indentation "evil" ()) +(defun eca-chat--detect-final-tool-prepare-p (args-text) + "Heuristic to detect if this is the final toolCallPrepare message." + (and (stringp args-text) + (or (string-suffix-p "}" args-text) + (string-suffix-p "}\"" args-text) + (string-match-p "\\s*}\\s*$" args-text)))) + (defun eca-chat--parse-unified-diff (diff-text) "Compatibility wrapper that delegates to `eca-diff-parse-unified-diff'. @@ -1186,15 +1206,16 @@ restore the chat display after smerge quits." 0.05 nil (lambda () - (eca-chat--with-current-buffer (eca-chat--get-buffer (eca-session)) - (display-line-numbers-mode -1) - (when (fboundp 'vi-tilde-fringe-mode) (vi-tilde-fringe-mode -1)) - (when (fboundp 'company-mode) - (setq-local company-backends '(company-capf) - company-minimum-prefix-length 0)) - (setq-local mode-line-format '(t (:eval (eca-chat--mode-line-string)))) - (force-mode-line-update) - (run-hooks 'eca-chat-mode-hook))))) + (eca-chat--with-current-buffer + (eca-chat--get-buffer (eca-session)) + (display-line-numbers-mode -1) + (when (fboundp 'vi-tilde-fringe-mode) (vi-tilde-fringe-mode -1)) + (when (fboundp 'company-mode) + (setq-local company-backends '(company-capf) + company-minimum-prefix-length 0)) + (setq-local mode-line-format '(t (:eval (eca-chat--mode-line-string)))) + (force-mode-line-update) + (run-hooks 'eca-chat-mode-hook))))) (face-remap-add-relative 'markdown-line-break-face '(:underline nil)) @@ -1301,314 +1322,316 @@ string." (let* ((role (plist-get params :role)) (content (plist-get params :content)) (roots (eca--session-workspace-folders session))) - (eca-chat--with-current-buffer (eca-chat--get-buffer session) - (setq-local eca-chat--empty nil) - (pcase (plist-get content :type) - - ("text" - (when-let* ((text (plist-get content :text))) - (pcase role - ("user" - (eca-chat--add-text-content - (propertize text - 'font-lock-face 'eca-chat-user-messages-face - 'line-prefix (propertize eca-chat-prompt-prefix - 'font-lock-face 'eca-chat-user-messages-face) - 'line-spacing 10) - 'eca-chat--user-message-id eca-chat--last-request-id) - (eca-chat--mark-header) - (font-lock-ensure)) - ("system" - (eca-chat--add-text-content - (propertize text - 'font-lock-face 'eca-chat-system-messages-face - 'line-height 20))) - (_ - (eca-chat--add-text-content text))))) - - ("url" - (eca-chat--add-header - (concat "🌐 " - (eca-buttonize - (plist-get content :title) - (lambda () (browse-url (plist-get content :url)))) - "\n\n"))) - - ("reasonStarted" - (let ((id (plist-get content :id)) - (label (propertize "Thinking..." 'font-lock-face 'eca-chat-reason-label-face))) - (eca-chat--add-expandable-content id label ""))) - - ("reasonText" - (let ((id (plist-get content :id)) - (label (propertize "Thinking..." 'font-lock-face 'eca-chat-reason-label-face)) - (text (plist-get content :text))) - (eca-chat--update-expandable-content id label text t))) - - ("reasonFinished" - (let* ((id (plist-get content :id)) - (base (propertize "Thought" 'font-lock-face 'eca-chat-reason-label-face)) - (time (when-let ((ms (plist-get content :totalTimeMs))) - (concat " " (eca-chat--time->presentable-time ms)))) - (label (concat base time))) - (eca-chat--update-expandable-content id label "" t))) - - ("toolCallPrepare" - (let* ((id (plist-get content :id)) - (name (plist-get content :name)) - (argsText (plist-get content :argumentsText)) - (summary (or (plist-get content :summary) - (format "Preparing tool: %s" name))) - (label (concat (propertize summary - 'font-lock-face 'eca-chat-mcp-tool-call-label-face) - " " eca-chat-mcp-tool-call-loading-symbol)) - (current-count (gethash id eca-chat--tool-prepare-counters 0)) - (should-process - (pcase eca-chat-tool-prepare-throttle - ('all t) - ('smart - (or (= current-count 0) - (and (integerp eca-chat-tool-prepare-update-interval) - (> eca-chat-tool-prepare-update-interval 0) - (= (mod current-count eca-chat-tool-prepare-update-interval) 0)))) - (_ t)))) - ;; update counter and only perform the expensive UI updates when - ;; `should-process' is true. This reduces churn for high-frequency - ;; streaming `toolCallPrepare' events. - (puthash id (1+ current-count) eca-chat--tool-prepare-counters) - (when should-process - (if (eca-chat--get-expandable-content id) - (eca-chat--update-expandable-content id label argsText t) - (eca-chat--add-expandable-content id label - (eca-chat--content-table - `(("Tool" . ,name) - ("Arguments" . ,argsText)))))))) - - ("toolCallRun" - (let* ((id (plist-get content :id)) - (name (plist-get content :name)) - (summary (or (plist-get content :summary) - (format "Calling tool: %s" name))) - (manual? (plist-get content :manualApproval)) - (approvalText (when manual? - (concat (eca-buttonize - (propertize "reject" - 'line-prefix (make-string (1+ (length eca-chat-expandable-block-open-symbol)) ?\s) - 'font-lock-face 'eca-chat-tool-call-cancel-face) - (lambda () - (eca-api-notify session - :method "chat/toolCallReject" - :params (list :chatId eca-chat--id - :toolCallId id)))) - " " - (eca-buttonize - (propertize "accept" 'font-lock-face 'eca-chat-tool-call-run-face) - (lambda () - (eca-api-notify session - :method "chat/toolCallApprove" - :params (list :chatId eca-chat--id - :toolCallId id))))))) - (details (plist-get content :details))) - (if (and (stringp (plist-get details :type)) - (string= "fileChange" (plist-get details :type))) - (let* ((path (plist-get details :path)) - (diff (plist-get details :diff)) - (view-btn (eca-buttonize - (propertize "view_diff" 'font-lock-face 'eca-chat-diff-view-face) - `(lambda () - (interactive) - (eca-chat--show-diff ,path ,diff))))) - (eca-chat--update-expandable-content - id - (concat (propertize summary 'font-lock-face 'eca-chat-mcp-tool-call-label-face) - " " - (eca-chat--file-change-details-label details) - eca-chat-mcp-tool-call-loading-symbol - "\n" - approvalText " " view-btn) - (concat "Tool: `" name "`\n" - (eca-chat--file-change-diff path diff roots)))) - (eca-chat--update-expandable-content - id - (concat (propertize summary 'font-lock-face 'eca-chat-mcp-tool-call-label-face) - " " eca-chat-mcp-tool-call-loading-symbol - "\n" - approvalText) - (eca-chat--content-table - `(("Tool" . ,name) - ("Arguments" . ,(plist-get content :arguments)))))))) - - ("toolCallRunning" - (let* ((id (plist-get content :id)) - (name (plist-get content :name)) - (summary (or (plist-get content :summary) - (format "Running tool: %s" name))) - (details (plist-get content :details)) - (status eca-chat-mcp-tool-call-loading-symbol)) - (if (and (stringp (plist-get details :type)) - (string= "fileChange" (plist-get details :type))) - (let* ((path (plist-get details :path)) - (diff (plist-get details :diff)) - (view-btn - (concat " " - (eca-buttonize + (eca-chat--with-current-buffer + (eca-chat--get-buffer session) + (setq-local eca-chat--empty nil) + (pcase (plist-get content :type) + ("text" + (when-let* ((text (plist-get content :text))) + (pcase role + ("user" + (eca-chat--add-text-content + (propertize text + 'font-lock-face 'eca-chat-user-messages-face + 'line-prefix (propertize eca-chat-prompt-prefix + 'font-lock-face 'eca-chat-user-messages-face) + 'line-spacing 10) + 'eca-chat--user-message-id eca-chat--last-request-id) + (eca-chat--mark-header) + (font-lock-ensure)) + ("system" + (eca-chat--add-text-content + (propertize text + 'font-lock-face 'eca-chat-system-messages-face + 'line-height 20))) + (_ (eca-chat--add-text-content text))))) + ("url" + (eca-chat--add-header + (concat "🌐 " + (eca-buttonize + (plist-get content :title) + (lambda () (browse-url (plist-get content :url)))) + "\n\n"))) + ("reasonStarted" + (let ((id (plist-get content :id)) + (label (propertize "Thinking..." 'font-lock-face 'eca-chat-reason-label-face))) + (eca-chat--add-expandable-content id label ""))) + ("reasonText" + (let ((id (plist-get content :id)) + (label (propertize "Thinking..." 'font-lock-face 'eca-chat-reason-label-face)) + (text (plist-get content :text))) + (eca-chat--update-expandable-content id label text t))) + ("reasonFinished" + (let* ((id (plist-get content :id)) + (base (propertize "Thought" 'font-lock-face 'eca-chat-reason-label-face)) + (time (when-let ((ms (plist-get content :totalTimeMs))) + (concat " " (eca-chat--time->presentable-time ms)))) + (label (concat base time))) + (eca-chat--update-expandable-content id label "" t))) + ("toolCallPrepare" + (let* ((id (plist-get content :id)) + (name (plist-get content :name)) + (argsText (plist-get content :argumentsText)) + (summary (or (plist-get content :summary) + (format "Preparing tool: %s" name))) + (current-count (gethash id eca-chat--tool-prepare-counters 0)) + (cached-content (gethash id eca-chat--tool-prepare-content-cache "")) + (new-content (concat cached-content argsText)) + (should-update-ui-p + (pcase eca-chat-tool-prepare-throttle + ('all t) + ('none nil) + ('first-last (or (= current-count 0) + (eca-chat--detect-final-tool-prepare-p argsText))) + ('smart (or (= current-count 0) + (= (mod current-count eca-chat-tool-prepare-update-interval) 0)))))) + (puthash id (1+ current-count) eca-chat--tool-prepare-counters) + (puthash id new-content eca-chat--tool-prepare-content-cache) + (puthash id (list :name name :summary summary) eca-chat--tool-prepare-metadata-cache) + (when should-update-ui-p + (let ((label (concat (propertize summary 'font-lock-face 'eca-chat-mcp-tool-call-label-face) + " " eca-chat-mcp-tool-call-loading-symbol))) + (if (eca-chat--get-expandable-content id) + (eca-chat--update-expandable-content + id label new-content nil) + (eca-chat--add-expandable-content + id label + (eca-chat--content-table + `(("Tool" . ,name) + ("Arguments" . ,new-content))))))))) + ("toolCallRun" + (let* ((id (plist-get content :id)) + (cached-metadata (gethash id eca-chat--tool-prepare-metadata-cache)) + (cached-args (gethash id eca-chat--tool-prepare-content-cache "")) + (name (or (plist-get content :name) + (plist-get cached-metadata :name))) + (summary (or (plist-get content :summary) + (plist-get cached-metadata :summary) + (format "Calling tool: %s" name))) + (manual? (plist-get content :manualApproval)) + (approvalText (when manual? + (concat (eca-buttonize + (propertize "reject" + 'line-prefix (make-string (1+ (length eca-chat-expandable-block-open-symbol)) ?\s) + 'font-lock-face 'eca-chat-tool-call-cancel-face) + (lambda () + (eca-api-notify session + :method "chat/toolCallReject" + :params (list :chatId eca-chat--id + :toolCallId id)))) + " " + (eca-buttonize + (propertize "accept" 'font-lock-face 'eca-chat-tool-call-run-face) + (lambda () + (eca-api-notify session + :method "chat/toolCallApprove" + :params (list :chatId eca-chat--id + :toolCallId id))))))) + (details (plist-get content :details))) + (if (and (stringp (plist-get details :type)) + (string= "fileChange" (plist-get details :type))) + (let* ((path (plist-get details :path)) + (diff (plist-get details :diff)) + (view-btn (eca-buttonize (propertize "view_diff" 'font-lock-face 'eca-chat-diff-view-face) `(lambda () (interactive) - (eca-chat--show-diff ,path ,diff)))))) - (eca-chat--update-expandable-content - id - (concat (propertize summary 'font-lock-face 'eca-chat-mcp-tool-call-label-face) - " " - (eca-chat--file-change-details-label details) - status - view-btn) - (concat "Tool: `" name "`\n" - (eca-chat--file-change-diff path diff roots)))) - (eca-chat--update-expandable-content - id - (concat (propertize summary 'font-lock-face 'eca-chat-mcp-tool-call-label-face) - " " status) - (eca-chat--content-table - `(("Tool" . ,name) - ("Arguments" . ,(plist-get content :arguments)))))))) - - ("toolCalled" - (let* ((id (plist-get content :id)) - (name (plist-get content :name)) - (summary (or (plist-get content :summary) - (format "Called tool: %s" name))) - (outputs (plist-get content :outputs)) - (output-text (if outputs - (mapconcat (lambda (o) (or (plist-get o :text) "")) outputs "\n") - "")) - (details (plist-get content :details)) - (status (if (plist-get content :error) - eca-chat-mcp-tool-call-error-symbol - eca-chat-mcp-tool-call-success-symbol))) - ;; Cleanup counters for this tool-call id to avoid unbounded growth - (remhash id eca-chat--tool-prepare-counters) - (if (and (stringp (plist-get details :type)) - (string= "fileChange" (plist-get details :type))) - (let* ((path (plist-get details :path)) - (diff (plist-get details :diff)) - (view-btn - (concat " " - (eca-buttonize - (propertize "view_diff" 'font-lock-face 'eca-chat-diff-view-face) - `(lambda () - (interactive) - (eca-chat--show-diff ,path ,diff)))))) - (eca-chat--update-expandable-content - id - (concat (propertize summary 'font-lock-face 'eca-chat-mcp-tool-call-label-face) - " " status - (eca-chat--file-change-details-label details) - view-btn) - (concat "Tool: `" name "`\n" - (eca-chat--file-change-diff path diff roots)))) - (eca-chat--update-expandable-content - id - (concat (propertize summary 'font-lock-face 'eca-chat-mcp-tool-call-label-face) - " " status) - (eca-chat--content-table - `(("Tool" . ,name) - ("Output" . ,output-text))))))) - - ("toolCallRejected" - (let* ((name (plist-get content :name)) - (origin (plist-get content :origin)) - (args (plist-get content :arguments)) - (details (plist-get content :details)) - (summary (plist-get content :summary)) - (id (plist-get content :id))) - (if (string= "fileChange" (plist-get details :type)) - (eca-chat--update-expandable-content - id - (concat (propertize summary 'font-lock-face 'eca-chat-mcp-tool-call-label-face) - " " - (eca-chat--file-change-details-label details) - eca-chat-mcp-tool-call-error-symbol) - (concat - "Tool: `" name "`\n" - (eca-chat--file-change-diff (plist-get details :path) (plist-get details :diff) roots))) - (eca-chat--update-expandable-content - id - (concat (propertize (format "Rejected %s tool: %s" - (if (string= "mcp" origin) "MCP" "ECA") - name) - 'font-lock-face 'eca-chat-mcp-tool-call-label-face) - " " - eca-chat-mcp-tool-call-error-symbol) - (eca-chat--content-table `(("Tool" . ,name) - ("Arguments" . ,args))))))) - - ("progress" - (pcase (plist-get content :state) - ("running" - (unless eca-chat--spinner-timer - (eca-chat--spinner-start session)) - (setq-local eca-chat--progress-text - (propertize (plist-get content :text) - 'font-lock-face 'eca-chat-system-messages-face))) - ("finished" - (eca-chat--spinner-stop) - (eca-chat--add-text-content "\n") - (eca-chat--set-chat-loading session nil) - (setq-local eca-chat--progress-text "")))) - - ("usage" - (setq-local eca-chat--message-input-tokens (plist-get content :messageInputTokens)) - (setq-local eca-chat--message-output-tokens (plist-get content :messageOutputTokens)) - (setq-local eca-chat--session-tokens (plist-get content :sessionTokens)) - (setq-local eca-chat--session-limit-context (plist-get (plist-get content :limit) :context)) - (setq-local eca-chat--session-limit-output (plist-get (plist-get content :limit) :output)) - (setq-local eca-chat--message-cost (plist-get content :messageCost)) - (setq-local eca-chat--session-cost (plist-get content :sessionCost))) - - (_ nil))))) + (eca-chat--show-diff ,path ,diff))))) + (eca-chat--update-expandable-content + id + (concat (propertize summary 'font-lock-face 'eca-chat-mcp-tool-call-label-face) + " " + (eca-chat--file-change-details-label details) + eca-chat-mcp-tool-call-loading-symbol + "\n" + approvalText " " view-btn) + (concat "Tool: `" name "`\n" + (eca-chat--file-change-diff path diff roots)))) + (eca-chat--update-expandable-content + id + (concat (propertize summary 'font-lock-face 'eca-chat-mcp-tool-call-label-face) + " " eca-chat-mcp-tool-call-loading-symbol + "\n" + approvalText) + (eca-chat--content-table + `(("Tool" . ,name) + ("Arguments" . ,cached-args))))))) + ("toolCallRunning" + (let* ((id (plist-get content :id)) + (cached-metadata (gethash id eca-chat--tool-prepare-metadata-cache)) + (cached-args (gethash id eca-chat--tool-prepare-content-cache "")) + (name (or (plist-get content :name) + (plist-get cached-metadata :name))) + (summary (or (plist-get content :summary) + (plist-get cached-metadata :summary) + (format "Running tool: %s" name))) + (details (plist-get content :details)) + (status eca-chat-mcp-tool-call-loading-symbol)) + (if (and (stringp (plist-get details :type)) + (string= "fileChange" (plist-get details :type))) + (let* ((path (plist-get details :path)) + (diff (plist-get details :diff)) + (view-btn + (concat " " + (eca-buttonize + (propertize "view_diff" 'font-lock-face 'eca-chat-diff-view-face) + `(lambda () + (interactive) + (eca-chat--show-diff ,path ,diff)))))) + (eca-chat--update-expandable-content + id + (concat (propertize summary 'font-lock-face 'eca-chat-mcp-tool-call-label-face) + " " + (eca-chat--file-change-details-label details) + status + view-btn) + (concat "Tool: `" name "`\n" + (eca-chat--file-change-diff path diff roots)))) + (eca-chat--update-expandable-content + id + (concat (propertize summary 'font-lock-face 'eca-chat-mcp-tool-call-label-face) + " " status) + (eca-chat--content-table + `(("Tool" . ,name) + ("Arguments" . ,cached-args))))))) + ("toolCalled" + (let* ((id (plist-get content :id)) + (name (plist-get content :name)) + (summary (or (plist-get content :summary) + (format "Called tool: %s" name))) + (outputs (plist-get content :outputs)) + (output-text (if outputs + (mapconcat (lambda (o) (or (plist-get o :text) "")) outputs "\n") + "")) + (details (plist-get content :details)) + (status (if (plist-get content :error) + eca-chat-mcp-tool-call-error-symbol + eca-chat-mcp-tool-call-success-symbol))) + (remhash id eca-chat--tool-prepare-counters) + (remhash id eca-chat--tool-prepare-content-cache) + (remhash id eca-chat--tool-prepare-metadata-cache) + (if (and (stringp (plist-get details :type)) + (string= "fileChange" (plist-get details :type))) + (let* ((path (plist-get details :path)) + (diff (plist-get details :diff)) + (view-btn + (concat " " + (eca-buttonize + (propertize "view_diff" 'font-lock-face 'eca-chat-diff-view-face) + `(lambda () + (interactive) + (eca-chat--show-diff ,path ,diff)))))) + (eca-chat--update-expandable-content + id + (concat (propertize summary 'font-lock-face 'eca-chat-mcp-tool-call-label-face) + " " status + (eca-chat--file-change-details-label details) + view-btn) + (concat "Tool: `" name "`\n" + (eca-chat--file-change-diff path diff roots)))) + (eca-chat--update-expandable-content + id + (concat (propertize summary 'font-lock-face 'eca-chat-mcp-tool-call-label-face) + " " status) + (eca-chat--content-table + `(("Tool" . ,name) + ("Output" . ,output-text))))))) + ("toolCallRejected" + (let* ((name (plist-get content :name)) + (origin (plist-get content :origin)) + (args (plist-get content :arguments)) + (details (plist-get content :details)) + (summary (plist-get content :summary)) + (id (plist-get content :id))) + (remhash id eca-chat--tool-prepare-counters) + (remhash id eca-chat--tool-prepare-content-cache) + (remhash id eca-chat--tool-prepare-metadata-cache) + (if (string= "fileChange" (plist-get details :type)) + (eca-chat--update-expandable-content + id + (concat (propertize summary 'font-lock-face 'eca-chat-mcp-tool-call-label-face) + " " + (eca-chat--file-change-details-label details) + eca-chat-mcp-tool-call-error-symbol) + (concat + "Tool: `" name "`\n" + (eca-chat--file-change-diff (plist-get details :path) (plist-get details :diff) roots))) + (eca-chat--update-expandable-content + id + (concat (propertize (format "Rejected %s tool: %s" + (if (string= "mcp" origin) "MCP" "ECA") + name) + 'font-lock-face 'eca-chat-mcp-tool-call-label-face) + " " + eca-chat-mcp-tool-call-error-symbol) + (eca-chat--content-table `(("Tool" . ,name) + ("Arguments" . ,args))))))) + ("progress" + (pcase (plist-get content :state) + ("running" + (unless eca-chat--spinner-timer + (eca-chat--spinner-start session)) + (setq-local eca-chat--progress-text + (propertize (plist-get content :text) + 'font-lock-face 'eca-chat-system-messages-face))) + ("finished" + (eca-chat--spinner-stop) + (eca-chat--add-text-content "\n") + (eca-chat--set-chat-loading session nil) + (setq-local eca-chat--progress-text "")))) + ("usage" + (setq-local eca-chat--message-input-tokens (plist-get content :messageInputTokens)) + (setq-local eca-chat--message-output-tokens (plist-get content :messageOutputTokens)) + (setq-local eca-chat--session-tokens (plist-get content :sessionTokens)) + (setq-local eca-chat--session-limit-context (plist-get (plist-get content :limit) :context)) + (setq-local eca-chat--session-limit-output (plist-get (plist-get content :limit) :output)) + (setq-local eca-chat--message-cost (plist-get content :messageCost)) + (setq-local eca-chat--session-cost (plist-get content :sessionCost))) + (_ nil))))) (defun eca-chat--handle-mcp-server-updated (session _server) "Handle mcp SERVER updated for SESSION." (eca-chat--with-current-buffer (eca-chat--get-buffer session) - (force-mode-line-update))) + (force-mode-line-update))) (defun eca-chat-open (session) "Open or create dedicated eca chat window for SESSION." (eca-assert-session-running session) (unless (buffer-live-p (eca-chat--get-buffer session)) (eca-chat--create-buffer session)) - (eca-chat--with-current-buffer (eca-chat--get-buffer session) - (unless (derived-mode-p 'eca-chat-mode) - (eca-chat-mode) - (eca-chat--track-cursor-position-schedule) - (when eca-chat-auto-add-cursor - (eca-chat--add-context (list :type "cursor"))) - (when eca-chat-auto-add-repomap - (eca-chat--add-context (list :type "repoMap")))) - (unless (eca--session-chat session) - (setf (eca--session-chat session) (current-buffer))) - (if (window-live-p (get-buffer-window (buffer-name))) - (eca-chat--select-window) - (eca-chat--pop-window))) + (eca-chat--with-current-buffer + (eca-chat--get-buffer session) + (unless (derived-mode-p 'eca-chat-mode) + (eca-chat-mode) + (eca-chat--track-cursor-position-schedule) + (when eca-chat-auto-add-cursor + (eca-chat--add-context (list :type "cursor"))) + (when eca-chat-auto-add-repomap + (eca-chat--add-context (list :type "repoMap")))) + (unless (eca--session-chat session) + (setf (eca--session-chat session) (current-buffer))) + (if (window-live-p (get-buffer-window (buffer-name))) + (eca-chat--select-window) + (eca-chat--pop-window))) (eca-chat--track-cursor)) (defun eca-chat-exit (session) "Exit the ECA chat for SESSION." (when (buffer-live-p (get-buffer (eca-chat-buffer-name session))) - (eca-chat--with-current-buffer (eca-chat--get-buffer session) - (setq eca-chat--closed t) - (force-mode-line-update) - (goto-char (point-max)) - (rename-buffer (concat (buffer-name) ":closed") t) - ;; Keep only the most recently closed chat buffer; kill older ones. - (let ((current (current-buffer))) - (dolist (b (buffer-list)) - (when (and (not (eq b current)) - (string-match-p "^:closed$" (buffer-name b))) - (kill-buffer b)))) - (when-let* ((window (get-buffer-window (eca-chat--get-buffer session)))) - (quit-window nil window))))) + (eca-chat--with-current-buffer + (eca-chat--get-buffer session) + (setq eca-chat--closed t) + (force-mode-line-update) + (goto-char (point-max)) + (rename-buffer (concat (buffer-name) ":closed") t) + ;; Keep only the most recently closed chat buffer; kill older ones. + (let ((current (current-buffer))) + (dolist (b (buffer-list)) + (when (and (not (eq b current)) + (string-match-p "^:closed$" (buffer-name b))) + (kill-buffer b)))) + (when-let* ((window (get-buffer-window (eca-chat--get-buffer session)))) + (quit-window nil window))))) ;;;###autoload (defun eca-chat-clear () @@ -1654,6 +1677,8 @@ string." (setq-local eca-chat--track-context t) ;; Reset per-buffer tool prepare counters to avoid leaking across sessions (setq-local eca-chat--tool-prepare-counters (make-hash-table :test 'equal)) + (setq-local eca-chat--tool-prepare-content-cache (make-hash-table :test 'equal)) + (setq-local eca-chat--tool-prepare-metadata-cache (make-hash-table :test 'equal)) (eca-chat--clear (eca-session)))) ;;;###autoload @@ -1690,10 +1715,10 @@ string." (interactive) (eca-assert-session-running (eca-session)) (eca-chat--with-current-buffer (eca-chat--get-buffer (eca-session)) - (unless (eca-chat--expandable-content-at-point) - (eca-chat-go-to-prev-expandable-block)) - (when-let ((ov (eca-chat--expandable-content-at-point))) - (eca-chat--expandable-content-toggle (overlay-get ov 'eca-chat--expandable-content-id))))) + (unless (eca-chat--expandable-content-at-point) + (eca-chat-go-to-prev-expandable-block)) + (when-let ((ov (eca-chat--expandable-content-at-point))) + (eca-chat--expandable-content-toggle (overlay-get ov 'eca-chat--expandable-content-id))))) ;;;###autoload (defun eca-chat-add-context-at-point () @@ -1706,10 +1731,11 @@ Consider the defun at point unless a region is selected." (-let (((s . e) (bounds-of-thing-at-point 'defun))) `(,(line-number-at-pos s) . ,(line-number-at-pos e))))) (path (buffer-file-name))) - (eca-chat--with-current-buffer (eca-chat--get-buffer (eca-session)) - (eca-chat--add-context (list :type "file" - :path path - :linesRange (list :start start :end end)))))) + (eca-chat--with-current-buffer + (eca-chat--get-buffer (eca-session)) + (eca-chat--add-context (list :type "file" + :path path + :linesRange (list :start start :end end)))))) ;;;###autoload (defun eca-chat-add-file-context (&optional arg) @@ -1720,10 +1746,11 @@ if ARG is current prefix, ask for file, otherwise add current file." (-let ((path (if (equal arg '(4)) (read-file-name "Select the file to add to context: " (eca-find-root-for-buffer)) (buffer-file-name)))) - (eca-chat--with-current-buffer (eca-chat--get-buffer (eca-session)) - (eca-chat--add-context (list :type "file" - :path path)) - (eca-chat-open (eca-session))))) + (eca-chat--with-current-buffer + (eca-chat--get-buffer (eca-session)) + (eca-chat--add-context (list :type "file" + :path path)) + (eca-chat-open (eca-session))))) ;;;###autoload (defun eca-chat-drop-file-context (&optional arg) @@ -1734,10 +1761,11 @@ if ARG is current prefix, ask for file, otherwise add current file." (-let ((path (if (equal arg '(4)) (read-file-name "Select the file to drop from context: " (eca-find-root-for-buffer)) (buffer-file-name)))) - (eca-chat--with-current-buffer (eca-chat--get-buffer (eca-session)) - (eca-chat--remove-context (list :type "file" - :path path)) - (eca-chat-open (eca-session))))) + (eca-chat--with-current-buffer + (eca-chat--get-buffer (eca-session)) + (eca-chat--remove-context (list :type "file" + :path path)) + (eca-chat-open (eca-session))))) ;;;###autoload (defun eca-chat-send-prompt (prompt) @@ -1750,15 +1778,16 @@ if ARG is current prefix, ask for file, otherwise add current file." (defun eca-chat-send-prompt-at-chat () "Send the prompt in chat if not empty." (interactive) - (eca-chat--with-current-buffer (eca-chat--get-buffer (eca-session)) - (let* ((prompt-start (eca-chat--prompt-field-start-point)) - (session (eca-session)) - (prompt (save-excursion - (goto-char prompt-start) - (string-trim (buffer-substring-no-properties (point) (point-max)))))) - (when (and (not (string-empty-p prompt)) - (not eca-chat--chat-loading)) - (eca-chat--send-prompt session prompt))))) + (eca-chat--with-current-buffer + (eca-chat--get-buffer (eca-session)) + (let* ((prompt-start (eca-chat--prompt-field-start-point)) + (session (eca-session)) + (prompt (save-excursion + (goto-char prompt-start) + (string-trim (buffer-substring-no-properties (point) (point-max)))))) + (when (and (not (string-empty-p prompt)) + (not eca-chat--chat-loading)) + (eca-chat--send-prompt session prompt))))) ;;;###autoload (defun eca-chat-toggle-window () @@ -1791,8 +1820,9 @@ if ARG is current prefix, ask for file, otherwise add current file." (let ((session (eca-session))) (eca-assert-session-running session) (eca-chat-open session) - (eca-chat--with-current-buffer (eca-chat--get-buffer session) - (goto-char (point-max))) + (eca-chat--with-current-buffer + (eca-chat--get-buffer session) + (goto-char (point-max))) (let ((buffer (get-buffer-create "*whisper-stdout*"))) (with-current-buffer buffer (erase-buffer) @@ -1802,10 +1832,11 @@ if ARG is current prefix, ask for file, otherwise add current file." (let ((transcription (buffer-substring (line-beginning-position) (line-end-position)))) - (eca-chat--with-current-buffer (eca-chat-buffer-name session) - (insert transcription) - (newline) - (eca-chat--key-pressed-return)))) + (eca-chat--with-current-buffer + (eca-chat-buffer-name session) + (insert transcription) + (newline) + (eca-chat--key-pressed-return)))) nil t) (whisper-run) (eca-info "Recording audio. Press RET when you are done.") From 5eefd71936b2eccceb1871784eef4423d00074cf Mon Sep 17 00:00:00 2001 From: "hadoopchetan@gmail.com" Date: Sun, 14 Sep 2025 14:02:54 -0400 Subject: [PATCH 4/6] fix(eca-chat): correct typo in last-message-cost option --- eca-chat.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eca-chat.el b/eca-chat.el index f504715..7cee143 100644 --- a/eca-chat.el +++ b/eca-chat.el @@ -138,7 +138,7 @@ Must be a valid model supported by server, check `eca-chat-select-model`." (const :tag "Total session cost" :session-cost) (const :tag "The context limit" :context-limit) (const :tag "The output limit" :output-limit) - (const :tag "Last message cost" :last-mesage-cost))) + (const :tag "Last message cost" :last-message-cost))) :group 'eca) (defcustom eca-chat-diff-tool 'ediff From 7bdf51e24c1c10c85acbbcdbbdeff3b70d7e582e Mon Sep 17 00:00:00 2001 From: CsBigDataHub Date: Mon, 15 Sep 2025 11:33:23 -0400 Subject: [PATCH 5/6] Update README.md with flyspell issue --- README.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/README.md b/README.md index 217a42e..efc4544 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,44 @@ Install and configure `exec-path-from-shell` to import your shell environment in (exec-path-from-shell-initialize))) ``` +##### Flyspell Performance in ECA Chat + +see - this [comment](https://github.com/editor-code-assistant/eca-emacs/pull/42#issuecomment-3292134511) + +If Flyspell is causing slowdowns during LLM streaming, you can enable spell-checking only while typing and disable it on submit by adding this to your personal Emacs config: + + +``` emacs-lisp + +(defun my/eca-chat-flyspell-setup () + "Enable Flyspell during typing and disable on submit in `eca-chat-mode`." + (when (derived-mode-p 'eca-chat-mode) + ;; Disable Flyspell when submitting prompts + (add-hook 'pre-command-hook + (lambda () + (when (and (memq this-command '(eca-chat--key-pressed-return + eca-chat-send-prompt-at-chat)) + flyspell-mode) + (flyspell-mode -1))) + nil t) + ;; Re-enable Flyspell when typing + (add-hook 'pre-command-hook + (lambda () + (when (and (eq this-command 'self-insert-command) + (not flyspell-mode)) + (flyspell-mode 1))) + nil t))) + +(add-hook 'eca-chat-mode-hook #'my/eca-chat-flyspell-setup) +``` + +How it works: + +Submit (Enter/Return): Disables Flyspell just before sending your prompt or programmatic send, preventing spell-checking overhead during streaming. + +Typing: Re-enables Flyspell on any character insertion (self-insert-command), giving you real-time spell checking while composing. + + ### ECA Server Connection Issues #### Problem: ECA server fails to start or connect From 0fd24ff8cbca66f1051e589b61bc666228e84639 Mon Sep 17 00:00:00 2001 From: CsBigDataHub Date: Mon, 15 Sep 2025 11:34:21 -0400 Subject: [PATCH 6/6] rename tool-call variables and functions --- eca-chat.el | 54 ++++++++++++++++++++++++++--------------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/eca-chat.el b/eca-chat.el index 7cee143..45c57e4 100644 --- a/eca-chat.el +++ b/eca-chat.el @@ -147,26 +147,26 @@ Must be a valid model supported by server, check `eca-chat-select-model`." (const :tag "Merge-style Smerge" smerge)) :group 'eca) -(defcustom eca-chat-tool-prepare-throttle 'smart +(defcustom eca-chat-tool-call-prepare-throttle 'smart "Throttle strategy for handling `toolCallPrepare` events. Possible values: `all` or `smart` (default)." :type '(choice (const :tag "Process all updates" all) (const :tag "Smart throttle" smart)) :group 'eca) -(defcustom eca-chat-tool-prepare-update-interval 5 +(defcustom eca-chat-tool-call-prepare-update-interval 5 "When `smart`, process every Nth `toolCallPrepare` update. Must be a positive integer." :type 'integer :group 'eca) -(defvar-local eca-chat--tool-prepare-counters (make-hash-table :test 'equal) +(defvar-local eca-chat--tool-call-prepare-counters (make-hash-table :test 'equal) "Hash table mapping toolCall ID to message count.") -(defvar-local eca-chat--tool-prepare-content-cache (make-hash-table :test 'equal) +(defvar-local eca-chat--tool-call-prepare-content-cache (make-hash-table :test 'equal) "Hash table mapping toolCall ID to accumulated argument text.") -(defvar-local eca-chat--tool-prepare-metadata-cache (make-hash-table :test 'equal) +(defvar-local eca-chat--tool-call-prepare-metadata-cache (make-hash-table :test 'equal) "Hash table mapping toolCall ID to tool metadata (name, summary, etc).") @@ -1123,7 +1123,7 @@ of (LINE . CHARACTER) representing the current selection or cursor position." (declare-function evil-delete-backward-word "evil" ()) (declare-function evil-delete-back-to-indentation "evil" ()) -(defun eca-chat--detect-final-tool-prepare-p (args-text) +(defun eca-chat--detect-final-tool-call-prepare-p (args-text) "Heuristic to detect if this is the final toolCallPrepare message." (and (stringp args-text) (or (string-suffix-p "}" args-text) @@ -1374,21 +1374,21 @@ string." (argsText (plist-get content :argumentsText)) (summary (or (plist-get content :summary) (format "Preparing tool: %s" name))) - (current-count (gethash id eca-chat--tool-prepare-counters 0)) - (cached-content (gethash id eca-chat--tool-prepare-content-cache "")) + (current-count (gethash id eca-chat--tool-call-prepare-counters 0)) + (cached-content (gethash id eca-chat--tool-call-prepare-content-cache "")) (new-content (concat cached-content argsText)) (should-update-ui-p - (pcase eca-chat-tool-prepare-throttle + (pcase eca-chat-tool-call-prepare-throttle ('all t) ('none nil) ('first-last (or (= current-count 0) - (eca-chat--detect-final-tool-prepare-p argsText))) + (eca-chat--detect-final-tool-call-prepare-p argsText))) ('smart (or (= current-count 0) - (= (mod current-count eca-chat-tool-prepare-update-interval) 0)))))) + (= (mod current-count eca-chat-tool-call-prepare-update-interval) 0)))))) ;; Always cache the metadata and content - (puthash id (1+ current-count) eca-chat--tool-prepare-counters) - (puthash id new-content eca-chat--tool-prepare-content-cache) - (puthash id (list :name name :summary summary) eca-chat--tool-prepare-metadata-cache) + (puthash id (1+ current-count) eca-chat--tool-call-prepare-counters) + (puthash id new-content eca-chat--tool-call-prepare-content-cache) + (puthash id (list :name name :summary summary) eca-chat--tool-call-prepare-metadata-cache) ;; Only update UI when throttling permits (when should-update-ui-p (let ((label (concat (propertize summary 'font-lock-face 'eca-chat-mcp-tool-call-label-face) @@ -1404,8 +1404,8 @@ string." ("Arguments" . ,new-content))))))))) ("toolCallRun" (let* ((id (plist-get content :id)) - (cached-metadata (gethash id eca-chat--tool-prepare-metadata-cache)) - (cached-args (gethash id eca-chat--tool-prepare-content-cache "")) + (cached-metadata (gethash id eca-chat--tool-call-prepare-metadata-cache)) + (cached-args (gethash id eca-chat--tool-call-prepare-content-cache "")) (name (or (plist-get content :name) (plist-get cached-metadata :name))) (summary (or (plist-get content :summary) @@ -1463,8 +1463,8 @@ string." ("Arguments" . ,cached-args))))))) ("toolCallRunning" (let* ((id (plist-get content :id)) - (cached-metadata (gethash id eca-chat--tool-prepare-metadata-cache)) - (cached-args (gethash id eca-chat--tool-prepare-content-cache "")) + (cached-metadata (gethash id eca-chat--tool-call-prepare-metadata-cache)) + (cached-args (gethash id eca-chat--tool-call-prepare-content-cache "")) (name (or (plist-get content :name) (plist-get cached-metadata :name))) (summary (or (plist-get content :summary) @@ -1514,9 +1514,9 @@ string." eca-chat-mcp-tool-call-error-symbol eca-chat-mcp-tool-call-success-symbol))) ;; Cleanup counters for this tool-call id to avoid unbounded growth - (remhash id eca-chat--tool-prepare-counters) - (remhash id eca-chat--tool-prepare-content-cache) - (remhash id eca-chat--tool-prepare-metadata-cache) + (remhash id eca-chat--tool-call-prepare-counters) + (remhash id eca-chat--tool-call-prepare-content-cache) + (remhash id eca-chat--tool-call-prepare-metadata-cache) (if (and (stringp (plist-get details :type)) (string= "fileChange" (plist-get details :type))) (let* ((path (plist-get details :path)) @@ -1551,9 +1551,9 @@ string." (summary (plist-get content :summary)) (id (plist-get content :id))) ;; Cleanup counters for this tool-call id - (remhash id eca-chat--tool-prepare-counters) - (remhash id eca-chat--tool-prepare-content-cache) - (remhash id eca-chat--tool-prepare-metadata-cache) + (remhash id eca-chat--tool-call-prepare-counters) + (remhash id eca-chat--tool-call-prepare-content-cache) + (remhash id eca-chat--tool-call-prepare-metadata-cache) (if (string= "fileChange" (plist-get details :type)) (eca-chat--update-expandable-content id @@ -1684,9 +1684,9 @@ string." (setq-local eca-chat--empty t) (setq-local eca-chat--track-context t) ;; Reset per-buffer tool prepare counters to avoid leaking across sessions - (setq-local eca-chat--tool-prepare-counters (make-hash-table :test 'equal)) - (setq-local eca-chat--tool-prepare-content-cache (make-hash-table :test 'equal)) - (setq-local eca-chat--tool-prepare-metadata-cache (make-hash-table :test 'equal)) + (setq-local eca-chat--tool-call-prepare-counters (make-hash-table :test 'equal)) + (setq-local eca-chat--tool-call-prepare-content-cache (make-hash-table :test 'equal)) + (setq-local eca-chat--tool-call-prepare-metadata-cache (make-hash-table :test 'equal)) (eca-chat--clear (eca-session)))) ;;;###autoload