Skip to content

Commit fdd81f0

Browse files
committed
Add Org links for pi sessions
Add optional pi-coding-agent-org support for storing and following pi: Org links. Links encode session files, project directories, and optional session names as query parameters. Expose session-open helpers for opening by directory/name or resuming a durable session file, including cwd inference from session headers and separate named buffers when needed. Preserve named-buffer identity for Org links before session metadata exists, and only prefer file links when the reported session file is readable so fresh sessions fall back to dir/name. Signed-off-by: Arthur Heymans <arthur@aheymans.xyz>
1 parent 7f8774a commit fdd81f0

6 files changed

Lines changed: 546 additions & 4 deletions

File tree

Makefile

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# shellcheck disable=all
12
# pi-coding-agent Makefile
23

34
EMACS ?= emacs
@@ -20,7 +21,7 @@ SELECTOR ?=
2021
# Example: make test VERBOSE=1
2122
VERBOSE ?=
2223

23-
.PHONY: test test-unit test-core test-ui test-render test-table test-input test-menu test-build
24+
.PHONY: test test-unit test-core test-ui test-render test-table test-input test-menu test-org test-build
2425
.PHONY: test-integration test-integration-fake test-integration-real test-integration-ci test-integration-ci-real test-gui test-gui-ci test-all
2526
.PHONY: bench bench-batch
2627
.PHONY: check check-parens compile lint lint-checkdoc lint-package clean clean-cache help
@@ -35,6 +36,7 @@ help:
3536
@echo " make test-table Table decoration tests only"
3637
@echo " make test-input Input buffer tests only"
3738
@echo " make test-menu Menu/session tests only"
39+
@echo " make test-org Org link tests only"
3840
@echo " make test-build Build/dependency helper tests only"
3941
@echo " make test-unit Compile + all unit tests"
4042
@echo " make test-integration Shared integration tests (fake first, then real; local target starts Ollama for the real lane)"
@@ -93,6 +95,7 @@ test: .deps-stamp
9395
-l pi-coding-agent-table-test \
9496
-l pi-coding-agent-input-test \
9597
-l pi-coding-agent-menu-test \
98+
-l pi-coding-agent-org-test \
9699
-l pi-coding-agent-build-test \
97100
-l pi-coding-agent-fake-pi-test \
98101
-l pi-coding-agent-gui-test-utils-test \
@@ -129,6 +132,9 @@ test-input: .deps-stamp
129132
test-menu: .deps-stamp
130133
@$(BATCH_TEST) -l pi-coding-agent-menu-test -f ert-run-tests-batch-and-exit
131134

135+
test-org: .deps-stamp
136+
@$(BATCH_TEST) -l pi-coding-agent-org-test -f ert-run-tests-batch-and-exit
137+
132138
test-build: .deps-stamp
133139
@$(BATCH_TEST) -l pi-coding-agent-build-test -f ert-run-tests-batch-and-exit
134140

@@ -265,7 +271,7 @@ ollama-status:
265271

266272
check-parens:
267273
@echo "=== Check Parens ==="
268-
@OUTPUT=$$($(BATCH) --eval '(condition-case err (dolist (f (list "scripts/pi-coding-agent-build.el" "scripts/install-deps.el" "scripts/install-ts-grammars.el" "pi-coding-agent-core.el" "pi-coding-agent-grammars.el" "pi-coding-agent-ui.el" "pi-coding-agent-table.el" "pi-coding-agent-render.el" "pi-coding-agent-input.el" "pi-coding-agent-menu.el" "pi-coding-agent.el")) (with-current-buffer (find-file-noselect f) (check-parens) (message "%s OK" f))) (user-error (message "FAIL: %s" (error-message-string err)) (kill-emacs 1)))' 2>&1); \
274+
@OUTPUT=$$($(BATCH) --eval '(condition-case err (dolist (f (list "scripts/pi-coding-agent-build.el" "scripts/install-deps.el" "scripts/install-ts-grammars.el" "pi-coding-agent-core.el" "pi-coding-agent-grammars.el" "pi-coding-agent-ui.el" "pi-coding-agent-table.el" "pi-coding-agent-render.el" "pi-coding-agent-input.el" "pi-coding-agent-menu.el" "pi-coding-agent-org.el" "pi-coding-agent.el")) (with-current-buffer (find-file-noselect f) (check-parens) (message "%s OK" f))) (user-error (message "FAIL: %s" (error-message-string err)) (kill-emacs 1)))' 2>&1); \
269275
echo "$$OUTPUT" | grep -E "OK$$|FAIL:"; \
270276
echo "$$OUTPUT" | grep -q "FAIL:" && exit 1 || true
271277

