branch: externals/ellama
commit 2cb57c3ac0e4493826b373f922d3fc805215226a
Author: Sergey Kostyaev <[email protected]>
Commit: Sergey Kostyaev <[email protected]>

    Fix chat session recovery after buffer revert
    
    Added a stable session UID layer stored in session extra and introduced 
runtime
    registries for uid-to-buffer and uid-to-session with centralized session
    resolution/registration helpers. Updated chat, stream,
    load/switch/delete/rename/provider, and transient flows to resolve sessions 
from
    the registry instead of relying on fragile buffer-local state, preserving
    session identity across renames and clearing both current session id and uid
    together. Added rehydration on `revert-buffer` to restore session mode/state
    from memory or the sidecar file, and made legacy `.session.el` data without
    `:uid` migrate transparently on load. Expanded tests to cover the `#144`
    regression path, legacy uid migration, rename uid stability, and transient
    behavior, and pinned test `load-path` to workspace sources.
---
 ellama-transient.el            |  13 +-
 ellama.el                      | 643 ++++++++++++++++++++++++-----------------
 tests/test-ellama-transient.el |  47 +--
 tests/test-ellama.el           |  77 +++++
 4 files changed, 492 insertions(+), 288 deletions(-)

diff --git a/ellama-transient.el b/ellama-transient.el
index 7171b4fa04..64557df3b3 100644
--- a/ellama-transient.el
+++ b/ellama-transient.el
@@ -43,6 +43,7 @@
 (defvar ellama-transient-context-length 4096)
 (defvar ellama-transient-host "localhost")
 (defvar ellama-transient-port 11434)
