diff --git a/pi-coding-agent-menu.el b/pi-coding-agent-menu.el index 13bfbc8..e7f3e32 100644 --- a/pi-coding-agent-menu.el +++ b/pi-coding-agent-menu.el @@ -273,6 +273,7 @@ Used when starting a new session." (when-let* ((chat-buf (pi-coding-agent--get-chat-buffer))) (with-current-buffer chat-buf (let ((inhibit-read-only t)) + (pi-coding-agent--clear-render-artifacts) (erase-buffer) (insert (pi-coding-agent--format-startup-header)) (insert "\n") diff --git a/pi-coding-agent-render.el b/pi-coding-agent-render.el index 9307e2b..0a6106c 100644 --- a/pi-coding-agent-render.el +++ b/pi-coding-agent-render.el @@ -927,6 +927,16 @@ Returns a plist with: :hidden-lines (if (and truncated-first-line (= hidden 0)) 1 hidden) :line-map line-map-vec))))) +(defun pi-coding-agent--clear-render-artifacts () + "Delete pi-owned render overlays in the current chat buffer. +This removes completed/pending tool overlays and diff overlays before +buffer reset or history rebuild, then clears the pending overlay slot so +buffer state and overlay state stay consistent. Tree-sitter overlays are +left alone." + (remove-overlays (point-min) (point-max) 'pi-coding-agent-tool-block t) + (remove-overlays (point-min) (point-max) 'pi-coding-agent-diff-overlay t) + (setq pi-coding-agent--pending-tool-overlay nil)) + (defun pi-coding-agent--tool-overlay-create (tool-name &optional path) "Create overlay for tool block TOOL-NAME at point. Optional PATH stores the file path for navigation. @@ -1831,6 +1841,7 @@ Note: When called from async callbacks, pass CHAT-BUF explicitly." (when (and chat-buf (buffer-live-p chat-buf)) (with-current-buffer chat-buf (let ((inhibit-read-only t)) + (pi-coding-agent--clear-render-artifacts) (erase-buffer) (insert (pi-coding-agent--format-startup-header) "\n") (when (vectorp messages) diff --git a/test/pi-coding-agent-menu-test.el b/test/pi-coding-agent-menu-test.el index e45cb64..a84be6d 100644 --- a/test/pi-coding-agent-menu-test.el +++ b/test/pi-coding-agent-menu-test.el @@ -125,6 +125,37 @@ ;; Tool args cache should be empty (should (= 0 (hash-table-count pi-coding-agent--tool-args-cache))))) +(ert-deftest pi-coding-agent-test-clear-chat-buffer-removes-pi-owned-render-overlays () + "Clearing chat buffer removes stale pi-owned tool and diff overlays." + (with-temp-buffer + (pi-coding-agent-chat-mode) + (let ((inhibit-read-only t)) + (insert "tool\n+ 1 added\n- 2 removed\n")) + (let ((tool-ov (make-overlay 1 5 nil nil nil)) + (tool-count 0) + (diff-count 0)) + (overlay-put tool-ov 'pi-coding-agent-tool-block t) + (setq pi-coding-agent--pending-tool-overlay tool-ov) + (pi-coding-agent--apply-diff-overlays 6 (point-max)) + (dolist (ov (overlays-in (point-min) (point-max))) + (when (overlay-get ov 'pi-coding-agent-tool-block) + (setq tool-count (1+ tool-count))) + (when (overlay-get ov 'pi-coding-agent-diff-overlay) + (setq diff-count (1+ diff-count)))) + (should (= tool-count 1)) + (should (= diff-count 4))) + (pi-coding-agent--clear-chat-buffer) + (let ((tool-count 0) + (diff-count 0)) + (dolist (ov (overlays-in (point-min) (point-max))) + (when (overlay-get ov 'pi-coding-agent-tool-block) + (setq tool-count (1+ tool-count))) + (when (overlay-get ov 'pi-coding-agent-diff-overlay) + (setq diff-count (1+ diff-count)))) + (should (= tool-count 0)) + (should (= diff-count 0)) + (should-not pi-coding-agent--pending-tool-overlay)))) + (ert-deftest pi-coding-agent-test-new-session-clears-buffer-from-different-context () "New session clears buffer and updates state even when callback runs elsewhere. This tests that the async callback properly captures the chat buffer reference, diff --git a/test/pi-coding-agent-render-test.el b/test/pi-coding-agent-render-test.el index af75102..7fb7232 100644 --- a/test/pi-coding-agent-render-test.el +++ b/test/pi-coding-agent-render-test.el @@ -388,6 +388,33 @@ agent_end + next section's leading newline must not create triple newlines." (should (cl-some (lambda (ov) (overlay-get ov 'pi-coding-agent-tool-block)) (overlays-in (point-min) (point-max)))))) +(ert-deftest pi-coding-agent-test-display-session-history-does-not-accumulate-stale-tool-overlays () + "Rebuilding session history removes stale pi-owned tool overlays first." + (with-temp-buffer + (pi-coding-agent-chat-mode) + (let ((messages [(:role "assistant" + :content [(:type "text" :text "Let me check.") + (:type "toolCall" :id "tc1" + :name "bash" + :arguments (:command "ls -la"))] + :timestamp 1704067200000) + (:role "toolResult" :toolCallId "tc1" + :toolName "bash" + :content [(:type "text" :text "total 42")] + :isError :json-false + :timestamp 1704067201000)])) + (pi-coding-agent--display-session-history messages (current-buffer)) + (pi-coding-agent--display-session-history messages (current-buffer)) + (let ((tool-count 0) + (zero-tool-count 0)) + (dolist (ov (overlays-in (point-min) (point-max))) + (when (overlay-get ov 'pi-coding-agent-tool-block) + (setq tool-count (1+ tool-count)) + (when (= (overlay-start ov) (overlay-end ov)) + (setq zero-tool-count (1+ zero-tool-count))))) + (should (= tool-count 1)) + (should (= zero-tool-count 0)))))) + (ert-deftest pi-coding-agent-test-history-renders-multiple-tools-in-order () "Multiple tool calls render with headers and output in order." (with-temp-buffer