diff options
| author | Craig Jennings <c@cjennings.net> | 2026-06-23 20:12:58 -0400 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-06-23 20:12:58 -0400 |
| commit | e41c25068d0cec9434895a6d3e3a25d3a26f645f (patch) | |
| tree | 5e30938a3fd6d80f501ffe3e6c1c187c5ddeb2c9 /modules/ai-conversations.el | |
| parent | a936e081b7270fbd4f1e7e9cb67ca1d4c2291ce6 (diff) | |
| download | dotemacs-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.el | 369 |
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 |
