Skip to content
Draft
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
40 changes: 32 additions & 8 deletions ai-code-backends-infra.el
Original file line number Diff line number Diff line change
Expand Up @@ -683,9 +683,16 @@ from the window where it was initially created."

;;; Session Helpers

(defun ai-code-backends-infra--session-working-directory ()
"Return the working directory, preferring the current project root."
(ai-code--session-project-root))
(defun ai-code-backends-infra--session-working-directory (&optional prompt-p)
"Return the working directory, preferring the current project root.
When PROMPT-P is non-nil, prompt with the computed directory as the default."
(let ((working-dir (ai-code--session-project-root)))
(if prompt-p
(read-directory-name "Working directory: "
working-dir
working-dir
t)
working-dir)))

(defun ai-code-backends-infra--normalize-session-directory (directory)
"Return DIRECTORY normalized for robust session matching."
Expand Down Expand Up @@ -1221,12 +1228,14 @@ OPTIONS is a plist with these keys:
:prepare-launch is an optional function called with (WORKING-DIR COMMAND).
When :prepare-launch is present, it may return :command, :cleanup-fn, and
:post-start-fn entries to customize session creation."
(let* ((working-dir (ai-code-backends-infra--session-working-directory))
(resolved (ai-code-backends-infra--resolve-start-command
(let* ((resolved (ai-code-backends-infra--resolve-start-command
(plist-get options :program)
(plist-get options :switches)
arg
(plist-get options :label)))
(working-dir (if arg
(ai-code-backends-infra--session-working-directory arg)
(ai-code-backends-infra--session-working-directory)))
(command (plist-get resolved :command))
(launch (when-let ((prepare-launch (plist-get options :prepare-launch)))
(funcall prepare-launch working-dir command)))
Expand All @@ -1247,6 +1256,19 @@ When :prepare-launch is present, it may return :command, :cleanup-fn, and
(plist-get options :multiline-input-sequence)
post-start-fn)))

(defun ai-code-backends-infra--last-accessed-session-buffer (session-prefix)
"Return the last accessed buffer when it belongs to SESSION-PREFIX."
(let ((buffer ai-code-backends-infra--last-accessed-buffer))
(when (and (buffer-live-p buffer)
(with-current-buffer buffer
(or (and (stringp ai-code-backends-infra--session-prefix)
(string= ai-code-backends-infra--session-prefix
session-prefix))
(ai-code-backends-infra--parse-session-buffer-name
(buffer-name buffer)
session-prefix))))
buffer)))

(defun ai-code-backends-infra--cli-switch-to-buffer (label session-prefix force-prompt)
"Switch to a CLI backend session.
LABEL is used in the missing-session message. SESSION-PREFIX identifies
Expand Down Expand Up @@ -1274,9 +1296,11 @@ the backend session group."
(defun ai-code-backends-infra--cli-show-resume-picker (session-prefix)
"Poke the resumed SESSION-PREFIX buffer so the CLI picker is shown."
(let* ((working-dir (ai-code-backends-infra--session-working-directory))
(buffer (ai-code-backends-infra--select-session-buffer
session-prefix
working-dir)))
(buffer (or (ai-code-backends-infra--last-accessed-session-buffer
session-prefix)
(ai-code-backends-infra--select-session-buffer
session-prefix
working-dir))))
(when buffer
(with-current-buffer buffer
(sit-for 0.5)
Expand Down
9 changes: 7 additions & 2 deletions ai-code-backends.el
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
(defvar ai-code-cli)
(defvar claude-code-terminal-backend)

(eval-when-compile
(defvar ai-code-selected-backend))

(declare-function claude-code--do-send-command "claude-code" (cmd))
(declare-function claude-code--term-send-string "claude-code" (backend string))
(declare-function ai-code--validate-git-repository "ai-code-git" ())
Expand Down Expand Up @@ -113,7 +116,9 @@ Argument _ARG is ignored."
;;;###autoload
(defun ai-code-cli-start (&optional arg)
"Start the current backend's CLI session when supported.
Argument ARG is passed to the backend's start function."
Argument ARG is passed to the backend's start function.
Interactively, a prefix argument keeps the existing CLI-args prompt and
also prompts for the working directory before the session starts."
Comment on lines 118 to +121
(interactive "P")
(ai-code--activate-effective-backend)
(prog1
Expand All @@ -131,7 +136,7 @@ Noninteractive callers pass ARG to the backend resume function.
When called interactively, any prefix argument is forwarded via
`current-prefix-arg', and it is up to the backend how to interpret
it (for example, some backends may use a non-nil prefix to prompt for
additional CLI arguments)."
additional CLI arguments and a working directory)."
(interactive "P")
(ai-code--activate-effective-backend)
(prog1
Expand Down
4 changes: 2 additions & 2 deletions ai-code-kilo.el
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,8 @@ If current buffer belongs to a project, start in the project's root
directory. Otherwise start in the directory of the current buffer file,
or the current value of `default-directory' if no project and no buffer file.

With double prefix ARG (\\[universal-argument] \\[universal-argument]),
prompt for the project directory."
With prefix ARG (\\[universal-argument]), keep the existing CLI-args prompt
and then prompt for the working directory."
(interactive "P")
(let ((ai-code-kilo-program-switches
(append ai-code-kilo-program-switches '("--continue"))))
Expand Down
4 changes: 2 additions & 2 deletions ai-code-opencode.el
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ If current buffer belongs to a project, start in the project's root
directory. Otherwise start in the directory of the current buffer file,
or the current value of `default-directory' if no project and no buffer file.

With double prefix ARG (\\[universal-argument] \\[universal-argument]),
prompt for the project directory."
With prefix ARG (\\[universal-argument]), keep the existing CLI-args prompt
and then prompt for the working directory."
(interactive "P")
(let ((ai-code-opencode-program-switches
(append ai-code-opencode-program-switches '("--continue"))))
Expand Down
101 changes: 98 additions & 3 deletions test/test_ai-code-backends-infra.el
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,20 @@ The result is a cons of whether SYMBOL is bound and its default value."
(should (equal (plist-get result :args) '("--resume")))
(should (equal (plist-get result :command) "claude --resume"))))))

(ert-deftest test-ai-code-backends-infra-session-working-directory-prompts-with-prefix ()
"A prefix argument should prompt for the working directory."
(let (seen)
(cl-letf (((symbol-function 'ai-code--session-project-root)
(lambda () "/project/"))
((symbol-function 'read-directory-name)
(lambda (prompt &optional dir default-filename mustmatch initial)
(setq seen (list prompt dir default-filename mustmatch initial))
"/custom/")))
Comment on lines +191 to +194
(should (equal (ai-code-backends-infra--session-working-directory 'prefix-arg)
"/custom/")))
(should (equal seen
'("Working directory: " "/project/" "/project/" t nil)))))

(ert-deftest test-ai-code-backends-infra-start-cli-session-forwards-options ()
"Generic CLI startup should resolve and forward backend options."
(let ((process-table (make-hash-table :test 'equal))
Expand All @@ -191,7 +205,7 @@ The result is a cons of whether SYMBOL is bound and its default value."
(post-start-fn (lambda (_buffer _process _instance) nil))
captured)
(cl-letf (((symbol-function 'ai-code-backends-infra--session-working-directory)
(lambda () "/project/"))
(lambda (&optional _prompt-p) "/project/"))
((symbol-function 'ai-code-backends-infra--resolve-start-command)
(lambda (program switches arg prompt-label)
(should (equal program "codex"))
Expand Down Expand Up @@ -233,12 +247,61 @@ The result is a cons of whether SYMBOL is bound and its default value."
"\r\n"
post-start-fn)))))

(ert-deftest test-ai-code-backends-infra-start-cli-session-prompts-dir-after-args ()
"Working directory prompting should happen after CLI args prompting."
(let ((process-table (make-hash-table :test 'equal))
call-order
captured)
(cl-letf (((symbol-function 'ai-code--session-project-root)
(lambda () "/project/"))
((symbol-function 'ai-code-backends-infra--resolve-start-command)
(lambda (program switches arg prompt-label)
(setq call-order (append call-order '(args)))
(should (equal program "codex"))
(should (equal switches '("--quiet")))
(should (eq arg 'prefix-arg))
(should (equal prompt-label "Codex"))
'(:command "codex --quiet")))
((symbol-function 'read-directory-name)
(lambda (&rest _args)
(setq call-order (append call-order '(dir)))
"/custom/"))
((symbol-function 'ai-code-backends-infra--toggle-or-create-session)
(lambda (&rest args)
(setq captured args))))
(ai-code-backends-infra--start-cli-session
(list :program "codex"
:switches '("--quiet")
:label "Codex"
:process-table process-table
:session-prefix "codex")
'prefix-arg))
(should (equal call-order '(args dir)))
(cl-destructuring-bind
(working-dir buffer-name seen-process-table command
&optional escape-fn cleanup-fn instance-name prefix
force-prompt env-vars multiline-input-sequence
post-start-fn)
captured
(should (equal working-dir "/custom/"))
(should-not buffer-name)
(should (eq seen-process-table process-table))
(should (equal command "codex --quiet"))
(should-not escape-fn)
(should-not cleanup-fn)
(should-not instance-name)
(should (equal prefix "codex"))
(should-not force-prompt)
(should-not env-vars)
(should-not multiline-input-sequence)
(should-not post-start-fn))))

(ert-deftest test-ai-code-backends-infra-cli-switch-and-send-use-project-session ()
"CLI wrapper switch and send helpers should resolve project sessions."
(let (switch-args
send-args)
(cl-letf (((symbol-function 'ai-code-backends-infra--session-working-directory)
(lambda () "/project/"))
(lambda (&optional _prompt-p) "/project/"))
((symbol-function 'ai-code-backends-infra--switch-to-session-buffer)
(lambda (&rest args)
(setq switch-args args)))
Expand All @@ -254,13 +317,45 @@ The result is a cons of whether SYMBOL is bound and its default value."
'(nil "No Codex session for this project"
"hello" "codex" "/project/")))))

(ert-deftest test-ai-code-backends-infra-cli-show-resume-picker-prefers-last-accessed-buffer ()
"Resume picker helper should reuse the last accessed matching session buffer."
(let ((buffer (generate-new-buffer "*codex[test-last]*"))
(ai-code-backends-infra--last-accessed-buffer nil)
sent
select-called)
(unwind-protect
(progn
(setq ai-code-backends-infra--last-accessed-buffer buffer)
(with-current-buffer buffer
(setq-local ai-code-backends-infra--session-prefix "codex")
(insert "prompt")
(goto-char (point-max)))
(cl-letf (((symbol-function 'ai-code-backends-infra--session-working-directory)
(lambda (&optional _prompt-p) "/project/"))
((symbol-function 'ai-code-backends-infra--select-session-buffer)
(lambda (&rest _args)
(setq select-called t)
nil))
((symbol-function 'sit-for)
(lambda (&rest _args) nil))
((symbol-function 'ai-code-backends-infra--terminal-send-string)
(lambda (string)
(setq sent string))))
(ai-code-backends-infra--cli-show-resume-picker "codex")
(with-current-buffer buffer
(should (equal sent ""))
(should (= (point) (point-min))))
(should-not select-called)))
(when (buffer-live-p buffer)
(kill-buffer buffer)))))

(ert-deftest test-ai-code-backends-infra-cli-show-resume-picker-pokes-buffer ()
"Resume picker helper should poke the selected session buffer."
(let ((buffer (generate-new-buffer "*codex[test]*"))
sent)
(unwind-protect
(cl-letf (((symbol-function 'ai-code-backends-infra--session-working-directory)
(lambda () "/project/"))
(lambda (&optional _prompt-p) "/project/"))
((symbol-function 'ai-code-backends-infra--select-session-buffer)
(lambda (prefix working-dir)
(should (equal prefix "codex"))
Expand Down