+(defvar ellama--current-session-uid)
 
 (defun ellama-transient-system-show ()
   "Show transient system message."
@@ -112,10 +113,9 @@ Otherwise, prompt the user to enter a system message."
 (transient-define-suffix ellama-transient-model-get-from-current-session ()
   "Fill transient model from current session."
   (interactive)
-  (when ellama--current-session-id
+  (when-let ((session (ellama-get-current-session)))
     (ellama-fill-transient-ollama-model
-     (with-current-buffer (ellama-get-session-buffer 
ellama--current-session-id)
-       (ellama-session-provider ellama--current-session)))))
+     (ellama-session-provider session))))
 
 (transient-define-suffix ellama-transient-set-provider ()
   "Set transient model to provider."
@@ -127,7 +127,8 @@ Otherwise, prompt the user to enter a system message."
          (ellama-construct-ollama-provider-from-transient))
     ;; if you change `ellama-provider' you probably want to start new chat 
session
     (when (equal provider 'ellama-provider)
-      (setq ellama--current-session-id nil))))
+      (setq ellama--current-session-id nil
+            ellama--current-session-uid nil))))
 
 ;;;###autoload (autoload 'ellama-select-ollama-model "ellama-transient" nil t)
 (transient-define-prefix ellama-select-ollama-model ()
@@ -228,9 +229,7 @@ Otherwise, prompt the user to enter a system message."
   "Summarise session and context for transient menus."
   (format "%s %s %s %s"
           (propertize "Session:" 'face 'ellama-key-face)
-         (if ellama--current-session
-             (ellama-session-id ellama--current-session)
-           ellama--current-session-id)
+          (ellama-get-current-session-id)
           (propertize "Context: " 'face 'ellama-key-face)
           (ellama--context-summary)))
 
diff --git a/ellama.el b/ellama.el
index 7a9ec21f49..29dd92d8a5 100644
--- a/ellama.el
+++ b/ellama.el
@@ -476,15 +476,21 @@ It should be a function with single argument generated 
text string."
   :type 'string)
 
 (defvar ellama--current-session-id nil)
+(defvar ellama--current-session-uid nil)
 
 (defun ellama--set-file-name-and-save ()
   "Set buffer file name and save buffer."
   (interactive)
-  (setq buffer-file-name
-       (file-name-concat
-        ellama-sessions-directory
-        (concat ellama--current-session-id
-                "." (ellama-get-session-file-extension))))
+  (let ((session-id (or (when ellama--current-session
+                          (ellama-session-id ellama--current-session))
+                        ellama--current-session-id)))
+    (unless session-id
+      (error "No active ellama session for this buffer"))
+    (setq buffer-file-name
+          (file-name-concat
+           ellama-sessions-directory
+           (concat session-id
+                   "." (ellama-get-session-file-extension)))))
   (save-buffer))
 
 (define-minor-mode ellama-session-mode
@@ -494,9 +500,11 @@ It should be a function with single argument generated 
text string."
   (if ellama-session-mode
       (progn
         (add-hook 'after-save-hook 'ellama--save-session nil t)
-        (add-hook 'kill-buffer-hook 'ellama--session-deactivate nil t))
+        (add-hook 'kill-buffer-hook 'ellama--session-deactivate nil t)
+        (add-hook 'after-revert-hook 'ellama--rehydrate-session-on-revert nil 
t))
     (remove-hook 'kill-buffer-hook 'ellama--session-deactivate)
     (remove-hook 'after-save-hook 'ellama--save-session)
+    (remove-hook 'after-revert-hook 'ellama--rehydrate-session-on-revert)
     (ellama--session-deactivate)))
 
 (defvar ellama-request-mode-map
@@ -748,6 +756,8 @@ This filter contains only subset of markdown syntax to be 
good enough."
   :type 'boolean)
 
 (defvar-local ellama--current-session nil)
+(defvar ellama--active-sessions (make-hash-table :test #'equal))
+(defvar ellama--active-session-states (make-hash-table :test #'equal))
 
 (defcustom ellama-session-line-template " ellama session: %s"
   "Template for formatting the current session line."
@@ -756,10 +766,13 @@ This filter contains only subset of markdown syntax to be 
good enough."
 (defun ellama-session-line ()
   "Return current session id line."
   (propertize (format ellama-session-line-template
-                     (if ellama--current-session
-                         (ellama-session-id ellama--current-session)
-                       ellama--current-session-id))
-             'face 'ellama-face))
+                      (or (when ellama--current-session
+                            (ellama-session-id ellama--current-session))
+                          (when-let* ((uid ellama--current-session-uid)
+                                      (session (gethash uid 
ellama--active-session-states)))
+                            (ellama-session-id session))
+                          ellama--current-session-id))
+              'face 'ellama-face))
 
 ;;;###autoload
 (define-minor-mode ellama-session-header-line-mode
@@ -796,10 +809,8 @@ This filter contains only subset of markdown syntax to be 
good enough."
   (when (listp mode-line-format)
     (let ((element '(:eval (ellama-session-line))))
       (if ellama-session-mode-line-mode
-         (add-to-list 'mode-line-format element t)
-       (setq mode-line-format (delete element mode-line-format))))))
-
-(defvar ellama--active-sessions (make-hash-table :test #'equal))
+          (add-to-list 'mode-line-format element t)
+        (setq mode-line-format (delete element mode-line-format))))))
 
 (cl-defstruct ellama-session
   "A structure represent ellama session.
@@ -817,9 +828,132 @@ CONTEXT will be ignored.  Use global context instead.
   EXTRA contains additional information."
   id provider file prompt context extra)
 
+(defun ellama--clear-current-session-selection ()
+  "Clear globally selected session."
+  (setq ellama--current-session-id nil
+        ellama--current-session-uid nil))
+
+(defun ellama--generate-session-uid ()
+  "Return unique identifier for runtime session registry."
+  (md5 (format "%s:%s:%s"
+               (float-time)
+               (emacs-pid)
+               (random most-positive-fixnum))))
+
+(defun ellama--session-uid (session)
+  "Return uid from SESSION extra plist."
+  (when-let* ((session)
+              (extra (ellama-session-extra session))
+              ((plistp extra)))
+    (plist-get extra :uid)))
+
+(defun ellama--ensure-session-uid (session)
+  "Ensure SESSION has stable uid in extra plist and return it."
+  (let* ((extra (if (plistp (ellama-session-extra session))
+                    (copy-sequence (ellama-session-extra session))
+                  nil))
+         (uid (or (plist-get extra :uid)
+                  (ellama-session-id session)
+                  (ellama--generate-session-uid))))
+    (setf (ellama-session-extra session) (plist-put extra :uid uid))
+    uid))
+
+(defun ellama--active-session-by-id (id)
+  "Return active session matching display ID."
+  (catch 'session
+    (maphash
+     (lambda (_uid session)
+       (when (and (ellama-session-p session)
+                  (equal (ellama-session-id session) id))
+         (throw 'session session)))
+     ellama--active-session-states)
+    nil))
+
+(defun ellama--active-session-by-uid (uid)
+  "Return active session matching UID."
+  (and uid (gethash uid ellama--active-session-states)))
+
+(defun ellama--session-uid-by-buffer (buffer)
+  "Return uid associated with BUFFER."
+  (catch 'uid
+    (maphash
+     (lambda (uid session-buffer)
+       (when (eq session-buffer buffer)
+         (throw 'uid uid)))
+     ellama--active-sessions)
+    nil))
+
+(defun ellama--register-session (session buffer &optional activate)
+  "Register SESSION with BUFFER.
+If ACTIVATE is non-nil, set global active session selection."
+  (let ((uid (ellama--ensure-session-uid session)))
+    (puthash uid buffer ellama--active-sessions)
+    (puthash uid session ellama--active-session-states)
+    (with-current-buffer buffer
+      (setq ellama--current-session session))
+    (when activate
+      (setq ellama--current-session-uid uid
+            ellama--current-session-id (ellama-session-id session)))
+    uid))
+
+(defun ellama--active-session-ids ()
+  "Return active session display ids."
+  (let (ids)
+    (maphash
+     (lambda (_uid session)
+       (when (ellama-session-p session)
+         (push (ellama-session-id session) ids)))
+     ellama--active-session-states)
+    (delete-dups ids)))
+
+(defun ellama--read-session-from-file (session-file-name)
+  "Read and normalize session from SESSION-FILE-NAME."
+  (when (and session-file-name
+             (file-exists-p session-file-name))
+    (with-temp-buffer
+      (insert-file-contents session-file-name)
+      (goto-char (point-min))
+      ;; old sessions support
+      (when (looking-at-p "(setq ")
+        (forward-char)
+        (forward-sexp)
+        (forward-sexp)
+        (skip-chars-forward " \n\t"))
+      (ellama--session-from-data (read (current-buffer))))))
+
+(defun ellama--session-from-data (session)
+  "Create runtime session object from SESSION read from storage."
+  (let* ((offset (cl-struct-slot-offset 'ellama-session 'extra))
+         (extra (when (> (length session)
+                         offset)
+                  (aref session offset)))
+         (result (make-ellama-session
+                  :id (ellama-session-id session)
+                  :provider (ellama-session-provider session)
+                  :file (ellama-session-file session)
+                  :prompt (ellama-session-prompt session)
+                  :extra extra)))
+    (ellama--ensure-session-uid result)
+    result))
+
+(defun ellama--resolve-session (&optional session session-id)
+  "Resolve and return session from SESSION or SESSION-ID."
+  (or (when (ellama-session-p session)
+        (ellama--ensure-session-uid session)
+        session)
+      (when session-id
+        (or (ellama--active-session-by-uid session-id)
+            (ellama--active-session-by-id session-id)))
+      (when ellama--current-session-uid
+        (ellama--active-session-by-uid ellama--current-session-uid))
+      (when ellama--current-session-id
+        (ellama--active-session-by-id ellama--current-session-id))))
+
 (defun ellama-get-session-buffer (id)
-  "Return ellama session buffer by provided ID."
-  (gethash id ellama--active-sessions))
+  "Return ellama session buffer by provided ID or UID."
+  (or (gethash id ellama--active-sessions)
+      (when-let ((session (ellama--active-session-by-id id)))
+        (gethash (ellama--session-uid session) ellama--active-sessions))))
 
 (defconst ellama--forbidden-file-name-characters (rx (any "/\\?%*:|\"<>.;=")))
 
@@ -915,40 +1049,40 @@ Defaults to md, but supports org.  Depends on 
`ellama-major-mode'."
 Provided PROVIDER and PROMPT will be used in new session.
 If EPHEMERAL non nil new session will not be associated with any file."
   (let* ((dir default-directory)
-        (name (ellama-generate-name provider 'ellama prompt))
-        (count 1)
-        (name-with-suffix (format "%s %d" name count))
-        (id (if (and (not (ellama-get-session-buffer name))
-                     (not (file-exists-p (file-name-concat
-                                          ellama-sessions-directory
-                                          (concat name "." 
(ellama-get-session-file-extension))))))
-                name
-              (while (or (ellama-get-session-buffer name-with-suffix)
-                         (file-exists-p (file-name-concat
-                                         ellama-sessions-directory
-                                         (concat name-with-suffix "." 
(ellama-get-session-file-extension)))))
-                (setq count (+ count 1))
-                (setq name-with-suffix (format "%s %d" name count)))
-              name-with-suffix))
-        (file-name (when (and (not ephemeral)
-                              ellama-session-auto-save)
-                     (file-name-concat
-                      ellama-sessions-directory
-                      (concat id "." (ellama-get-session-file-extension)))))
-        (session (make-ellama-session
-                  :id id :provider provider :file file-name :extra `(:dir 
,dir)))
-        (buffer (if file-name
-                    (progn
-                      (make-directory ellama-sessions-directory t)
-                      (find-file-noselect file-name))
-                  (get-buffer-create id))))
-    (setq ellama--current-session-id id)
-    (puthash id buffer ellama--active-sessions)
+         (name (ellama-generate-name provider 'ellama prompt))
+         (count 1)
+         (name-with-suffix (format "%s %d" name count))
+         (id (if (and (not (ellama-get-session-buffer name))
+                      (not (file-exists-p (file-name-concat
+                                           ellama-sessions-directory
+                                           (concat name "." 
(ellama-get-session-file-extension))))))
+                 name
+               (while (or (ellama-get-session-buffer name-with-suffix)
+                          (file-exists-p (file-name-concat
+                                          ellama-sessions-directory
+                                          (concat name-with-suffix "." 
(ellama-get-session-file-extension)))))
+                 (setq count (+ count 1))
+                 (setq name-with-suffix (format "%s %d" name count)))
+               name-with-suffix))
+         (file-name (when (and (not ephemeral)
+                               ellama-session-auto-save)
+                      (file-name-concat
+                       ellama-sessions-directory
+                       (concat id "." (ellama-get-session-file-extension)))))
+         (session (make-ellama-session
+                   :id id :provider provider :file file-name
+                   :extra `(:dir ,dir :uid ,(ellama--generate-session-uid))))
+         (buffer (if file-name
+                     (progn
+                       (make-directory ellama-sessions-directory t)
+                       (find-file-noselect file-name))
+                   (get-buffer-create id))))
     (with-current-buffer buffer
       (setq default-directory dir)
       (funcall ellama-major-mode)
       (setq ellama--current-session session)
       (ellama-session-mode +1))
+    (ellama--register-session session buffer t)
     session))
 
 (defun ellama--make-request-context (buffers)
@@ -1030,13 +1164,28 @@ REQUEST-CONTEXT is a request context."
 (defun ellama--session-deactivate ()
   "Deactivate current session."
   (ellama--cancel-current-request)
-  (when-let* ((session ellama--current-session)
-              (id (ellama-session-id session)))
-    (when (string= (buffer-name)
-                   (buffer-name (ellama-get-session-buffer id)))
-      (remhash id ellama--active-sessions)
-      (when (equal ellama--current-session-id id)
-       (setq ellama--current-session-id nil)))))
+  (when-let ((uid (or (when (ellama-session-p ellama--current-session)
+                        (ellama--session-uid ellama--current-session))
+                      (ellama--session-uid-by-buffer (current-buffer)))))
+    (remhash uid ellama--active-sessions)
+    (remhash uid ellama--active-session-states)
+    (when (equal ellama--current-session-uid uid)
+      (ellama--clear-current-session-selection))))
+
+(defun ellama--rehydrate-session-on-revert ()
+  "Rehydrate current buffer session after `revert-buffer'."
+  (when buffer-file-name
+    (let* ((uid (or (ellama--session-uid-by-buffer (current-buffer))
+                    ellama--current-session-uid))
+           (session (or (ellama--active-session-by-uid uid)
+                        (when ellama--current-session-id
+                          (ellama--active-session-by-id 
ellama--current-session-id))
+                        (ellama--read-session-from-file
+                         (ellama--get-session-file-name buffer-file-name)))))
+      (when (ellama-session-p session)
+        (setq-local ellama--current-session session)
+        (ellama--register-session session (current-buffer) t)
+        (ellama-session-mode +1)))))
 
 (defun ellama--get-session-file-name (file-name)
   "Get ellama session file name for FILE-NAME."
@@ -1075,92 +1224,55 @@ REQUEST-CONTEXT is a request context."
   "Load ellama session from file."
   (interactive)
   (when-let* ((dir (if current-prefix-arg
-                      (read-directory-name
-                       "Select directory containing sessions: "
-                       ellama-sessions-directory)
-                    ellama-sessions-directory))
-             (file-name (file-name-concat
-                         ellama-sessions-directory
-                         (completing-read
-                          "Select session to load: "
-                          (directory-files
-                           ellama-sessions-directory nil "^[^\.].*"))))
-             (session-file-name (ellama--get-session-file-name file-name))
-             (session-file-exists (file-exists-p session-file-name))
-             (buffer (find-file-noselect file-name))
-             (session-buffer (find-file-noselect session-file-name)))
-    (with-current-buffer session-buffer
-      (goto-char (point-min))
-      ;; old sessions support
-      (when (string= "(setq "
-                    (buffer-substring-no-properties 1 7))
-       (goto-char (point-min))
-       ;; skip "("
-       (forward-char)
-       ;; skip setq
-       (forward-sexp)
-       ;; skip ellama--current-session
-       (forward-sexp)
-       ;; skip space
-       (forward-char)
-       ;; remove all above
-       (kill-region (point-min) (point))
-       (goto-char (point-max))
-       ;; remove ")"
-       (delete-char -1)
-       ;; save session in new format
-       (save-buffer)
-       (goto-char (point-min))))
+                       (read-directory-name
+                        "Select directory containing sessions: "
+                        ellama-sessions-directory)
+                     ellama-sessions-directory))
+              (file-name (file-name-concat
+                          dir
+                          (completing-read
+                           "Select session to load: "
+                           (directory-files dir nil "^[^\.].*"))))
+              (session-file-name (ellama--get-session-file-name file-name))
+              ((file-exists-p session-file-name))
+              (buffer (find-file-noselect file-name)))
     (with-current-buffer buffer
       ;; support sessions without user nick at the end of buffer
       (when (not (save-excursion
-                  (save-match-data
-                    (goto-char (point-max))
-                    (and (search-backward (concat 
(ellama-get-nick-prefix-for-mode) " " ellama-user-nick ":\n") nil t)
-                         (search-forward (concat 
(ellama-get-nick-prefix-for-mode) " " ellama-user-nick ":\n") nil t)
-                         (equal (point) (point-max))))))
-       (goto-char (point-max))
-       (insert (ellama-get-nick-prefix-for-mode) " " ellama-user-nick ":\n")
-       (save-buffer))
-      (let* ((session (read session-buffer))
-            ;; workaround for old sessions
-            (offset (cl-struct-slot-offset 'ellama-session 'extra))
-            (extra (when (> (length session)
-                            offset)
-                     (aref session offset))))
-       (setq ellama--current-session
-             (make-ellama-session
-              :id (ellama-session-id session)
-              :provider (ellama-session-provider session)
-              :file (ellama-session-file session)
-              :prompt (ellama-session-prompt session)
-              :extra extra))
-       (with-current-buffer buffer
-         (when-let* ((extra)
-                     ((plistp extra))
-                     (dir (plist-get extra :dir))
-                     ((file-exists-p dir)))
-           (setq default-directory dir))))
-      (setq ellama--current-session-id (ellama-session-id 
ellama--current-session))
-      (puthash (ellama-session-id ellama--current-session)
-              buffer ellama--active-sessions)
-      (ellama-session-mode +1))
-    (kill-buffer session-buffer)
+                   (save-match-data
+                     (goto-char (point-max))
+                     (and (search-backward (concat 
(ellama-get-nick-prefix-for-mode) " " ellama-user-nick ":\n") nil t)
+                          (search-forward (concat 
(ellama-get-nick-prefix-for-mode) " " ellama-user-nick ":\n") nil t)
+                          (equal (point) (point-max))))))
+        (goto-char (point-max))
+        (insert (ellama-get-nick-prefix-for-mode) " " ellama-user-nick ":\n")
+        (save-buffer))
+      (let ((session (ellama--read-session-from-file session-file-name)))
+        (unless (ellama-session-p session)
+          (error "Failed to load ellama session from %s" session-file-name))
+        (setq ellama--current-session session)
+        (when-let* ((extra (ellama-session-extra session))
+                    ((plistp extra))
+                    (session-dir (plist-get extra :dir))
+                    ((file-exists-p session-dir)))
+          (setq default-directory session-dir))
+        (ellama-session-mode +1)
+        (ellama--register-session session buffer t)))
     (ellama-hide-quotes)
     (display-buffer buffer (when ellama-chat-display-action-function
-                            `((ignore . 
(,ellama-chat-display-action-function)))))))
+                             `((ignore . 
(,ellama-chat-display-action-function)))))))
 
 ;;;###autoload
 (defun ellama-session-delete ()
   "Delete ellama session."
   (interactive)
   (let* ((id (completing-read
-             "Select session to remove: "
-             (hash-table-keys ellama--active-sessions)))
-        (buffer (ellama-get-session-buffer id))
-        (file (when buffer (buffer-file-name buffer)))
-        (session-file (when file (ellama--get-session-file-name file)))
-        (translation-file (when file (ellama--get-translation-file-name 
file))))
+              "Select session to remove: "
+              (ellama--active-session-ids)))
+         (buffer (ellama-get-session-buffer id))
+         (file (when buffer (buffer-file-name buffer)))
+         (session-file (when file (ellama--get-session-file-name file)))
+         (translation-file (when file (ellama--get-translation-file-name 
file))))
     (when buffer (kill-buffer buffer))
     (when file (delete-file file t))
     (when session-file (delete-file session-file t))
