Skip to content

Commit a23785b

Browse files
authored
Make session resume use pi's session file (#206)
Pi may store sessions outside ~/.pi/agent/sessions, for example when the user sets PI_CODING_AGENT_DIR or resumes from a custom path. Use the sessionFile reported by get_state and offer valid sibling session files from that directory instead of reconstructing pi's storage path in Emacs. If cached state has no session file yet, refresh get_state before listing sessions rather than making the user retry. Filter out JSONL files that are not pi sessions so arbitrary sibling files are not offered in the resume picker. Fixes #205.
1 parent 9077354 commit a23785b

3 files changed

Lines changed: 367 additions & 197 deletions

File tree

pi-coding-agent-menu.el

Lines changed: 117 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -189,14 +189,36 @@ from either chat or input buffer."
189189
(message "Pi: New session started"))
190190
(message "Pi: New session cancelled")))))))
191191

192-
(defun pi-coding-agent--session-dir-name (dir)
193-
"Convert DIR to session directory name.
194-
Matches pi's encoding: --path-with-dashes--.
195-
Note: Handles both Unix and Windows path separators."
196-
(let* ((clean-dir (directory-file-name dir)) ; Remove trailing slash
197-
(safe-path (replace-regexp-in-string "[/\\\\:]" "-"
198-
(replace-regexp-in-string "^[/\\\\]" "" clean-dir))))
199-
(concat "--" safe-path "--")))
192+
(defun pi-coding-agent--session-list-directory (&optional chat-buf)
193+
"Return the directory containing CHAT-BUF's current JSONL session file.
194+
Return nil when the current state has no usable session file. Relative
195+
session file names are resolved from the chat buffer's stable session
196+
directory."
197+
(let ((chat-buf (or chat-buf (pi-coding-agent--get-chat-buffer))))
198+
(when (and chat-buf (buffer-live-p chat-buf))
199+
(with-current-buffer chat-buf
200+
(when-let* ((session-file (plist-get pi-coding-agent--state
201+
:session-file))
202+
((stringp session-file))
203+
((not (string-empty-p session-file))))
204+
(file-name-directory
205+
(expand-file-name session-file
206+
(pi-coding-agent--chat-session-directory
207+
chat-buf))))))))
208+
209+
(defun pi-coding-agent--with-session-list-directory (proc chat-buf callback)
210+
"Call CALLBACK with the session list directory for CHAT-BUF.
211+
When cached state has no session file, fetch fresh state from PROC first."
212+
(if-let* ((session-dir (pi-coding-agent--session-list-directory chat-buf)))
213+
(funcall callback session-dir)
214+
(pi-coding-agent--rpc-async proc '(:type "get_state")
215+
(lambda (response)
216+
(when (and (plist-get response :success)
217+
(buffer-live-p chat-buf))
218+
(pi-coding-agent--apply-state-response chat-buf response))
219+
(when (buffer-live-p chat-buf)
220+
(funcall callback
221+
(pi-coding-agent--session-list-directory chat-buf)))))))
200222

201223
(defun pi-coding-agent--session-metadata (path)
202224
"Extract metadata from session file PATH.
@@ -251,43 +273,43 @@ Call this from the chat buffer after switching or loading a session."
251273
(let ((metadata (pi-coding-agent--session-metadata session-file)))
252274
(setq pi-coding-agent--session-name (plist-get metadata :session-name)))))
253275

254-
(defun pi-coding-agent--list-sessions (dir)
255-
"List available session files for project DIR.
256-
Returns list of absolute paths to .jsonl files, sorted by modification
257-
time with most recently used first."
258-
(let* ((sessions-base (expand-file-name "~/.pi/agent/sessions/"))
259-
(session-dir (expand-file-name (pi-coding-agent--session-dir-name dir) sessions-base)))
260-
(when (file-directory-p session-dir)
261-
;; Sort by modification time descending (most recently used first)
262-
(sort (directory-files session-dir t "\\.jsonl$")
276+
(defun pi-coding-agent--list-sessions (session-dir)
277+
"List valid session files in SESSION-DIR.
278+
Returns absolute paths to JSONL pi sessions, sorted by modification time with
279+
most recently used first."
280+
(when (and session-dir (file-directory-p session-dir))
281+
(let ((sessions (delq nil
282+
(mapcar (lambda (path)
283+
(and (pi-coding-agent--session-metadata path)
284+
path))
285+
(directory-files session-dir t
286+
"\\.jsonl\\'")))))
287+
(sort sessions
263288
(lambda (a b)
264-
(time-less-p (file-attribute-modification-time (file-attributes b))
265-
(file-attribute-modification-time (file-attributes a))))))))
289+
(time-less-p (file-attribute-modification-time
290+
(file-attributes b))
291+
(file-attribute-modification-time
292+
(file-attributes a))))))))
266293

