ChatGPT Shell: Confirm Before Closing and Split Compose Buffer

I admit: I’ve been relying heavily on ChatGPT to get to grips with some PHP things. Asking for interpretation, alternatives, and PHP 8-specific stuff was a lot of help.

I’ve been using this in a separate floating window (aka ‘frame’) in Emacs next to my editing context, and that was great.

Until I accidentally closed the buffer and lost the history.

Twice.

Álvaro Ramírez, author of chatgpt-shell, kindly suggested I could ask for confirmation before closing ChatGPT buffers. Haven’t thought of that, obviously, so I was very grateful.

Update 2024-01-16: All of this is now part of shell-maker.el which is used by chatgpt-shell (and other interactive shells, like kagi), so if you install the bleeding edge version from MELPA or source now, you get a confirmation dialog out of the box. I’m leaving this post for history.

A web search later, I found out that kill-buffer-query-functions exists and is triggered as a sort of filter that intercepts buffer closing actions. The documentation says:

List of functions called with no args to query before killing a buffer.
The buffer being killed will be current while the functions are running.
See ‘kill-buffer’.

If any of them returns nil, the buffer is not killed. Functions run by
this hook are supposed to not change the current buffer.

Confusingly, though, you can use yes-or-no-p to ask the user for confirmation, and confirming returns t for “yes” and nil for “no”, but the returned valued just works in a kill-buffer-query-functions function …?! Also, returning t actually skips the function and is the neutral element here.

That doesn’t sound right but StackOverflow agrees, so I’m keeping it.

I don’t know.

Screenshot of Emacs with a bottom split showing the compose window

Anyway, here’s the config:

(use-package chatgpt-shell
  :commands (chatgpt-shell
             chatgpt-shell-prompt-compose)
  :bind (("C-c C-e" . chatgpt-shell-prompt-compose))
  :config

  (defun ct/confirm-before-killing-chatgpt ()
    (let ((buf (current-buffer)))
      (if (and (buffer-match-p "^\\*chatgpt\\*" buf)
               (not (buffer-match-p " compose$" buf)))
          (yes-or-no-p "ChatGPT Buffer! Kill anyway? ")
        t)))
  (add-to-list 'kill-buffer-query-functions #'ct/confirm-before-killing-chatgpt)

  (add-to-list 'display-buffer-alist
               '("\\*chatgpt\\*.*compose"
                 (display-buffer-reuse-window
                  display-buffer-in-side-window)
                 (reusable-frames  . visible)
                 (side             . bottom)
                 (window-height    . 0.3))))

;; Later:
(with-eval-after-load 'chatgpt-shell
  (with-eval-after-load 'xah-fly-keys
    (define-key xah-fly-leader-key-map (kbd "i k") #'chatgpt-shell-prompt-compose)))

I bound the composition window to SPC i k, the space key being the leader key in command-mode. That was free and is pretty convenient to pop up a composition buffer. I like the dedicated side window split for this.

Since I’m using the composition function, the buffer closing interception checks for “starts with *chatgpt*” and “does not end with ` compose`” so that the pop-up composition buffers can come and go, only a real history buffer is protected.

Maybe I should add autosaving of the transcript next, deleting transcripts that are 5 days old or older automatically.