Skip to content
Open
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
2 changes: 2 additions & 0 deletions HISTORY.org
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
* Release history

** Main branch change
- Merge the AI session checkpoint (`P`) into agent handoff (`H`), dispatching on buffer type
- Load a handoff section from anywhere inside it, not only from its headline

** 1.95
- Default PR target branch to the reflog-recorded parent branch
Expand Down
10 changes: 6 additions & 4 deletions README.org
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ appends the destination PNG path when invoking it:
- If the default wide layout does not fit your frame well, set `ai-code-menu-layout` to `two-columns` for a narrower menu with the same commands.
- *AI CLI Session Management*: Start (`a`), resume (`R`), or jump back into (`z`) the active AI CLI buffer, instantly swap backends (`s`), upgrade them (`u`), install backend skills (`S`), edit backend configs (`g`), open backend agent file (`G`), and run prompts against the current file (`|`). It supports multiple sessions per project. The `S` entry is especially useful for installing shared skill packs such as [[https://github.com/obra/superpowers][obra/superpowers]].
- `j` opens the AI session dashboard, where you can inspect active sessions, refresh the list, visit a session buffer, kill a session, or open the repository's Magit status.
- `P` sends a session checkpoint prompt that asks the AI to summarize the goal, files changed, current hypothesis, tests/build result, blockers, and next action.
- `H` sends a session checkpoint prompt when run from an AI session buffer, asking the AI to summarize the goal, files changed, current hypothesis, tests/build result, blockers, and next action.
- When resuming, selecting a UUID in the region appends that UUID automatically so you can continue a specific session.
- In AI session side windows, `C-.` grows and `C-,` shrinks the panel immediately. On left/right side windows it adjusts width; on top/bottom side windows it adjusts height.
- *AI CLI Editor Viewport*: Managed terminal TUI sessions support editing in a native Emacs viewport.
Expand Down Expand Up @@ -457,10 +457,12 @@ When working with multiple AI sessions, it can be useful to receive desktop noti
To transfer development work between different AI coding backends (e.g., from Codex to Antigravity CLI) without relying on backend-specific session histories, *ai-code-interface.el* supports portable agent handoffs using Org-mode task files.

- *Create or Open Task Files (`k` / `ai-code-create-or-open-task-file`)*: Quickly initialize a new task file (by default under `.ai.code.files/`) with date, branch, selected backend, and structured Org headings (`Task Description`, `Investigation`, `Code Change`).
- *Generate Portable Handoff (`H` / `ai-code-agent-handoff`)*:
- Run `C-c a H` (without a prefix) when not on an Org heading to ask the current AI agent to append a structured handoff section (`* Agent Handoff YYYY-MM-DD HH:MM`) summarizing the task objective, current progress, files modified, design decisions, failed approaches, test results, and a startup prompt for the next agent.
- *Generate Portable Handoff (`H` / `ai-code-agent-handoff-or-checkpoint`)*: `C-c a H` picks its behavior from the current buffer.
- In an Org buffer, run `C-c a H` (without a prefix) outside any handoff section to ask the current AI agent to append a structured handoff section (`* Agent Handoff YYYY-MM-DD HH:MM`) summarizing the task objective, current progress, files modified, design decisions, failed approaches, test results, and a startup prompt for the next agent.
- Run `C-c a H` with a prefix argument (e.g. `C-u C-c a H`) to load the whole task file as neutral handoff context in a new AI session.
- Run `C-c a H` when the cursor is positioned on an Org heading to load only that specific subtree as prompt context for the active session.
- Run `C-c a H` anywhere inside a handoff section, or with the cursor on any Org heading, to load only that subtree as prompt context for the active session. The cursor does not have to sit on the handoff headline itself.
- In an AI session buffer, `C-c a H` sends the session checkpoint prompt (`ai-code-session-checkpoint`) instead.
- In any other buffer, `C-c a H` asks which of the two actions to run.

** AI coding CLI backend

Expand Down
54 changes: 45 additions & 9 deletions ai-code-task.el
Original file line number Diff line number Diff line change
Expand Up @@ -271,16 +271,51 @@ Return the relevant file paths, matched excerpts, and a concise summary."
(insert-file-contents task-file)
(buffer-substring-no-properties (point-min) (point-max)))))

