rename: ollie-* symbols to ellie-* throughout ellie.el

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Levi Neely 2026-04-18 13:36:12 +02:00
parent 18e94c7de0
commit ed3df6c7b3
2 changed files with 195 additions and 195 deletions

View File

@ -45,13 +45,13 @@ Optionally set a mount path and default session options:
| Command | Description | | Command | Description |
|---------|-------------| |---------|-------------|
| `M-x ollie` | Open chat; attach to last session for `default-directory`, or create a new one | | `M-x ellie` | Open chat; attach to last session for `default-directory`, or create a new one |
| `M-x ollie-new-and-open` | Force-create a new session and open chat | | `M-x ellie-new-and-open` | Force-create a new session and open chat |
| `M-x ollie-attach-and-open` | Attach to an existing session by ID | | `M-x ellie-attach-and-open` | Attach to an existing session by ID |
## UI ## UI
In the `*ollie*` buffer: In the `*ellie*` buffer:
| Key | Action | | Key | Action |
|-----|--------| |-----|--------|
@ -65,7 +65,7 @@ In the `*ollie*` buffer:
| `C-c C-d` | Open the ollie mount in Dired | | `C-c C-d` | Open the ollie mount in Dired |
| `g` | Force-refresh the chat log | | `g` | Force-refresh the chat log |
In the `*ollie-input*` window: In the `*ellie-input*` window:
| Key | Action | | Key | Action |
|-----|--------| |-----|--------|

380
ellie.el
View File

