diff --git a/eca-chat.el b/eca-chat.el index d3524f6..ecd191d 100644 --- a/eca-chat.el +++ b/eca-chat.el @@ -549,7 +549,7 @@ are not currently selected." ;; Internal -(defvar-local eca-chat--closed nil) + (defvar-local eca-chat--history '()) (defvar-local eca-chat--history-index -1) (defvar-local eca-chat--id nil) @@ -637,13 +637,6 @@ A plist with :session :request :question :options :tool-call-id :allow-freeform. (defvar eca--chat-init-session nil "Dynamically bound session during `eca-chat-mode' initialization.") -(defun eca-chat-new-buffer-name (session) - "Return the chat buffer name for SESSION." - (format "" - (eca--session-project-name session) - (eca--session-id session) - eca-chat--new-chat-id)) - (defvar eca-chat-mode-map (let ((map (make-sparse-keymap))) (set-keymap-parent map markdown-mode-map) @@ -700,11 +693,11 @@ A plist with :session :request :question :options :tool-call-id :allow-freeform. (or (when-let (last-buff (eca--session-last-chat-buffer session)) (when (buffer-live-p last-buff) last-buff)) - (get-buffer (eca-chat-new-buffer-name session)))) + (get-buffer (funcall eca-generate-buffer-name-function "eca-chat" :session session :chat-id eca-chat--new-chat-id)))) (defun eca-chat--create-buffer (session) "Create the eca chat buffer for SESSION." - (get-buffer-create (generate-new-buffer-name (eca-chat-new-buffer-name session)))) + (get-buffer-create (generate-new-buffer-name (funcall eca-generate-buffer-name-function "eca-chat" :session session :chat-id eca-chat--new-chat-id)))) (defun eca-chat--get-chat-buffer (session chat-id) "Get chat buffer for SESSION and CHAT-ID, or nil when none registered. @@ -720,7 +713,7 @@ its real id and there is no `'empty' placeholder to migrate from." (and (symbolp this-command) (string-prefix-p "eca-" (symbol-name this-command)))) eca-chat--id - (not eca-chat--closed) + (not eca-chat-closed) (yes-or-no-p "Delete chat from server side (otherwise it will just kill the buffer) ?")) (eca-api-request-sync (eca-session) :method "chat/delete" @@ -1345,7 +1338,7 @@ Resteps a list of context plists found in the prompt field." (defun eca-chat--send-prompt (session prompt) "Send PROMPT to server for SESSION." - (when eca-chat--closed + (when eca-chat-closed (user-error (eca-error "This chat is closed"))) (let* ((prompt-contexts (eca-chat--extract-contexts-from-prompt)) (refined-contexts (-map #'eca-chat--refine-context @@ -2008,7 +2001,7 @@ are in progress." (defun eca-chat--mode-line-string (session) "Build mode-line string for SESSION from `eca-chat-mode-line-format'." - (if eca-chat--closed + (if eca-chat-closed (propertize "*Closed session*" 'font-lock-face 'eca-chat-system-messages-face) (let* ((fmt eca-chat-mode-line-format) @@ -3576,7 +3569,7 @@ When ACTIVE is non-nil, show the question prefix; otherwise restore normal." (when eca-chat--stopping-safety-timer (cancel-timer eca-chat--stopping-safety-timer) (setq-local eca-chat--stopping-safety-timer nil)) - (setq eca-chat--closed t) + (setq-local eca-chat-closed t) (force-mode-line-update) (goto-char (point-max)) (rename-buffer (concat (buffer-name) ":closed") t) @@ -3584,7 +3577,7 @@ When ACTIVE is non-nil, show the question prefix; otherwise restore normal." (let ((current (current-buffer))) (dolist (b (buffer-list)) (when (and (not (eq b current)) - (string-match-p "^:closed$" (buffer-name b))) + (buffer-local-value 'eca-chat-closed b)) (kill-buffer b)))) (when-let* ((window (get-buffer-window chat-buffer))) (quit-window nil window)))))) diff --git a/eca-process.el b/eca-process.el index 077c918..dfbeb25 100644 --- a/eca-process.el +++ b/eca-process.el @@ -114,18 +114,6 @@ If current `gc-cons-threshold` is lower use that on filter server messages.'" :type 'integer :group 'eca) -(defun eca-process--buffer-name (session) - "Return the process buffer name for SESSION." - (format "" - (eca--session-project-name session) - (eca--session-id session))) - -(defun eca-process--stderr-buffer-name (session) - "Return the stderr buffer name for SESSION." - (format "" - (eca--session-project-name session) - (eca--session-id session))) - (defcustom eca-server-releases-cache-ttl 3600 "Time-to-live (seconds) for the cached eca server releases list. Once expired, the next call that needs the releases list will refetch @@ -544,12 +532,12 @@ Call HANDLE-MSG for new msgs processed." :connection-type 'pipe :name "eca" :command command - :buffer (eca-process--buffer-name session) - :stderr (get-buffer-create (eca-process--stderr-buffer-name session)) + :buffer (funcall eca-generate-buffer-name-function "eca" :session session) + :stderr (get-buffer-create (funcall eca-generate-buffer-name-function "eca:stderr" :session session)) :filter (eca-process--make-filter handle-msg) :sentinel (lambda (process exit-str) (unless (process-live-p process) - (when-let* ((name (eca-process--stderr-buffer-name session)) + (when-let* ((name (funcall eca-generate-buffer-name-function "eca:stderr" :session session)) (buf (get-buffer name))) (with-current-buffer buf (rename-buffer (concat (buffer-name) ":closed") t) @@ -581,12 +569,13 @@ Call HANDLE-MSG for new msgs processed." "Stop the eca process for SESSION if running." (when session (kill-process (eca--session-process session)) - (kill-buffer (eca-process--buffer-name session)) + (kill-buffer (funcall eca-generate-buffer-name-function "eca" :session session)) ;; Rename stderr buffer to closed and clean up older closed ones - (let ((stderr-buffer (get-buffer (eca-process--stderr-buffer-name session)))) + (let ((stderr-buffer (get-buffer (funcall eca-generate-buffer-name-function "eca:stderr" :session session)))) (when stderr-buffer (with-current-buffer stderr-buffer (rename-buffer (concat (buffer-name) ":closed") t) + (setq-local eca-chat-closed t) (setq-local mode-line-format '("*Closed session*")) (when-let ((win (get-buffer-window (current-buffer)))) (quit-window nil win)) @@ -596,12 +585,12 @@ Call HANDLE-MSG for new msgs processed." (let ((current (current-buffer))) (dolist (b (buffer-list)) (when (and (not (eq b current)) - (string-match-p "^:closed" (buffer-name b))) + (buffer-local-value 'eca-chat-closed b)) (kill-buffer b))))))))) (defun eca-process-show-stderr (session) "Open the eca process stderr buffer for SESSION." - (if-let ((buf (get-buffer (eca-process--stderr-buffer-name session)))) + (if-let ((buf (get-buffer (funcall eca-generate-buffer-name-function "eca:stderr" :session session)))) (if (window-live-p (get-buffer-window buf)) (select-window (get-buffer-window buf)) (display-buffer buf)) diff --git a/eca-settings.el b/eca-settings.el index 7903caa..b05f103 100644 --- a/eca-settings.el +++ b/eca-settings.el @@ -82,22 +82,15 @@ preserving tab order." (defvar-local eca-settings--tab-key nil "The tab key for this settings buffer.") -(defun eca-settings--buffer-name (tab-key session) - "Return buffer name for TAB-KEY in SESSION." - (format "" - (eca--session-project-name session) - tab-key - (eca--session-id session))) - (defun eca-settings--get-buffer (tab-key session) "Get existing settings buffer for TAB-KEY in SESSION." - (get-buffer (eca-settings--buffer-name tab-key session))) + (get-buffer (funcall eca-generate-buffer-name-function "eca-settings" :session session :chat-id tab-key))) (defun eca-settings--create-buffer (tab-key session) "Create a new settings buffer for TAB-KEY in SESSION." (get-buffer-create (generate-new-buffer-name - (eca-settings--buffer-name tab-key session)))) + (funcall eca-generate-buffer-name-function "eca-settings" :session session :chat-id tab-key)))) (defun eca-settings--get-or-create-tab-buffer (tab-key session) "Get or create settings buffer for TAB-KEY in SESSION. diff --git a/eca-util.el b/eca-util.el index 69dfcf6..aae8f83 100644 --- a/eca-util.el +++ b/eca-util.el @@ -320,6 +320,35 @@ workspace folder. Falls back to \"unknown\"." "Display eca error message with FORMAT with ARGS." (message "%s :: %s" (propertize "ECA" 'face 'error) (apply #'format format args))) +(defvar-local eca-chat-closed nil + "Non-nil when the chat buffer belongs to a closed session.") + +(cl-defun eca-generate-buffer-name-default-function (mode &key session chat-id) + "Generate a buffer name for optional SESSION and optional CHAT-ID. +MODE is a string describing the buffer type (e.g. eca-chat)." + (cond + ((null session) + (format "<%s>" mode)) + (chat-id + (format "<%s[%s]:%s:%s>" mode + (eca--session-project-name session) + (eca--session-id session) + chat-id)) + (t + (format "<%s[%s]:%s>" mode + (eca--session-project-name session) + (eca--session-id session))))) + +(defcustom eca-generate-buffer-name-function + #'eca-generate-buffer-name-default-function + "The function used to generate the name for an ECA buffer. +The function is called with a required MODE (string), followed by +keyword arguments :session and :chat-id. It must return a string +to use as the buffer name." + :type `(radio (function-item ,#'eca-generate-buffer-name-default-function) + (function :tag "Function")) + :group 'eca) + (defun eca-buttonize (base-map text callback) "Create a actionable TEXT that call CALLBACK when actioned. Inheirits BASE-MAP." diff --git a/eca.el b/eca.el index 1187388..e39b10c 100644 --- a/eca.el +++ b/eca.el @@ -121,20 +121,12 @@ with `eca-process-wrapper-function' for a fully sandboxed setup." ;; Internal -(defvar eca-workspaces-buffer-name "*eca-workspaces*") - -(defun eca--emacs-errors-buffer-name (session) - "Return the Emacs errors buffer name for SESSION." - (format "" - (eca--session-project-name session) - (eca--session-id session))) - (defun eca--log-error (session err &optional context backtrace) "Log error ERR to the Emacs errors buffer for SESSION. Optional CONTEXT is a string describing what was happening when the error occurred. Optional BACKTRACE is a list of frames captured via `backtrace-get-frames'." - (let ((buffer (get-buffer-create (eca--emacs-errors-buffer-name session)))) + (let ((buffer (get-buffer-create (funcall eca-generate-buffer-name-function "eca:emacs-errors" :session session)))) (with-current-buffer buffer (goto-char (point-max)) (insert (format "[%s] %s%s\n" @@ -150,7 +142,7 @@ frames captured via `backtrace-get-frames'." (defun eca-show-emacs-errors (session) "Open the Emacs errors buffer for SESSION." - (let ((buffer (get-buffer (eca--emacs-errors-buffer-name session)))) + (let ((buffer (get-buffer (funcall eca-generate-buffer-name-function "eca:emacs-errors" :session session)))) (if buffer (with-current-buffer buffer (if (window-live-p (get-buffer-window (buffer-name))) @@ -166,20 +158,19 @@ frames captured via `backtrace-get-frames'." (defun eca--emacs-errors-exit (session) "Clean up the Emacs errors buffer for SESSION on stop." - (let ((buffer (get-buffer (eca--emacs-errors-buffer-name session)))) + (let ((buffer (get-buffer (funcall eca-generate-buffer-name-function "eca:emacs-errors" :session session)))) (when buffer (with-current-buffer buffer (rename-buffer (concat (buffer-name) ":closed") t) - (setq-local mode-line-format '("*Closed session*")) - (when-let ((win (get-buffer-window (current-buffer)))) - (quit-window nil win)) - ;; Keep only the most recently closed errors buffer; kill older ones. - (let ((current (current-buffer))) - (dolist (b (buffer-list)) - (when (and (not (eq b current)) - (or - (string-match-p "^:closed$" (buffer-name b)) - (string-match-p "^$" (buffer-name b)))) + (setq-local eca-chat-closed t) + (setq-local mode-line-format '("*Closed session*")) + (when-let ((win (get-buffer-window (current-buffer)))) + (quit-window nil win)) + ;; Keep only the most recently closed errors buffer; kill older ones. + (let ((current (current-buffer))) + (dolist (b (buffer-list)) + (when (and (not (eq b current)) + (buffer-local-value 'eca-chat-closed b)) (kill-buffer b)))))))) (defun eca--get-message-type (json-data) @@ -364,7 +355,7 @@ backtrace. On older Emacs, runs BODY without capture." "Connect in eca nrepl port for development." (interactive) (eca-assert-session-running (eca-session)) - (with-current-buffer (eca-process--stderr-buffer-name (eca-session)) + (with-current-buffer (funcall eca-generate-buffer-name-function "eca:stderr" :session (eca-session)) (save-excursion (goto-char (point-min)) (when (re-search-forward "started on port \\([0-9]+\\)" nil t) @@ -466,13 +457,14 @@ When ARG is current prefix, ask for workspace roots to use." (seq-doseq (chat-by-id (eca--session-chats session)) (when (buffer-live-p (cdr chat-by-id)) (hierarchy-add-tree h (cdr chat-by-id) parent-fn)))) - (let ((b (or (when-let ((b (get-buffer eca-workspaces-buffer-name))) - (when (buffer-live-p b) - (with-current-buffer b - (let ((inhibit-read-only t)) - (erase-buffer)))) - b) - (generate-new-buffer eca-workspaces-buffer-name)))) + (let* ((buffer-name (funcall eca-generate-buffer-name-function "eca-workspaces")) + (b (or (when-let ((b (get-buffer buffer-name))) + (when (buffer-live-p b) + (with-current-buffer b + (let ((inhibit-read-only t)) + (erase-buffer)))) + b) + (generate-new-buffer buffer-name)))) (with-current-buffer b (setq-local tree-widget-image-enable nil) (widget-create (eca--tree-widget-open-all