Skip to content
Merged
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
31 changes: 12 additions & 19 deletions ai-code-backends-infra.el
Original file line number Diff line number Diff line change
Expand Up @@ -496,18 +496,20 @@ MULTILINE-INPUT-SEQUENCE configures `S-<return>' and `C-<return>' when non-nil."
(puthash key session-buffer ai-code-backends-infra--file-session-map)
(remhash key ai-code-backends-infra--file-session-map))))

(defun ai-code-backends-infra--attached-file-session (prefix source-buffer working-dir)
"Return attached session state for PREFIX, SOURCE-BUFFER and WORKING-DIR.
(defun ai-code-backends-infra--attached-file-session (prefix source-buffer _working-dir)
"Return attached session state for PREFIX and SOURCE-BUFFER.
The working-directory argument is accepted for interface compatibility but
ignored here because
an explicit file attachment should win as long as the attached buffer is live.
Return a cons of (BUFFER . MISSING-P)."
(let ((key (ai-code-backends-infra--file-session-map-key prefix source-buffer)))
(if (null key)
(cons nil nil)
(let* ((attached (gethash key ai-code-backends-infra--file-session-map))
(valid (and (buffer-live-p attached)
(memq attached
(ai-code-backends-infra--find-session-buffers
prefix
working-dir)))))
(ai-code-backends-infra--parse-session-buffer-name
(buffer-name attached)
prefix))))
(cond
(valid
(cons attached nil))
Expand All @@ -523,25 +525,16 @@ Return a cons of (BUFFER . MISSING-P)."
MISSING-MESSAGE is used when no target session exists.
When PREFIX and WORKING-DIR are present, prefer the attached session for
SOURCE-BUFFER unless FORCE-PROMPT is non-nil."
(let* ((file-key (and prefix
source-buffer
(ai-code-backends-infra--file-session-map-key
prefix
source-buffer)))
(attached-state (and prefix working-dir
(let* ((attached-state (and prefix working-dir
(ai-code-backends-infra--attached-file-session
prefix
source-buffer
working-dir)))
(attached-buffer (car-safe attached-state))
(attached-missing (cdr-safe attached-state))
(needs-initial-file-selection (and (null buffer-name)
file-key
(null attached-buffer)
(not attached-missing)))
(effective-force-prompt (or force-prompt
attached-missing
needs-initial-file-selection))
(effective-force-prompt
(or force-prompt
attached-missing))
Comment on lines +535 to +537
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Force selection for first file binding with multiple sessions

Dropping needs-initial-file-selection from effective-force-prompt means a newly opened file with no existing attachment now silently reuses whatever repo session is remembered. In a repo with multiple active sessions, that remembered buffer can belong to an unrelated task, so the first send/switch for the file can bind it to the wrong session and persist that mapping without any chooser prompt. The prior logic explicitly forced a prompt for this first-time file association, so this is a behavior regression introduced here.

Useful? React with 👍 / 👎.

(buffer (or (and buffer-name (get-buffer buffer-name))
(and attached-buffer (not force-prompt) attached-buffer)
(and prefix working-dir
Expand Down
128 changes: 116 additions & 12 deletions test/test_ai-code-backends-infra.el
Original file line number Diff line number Diff line change
Expand Up @@ -361,13 +361,14 @@
(when (buffer-live-p buf)
(kill-buffer buf))))))

(ert-deftest test-ai-code-backends-infra-send-line-unassociated-file-forces-selection ()
"Unassociated file should force session selection even if a session is remembered."
(ert-deftest test-ai-code-backends-infra-send-line-unassociated-file-reuses-remembered-session ()
"Unassociated file should reuse the remembered repo session."
(let* ((prefix "codex")
(working-dir "/tmp/ai-code-file-new-association/")
(source (generate-new-buffer " *ai-code-source-new-association*"))
(session-a (get-buffer-create "*codex[file-new-association:a]*"))
(session-b (get-buffer-create "*codex[file-new-association:b]*"))
(selection-count 0)
(force-prompts nil)
(send-targets nil))
(unwind-protect
Expand All @@ -383,15 +384,14 @@
(setq-local ai-code-backends-infra--session-directory working-dir))
(with-current-buffer session-b
(setq-local ai-code-backends-infra--session-directory working-dir))
;; Simulate repo-level remembered session (the previous behavior picked this directly).
;; Simulate the current repo-level active/remembered session.
(ai-code-backends-infra--remember-session-buffer prefix working-dir session-b)

(cl-letf (((symbol-function 'ai-code-backends-infra--select-session-buffer)
(lambda (_prefix _dir &optional force-prompt)
(setq selection-count (1+ selection-count))
(push force-prompt force-prompts)
(if (= (length force-prompts) 1)
session-a
(ert-fail "Should not prompt again once file is associated."))))
session-b))
((symbol-function 'ai-code-backends-infra--terminal-send-string)
(lambda (&rest _args)
(push (buffer-name (current-buffer)) send-targets)))
Expand All @@ -405,14 +405,15 @@
(ai-code-backends-infra--send-line-to-session
nil "missing" "line-2" prefix working-dir)))

(should (equal (nreverse force-prompts) (list t)))
(should (= selection-count 1))
(should (equal (nreverse force-prompts) (list nil)))
(should (equal (nreverse send-targets)
(list "*codex[file-new-association:a]*"
"*codex[file-new-association:a]*")))
(list "*codex[file-new-association:b]*"
"*codex[file-new-association:b]*")))
(should (eq (gethash
(ai-code-backends-infra--file-session-map-key prefix source)
ai-code-backends-infra--file-session-map)
session-a)))
session-b)))
(dolist (buf (list source session-a session-b))
(when (buffer-live-p buf)
(kill-buffer buf))))))
Expand Down Expand Up @@ -467,7 +468,7 @@
(ai-code-backends-infra--send-line-to-session
nil "missing" "line-2" prefix working-dir)))

