Skip to content

Commit f5632e4

Browse files
authored
Merge pull request #269 from shohi/feat/refine-eca-workspaces
make eca-workspaces buffer navigable and interactive
2 parents a0b2346 + d25d973 commit f5632e4

4 files changed

Lines changed: 459 additions & 36 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,12 @@
22

33
## Unreleased
44

5+
- Make the `eca-workspaces` tree buffer more user-friendly. It now uses a dedicated `eca-workspaces-mode` (derived from `special-mode`), so `q` closes the window and `g` refreshes. Folding/unfolding a workspace no longer requires hitting the small arrow icon: pressing `TAB`/`RET` on its line toggles it, and pressing `RET` anywhere on a chat line switches to that chat. `n`/`p` move to the next/previous workspace entry. When `eca-buttons-allow-mouse` is enabled, clicking the workspace text folds/unfolds it and clicking a chat switches to it.
56
- Add `eca-mcp-toggle-server`, available as `M` in the ECA transient menu and `C-c C-S-m` in chat buffers, to filter configured MCP servers and start, stop, or connect the selected server from aligned, color-coded operational-state candidates.
67
- Bugfix: TAB now accepts the selected candidate in completion popups (corfu) in GUI Emacs. `eca-chat-mode-map` bound the raw `<tab>` event, which blocked the `<tab>` -> `TAB` key translation and shadowed the completion UI's own `TAB` binding, restarting the completion session instead; only `TAB` is bound now, so keymap precedence decides while a popup is open. #281
78
- Bugfix: a forward deletion of the first prompt character now works instead of dinging. `eca-chat--key-pressed-deletion` guarded the prompt boundary by rejecting any deletion at the prompt-field start regardless of direction, so `C-d` and, under evil, the `~` (`evil-invert-char`), `r`, `x` and `s` operators left the first char in place (`~` prepended the inverted char instead of replacing it). At the prompt-field start the guard now blocks only backward-deleting commands (`delete-backward-char`, `backward-kill-word`, `evil-delete-backward-char`, ...); deletions above the prompt (context/progress area) and everywhere else are unchanged.
89
- Show the shell/git command breakdown in tool call approvals: raw command plus per-command lines colored by remember state (green saved, yellow not yet), and `Accept and remember` lists the command keys it would newly save (e.g. `git status, rg`), hiding when nothing new would be remembered.
9-
1010
- Support TRAMP / remote hosts: the eca server starts on the remote host, the binary is resolved from the remote `PATH`, and paths are translated both ways using mappings derived from TRAMP workspace folders (explicit `eca-local-to-remote-prefix-map` entries win). Auto-install is local only: install `eca` on the remote or point `eca-custom-command` at it. #270
11-
1211
- Improve chat `@`/`#` completion: selecting a directory now drills into it (keeps completing its contents) instead of finalizing; candidates are workspace-relative paths with directories ending in `/`; candidates no longer vanish mid-token depending on the user's `completion-styles`; TAB in the prompt triggers completion for `@`/`#`/`/` tokens; typing a space after a raw `@path` turns it into a context chip and raw `@path` tokens still count as contexts when the prompt is sent.
1312
- Respect `eca-chat-window-side` (and width/height) even when `eca-chat-use-side-window` is nil, displaying the chat on that side via `display-buffer-in-direction`. To avoid overlapping/duplicate windows, placement now reuses the window already showing the chat, otherwise replaces another visible chat window in place (new-tab behavior), and only opens a new window when there is nothing to reuse.
1413
- Bugfix: `eca-chat-add-context-to-user-prompt` and `eca-chat-add-filepath-to-user-prompt` no longer error with `window-live-p, nil` when the chat buffer is not visible; the chat window is now displayed first. With a prefix arg (`C-u`) they add the context without selecting the chat window, leaving point where it was. #266

‎README.md‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,12 @@ Server / process
6161
- `eca`: Starts eca server/session + open chat
6262
- `eca-stop`: Stop eca server/session
6363
- `eca-restart`: Restart eca server/session
64-
- `eca-workspaces`: show a view with all worskpaces and chats
64+
- `eca-workspaces`: show a view with all worskpaces and chats. In the
65+
buffer, press `q` to close the window, `TAB` to fold/unfold a
66+
workspace, `RET` to fold/unfold a workspace or switch to the chat
67+
under point, `n`/`p` to move to the next/previous workspace, and `g`
68+
to refresh. When `eca-buttons-allow-mouse` is enabled, clicking the
69+
workspace text folds/unfolds it and clicking a chat switches to it
6570
- `eca-settings`: Open the centralized settings panel (MCP servers, and more in the future)
6671
- `eca-mcp-toggle-server`: Filter MCP servers and start, stop, or connect one
6772
- `eca-open-global-config`: Open ECA global config file

