aboutsummaryrefslogtreecommitdiff
path: root/modules/ai-quick-ask.el
blob: 16f3afae4a3910e52216d466434d15081cb367b7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
;;; ai-quick-ask.el --- One-shot GPTel quick-ask -*- lexical-binding: t; coding: utf-8; -*-

;; Author: Craig Jennings <c@cjennings.net>

;;; Commentary:
;; Provides `cj/gptel-quick-ask': read a single prompt in the
;; minibuffer, stream the response into a transient *GPTel-Quick*
;; buffer.  The transient buffer is dismissible with q or escape and
;; can be escalated with c into a full *AI-Assistant* conversation
;; seeded with the prompt + response.
;;
;; Designed for impromptu help where the conversation thread doesn't
;; matter.  Doesn't touch the *AI-Assistant* side window unless the
;; user explicitly escalates, doesn't autosave anywhere.

;;; Code:

(require 'cj-window-toggle-lib)  ;; cj/side-window-display

;; Shared *AI-Assistant* panel-width state, owned by ai-config.el.  Forward-
;; declared here so the escalation reopens the panel at the same remembered
;; width as the F-key toggle without a circular require.
(defvar cj/ai-assistant-window-width)
(defvar cj/--ai-assistant-width)

(defvar-local cj/gptel-quick--prompt nil
  "Buffer-local: the prompt used for the current *GPTel-Quick* session.")

(defconst cj/gptel-quick--buffer-name "*GPTel-Quick*"
  "Buffer used for one-shot quick-ask Q&A.")

(defconst cj/gptel-quick--response-marker "A: "
  "String inserted before the response in the quick-ask buffer.")

(defvar-keymap cj/gptel-quick-mode-map
  :doc "Keymap for `cj/gptel-quick-mode'."
  "q"        #'cj/gptel-quick-dismiss
  "<escape>" #'cj/gptel-quick-dismiss
  "c"        #'cj/gptel-quick-continue)

(define-derived-mode cj/gptel-quick-mode special-mode "GPTel-Quick"
  "Major mode for the one-shot *GPTel-Quick* buffer."
  ;; Allow gptel-request to stream into the buffer despite the
  ;; special-mode read-only default.
  (setq-local buffer-read-only nil))

(defun cj/gptel-quick--initial-text (prompt)
  "Return the initial buffer body for a quick-ask of PROMPT.
The result is \"Q: <prompt>\\n\\nA: \", with the response marker at
the end so the streamed response lands right after it."
  (format "Q: %s\n\n%s" prompt cj/gptel-quick--response-marker))

(defun cj/gptel-quick--extract-response (text)
  "Return the response portion of TEXT, or nil if not found.
TEXT is the contents of a *GPTel-Quick* buffer.  The response is
everything after the first occurrence of `cj/gptel-quick--response-marker'
on its own line.  Returns nil when the marker is absent."
  (when (string-match
         (concat "^" (regexp-quote cj/gptel-quick--response-marker))
         text)
    (substring text (match-end 0))))

(defun cj/gptel-quick--seed-text (prompt response)
  "Format a *AI-Assistant* seed from PROMPT and RESPONSE.
Matches the org-heading shape that `cj/gptel--fresh-org-prefix' and
`cj/gptel-insert-model-heading' produce: a user heading followed by
the prompt body, followed by an AI heading followed by the response."
  (let ((ts (format-time-string "[%Y-%m-%d %H:%M:%S]")))
    (format "* %s %s\n%s\n\n* AI %s\n%s\n"
            user-login-name ts prompt
            ts (or response ""))))

;;;###autoload
(defun cj/gptel-quick-ask (prompt)
  "Read a one-shot PROMPT in the minibuffer and stream the answer.
The response lands in a transient *GPTel-Quick* buffer.  Press q or
escape to dismiss, or c to escalate into a full *AI-Assistant*
conversation seeded with the prompt and response."
  (interactive (list (read-string "Quick ask: ")))
  (when (string-empty-p prompt)
    (user-error "Empty prompt"))
  (let ((buf (get-buffer-create cj/gptel-quick--buffer-name)))
    (with-current-buffer buf
      (cj/gptel-quick-mode)
      (let ((inhibit-read-only t))
        (erase-buffer)
        (insert (cj/gptel-quick--initial-text prompt))
        (setq-local cj/gptel-quick--prompt prompt)))
    (unless (featurep 'gptel)
      (require 'gptel))
    (when (fboundp 'cj/ensure-gptel-backends)
      (cj/ensure-gptel-backends))
    (gptel-request prompt
      :buffer buf
      :position (with-current-buffer buf (point-max))
      :stream t)
    (display-buffer buf
                    '((display-buffer-reuse-window
                       display-buffer-pop-up-window)
                      (window-height . 0.3)))
    buf))

(defun cj/gptel-quick-dismiss ()
  "Kill the *GPTel-Quick* buffer if it exists."
  (interactive)
  (when-let ((buf (get-buffer cj/gptel-quick--buffer-name)))
    (when-let ((win (get-buffer-window buf)))
      (delete-window win))
    (kill-buffer buf)))

(defun cj/gptel-quick-continue ()
  "Escalate the current quick-ask into a full *AI-Assistant* conversation.
Reads the prompt and response from the *GPTel-Quick* buffer, seeds
them into *AI-Assistant* under proper org headings, displays the
side window, then dismisses the quick buffer."
  (interactive)
  (unless (eq major-mode 'cj/gptel-quick-mode)
    (user-error "Not in a *GPTel-Quick* buffer"))
  (let* ((prompt cj/gptel-quick--prompt)
         (response (cj/gptel-quick--extract-response (buffer-string)))
         (seed (cj/gptel-quick--seed-text prompt response)))
    (unless prompt
      (user-error "No prompt recorded in this buffer"))
    ;; Ensure *AI-Assistant* exists in gptel-mode.
    (unless (featurep 'gptel)
      (require 'gptel))
    (let ((ai-buf (get-buffer "*AI-Assistant*")))
      (unless ai-buf
        (when (fboundp 'cj/ensure-gptel-backends)
          (cj/ensure-gptel-backends))
        (gptel "*AI-Assistant*")
        (setq ai-buf (get-buffer "*AI-Assistant*")))
      (with-current-buffer ai-buf
        (goto-char (point-max))
        (insert seed))
      (cj/side-window-display
       ai-buf 'right 'cj/--ai-assistant-width cj/ai-assistant-window-width)
      (cj/gptel-quick-dismiss))))

(provide 'ai-quick-ask)
;;; ai-quick-ask.el ends here