Skip to content

Commit d42762c

Browse files
committed
fix(chat): go to a sibling chat when closing, not an unrelated buffer
Closing a chat (kill-buffer, C-c C-k, tab close, server delete) now switches its window to the previous chat (or the only one left) and drops it from the session registry, instead of falling back to the settings buffer. C-c C-k only starts a new chat when it was the last. Also swap the [copy]/[copy response] labels for a clipboard icon.
1 parent 51d11e2 commit d42762c

5 files changed

Lines changed: 350 additions & 45 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
- Bugfix: closing a chat (`kill-buffer`, `C-c C-k`, or the tab close button) now switches the chat window to a sibling chat (the previous tab, or the only one left) instead of falling back to an unrelated buffer like the settings buffer, and drops the dead chat from the session registry. `C-c C-k` (`eca-chat-reset`) only starts a fresh chat when the closed chat was the last one.
6+
- Replace the `[copy response]` and `[copy]` chat button labels with a clipboard icon (same click/RET/mouse behavior and tooltips). Customizable via `eca-chat-copy-button-symbol` and `eca-chat-copy-button-symbol-tty` (terminal fallback).
57
- Bugfix: guard `chat/askQuestion` against a non-sequence `:options` (e.g. a malformed string from a misbehaving server). `append` was splitting such a string into character integers that rendered as a list of random numbers; non-sequence options are now treated as empty.
68
- Add a context-usage bar in the chat mode-line, left of the token usage, showing how full the model context window is, colored by category (system prompt, rules, skills, AGENTS.md, tool definitions, tool calls, conversation, free space), each a distinct color. In graphical frames it renders pixel-width thin segments for high granularity (small percentages stay visible); terminals fall back to block characters. Same footprint either way (`eca-chat-context-bar-width`). Colors come from the server (canonical `color`/`freeColor`) so they are consistent; hover the bar for a legend that maps each category to its server emoji swatch (`emoji`/`freeEmoji`, matching the `/context` command output), or click to run the new `/context` command. Configurable via `eca-chat-context-bar-width` and the `:context-bar` module in `eca-chat-mode-line-format`. Needs an eca server that sends `contextBreakdown` in `usage` content.
79
- Auto-dismiss a pending `ask_user` question when another client (e.g. an SSE/web client in remote mode, see eca 0.139.0) answers it first. The server resolves the `ask_user` tool out from under us and sends a `toolCalled`/`toolCallRejected` for that tool-call id but no longer expects our answer (it cancels our request); we now correlate that id with `eca-chat--pending-question` and clear the stale answer-mode prompt state instead of staying stuck waiting for input.

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,8 @@ Chat
156156
- `eca-chat-expand-pending-approval-tools`: Whether to auto-expand tool calls that are pending approval.
157157
- `eca-chat-shrink-called-tools`: Whether to auto-shrink tool calls after they have been executed.
158158
- `eca-chat-show-copy-buttons`: Whether to show copy buttons for chat responses and fenced code blocks (default `t`).
159+
- `eca-chat-copy-button-symbol`: Glyph used for chat copy buttons on graphic frames (default `📋`).
160+
- `eca-chat-copy-button-symbol-tty`: Glyph used for chat copy buttons on terminal (non-graphic) frames.
159161
- `eca-chat-tab-line`: Whether to show a tab line with chat tabs at the top of each chat window (default `t`). Each tab shows the chat status (pending approval, loading) and title.
160162
- `eca-chat-custom-model`: Override the model used for chat (nil = server default).
161163
- `eca-chat-custom-agent`: Override the chat agent (nil = server default).

eca-chat.el

Lines changed: 104 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,16 @@ to nil to keep the whole chat buffer writable."
158158
:type 'string
159159
:group 'eca)
160160