@ -12,9 +12,9 @@
;; Quick start: ;; Quick start:
;; ;;
;; (require 'ellie) ; or use use-package / :load-path ;; (require 'ellie) ; or use use-package / :load-path
;; M-x ollie ; opens *ollie*, attaches to last session or creates one ;; M-x ellie ; opens *ellie*, attaches to last session or creates one
;; ;;
;; In the *ollie* buffer: ;; In the *ellie* buffer:
;; ;;
;; RET open an input window to compose and submit a prompt ;; RET open an input window to compose and submit a prompt
;; C-c C-q open an input window to compose a queued prompt ;; C-c C-q open an input window to compose a queued prompt
@ -25,7 +25,7 @@
;; C-c C-p show the rendered system prompt ;; C-c C-p show the rendered system prompt
;; g force-refresh the chat log ;; g force-refresh the chat log
;; ;;
;; In the *ollie-input* window: ;; In the *ellie-input* window:
;; ;;
;; C-c C-c submit ;; C-c C-c submit
;; C-c C-k cancel ;; C-c C-k cancel
@ -44,74 +44,74 @@
;;;; ──────────────── Customization ──────────────── ;;;; ──────────────── Customization ────────────────
(defgroup ollie nil (defgroup ellie nil
"Emacs client for ollie-9p." "Emacs client for ollie-9p."
:group 'tools :group 'tools
:prefix "ollie-") :prefix "ellie-")
(defcustom ollie-mount-directory (defcustom ellie-mount-directory
(or (getenv "OLLIE") (expand-file-name "~/mnt/ollie")) (or (getenv "OLLIE") (expand-file-name "~/mnt/ollie"))
"Mount point for the ollie-9p filesystem." "Mount point for the ollie-9p filesystem."
:type 'directory :type 'directory
:group 'ollie) :group 'ellie)
(defcustom ollie-default-session-opts nil (defcustom ellie-default-session-opts nil
"Default options passed when creating a new session. "Default options passed when creating a new session.
An alist of (KEY . VALUE) string pairs; valid keys are An alist of (KEY . VALUE) string pairs; valid keys are
\"backend\", \"model\", \"agent\", and \"cwd\"." \"backend\", \"model\", \"agent\", and \"cwd\"."
:type '(alist :key-type string :value-type string) :type '(alist :key-type string :value-type string)
:group 'ollie) :group 'ellie)
(defcustom ollie-poll-interval 0.5 (defcustom ellie-poll-interval 0.5
"Seconds between chat-file polls (used when file-notify is unavailable)." "Seconds between chat-file polls (used when file-notify is unavailable)."
:type 'number :type 'number
:group 'ollie) :group 'ellie)
(defcustom ollie-input-window-height 8 (defcustom ellie-input-window-height 8
"Height in lines of the prompt input window." "Height in lines of the prompt input window."
:type 'integer :type 'integer
:group 'ollie) :group 'ellie)
;;;; ──────────────── Internal state ──────────────── ;;;; ──────────────── Internal state ────────────────
(defvar ollie--session-id nil (defvar ellie--session-id nil
"ID of the currently active session (global).") "ID of the currently active session (global).")
(defvar ollie--chat-size -1 (defvar ellie--chat-size -1
"Last known byte count of the chat file; -1 means unread.") "Last known byte count of the chat file; -1 means unread.")
(defvar ollie--poll-timer nil (defvar ellie--poll-timer nil
"Timer for periodic refresh.") "Timer for periodic refresh.")
(defconst ollie--buf "*ollie*" "Name of the chat display buffer.") (defconst ellie--buf "*ellie*" "Name of the chat display buffer.")
(defconst ollie--ibuf "*ollie-input*" "Name of the prompt input buffer.") (defconst ellie--ibuf "*ellie-input*" "Name of the prompt input buffer.")
;;;; ──────────────── Paths ──────────────── ;;;; ──────────────── Paths ────────────────
(defun ollie--sessions-dir () (defun ellie--sessions-dir ()
(expand-file-name "s" ollie-mount-directory)) (expand-file-name "s" ellie-mount-directory))
(defun ollie--sessions-new-path () (defun ellie--sessions-new-path ()
(expand-file-name "new" (ollie--sessions-dir))) (expand-file-name "new" (ellie--sessions-dir)))
(defun ollie--session-file (name) (defun ellie--session-file (name)
"Return the full path to file NAME inside the current session directory. "Return the full path to file NAME inside the current session directory.
Signals `user-error' if no session is active." Signals `user-error' if no session is active."
(unless ollie--session-id (unless ellie--session-id
(user-error "No active ollie session")) (user-error "No active ollie session"))
(expand-file-name name (expand-file-name name
(expand-file-name ollie--session-id (ollie--sessions-dir)))) (expand-file-name ellie--session-id (ellie--sessions-dir))))
(defun ollie--last-session-path () (defun ellie--last-session-path ()
(expand-file-name "~/.config/ollie/last-session")) (expand-file-name "~/.config/ollie/last-session"))
;;;; ──────────────── Low-level file helpers ──────────────── ;;;; ──────────────── Low-level file helpers ────────────────
(defun ollie--fwrite (path text) (defun ellie--fwrite (path text)
"Write TEXT to PATH, suppressing the minibuffer message." "Write TEXT to PATH, suppressing the minibuffer message."
(write-region text nil path nil 'silent)) (write-region text nil path nil 'silent))
(defun ollie--fread (path) (defun ellie--fread (path)
"Return the contents of PATH as a string, or nil if it does not exist." "Return the contents of PATH as a string, or nil if it does not exist."
(when (file-exists-p path) (when (file-exists-p path)
(with-temp-buffer (with-temp-buffer
@ -121,9 +121,9 @@ Signals `user-error' if no session is active."
;;;; ──────────────── Session enumeration ──────────────── ;;;; ──────────────── Session enumeration ────────────────
(defun ollie--session-ids () (defun ellie--session-ids ()
"Return a list of all current session IDs." "Return a list of all current session IDs."
(let ((sdir (ollie--sessions-dir))) (let ((sdir (ellie--sessions-dir)))
(when (file-directory-p sdir) (when (file-directory-p sdir)
(seq-filter (seq-filter
(lambda (name) (not (string-prefix-p "." name))) (lambda (name) (not (string-prefix-p "." name)))
@ -133,11 +133,11 @@ Signals `user-error' if no session is active."
;;;; ──────────────── Last-session persistence ──────────────── ;;;; ──────────────── Last-session persistence ────────────────
(defun ollie--persist-session (id) (defun ellie--persist-session (id)
"Upsert the {cwd}TAB{id} entry for `default-directory' in the last-session file." "Upsert the {cwd}TAB{id} entry for `default-directory' in the last-session file."
(let* ((path (ollie--last-session-path)) (let* ((path (ellie--last-session-path))
(cwd (expand-file-name default-directory)) (cwd (expand-file-name default-directory))
(raw (ollie--fread path)) (raw (ellie--fread path))
(lines (when raw (lines (when raw
(seq-filter (lambda (l) (not (string-empty-p (string-trim l)))) (seq-filter (lambda (l) (not (string-empty-p (string-trim l))))
(split-string raw "\n")))) (split-string raw "\n"))))
@ -151,17 +151,17 @@ Signals `user-error' if no session is active."
l))) l)))
lines))) lines)))
(make-directory (file-name-directory path) t) (make-directory (file-name-directory path) t)
(ollie--fwrite path (ellie--fwrite path
(concat (mapconcat #'identity (concat (mapconcat #'identity
(if found new-lines (append new-lines (list entry))) (if found new-lines (append new-lines (list entry)))
"\n") "\n")
"\n")))) "\n"))))
(defun ollie--recall-session () (defun ellie--recall-session ()
"Return the last-used session ID for `default-directory' if it still exists, else nil." "Return the last-used session ID for `default-directory' if it still exists, else nil."
(let* ((path (ollie--last-session-path)) (let* ((path (ellie--last-session-path))
(cwd (expand-file-name default-directory)) (cwd (expand-file-name default-directory))
(raw (ollie--fread path))) (raw (ellie--fread path)))
(when raw (when raw
(catch 'found (catch 'found
(dolist (line (split-string raw "\n")) (dolist (line (split-string raw "\n"))
@ -170,66 +170,66 @@ Signals `user-error' if no session is active."
(string= (car parts) cwd)) (string= (car parts) cwd))
(let ((id (string-trim (cadr parts)))) (let ((id (string-trim (cadr parts))))
(when (and (not (string-empty-p id)) (when (and (not (string-empty-p id))
(file-directory-p (expand-file-name id (ollie--sessions-dir)))) (file-directory-p (expand-file-name id (ellie--sessions-dir))))
(throw 'found id)))))))))) (throw 'found id))))))))))
;;;; ──────────────── Session lifecycle ──────────────── ;;;; ──────────────── Session lifecycle ────────────────
(defun ollie-new-session (&optional opts) (defun ellie-new-session (&optional opts)
"Create a new ollie session and make it current. "Create a new ollie session and make it current.
OPTS is an alist of option pairs; defaults to `ollie-default-session-opts'. OPTS is an alist of option pairs; defaults to `ellie-default-session-opts'.
Returns the new session ID." Returns the new session ID."
(interactive) (interactive)
(unless (file-directory-p ollie-mount-directory) (unless (file-directory-p ellie-mount-directory)
(user-error "ollie-9p not mounted at %s (customize `ollie-mount-directory')" (user-error "ollie-9p not mounted at %s (customize `ellie-mount-directory')"
ollie-mount-directory)) ellie-mount-directory))
(let* ((effective-opts (or opts ollie-default-session-opts)) (let* ((effective-opts (or opts ellie-default-session-opts))
;; cwd is required; inject it if not already present. ;; cwd is required; inject it if not already present.
(effective-opts (if (assoc "cwd" effective-opts) (effective-opts (if (assoc "cwd" effective-opts)
effective-opts effective-opts
(cons (cons "cwd" default-directory) effective-opts))) (cons (cons "cwd" default-directory) effective-opts)))
(before (ollie--session-ids)) (before (ellie--session-ids))
(seen (make-hash-table :test 'equal)) (seen (make-hash-table :test 'equal))
(cmd (mapconcat (lambda (kv) (cmd (mapconcat (lambda (kv)
(format "%s=%s" (car kv) (cdr kv))) (format "%s=%s" (car kv) (cdr kv)))
effective-opts " "))) effective-opts " ")))
(dolist (id before) (puthash id t seen)) (dolist (id before) (puthash id t seen))
(ollie--fwrite (ollie--sessions-new-path) (concat cmd "\n")) (ellie--fwrite (ellie--sessions-new-path) (concat cmd "\n"))
(let ((deadline (+ (float-time) 5.0)) (let ((deadline (+ (float-time) 5.0))
result) result)
(while (and (not result) (< (float-time) deadline)) (while (and (not result) (< (float-time) deadline))
(dolist (id (ollie--session-ids)) (dolist (id (ellie--session-ids))
(unless (gethash id seen) (setq result id))) (unless (gethash id seen) (setq result id)))
(unless result (sleep-for 0.05))) (unless result (sleep-for 0.05)))
(unless result (unless result
(user-error "Timed out waiting for a new ollie session")) (user-error "Timed out waiting for a new ollie session"))
(setq ollie--session-id result) (setq ellie--session-id result)
(ollie--persist-session result) (ellie--persist-session result)
(message "Created ollie session %s" result) (message "Created ollie session %s" result)
result))) result)))
(defun ollie-attach (id) (defun ellie-attach (id)
"Attach to an existing session by ID and make it current." "Attach to an existing session by ID and make it current."
(interactive (interactive
(let ((ids (ollie--session-ids))) (let ((ids (ellie--session-ids)))
(unless ids (user-error "No ollie sessions available")) (unless ids (user-error "No ollie sessions available"))
(list (completing-read "Attach to session: " ids nil t)))) (list (completing-read "Attach to session: " ids nil t))))
(unless (file-directory-p (expand-file-name id (ollie--sessions-dir))) (unless (file-directory-p (expand-file-name id (ellie--sessions-dir)))
(user-error "Session not found: %s" id)) (user-error "Session not found: %s" id))
(setq ollie--session-id id) (setq ellie--session-id id)
(ollie--persist-session id) (ellie--persist-session id)
(message "Attached to ollie session %s" id)) (message "Attached to ollie session %s" id))
(defun ollie-kill-session () (defun ellie-kill-session ()
"Kill the current ollie session." "Kill the current ollie session."
(interactive) (interactive)
(unless ollie--session-id (user-error "No active session")) (unless ellie--session-id (user-error "No active session"))
(when (yes-or-no-p (format "Kill ollie session %s? " ollie--session-id)) (when (yes-or-no-p (format "Kill ollie session %s? " ellie--session-id))
(delete-directory (expand-file-name ollie--session-id (ollie--sessions-dir)) t) (delete-directory (expand-file-name ellie--session-id (ellie--sessions-dir)) t)
(ollie--stop-watching) (ellie--stop-watching)
(setq ollie--session-id nil (setq ellie--session-id nil
ollie--chat-size -1) ellie--chat-size -1)
(when-let (buf (get-buffer ollie--buf)) (when-let (buf (get-buffer ellie--buf))
(with-current-buffer buf (with-current-buffer buf
(let ((inhibit-read-only t)) (let ((inhibit-read-only t))
(erase-buffer) (erase-buffer)
@ -237,87 +237,87 @@ Returns the new session ID."
(insert "Press 'n' or C-c C-n to start a new session.\n")))) (insert "Press 'n' or C-c C-n to start a new session.\n"))))
(message "Session killed"))) (message "Session killed")))
(defun ollie-rename-session (new-name) (defun ellie-rename-session (new-name)
"Rename the current ollie session to NEW-NAME." "Rename the current ollie session to NEW-NAME."
(interactive "sNew session name: ") (interactive "sNew session name: ")
(unless ollie--session-id (user-error "No active session")) (unless ellie--session-id (user-error "No active session"))
(when (string-empty-p (string-trim new-name)) (when (string-empty-p (string-trim new-name))
(user-error "Empty name")) (user-error "Empty name"))
(let ((old-dir (expand-file-name ollie--session-id (ollie--sessions-dir))) (let ((old-dir (expand-file-name ellie--session-id (ellie--sessions-dir)))
(new-dir (expand-file-name new-name (ollie--sessions-dir)))) (new-dir (expand-file-name new-name (ellie--sessions-dir))))
(rename-file old-dir new-dir) (rename-file old-dir new-dir)
(setq ollie--session-id new-name) (setq ellie--session-id new-name)
(ollie--persist-session new-name) (ellie--persist-session new-name)
(force-mode-line-update) (force-mode-line-update)
(message "Renamed session to %s" new-name))) (message "Renamed session to %s" new-name)))
;;;; ──────────────── State ──────────────── ;;;; ──────────────── State ────────────────
(defun ollie--state () (defun ellie--state ()
"Return the current session state string, or \"unknown\" on any error." "Return the current session state string, or \"unknown\" on any error."
(condition-case nil (condition-case nil
(or (when-let (s (ollie--fread (ollie--session-file "state"))) (or (when-let (s (ellie--fread (ellie--session-file "state")))
(string-trim s)) (string-trim s))
"unknown") "unknown")
(error "unknown"))) (error "unknown")))
(defun ollie--idle-p () (defun ellie--idle-p ()
"Return non-nil if the session is currently idle." "Return non-nil if the session is currently idle."
(ignore-errors (string= (ollie--state) "idle"))) (ignore-errors (string= (ellie--state) "idle")))
;;;; ──────────────── Agent operations ──────────────── ;;;; ──────────────── Agent operations ────────────────
(defun ollie-submit (text) (defun ellie-submit (text)
"Submit TEXT as a prompt to the current session. "Submit TEXT as a prompt to the current session.
The server dispatches it asynchronously." The server dispatches it asynchronously."
(when (string-empty-p (string-trim text)) (when (string-empty-p (string-trim text))
(user-error "Empty prompt")) (user-error "Empty prompt"))
(ollie--fwrite (ollie--session-file "prompt") text)) (ellie--fwrite (ellie--session-file "prompt") text))
(defun ollie-queue (text) (defun ellie-queue (text)
"Enqueue TEXT as a prompt to run after the current turn finishes." "Enqueue TEXT as a prompt to run after the current turn finishes."
(when (string-empty-p (string-trim text)) (when (string-empty-p (string-trim text))
(user-error "Empty prompt")) (user-error "Empty prompt"))
(ollie--fwrite (ollie--session-file "enqueue") text)) (ellie--fwrite (ellie--session-file "enqueue") text))
(defun ollie-stop () (defun ellie-stop ()
"Send a stop signal to the current turn." "Send a stop signal to the current turn."
(interactive) (interactive)
(ollie--fwrite (ollie--session-file "ctl") "stop\n") (ellie--fwrite (ellie--session-file "ctl") "stop\n")
(message "ollie: stopped")) (message "ollie: stopped"))
;;;; ──────────────── Diff faces ──────────────── ;;;; ──────────────── Diff faces ────────────────
(defface ollie-diff-added (defface ellie-diff-added
'((((background dark)) :foreground "#5faf5f") '((((background dark)) :foreground "#5faf5f")
(((background light)) :foreground "#3a7a3a")) (((background light)) :foreground "#3a7a3a"))
"Face for added lines in tool-result diff output." "Face for added lines in tool-result diff output."
:group 'ollie) :group 'ellie)
(defface ollie-diff-removed (defface ellie-diff-removed
'((((background dark)) :foreground "#d75f5f") '((((background dark)) :foreground "#d75f5f")
(((background light)) :foreground "#a02020")) (((background light)) :foreground "#a02020"))
"Face for removed lines in tool-result diff output." "Face for removed lines in tool-result diff output."
:group 'ollie) :group 'ellie)
(defface ollie-diff-hunk-header (defface ellie-diff-hunk-header
'((((background dark)) :foreground "#5fafaf") '((((background dark)) :foreground "#5fafaf")
(((background light)) :foreground "#1a6a6a")) (((background light)) :foreground "#1a6a6a"))
"Face for hunk headers in tool-result diff output." "Face for hunk headers in tool-result diff output."
:group 'ollie) :group 'ellie)
;;;; ──────────────── Chat display ──────────────── ;;;; ──────────────── Chat display ────────────────
;; We cache the mtime so we only re-read the file when it actually changes. ;; We cache the mtime so we only re-read the file when it actually changes.
;; The mode-line state is updated on every refresh tick. ;; The mode-line state is updated on every refresh tick.
(defvar-local ollie--mode-line-state "unknown" (defvar-local ellie--mode-line-state "unknown"
"Cached session state string for the mode line.") "Cached session state string for the mode line.")
(defvar-local ollie--mode-line-usage "" (defvar-local ellie--mode-line-usage ""
"Cached usage string for the mode line.") "Cached usage string for the mode line.")
(defun ollie--block-start-p (line) (defun ellie--block-start-p (line)
"Return non-nil if LINE begins a named chat block." "Return non-nil if LINE begins a named chat block."
(or (string-prefix-p "-> " line) (or (string-prefix-p "-> " line)
(string-prefix-p "user: " line) (string-prefix-p "user: " line)
@ -326,7 +326,7 @@ The server dispatches it asynchronously."
(string-prefix-p "error: " line) (string-prefix-p "error: " line)
(string-prefix-p "agent stalled" line))) (string-prefix-p "agent stalled" line)))
(defun ollie--colorize-diffs () (defun ellie--colorize-diffs ()
"Add diff highlighting to tool-result blocks in the current buffer. "Add diff highlighting to tool-result blocks in the current buffer.
Must be called after `ansi-color-apply-on-region'." Must be called after `ansi-color-apply-on-region'."
(save-excursion (save-excursion
@ -341,195 +341,195 @@ Must be called after `ansi-color-apply-on-region'."
((string-prefix-p "-> " line) ((string-prefix-p "-> " line)
(setq in-result t)) (setq in-result t))
;; Block-start: exit result mode; don't colorize this line. ;; Block-start: exit result mode; don't colorize this line.
((and in-result (ollie--block-start-p line)) ((and in-result (ellie--block-start-p line))
(setq in-result nil)) (setq in-result nil))
;; Inside a result block: colorize diff lines. ;; Inside a result block: colorize diff lines.
(in-result (in-result
(let ((face (cond (let ((face (cond
((or (string-prefix-p "+++" line) ((or (string-prefix-p "+++" line)
(string-prefix-p "−--" line)) 'shadow) (string-prefix-p "−--" line)) 'shadow)
((string-prefix-p "+" line) 'ollie-diff-added) ((string-prefix-p "+" line) 'ellie-diff-added)
((string-prefix-p "−" line) 'ollie-diff-removed) ((string-prefix-p "−" line) 'ellie-diff-removed)
((string-prefix-p "@@" line) 'ollie-diff-hunk-header)))) ((string-prefix-p "@@" line) 'ellie-diff-hunk-header))))
(when face (when face
(put-text-property bol eol 'face face))))) (put-text-property bol eol 'face face)))))
(forward-line 1)))))) (forward-line 1))))))
(defun ollie--refresh () (defun ellie--refresh ()
"Refresh the chat buffer if the chat file has grown on disk. "Refresh the chat buffer if the chat file has grown on disk.
Also updates the cached mode-line state." Also updates the cached mode-line state."
(let* ((buf (get-buffer ollie--buf)) (let* ((buf (get-buffer ellie--buf))
(chat (condition-case nil (ollie--session-file "chat") (error nil))) (chat (condition-case nil (ellie--session-file "chat") (error nil)))
(attrs (and chat (file-attributes chat))) (attrs (and chat (file-attributes chat)))
(size (and attrs (file-attribute-size attrs)))) (size (and attrs (file-attribute-size attrs))))
(when buf (when buf
(with-current-buffer buf (with-current-buffer buf
(setq ollie--mode-line-state (ollie--state)) (setq ellie--mode-line-state (ellie--state))
(setq ollie--mode-line-usage (setq ellie--mode-line-usage
(or (condition-case nil (or (condition-case nil
(when-let (s (ollie--fread (ollie--session-file "ctxsz"))) (when-let (s (ellie--fread (ellie--session-file "ctxsz")))
(string-trim s)) (string-trim s))
(error nil)) (error nil))
"")) ""))
;; The chat log is append-only, so size increasing means new content. ;; The chat log is append-only, so size increasing means new content.
(when (and size (> size ollie--chat-size)) (when (and size (> size ellie--chat-size))
(setq ollie--chat-size size) (setq ellie--chat-size size)
(let ((inhibit-read-only t) (let ((inhibit-read-only t)
(at-end (= (point) (point-max)))) (at-end (= (point) (point-max))))
(erase-buffer) (erase-buffer)
(insert-file-contents chat) (insert-file-contents chat)
(ansi-color-apply-on-region (point-min) (point-max)) (ansi-color-apply-on-region (point-min) (point-max))
(ollie--colorize-diffs) (ellie--colorize-diffs)
(when at-end (goto-char (point-max))))) (when at-end (goto-char (point-max)))))
(force-mode-line-update))))) (force-mode-line-update)))))
;;;; ──────────────── Polling ──────────────── ;;;; ──────────────── Polling ────────────────
;; inotify does not fire on 9P mounts, so we always use a timer. ;; inotify does not fire on 9P mounts, so we always use a timer.
;; The mtime check in `ollie--refresh' keeps this cheap. ;; The mtime check in `ellie--refresh' keeps this cheap.
(defun ollie--start-watching () (defun ellie--start-watching ()
"Start a periodic timer to tail the chat file." "Start a periodic timer to tail the chat file."
(ollie--stop-watching) (ellie--stop-watching)
(setq ollie--poll-timer (setq ellie--poll-timer
(run-with-timer ollie-poll-interval ollie-poll-interval #'ollie--refresh))) (run-with-timer ellie-poll-interval ellie-poll-interval #'ellie--refresh)))
(defun ollie--stop-watching () (defun ellie--stop-watching ()
"Cancel the refresh timer." "Cancel the refresh timer."
(when ollie--poll-timer (when ellie--poll-timer
(cancel-timer ollie--poll-timer) (cancel-timer ellie--poll-timer)
(setq ollie--poll-timer nil))) (setq ellie--poll-timer nil)))
;;;; ──────────────── Input buffer ──────────────── ;;;; ──────────────── Input buffer ────────────────
(defvar-local ollie--input-queued nil (defvar-local ellie--input-queued nil
"Non-nil when this input buffer will enqueue rather than submit.") "Non-nil when this input buffer will enqueue rather than submit.")
(defvar ollie-input-mode-map (defvar ellie-input-mode-map
(let ((m (make-sparse-keymap))) (let ((m (make-sparse-keymap)))
(define-key m (kbd "C-c C-c") #'ollie-input-submit) (define-key m (kbd "C-c C-c") #'ellie-input-submit)
(define-key m (kbd "C-c C-k") #'ollie-input-cancel) (define-key m (kbd "C-c C-k") #'ellie-input-cancel)
m) m)
"Keymap for `ollie-input-mode'.") "Keymap for `ellie-input-mode'.")
(define-derived-mode ollie-input-mode text-mode "Ollie-Input" (define-derived-mode ellie-input-mode text-mode "Ollie-Input"
"Major mode for composing ollie prompts. "Major mode for composing ollie prompts.
\\{ollie-input-mode-map}" \\{ellie-input-mode-map}"
(setq-local header-line-format (setq-local header-line-format
'(:eval '(:eval
(format " %s · C-c C-c submit · C-c C-k cancel" (format " %s · C-c C-c submit · C-c C-k cancel"
(if ollie--input-queued (if ellie--input-queued
(propertize "Queue prompt" 'face 'warning) (propertize "Queue prompt" 'face 'warning)
(propertize "Send prompt" 'face 'success)))))) (propertize "Send prompt" 'face 'success))))))
(defun ollie--open-input (&optional queued) (defun ellie--open-input (&optional queued)
"Open the prompt input window below the current window. "Open the prompt input window below the current window.
QUEUED non-nil means the prompt will be enqueued, not submitted immediately." QUEUED non-nil means the prompt will be enqueued, not submitted immediately."
(let ((buf (get-buffer-create ollie--ibuf))) (let ((buf (get-buffer-create ellie--ibuf)))
(with-current-buffer buf (with-current-buffer buf
(erase-buffer) (erase-buffer)
(ollie-input-mode) (ellie-input-mode)
(setq ollie--input-queued queued)) (setq ellie--input-queued queued))
(display-buffer buf (display-buffer buf
`(display-buffer-below-selected `(display-buffer-below-selected
(window-height . ,ollie-input-window-height))) (window-height . ,ellie-input-window-height)))
(when-let (win (get-buffer-window buf t)) (when-let (win (get-buffer-window buf t))
(select-window win)))) (select-window win))))
(defun ollie-input-submit () (defun ellie-input-submit ()
"Submit the contents of the input buffer as a prompt." "Submit the contents of the input buffer as a prompt."
(interactive) (interactive)
(let ((text (buffer-string)) (let ((text (buffer-string))
(queued ollie--input-queued)) (queued ellie--input-queued))
(ollie--close-input-window) (ellie--close-input-window)
(if queued (if queued
(ollie-queue text) (ellie-queue text)
(ollie-submit text)))) (ellie-submit text))))
(defun ollie-input-cancel () (defun ellie-input-cancel ()
"Discard the input and close the input window." "Discard the input and close the input window."
(interactive) (interactive)
(ollie--close-input-window)) (ellie--close-input-window))
(defun ollie--close-input-window () (defun ellie--close-input-window ()
"Kill the input buffer and delete its window." "Kill the input buffer and delete its window."
(when-let (win (get-buffer-window ollie--ibuf t)) (when-let (win (get-buffer-window ellie--ibuf t))
(delete-window win)) (delete-window win))
(when-let (buf (get-buffer ollie--ibuf)) (when-let (buf (get-buffer ellie--ibuf))
(kill-buffer buf))) (kill-buffer buf)))
;;;; ──────────────── Chat major mode ──────────────── ;;;; ──────────────── Chat major mode ────────────────
(defvar ollie-mode-map (defvar ellie-mode-map
(let ((m (make-sparse-keymap))) (let ((m (make-sparse-keymap)))
(define-key m (kbd "RET") #'ollie-send-input) (define-key m (kbd "RET") #'ellie-send-input)
(define-key m (kbd "C-c C-q") #'ollie-queue-input) (define-key m (kbd "C-c C-q") #'ellie-queue-input)
(define-key m (kbd "C-c C-c") #'ollie-stop) (define-key m (kbd "C-c C-c") #'ellie-stop)
(define-key m (kbd "C-c C-k") #'ollie-kill-session) (define-key m (kbd "C-c C-k") #'ellie-kill-session)
(define-key m (kbd "C-c C-r") #'ollie-rename-session) (define-key m (kbd "C-c C-r") #'ellie-rename-session)
(define-key m (kbd "C-c C-n") #'ollie-new-and-open) (define-key m (kbd "C-c C-n") #'ellie-new-and-open)
(define-key m (kbd "C-c C-a") #'ollie-attach-and-open) (define-key m (kbd "C-c C-a") #'ellie-attach-and-open)
(define-key m (kbd "C-c C-d") #'ollie-dired) (define-key m (kbd "C-c C-d") #'ellie-dired)
(define-key m (kbd "C-c C-p") #'ollie-show-system-prompt) (define-key m (kbd "C-c C-p") #'ellie-show-system-prompt)
(define-key m (kbd "n") #'ollie-new-and-open) (define-key m (kbd "n") #'ellie-new-and-open)
(define-key m (kbd "g") #'ollie-refresh) (define-key m (kbd "g") #'ellie-refresh)
m) m)
"Keymap for `ollie-mode'.") "Keymap for `ellie-mode'.")
(defun ollie-send-input () (defun ellie-send-input ()
"Open the input window to compose and submit a prompt." "Open the input window to compose and submit a prompt."
(interactive) (interactive)
(ollie--open-input nil)) (ellie--open-input nil))
(defun ollie-queue-input () (defun ellie-queue-input ()
"Open the input window to compose a queued prompt." "Open the input window to compose a queued prompt."
(interactive) (interactive)
(ollie--open-input t)) (ellie--open-input t))
(defun ollie-refresh () (defun ellie-refresh ()
"Force a refresh of the chat log from disk." "Force a refresh of the chat log from disk."
(interactive) (interactive)
(setq ollie--chat-size -1) (setq ellie--chat-size -1)
(ollie--refresh)) (ellie--refresh))
(defun ollie-dired () (defun ellie-dired ()
"Open the ollie-9p mount point in Dired." "Open the ollie-9p mount point in Dired."
(interactive) (interactive)
(dired ollie-mount-directory)) (dired ellie-mount-directory))
(define-derived-mode ollie-mode special-mode "Ollie" (define-derived-mode ellie-mode special-mode "Ollie"
"Major mode for the ollie chat interface. "Major mode for the ollie chat interface.
Key bindings: Key bindings:
\\{ollie-mode-map}" \\{ellie-mode-map}"
:group 'ollie :group 'ellie
(buffer-disable-undo) (buffer-disable-undo)
(setq-local mode-line-format (setq-local mode-line-format
`("%e" `("%e"
mode-line-front-space mode-line-front-space
(:eval (:eval
(propertize (format " ollie:%s " (or ollie--session-id "-")) (propertize (format " ellie:%s" (or ellie--session-id "-"))
'face 'mode-line-buffer-id)) 'face 'mode-line-buffer-id))
(:eval (:eval
(if (string= ollie--mode-line-state "idle") (if (string= ellie--mode-line-state "idle")
(propertize "idle" 'face 'success) (propertize "idle" 'face 'success)
(propertize ollie--mode-line-state 'face 'warning))) (propertize ellie--mode-line-state 'face 'warning)))
(:eval (:eval
(when (not (string-empty-p ollie--mode-line-usage)) (when (not (string-empty-p ellie--mode-line-usage))
(concat " " (replace-regexp-in-string "%" "%%" ollie--mode-line-usage)))) (concat " " (replace-regexp-in-string "%" "%%" ellie--mode-line-usage))))
" " " "
mode-line-end-spaces))) mode-line-end-spaces)))
;;;; ──────────────── System prompt ──────────────── ;;;; ──────────────── System prompt ────────────────
(defun ollie-show-system-prompt () (defun ellie-show-system-prompt ()
"Display the rendered system prompt for the current session in a buffer." "Display the rendered system prompt for the current session in a buffer."
(interactive) (interactive)
(let* ((path (ollie--session-file "systemprompt")) (let* ((path (ellie--session-file "systemprompt"))
(text (ollie--fread path))) (text (ellie--fread path)))
(unless text (unless text
(user-error "Could not read system prompt")) (user-error "Could not read system prompt"))
(let ((buf (get-buffer-create "*ollie-system-prompt*"))) (let ((buf (get-buffer-create "*ellie-system-prompt*")))
(with-current-buffer buf (with-current-buffer buf
(let ((inhibit-read-only t)) (let ((inhibit-read-only t))
(erase-buffer) (erase-buffer)
@ -543,47 +543,47 @@ Key bindings:
;;;; ──────────────── Entry points ──────────────── ;;;; ──────────────── Entry points ────────────────
;;;###autoload ;;;###autoload
(defun ollie () (defun ellie ()
"Open the ollie chat interface. "Open the ollie chat interface.
Attaches to the last session if it still exists; otherwise creates a new one." Attaches to the last session if it still exists; otherwise creates a new one."
(interactive) (interactive)
(unless (file-directory-p ollie-mount-directory) (unless (file-directory-p ellie-mount-directory)
(user-error "ollie-9p not mounted at %s (customize `ollie-mount-directory')" (user-error "ollie-9p not mounted at %s (customize `ellie-mount-directory')"
ollie-mount-directory)) ellie-mount-directory))
(unless ollie--session-id (unless ellie--session-id
(if-let (last (ollie--recall-session)) (if-let (last (ellie--recall-session))
(progn (progn
(setq ollie--session-id last) (setq ellie--session-id last)
(message "Attached to last ollie session %s" last)) (message "Attached to last ollie session %s" last))
(ollie-new-session))) (ellie-new-session)))
(ollie--open-buf)) (ellie--open-buf))
;;;###autoload ;;;###autoload
(defun ollie-new-and-open () (defun ellie-new-and-open ()
"Create a new ollie session and open the chat buffer." "Create a new ollie session and open the chat buffer."
(interactive) (interactive)
(ollie-new-session) (ellie-new-session)
(ollie--open-buf)) (ellie--open-buf))
;;;###autoload ;;;###autoload
(defun ollie-attach-and-open (id) (defun ellie-attach-and-open (id)
"Attach to session ID and open the chat buffer." "Attach to session ID and open the chat buffer."
(interactive (interactive
(let ((ids (ollie--session-ids))) (let ((ids (ellie--session-ids)))
(unless ids (user-error "No ollie sessions available")) (unless ids (user-error "No ollie sessions available"))
(list (completing-read "Session: " ids nil t)))) (list (completing-read "Session: " ids nil t))))
(ollie-attach id) (ellie-attach id)
(ollie--open-buf)) (ellie--open-buf))
(defun ollie--open-buf () (defun ellie--open-buf ()
"Open or switch to the *ollie* chat buffer." "Open or switch to the *ollie* chat buffer."
(let ((buf (get-buffer-create ollie--buf))) (let ((buf (get-buffer-create ellie--buf)))
(with-current-buffer buf (with-current-buffer buf
(unless (derived-mode-p 'ollie-mode) (unless (derived-mode-p 'ellie-mode)
(ollie-mode)) (ellie-mode))
(setq ollie--chat-size -1) (setq ellie--chat-size -1)
(ollie--refresh) (ellie--refresh)
(ollie--start-watching)) (ellie--start-watching))
(pop-to-buffer buf))) (pop-to-buffer buf)))
(provide 'ellie) (provide 'ellie)