Skip to content

Commit fa2703b

Browse files
authored
Merge pull request #297 from agzam/restore-prompt-on-rollback
Restore rolled-back message into the prompt field
2 parents 231637c + 2e6143f commit fa2703b

3 files changed

Lines changed: 156 additions & 9 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Unreleased
44

5+
- Restore the rolled-back user message into the prompt field. `Rollback chat to before this message` (when messages are included) used to discard the clicked message's text together with the rest of the turn; the text now lands in the prompt unsent, ready to edit and resend, and any draft already typed in the prompt is kept below it, separated by a blank line.
56
- Fall back to the buffer's `xref` backend (eglot, etags with a loaded tags table, any custom backend) for `editor/getDefinition`/`editor/getReferences` when lsp-mode cannot serve the file, instead of answering no-server. `includeDeclaration: false` is honored by filtering definition locations out of the references, since LSP-backed xref backends always include the declaration. (editor-code-assistant/eca#351)
67

78
- Support `editor/getDefinition` and `editor/getReferences` server requests via lsp-mode, letting the LLM navigate to a symbol definition/references using the editor's LSP. When no server is attached yet for a file of a known lsp project, lsp is started in background and `starting` is answered so the eca server retries. `starting` is also answered while an attached workspace is still initializing, since lsp-mode would reject the request with a misleading "does not support method" capability error before the initialize handshake completes. Synchronous lsp-mode errors are answered as `error` responses, and any server-request handler error now answers an error response instead of escaping into the process filter ("error in process filter" in Messages) and leaving the request pending until the server times it out (30s). Requires eca server with editor navigation support (editor-code-assistant/eca#351).

‎eca-chat.el‎

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -708,6 +708,12 @@ Set by `eca-chat--kill-buffer-query' and consumed by
708708
Set when the welcome message and resume link are inserted in
709709
`eca-chat-mode'; cleared by `eca-chat--clear'. Used to decide
710710
whether the first user prompt should erase the banner first.")
711+
712+
(defvar-local eca-chat--prompt-after-clear nil
713+
"Prompt text to restore after the next server-initiated clear.
714+
Set by `eca-chat--rollback' so the rolled-back user message and
715+
any prompt draft survive the `chat/cleared' redraw; consumed
716+
once by `eca-chat-cleared'.")
711717
(defvar-local eca-chat--id nil)
712718
(defvar-local eca-chat--title nil)
713719
(defvar-local eca-chat--custom-title nil)
@@ -1646,8 +1652,23 @@ stopping while one is pending: cancel it, then notify the server."
16461652
:params (list :chatId eca-chat--id))
16471653
(eca-chat--set-chat-loading session 'stopping)))
16481654

1649-
(defun eca-chat--rollback (session content-id)
1650-
"Rollback chat messages for SESSION to before CONTENT-ID."
1655+
(defun eca-chat--rollback-prompt-text (text)
1656+
"Join rolled-back TEXT with the current prompt draft.
1657+
The draft goes after TEXT, separated by a blank line, so edits
1658+
typed before the rollback are kept. Return nil when both are
1659+
empty."
1660+
(let ((restored (string-trim (or text "")))
1661+
(draft (or (eca-chat--prompt-content) "")))
1662+
(cond
1663+
((string-empty-p draft) (unless (string-empty-p restored) restored))
1664+
((string-empty-p restored) draft)
1665+
(t (concat restored "\n\n" draft)))))
1666+
1667+
(defun eca-chat--rollback (session content-id &optional text)
1668+
"Rollback chat messages for SESSION to before CONTENT-ID.
1669+
TEXT is the rolled-back user message text; when the rollback
1670+
removes messages, it is restored into the prompt field with any
1671+
current draft appended, after the server clears the chat."
16511672
(unless eca-chat--chat-loading
16521673
(let ((rollback-messages-and-tools-str "1. Rollback messages and changes done by tool calls")
16531674
(rollback-messages-str "2. Rollback only messages")
@@ -1666,11 +1687,18 @@ stopping while one is pending: cancel it, then notify the server."
16661687
((string= rollback-type rollback-messages-str) ["messages"])
16671688
((string= rollback-type rollback-tools-str) ["tools"])
16681689
((string= rollback-type rollback-messages-and-tools-str) ["messages" "tools"]))))
1669-
(eca-api-request-sync session
1670-
:method "chat/rollback"
1671-
:params (list :chatId eca-chat--id
1672-
:contentId content-id
1673-
:include include)))))))
1690+
(when (member "messages" (append include nil))
1691+
(setq-local eca-chat--prompt-after-clear
1692+
(eca-chat--rollback-prompt-text text)))
1693+
(unwind-protect
1694+
(eca-api-request-sync session
1695+
:method "chat/rollback"
1696+
:params (list :chatId eca-chat--id
1697+
:contentId content-id
1698+
:include include))
1699+
;; `eca-chat-cleared' consumes the stash during the sync
1700+
;; wait; this reset only drops a leftover on request failure.
1701+
(setq-local eca-chat--prompt-after-clear nil)))))))
16741702