161+
(defcustom eca-chat-copy-button-symbol "📋"
162+
"The glyph used in eca chat buffer for copy buttons on graphic frames."
163+
:type 'string
164+
:group 'eca)
165+
166+
(defcustom eca-chat-copy-button-symbol-tty "⎘"
167+
"Copy button glyph used on terminal (non-graphic) frames."
168+
:type 'string
169+
:group 'eca)
170+
161171
(defcustom eca-chat-expand-pending-approval-tools t
162172
"Whether to auto expand tool calls when pending approval."
163173
:type 'boolean
@@ -856,18 +866,59 @@ time (see `eca-chat-open'), every known chat is registered under
856866
its real id and there is no `'empty' placeholder to migrate from."
857867
(eca-get (eca--session-chats session) chat-id))
858868

869+
(defun eca-chat--sibling-chat-buffer (session buffer)
870+
"Return the chat buffer to focus when BUFFER is closed in SESSION.
871+
Prefers the previous chat (the tab to the left of BUFFER); when
872+
BUFFER is the leftmost chat, returns the next one (to the right).
873+
When BUFFER is not registered, returns any other live chat.
874+
Returns nil when SESSION has no other live chat."
875+
(let* ((chats (-filter #'buffer-live-p
876+
(eca-chat--session-chats-oldest-first session)))
877+
(pos (cl-position buffer chats)))
878+
(cond
879+
((null pos) (-first (lambda (b) (not (eq b buffer))) chats))
880+
((> pos 0) (nth (1- pos) chats))
881+
((< (1+ pos) (length chats)) (nth (1+ pos) chats))
882+
(t nil))))
883+
884+
(defun eca-chat--switch-windows-to-sibling (session buffer)
885+
"Switch any window showing BUFFER to a sibling chat of SESSION.
886+
Updates the session `last-chat-buffer' and preserves each window's
887+
dedication flag so the dedicated chat window keeps showing a chat
888+
instead of falling back to an unrelated buffer. Returns the
889+
sibling buffer switched to, or nil when SESSION has no other chat."
890+
(when-let* ((other (eca-chat--sibling-chat-buffer session buffer)))
891+
(setf (eca--session-last-chat-buffer session) other)
892+
(dolist (win (get-buffer-window-list buffer nil t))
893+
(let ((dedicated (window-dedicated-p win)))
894+
(set-window-dedicated-p win nil)
895+
(set-window-buffer win other)
896+
(set-window-dedicated-p win dedicated)))
897+
other))
898+
859899
(defun eca-chat--delete-chat ()
860-
"Delete current chat."
900+
"Handle killing of the current chat buffer.
901+
Switches any window showing this chat to a sibling chat (so a
902+
dedicated chat window is not replaced by an unrelated buffer such
903+
as settings), drops the chat from the session registry, and, when
904+
confirmed, deletes it server-side."
861905
(when (and (or (eq #'kill-current-buffer this-command)
862906
(eq #'kill-buffer this-command)
863907
(and (symbolp this-command)
864908
(string-prefix-p "eca-" (symbol-name this-command))))
865909
eca-chat--id
866-
(not eca-chat--closed)
867-
(yes-or-no-p "Delete chat from server side (otherwise it will just kill the buffer) ?"))
868-
(eca-api-request-sync (eca-session)
869-
:method "chat/delete"
870-
:params (list :chatId eca-chat--id))))
910+
(not eca-chat--closed))
911+
(let ((buffer (current-buffer))
912+
(chat-id eca-chat--id))
913+
(when-let* ((session (ignore-errors (eca-session))))
914+
(eca-chat--switch-windows-to-sibling session buffer)
915+
(setf (eca--session-chats session)
916+
(eca-dissoc (eca--session-chats session) chat-id))
917+
(eca-chat--force-tab-line-update))
918+
(when (yes-or-no-p "Delete chat from server side (otherwise it will just kill the buffer) ?")
919+
(eca-api-request-sync (eca-session)
920+
:method "chat/delete"
921+
:params (list :chatId chat-id))))))
871922

872923
(defun eca-chat--insert (&rest contents)
873924
"Insert CONTENTS reseting undo-list to avoid buffer inconsistencies."
@@ -2325,28 +2376,17 @@ Tabs are ordered oldest-first so new chats appear on the right."
23252376

23262377
(defun eca-chat--tab-line-close-tab (&optional e)
23272378
"Close the chat tab clicked on.
2328-
If closing the current buffer, switch to another chat tab first.
2329-
E is the mouse event."
2379+
The chat's `kill-buffer' hook switches any window showing it to a
2380+
sibling chat first, so the dedicated chat window keeps showing a
2381+
chat. E is the mouse event."
23302382
(interactive "e")
23312383
(let* ((posnp (event-start e))
2332-
(window (posn-window posnp))
23332384
(tab-prop (get-pos-property 1 'tab (car (posn-string posnp))))
23342385
(buffer (if (bufferp tab-prop)
23352386
tab-prop
23362387
(cdr (assq 'buffer tab-prop)))))
23372388
(when (and buffer (buffer-live-p buffer))
2338-
(with-selected-window window
2339-
(when (eq buffer (current-buffer))
2340-
;; Switch to another chat before killing
2341-
(let* ((session (ignore-errors (eca-session)))
2342-
(other (-first (lambda (buf)
2343-
(and (buffer-live-p buf)
2344-
(not (eq buf buffer))))
2345-
(eca-vals (eca--session-chats session)))))
2346-
(when other
2347-
(setf (eca--session-last-chat-buffer session) other)
2348-
(switch-to-buffer other))))
2349-
(kill-buffer buffer)))))
2389+
(kill-buffer buffer))))
23502390

23512391
(defvar eca-chat--tab-close-map
23522392
(let ((map (make-sparse-keymap)))
@@ -2794,6 +2834,14 @@ Return the position after the inserted button."
27942834
"Return regexp matching the closing FENCE line."
27952835
(concat "^[ \t]*" (regexp-quote fence) "[ \t]*$"))
27962836

2837+
(defun eca-chat--copy-button-label ()
2838+
"Return the copy button glyph followed by a newline.
2839+
Uses the TTY glyph on terminal (non-graphic) frames."
2840+
(concat (if (display-graphic-p)
2841+
eca-chat-copy-button-symbol
2842+
eca-chat-copy-button-symbol-tty)
2843+
"\n"))
2844+
27972845
(defun eca-chat--refresh-code-copy-buttons (&optional from to)
27982846
"Refresh copy buttons for fenced code blocks between FROM and TO."
27992847
(let* ((start (or from (point-min)))
@@ -2812,7 +2860,7 @@ Return the position after the inserted button."
28122860
(when (re-search-forward close-regexp limit t)
28132861
(let* ((body-end (copy-marker (match-beginning 0)))
28142862
(button (eca-chat--copy-button-text
2815-
"[copy]\n"
2863+
(eca-chat--copy-button-label)
28162864
(lambda ()
28172865
(interactive)
28182866
(eca-chat--copy-region
@@ -2838,7 +2886,7 @@ Return the position after the inserted button."
28382886
(let ((copy-start (make-marker))
28392887
(copy-end (copy-marker end)))
28402888
(let* ((button (eca-chat--copy-button-text
2841-
"[copy response]\n"
2889+
(eca-chat--copy-button-label)
28422890
(lambda ()
28432891
(interactive)
28442892
(eca-chat--copy-region
@@ -4135,10 +4183,18 @@ selectAgent, selectVariant, selectTrust) are scoped by `chatId':
41354183
(eca-chat--apply-per-chat-config chat-config chat-buffer)))))
41364184

41374185
(defun eca-chat-deleted (session params)
4138-
"Handle chat deleted notification for SESSION with PARAMS."
4186+
"Handle chat deleted notification for SESSION with PARAMS.
4187+
Switches any window showing the deleted chat to a sibling chat
4188+
before removing it (so a dedicated chat window keeps showing a
4189+
chat), and marks it closed so the `kill-buffer' hook does not
4190+
prompt or re-issue a redundant `chat/delete'."
41394191
(let* ((chat-id (plist-get params :chatId))
41404192
(chat-buffer (eca-get (eca--session-chats session) chat-id)))
41414193
(when chat-buffer
4194+
(when (buffer-live-p chat-buffer)
4195+
(eca-chat--switch-windows-to-sibling session chat-buffer)
4196+
(with-current-buffer chat-buffer
4197+
(setq-local eca-chat--closed t)))
41424198
(setf (eca--session-chats session)
41434199
(eca-dissoc (eca--session-chats session) chat-id))
41444200
(when (buffer-live-p chat-buffer)
@@ -4668,12 +4724,19 @@ Starting from the beginning of the buffer."
46684724

46694725
;;;###autoload
46704726
(defun eca-chat-reset ()
4671-
"Kill current chat (asking to keep or not history) and start a new."
4727+
"Kill the current chat (asking whether to delete it server-side).
4728+
Switch to the previous chat when the session has others, or start
4729+
a fresh chat when this was the only one."
46724730
(interactive)
4673-
(eca-chat--with-current-buffer (eca-chat--get-last-buffer (eca-session))
4674-
(when eca-chat--id
4675-
(kill-buffer)
4676-
(eca-chat-new))))
4731+
(let* ((session (eca-session))
4732+
(buffer (eca-chat--get-last-buffer session)))
4733+
(eca-assert-session-running session)
4734+
(when (and (buffer-live-p buffer)
4735+
(buffer-local-value 'eca-chat--id buffer))
4736+
(let ((sibling (eca-chat--sibling-chat-buffer session buffer)))
4737+
(kill-buffer buffer)
4738+
(unless sibling
4739+
(eca-chat--new-chat session))))))
46774740

46784741
;;;###autoload
46794742
(defun eca-chat-go-to-prev-user-message ()
@@ -5131,15 +5194,7 @@ another chat first."
51315194
(buffer-local-value 'eca-chat--id buffer))))
51325195
(unless (and (buffer-live-p buffer) chat-id)
51335196
(user-error "No active chat to delete"))
5134-
(when-let ((other (-first (lambda (buf)
5135-
(and (buffer-live-p buf) (not (eq buf buffer))))
5136-
(eca-vals (eca--session-chats session)))))
5137-
(setf (eca--session-last-chat-buffer session) other)
5138-
(dolist (win (get-buffer-window-list buffer nil t))
5139-
(let ((dedicated (window-dedicated-p win)))
5140-
(set-window-dedicated-p win nil)
5141-
(set-window-buffer win other)
5142-
(set-window-dedicated-p win dedicated))))
5197+
(eca-chat--switch-windows-to-sibling session buffer)
51435198
;; Mark closed so the kill-buffer hook neither prompts nor sends a
51445199
;; second chat/delete when BUFFER is killed below.
51455200
(with-current-buffer buffer
@@ -5156,10 +5211,17 @@ another chat first."
51565211
(interactive)
51575212
(let ((session (eca-session)))
51585213
(eca-assert-session-running session)
5159-
(let ((_ (cl-incf eca-chat--new-chat-id))
5160-
(new-chat-buffer (eca-chat--create-buffer session)))
5161-
(setf (eca--session-last-chat-buffer session) new-chat-buffer)
5162-
(eca-chat-open session))))
5214+
(eca-chat--new-chat session)))
5215+
5216+
(defun eca-chat--new-chat (session)
5217+
"Create and open a fresh chat buffer for SESSION.
5218+
Unlike `eca-chat-new', SESSION is passed explicitly so this works
5219+
even when called right after the previous chat buffer was killed
5220+
\(and `eca-session' could no longer resolve from the dead buffer)."
5221+
(let ((_ (cl-incf eca-chat--new-chat-id))
5222+
(new-chat-buffer (eca-chat--create-buffer session)))
5223+
(setf (eca--session-last-chat-buffer session) new-chat-buffer)
5224+
(eca-chat-open session)))
51635225

51645226
(declare-function whisper-run "ext:whisper" ())
51655227

0 commit comments

Comments
 (0)