(should (equal (nreverse force-prompts) (list t t)))
(should (equal (nreverse force-prompts) (list nil t)))
(should (equal (nreverse send-targets)
(list "*codex[file-rebind:a]*"
"*codex[file-rebind:b]*")))
Expand All @@ -477,6 +478,60 @@
(when (buffer-live-p buf)
(kill-buffer buf))))))

(ert-deftest test-ai-code-backends-infra-switch-new-file-prompts-when-multiple-sessions-active ()
"A newly opened file should prompt when multiple repo sessions are active."
(let* ((prefix "codex")
(working-dir "/tmp/ai-code-file-multi-active/")
(source (generate-new-buffer " *ai-code-source-multi-active*"))
(session-a (get-buffer-create "*codex[file-multi-active:a]*"))
(session-b (get-buffer-create "*codex[file-multi-active:b]*"))
(captured-collection nil)
(captured-default nil))
(unwind-protect
(progn
(clrhash ai-code-backends-infra--directory-buffer-map)
(when (boundp 'ai-code-backends-infra--file-session-map)
(clrhash ai-code-backends-infra--file-session-map))

(with-current-buffer source
(setq buffer-file-name "/tmp/ai-code-file-multi-active/main.el")
(setq default-directory working-dir))
(with-current-buffer session-a
(setq-local ai-code-backends-infra--session-directory working-dir))
(with-current-buffer session-b
(setq-local ai-code-backends-infra--session-directory working-dir))

(cl-letf (((symbol-function 'ai-code-backends-infra--find-session-buffers)
(lambda (_prefix _dir)
(list session-a session-b)))
((symbol-function 'completing-read)
(lambda (_prompt collection _predicate _require-match
&optional _initial-input _hist def &rest _)
(setq captured-collection collection)
(setq captured-default def)
"b"))
((symbol-function 'get-buffer-window)
(lambda (&rest _args) nil))
((symbol-function 'ai-code-backends-infra--display-buffer-in-side-window)
(lambda (_buffer) nil)))
(with-current-buffer source
(ai-code-backends-infra--switch-to-session-buffer
nil
"missing"
prefix
working-dir
nil)))

(should (equal captured-collection '("a" "b")))
(should (equal captured-default "a"))
(should (eq (gethash
(ai-code-backends-infra--file-session-map-key prefix source)
ai-code-backends-infra--file-session-map)
session-b)))
(dolist (buf (list source session-a session-b))
(when (buffer-live-p buf)
(kill-buffer buf))))))

(ert-deftest test-ai-code-backends-infra-switch-force-prompt-prioritizes-attached-session ()
"Force prompt should place attached file session at the top and as default."
(let* ((prefix "codex")
Expand Down Expand Up @@ -633,7 +688,7 @@
(ai-code-backends-infra--send-line-to-session
nil "missing" "line-2" prefix working-dir)))

(should (equal (nreverse force-prompts) (list t t)))
(should (equal (nreverse force-prompts) (list nil t)))
(should (equal (nreverse send-targets)
(list "*codex[file-missing:a]*"
"*codex[file-missing:b]*")))
Expand All @@ -645,6 +700,55 @@
(when (buffer-live-p buf)
(kill-buffer buf))))))

(ert-deftest test-ai-code-backends-infra-switch-reuses-live-attached-session-despite-working-dir-mismatch ()
"Reuse a live attached session even when WORKING-DIR no longer matches it."
(let* ((prefix "codex")
(session-dir "/tmp/ai-code-file-attached-root/")
(working-dir "/tmp/ai-code-file-attached-root/subdir/")
(source (generate-new-buffer " *ai-code-source-attached-live*"))
(attached (get-buffer-create "*codex[file-attached-root:attached]*"))
(displayed nil))
(unwind-protect
(progn
(clrhash ai-code-backends-infra--directory-buffer-map)
(when (boundp 'ai-code-backends-infra--file-session-map)
(clrhash ai-code-backends-infra--file-session-map))

(with-current-buffer source
(setq buffer-file-name "/tmp/ai-code-file-attached-root/main.el")
(setq default-directory working-dir))
(with-current-buffer attached
(setq-local ai-code-backends-infra--session-directory session-dir))
(ai-code-backends-infra--remember-file-session-buffer prefix source attached)

(cl-letf (((symbol-function 'ai-code-backends-infra--find-session-buffers)
(lambda (_prefix _dir) nil))
((symbol-function 'ai-code-backends-infra--select-session-buffer)
(lambda (&rest _args)
(ert-fail "Should reuse the live attached session without prompting.")))
((symbol-function 'get-buffer-window)
(lambda (&rest _args) nil))
((symbol-function 'ai-code-backends-infra--display-buffer-in-side-window)
(lambda (buffer)
(setq displayed buffer)
nil)))
(with-current-buffer source
(ai-code-backends-infra--switch-to-session-buffer
nil
"missing"
prefix
working-dir
nil)))

(should (eq displayed attached))
(should (eq (gethash
(ai-code-backends-infra--file-session-map-key prefix source)
ai-code-backends-infra--file-session-map)
attached)))
(dolist (buf (list source attached))
(when (buffer-live-p buf)
(kill-buffer buf))))))

(ert-deftest test-ai-code-backends-infra-toggle-or-create-session-passes-env-vars ()
"ENV-VARS are forwarded to `ai-code-backends-infra--create-terminal-session'."
(let* ((ai-code-backends-infra--processes (make-hash-table :test 'equal))
Expand Down