ellie: point preservation, stale-stream guard, block-headers, keybinding polish, and doc screenshot

ellie.el:
- Preserve tree selection and cursor across tree rerenders.
- Keep chat cursor stationary; only update when already at end.
- Guard chat-filter against stale output from replaced ollie-9p read processes.
- Render [[[name]]] markers as bold underlined block headers; suppress [[[end]]].
- Refresh session/idx and start timer on startup so tree displays immediately.
- Fix Active/Inactive header tuple layout (was missing 'nil' slot).
- Bind TAB for tree expand/collapse, C-c C-SPC and C-c C-p for prompt input,
  SPC in chat for prompt, q in chat for quit (stops stream + buries buffer).
- RET on an agent now opens the chat buffer.
- Prompt input window selects and focuses itself after opening.

doc/ellie.md:
- Embed ellie screenshot (img/ellie.png) centered at 75% width above Session tree.
This commit is contained in:
Ollie Agent 2026-08-04 20:59:23 +02:00
parent 7888627e90
commit 6fe9c20b54
3 changed files with 52 additions and 15 deletions

View File

@ -394,7 +394,7 @@ Prompts for agent profile and other options."
(setq ellie--tree-expanded '("active"))
(dolist (s sessions)
(push (nth 2 s) ellie--tree-expanded)))
(push (list 'header "active" "Active") result)
(push (list 'header "active" nil "Active") result)
(when (and active (member "active" ellie--tree-expanded))
(dolist (s active)
(let ((name (nth 1 s)) (id (nth 2 s)) (agents (nth 3 s)))
@ -409,7 +409,7 @@ Prompts for agent profile and other options."
result)))))))
;; Inactive header
(when inactive
(push (list 'header "inactive" "Inactive") result)
(push (list 'header "inactive" nil "Inactive") result)
(when (member "inactive" ellie--tree-expanded)
(dolist (s inactive)
(let ((name (nth 1 s)) (id (nth 2 s)) (agents (nth 3 s)))
@ -486,7 +486,7 @@ Prompts for agent profile and other options."
(setq ellie--session-id parent
ellie--agent-id key)
(ellie--persist-session parent)
(message "Agent: %s / %s" parent key)))))
(ellie-tree-chat)))))
(defun ellie-tree-chat ()
"Open chat buffer for the selected session/agent."
@ -555,15 +555,35 @@ Prompts for agent profile and other options."
(defvar ellie--chat-stream nil
"Process object for the chat stream, or nil.")
(defun ellie--chat-filter (_proc output)
(defun ellie--render-chat-output (output)
"Render block markers in OUTPUT as block headers.
Suppress `[[[end]]]` markers."
(let ((start 0)
rendered)
(while (string-match "\\[\\[\\[\\([^]]+\\)\\]\\]\\]" output start)
(push (substring output start (match-beginning 0)) rendered)
(let ((name (match-string 1 output)))
(unless (string= name "end")
(push (propertize (concat name "\n")
'face '(:weight bold :underline t))
rendered)))
(setq start (match-end 0)))
(apply #'concat (nreverse (cons (substring output start) rendered)))))
(defun ellie--chat-filter (proc output)
"Filter function for the chat stream process.
Inserts OUTPUT into the chat buffer as it arrives."
(when (buffer-live-p (get-buffer ellie--buf))
Ignore output from a stream that has already been replaced."
(when (and (eq proc ellie--chat-stream)
(buffer-live-p (get-buffer ellie--buf)))
(with-current-buffer ellie--buf
(let ((inhibit-read-only t)
(at-end (= (point) (point-max))))
(insert output)
(ansi-color-apply-on-region (point-marker) (point))
(at-end (= (point) (point-max)))
(start (copy-marker (point-max) t)))
(save-excursion
(goto-char (point-max))
(insert (ellie--render-chat-output output))
(ansi-color-apply-on-region start (point)))
(set-marker start nil)
(font-lock-ensure)
(when at-end (goto-char (point-max))))
(force-mode-line-update))))
@ -614,9 +634,13 @@ QUEUED non-nil means the prompt will be enqueued, not submitted immediately."
(erase-buffer)
(ellie-input-mode)
(setq ellie--input-queued queued))
(display-buffer buf
`(display-buffer-below-selected
(window-height . ,ellie-input-window-height)))))
(let ((window (display-buffer buf
`(display-buffer-below-selected
(window-height . ,ellie-input-window-height)))))
(when (window-live-p window)
(select-window window)
(with-current-buffer buf
(goto-char (point-max)))))))
(defun ellie-input-submit ()
"Submit the contents of the input buffer as a prompt."
@ -645,6 +669,7 @@ QUEUED non-nil means the prompt will be enqueued, not submitted immediately."
(defvar ellie-mode-map
(let ((m (make-sparse-keymap)))
(define-key m (kbd "RET") #'ellie-send-input)
(define-key m (kbd "SPC") #'ellie-send-input)
(define-key m (kbd "C-c C-q") #'ellie-queue-input)
(define-key m (kbd "C-c C-c") #'ellie-stop)
(define-key m (kbd "C-c C-k") #'ellie-kill-session)
@ -653,7 +678,7 @@ QUEUED non-nil means the prompt will be enqueued, not submitted immediately."
(define-key m (kbd "C-c C-p") #'ellie-show-system-prompt)
(define-key m (kbd "n") #'ellie-new-and-open)
(define-key m (kbd "g") #'ellie-refresh-chat)
(define-key m (kbd "q") #'ellie-tree)
(define-key m (kbd "q") #'ellie-quit-chat)
m)
"Keymap for `ellie-mode'.")
@ -667,6 +692,12 @@ QUEUED non-nil means the prompt will be enqueued, not submitted immediately."
(interactive)
(ellie--open-input t))
(defun ellie-quit-chat ()
"Stop the chat stream and bury the chat buffer."
(interactive)
(ellie--stop-watching)
(bury-buffer (get-buffer ellie--buf)))
(defun ellie-refresh-chat ()
"Restart the chat stream."
(interactive)
@ -740,13 +771,15 @@ QUEUED non-nil means the prompt will be enqueued, not submitted immediately."
(message "Attached to session %s" (car last)))
(setq ellie--session-id nil
ellie--agent-id nil))
(ellie--tree-render))
(ellie--tree-render)
(run-at-time 0.5 nil #'ellie-tree-refresh))
;;;###autoload
(defun ellie-tree ()
"Open the session tree buffer."
(interactive)
(ellie--tree-render))
(ellie--tree-render)
(run-at-time 0.5 nil #'ellie-tree-refresh))
(defun ellie-new-and-open ()
"Create a new session and open the chat buffer."

View File

@ -44,6 +44,10 @@ M-x ellie
Opens the session tree (`*ellie-sessions*`), attached to the last-known
session for your working directory if it still exists.
<p align="center">
<img src="img/ellie.png" alt="ellie Emacs interface showing the Ollie session tree and chat window" width="75%">
</p>
## Session tree
The tree shows all sessions organized by active/inactive state:

BIN
doc/img/ellie.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB