aboutsummaryrefslogtreecommitdiff
path: root/modules/ai-conversations-browser.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-browser.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-browser.el')
-rw-r--r--modules/ai-conversations-browser.el241
1 files changed, 0 insertions, 241 deletions
diff --git a/modules/ai-conversations-browser.el b/modules/ai-conversations-browser.el
deleted file mode 100644
index 9f2a7de43..000000000
--- a/modules/ai-conversations-browser.el
+++ /dev/null
@@ -1,241 +0,0 @@
-;;; ai-conversations-browser.el --- Browse saved GPTel conversations -*- lexical-binding: t; coding: utf-8; -*-
-
-;; Author: Craig Jennings <c@cjennings.net>
-
-;;; Commentary:
-;; Provides `cj/gptel-browse-conversations': a dired-style buffer
-;; listing saved conversations in `cj/gptel-conversations-directory'.
-;; Each row shows date, time, topic, and a short preview of the most
-;; recent message. Single-key bindings load / delete / rename a
-;; conversation in place.
-;;
-;; RET, l Load the conversation under point
-;; d Delete the conversation under point
-;; r Rename the conversation under point (renames the file)
-;; g Refresh the listing
-;; n / p Move to next / previous row
-;; q Quit the browser window
-
-;;; Code:
-
-(require 'cl-lib)
-(require 'subr-x)
-
-(declare-function cj/gptel-load-conversation "ai-conversations" ())
-(declare-function cj/gptel--slugify-topic "ai-conversations" (s))
-(declare-function cj/gptel--timestamp-from-filename "ai-conversations" (filename))
-
-(defcustom cj/gptel-browser-preview-length 60
- "Number of preview characters shown per row in the browser."
- :type 'integer
- :group 'cj/ai-conversations)
-
-(defconst cj/gptel-browser--buffer-name "*GPTel-Conversations*"
- "Buffer name for the saved-conversations browser.")
-
-(defvar-keymap cj/gptel-browser-mode-map
- :doc "Keymap for `cj/gptel-browser-mode'."
- "RET" #'cj/gptel-browser-load
- "l" #'cj/gptel-browser-load
- "d" #'cj/gptel-browser-delete
- "r" #'cj/gptel-browser-rename
- "g" #'cj/gptel-browser-refresh
- "n" #'next-line
- "p" #'previous-line
- "q" #'quit-window)
-
-(define-derived-mode cj/gptel-browser-mode special-mode "GPTel-Browser"
- "Major mode for browsing saved GPTel conversations."
- (setq-local truncate-lines t))
-
-;; -------------------------- helpers (pure where possible)
-
-(defun cj/gptel-browser--topic-from-filename (filename)
- "Return the topic slug from FILENAME, or nil if it isn't a gptel file."
- (when (string-match "\\`\\(.+\\)_[0-9]\\{8\\}-[0-9]\\{6\\}\\.gptel\\'" filename)
- (match-string 1 filename)))
-
-(defun cj/gptel-browser--strip-headers (text)
- "Drop the org #+STARTUP / #+VISIBILITY headers from TEXT and return the rest."
- (let ((s text))
- (while (string-match "\\`#\\+\\(STARTUP\\|VISIBILITY\\):.*\n" s)
- (setq s (substring s (match-end 0))))
- (while (and (> (length s) 0) (eq (aref s 0) ?\n))
- (setq s (substring s 1)))
- s))
-
-(defun cj/gptel-browser--last-message (text)
- "Return a short preview of the last user/AI message in TEXT.
-Returns the empty string when no message body is present."
- (let* ((stripped (cj/gptel-browser--strip-headers text))
- ;; Last org-mode top-level heading body, or the whole text if
- ;; there isn't one.
- (body (if (string-match "\\`\\*+[^\n]*\n\\(\\(?:.\\|\n\\)*\\)\\'" stripped)
- (let* ((all-text stripped)
- ;; Walk backward to find the last '* ' or '** ' heading
- (idx (or (cl-loop for i from (1- (length all-text)) downto 0
- when (and (or (zerop i)
- (eq (aref all-text (1- i)) ?\n))
- (eq (aref all-text i) ?*))
- return i)
- 0)))
- (substring all-text idx))
- stripped)))
- ;; Drop the heading line itself, then collapse whitespace.
- (when (string-match "\\`\\*+[^\n]*\n" body)
- (setq body (substring body (match-end 0))))
- (setq body (replace-regexp-in-string "[\n\t ]+" " " body))
- (string-trim body)))
-
-(defun cj/gptel-browser--preview (text length)
- "Return a LENGTH-char preview from TEXT, ellipsized when truncated."
- (let* ((line (cj/gptel-browser--last-message text))
- (max-len (max 1 length)))
- (cond
- ((string-empty-p line) "")
- ((> (length line) max-len)
- (concat (substring line 0 (1- max-len)) "…"))
- (t line))))
-
-(defun cj/gptel-browser--row-for-file (file dir)
- "Return a propertized row string for FILE under DIR, or nil."
- (let* ((filename (file-name-nondirectory file))
- (topic (cj/gptel-browser--topic-from-filename filename))
- (ts (and topic (cj/gptel--timestamp-from-filename filename))))
- (when (and topic ts)
- (let* ((preview (with-temp-buffer
- (ignore-errors (insert-file-contents file))
- (cj/gptel-browser--preview
- (buffer-string) cj/gptel-browser-preview-length)))
- (row (format "%s %-22s %s"
- (format-time-string "%Y-%m-%d %H:%M" ts)
- topic preview)))
- (propertize row
- 'cj/gptel-browser-file filename
- 'cj/gptel-browser-topic topic)))))
-
-(defun cj/gptel-browser--rows ()
- "Return propertized row strings for every conversation in the directory."
- (when (and (boundp 'cj/gptel-conversations-directory)
- (file-directory-p cj/gptel-conversations-directory))
- (let ((dir cj/gptel-conversations-directory))
- (delq nil
- (mapcar (lambda (f) (cj/gptel-browser--row-for-file f dir))
- (directory-files dir t "\\.gptel\\'"))))))
-
-(defun cj/gptel-browser--render ()
- "Replace the current buffer's contents with the conversation listing.
-Sort newest first."
- (let ((inhibit-read-only t)
- (rows (sort (cj/gptel-browser--rows)
- (lambda (a b)
- (string> (substring-no-properties a 0 16)
- (substring-no-properties b 0 16))))))
- (erase-buffer)
- (insert (propertize
- "Saved GPTel conversations -- RET/l load d delete r rename g refresh q quit\n\n"
- 'face 'header-line))
- (cond
- ((null rows)
- (insert " (no saved conversations)\n"))
- (t
- (dolist (row rows)
- (insert row "\n"))))
- (goto-char (point-min))
- (forward-line 2)))
-
-;; -------------------------- entry point
-
-;;;###autoload
-(defun cj/gptel-browse-conversations ()
- "Open the saved GPTel conversations browser."
- (interactive)
- (let ((buf (get-buffer-create cj/gptel-browser--buffer-name)))
- (with-current-buffer buf
- (cj/gptel-browser-mode)
- (cj/gptel-browser--render))
- (pop-to-buffer buf)))
-
-(defun cj/gptel-browser-refresh ()
- "Re-read the conversations directory and refresh the browser."
- (interactive)
- (cj/gptel-browser--render))
-
-;; -------------------------- row-level actions
-
-(defun cj/gptel-browser--filename-at-point ()
- "Return the conversation filename on the current line, or nil."
- (get-text-property (line-beginning-position) 'cj/gptel-browser-file))
-
-(defun cj/gptel-browser--filepath-at-point ()
- "Return the absolute filepath for the row at point, or nil."
- (when-let ((filename (cj/gptel-browser--filename-at-point)))
- (expand-file-name filename cj/gptel-conversations-directory)))
-
-(defun cj/gptel-browser-load ()
- "Load the conversation on the current row via `cj/gptel-load-conversation'.
-The browser is buried after the load fires."
- (interactive)
- (let ((filepath (cj/gptel-browser--filepath-at-point)))
- (unless filepath
- (user-error "No conversation on this line"))
- (let ((filename (file-name-nondirectory filepath)))
- ;; Stand in for cj/gptel-load-conversation's completing-read so
- ;; the user doesn't get prompted twice.
- (cl-letf (((symbol-function 'completing-read)
- (lambda (_p cands &rest _)
- (or (car (cl-find filename cands
- :key (lambda (c) (cdr c))
- :test #'equal))
- (caar cands))))
- ((symbol-function 'y-or-n-p) (lambda (&rest _) nil)))
- (cj/gptel-load-conversation)))
- (quit-window)))
-
-(defun cj/gptel-browser-delete ()
- "Delete the conversation file on the current row, after confirmation."
- (interactive)
- (let ((filepath (cj/gptel-browser--filepath-at-point)))
- (unless filepath
- (user-error "No conversation on this line"))
- (let ((filename (file-name-nondirectory filepath)))
- (when (y-or-n-p (format "Delete %s? " filename))
- (delete-file filepath)
- (message "Deleted %s" filename)
- (cj/gptel-browser--render)))))
-
-(defun cj/gptel-browser--rename-target (filepath new-topic)
- "Compute the renamed FILEPATH for NEW-TOPIC, preserving the timestamp.
-NEW-TOPIC is slugified. Returns the new absolute filepath."
- (let* ((dir (file-name-directory filepath))
- (filename (file-name-nondirectory filepath))
- (timestamp (and (string-match "_\\([0-9]\\{8\\}-[0-9]\\{6\\}\\)\\.gptel\\'"
- filename)
- (match-string 1 filename)))
- (slug (cj/gptel--slugify-topic new-topic)))
- (unless timestamp
- (error "Cannot extract timestamp from filename: %s" filename))
- (expand-file-name (format "%s_%s.gptel" slug timestamp) dir)))
-
-(defun cj/gptel-browser-rename ()
- "Rename the conversation file on the current row, preserving its timestamp."
- (interactive)
- (let ((filepath (cj/gptel-browser--filepath-at-point)))
- (unless filepath
- (user-error "No conversation on this line"))
- (let* ((old (file-name-nondirectory filepath))
- (current-topic (cj/gptel-browser--topic-from-filename old))
- (new-topic (read-string
- (format "New topic (was %s): " current-topic)
- current-topic))
- (target (cj/gptel-browser--rename-target filepath new-topic)))
- (when (equal target filepath)
- (user-error "Topic unchanged"))
- (when (file-exists-p target)
- (user-error "Target already exists: %s" (file-name-nondirectory target)))
- (rename-file filepath target)
- (message "Renamed to %s" (file-name-nondirectory target))
- (cj/gptel-browser--render))))
-
-(provide 'ai-conversations-browser)
-;;; ai-conversations-browser.el ends here