Commit 86c4659999eb

Vincent Demeester <vincent@sbr.pm>
2026-07-30 10:51:20
feat(emacs): add pi session from GitHub PR/issue
Added pi-from-github.el with two commands to start a pi-coding-agent session from a GitHub PR or issue. Uses gh CLI for completion and lazyworktree for worktree creation, with gh pr checkout fallback for fork PRs.
1 parent cdbba48
Changed files (2)
dots
config
dots/config/emacs/site-lisp/pi-from-github.el
@@ -0,0 +1,151 @@
+;;; pi-from-github.el --- Start pi sessions from GitHub PRs/issues -*- lexical-binding: t -*-
+;;; Commentary:
+;; Start a pi-coding-agent session from a GitHub PR or issue.
+;; Uses `gh` CLI for listing and `lazyworktree` for worktree creation.
+;;; Code:
+
+(require 'json)
+(require 'project)
+
+;; Declare pi-coding-agent functions
+(declare-function pi-coding-agent--setup-session "pi-coding-agent")
+(declare-function pi-coding-agent--show-session-buffers "pi-coding-agent")
+
+(defun pi-from-github--repo-root ()
+  "Return the git repository root for the current project."
+  (let ((default-directory (if (and (fboundp 'project-current) (project-current))
+                               (project-root (project-current))
+                             default-directory)))
+    (string-trim (shell-command-to-string "git rev-parse --show-toplevel"))))
+
+(defun pi-from-github--in-github-repo-p ()
+  "Return non-nil if current project is a GitHub repository."
+  (let ((default-directory (pi-from-github--repo-root)))
+    (string-match-p "github\\.com"
+                    (shell-command-to-string "git remote -v"))))
+
+(defun pi-from-github--fetch-prs ()
+  "Fetch open PRs from current GitHub repo."
+  (let* ((default-directory (pi-from-github--repo-root))
+         (output (shell-command-to-string
+                  "gh pr list --limit=500 --json number,title,author,headRefName,isDraft")))
+    (json-read-from-string output)))
+
+(defun pi-from-github--fetch-issues ()
+  "Fetch open issues from current GitHub repo."
+  (let* ((default-directory (pi-from-github--repo-root))
+         (output (shell-command-to-string
+                  "gh issue list --limit=500 --json number,title,author,labels")))
+    (json-read-from-string output)))
+
+(defun pi-from-github--format-pr (pr)
+  "Format PR for completion display."
+  (let-alist pr
+    (cons (format "#%-5d %s%s (by @%s) [%s]"
+                  .number .title
+                  (if (eq .isDraft t) " 🚧" "")
+                  .author.login .headRefName)
+          .number)))
+
+(defun pi-from-github--format-issue (issue)
+  "Format ISSUE for completion display."
+  (let-alist issue
+    (let ((label-str (mapconcat (lambda (l) (alist-get 'name l))
+                                (append .labels nil) ",")))
+      (cons (format "#%-5d %s (by @%s)%s"
+                    .number .title .author.login
+                    (if (string-empty-p label-str) "" (format " [%s]" label-str)))
+            .number))))
+
+(defun pi-from-github--create-worktree-for-pr (number)
+  "Create a worktree for PR NUMBER using lazyworktree.
+Uses `gh pr checkout' to fetch the PR (handles forks transparently),
+then creates a worktree from the resulting local branch.  Returns the
+worktree path."
+  (let* ((default-directory (pi-from-github--repo-root))
+         ;; First try lazyworktree directly
+         (cmd (format "lazyworktree create --from-pr %d --json --silent 2>&1" number))
+         (output (shell-command-to-string cmd))
+         (json-data (condition-case nil
+                        (json-read-from-string output)
+                      (error nil))))
+    (if (and json-data (alist-get 'path json-data))
+        (alist-get 'path json-data)
+      ;; Fallback: gh pr checkout handles forks transparently
+      (message "Fetching PR #%d via gh..." number)
+      (let* ((branch (format "pr-%d" number))
+             (checkout-cmd (format "gh pr checkout %d --branch %s 2>&1"
+                                   number (shell-quote-argument branch)))
+             (checkout-out (shell-command-to-string checkout-cmd)))
+        ;; gh pr checkout switches the current worktree — switch back
+        (shell-command-to-string "git checkout - 2>/dev/null")
+        ;; Now create worktree from the local branch
+        (let* ((cmd2 (format "lazyworktree create --from-branch %s --json --silent 2>&1"
+                             (shell-quote-argument branch)))
+               (out2 (shell-command-to-string cmd2))
+               (json2 (condition-case nil
+                          (json-read-from-string out2)
+                        (error nil))))
+          (if (and json2 (alist-get 'path json2))
+              (alist-get 'path json2)
+            (error "Failed to create worktree for PR #%d: %s" number out2)))))))
+
+(defun pi-from-github--create-worktree-for-issue (number)
+  "Create a worktree for issue NUMBER using lazyworktree.
+Returns the worktree path."
+  (let* ((default-directory (pi-from-github--repo-root))
+         (cmd (format "lazyworktree create --from-issue %d --json --silent 2>&1" number))
+         (output (shell-command-to-string cmd))
+         (json-data (condition-case nil
+                        (json-read-from-string output)
+                      (error nil))))
+    (if (and json-data (alist-get 'path json-data))
+        (alist-get 'path json-data)
+      (error "Failed to create worktree for issue #%d: %s" number output))))
+
+;;;###autoload
+(defun pi-from-github-pr ()
+  "Start a pi-coding-agent session from a GitHub PR.
+Lists open PRs using `gh`, creates a worktree with `lazyworktree`,
+and starts a pi session in that worktree."
+  (interactive)
+  (unless (pi-from-github--in-github-repo-p)
+    (user-error "Not in a GitHub repository"))
+  (let* ((prs (pi-from-github--fetch-prs))
+         (candidates (mapcar #'pi-from-github--format-pr (append prs nil)))
+         (selected (completing-read "PR: " candidates nil t))
+         (number (cdr (assoc selected candidates))))
+    (unless number (user-error "No PR selected"))
+    (message "Creating worktree for PR #%d..." number)
+    (let ((wt-path (pi-from-github--create-worktree-for-pr number)))
+      (message "Starting pi session in %s" wt-path)
+      (let* ((chat-buf (pi-coding-agent--setup-session wt-path
+                                                        (format "pr-%d" number)))
+             (input-buf (buffer-local-value 'pi-coding-agent--input-buffer
+                                            chat-buf)))
+        (pi-coding-agent--show-session-buffers chat-buf input-buf)))))
+
+;;;###autoload
+(defun pi-from-github-issue ()
+  "Start a pi-coding-agent session from a GitHub issue.
+Lists open issues using `gh`, creates a worktree with `lazyworktree`,
+and starts a pi session in that worktree."
+  (interactive)
+  (unless (pi-from-github--in-github-repo-p)
+    (user-error "Not in a GitHub repository"))
+  (let* ((issues (pi-from-github--fetch-issues))
+         (candidates (mapcar #'pi-from-github--format-issue (append issues nil)))
+         (selected (completing-read "Issue: " candidates nil t))
+         (number (cdr (assoc selected candidates))))
+    (unless number (user-error "No issue selected"))
+    (message "Creating worktree for issue #%d..." number)
+    (let ((wt-path (pi-from-github--create-worktree-for-issue number)))
+      (message "Starting pi session in %s" wt-path)
+      (let* ((chat-buf (pi-coding-agent--setup-session wt-path
+                                                        (format "issue-%d" number)))
+             (input-buf (buffer-local-value 'pi-coding-agent--input-buffer
+                                            chat-buf)))
+        (pi-coding-agent--show-session-buffers chat-buf input-buf)))))
+
+(provide 'pi-from-github)
+;;; pi-from-github.el ends here
dots/config/emacs/init.el
@@ -3293,6 +3293,12 @@ With \\[universal-argument] \\[universal-argument]: always prompt with completio
 (use-package phscroll
   :defer t)
 
+(use-package pi-from-github
+  :after pi-coding-agent
+  :commands (pi-from-github-pr pi-from-github-issue)
+  :bind (("C-c a r" . pi-from-github-pr)
+         ("C-c a i" . pi-from-github-issue)))
+
 (use-package pi-coding-agent
   :commands (pi-coding-agent pi-coding-agent-toggle)
   :bind (("C-c a p" . vde/pi-coding-agent-dwim)