aboutsummaryrefslogtreecommitdiff
path: root/modules/ai-config.el
diff options
context:
space:
mode:
Diffstat (limited to 'modules/ai-config.el')
-rw-r--r--modules/ai-config.el577
1 files changed, 0 insertions, 577 deletions
diff --git a/modules/ai-config.el b/modules/ai-config.el
deleted file mode 100644
index 20bf6ec88..000000000
--- a/modules/ai-config.el
+++ /dev/null
@@ -1,577 +0,0 @@
-;;; ai-config.el --- Configuration for AI Integrations -*- lexical-binding: t; coding: utf-8; -*-
-;; author Craig Jennings <c@cjennings.net>
-;;
-;;; Commentary:
-;;
-;; Layer: 3 (Domain Workflow).
-;; Category: D/P.
-;; Load shape: eager.
-;; Eager reason: registers the cj/ai-keymap (C-; a); GPTel itself should load on
-;; command, a Phase 5 deferral candidate.
-;; Top-level side effects: defines cj/ai-keymap, registers it under cj/custom-keymap.
-;; Runtime requires: keybindings, system-lib.
-;; Direct test load: yes (requires keybindings explicitly).
-;;
-;; Configuration for AI integrations in Emacs, focused on GPTel.
-;;
-;; Main Features:
-;; - Quick toggle for AI assistant window (C-; a t)
-;; - Custom keymap (C-; a prefix) for AI-related commands.
-;; - Enhanced org-mode conversation formatting with timestamps
-;; allows switching models and easily compare and track responses.
-;; - Various specialized AI directives (coder, reviewer, etc.)
-;; - Context management for adding files/buffers to conversations
-;; - Conversation persistence with save/load functionality
-;; - Integration with Magit for code review
-;;
-;; Basic Workflow
-;;
-;; Using a side-chat window:
-;; - Launch GPTel via C-; a t, and chat in the AI-Assistant side window (C-<return> to send)
-;; - Change system prompt (expertise, personalities) with C-; a p
-;; - Add context from files (C-; a f) or current buffer (C-; a .)
-;; - Save conversations with C-; a s, load previous ones with C-; a l
-;; - Clear the conversation and start over with C-; a x
-;; Or in any buffer:
-;; - Add directive as above, and select a region to rewrite with C-; a r.
-;;
-
-;;; Code:
-
-(require 'keybindings) ;; provides cj/custom-keymap
-(require 'system-lib) ;; provides cj/auth-source-secret-value
-(require 'cj-window-toggle-lib) ;; side-window size memory for the panel
-
-(autoload 'cj/gptel-save-conversation "ai-conversations" "Save the AI conversation to a file." t)
-(autoload 'cj/gptel-load-conversation "ai-conversations" "Load a saved AI conversation." t)
-(autoload 'cj/gptel-delete-conversation "ai-conversations" "Delete a saved AI conversation." t)
-(autoload 'cj/gptel-autosave-toggle "ai-conversations" "Toggle autosave in the current GPTel buffer." t)
-(autoload 'cj/gptel-quick-ask "ai-quick-ask" "One-shot quick-ask in a transient buffer." t)
-(autoload 'cj/gptel-rewrite-with-directive "ai-rewrite" "Pick a directive and run gptel-rewrite on the region." t)
-(autoload 'cj/gptel-rewrite-redo-with-different-directive "ai-rewrite" "Re-run the previous rewrite with a different directive." t)
-(autoload 'cj/gptel-browse-conversations "ai-conversations-browser" "Browse saved GPTel conversations." t)
-
-;;; ------------------------- AI Config Helper Functions ------------------------
-
-;; Define variables upfront
-(defvar cj/anthropic-api-key-cached nil "Cached Anthropic API key.")
-(defvar cj/openai-api-key-cached nil "Cached OpenAI API key.")
-(defvar gptel-claude-backend nil "Claude backend, lazy-initialized.")
-(defvar gptel-chatgpt-backend nil "ChatGPT backend, lazy-initialized.")
-
-(defcustom cj/gptel-tools-directory
- (expand-file-name "gptel-tools/" user-emacs-directory)
- "Directory containing optional local GPTel tool modules."
- :type 'directory
- :group 'cj)
-
-(defcustom cj/gptel-local-tool-features
- '(read_buffer
- read_text_file
- write_text_file
- update_text_file
- list_directory_files
- move_to_trash
- git_status
- git_log
- git_diff
- web_fetch)
- "Feature symbols for optional local GPTel tool modules."
- :type '(repeat symbol)
- :group 'cj)
-
-(defun cj/gptel-load-local-tools
- (&optional tools-directory tool-features)
- "Load optional GPTel tools from TOOLS-DIRECTORY.
-TOOL-FEATURES defaults to `cj/gptel-local-tool-features'. Return a list
-of loaded feature symbols. Missing directories or individual optional
-tools are reported with `message' and do not signal."
- (let ((dir (file-name-as-directory
- (expand-file-name (or tools-directory cj/gptel-tools-directory))))
- (features (or tool-features cj/gptel-local-tool-features))
- (loaded nil))
- (cond
- ((not (file-directory-p dir))
- (message "GPTel tools directory not found: %s" dir)
- nil)
- (t
- (add-to-list 'load-path dir)
- (dolist (feature features)
- (condition-case err
- (if (require feature nil 'noerror)
- (push feature loaded)
- (message "Optional GPTel tool not found: %s" feature))
- (error
- (message "Failed to load GPTel tool %s: %s"
- feature
- (error-message-string err)))))
- (nreverse loaded)))))
-
-(with-eval-after-load 'gptel
- (require 'ai-conversations)
- (cj/gptel-load-local-tools))
-
-(defun cj/auth-source-secret (host user)
- "Fetch a required secret from auth-source for HOST and USER.
-
-HOST and USER must be strings that identify the credential to return.
-Errors when no secret is found."
- (or (cj/auth-source-secret-value host user)
- (error "No usable secret found for host %s and user %s" host user)))
-
-(defun cj/anthropic-api-key ()
- "Return the Anthropic API key, caching the result after first retrieval."
- (or cj/anthropic-api-key-cached
- (setq cj/anthropic-api-key-cached
- (cj/auth-source-secret "api.anthropic.com" "apikey"))))
-
-(defun cj/openai-api-key ()
- "Return the OpenAI API key, caching the result after first retrieval."
- (or cj/openai-api-key-cached
- (setq cj/openai-api-key-cached
- (cj/auth-source-secret "api.openai.com" "apikey"))))
-
-(defun cj/--gptel-load-backend-libs ()
- "Require the gptel backend libraries so their `gptel-make-*' constructors exist.
-The local fork (`:load-path \"~/code/gptel\"', `:ensure nil') ships no generated
-autoloads, so requiring `gptel' alone never loads `gptel-anthropic' /
-`gptel-openai', where the constructors are defined."
- (require 'gptel-anthropic)
- (require 'gptel-openai))
-
-(defun cj/ensure-gptel-backends ()
- "Initialize GPTel backends if they are not already available.
-Loads the backend libraries first so the `gptel-make-*' constructors are
-defined even when gptel is the local fork without generated autoloads."
- (cj/--gptel-load-backend-libs)
- (unless gptel-claude-backend
- (setq gptel-claude-backend
- (gptel-make-anthropic
- "Claude"
- :key (cj/anthropic-api-key)
- :models '(
- "claude-opus-4-7"
- "claude-sonnet-4-6"
- "claude-haiku-4-5-20251001"
- )
- :stream t)))
- (unless gptel-chatgpt-backend
- (setq gptel-chatgpt-backend
- (gptel-make-openai
- "ChatGPT"
- :key (cj/openai-api-key)
- :models '(
- "gpt-5.5"
- "gpt-5.4-mini"
- "o3"
- )
- :stream t)))
- ;; Set default backend and model
- (unless gptel-backend
- (setq gptel-backend (or gptel-chatgpt-backend gptel-claude-backend))
- (setq gptel-model 'gpt-5.5)))
-
-;; ------------------ GPTel Conversation And Utility Commands ------------------
-
-(defun cj/gptel--available-backends ()
- "Return an alist of (NAME . BACKEND).
-Ensures gptel and backends are initialized."
- (unless (featurep 'gptel)
- (require 'gptel))
- (cj/ensure-gptel-backends)
- (delq nil
- (list (and (bound-and-true-p gptel-claude-backend)
- (cons "Anthropic - Claude" gptel-claude-backend))
- (and (bound-and-true-p gptel-chatgpt-backend)
- (cons "OpenAI - ChatGPT" gptel-chatgpt-backend)))))
-
-(defun cj/gptel--model-to-string (m)
- "Return model M as a string regardless of its type."
- (cond
- ((stringp m) m)
- ((symbolp m) (symbol-name m))
- (t (format "%s" m))))
-
-(defun cj/gptel--model-to-symbol (m)
- "Return model M as a symbol regardless of its type.
-`gptel-model' must be a symbol: gptel's modeline code calls `symbolp'
-on it and signals `wrong-type-argument' on a string, which surfaces as a
-redisplay hang. Coerce any model value through this before assigning it."
- (cond
- ((symbolp m) m)
- ((stringp m) (intern m))
- (t (intern (format "%s" m)))))
-
-;; Backend/model switching helpers (pure logic, extracted for testability)
-
-(defun cj/gptel--build-model-list (backends model-fn)
- "Build a flat list of all models across BACKENDS.
-BACKENDS is an alist of (NAME . BACKEND-OBJECT). MODEL-FN is called
-with each backend object and should return a list of model identifiers.
-Returns a list of entries: (DISPLAY-STRING BACKEND MODEL-STRING BACKEND-NAME)
-where DISPLAY-STRING is \"Backend: model\" for use in completing-read."
- (mapcan
- (lambda (pair)
- (let* ((backend-name (car pair))
- (backend (cdr pair))
- (models (funcall model-fn backend)))
- (mapcar (lambda (m)
- (list (format "%s: %s" backend-name (cj/gptel--model-to-string m))
- backend
- (cj/gptel--model-to-string m)
- backend-name))
- models)))
- backends))
-
-(defun cj/gptel--current-model-selection (backends current-backend current-model)
- "Format the current backend/model as a display string.
-BACKENDS is the alist from `cj/gptel--available-backends'.
-CURRENT-BACKEND and CURRENT-MODEL are the active gptel settings.
-Returns a string like \"Anthropic - Claude: claude-opus-4-7\"."
- (let ((backend-name (car (rassoc current-backend backends))))
- (format "%s: %s"
- (or backend-name "AI")
- (cj/gptel--model-to-string current-model))))
-
-;; Backend/model switching commands
-(defun cj/gptel-change-model ()
- "Change the GPTel backend and select a model from that backend.
-Present all available models from every backend, switching backends when
-necessary. Prompt for whether to apply the selection globally or buffer-locally."
- (interactive)
- (let* ((backends (cj/gptel--available-backends))
- (all-models (cj/gptel--build-model-list
- backends
- (lambda (b)
- (when (fboundp 'gptel-backend-models)
- (gptel-backend-models b)))))
- (current-selection (cj/gptel--current-model-selection
- backends
- (bound-and-true-p gptel-backend)
- (bound-and-true-p gptel-model)))
- (scope (completing-read "Set model for: " '("buffer" "global") nil t))
- (selected (completing-read
- (format "Select model (current: %s): " current-selection)
- (mapcar #'car all-models) nil t nil nil current-selection)))
- (let* ((model-info (assoc selected all-models))
- (backend (nth 1 model-info))
- (model (intern (nth 2 model-info)))
- (backend-name (nth 3 model-info)))
- (if (string= scope "global")
- (progn
- (setq gptel-backend backend)
- (setq gptel-model model)
- (message "Changed to %s model: %s (global)" backend-name model))
- (setq-local gptel-backend backend)
- (setq-local gptel-model (if (stringp model) (intern model) model))
- (message "Changed to %s model: %s (buffer-local)" backend-name model)))))
-
-(defun cj/gptel-switch-backend ()
- "Switch the GPTel backend and then choose one of its models."
- (interactive)
- (let* ((backends (cj/gptel--available-backends))
- (choice (completing-read "Select GPTel backend: " (mapcar #'car backends) nil t))
- (backend (cdr (assoc choice backends))))
- (unless backend
- (user-error "Invalid GPTel backend: %s" choice))
- (let* ((models (when (fboundp 'gptel-backend-models)
- (gptel-backend-models backend)))
- (model (completing-read (format "Select %s model: " choice)
- (mapcar #'cj/gptel--model-to-string models)
- nil t nil nil (cj/gptel--model-to-string (bound-and-true-p gptel-model)))))
- (setq gptel-backend backend
- gptel-model (cj/gptel--model-to-symbol model))
- (message "Switched to %s with model: %s" choice model))))
-
-;; Clear assistant buffer (moved out so it's always available)
-(defun cj/gptel-clear-buffer ()
- "Erase the current GPTel buffer while preserving the initial Org heading.
-Operate only when `gptel-mode' is active in an Org buffer so the heading
-can be reinserted."
- (interactive)
- (let ((is-gptel (bound-and-true-p gptel-mode))
- (is-org (derived-mode-p 'org-mode)))
- (if (and is-gptel is-org)
- (progn
- (erase-buffer)
- (when (fboundp 'cj/gptel--fresh-org-prefix)
- (insert (cj/gptel--fresh-org-prefix)))
- (message "GPTel buffer cleared and heading reset"))
- (message "Not a GPTel buffer in org-mode. Nothing cleared."))))
-
-;; ----------------------------- Context Management ----------------------------
-
-(defun cj/gptel--add-file-to-context (file-path)
- "Add FILE-PATH to the GPTel context.
-Returns t on success, nil on failure.
-Provides consistent user feedback about the context state."
- (when (and file-path (file-exists-p file-path))
- (gptel-add-file file-path)
- (let ((context-count (if (boundp 'gptel-context--alist)
- (length gptel-context--alist)
- 0)))
- (message "Added %s to GPTel context (%d sources total)"
- (file-name-nondirectory file-path)
- context-count))
- t))
-
-(defun cj/gptel-add-file ()
- "Add a file to the GPTel context.
-If inside a Projectile project, prompt from that project's file list.
-Otherwise, prompt with `read-file-name'."
- (interactive)
- (let* ((in-proj (and (featurep 'projectile)
- (fboundp 'projectile-project-p)
- (projectile-project-p)))
- (file-name (if in-proj
- (let ((cands (projectile-current-project-files)))
- (if (fboundp 'projectile-completing-read)
- (projectile-completing-read "GPTel add file: " cands)
- (completing-read "GPTel add file: " cands nil t)))
- (read-file-name "GPTel add file: ")))
- (file-path (if in-proj
- (expand-file-name file-name (projectile-project-root))
- file-name)))
- (unless (cj/gptel--add-file-to-context file-path)
- (error "Failed to add file: %s" file-path))))
-
-(defun cj/gptel-add-buffer-file ()
- "Select a buffer and add its associated file to the GPTel context.
-Lists all open buffers for selection. If the selected buffer is visiting
-a file, that file is added to the GPTel context. Otherwise, an error
-message is displayed."
- (interactive)
- (let* ((buffers (mapcar #'buffer-name (buffer-list)))
- (selected-buffer-name (completing-read "Add file from buffer: " buffers nil t))
- (selected-buffer (get-buffer selected-buffer-name))
- (file-path (and selected-buffer
- (buffer-file-name selected-buffer))))
- (if file-path
- (cj/gptel--add-file-to-context file-path)
- (message "Buffer '%s' is not visiting a file" selected-buffer-name))))
-
-(defun cj/gptel-add-this-buffer ()
- "Add the current buffer to the GPTel context.
-Works for any buffer, whether it's visiting a file or not."
- (interactive)
- ;; Load gptel-context if needed
- (unless (featurep 'gptel-context)
- (require 'gptel-context))
- ;; Use gptel-add with prefix arg '(4) to add current buffer
- (gptel-add '(4))
- (message "Added buffer '%s' to GPTel context" (buffer-name)))
-
-;;; -------------------------- Org Header Construction --------------------------
-
-(defun cj/gptel--fresh-org-prefix ()
- "Generate a fresh org-mode header with current timestamp for user messages."
- (concat "* " user-login-name " " (format-time-string "[%Y-%m-%d %H:%M:%S]") "\n"))
-
-(defun cj/gptel--refresh-org-prefix (&rest _)
- "Update the org-mode prefix with fresh timestamp before sending message."
- (setf (alist-get 'org-mode gptel-prompt-prefix-alist)
- (cj/gptel--fresh-org-prefix)))
-
-(defun cj/gptel-backend-and-model ()
- "Return backend, model, and timestamp as a single string."
- (let* ((backend (pcase (bound-and-true-p gptel-backend)
- ((and v (pred vectorp)) (aref v 1))
- (_ "AI")))
- (model (format "%s" (or (bound-and-true-p gptel-model) "")))
- (ts (format-time-string "[%Y-%m-%d %H:%M:%S]")))
- (format "%s: %s %s" backend model ts)))
-
-(defun cj/gptel-insert-model-heading (response-begin-pos _response-end-pos)
- "Insert an Org heading for the AI reply at RESPONSE-BEGIN-POS."
- (save-excursion
- (goto-char response-begin-pos)
- (insert (format "* %s\n" (cj/gptel-backend-and-model)))))
-
-;;; ---------------------------- GPTel Configuration ----------------------------
-
-(use-package gptel
- :load-path "~/code/gptel"
- :ensure nil
- :defer t
- :commands (gptel gptel-send gptel-menu)
- :bind
- (:map gptel-mode-map
- ("C-<return>" . gptel-send))
- :custom
- (gptel-default-mode 'org-mode)
- (gptel-expert-commands t)
- (gptel-track-media t)
- ;; Options: t (include + resend), 'ignore (show but don't resend),
- ;; nil (discard), or a buffer name to redirect reasoning to
- (gptel-include-reasoning "*AI-Reasoning*")
- (gptel-log-level 'info)
- (gptel--debug nil)
- :config
- (cj/ensure-gptel-backends)
- ;; Set ChatGPT (gpt-5.5) as default after initialization. Model
- ;; must be a symbol -- gptel's modeline-display code calls `symbolp'
- ;; on it and signals `wrong-type-argument' otherwise.
- (setq gptel-backend gptel-chatgpt-backend)
- (setq gptel-model 'gpt-5.5)
-
- (setq gptel-confirm-tool-calls nil) ;; allow tool access by default
-
- ;; Initialize org-mode user prefix and wire up hooks
- (setf (alist-get 'org-mode gptel-prompt-prefix-alist)
- (cj/gptel--fresh-org-prefix))
- (advice-add 'gptel-send :before #'cj/gptel--refresh-org-prefix)
- (add-hook 'gptel-post-response-functions #'cj/gptel-insert-model-heading))
-
-;;; ---------------------------- Toggle GPTel Window ----------------------------
-
-(defvar cj/ai-assistant-window-width 0.4
- "Default fraction of frame width for the *AI-Assistant* side window.
-Used until the panel is resized and toggled off this session; after that,
-the toggled-off width is remembered in `cj/--ai-assistant-width'.")
-
-(defvar cj/--ai-assistant-width nil
- "Last width fraction the *AI-Assistant* side window was toggled off at.
-nil falls back to `cj/ai-assistant-window-width'. Shared by the panel's
-entry points (toggle, load-conversation, quick-ask escalation) so the
-panel reopens at one consistent width. In-memory only -- resets each
-Emacs session.")
-
-(defun cj/toggle-gptel ()
- "Toggle the visibility of the AI-Assistant buffer, and place point at its end.
-The panel opens at `cj/ai-assistant-window-width'; once it has been resized
-and toggled off this session, it reopens at that remembered width."
- (interactive)
- (let* ((buf-name "*AI-Assistant*")
- (buffer (get-buffer buf-name))
- (win (and buffer (get-buffer-window buffer))))
- (if win
- (progn
- (cj/side-window-capture-size win 'right 'cj/--ai-assistant-width)
- (delete-window win))
- ;; Ensure GPTel and our backends are initialized before creating the buffer
- (unless (featurep 'gptel)
- (require 'gptel))
- (cj/ensure-gptel-backends)
- (unless buffer
- ;; Pass backend, not model
- (gptel buf-name gptel-backend))
- (setq buffer (get-buffer buf-name))
- (setq win
- (cj/side-window-display
- buffer 'right 'cj/--ai-assistant-width
- cj/ai-assistant-window-width))
- (select-window win)
- (with-current-buffer buffer
- (goto-char (point-max))))))
-
-;; ------------------------------- Clear Context -------------------------------
-
-(defun cj/gptel-context-clear ()
- "Clear all GPTel context sources, with compatibility across GPTel versions."
- (interactive)
- (cond
- ((fboundp 'gptel-context-remove-all)
- (call-interactively 'gptel-context-remove-all)
- (message "GPTel context cleared"))
- ((fboundp 'gptel-context-clear)
- (call-interactively 'gptel-context-clear)
- (message "GPTel context cleared"))
- ((boundp 'gptel-context--alist)
- (setq gptel-context--alist nil)
- (message "GPTel context cleared"))
- (t
- (message "No known GPTel context clearing function available"))))
-
-;;; -------------------------------- GPTel-Magit --------------------------------
-
-;; Each integration point waits on its actual dependency, not on `magit'
-;; broadly. `magit.el' calls `(provide 'magit)' BEFORE its
-;; `cl-eval-when (load eval) ...' block requires `magit-commit' and
-;; `magit-stash', so a single `with-eval-after-load 'magit' fires while
-;; the transient prefixes the wiring references are still undefined.
-;; `transient-append-suffix' silently no-ops on missing prefixes (it
-;; calls `message' unless `transient-error-on-insert-failure' is set),
-;; which is how the failure stayed invisible.
-;;
-;; Keys:
-;; M-g — generate commit message (in commit message buffer)
-;; g — generate commit (in magit-commit transient)
-;; x — explain diff (in magit-diff transient)
-
-(use-package gptel-magit
- :defer t
- :commands (gptel-magit-generate-message
- gptel-magit-commit-generate
- gptel-magit-diff-explain)
- :init
- (with-eval-after-load 'git-commit
- (define-key git-commit-mode-map (kbd "M-g") #'gptel-magit-generate-message))
- (with-eval-after-load 'magit-commit
- (transient-append-suffix 'magit-commit #'magit-commit-create
- '("g" "Generate commit" gptel-magit-commit-generate)))
- (with-eval-after-load 'magit-diff
- (transient-append-suffix 'magit-diff #'magit-stash-show
- '("x" "Explain" gptel-magit-diff-explain))))
-
-;; ------------------------------ GPTel Directives -----------------------------
-
-(use-package gptel-prompts
- :load-path (lambda () (expand-file-name "custom/" user-emacs-directory))
- :after gptel
- :if (file-exists-p (expand-file-name "custom/gptel-prompts.el" user-emacs-directory))
- :custom
- (gptel-prompts-directory (concat user-emacs-directory "ai-prompts"))
- :config
- (gptel-prompts-update)
- (gptel-prompts-add-update-watchers)
- ;; gptel--system-message is set at gptel load time, before gptel-prompts
- ;; replaces the default directive. Re-apply it now.
- (when-let* ((dir (alist-get 'default gptel-directives)))
- (setq gptel--system-message dir)))
-
-;;; --------------------------------- AI Keymap ---------------------------------
-
-(defvar-keymap cj/ai-keymap
- :doc "Keymap for gptel and other AI operations."
- "A" #'cj/gptel-autosave-toggle ;; toggle autosave on the current GPTel buffer
- "B" #'cj/gptel-switch-backend ;; change the backend (OpenAI, Anthropic, etc.
- "M" #'gptel-menu ;; gptel's transient menu
- "d" #'cj/gptel-delete-conversation ;; delete conversation
- "." #'cj/gptel-add-this-buffer ;; add buffer to context
- "f" #'cj/gptel-add-file ;; add a file to context
- "b" #'cj/gptel-browse-conversations ;; browse saved conversations
- "l" #'cj/gptel-load-conversation ;; load and continue conversation
- "m" #'cj/gptel-change-model ;; change the LLM model
- "p" #'gptel-system-prompt ;; change prompt
- "q" #'cj/gptel-quick-ask ;; one-shot quick ask
- "r" #'cj/gptel-rewrite-with-directive ;; rewrite region with a chosen directive
- "R" #'cj/gptel-rewrite-redo-with-different-directive ;; redo last rewrite, new directive
- "c" #'cj/gptel-context-clear ;; clear all context
- "s" #'cj/gptel-save-conversation ;; save conversation
- "t" #'cj/toggle-gptel ;; toggles the ai-assistant window
- "x" #'cj/gptel-clear-buffer) ;; clears the assistant buffer
-(cj/register-prefix-map "a" cj/ai-keymap)
-
-(with-eval-after-load 'which-key
- (which-key-add-key-based-replacements
- "C-; a" "AI assistant menu"
- "C-; a A" "toggle autosave"
- "C-; a B" "switch backend"
- "C-; a M" "gptel menu"
- "C-; a b" "browse conversations"
- "C-; a d" "delete conversation"
- "C-; a ." "add buffer"
- "C-; a f" "add file"
- "C-; a l" "load conversation"
- "C-; a m" "change model"
- "C-; a p" "change prompt"
- "C-; a q" "quick ask"
- "C-; a r" "rewrite region (directive)"
- "C-; a R" "redo rewrite, new directive"
- "C-; a c" "clear context"
- "C-; a s" "save conversation"
- "C-; a t" "toggle window"
- "C-; a x" "clear buffer"))
-
-(provide 'ai-config)
-;;; ai-config.el ends here.