aboutsummaryrefslogtreecommitdiff
path: root/modules/ai-conversations.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
commite41c25068d0cec9434895a6d3e3a25d3a26f645f (patch)
tree5e30938a3fd6d80f501ffe3e6c1c187c5ddeb2c9 /modules/ai-conversations.el
parenta936e081b7270fbd4f1e7e9cb67ca1d4c2291ce6 (diff)
downloaddotemacs-e41c25068d0cec9434895a6d3e3a25d3a26f645f.tar.gz
dotemacs-e41c25068d0cec9434895a6d3e3a25d3a26f645f.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 'modules/ai-conversations.el')
-rw-r--r--modules/ai-conversations.el369
1 files changed, 0 insertions, 369 deletions
diff --git a/modules/ai-conversations.el b/modules/ai-conversations.el
deleted file mode 100644
index 8061051a8..000000000
--- a/modules/ai-conversations.el
+++ /dev/null
@@ -1,369 +0,0 @@
-;;; ai-conversations.el --- GPTel conversation persistence and autosave -*- lexical-binding: t; coding: utf-8; -*-
-;; Author: Craig Jennings <c@cjennings.net>
-;; Maintainer: Craig Jennings <c@cjennings.net>
-;; Version 0.1
-;; Package-Requires: ((emacs "27.1"))
-;; Keywords: convenience, tools
-;;
-;;; Commentary:
-;; Provides conversation save/load/delete, autosave after responses, and
-;; org-visibility headers for GPTel-powered assistant buffers.
-;;
-;; Loads lazily via autoloads for the interactive entry points.
-
-;;; Code:
-
-(require 'cj-window-toggle-lib) ;; cj/side-window-display
-
-;; Shared *AI-Assistant* remembered-width state, owned by ai-config.el.
-;; Forward-declared so loading a conversation reopens the panel at the same
-;; width as the F-key toggle without a circular require.
-(defvar cj/--ai-assistant-width)
-
-(defgroup cj/ai-conversations nil
- "Conversation persistence for GPTel (save/load/delete, autosave)."
- :group 'gptel
- :prefix "cj/")
-
-(defcustom cj/gptel-conversations-directory
- (expand-file-name "ai-conversations" user-emacs-directory)
- "Directory where GPTel conversations are stored."
- :type 'directory
- :group 'cj/ai-conversations)
-
-(defcustom cj/gptel-conversations-window-side 'right
- "Side to display the AI-Assistant buffer when loading a conversation."
- :type '(choice (const :tag "Right" right)
- (const :tag "Left" left)
- (const :tag "Bottom" bottom)
- (const :tag "Top" top))
- :group 'cj/ai-conversations)
-
-(defcustom cj/gptel-conversations-window-width 0.4
- "Set the side window width when loading a conversation.
-
-If displaying on the top or bottom, treat this value as a height fraction."
- :type 'number
- :group 'cj/ai-conversations)
-
-(defcustom cj/gptel-conversations-sort-order 'newest-first
- "Sort order for conversation selection prompts."
- :type '(choice (const :tag "Newest first" newest-first)
- (const :tag "Oldest first" oldest-first))
- :group 'cj/ai-conversations)
-
-(defvar-local cj/gptel-autosave-enabled nil
- "Non-nil means auto-save after each AI response in GPTel buffers.")
-
-(defvar-local cj/gptel-autosave-filepath nil
- "File path used for auto-saving the conversation buffer.")
-
-(defvar-local cj/gptel-autosave--timer nil
- "Repeating timer used to auto-save the current GPTel buffer.")
-
-(defcustom cj/gptel-autosave-interval 60
- "Seconds between periodic GPTel conversation autosaves."
- :type 'number
- :group 'cj/ai-conversations)
-
-(defvar cj/gptel-autosave-mode-line-format
- '(:eval (when (bound-and-true-p cj/gptel-autosave-enabled) " [AS]"))
- "Mode-line construct that surfaces autosave state in GPTel buffers.")
-(put 'cj/gptel-autosave-mode-line-format 'risky-local-variable t)
-
-(defun cj/gptel--autosave-active-p ()
- "Return non-nil when the current buffer has an autosave target."
- (and (bound-and-true-p gptel-mode)
- cj/gptel-autosave-enabled
- (stringp cj/gptel-autosave-filepath)
- (> (length cj/gptel-autosave-filepath) 0)))
-
-(defun cj/gptel--autosave-stop-timer ()
- "Cancel the current buffer's periodic autosave timer, if any."
- (when cj/gptel-autosave--timer
- (cancel-timer cj/gptel-autosave--timer)
- (setq-local cj/gptel-autosave--timer nil)))
-
-(defun cj/gptel--autosave-timer-callback (buffer)
- "Auto-save BUFFER from a periodic timer when autosave is still active."
- (when (buffer-live-p buffer)
- (with-current-buffer buffer
- (if (cj/gptel--autosave-active-p)
- (condition-case err
- (cj/gptel--save-buffer-to-file (current-buffer) cj/gptel-autosave-filepath)
- (error (message "cj/gptel periodic autosave failed: %s"
- (error-message-string err))))
- (cj/gptel--autosave-stop-timer)))))
-
-(defun cj/gptel--autosave-start-timer ()
- "Start the current buffer's periodic autosave timer when autosave is active."
- (when (and (cj/gptel--autosave-active-p)
- (not cj/gptel-autosave--timer))
- (setq-local cj/gptel-autosave--timer
- (run-with-timer cj/gptel-autosave-interval
- cj/gptel-autosave-interval
- #'cj/gptel--autosave-timer-callback
- (current-buffer)))))
-
-(defun cj/gptel-autosave-toggle ()
- "Toggle autosave on/off in the current GPTel buffer.
-Flips `cj/gptel-autosave-enabled' and forces a mode-line redisplay so
-the [AS] indicator updates immediately. When turning autosave ON
-without a configured filepath, prompt to save the conversation first
-so a path exists to autosave to."
- (interactive)
- (unless (bound-and-true-p gptel-mode)
- (user-error "Not a GPTel buffer"))
- (if cj/gptel-autosave-enabled
- (progn
- (setq-local cj/gptel-autosave-enabled nil)
- (cj/gptel--autosave-stop-timer)
- (message "Autosave disabled"))
- (cond
- ((and (stringp cj/gptel-autosave-filepath)
- (> (length cj/gptel-autosave-filepath) 0))
- (setq-local cj/gptel-autosave-enabled t)
- (cj/gptel--autosave-start-timer)
- (message "Autosave enabled (saving to %s)"
- (file-name-nondirectory cj/gptel-autosave-filepath)))
- ((y-or-n-p "No save target yet. Save conversation first? ")
- (call-interactively #'cj/gptel-save-conversation))
- (t
- (message "Autosave not enabled (no save target)"))))
- (force-mode-line-update))
-
-(defcustom cj/gptel-conversations-autosave-on-send t
- "Non-nil means auto-save the conversation immediately after `gptel-send'."
- :type 'boolean
- :group 'cj/ai-conversations)
-
-(defun cj/gptel--autosave-after-send (&rest _args)
- "Auto-save current GPTel buffer right after `gptel-send' if enabled."
- (when (and cj/gptel-conversations-autosave-on-send
- (cj/gptel--autosave-active-p))
- (condition-case err
- (cj/gptel--save-buffer-to-file (current-buffer) cj/gptel-autosave-filepath)
- (error (message "cj/gptel autosave-on-send failed: %s" (error-message-string err))))))
-
-(with-eval-after-load 'gptel
- (unless (advice-member-p #'cj/gptel--autosave-after-send #'gptel-send)
- (advice-add 'gptel-send :after #'cj/gptel--autosave-after-send)))
-
-(defun cj/gptel--install-autosave-mode-line ()
- "Add the [AS] autosave indicator to the current buffer's mode-line.
-Idempotent: re-running in the same buffer does not duplicate the
-construct."
- (unless (member 'cj/gptel-autosave-mode-line-format mode-line-format)
- (setq-local mode-line-format
- (append mode-line-format
- (list 'cj/gptel-autosave-mode-line-format)))))
-
-(defun cj/gptel--install-autosave-buffer-hooks ()
- "Install buffer-local cleanup hooks for GPTel autosave."
- (add-hook 'kill-buffer-hook #'cj/gptel--autosave-stop-timer nil t))
-
-(with-eval-after-load 'gptel
- (add-hook 'gptel-mode-hook #'cj/gptel--install-autosave-mode-line)
- (add-hook 'gptel-mode-hook #'cj/gptel--install-autosave-buffer-hooks))
-
-(defun cj/gptel--slugify-topic (s)
- "Return a filesystem-friendly slug for topic string S."
- (let* ((down (downcase (or s "")))
- (repl (replace-regexp-in-string "[^a-z0-9]+" "-" down))
- (trim (replace-regexp-in-string "^-+\\|-+$" "" repl)))
- (or (and (> (length trim) 0) trim) "conversation")))
-
-(defun cj/gptel--existing-topics ()
- "Return topic slugs, without timestamps, present in the conversations directory."
- (when (file-exists-p cj/gptel-conversations-directory)
- (let* ((files (directory-files cj/gptel-conversations-directory nil "\\.gptel$")))
- (delete-dups
- (mapcar
- (lambda (f)
- (replace-regexp-in-string "_[0-9]\\{8\\}-[0-9]\\{6\\}\\.gptel$" "" f))
- files)))))
-
-(defun cj/gptel--latest-file-for-topic (topic-slug)
- "Return the newest saved conversation filename for TOPIC-SLUG, or nil."
- (let* ((rx (format "^%s_[0-9]\\{8\\}-[0-9]\\{6\\}\\.gptel$"
- (regexp-quote topic-slug)))
- (files (and (file-exists-p cj/gptel-conversations-directory)
- (directory-files cj/gptel-conversations-directory nil rx))))
- (car (sort files #'string>))))
-
-(defun cj/gptel--timestamp-from-filename (filename)
- "Return an Emacs timestamp extracted from FILENAME, or nil.
-
-Expect FILENAME to match _YYYYMMDD-HHMMSS.gptel."
- (when (string-match "_\\([0-9]\\{8\\}\\)-\\([0-9]\\{6\\}\\)\\.gptel\\'" filename)
- (let* ((date (match-string 1 filename))
- (time (match-string 2 filename))
- (Y (string-to-number (substring date 0 4)))
- (M (string-to-number (substring date 4 6)))
- (D (string-to-number (substring date 6 8)))
- (h (string-to-number (substring time 0 2)))
- (m (string-to-number (substring time 2 4)))
- (s (string-to-number (substring time 4 6))))
- (encode-time s m h D M Y))))
-
-(defun cj/gptel--conversation-candidates ()
- "Return conversation candidates sorted per `cj/gptel-conversations-sort-order'."
- (unless (file-exists-p cj/gptel-conversations-directory)
- (user-error "Conversations directory doesn't exist: %s" cj/gptel-conversations-directory))
- (let* ((files (directory-files cj/gptel-conversations-directory nil "\\.gptel$"))
- (enriched
- (mapcar
- (lambda (f)
- (let* ((full (expand-file-name f cj/gptel-conversations-directory))
- (ptime (or (cj/gptel--timestamp-from-filename f)
- (nth 5 (file-attributes full))))
- (disp (format "%s [%s]" f (format-time-string "%Y-%m-%d %H:%M" ptime))))
- (list :file f :time ptime :display disp)))
- files))
- (sorted
- (sort enriched
- (lambda (a b)
- (let ((ta (plist-get a :time))
- (tb (plist-get b :time)))
- (if (eq cj/gptel-conversations-sort-order 'newest-first)
- (time-less-p tb ta) ;; tb earlier than ta => a first
- (time-less-p ta tb))))))
- (cands (mapcar (lambda (pl)
- (cons (plist-get pl :display)
- (plist-get pl :file)))
- sorted)))
- cands))
-
-(defun cj/gptel--save-buffer-to-file (buffer filepath)
- "Save BUFFER content to FILEPATH with Org visibility properties."
- (with-current-buffer buffer
- (let ((content (buffer-string)))
- (with-temp-buffer
- (insert "#+STARTUP: showeverything\n")
- (insert "#+VISIBILITY: all\n\n")
- (insert content)
- (write-region (point-min) (point-max) filepath nil 'silent))))
- filepath)
-
-(defun cj/gptel--ensure-ai-buffer ()
- "Return the *AI-Assistant* buffer, creating it via `gptel' if needed."
- (let* ((buf-name "*AI-Assistant*")
- (buffer (get-buffer buf-name)))
- (unless buffer
- (gptel buf-name))
- (or (get-buffer buf-name)
- (user-error "Could not create or find *AI-Assistant* buffer"))))
-
-(defun cj/gptel-save-conversation ()
- "Save the current AI-Assistant buffer to a .gptel file.
-
-Enable autosave for subsequent AI responses to the same file."
- (interactive)
- (let ((buf (get-buffer "*AI-Assistant*")))
- (unless buf
- (user-error "No AI-Assistant buffer found"))
- (unless (file-exists-p cj/gptel-conversations-directory)
- (make-directory cj/gptel-conversations-directory t)
- (message "Created directory: %s" cj/gptel-conversations-directory))
- (let* ((topics (or (cj/gptel--existing-topics) '()))
- (input (completing-read "Conversation topic: " topics nil nil))
- (topic-slug (cj/gptel--slugify-topic input))
- (latest (cj/gptel--latest-file-for-topic topic-slug))
- (use-existing (and latest
- (y-or-n-p (format "Update existing file %s? " latest))))
- (filepath (if use-existing
- (expand-file-name latest cj/gptel-conversations-directory)
- (let* ((timestamp (format-time-string "%Y%m%d-%H%M%S"))
- (filename (format "%s_%s.gptel" topic-slug timestamp)))
- (expand-file-name filename cj/gptel-conversations-directory)))))
- (cj/gptel--save-buffer-to-file buf filepath)
- (with-current-buffer buf
- (setq-local cj/gptel-autosave-filepath filepath)
- (setq-local cj/gptel-autosave-enabled t)
- (cj/gptel--autosave-start-timer))
- (message "Conversation saved to: %s" filepath))))
-
-(defun cj/gptel-delete-conversation ()
- "Delete a saved GPTel conversation file (chronologically sorted candidates)."
- (interactive)
- (unless (file-exists-p cj/gptel-conversations-directory)
- (user-error "Conversations directory doesn't exist: %s" cj/gptel-conversations-directory))
- (let* ((cands (cj/gptel--conversation-candidates)))
- (unless cands
- (user-error "No saved conversations found in %s" cj/gptel-conversations-directory))
- (let* ((completion-extra-properties '(:display-sort-function identity
- :cycle-sort-function identity))
- (selection (completing-read "Delete conversation: " cands nil t))
- (filename (cdr (assoc selection cands)))
- (filepath (and filename
- (expand-file-name filename cj/gptel-conversations-directory))))
- (unless filename
- (user-error "No conversation selected"))
- (when (y-or-n-p (format "Really delete %s? " filename))
- (delete-file filepath)
- (message "Deleted conversation: %s" filename)))))
-
-(defun cj/gptel--strip-visibility-headers ()
- "Strip org visibility headers at the top of the current buffer if present."
- (save-excursion
- (goto-char (point-min))
- (while (looking-at "^#\\+\\(STARTUP\\|VISIBILITY\\):.*\n")
- (delete-region (match-beginning 0) (match-end 0)))
- (when (looking-at "^\n+")
- (delete-region (point) (match-end 0)))))
-
-(defun cj/gptel-load-conversation ()
- "Load a saved GPTel conversation into the AI-Assistant buffer.
-
-Prompt to save the current conversation first when appropriate, then
-enable autosave."
- (interactive)
- (let ((ai-buffer (get-buffer-create "*AI-Assistant*")))
- (when (and (with-current-buffer ai-buffer (> (buffer-size) 0))
- (with-current-buffer ai-buffer (bound-and-true-p gptel-mode)))
- (when (y-or-n-p "Save current conversation before loading new one? ")
- (with-current-buffer ai-buffer
- (call-interactively #'cj/gptel-save-conversation)))))
- (unless (file-exists-p cj/gptel-conversations-directory)
- (user-error "Conversations directory doesn't exist: %s" cj/gptel-conversations-directory))
- (let* ((cands (cj/gptel--conversation-candidates)))
- (unless cands
- (user-error "No saved conversations found in %s" cj/gptel-conversations-directory))
- (let* ((completion-extra-properties '(:display-sort-function identity
- :cycle-sort-function identity))
- (selection (completing-read "Load conversation: " cands nil t))
- (filename (cdr (assoc selection cands)))
- (filepath (and filename
- (expand-file-name filename cj/gptel-conversations-directory))))
- (unless filename
- (user-error "No conversation selected"))
- (with-current-buffer (cj/gptel--ensure-ai-buffer)
- (erase-buffer)
- (insert-file-contents filepath)
- (cj/gptel--strip-visibility-headers)
- (goto-char (point-max))
- (set-buffer-modified-p t)
- (setq-local cj/gptel-autosave-filepath filepath)
- (setq-local cj/gptel-autosave-enabled t)
- (cj/gptel--autosave-start-timer))
- (let ((buf (get-buffer "*AI-Assistant*")))
- (unless (get-buffer-window buf)
- (cj/side-window-display
- buf cj/gptel-conversations-window-side
- 'cj/--ai-assistant-width cj/gptel-conversations-window-width)))
- (select-window (get-buffer-window "*AI-Assistant*"))
- (message "Loaded conversation from: %s" filepath))))
-
-(defun cj/gptel--autosave-after-response (&rest _args)
- "Auto-save the current GPTel buffer when enabled."
- (when (cj/gptel--autosave-active-p)
- (condition-case err
- (cj/gptel--save-buffer-to-file (current-buffer) cj/gptel-autosave-filepath)
- (error (message "cj/gptel autosave failed: %s" (error-message-string err))))))
-
-(with-eval-after-load 'gptel
- (unless (member #'cj/gptel--autosave-after-response gptel-post-response-functions)
- (add-hook 'gptel-post-response-functions #'cj/gptel--autosave-after-response)))
-
-(provide 'ai-conversations)
-;;; ai-conversations.el ends here