267294
(defun pi-coding-agent--format-session-choice (path)
268295
"Format session PATH for display in selector.
269-
Returns (display-string . path) for `completing-read'.
270-
Prefers session name over first message when available."
271-
(let ((metadata (pi-coding-agent--session-metadata path)))
272-
(if metadata
273-
(let* ((modified-time (plist-get metadata :modified-time))
274-
(session-name (plist-get metadata :session-name))
275-
(first-msg (plist-get metadata :first-message))
276-
(msg-count (plist-get metadata :message-count))
277-
(relative-time (pi-coding-agent--format-relative-time modified-time))
278-
;; Prefer session name, fall back to first message preview
279-
(label (cond
280-
(session-name (pi-coding-agent--truncate-string session-name 50))
281-
(first-msg (pi-coding-agent--truncate-string first-msg 50))
282-
(t nil)))
283-
(display (if label
284-
(format "%s · %s (%d msgs)"
285-
label relative-time msg-count)
286-
(format "[empty session] · %s" relative-time))))
287-
(cons display path))
288-
;; Fallback to filename if metadata extraction fails
289-
(let ((filename (file-name-nondirectory path)))
290-
(cons filename path)))))
296+
Returns (display-string . path) for `completing-read', or nil when PATH is not
297+
a valid pi session. Prefers session name over first message when available."
298+
(when-let* ((metadata (pi-coding-agent--session-metadata path)))
299+
(let* ((modified-time (plist-get metadata :modified-time))
300+
(session-name (plist-get metadata :session-name))
301+
(first-msg (plist-get metadata :first-message))
302+
(msg-count (plist-get metadata :message-count))
303+
(relative-time (pi-coding-agent--format-relative-time modified-time))
304+
(label (cond
305+
(session-name (pi-coding-agent--truncate-string session-name 50))
306+
(first-msg (pi-coding-agent--truncate-string first-msg 50))
307+
(t nil)))
308+
(display (if label
309+
(format "%s · %s (%d msgs)"
310+
label relative-time msg-count)
311+
(format "[empty session] · %s" relative-time))))
312+
(cons display path))))
291313

292314
(defun pi-coding-agent--reset-session-state ()
293315
"Reset all session-specific state for a new session.
@@ -465,43 +487,65 @@ chat buffer from session history."
465487
(message "Pi: Failed to reload - %s"
466488
(or (plist-get response :error) "unknown error"))))))))))))
467489