‎eca.el‎

Lines changed: 198 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -352,23 +352,35 @@ backtrace. On older Emacs, runs BODY without capture."
352352
(complete-with-action action '("no" "yes") string pred)))))))
353353
workspaces))
354354

355+
(defvar eca-workspaces-widget-icon-keymap
356+
(let ((map (make-sparse-keymap)))
357+
;; Keep the tree-widget mouse behaviour (fold on click) but route
358+
;; keyboard RET/TAB to our commands so they work on every line,
359+
;; including leaf (chat) lines where the icon has no useful action.
360+
(set-keymap-parent map tree-widget-button-keymap)
361+
(define-key map (kbd "RET") #'eca-workspaces-visit-or-toggle)
362+
(define-key map (kbd "TAB") #'eca-workspaces-toggle-line)
363+
(define-key map (kbd "<tab>") #'eca-workspaces-toggle-line)
364+
map)
365+
"Keymap used on tree-widget icons in the ECA workspaces buffer.")
366+
355367
(defun eca--tree-widget-open-all (tree-widget)
356368
"Recursively add :open t and custom icons to TREE-WIDGET and all its children."
357369
(let ((args (plist-get (cdr tree-widget) :args)))
358370
(when args
359371
(plist-put (cdr tree-widget) :args
360372
(mapcar #'eca--tree-widget-open-all args))))
361373
(append tree-widget `(:open t
362-
:open-icon (tree-widget-icon :tag ,"▼ ")
363-
:close-icon (tree-widget-icon :tag ,"▶ ")
364-
:empty-icon (tree-widget-icon :tag ,(propertize "" 'face 'shadow))
365-
:leaf-icon (tree-widget-icon :tag ,(propertize "" 'face 'shadow))
366-
:guide (tree-widget-icon :tag ,(propertize "│" 'face 'shadow))
367-
:end-guide (tree-widget-icon :tag ,(propertize "└" 'face 'shadow))
368-
:no-guide (tree-widget-icon :tag " ")
369-
:handle (tree-widget-icon :tag ,(propertize "─" 'face 'shadow))
370-
:no-handle (tree-widget-icon :tag " ")
371-
:nohandle-guide (tree-widget-icon :tag ,(propertize "│" 'face 'shadow)))))
374+
:open-icon (tree-widget-icon :tag ,"▼ " :keymap ,eca-workspaces-widget-icon-keymap)
375+
:close-icon (tree-widget-icon :tag ,"▶ " :keymap ,eca-workspaces-widget-icon-keymap)
376+
:empty-icon (tree-widget-icon :tag ,(propertize "" 'face 'shadow) :keymap ,eca-workspaces-widget-icon-keymap)
377+
:leaf-icon (tree-widget-icon :tag ,(propertize "" 'face 'shadow) :keymap ,eca-workspaces-widget-icon-keymap)
378+
:guide (tree-widget-icon :tag ,(propertize "│" 'face 'shadow) :keymap ,eca-workspaces-widget-icon-keymap)
379+
:end-guide (tree-widget-icon :tag ,(propertize "└" 'face 'shadow) :keymap ,eca-workspaces-widget-icon-keymap)
380+
:no-guide (tree-widget-icon :tag " " :keymap ,eca-workspaces-widget-icon-keymap)
381+
:handle (tree-widget-icon :tag ,(propertize "─" 'face 'shadow) :keymap ,eca-workspaces-widget-icon-keymap)
382+
:no-handle (tree-widget-icon :tag " " :keymap ,eca-workspaces-widget-icon-keymap)
383+
:nohandle-guide (tree-widget-icon :tag ,(propertize "│" 'face 'shadow) :keymap ,eca-workspaces-widget-icon-keymap))))
372384

373385
;; Public
374386

@@ -467,10 +479,145 @@ When ARG is current prefix, ask for workspace roots to use."
467479
(eca-stop)
468480
(eca))
469481

470-
;;;###autoload
471-
(defun eca-workspaces ()
472-
"Display all running ECA sessions and their chats in a tree view."
473-
(interactive)
482+
(defvar eca-workspaces-mode-map
483+
(let ((map (make-sparse-keymap)))
484+
;; `q', `g' (revert), scrolling, etc. are inherited from
485+
;; `special-mode'. Only the tree-specific keys are defined here.
486+
(define-key map (kbd "RET") #'eca-workspaces-visit-or-toggle)
487+
(define-key map (kbd "TAB") #'eca-workspaces-toggle-line)
488+
(define-key map (kbd "<tab>") #'eca-workspaces-toggle-line)
489+
(define-key map (kbd "n") #'eca-workspaces-next-workspace)
490+
(define-key map (kbd "p") #'eca-workspaces-previous-workspace)
491+
map)
492+
"Keymap for `eca-workspaces-mode'.")
493+
494+
(define-derived-mode eca-workspaces-mode special-mode "ECA-Workspaces"
495+
"Major mode for the ECA workspaces tree buffer.
496+
497+
\\{eca-workspaces-mode-map}"
498+
(setq-local tree-widget-image-enable nil)
499+
(setq-local truncate-lines t)
500+
(setq-local revert-buffer-function
501+
(lambda (&rest _) (eca-workspaces-refresh))))
502+
503+
(defun eca-workspaces--chat-action-on-line ()
504+
"Return the chat-open action stored on the current line, if any."
505+
(save-excursion
506+
(beginning-of-line)
507+
(let ((eol (line-end-position))
508+
(action nil))
509+
(while (and (not action) (< (point) eol))
510+
(setq action (get-text-property (point) 'eca-button-on-action))
511+
(unless action
512+
(forward-char 1)))
513+
action)))
514+
515+
(defun eca-workspaces-toggle-line (&optional event)
516+
"Fold or unfold the workspace tree node on the current line.
517+
Does nothing on a leaf (chat) line, which has nothing to fold.
518+
EVENT, when non-nil, is the mouse event that triggered the command."
519+
(interactive (list last-nonmenu-event))
520+
(when (mouse-event-p event)
521+
(mouse-set-point event))
522+
(save-excursion
523+
(beginning-of-line)
524+
(let ((eol (line-end-position))
525+
(toggled nil))
526+
(while (and (not toggled) (< (point) eol))
527+
(let ((widget (widget-at (point))))
528+
;; The expand/collapse icon is the tree-widget icon that
529+
;; carries a :node and is not a leaf. Guides have no :node
530+
;; and leaf icons cannot be folded, so skip both.
531+
(if (and widget
532+
(tree-widget-p (widget-get widget :parent))
533+
(widget-get widget :node)
534+
(not (widget-get widget :tree-widget--leaf-flag)))
535+
(progn
536+
(widget-apply (widget-get widget :parent) :action)
537+
(setq toggled t))
538+
(forward-char 1))))
539+
toggled)))
540+
541+
(defun eca-workspaces-visit-or-toggle (&optional event)
542+
"Switch to the chat on the current line, or toggle the workspace node.
543+
EVENT, when non-nil, is the mouse event that triggered the command."
544+
(interactive (list last-nonmenu-event))
545+
(when (mouse-event-p event)
546+
(mouse-set-point event))
547+
(let ((action (eca-workspaces--chat-action-on-line)))
548+
(if action
549+
(funcall action)
550+
(eca-workspaces-toggle-line))))
551+
552+
(defun eca-workspaces--session-line-p ()
553+
"Return non-nil if the current line is a workspace (session) entry."
554+
(save-excursion
555+
(beginning-of-line)
556+
(let ((eol (line-end-position))
557+
(found nil))
558+
(while (and (not found) (< (point) eol))
559+
(if (get-text-property (point) 'eca-workspaces-session)
560+
(setq found t)
561+
(forward-char 1)))
562+
found)))
563+
564+
(defun eca-workspaces--goto-session-label ()
565+
"Move point to the workspace label on the current line."
566+
(beginning-of-line)
567+
(let ((eol (line-end-position)))
568+
(while (and (< (point) eol)
569+
(not (get-text-property (point) 'eca-workspaces-session)))
570+
(forward-char 1))))
571+
572+
(defun eca-workspaces-next-workspace (&optional count)
573+
"Move point to the next workspace (session) entry.
574+
With prefix COUNT, repeat that many times."
575+
(interactive "p")
576+
(dotimes (_ (or count 1))
577+
(let ((origin (point))
578+
(found nil))
579+
(forward-line 1)
580+
(while (and (not (eobp)) (not found))
581+
(if (eca-workspaces--session-line-p)
582+
(setq found t)
583+
(forward-line 1)))
584+
(if found
585+
(eca-workspaces--goto-session-label)
586+
(goto-char origin)))))
587+
588+
(defun eca-workspaces-previous-workspace (&optional count)
589+
"Move point to the previous workspace (session) entry.
590+
With prefix COUNT, repeat that many times."
591+
(interactive "p")
592+
(dotimes (_ (or count 1))
593+
(let ((origin (point))
594+
(found nil))
595+
(beginning-of-line)
596+
(while (and (not (bobp)) (not found))
597+
(forward-line -1)
598+
(when (eca-workspaces--session-line-p)
599+
(setq found t)))
600+
(if found
601+
(eca-workspaces--goto-session-label)
602+
(goto-char origin)))))
603+
604+
(defun eca-workspaces--foldable-label (text)
605+
"Return TEXT that toggles its tree node on RET/TAB.
606+
When `eca-buttons-allow-mouse' is non-nil, clicking TEXT also
607+
toggles it and the label shows a clickable affordance."
608+
(let* ((map (make-sparse-keymap))
609+
(props (list 'keymap map)))
610+
(define-key map (kbd "RET") #'eca-workspaces-toggle-line)
611+
(define-key map (kbd "TAB") #'eca-workspaces-toggle-line)
612+
(when eca-buttons-allow-mouse
613+
(define-key map (kbd "<mouse-1>") #'eca-workspaces-toggle-line)
614+
(setq props (append props (list 'pointer 'hand
615+
'help-echo "mouse-1: fold/unfold"))))
616+
(add-text-properties 0 (length text) props text)
617+
text))
618+
619+
(defun eca-workspaces--refresh-buffer (buffer)
620+
"Rebuild the ECA workspaces tree inside BUFFER."
474621
(let ((h (hierarchy-new))
475622
(parent-fn (lambda (item)
476623
(cond
@@ -483,8 +630,10 @@ When ARG is current prefix, ask for workspace roots to use."
483630
(insert
484631
(cond
485632
((eca--session-p item)
486-
(propertize (string-join (eca--session-workspace-folders item) ", ")
487-
'face 'shadow))
633+
(eca-workspaces--foldable-label
634+
(propertize (string-join (eca--session-workspace-folders item) ", ")
635+
'face 'shadow
636+
'eca-workspaces-session t)))
488637
((bufferp item)
489638
(with-current-buffer item
490639
(concat
@@ -506,26 +655,42 @@ When ARG is current prefix, ask for workspace roots to use."
506655
(seq-doseq (chat-by-id (eca--session-chats session))
507656
(when (buffer-live-p (cdr chat-by-id))
508657
(hierarchy-add-tree h (cdr chat-by-id) parent-fn))))
509-
(let ((b (or (when-let ((b (get-buffer eca-workspaces-buffer-name)))
510-
(when (buffer-live-p b)
511-
(with-current-buffer b
512-
(let ((inhibit-read-only t))
513-
(erase-buffer))))
514-
b)
515-
(generate-new-buffer eca-workspaces-buffer-name))))
516-
(with-current-buffer b
517-
(setq-local tree-widget-image-enable nil)
658+
(with-current-buffer buffer
659+
(unless (derived-mode-p 'eca-workspaces-mode)
660+
(eca-workspaces-mode))
661+
(let ((inhibit-read-only t))
662+
(erase-buffer)
518663
(widget-create (eca--tree-widget-open-all
519664
(hierarchy-convert-to-tree-widget h label-fn)))
520665
(widget-setup))
521-
(select-window
522-
(display-buffer
523-
b
524-
'((display-buffer-in-side-window)
525-
(side . bottom)
526-
(slot . 0)
527-
(dedicated . t)
528-
(window-parameters . ((no-delete-other-windows . t)))))))))
666+
(goto-char (point-min)))))
667+
668+
(defun eca-workspaces-refresh ()
669+
"Refresh the ECA workspaces buffer if it exists."
670+
(interactive)
671+
(when-let* ((b (get-buffer eca-workspaces-buffer-name)))
672+
(when (buffer-live-p b)
673+
(eca-workspaces--refresh-buffer b))))
674+
675+
;;;###autoload
676+
(defun eca-workspaces ()
677+
"Display all running ECA sessions and their chats in a tree view.
678+
679+
In the buffer, press \\<eca-workspaces-mode-map>\\[quit-window] to \
680+
close the window,
681+
\\[eca-workspaces-toggle-line] to fold/unfold a workspace, and
682+
\\[eca-workspaces-visit-or-toggle] to switch to the chat under point."
683+
(interactive)
684+
(let ((b (get-buffer-create eca-workspaces-buffer-name)))
685+
(eca-workspaces--refresh-buffer b)
686+
(select-window
687+
(display-buffer
688+
b
689+
'((display-buffer-in-side-window)
690+
(side . bottom)
691+
(slot . 0)
692+
(dedicated . t)
693+
(window-parameters . ((no-delete-other-windows . t))))))))
529694

530695
(defun eca--chat-buffer-candidates-for-session (session seen)
531696
"Return completion candidates for SESSION.

0 commit comments

Comments
 (0)