aboutsummaryrefslogtreecommitdiff
path: root/archive/gptel/modules/ai-config.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-23 20:12:58 -0400
committerCraig Jennings <c@cjennings.net>2026-06-23 20:12:58 -0400
commit10fa6f4e2e7150ad99827721ada1ae4badcc5e90 (patch)
tree960065c8e69f1e7a4150ecf522e2f813c239b5ee /archive/gptel/modules/ai-config.el
parentf4cc70c69e7707dd4a686637e14885f5443fcca6 (diff)
downloaddotemacs-10fa6f4e2e7150ad99827721ada1ae4badcc5e90.tar.gz
dotemacs-10fa6f4e2e7150ad99827721ada1ae4badcc5e90.zip
chore(ai): archive gptel and remove it from the live config
I archived gptel to archive/gptel/ since I rarely use it. Moved there: the six gptel modules (ai-config, ai-conversations, ai-conversations-browser, ai-mcp, ai-quick-ask, ai-rewrite), the gptel-tools/ directory, custom/gptel-prompts.el, their test files and utilities, and the four gptel-only specs. Scrubbed from the live config: the ai-config require in init.el, which also drops the whole C-; a keymap; the gptel-mode emojify hook in font-config.el; the gptel-tools entries in the Makefile clean target and the coverage runner; and the gptel feature notes in README. Cancelled the open gptel tasks in todo.org (the AI Open Work issues, the feature-extension brainstorm, the velox gptel-magit bug). ai-term stays. It is the ghostel Claude launcher, independent of gptel. Verified: every module loads, a batch init launch reaches completion clean, and the full test suite shows only pre-existing coverage failures unrelated to this change.
Diffstat (limited to 'archive/gptel/modules/ai-config.el')
-rw-r--r--archive/gptel/modules/ai-config.el585
1 files changed, 585 insertions, 0 deletions
diff --git a/archive/gptel/modules/ai-config.el b/archive/gptel/modules/ai-config.el
new file mode 100644
index 00000000..97af1296
--- /dev/null
+++ b/archive/gptel/modules/ai-config.el
@@ -0,0 +1,585 @@
+;;; 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))))
+
+(defun cj/--gptel-apply-model-selection (scope backend model backend-name)
+ "Set gptel BACKEND and MODEL, globally or buffer-locally per SCOPE.
+SCOPE is \"global\" or \"buffer\"; any non-\"global\" value is buffer-local.
+MODEL is a symbol. BACKEND-NAME is the display name for the confirmation.
+Returns the confirmation message string."
+ (if (string= scope "global")
+ (progn
+ (setq gptel-backend backend)
+ (setq gptel-model model)
+ (format "Changed to %s model: %s (global)" backend-name model))
+ (setq-local gptel-backend backend)
+ (setq-local gptel-model model)
+ (format "Changed to %s model: %s (buffer-local)" backend-name 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)))
+ (message "%s" (cj/--gptel-apply-model-selection
+ scope backend model backend-name)))))
+
+(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.