490+
(defun pi-coding-agent--resume-selected-session (proc chat-buf selected-path)
491+
"Resume SELECTED-PATH using PROC and rebuild CHAT-BUF from its history."
492+
(pi-coding-agent--rpc-async
493+
proc
494+
(list :type "switch_session" :sessionPath selected-path)
495+
(lambda (response)
496+
(let* ((data (plist-get response :data))
497+
(cancelled (plist-get data :cancelled)))
498+
(if (and (plist-get response :success)
499+
(pi-coding-agent--json-false-p cancelled))
500+
(progn
501+
(pi-coding-agent--refresh-session-state proc chat-buf selected-path)
502+
(pi-coding-agent--load-session-history
503+
proc
504+
(lambda (count)
505+
(message "Pi: Resumed session (%d messages)" count))
506+
chat-buf))
507+
(message "Pi: Failed to resume session"))))))
508+
509+
(defun pi-coding-agent--resume-session-from-directory (proc chat-buf session-dir)
510+
"Prompt for a session from SESSION-DIR, then resume it using PROC.
511+
CHAT-BUF is rebuilt from the selected session history."
512+
(let ((sessions (pi-coding-agent--list-sessions session-dir)))
513+
(if (null sessions)
514+
(message "Pi: No previous sessions found")
515+
(let* ((choices (delq nil
516+
(mapcar #'pi-coding-agent--format-session-choice
517+
sessions)))
518+
(choice-strings (mapcar #'car choices)))
519+
(if (null choices)
520+
(message "Pi: No previous sessions found")
521+
(let* ((choice (completing-read
522+
"Resume session: "
523+
(lambda (string pred action)
524+
(if (eq action 'metadata)
525+
'(metadata (display-sort-function . identity))
526+
(complete-with-action action choice-strings
527+
string pred)))
528+
nil t))
529+
(selected-path (cdr (assoc choice choices))))
530+
(when selected-path
531+
(pi-coding-agent--resume-selected-session
532+
proc chat-buf selected-path))))))))
533+
468534
(defun pi-coding-agent-resume-session ()
469-
"Resume a previous pi session from the current project."
535+
"Resume a previous pi session stored beside the current session file."
470536
(interactive)
471537
(when-let* ((proc (pi-coding-agent--get-process))
472-
(dir (pi-coding-agent--session-directory))
473538
(chat-buf (pi-coding-agent--get-chat-buffer)))
474539
(when (pi-coding-agent--session-transition-ready-p chat-buf "resume")
475-
(let ((sessions (pi-coding-agent--list-sessions dir)))
476-
(if (null sessions)
477-
(message "Pi: No previous sessions found")
478-
(let* ((choices (mapcar #'pi-coding-agent--format-session-choice sessions))
479-
(choice-strings (mapcar #'car choices))
480-
(choice (completing-read "Resume session: "
481-
(lambda (string pred action)
482-
(if (eq action 'metadata)
483-
'(metadata (display-sort-function . identity))
484-
(complete-with-action action choice-strings string pred)))
485-
nil t))
486-
(selected-path (cdr (assoc choice choices))))
487-
(when selected-path
488-
(pi-coding-agent--rpc-async
489-
proc
490-
(list :type "switch_session" :sessionPath selected-path)
491-
(lambda (response)
492-
(let* ((data (plist-get response :data))
493-
(cancelled (plist-get data :cancelled)))
494-
(if (and (plist-get response :success)
495-
(pi-coding-agent--json-false-p cancelled))
496-
(progn
497-
(pi-coding-agent--refresh-session-state
498-
proc chat-buf selected-path)
499-
(pi-coding-agent--load-session-history
500-
proc
501-
(lambda (count)
502-
(message "Pi: Resumed session (%d messages)" count))
503-
chat-buf))
504-
(message "Pi: Failed to resume session"))))))))))))
540+
(pi-coding-agent--with-session-list-directory
541+
proc chat-buf
542+
(lambda (session-dir)
543+
(cond
544+
((not session-dir)
545+
(message "Pi: Session file not available"))
546+
((pi-coding-agent--session-transition-ready-p chat-buf "resume")
547+
(pi-coding-agent--resume-session-from-directory
548+
proc chat-buf session-dir))))))))
505549

506550
;;;; Model and Thinking
507551

test/pi-coding-agent-input-test.el

Lines changed: 0 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1367,48 +1367,6 @@ This ensures history loads correctly when callback runs in arbitrary context."
13671367
(when (buffer-live-p chat-buf)
13681368
(kill-buffer chat-buf)))))
13691369

1370-
(ert-deftest pi-coding-agent-test-session-dir-name ()
1371-
"Session directory name derived from project path."
1372-
(should (equal (pi-coding-agent--session-dir-name "/home/daniel/co/pi-coding-agent")
1373-
"--home-daniel-co-pi-coding-agent--"))
1374-
(should (equal (pi-coding-agent--session-dir-name "/tmp/test")
1375-
"--tmp-test--")))
1376-
1377-
(ert-deftest pi-coding-agent-test-list-sessions-sorted-by-mtime ()
1378-
"Sessions are sorted by modification time, most recent first.
1379-
Regression test for #25: sessions were sorted by filename (creation time)
1380-
and then re-sorted alphabetically by completing-read."
1381-
(let* ((temp-base (make-temp-file "pi-coding-agent-sessions-" t))
1382-
(session-dir (expand-file-name "--test-project--" temp-base))
1383-
;; Create files with names that would sort differently alphabetically
1384-
(old-file (expand-file-name "2024-01-01_10-00-00.jsonl" session-dir))
1385-
(new-file (expand-file-name "2024-01-01_09-00-00.jsonl" session-dir)))
1386-
(unwind-protect
1387-
(progn
1388-
(make-directory session-dir t)
1389-
(let* ((now (current-time))
1390-
(old-time (time-subtract now (seconds-to-time 10)))
1391-
(new-time (time-subtract now (seconds-to-time 5))))
1392-
;; Create "old" file first
1393-
(with-temp-file old-file (insert "{}"))
1394-
(set-file-times old-file old-time)
1395-
;; Create "new" file second (more recent mtime despite earlier filename)
1396-
(with-temp-file new-file (insert "{}"))
1397-
(set-file-times new-file new-time))
1398-
;; Directly call directory-files and sort logic to test sorting
1399-
(let* ((files (directory-files session-dir t "\\.jsonl$"))
1400-
(sorted (sort files
1401-
(lambda (a b)
1402-
(time-less-p
1403-
(file-attribute-modification-time (file-attributes b))
1404-
(file-attribute-modification-time (file-attributes a)))))))
1405-
;; new-file should be first (most recent mtime)
1406-
;; even though "09-00-00" < "10-00-00" alphabetically
1407-
(should (equal (length sorted) 2))
1408-
(should (string-suffix-p "09-00-00.jsonl" (car sorted)))))
1409-
;; Cleanup
1410-
(delete-directory temp-base t))))
1411-
14121370
(ert-deftest pi-coding-agent-test-session-metadata-extracts-first-message ()
14131371
"pi-coding-agent--session-metadata extracts first user message text."
14141372
(let ((temp-file (make-temp-file "pi-coding-agent-test-session" nil ".jsonl")))

0 commit comments

Comments
 (0)