This package supports a threaded AI chat inside any org-mode buffer. Here’s what it looks like:
The design was inspired by the ChatGPT web interface. I’ve been using this package as an experiment in ways to tighten the editing loop with AI in Emacs, via context, diffs, etc.
The package comes with one main function, ai-org-chat-respond
, that operates in any org-mode buffer. It extracts a conversation history from the parent entries, treating an entry as belonging to the AI if its heading is “AI” and otherwise to the user. This conversation history is passed along to the LLM provider to generate a response.
Narrowing provides a simple way to truncate the conversation history if it becomes too long – only the restricted part of the buffer is considered.
The PROPERTIES drawers are used to provide
- context, which augments the system message with the contents of certain buffers or files, and
- tools, which allow the AI to operate directly on the environment.
There is support for quickly launching Ediff sessions that compare modifications suggested by the AI to their sources, making it easier to review and apply changes (see Compare Feature).
Download this repository, install using M-x package-install-file
(or package-vc-install, straight, elpaca, …), and add (use-package ai-org-chat)
to your init file. For a more comprehensive setup, use something like the following:
(use-package ai-org-chat
:bind
(:map global-map
("C-c /" . ai-org-chat-new))
(:map ai-org-chat-minor-mode-map
("C-c <return>" . ai-org-chat-respond))
:custom
(ai-org-chat-user-name "Paul")
(ai-org-chat-dir "~/ai-chats") ; Directory for saving chat files
:config
;; Uncomment to select your preferred model
;; (ai-org-chat-select-model "sonnet 3.7")
)
To use the package, you’ll need to set up the llm library. (A compatibility version using the gptel library is available on the ‘gptel’ branch if needed.)
You’ll need to select a model to use with the package. This is handled through the ai-org-chat-select-model
command:
- The package comes with several pre-configured models in the
ai-org-chat-models
variable, including Claude, GPT, Gemini, and Ollama-based local models. - Run
M-x ai-org-chat-select-model
to choose your preferred model from the list. - You can configure this in your init file by uncommenting and adjusting the
(ai-org-chat-select-model "model-name")
line at the bottom of theuse-package
declaration above. - Custom models can be added by customizing the
ai-org-chat-models
variable.
As a final tip, the following makes environment variables available in Emacs on MacOS:
(use-package exec-path-from-shell
:ensure
:init
(exec-path-from-shell-initialize))
When you want to ask the AI something, do M-x ai-org-chat-new
(or C-c /
, if you followed the above configuration). This creates a new file in the specified directory (controlled by the `ai-org-chat-dir` variable, which defaults to “~/ai-chats”).
Two useful ways to start a chat:
- With selected text: If the region is active when you call
ai-org-chat-new
, the selected text will be quoted in the new buffer and a reference to the source will be stored in theSOURCE_BUFFER
property. This maintains a live connection to the original content so the AI always has access to the most recent version. - With visible buffers as context: With a prefix argument (
C-u ai-org-chat-new
), it will automatically add all visible buffers as context to the new chat - perfect for asking questions about code you’re currently looking at.
You can add additional source buffers or context at any time through the transient menu (see below).
The org-mode buffer has ai-org-chat-minor-mode
activated, which supports user-defined keybindings like in the above use-package
declaration. If you want to work in some other org file, you can either activate this minor mode manually or do M-x ai-org-chat-setup-buffer
.
The primary way to access all package functions is through the transient menu, activated by supplying a C-u
prefix argument to M-x ai-org-chat-respond
(with the above bindings, C-u C-c <return>
). The menu provides easy access to all package features, organized into sections:
- Model selection
- Context management
- Source buffer management
- Tools configuration
- Formatting operations
- Conversation operations
The following commands are all accessible through the transient menu:
ai-org-chat-respond
- The main function, which tells the AI to generate a new response to the conversation node at point. It works in any org-mode buffer, not just ones created via
ai-org-chat-new
. Bound toC-c <return>
by default. ai-org-chat-branch
- Create a new conversation branch at point.
ai-org-chat-compare
- Launch an Ediff session to compare the org-mode block at point with the contents of another visible buffer. This helps you review and apply AI-suggested changes to your codebase. See Compare Feature for more details.
ai-org-chat-convert-markdown-blocks-to-org
- LLMs often return code in markdown format (even when you instruct them otherwise). This function converts all markdown code blocks between (point) and (point-max) to org-mode code blocks.
ai-org-chat-replace-backticks-with-equal-signs
- Interactively replace backtick quotes with
org-mode
verbatim quotes. ai-org-chat-copy-conversation-to-clipboard
- Copy the current conversation in a format suitable for sharing or submitting to another AI system.
ai-org-chat uses PROPERTIES drawers to manage all state related to the conversation. Context sources, tools, and source buffers can be set at the top level of the file or in individual nodes, with properties at deeper levels inheriting from their ancestors.
Context is managed through the CONTEXT
property. This property can contain a list of items that provide additional information to the AI. These items can be:
- Buffer names
- File names as absolute paths, paths relative to the current directory, or paths relative to any subdirectory of the current Emacs project, searched in this order
- Elisp function names (functions that return strings to be included in the context)
Example:
:PROPERTIES: :CONTEXT: buffer-name.txt project-file.el my-context-function :END:
Source buffers are managed through the SOURCE_BUFFER
property. When you create a new chat with an active region using ai-org-chat-new
, the package creates an indirect buffer containing that region and stores a reference to it in the SOURCE_BUFFER
property. This allows the package to maintain a live connection to the original buffer.
Source buffers serve a special role distinct from context sources:
- They represent the primary material being discussed or modified in the conversation
- Their current content is included with the final message sent to the AI, ensuring it always sees the latest version
- They’re given priority when using
ai-org-chat-compare
to compare AI suggestions with your original code - They’re automatically cleaned up when the chat buffer is closed
Like other properties, SOURCE_BUFFER
can be set at any level in the org hierarchy and inherits from parent nodes. This allows you to:
- Set a main source buffer at the top level for the whole conversation
- Add additional source buffers to specific conversation branches
- Override source buffers for specific parts of the conversation
While the AI can access all source buffers in its context, ai-org-chat-compare
will prioritize the first available source buffer when comparing source blocks.
Tools (or “function calls”) are specified using the TOOLS
property. This property should contain a list of tool names that reference tools registered in the ai-org-chat-tools
variable. For tools to work:
- The AI provider must support tools/function calls
- The tools must be properly registered in the
ai-org-chat-tools
list
You can register tools using the ai-org-chat-register-tool
function, which takes an llm-tool
struct:
(ai-org-chat-register-tool
(llm-make-tool :name "my-tool"
:function #'my-tool-function
:description "Description of what the tool does"
:args '(...)))
Alternatively, you can set the ai-org-chat-tools
variable directly. Once registered, tools can be referenced by name in the TOOLS property:
:PROPERTIES: :TOOLS: my-tool another-tool :END:
The llm-tool-collection package is intended as a repository for sharing and developing tools. It also provides a convenient macro llm-tool-collection-deftool
for defining them. After installing that package, you can register its tools for use with ai-org-chat
via
(mapcar #'ai-org-chat-register-tool-spec (llm-tool-collection-get-all))
If you’re interested in developing tools, it may be useful to have them update automatically in ai-org-chat
as soon as their llm-tool-collection-deftool
form is evaluated. This can be achieved as follows:
(add-hook 'llm-tool-collection-post-define-functions #'ai-org-chat-register-tool-spec)
While you can directly edit PROPERTIES drawers using Org mode’s built-in commands (e.g., C-c C-x p
for org-set-property
), ai-org-chat
provides some helper commands for managing context and tools (which are also accessible via the transient menu mentioned above):
ai-org-chat-add-buffer-context
: Add selected buffers as context.ai-org-chat-add-visible-buffers-context
: Add all visible buffers as context.ai-org-chat-add-file-context
: Add selected files as context.ai-org-chat-add-project-files-context
: Add all files from a selected project as context.ai-org-chat-add-source-buffer
: Add selected buffers as source buffers.ai-org-chat-add-tools
: Add selected tools fromai-org-chat-tools
to the current node.
These commands are designed to simplify context/tool management, but are not required for using the package.
The “compare” feature streamlines the process of reviewing and applying code changes suggested by the AI. This is one of the most powerful features of the package, making it easy to iterate on code modifications.
The simplest workflow leverages the automatic SOURCE_BUFFER
connections:
- Select code to modify: Select the region you want the AI to modify.
- Create chat with selection: Run
ai-org-chat-new
(C-c /
) with the region selected. This automatically creates a live connection to the source via theSOURCE_BUFFER
property. - Request modifications: Ask the AI to revise the code. For me, this typically involves several rounds of back-and-forth, additional context, (…).
- Convert if needed: If the AI responds with markdown code blocks rather than org-mode source blocks, use
ai-org-chat-convert-markdown-blocks-to-org
from the transient menu. - Compare and apply: With cursor on the source block, use
ai-org-chat-compare
from the transient menu to launch an Ediff session comparing the AI’s version with your original. - Select changes: Use standard Ediff commands to apply changes (
C-u b
in Emacs 31+).
When no SOURCE_BUFFER
is available, the compare feature uses the following prioritized rules to find a buffer to compare against:
- Smart function matching: If the AI-generated code is a complete function, the package will automatically find the matching function in any visible or context buffer.
- Visible buffers: If there are visible buffers in other windows, you’ll be prompted to select one (using ace-window if there are multiple).
- Context buffers: If there are buffers specified in the
CONTEXT
property, they’ll be available as comparison candidates.
This flexibility means you can combine the comparison feature with other Emacs techniques like narrowing or indirect buffers. For example:
- Narrow a buffer to a region of interest (
C-x n n
), make it visible, then use compare - Set up multiple visible windows with related code files, then choose which to compare against
- Add important files as
CONTEXT
to always have them available for comparison - Split source blocks returned by the LLM into individual defuns (using
M-x org-babel-demarcate-block
orC-c C-v d
) and compare each individually.
A common workflow is to create a file ai.org
in a project’s src directory, run M-x ai-org-chat-setup-buffer
, and use ai-org-chat-add-file-context
.
Here’s an example of using functions as context to create an AI chat interface to your agenda. This setup gives the AI access to the current time, your diary, weekly agenda, and yearly project timeline. The setup assumes you use the diary, org-mode and the agenda for task management, together with a “projects.org” for managing long term commitments. It should be easy to adapt this setup to other ways of managing your schedule.
- Create an org file, say
planner.org
- Run
M-x ai-org-chat-setup-buffer
- Add a top-level PROPERTIES drawer containing:
:CONTEXT: my/current-date-and-time ~/.emacs.d/diary my/agenda-for-week my/projects-for-year
After these steps, the beginning of planner.org
should look like this:
# -*- eval: (ai-org-chat-minor-mode 1); -*- :PROPERTIES: :CONTEXT: my/current-date-and-time ~/.emacs.d/diary my/agenda-for-week my/projects-for-year :END:
The context consists of:
- A function that provides the current date and time
- Your diary file (~/.emacs.d/diary)
- A function that provides your agenda for the next seven days
- A function that provides your project timeline for the next year
With this setup, you can chat with an AI that has continuous access to your schedule and plans. For example, you can ask “Which afternoons do I have free this week?” or “When’s the best time to schedule a trip in March?”
The three agenda-related functions can be implemented as follows:
(defun my/current-date-and-time ()
"Return string describing current date and time."
(format-time-string "%A, %B %d, %Y at %I:%M %p"))
(defun my/agenda-for-week ()
"Return string containing full agenda for the next seven days."
(interactive)
(save-window-excursion
(require 'org-agenda)
(let ((org-agenda-span 'day)
(org-agenda-start-on-weekday nil) ; start from today regardless of weekday
(org-agenda-start-day (format-time-string "%Y-%m-%d"))
(org-agenda-ndays 7)
(org-agenda-prefix-format
'((agenda . " %-12:c%?-12t%6e %s"))))
(org-agenda nil "a")
(buffer-substring-no-properties (point-min) (point-max)))))
(defun my/filter-diary-contents ()
"Return diary contents without holiday entries."
(with-temp-buffer
(insert-file-contents diary-file)
(goto-char (point-min))
(keep-lines "^[^&]" (point-min) (point-max))
(buffer-string)))
(defun my/with-filtered-diary (fn)
"Execute FN with a filtered version of the diary.
Temporarily creates and uses a diary file without holiday entries."
(let ((filtered-contents (my/filter-diary-contents)))
(with-temp-file "/tmp/temp-diary"
(insert filtered-contents))
(let ((diary-file "/tmp/temp-diary"))
(funcall fn))))
(defun my/projects-for-year ()
"Return string containing projects.org agenda for next year.
Skips empty days and diary holidays."
(interactive)
(save-window-excursion
(require 'org-agenda)
(let ((org-agenda-files (list my-projects-file))
(org-agenda-span 365)
(org-agenda-start-on-weekday nil)
(org-agenda-start-day (format-time-string "%Y-%m-%d"))
(org-agenda-prefix-format
'((agenda . " %-12:c%?-12t%6e %s")))
(org-agenda-include-diary t)
(diary-show-holidays-flag nil)
(org-agenda-show-all-dates nil))
(my/with-filtered-diary
(lambda ()
(org-agenda nil "a")
(buffer-substring-no-properties (point-min) (point-max)))))))