Skip to content

11. The Prompt Buffer

In this lesson, we will implement the chatgpt command to display and select the prompt buffer at the bottom of the frame. We will also define chatgpt-mode and apply it within the prompt buffer.

Displaying the Prompt Buffer with chatgpt

Previously, we utilized the buffer *chatgpt* to enter prompts for OpenAI. We will continue using this buffer but will replace the switch-to-buffer command with our new chatgpt command, which displays the buffer at the bottom of the frame.

(defun chatgpt ()
  "Display and select the prompt buffer."
  (interactive)
  (let ((buff (get-buffer-create "*chatgpt*")))
    (select-window
     (display-buffer-at-bottom
      buff '(display-buffer-below-selected (window-height . 6))))))

The display-buffer-at-bottom function opens the specified buffer at the bottom of the frame and returns the associated window. We then use select-window to select that window.

Defining chatgpt-mode

We derive chatgpt-mode from markdown-mode for the prompt buffer:

(define-derived-mode chatgpt-mode markdown-mode "ChatGPT"
  "ChatGPT mode.")

To activate chatgpt-mode in the prompt buffer, we invoke it within the chatgpt command after selecting the window:

(defun chatgpt ()
  "Display and select the prompt buffer."
  (interactive)
  (let ((buff (get-buffer-create "*chatgpt*")))
    (select-window
     (display-buffer-at-bottom
      buff '(display-buffer-below-selected (window-height . 6))))
    (chatgpt-mode)))

Upon calling chatgpt, the *chatgpt* buffer will appear at the bottom of the frame. By evaluating major-mode in the minibuffer (using the eval-expression command) we can verify that the active major mode is chatgpt-mode.

Introducing chatgpt-model Variable

Next, we introduce the chatgpt-model variable to use in the chatgpt-request function, replacing the previous hardcoded gpt-4o model string:

(defvar chatgpt-model "gpt-4o"
  "The OpenAI model to use.")

(defun chatgpt-request (prompt)
  "Return an OpenAI request with PROMPT."
  `(:model ,chatgpt-model
    :messages ,(vector `(:role "user" :content ,prompt)))

For example:

(let ((chatgpt-model "gpt-4o"))
  (chatgpt-request "foo"))
;; (:model "gpt-4o" :messages [(:role "user" :content "foo")])
(let ((chatgpt-model "gpt-4o-mini"))
  (chatgpt-request "foo"))
;; (:model "gpt-4o-mini" :messages [(:role "user" :content "foo")])

Mode Line of the Prompt Buffer

We enhance the mode line in the prompt buffer to display the currently selected chatgpt-model. To do so, we modify chatgpt-mode to set mode-line-format accordingly:

(define-derived-mode chatgpt-mode markdown-mode "ChatGPT"
  "Mode for interacting with ChatGPT."
  (setq mode-line-format
        '(" "
          mode-line-buffer-identification
          " "
          chatgpt-model
          " "
          mode-line-misc-info)))

Once chatgpt-mode is activated in the prompt buffer, the mode line will display:

*chatgpt*    gpt-4o

Note that the mode-line-misc-info variable allows other commands or minor modes to append additional information to the mode line via global-mode-string.

Executing chatgpt-mode Once

Let's enhance the existing chatgpt command to ensure that chatgpt-mode is invoked only when the *chatgpt* buffer is created for the first time. Currently, chatgpt-mode is executed every time we call the chatgpt function, which is unnecessary.

We modify the function to check for the buffer's existence using get-buffer. If the *chatgpt* buffer does not exist at the time chatgpt is called, we enable chatgpt-mode.

Here's the revised code:

(defun chatgpt ()
  "Display and Select the prompt buffer."
  (interactive)
  (let* ((buff-name "*chatgpt*")
         (buff-p (get-buffer buff-name))
         (buff (get-buffer-create buff-name)))
    (select-window
     (display-buffer-at-bottom
      buff '(display-buffer-below-selected (window-height . 6))))
    (when (not buff-p) (chatgpt-mode))))

This modification ensures that chatgpt-mode is only activated when creating the buffer for the first time.

Creating chatgpt-dir in chatgpt-mode

We can further improve our implementation by creating the chatgpt-dir in the chatgpt-mode definition instead of in the chatgpt-send function. This ensures the directory is created only once if it doesn't exist yet. So we remove the call to make-directory in chatgpt-send and add it to chatgpt-mode:

(define-derived-mode chatgpt-mode markdown-mode "ChatGPT"
  "ChatGPT mode."
  (setq mode-line-format
        '(" "
          mode-line-buffer-identification
          " "
          chatgpt-model
          " "
          mode-line-misc-info))
  (make-directory chatgpt-dir t))

Defining chatgpt-mode-map keymap

Lastly, we bind the C-c C-c key combination to the chatgpt-send command within the chatgpt-mode-map keymap:

(defvar chatgpt-mode-map
  (let ((map (make-sparse-keymap)))
    (define-key map (kbd "C-c C-c") 'chatgpt-send)
    map)
  "Keymap for `chatgpt-mode'.")

To apply these changes in our current Emacs session, we call the chatgpt command to access the prompt buffer and activate chatgpt-mode.

chatgpt.el

The current implementation of the chatgpt.el package is as follows: \begingroup \hyphenpenalty=10000 \exhyphenpenalty=10000

;;; chatgpt.el --- Simple ChatGPT integration -*- lexical-binding: t; -*-

(require 'json)
(require 'markdown-mode)

(defvar chatgpt-api-key
  "sk-proj-7pQDxN...w-D40A"
  "OpenAI API key.")

(defvar chatgpt-dir "/home/tony/chatgpt-emacs/requests/"
  "Request directory.")

(defun chatgpt-json-read ()
  (let ((json-key-type 'keyword)
        (json-object-type 'plist)
        (json-array-type 'vector))
    (json-read)))

(defun chatgpt-json-encode (object)
  (let ((json-encoding-pretty-print t))
    (json-encode object)))

(defun chatgpt-command (req-path)
  "Return the curl command."
  (format
   (concat "curl https://api.openai.com/v1/chat/completions "
           "-H 'Content-Type: application/json' "
           "-H 'Authorization: Bearer %s' "
           "-d @%s")
   chatgpt-api-key req-path))

(defvar chatgpt-model "gpt-4o"
  "OpenAI model.")

(defun chatgpt-request (prompt)
  "Return an OpenAI request with PROMPT."
  `(:model ,chatgpt-model
    :messages ,(vector `(:role "user" :content ,prompt))))

(defun chatgpt-callback (prompt response req-dir)
  "Append PROMPT and RESPONSE to the prompt buffer with a link to REQ-DIR."
  (let ((buff (get-buffer-create "*chatgpt[requests]*")))
    (with-current-buffer buff
      (markdown-mode)
      (goto-char (point-max))
      (insert "# Request\n\n"
              "<!-- [](" req-dir ") -->\n\n"
              "## Prompt\n\n" prompt "\n\n"
              "## Response\n\n" response "\n\n"))))

(defun chatgpt-send ()
  "Send a request to OpenAI."
  (interactive)
  (make-directory chatgpt-dir t)
  (let* ((prompt (buffer-string))
         (req (chatgpt-request prompt))
         (temporary-file-directory chatgpt-dir)
         (req-dir (file-name-as-directory (make-temp-file nil t)))
         (req-path (concat req-dir "request.json"))
         (command (chatgpt-command req-path)))
    (message "chatgpt: %s" req-dir)
    (write-region (chatgpt-json-encode req) nil req-path)
    (make-process
     :name "chatgpt"
     :buffer (generate-new-buffer-name "chatgpt")
     :command (list "sh" "-c" command)
     :sentinel
     (lambda (process event)
       (if (not (string= event "finished\n"))
           (message "Error")
         (let* ((resp (with-current-buffer (process-buffer process)
                        (goto-char (point-min))
                        (chatgpt-json-read)))
                (response (map-nested-elt resp [:choices 0 :message :content]))
                (resp-path (concat req-dir "response.json")))
           (write-region (chatgpt-json-encode resp) nil resp-path)
           (chatgpt-callback prompt response))
         (kill-buffer (process-buffer process)))))))

(defvar chatgpt-mode-map
  (let ((map (make-sparse-keymap)))
    (define-key map (kbd "C-c C-c") 'chatgpt-send)
    map)
  "Keymap of `chatgpt-mode'.")

(define-derived-mode chatgpt-mode markdown-mode "ChatGPT"
  "ChatGPT mode."
  (setq mode-line-format
        '(" "
          mode-line-buffer-identification
          " "
          chatgpt-model
          " "
          mode-line-misc-info))
  (make-directory chatgpt-dir t))

(defun chatgpt ()
  "Display and Select the prompt buffer."
  (interactive)
  (let* ((buff-name "*chatgpt*")
         (buff-p (get-buffer buff-name))
         (buff (get-buffer-create buff-name)))
    (select-window
     (display-buffer-at-bottom
      buff '(display-buffer-below-selected (window-height . 6))))
    (when (not buff-p) (chatgpt-mode))))

(provide 'chatgpt)