(defun ai-code--agent-handoff-current-subtree-text ()
"Return the current Org heading subtree text."
(defun ai-code--agent-handoff-subtree-text (position)
"Return the Org subtree text for the heading at POSITION."
(save-excursion
(goto-char position)
(org-back-to-heading t)
(let ((start (point))
(end (save-excursion
(org-end-of-subtree t t)
(point))))
(buffer-substring-no-properties start end))))

(defconst ai-code--agent-handoff-headline-regexp
"\\`[ \t]*\\(?:agent[ \t]+\\)?handoff\\b"
"Regexp matching an Org headline that holds an agent handoff.
Matched case-insensitively against the headline text generated by
`ai-code--agent-handoff-dump-prompt'.")

(defun ai-code--agent-handoff-headline-p ()
"Return non-nil when the Org heading at point holds an agent handoff."
(when-let* ((heading (org-get-heading t t t t)))
(let ((case-fold-search t))
(string-match-p ai-code--agent-handoff-headline-regexp heading))))

(defun ai-code--agent-handoff-enclosing-section-start ()
"Return the start of the handoff section surrounding point, or nil.
Point does not have to sit on the headline: the heading at point and its
ancestors are searched for a handoff headline."
(save-excursion
(when (ignore-errors (org-back-to-heading t) t)
(let ((start nil)
(searching t))
(while (and searching (not start))
(if (ai-code--agent-handoff-headline-p)
(setq start (point))
(setq searching (org-up-heading-safe))))
start))))

(defun ai-code--agent-handoff-load-start ()
"Return the position whose Org subtree should be loaded as handoff context.
Return nil when the current buffer position selects no subtree to load."
(when (derived-mode-p 'org-mode)
(if (org-at-heading-p)
(point)
(ai-code--agent-handoff-enclosing-section-start))))

(defun ai-code--agent-handoff-load-prompt (content whole-file-p)
"Build a prompt to load handoff CONTENT.
WHOLE-FILE-P controls whether CONTENT came from the whole task file."
Expand Down Expand Up @@ -317,24 +352,25 @@ writing the handoff."
;;;###autoload
(defun ai-code-agent-handoff (&optional arg)
"Create or load a portable agent handoff through the current task file.
When point is on an Org heading, load that subtree as context for the current
backend. When ARG is non-nil, load the whole task file as context. Otherwise,
ask the current agent to append a top-level handoff section to the task file."
When ARG is non-nil, load the whole task file as context. When point is on
an Org heading, or anywhere inside a handoff section, load that subtree as
context for the current backend. Otherwise, ask the current agent to append
a top-level handoff section to the task file."
(interactive "P")
(let ((task-file (ai-code--agent-handoff-read-task-file)))
(let ((task-file (ai-code--agent-handoff-read-task-file))
(load-start (unless arg (ai-code--agent-handoff-load-start))))
(cond
(arg
(ai-code--confirm-and-send
"Confirm handoff load prompt: "
(ai-code--agent-handoff-load-prompt
(ai-code--agent-handoff-read-file-or-buffer task-file)
t)))
((and (derived-mode-p 'org-mode)
(org-at-heading-p))
(load-start
(ai-code--confirm-and-send
"Confirm handoff load prompt: "
(ai-code--agent-handoff-load-prompt
(ai-code--agent-handoff-current-subtree-text)
(ai-code--agent-handoff-subtree-text load-start)
nil)))
(t
(ai-code--confirm-and-send
Expand Down
33 changes: 31 additions & 2 deletions ai-code.el
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,36 @@ ARG is the prefix argument."
ai-code-session-checkpoint-prompt)))
(ai-code--insert-prompt prompt)))

(defconst ai-code--handoff-or-checkpoint-choices
'(("Agent handoff" . ai-code-agent-handoff)
("Session checkpoint" . ai-code-session-checkpoint))
"Fallback choices offered by `ai-code-agent-handoff-or-checkpoint'.")

(defun ai-code--read-handoff-or-checkpoint-command ()
"Ask which handoff or checkpoint command to run and return its symbol."
(let ((choice (completing-read
"Neither an Org nor an AI session buffer; choose action: "
(mapcar #'car ai-code--handoff-or-checkpoint-choices)
nil t)))
(cdr (assoc choice ai-code--handoff-or-checkpoint-choices))))

;;;###autoload
(defun ai-code-agent-handoff-or-checkpoint (&optional arg)
"Run agent handoff or session checkpoint based on the current buffer.
In an Org buffer, run `ai-code-agent-handoff' and forward ARG to it. In an
AI session buffer, run `ai-code-session-checkpoint'. Anywhere else, ask
which of the two commands to run."
(interactive "P")
(cond
((derived-mode-p 'org-mode)
(ai-code-agent-handoff arg))
((ai-code-backends-infra--session-buffer-p (current-buffer))
(ai-code-session-checkpoint))
((eq (ai-code--read-handoff-or-checkpoint-command) 'ai-code-agent-handoff)
(ai-code-agent-handoff arg))
(t
(ai-code-session-checkpoint))))

;;;###autoload
(defun ai-code-cli-resume-with-session-checkpoint (&optional arg prompt-for-checkpoint)
"Resume the current backend's CLI session and optionally request a checkpoint.
Expand Down Expand Up @@ -599,7 +629,7 @@ Shows the current backend label to the right."
(transient-define-group ai-code--menu-other-tools
(ai-code--infix-toggle-auto-follow-up)
("k" "Create/Open task file" ai-code-create-or-open-task-file)
("H" "Agent handoff (C-u: whole task)" ai-code-agent-handoff)
("H" "Handoff / checkpoint" ai-code-agent-handoff-or-checkpoint)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Restore the prefix hint in the handoff menu description

When test/test_ai-code.el runs, ai-code-test-menu-other-tools-includes-agent-handoff-entry requires this suffix description to equal "Handoff / checkpoint (C-u: whole task)", but the changed transient definition supplies only "Handoff / checkpoint". That equality always fails, preventing both the targeted test and the full ERT suite from passing; restore the expected description while retaining the useful prefix-argument hint.

Useful? React with 👍 / 👎.

("/" "Search notes with AI" ai-code-search-notes-with-ai)
("n" "Take notes from AI session" ai-code-take-notes)
("p" "Open prompt history file" ai-code-open-prompt-file)
Expand All @@ -608,7 +638,6 @@ Shows the current backend label to the right."
;; ("m" "Debug python MCP server" ai-code-debug-mcp)
;; ("N" "Toggle notifications" ai-code-notifications-toggle)
;; DONE: Add a menu item here: Given a new customized variable, which suppose to be a list of strings, by default it is nil. User can choose one from it, probably with complet-reading, and it will be sent to AI session with ai-code--insert-prompt. This is useful for user to quickly send some pre-defined prompt templates or instructions to AI, like a shortcut.
("P" "AI session checkpoint" ai-code-session-checkpoint)
;; ("o" "Open recent file (C-u: insert)" ai-code-git-repo-recent-modified-files)
("|" "Apply prompt on file" ai-code-apply-prompt-on-current-file)
("h" "Help / Quick Start" ai-code-onboarding-open-quickstart))
Expand Down
47 changes: 47 additions & 0 deletions test/test_ai-code-task.el
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,53 @@
(should (string-match-p "Build handoff support" sent-prompt))
(should (string-match-p "Carry all task notes forward" sent-prompt))))

(ert-deftest ai-code-test-agent-handoff-inside-handoff-section-loads-subtree ()
"Point inside a handoff section loads it without sitting on the headline."
(let (sent-prompt)
(with-temp-buffer
(setq buffer-file-name "/tmp/project/.ai.code.files/task.org")
(insert "* Task Description\n")
(insert "Build handoff support.\n")
(insert "* Agent Handoff 2026-08-31 10:00\n")
(insert "Task objective: merge the checkpoint command.\n")
(insert "* Other Section\n")
(insert "This content should not be sent.\n")
(ai-code-prompt-mode)
(goto-char (point-min))
(search-forward "Task objective")
(cl-letf (((symbol-function 'ai-code--confirm-and-send)
(lambda (_label prompt)
(setq sent-prompt prompt))))
(ai-code-agent-handoff nil)))
(should (string-match-p "Load this agent handoff context" sent-prompt))
(should (string-match-p "Agent Handoff 2026-08-31 10:00" sent-prompt))
(should (string-match-p "merge the checkpoint command" sent-prompt))
(should-not (string-match-p "Build handoff support" sent-prompt))
(should-not (string-match-p "This content should not be sent" sent-prompt))))

(ert-deftest ai-code-test-agent-handoff-under-handoff-subheading-loads-whole-section ()
"Point under a handoff sub-heading loads the whole handoff section."
(let (sent-prompt)
(with-temp-buffer
(setq buffer-file-name "/tmp/project/.ai.code.files/task.org")
(insert "* Agent Handoff 2026-08-31 10:00\n")
(insert "Current progress: dispatcher drafted.\n")
(insert "** Files modified\n")
(insert "ai-code.el and ai-code-task.el.\n")
(insert "* Other Section\n")
(insert "This content should not be sent.\n")
(ai-code-prompt-mode)
(goto-char (point-min))
(search-forward "ai-code.el and")
(cl-letf (((symbol-function 'ai-code--confirm-and-send)
(lambda (_label prompt)
(setq sent-prompt prompt))))
(ai-code-agent-handoff nil)))
(should (string-match-p "Load this agent handoff context" sent-prompt))
(should (string-match-p "Current progress: dispatcher drafted" sent-prompt))
(should (string-match-p "ai-code.el and ai-code-task.el" sent-prompt))
(should-not (string-match-p "This content should not be sent" sent-prompt))))

(ert-deftest ai-code-test-agent-handoff-off-heading-dumps-to-task-file ()
"Agent handoff off a heading asks the agent to append a handoff."
(let (sent-prompt)
Expand Down
76 changes: 66 additions & 10 deletions test/test_ai-code.el
Original file line number Diff line number Diff line change
Expand Up @@ -482,23 +482,79 @@
(call-interactively #'ai-code-cli-resume-with-session-checkpoint)))
(should (equal resume-arg '(4)))))

(ert-deftest ai-code-test-menu-other-tools-includes-session-checkpoint-entry ()
"Test that the Other Tools menu exposes session checkpoint."
(let* ((suffix (transient-get-suffix 'ai-code--menu-other-tools "P"))
(definition (cdr suffix)))
(should suffix)
(should (eq (plist-get definition :command)
'ai-code-session-checkpoint))))
(ert-deftest ai-code-test-menu-other-tools-removes-session-checkpoint-entry ()
"Test that session checkpoint no longer owns a dedicated Other Tools key."
(should-error (transient-get-suffix 'ai-code--menu-other-tools "P")
:type 'error))

(ert-deftest ai-code-test-handoff-or-checkpoint-dispatches-to-handoff-in-org-buffer ()
"Test that Org buffers route the merged command to agent handoff."
(let (handoff-arg checkpoint-called)
(cl-letf (((symbol-function 'ai-code-agent-handoff)
(lambda (&optional arg) (setq handoff-arg (list arg))))
((symbol-function 'ai-code-session-checkpoint)
(lambda () (setq checkpoint-called t))))
(with-temp-buffer
(org-mode)
(ai-code-agent-handoff-or-checkpoint '(4))))
(should (equal handoff-arg '((4))))
(should-not checkpoint-called)))

(ert-deftest ai-code-test-handoff-or-checkpoint-dispatches-to-checkpoint-in-session-buffer ()
"Test that AI session buffers route the merged command to the checkpoint."
(let ((buffer (get-buffer-create "*ai-code-test-backend[project]*"))
handoff-called checkpoint-called)
(unwind-protect
(cl-letf (((symbol-function 'ai-code-agent-handoff)
(lambda (&optional _arg) (setq handoff-called t)))
((symbol-function 'ai-code-session-checkpoint)
(lambda () (setq checkpoint-called t))))
(with-current-buffer buffer
(ai-code-agent-handoff-or-checkpoint nil)))
(kill-buffer buffer))
(should checkpoint-called)
(should-not handoff-called)))

(ert-deftest ai-code-test-handoff-or-checkpoint-asks-in-other-buffers ()
"Test that other buffers ask which of the two commands to run."
(let (prompted-collection handoff-arg)
(cl-letf (((symbol-function 'completing-read)
(lambda (_prompt collection &rest _args)
(setq prompted-collection collection)
"Agent handoff"))
((symbol-function 'ai-code-agent-handoff)
(lambda (&optional arg) (setq handoff-arg (list arg))))
((symbol-function 'ai-code-session-checkpoint)
(lambda () (error "Checkpoint must not run for this choice"))))
(with-temp-buffer
(fundamental-mode)
(ai-code-agent-handoff-or-checkpoint '(4))))
(should (equal prompted-collection '("Agent handoff" "Session checkpoint")))
(should (equal handoff-arg '((4))))))

(ert-deftest ai-code-test-handoff-or-checkpoint-honors-checkpoint-choice ()
"Test that picking the checkpoint in other buffers runs the checkpoint."
(let (checkpoint-called)
(cl-letf (((symbol-function 'completing-read)
(lambda (&rest _args) "Session checkpoint"))
((symbol-function 'ai-code-agent-handoff)
(lambda (&optional _arg) (error "Handoff must not run")))
((symbol-function 'ai-code-session-checkpoint)
(lambda () (setq checkpoint-called t))))
(with-temp-buffer
(fundamental-mode)
(ai-code-agent-handoff-or-checkpoint nil)))
(should checkpoint-called)))

(ert-deftest ai-code-test-menu-other-tools-includes-agent-handoff-entry ()
"Test that the Other Tools menu exposes agent handoff."
"Test that the Other Tools menu exposes the merged handoff/checkpoint entry."
(let* ((suffix (transient-get-suffix 'ai-code--menu-other-tools "H"))
(definition (cdr suffix)))
(should suffix)
(should (eq (plist-get definition :command)
'ai-code-agent-handoff))
'ai-code-agent-handoff-or-checkpoint))
(should (equal (plist-get definition :description)
"Agent handoff (C-u: whole task)"))))
"Handoff / checkpoint (C-u: whole task)"))))

(ert-deftest ai-code-test-menu-prefix-command-default-layout ()
"Test that the default menu layout uses the original transient."
Expand Down
Loading