@@ -1178,16 +1290,19 @@ REQUEST-CONTEXT is a request context."
 
 (defun ellama-activate-session (id)
   "Change current active session to session with ID."
-  (setq ellama--current-session-id id))
+  (if-let ((session (ellama--resolve-session nil id)))
+      (setq ellama--current-session-id (ellama-session-id session)
+            ellama--current-session-uid (ellama--ensure-session-uid session))
+    (ellama--clear-current-session-selection)))
 
 ;;;###autoload
 (defun ellama-session-switch ()
   "Change current active session."
   (interactive)
   (let* ((id (completing-read
-             "Select session to activate: "
-             (hash-table-keys ellama--active-sessions)))
-        (buffer (ellama-get-session-buffer id)))
+              "Select session to activate: "
+              (ellama--active-session-ids)))
+         (buffer (ellama-get-session-buffer id)))
     (ellama-activate-session id)
     (display-buffer buffer (when ellama-chat-display-action-function
                              `((ignore . 
(,ellama-chat-display-action-function)))))))
@@ -1197,33 +1312,38 @@ REQUEST-CONTEXT is a request context."
   "Select and kill one of active sessions."
   (interactive)
   (let* ((id (completing-read
-             "Select session to kill: "
-             (hash-table-keys ellama--active-sessions)))
-        (buffer (ellama-get-session-buffer id)))
+              "Select session to kill: "
+              (ellama--active-session-ids)))
+         (buffer (ellama-get-session-buffer id)))
     (when buffer (kill-buffer buffer))))
 
 ;;;###autoload
 (defun ellama-session-rename ()
   "Rename current ellama session."
   (interactive)
-  (let* ((id (if ellama--current-session
-                (ellama-session-id ellama--current-session)
-              ellama--current-session-id))
-        (buffer (when id (ellama-get-session-buffer id)))
-        (session (when buffer (with-current-buffer buffer
-                                ellama--current-session)))
-        (file-name (when buffer (buffer-file-name buffer)))
-        (file-ext (when file-name (file-name-extension file-name)))
-        (dir (when file-name (file-name-directory file-name)))
-        (session-file-name (when file-name (ellama--get-session-file-name 
file-name)))
-        (new-id (read-string
-                 "New session name: "
-                 id))
-        (new-file-name (when dir (file-name-concat
-                                  dir
-                                  (concat new-id "." file-ext))))
-        (new-session-file-name
-         (when new-file-name (ellama--get-session-file-name new-file-name))))
+  (let* ((session (or ellama--current-session
+                      (ellama-get-current-session)))
+         (_ (unless session
+              (error "No active ellama session to rename")))
+         (uid (ellama--ensure-session-uid session))
+         (id (ellama-session-id session))
+         (buffer (or (gethash uid ellama--active-sessions)
+                     (ellama-get-session-buffer id)))
+         (file-name (when buffer (buffer-file-name buffer)))
+         (file-ext (when file-name (file-name-extension file-name)))
+         (dir (when file-name (file-name-directory file-name)))
+         (session-file-name (when file-name (ellama--get-session-file-name 
file-name)))
+         (new-id (read-string
+                  "New session name: "
+                  id))
+         (_ (when (and (not (equal new-id id))
+                       (ellama--active-session-by-id new-id))
+              (error "Session with name %s already exists" new-id)))
+         (new-file-name (when dir (file-name-concat
+                                   dir
+                                   (concat new-id "." file-ext))))
+         (new-session-file-name
+          (when new-file-name (ellama--get-session-file-name new-file-name))))
     (when new-file-name (with-current-buffer buffer
                           (set-visited-file-name new-file-name)))
     (when buffer (with-current-buffer buffer
@@ -1233,10 +1353,8 @@ REQUEST-CONTEXT is a request context."
     (when (and session-file-name (file-exists-p session-file-name))
       (rename-file session-file-name new-session-file-name))
     (when session (setf (ellama-session-id session) new-id))
-    (when (equal ellama--current-session-id id)
+    (when (equal ellama--current-session-uid uid)
       (setq ellama--current-session-id new-id))
-    (remhash id ellama--active-sessions)
-    (puthash new-id buffer ellama--active-sessions)
     (when (and buffer ellama-session-auto-save)
       (with-current-buffer buffer
         (save-buffer)))))
@@ -1289,7 +1407,9 @@ If buffer contains ellama session return its id.
 Otherwire return id of current active session."
   (if ellama--current-session
       (ellama-session-id ellama--current-session)
-    ellama--current-session-id))
+    (if-let ((session (ellama--resolve-session)))
+        (ellama-session-id session)
+      ellama--current-session-id)))
 
 (defun ellama-get-current-session ()
   "Return current session.
@@ -1297,9 +1417,7 @@ If buffer contains ellama session return it.
 Otherwire return current active session."
   (if ellama--current-session
       ellama--current-session
-    (when ellama--current-session-id
-      (with-current-buffer (ellama-get-session-buffer 
ellama--current-session-id)
-       ellama--current-session))))
+    (ellama--resolve-session nil ellama--current-session-id)))
 
 (defun ellama-collapse-org-quotes ()
   "Collapse quote blocks in curent buffer."
@@ -1318,8 +1436,8 @@ Otherwire return current active session."
 (defun ellama-hide-quotes ()
   "Hide quotes in current session buffer if needed."
   (when-let* ((ellama-session-hide-org-quotes)
-             (session-id ellama--current-session-id)
-             (buf (ellama-get-session-buffer session-id)))
+              (session (ellama--resolve-session))
+              (buf (ellama-get-session-buffer (ellama--session-uid session))))
     (with-current-buffer buf
       (ellama-collapse-org-quotes))))
 
@@ -1702,34 +1820,33 @@ failure (with BUFFER current).
   (declare-function spinner-stop "ext:spinner")
   (declare-function ellama-context-prompt-with-context "ellama-context")
   (let* ((session-id (plist-get args :session-id))
-        (session (or (plist-get args :session)
-                     (when session-id
-                       (with-current-buffer (ellama-get-session-buffer 
session-id)
-                         ellama--current-session))))
-        (provider (if session
-                      (ellama-session-provider session)
-                    (or (plist-get args :provider)
-                        ellama-provider
-                        (ellama-get-first-ollama-chat-model))))
-        (buffer (or (plist-get args :buffer)
-                    (when (ellama-session-p session)
-                      (ellama-get-session-buffer (ellama-session-id session)))
-                    (current-buffer)))
-        (reasoning-buffer (get-buffer-create
-                           (concat (make-temp-name "*ellama-reasoning-") "*")))
-        (point (or (plist-get args :point)
-                   (with-current-buffer buffer (point))))
-        (replace-beg (plist-get args :replace-beg))
-        (replace-end (plist-get args :replace-end))
-        (replace-region-p (and replace-beg replace-end))
-        (filter (or (plist-get args :filter) #'identity))
-        (errcb (or (plist-get args :on-error)
-                   (lambda (msg)
-                     (error "Error calling the LLM: %s" msg))))
-        (donecb (or (plist-get args :on-done) #'ignore))
-        (prompt-with-ctx (ellama-context-prompt-with-context prompt))
-        (system (ellama-get-system-message (plist-get args :system)))
-        (session-tools (and session
+         (session (ellama--resolve-session
+                   (plist-get args :session)
+                   session-id))
+         (provider (if session
+                       (ellama-session-provider session)
+                     (or (plist-get args :provider)
+                         ellama-provider
+                         (ellama-get-first-ollama-chat-model))))
+         (buffer (or (plist-get args :buffer)
+                     (when (ellama-session-p session)
+                       (ellama-get-session-buffer (ellama--session-uid 
session)))
+                     (current-buffer)))
+         (reasoning-buffer (get-buffer-create
+                            (concat (make-temp-name "*ellama-reasoning-") 
"*")))
+         (point (or (plist-get args :point)
+                    (with-current-buffer buffer (point))))
+         (replace-beg (plist-get args :replace-beg))
+         (replace-end (plist-get args :replace-end))
+         (replace-region-p (and replace-beg replace-end))
+         (filter (or (plist-get args :filter) #'identity))
+         (errcb (or (plist-get args :on-error)
+                    (lambda (msg)
+                      (error "Error calling the LLM: %s" msg))))
+         (donecb (or (plist-get args :on-done) #'ignore))
+         (prompt-with-ctx (ellama-context-prompt-with-context prompt))
+         (system (ellama-get-system-message (plist-get args :system)))
+         (session-tools (and session
                              (ellama-session-extra session)
                              (plist-get (ellama-session-extra session) 
:tools)))
          (tools (or session-tools
@@ -1859,30 +1976,31 @@ last step only.
 
 :show BOOL - if BOOL show buffer for this step."
   (let* ((hd (car forms))
-        (tl (cdr forms))
-        (provider (or (plist-get hd :provider)
-                      ellama-provider
-                      (ellama-get-first-ollama-chat-model)))
-        (transform (plist-get hd :transform))
-        (prompt (if transform
-                    (apply transform (list initial-prompt acc))
-                  initial-prompt))
-        (session-id (plist-get hd :session-id))
-        (session (or (plist-get hd :session)
-                     (when session-id
-                       (with-current-buffer (ellama-get-session-buffer 
session-id)
-                         ellama--current-session))))
-        (chat (plist-get hd :chat))
-        (show (or (plist-get hd :show) ellama-always-show-chain-steps))
-        (buf (if (or (and (not chat)) (not session))
-                 (get-buffer-create (make-temp-name
-                                     (ellama-generate-name provider 
real-this-command prompt)))
-               (ellama-get-session-buffer ellama--current-session-id))))
+         (tl (cdr forms))
+         (provider (or (plist-get hd :provider)
+                       ellama-provider
+                       (ellama-get-first-ollama-chat-model)))
+         (transform (plist-get hd :transform))
+         (prompt (if transform
+                     (apply transform (list initial-prompt acc))
+                   initial-prompt))
+         (session-id (plist-get hd :session-id))
+         (session (ellama--resolve-session
+                   (plist-get hd :session)
+                   session-id))
+         (chat (plist-get hd :chat))
+         (show (or (plist-get hd :show) ellama-always-show-chain-steps))
+         (buf (if (or (and (not chat)) (not session))
+                  (get-buffer-create (make-temp-name
+                                      (ellama-generate-name provider 
real-this-command prompt)))
+                (ellama-get-session-buffer
+                 (or ellama--current-session-uid
+                     ellama--current-session-id)))))
     (when show
       (display-buffer buf (if chat (when ellama-chat-display-action-function
-                                    `((ignore . 
(,ellama-chat-display-action-function))))
-                           (when ellama-instant-display-action-function
-                             `((ignore . 
(,ellama-instant-display-action-function)))))))
+                                     `((ignore . 
(,ellama-chat-display-action-function))))
+                            (when ellama-instant-display-action-function
+                              `((ignore . 
(,ellama-instant-display-action-function)))))))
     (with-current-buffer buf
       (funcall ellama-major-mode))
     (if chat
@@ -2101,49 +2219,58 @@ ARGS contains keys for fine control.
 the full response text when the request completes (with BUFFER current)."
   (interactive "sAsk ellama: ")
   (let* ((providers (append
-                    `(("default model" . ellama-provider)
-                      ("ollama model" . (ellama-get-ollama-local-model)))
-                    ellama-providers))
-        (variants (mapcar #'car providers))
-        (system (plist-get args :system))
-        (donecb (plist-get args :on-done))
-        (provider (if current-prefix-arg
-                      (eval (alist-get
-                             (completing-read "Select model: " variants)
-                             providers nil nil #'string=))
-                    (or (plist-get args :provider)
-                        ellama-provider
-                        (ellama-get-first-ollama-chat-model))))
-        (ephemeral (plist-get args :ephemeral))
-        (session (or (plist-get args :session)
-                     (if (or create-session
-                             current-prefix-arg
-                             (and provider
-                                  (or (plist-get args :provider)
-                                      (not (equal provider ellama-provider)))
-                                  ellama--current-session-id
-                                  (with-current-buffer 
(ellama-get-session-buffer
-                                                        
ellama--current-session-id)
-                                    (not (equal
-                                          provider
-                                          (ellama-session-provider 
ellama--current-session)))))
-                             (and (not ellama--current-session)
-                                  (not ellama--current-session-id)))
-                         (ellama-new-session provider prompt ephemeral)
-                       (or ellama--current-session
-                           (with-current-buffer (ellama-get-session-buffer
-                                                 (or (plist-get args 
:session-id)
-                                                     
ellama--current-session-id))
-                             ellama--current-session)))))
-        (buffer (ellama-get-session-buffer
-                 (ellama-session-id session)))
-        (file-name (ellama-session-file session))
-        (translation-buffer (when ellama-chat-translation-enabled
-                              (if file-name
-                                  (progn
-                                    (find-file-noselect
-                                     (ellama--get-translation-file-name 
file-name)))
-                                (get-buffer-create (ellama-session-id 
session))))))
+                     `(("default model" . ellama-provider)
+                       ("ollama model" . (ellama-get-ollama-local-model)))
+                     ellama-providers))
+         (variants (mapcar #'car providers))
+         (system (plist-get args :system))
+         (donecb (plist-get args :on-done))
+         (provider (if current-prefix-arg
+                       (eval (alist-get
+                              (completing-read "Select model: " variants)
+                              providers nil nil #'string=))
+                     (or (plist-get args :provider)
+                         ellama-provider
+                         (ellama-get-first-ollama-chat-model))))
+         (ephemeral (plist-get args :ephemeral))
+         (explicit-session (ellama--resolve-session
+                            (plist-get args :session)
+                            (plist-get args :session-id)))
+         (current-session (or explicit-session
+                              (ellama--resolve-session nil 
ellama--current-session-id)))
+         (need-new-session (and (not explicit-session)
+                                (or create-session
+                                    current-prefix-arg
+                                    (and provider
+                                         current-session
+                                         (or (plist-get args :provider)
+                                             (not (equal provider 
ellama-provider)))
+                                         (not (equal provider
+                                                     (ellama-session-provider 
current-session))))
+                                    (not current-session))))
+         (session (or explicit-session
+                      (if need-new-session
+                          (ellama-new-session provider prompt ephemeral)
+                        current-session)))
+         (_ (unless (ellama-session-p session)
+              (error "Unable to resolve ellama session")))
+         (buffer (or (ellama-get-session-buffer
+                      (ellama--session-uid session))
+                     (if-let ((session-file (ellama-session-file session)))
+                         (find-file-noselect session-file)
+                       (get-buffer-create (ellama-session-id session)))))
+         (_ (with-current-buffer buffer
+              (setq ellama--current-session session)
+              (unless ellama-session-mode
+                (ellama-session-mode +1))))
+         (_ (ellama--register-session session buffer t))
+         (file-name (ellama-session-file session))
+         (translation-buffer (when ellama-chat-translation-enabled
+                               (if file-name
+                                   (progn
+                                     (find-file-noselect
+                                      (ellama--get-translation-file-name 
file-name)))
+                                 (get-buffer-create (ellama-session-id 
session))))))
     ;; Add C-c C-c shortcut when the chat buffer is in org-mode
     (with-current-buffer buffer
       (when (and
@@ -2862,10 +2989,10 @@ Call CALLBACK on result list of strings.  ARGS contains 
keys for fine control.
                      ellama-providers))
          (variants (mapcar #'car providers)))
     (setq ellama-provider
-         (eval (alist-get
-                (completing-read "Select model: " variants)
-                providers nil nil #'string=)))
-    (setq ellama--current-session-id nil)))
+          (eval (alist-get
+                 (completing-read "Select model: " variants)
+                 providers nil nil #'string=)))
+    (ellama--clear-current-session-selection)))
 
 ;;;###autoload
 (defun ellama-chat-translation-enable ()
diff --git a/tests/test-ellama-transient.el b/tests/test-ellama-transient.el
index 9f95def1b4..73e38c1a26 100644
--- a/tests/test-ellama-transient.el
+++ b/tests/test-ellama-transient.el
@@ -24,6 +24,8 @@
 
 ;;; Code:
 
+(add-to-list 'load-path default-directory)
+
 (require 'cl-lib)
 (require 'ellama)
 (require 'ellama-context)
@@ -113,6 +115,7 @@
   (let ((ellama-provider :default-provider)
         (ellama-coding-provider :coding-provider)
         (ellama--current-session-id "session-1")
+        (ellama--current-session-uid "uid-1")
         (providers '("ellama-provider" "ellama-coding-provider"))
         (values '(:new-default :new-coding)))
     (cl-letf (((symbol-function 'completing-read)
@@ -126,40 +129,38 @@
       (ellama-transient-set-provider)
       (should (eq ellama-provider :new-default))
       (should-not ellama--current-session-id)
+      (should-not ellama--current-session-uid)
       (setq ellama--current-session-id "session-2")
+      (setq ellama--current-session-uid "uid-2")
       (ellama-transient-set-provider)
       (should (eq ellama-coding-provider :new-coding))
-      (should (equal ellama--current-session-id "session-2")))))
+      (should (equal ellama--current-session-id "session-2"))
+      (should (equal ellama--current-session-uid "uid-2")))))
 
 (ert-deftest
     
test-ellama-transient-model-get-from-current-session-guard-and-provider-flow
-  ()
+    ()
   (let ((called 0))
-    (cl-letf (((symbol-function 'ellama-fill-transient-ollama-model)
+    (cl-letf (((symbol-function 'ellama-get-current-session)
+               (lambda () nil))
+              ((symbol-function 'ellama-fill-transient-ollama-model)
                (lambda (&rest _args)
                  (cl-incf called))))
-      (let ((ellama--current-session-id nil))
-        (ellama-transient-model-get-from-current-session))
+      (ellama-transient-model-get-from-current-session)
       (should (= called 0))))
-  (let ((buffer (generate-new-buffer " *ellama-transient-test-session*"))
-        (ellama--current-session-id "session-id")
-        (ellama--current-session
-         (make-ellama-session :provider :session-provider))
-        provided-id
+  (let ((session (make-ellama-session :provider :session-provider))
+        provided-session
         provided-provider)
-    (unwind-protect
-        (cl-letf (((symbol-function 'ellama-get-session-buffer)
-                   (lambda (id)
-                     (setq provided-id id)
-                     buffer))
-                  ((symbol-function 'ellama-fill-transient-ollama-model)
-                   (lambda (provider)
-                     (setq provided-provider provider))))
-          (ellama-transient-model-get-from-current-session)
-          (should (equal provided-id "session-id"))
-          (should (eq provided-provider :session-provider)))
-      (when (buffer-live-p buffer)
-        (kill-buffer buffer)))))
+    (cl-letf (((symbol-function 'ellama-get-current-session)
+               (lambda ()
+                 (setq provided-session session)
+                 session))
+              ((symbol-function 'ellama-fill-transient-ollama-model)
+               (lambda (provider)
+                 (setq provided-provider provider))))
+      (ellama-transient-model-get-from-current-session)
+      (should (eq provided-session session))
+      (should (eq provided-provider :session-provider)))))
 
 (ert-deftest test-ellama-transient-set-system-region-and-prompt-paths ()
   (let ((ellama-global-system "old"))
diff --git a/tests/test-ellama.el b/tests/test-ellama.el
index 3c0b48f0f1..e8dca852b8 100644
--- a/tests/test-ellama.el
+++ b/tests/test-ellama.el
@@ -24,6 +24,8 @@
 
 ;;; Code:
 
+(add-to-list 'load-path default-directory)
+
 (require 'cl-lib)
 (require 'ellama)
 (require 'ellama-transient)
@@ -1299,6 +1301,81 @@ region, season, or type)! 🍎🍊"))))
     (should (equal (ellama-get-nick-prefix-for-mode) "##"))
     (should (equal (ellama-get-session-file-extension) "md"))))
 
+(ert-deftest test-ellama-chat-rehydrate-after-revert ()
+  (let* ((provider (make-llm-fake :chat-action-func (lambda () "ok")))
+         (ellama-provider provider)
+         (ellama-coding-provider provider)
+         (ellama-major-mode 'text-mode)
+         (ellama-session-auto-save t)
+         (ellama-spinner-enabled nil)
+         (ellama-response-process-method 'sync)
+         (ellama-sessions-directory (make-temp-file "ellama-test-" t))
+         (session (ellama-new-session provider "initial prompt"))
+         (uid (ellama--session-uid session))
+         (buffer (ellama-get-session-buffer uid))
+         file-name)
+    (unwind-protect
+        (progn
+          (with-current-buffer buffer
+            (save-buffer)
+            (setq file-name (buffer-file-name)))
+          (with-temp-file file-name
+            (insert "External update\n"))
+          (with-current-buffer buffer
+            (revert-buffer :ignore-auto :noconfirm)
+            (should (ellama-session-p (ellama--resolve-session nil uid))))
+          (cl-letf (((symbol-function 'ellama-stream)
+                     (lambda (&rest _args) :stubbed)))
+            (ellama-chat "repro prompt"))
+          (with-current-buffer buffer
+            (should ellama-session-mode)
+            (should (ellama-session-p ellama--current-session)))
+          (should (equal ellama--current-session-uid uid))
+          (should (equal ellama--current-session-id
+                         (ellama-session-id session))))
+      (when (buffer-live-p buffer)
+        (kill-buffer buffer)))))
+
+(ert-deftest test-ellama-read-legacy-session-adds-uid ()
+  (let* ((file-name (make-temp-file "ellama-session-" nil ".el"))
+         (legacy-session (make-ellama-session
+                          :id "legacy"
+                          :provider :provider
+                          :prompt "prompt"
+                          :extra '(:dir "/tmp"))))
+    (unwind-protect
+        (progn
+          (with-temp-file file-name
+            (insert (prin1-to-string legacy-session)))
+          (let ((session (ellama--read-session-from-file file-name)))
+            (should (ellama-session-p session))
+            (should (equal (ellama-session-id session) "legacy"))
+            (should (equal (ellama--session-uid session) "legacy"))))
+      (delete-file file-name t))))
+
+(ert-deftest test-ellama-session-rename-keep-uid ()
+  (let* ((provider (make-llm-fake :chat-action-func (lambda () "ok")))
+         (ellama-provider provider)
+         (ellama-coding-provider provider)
+         (ellama-major-mode 'text-mode)
+         (ellama-session-auto-save nil)
+         (session (ellama-new-session provider "initial prompt" t))
+         (uid (ellama--session-uid session))
+         (buffer (ellama-get-session-buffer uid)))
+    (unwind-protect
+        (with-current-buffer buffer
+          (cl-letf (((symbol-function 'read-string)
+                     (lambda (&rest _args) "renamed-session")))
+            (ellama-session-rename))
+          (should (equal (ellama-session-id ellama--current-session)
+                         "renamed-session"))
+          (should (equal (ellama--session-uid ellama--current-session) uid))
+          (should (eq (gethash uid ellama--active-session-states)
+                      ellama--current-session))
+          (should (equal ellama--current-session-id "renamed-session")))
+      (when (buffer-live-p buffer)
+        (kill-buffer buffer)))))
+
 (provide 'test-ellama)
 
 ;;; test-ellama.el ends here


Reply via email to