@@ -277,7 +283,7 @@ compile: .deps-stamp
277283
--eval "(package-initialize)" \
278284
$(LOCAL_LOAD_PATH) \
279285
--eval "(setq byte-compile-error-on-warn t)" \
280-
-f batch-byte-compile scripts/pi-coding-agent-build.el scripts/install-deps.el scripts/install-ts-grammars.el pi-coding-agent-core.el pi-coding-agent-grammars.el pi-coding-agent-ui.el pi-coding-agent-table.el pi-coding-agent-render.el pi-coding-agent-input.el pi-coding-agent-menu.el pi-coding-agent.el
286+
-f batch-byte-compile scripts/pi-coding-agent-build.el scripts/install-deps.el scripts/install-ts-grammars.el pi-coding-agent-core.el pi-coding-agent-grammars.el pi-coding-agent-ui.el pi-coding-agent-table.el pi-coding-agent-render.el pi-coding-agent-input.el pi-coding-agent-menu.el pi-coding-agent-org.el pi-coding-agent.el
281287

282288
lint: lint-checkdoc lint-package
283289

@@ -296,6 +302,7 @@ lint-checkdoc:
296302
--eval "(checkdoc-file \"pi-coding-agent-render.el\")" \
297303
--eval "(checkdoc-file \"pi-coding-agent-input.el\")" \
298304
--eval "(checkdoc-file \"pi-coding-agent-menu.el\")" \
305+
--eval "(checkdoc-file \"pi-coding-agent-org.el\")" \
299306
--eval "(checkdoc-file \"pi-coding-agent.el\")" 2>&1); \
300307
WARNINGS=$$(echo "$$OUTPUT" | grep -A1 "^Warning" | grep -v "^Warning\|^--$$"); \
301308
if [ -n "$$WARNINGS" ]; then echo "$$WARNINGS"; exit 1; else echo "OK"; fi
@@ -311,7 +318,7 @@ lint-package:
311318
(package-install 'package-lint))" \
312319
--eval "(require 'package-lint)" \
313320
--eval "(setq package-lint-main-file \"pi-coding-agent.el\")" \
314-
-f package-lint-batch-and-exit pi-coding-agent.el pi-coding-agent-ui.el pi-coding-agent-table.el pi-coding-agent-render.el pi-coding-agent-input.el pi-coding-agent-menu.el pi-coding-agent-core.el pi-coding-agent-grammars.el
321+
-f package-lint-batch-and-exit pi-coding-agent.el pi-coding-agent-ui.el pi-coding-agent-table.el pi-coding-agent-render.el pi-coding-agent-input.el pi-coding-agent-menu.el pi-coding-agent-org.el pi-coding-agent-core.el pi-coding-agent-grammars.el
315322

316323
check: compile lint test
317324

README.org

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,36 @@ transcript on disk. Saving does not interrupt or replace the live pi session.
324324
If you want a shareable export instead, use HTML export (=C-c C-p e= or
325325
=/export=).
326326

327+
** 🗂 Org links
328+
329+
pi-coding-agent includes optional Org link support in =pi-coding-agent-org.el=.
330+
Load it from your Org configuration if you want =pi:= links:
331+
332+
#+begin_src emacs-lisp
333+
(with-eval-after-load 'org
334+
(require 'pi-coding-agent-org))
335+
#+end_src
336+
337+
From a pi chat or input buffer, use =org-store-link= (normally =C-c l=, or
338+
=C-c n l= in some Doom setups) to store a link to the current session. Insert
339+
it later in Org with =org-insert-link=.
340+
341+
Durable session-file links open that session directly:
342+
343+
#+begin_src org
344+
[[pi:session?file=/abs/path/to/.pi/sessions/session.jsonl][Open pi session]]
345+
#+end_src
346+
347+
Links can also target a project directory and named session:
348+
349+
#+begin_src org
350+
[[pi:session?dir=/abs/path/to/project/&name=feature-try][Open pi session]]
351+
#+end_src
352+
353+
When a link opens a different session in a directory that already has a pi
354+
buffer, pi-coding-agent uses a separate named session buffer, like =C-u M-x
355+
pi-coding-agent=.
356+
327357
** 🌿 Forking and Context Management
328358

329359
When a conversation gets long, the AI's context window fills up. The menu

pi-coding-agent-org.el

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
;;; pi-coding-agent-org.el --- Org links for pi coding agent -*- lexical-binding: t; -*-
2+
3+
;; Copyright (C) 2026 Daniel Nouri
4+
5+
;; Author: Daniel Nouri <daniel.nouri@gmail.com>
6+
;; Maintainer: Daniel Nouri <daniel.nouri@gmail.com>
7+
;; URL: https://github.com/dnouri/pi-coding-agent
8+
9+
;; SPDX-License-Identifier: GPL-3.0-or-later
10+
11+
;; This file is not part of GNU Emacs.
12+
13+
;;; Commentary:
14+
15+
;; Optional Org link support for pi-coding-agent sessions.
16+
;; Loading the base pi-coding-agent package does not require Org; users can load
17+
;; this file from `org-mode' configuration to enable pi: links.
18+
19+
;;; Code:
20+
21+
(require 'pi-coding-agent)
22+
(require 'org)
23+
(require 'subr-x)
24+
(require 'url-util)
25+
26+
(defun pi-coding-agent-org--query-encode (value)
27+
"URL-encode VALUE for a pi Org link query parameter."
28+
(url-hexify-string (or value "")))
29+
30+
(defun pi-coding-agent-org--query-decode (value)
31+
"URL-decode VALUE from a pi Org link query parameter."
32+
(url-unhex-string (replace-regexp-in-string "+" " " (or value ""))))
33+
34+
(defun pi-coding-agent-org--parse-link (path)
35+
"Parse a pi Org link PATH.
36+
Returns a plist with :kind and decoded query parameter keys such as :file,
37+
:dir, and :name."
38+
(let* ((parts (split-string path "\\?" t))
39+
(kind (car parts))
40+
(query (cadr parts))
41+
(plist (list :kind kind)))
42+
(when query
43+
(dolist (part (split-string query "&" t))
44+
(when (string-match "\\`\\([^=]+\\)=\\(.*\\)\\'" part)
45+
(let ((key (intern (concat ":" (match-string 1 part))))
46+
(value (pi-coding-agent-org--query-decode
47+
(match-string 2 part))))
48+
(setq plist (plist-put plist key value))))))
49+
plist))
50+
51+
(defun pi-coding-agent-org-session-link-target (&optional file dir name)
52+
"Return a pi: session link target from FILE or DIR and NAME.
53+
FILE is preferred as the durable session identity. DIR and NAME are used for
54+
links to named sessions that do not yet have a known durable session file."
55+
(concat
56+
"session?"
57+
(cond
58+
((and file (not (string-empty-p file)))
59+
(concat "file=" (pi-coding-agent-org--query-encode
60+
(expand-file-name file))
61+
(when dir
62+
(concat "&dir=" (pi-coding-agent-org--query-encode
63+
(file-name-as-directory
64+
(expand-file-name dir)))))
65+
(when (and name (not (string-empty-p name)))
66+
(concat "&name=" (pi-coding-agent-org--query-encode name)))))
67+
((and dir name (not (string-empty-p name)))
68+
(concat "dir=" (pi-coding-agent-org--query-encode
69+
(file-name-as-directory (expand-file-name dir)))
70+
"&name=" (pi-coding-agent-org--query-encode name)))
71+
(dir
72+
(concat "dir=" (pi-coding-agent-org--query-encode
73+
(file-name-as-directory (expand-file-name dir)))))
74+
(t
75+
(user-error "No pi session file or directory known")))))
76+
77+
(defun pi-coding-agent-org-session-link (&optional file dir name description)
78+
"Return an Org bracket link for a pi session.
79+
FILE, DIR, and NAME are passed to `pi-coding-agent-org-session-link-target'.
80+
DESCRIPTION defaults to a generic session label."
81+
(format "[[pi:%s][%s]]"
82+
(pi-coding-agent-org-session-link-target file dir name)
83+
(or description "Open pi session")))
84+
85+
(defun pi-coding-agent-org--current-session-link-data (&optional chat-buffer)
86+
"Return Org link data for CHAT-BUFFER or the current pi session.
87+
The result is a plist with :link and :description, or nil when point is not in
88+
a pi chat/input session buffer."
89+
(let ((chat-buf (or chat-buffer (pi-coding-agent--get-chat-buffer))))
90+
(when (and (buffer-live-p chat-buf)
91+
(with-current-buffer chat-buf
92+
(derived-mode-p 'pi-coding-agent-chat-mode)))
93+
(with-current-buffer chat-buf
94+
(let* ((file (pi-coding-agent-session-file chat-buf))
95+
(dir (pi-coding-agent--chat-session-directory chat-buf))
96+
(name (pi-coding-agent-session-display-name chat-buf))
97+
(target (pi-coding-agent-org-session-link-target file dir name))
98+
(description (if (and name (not (string-empty-p name)))
99+
(format "pi session: %s" name)
100+
"pi session")))
101+
(list :link (concat "pi:" target)
102+
:description description))))))
103+
104+
(defun pi-coding-agent-org-store-session-link (&optional _interactive)
105+
"Store an Org link to the current pi session.
106+
This is used by `org-store-link' from pi chat and input buffers."
107+
(when-let* ((data (pi-coding-agent-org--current-session-link-data)))
108+
(org-link-store-props :type "pi"
109+
:link (plist-get data :link)
110+
:description (plist-get data :description))
111+
t))
112+
113+
;;;###autoload
114+
(defun pi-coding-agent-org-open-link (path _)
115+
"Open pi Org link PATH."
116+
(let* ((info (pi-coding-agent-org--parse-link path))
117+
(kind (plist-get info :kind)))
118+
(pcase kind
119+
("session"
120+
(cond
121+
((plist-get info :file)
122+
(pi-coding-agent-session-open-file
123+
(plist-get info :file)
124+
(plist-get info :dir)
125+
(plist-get info :name)))
126+
((plist-get info :dir)
127+
(pi-coding-agent-session-open
128+
(plist-get info :dir)
129+
(plist-get info :name)))
130+
(t
131+
(user-error "pi session link lacks file or dir"))))
132+
(_
133+
(user-error "Unsupported pi link kind: %s" kind)))))
134+
135+
(defun pi-coding-agent-org-export-link (path description _backend)
136+
"Export pi Org link PATH with DESCRIPTION."
137+
(or description (format "pi:%s" path)))
138+
139+
(org-link-set-parameters "pi"
140+
:follow #'pi-coding-agent-org-open-link
141+
:export #'pi-coding-agent-org-export-link
142+
:store #'pi-coding-agent-org-store-session-link)
143+
144+
(provide 'pi-coding-agent-org)
145+
;;; pi-coding-agent-org.el ends here

pi-coding-agent-ui.el

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -657,6 +657,11 @@ still uses this name to find the live conversation.")
657657
Project lookup, window toggling, and path completion use this directory even
658658
when the buffer is also backed by a transcript file elsewhere.")
659659

660+
(defvar-local pi-coding-agent--canonical-session-name nil
661+
"Stable named-session suffix for this chat buffer.
662+
This is the optional SESSION passed to `pi-coding-agent--setup-session', not
663+
necessarily the persisted pi session display name.")
664+
660665
(defvar pi-coding-agent--chat-buffer)
661666

662667
(defun pi-coding-agent--chat-session-buffer-name (&optional buffer)
@@ -680,6 +685,8 @@ DIR is the session directory and SESSION is the optional named-session suffix."
680685
(setq pi-coding-agent--canonical-buffer-name
681686
(pi-coding-agent--buffer-name :chat dir session)
682687
pi-coding-agent--canonical-session-directory dir
688+
pi-coding-agent--canonical-session-name
689+
(and session (not (string-empty-p session)) session)
683690
default-directory dir))
684691

685692
(defun pi-coding-agent--restore-chat-buffer-read-only ()

0 commit comments

Comments
 (0)