Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased

- Add `eca-scratch-chat`: start or switch to a chat that belongs to no project, for questions unrelated to the code being visited. The session runs in the directory named by `eca-scratch-directory`: by default an `eca-scratch` folder inside the system temp directory, created on demand, but when the option is set every scratch chat uses that directory instead. Either way no project context reaches the model, and repeated calls return to the same session like `scratch-buffer` does.
- Bugfix: under evil (e.g. Doom), RET in normal state in chat buffers ran `markdown-do` from evil-collection's `markdown-mode-map` bindings, which `eca-chat-mode-map` inherits. It followed links but inserted a GFM checkbox (`* [ ]`) anywhere else, e.g. next to a question option. RET in normal state now goes through `eca-chat--key-pressed-return`.
- Bugfix: a tool call awaiting approval whose expanded body is taller than the window no longer gets its label and Accept/Reject buttons scrolled above the window (#308). The window is anchored on the tool call with point on its Accept button (RET accepts), and once it resolves the view moves on to the next pending approval or back to the prompt.
- Add `eca-chat-tool-call-functions`, an abnormal hook run with the session and content when a tool call changes state, e.g. to refresh magit after file edits.
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ M-x package-install eca
Server / process

- `eca`: Starts eca server/session + open chat
- `eca-scratch-chat`: Starts/opens a scratch session in
`eca-scratch-directory`, for questions unrelated to any project
- `eca-stop`: Stop eca server/session
- `eca-restart`: Restart eca server/session
- `eca-workspaces`: live dashboard with all workspaces and chats,
Expand Down Expand Up @@ -138,6 +140,7 @@ Inline prompt (chat from any buffer)
Server / process

- `eca-custom-command`: The `eca` server command; when nil ECA auto-downloads or uses `eca` from `$PATH`.
- `eca-scratch-directory`: Workspace used by `eca-scratch-chat`, by default an `eca-scratch` folder inside the system temp directory.
- `eca-server-download-method`: Method to download server (`curl` or `url-retrieve`, Emacs built-in way).
- `eca-server-download-url`: Custom URL to download the ECA server archive.
- `eca-server-fetch-timeout`: Seconds before a GitHub fetch (release check/download connection) is considered stuck.
Expand Down
32 changes: 32 additions & 0 deletions eca.el
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,18 @@ Tries git info first, then package.el version, then file modification date."
:type 'hook
:group 'eca)

(defcustom eca-scratch-directory
(expand-file-name "eca-scratch" temporary-file-directory)
"Directory used as the workspace of `eca-scratch-chat'.
Every scratch chat runs here, whatever this is set to. The default is
a dedicated folder inside the variable `temporary-file-directory'
rather than that directory itself, so scratch chats do not pick up the
files other programs leave there. Created on demand; ECA never cleans
it up, though with the default most systems empty the temp directory
on reboot."
:type 'directory
:group 'eca)

(defcustom eca-send-process-id t
"Whether to send the Emacs process ID to the ECA server.
When non-nil, the server uses it to detect when Emacs exits and
Expand Down Expand Up @@ -426,6 +438,26 @@ When ARG is current prefix, ask for workspace roots to use."
('started (eca-chat-open session))
('starting (eca-info "eca server is already starting")))))

;;;###autoload
(defun eca-scratch-chat ()
"Start or switch to a scratch eca session, unrelated to any project.
Like `scratch-buffer', this is where a quick question goes when it
has nothing to do with the code you happen to be visiting: the
session runs in the directory named by `eca-scratch-directory', so
no project context reaches the model. That directory is created
when missing."
(interactive)
(let ((root (directory-file-name (expand-file-name eca-scratch-directory))))
(unless (file-directory-p root)
(make-directory root t))
;; Run from a scratch buffer: `eca' resolves the session from the
;; current buffer, whose cached session belongs to the project being
;; visited -- precisely what this chat must not reuse. A throwaway
;; buffer starts with no cache, and the one `eca' sets dies with it.
(with-temp-buffer
(let ((eca-find-root-for-buffer-function (lambda () root)))
(eca)))))

(defun eca-stop-session (session)
"Stop SESSION if running."
(when (eca-process-running-p session)
Expand Down
69 changes: 69 additions & 0 deletions test/eca-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,74 @@
(eca--handle-message session request)
(expect 'eca-api-send-request-response :not :to-have-been-called))))

(describe "eca-scratch-chat"
:var (scratch-dir)

(before-each
;; Under `temporary-file-directory' rather than a literal /tmp:
;; these tests also run on Windows CI.
(setq scratch-dir (expand-file-name (make-temp-name "eca-scratch-test-")
temporary-file-directory))
(setq eca-scratch-directory scratch-dir)
(setq eca--sessions '())
(spy-on 'eca-process-start)
(spy-on 'eca-chat-open))

(after-each
(setq eca--sessions '())
(when (file-directory-p scratch-dir)
(delete-directory scratch-dir t)))

(it "creates the scratch directory when it does not exist"
(expect (file-directory-p scratch-dir) :to-be nil)
(eca-scratch-chat)
(expect (file-directory-p scratch-dir) :to-be-truthy))

(it "starts a session whose workspace is the scratch directory"
(eca-scratch-chat)
(expect (length (eca-vals eca--sessions)) :to-equal 1)
(expect (eca--session-workspace-folders (car (eca-vals eca--sessions)))
:to-equal (list (directory-file-name scratch-dir)))
(expect 'eca-process-start :to-have-been-called))

(it "reuses the scratch session instead of starting a second one"
(eca-scratch-chat)
(let ((session (car (eca-vals eca--sessions))))
(setf (eca--session-status session) 'started)
(eca-scratch-chat)
(expect (length (eca-vals eca--sessions)) :to-equal 1)
(expect 'eca-chat-open :to-have-been-called-with session)))

;; The point of the command: the chat must not land in the session of
;; whatever project the current buffer belongs to.
(it "does not reuse the session of the current buffer's project"
(let ((project (eca-create-session (list "/some/project"))))
(setf (eca--session-status project) 'started)
(setq-local eca--session-id-cache (eca--session-id project))
(eca-scratch-chat)
(expect (length (eca-vals eca--sessions)) :to-equal 2)
(expect 'eca-chat-open :not :to-have-been-called-with project)))

;; The command must not disturb the buffer it was called from.
(it "leaves the current buffer's session cache untouched"
(let ((project (eca-create-session (list "/some/project"))))
(setf (eca--session-status project) 'started)
(setq-local eca--session-id-cache (eca--session-id project))
(eca-scratch-chat)
(expect eca--session-id-cache :to-equal (eca--session-id project))))

(it "honours a custom eca-scratch-directory"
(let ((custom (expand-file-name (make-temp-name "eca-scratch-custom-")
temporary-file-directory)))
(unwind-protect
(progn
(setq eca-scratch-directory custom)
(eca-scratch-chat)
(expect (eca--session-workspace-folders
(car (eca-vals eca--sessions)))
:to-equal (list (directory-file-name custom))))
(when (file-directory-p custom)
(delete-directory custom t))))))

(provide 'eca-test)
;;; eca-test.el ends here
Loading