16751703
(defun eca-chat--remove-flag (session content-id)
16761704
"Remove a flag identified by CONTENT-ID from the chat via SESSION."
@@ -4003,7 +4031,7 @@ Must be called with `eca-chat--with-current-buffer' or equivalent."
40034031
(eca-buttonize
40044032
eca-chat-mode-map
40054033
(propertize "Rollback chat to before this message" 'font-lock-face 'eca-chat-rollback-face)
4006-
(lambda () (eca-chat--rollback session content-id))))
4034+
(lambda () (eca-chat--rollback session content-id (string-trim text)))))
40074035
(when-let* ((ov (eca-chat--get-expandable-content content-id)))
40084036
(overlay-put ov 'eca-chat--user-message-id content-id)
40094037
(overlay-put ov 'eca-chat--timestamp (float-time)))
@@ -4596,7 +4624,9 @@ Shown at the top of the buffer only when an older page is available
45964624
(when (buffer-live-p chat-buffer)
45974625
(eca-chat--with-current-buffer chat-buffer
45984626
(when messages?
4599-
(eca-chat--clear))))))
4627+
(let ((new-prompt eca-chat--prompt-after-clear))
4628+
(setq-local eca-chat--prompt-after-clear nil)
4629+
(eca-chat--clear new-prompt)))))))
46004630

46014631
(defun eca-chat--legacy-open-config-p (chat-config)
46024632
"Return non-nil for a legacy unscoped chat/open restore update."

‎test/eca-chat-test.el‎

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1788,4 +1788,120 @@ CHOOSE receives the candidate list the command offers."
17881788
(expect (eca-chat--mode-line-module eca-chat-test--ws-session :remove-workspace-button)
17891789
:to-be nil)))
17901790

1791+
(describe "eca-chat--rollback-prompt-text"
1792+
1793+
(it "returns the rolled-back text when there is no draft"
1794+
(with-temp-buffer
1795+
(expect (eca-chat--rollback-prompt-text "redo this") :to-equal "redo this")))
1796+
1797+
(it "returns nil when both text and draft are empty"
1798+
(with-temp-buffer
1799+
(expect (eca-chat--rollback-prompt-text nil) :to-be nil)
1800+
(expect (eca-chat--rollback-prompt-text " ") :to-be nil)))
1801+
1802+
(it "appends the draft after the rolled-back text"
1803+
(let ((buf (eca-chat-test--make-prompt-buffer "draft edit")))
1804+
(unwind-protect
1805+
(with-current-buffer buf
1806+
(expect (eca-chat--rollback-prompt-text "old msg")
1807+
:to-equal "old msg\n\ndraft edit"))
1808+
(kill-buffer buf))))
1809+
1810+
(it "keeps a lone draft when there is no rolled-back text"
1811+
(let ((buf (eca-chat-test--make-prompt-buffer "draft edit")))
1812+
(unwind-protect
1813+
(with-current-buffer buf
1814+
(expect (eca-chat--rollback-prompt-text nil) :to-equal "draft edit"))
1815+
(kill-buffer buf))))
1816+
1817+
(it "ignores a whitespace-only draft"
1818+
(let ((buf (eca-chat-test--make-prompt-buffer " ")))
1819+
(unwind-protect
1820+
(with-current-buffer buf
1821+
(expect (eca-chat--rollback-prompt-text "old msg") :to-equal "old msg"))
1822+
(kill-buffer buf)))))
1823+
1824+
(describe "eca-chat--rollback"
1825+
1826+
(it "stashes restored text plus draft for a messages rollback"
1827+
(let ((buf (eca-chat-test--make-prompt-buffer "draft edit"))
1828+
(session (make-eca--session))
1829+
(captured 'unset))
1830+
(unwind-protect
1831+
(with-current-buffer buf
1832+
(setq-local eca-chat--id "chat-1")
1833+
(spy-on 'completing-read
1834+
:and-return-value "2. Rollback only messages")
1835+
;; Capture the stash as the server request sees it: the
1836+
;; `chat/cleared' notification consumes it during the sync wait.
1837+
(spy-on 'eca-api-request-sync :and-call-fake
1838+
(lambda (&rest _)
1839+
(setq captured eca-chat--prompt-after-clear)))
1840+
(eca-chat--rollback session "content-1" "old msg")
1841+
(expect captured :to-equal "old msg\n\ndraft edit")
1842+
(expect 'eca-api-request-sync :to-have-been-called)
1843+
;; The unwind reset drops any unconsumed leftover.
1844+
(expect eca-chat--prompt-after-clear :to-be nil))
1845+
(kill-buffer buf))))
1846+
1847+
(it "does not stash for a tools-only rollback"
1848+
(let ((buf (eca-chat-test--make-prompt-buffer "draft edit"))
1849+
(session (make-eca--session))
1850+
(captured 'unset))
1851+
(unwind-protect
1852+
(with-current-buffer buf
1853+
(setq-local eca-chat--id "chat-1")
1854+
(spy-on 'completing-read
1855+
:and-return-value "3. Rollback only changes done by tool calls")
1856+
(spy-on 'eca-api-request-sync :and-call-fake
1857+
(lambda (&rest _)
1858+
(setq captured eca-chat--prompt-after-clear)))
1859+
(eca-chat--rollback session "content-1" "old msg")
1860+
(expect captured :to-be nil)
1861+
(expect 'eca-api-request-sync :to-have-been-called))
1862+
(kill-buffer buf)))))
1863+
1864+
(describe "eca-chat-cleared"
1865+
1866+
(it "restores the stashed prompt into the rebuilt prompt field"
1867+
(let ((buf (eca-chat-test--make-prompt-buffer "draft edit"))
1868+
(session (make-eca--session)))
1869+
(unwind-protect
1870+
(with-current-buffer buf
1871+
(setq-local eca-chat--id "chat-1")
1872+
(setq-local eca-chat--prompt-after-clear "old msg\n\ndraft edit")
1873+
(spy-on 'eca-chat--get-chat-buffer :and-return-value buf)
1874+
(spy-on 'eca-chat--refresh-context)
1875+
(eca-chat-cleared session (list :chatId "chat-1" :messages t))
1876+
(expect (eca-chat-test--prompt-text buf)
1877+
:to-equal "old msg\n\ndraft edit")
1878+
(expect eca-chat--prompt-after-clear :to-be nil))
1879+
(kill-buffer buf))))
1880+
1881+
(it "leaves the prompt empty when nothing is stashed"
1882+
(let ((buf (eca-chat-test--make-prompt-buffer "leftover")))
1883+
(unwind-protect
1884+
(with-current-buffer buf
1885+
(setq-local eca-chat--id "chat-1")
1886+
(spy-on 'eca-chat--get-chat-buffer :and-return-value buf)
1887+
(spy-on 'eca-chat--refresh-context)
1888+
(eca-chat-cleared (make-eca--session)
1889+
(list :chatId "chat-1" :messages t))
1890+
(expect (eca-chat-test--prompt-text buf) :to-equal ""))
1891+
(kill-buffer buf))))
1892+
1893+
(it "does not clear nor consume the stash when messages is nil"
1894+
(let ((buf (eca-chat-test--make-prompt-buffer "draft edit")))
1895+
(unwind-protect
1896+
(with-current-buffer buf
1897+
(setq-local eca-chat--id "chat-1")
1898+
(setq-local eca-chat--prompt-after-clear "old msg")
1899+
(spy-on 'eca-chat--get-chat-buffer :and-return-value buf)
1900+
(spy-on 'eca-chat--clear)
1901+
(eca-chat-cleared (make-eca--session)
1902+
(list :chatId "chat-1" :messages nil))
1903+
(expect 'eca-chat--clear :not :to-have-been-called)
1904+
(expect eca-chat--prompt-after-clear :to-equal "old msg"))
1905+
(kill-buffer buf)))))
1906+
17911907
;;; eca-chat-test.el ends here

0 commit comments

Comments
 (0)