From 55cc8fb49a3714e7f35d398b6b6b5f0c30e1c1d1 Mon Sep 17 00:00:00 2001 From: Juha Itkonen Date: Sun, 5 Jul 2026 12:05:18 +0300 Subject: [PATCH] feat(chat,table): add opt-in markdown markup visibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce `eca-chat-hide-markdown-markup` so users who see fenced code blocks jump while typing or streaming can keep markdown fences/backticks visible without changing the default chat rendering behavior. Apply the same visibility setting to table helper buffers so table width measurement and `eca-table-open` stay consistent with chat display. Add focused tests and document the option in the README and changelog. 🤖 Generated with [ECA](https://eca.dev) (openai/gpt-5.5 - xhigh) Co-Authored-By: eca-agent --- CHANGELOG.md | 1 + README.md | 1 + eca-chat.el | 19 +++++++++++++++++-- eca-table.el | 17 ++++++++++++----- test/eca-chat-test.el | 16 ++++++++++++++++ test/eca-table-test.el | 24 ++++++++++++++++++++++++ 6 files changed, 71 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81e2aa2..718d21c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased - 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 +- Add `eca-chat-hide-markdown-markup`, an opt-in display control for users seeing fenced code blocks jump in chat buffers. The default preserves the existing hidden-markup rendering; set it to `nil` to keep markdown fences/backticks visible, which avoids markdown-mode's fenced-code boundary invisibility during typing and streaming. - Bugfix: don't move the cursor while the chat is streaming. `eca-chat-content-received` rendered without a `save-excursion`, so content streamed above the prompt (e.g. while "thinking") dragged point up to the middle of the buffer; point is now preserved. - Bugfix: keep the `:usage` and `:trust` mode-line segments visible after adding the context-usage bar. The right-alignment reserved space from `(length right)`, which counts the bar's pixel-width `display` spaces (and wide glyphs) as ~1 char each, so the segments overflowed off the right edge. It now measures the real rendered width via `string-pixel-width` and aligns flush to the right edge in pixels (Emacs 29+ only; on Emacs 28 right segments follow left without alignment). - 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. diff --git a/README.md b/README.md index 8e74276..e534535 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,7 @@ Chat - `eca-chat-prompt-prefix`: Prompt prefix string shown before user input. - `eca-chat-prompt-prefix-loading`: Prompt prefix string while a request is in progress. - `eca-chat-read-only-history`: Whether the chat history/output, the `---` separator and the task area are read-only so only the progress, `@`-context and prompt input lines stay editable (default `t`). Set to `nil` to keep the whole buffer writable. +- `eca-chat-hide-markdown-markup`: Whether to hide markdown markup in chat buffers (default `t`). Set to `nil` to keep fences/backticks visible, which may avoid fenced code blocks jumping while typing or streaming. - `eca-chat-history-page-size`: Number of newest messages to load when opening a persisted chat (default `50`). When non-nil, `eca-chat-resume` opens chats with a bounded window and shows a "Load older messages" control to page through earlier history on demand; set to `nil` to replay the entire history on open. - `eca-chat-context-prefix`: Prefix used for context references in the chat buffer (default `@`). - `eca-chat-filepath-prefix`: Prefix used for file path references in the chat buffer (default `#`). diff --git a/eca-chat.el b/eca-chat.el index e8ec223..460f444 100644 --- a/eca-chat.el +++ b/eca-chat.el @@ -289,6 +289,21 @@ updates during streaming)." (number :tag "Seconds")) :group 'eca) +(defcustom eca-chat-hide-markdown-markup t + "Whether ECA chat renders `markdown-markup' as invisible. +When non-nil, preserve the historical hidden-markup look. When +nil, keep markup visible while leaving `markdown-hide-markup' +enabled. This is recommended if fenced code blocks jump while +typing or streaming." + :type 'boolean + :group 'eca) + +(defun eca-chat--apply-markdown-markup-visibility () + "Apply `eca-chat-hide-markdown-markup' in current buffer." + (if eca-chat-hide-markdown-markup + (add-to-invisibility-spec 'markdown-markup) + (remove-from-invisibility-spec 'markdown-markup))) + (defvar-local eca-chat--tool-call-prepare-counters (make-hash-table :test 'equal) "Hash table mapping toolCall ID to message count.") @@ -3110,9 +3125,9 @@ CHILD, NAME, DOCSTRING and BODY are passed down." ;; Show diff blocks in markdown-mode with colors. (setq-local markdown-fontify-code-blocks-natively t) - ;; Enable gfm-view-mode-like rendering without read-only + ;; Enable gfm-view-mode-like rendering without read-only. (setq-local markdown-hide-markup t) - (add-to-invisibility-spec 'markdown-markup) + (eca-chat--apply-markdown-markup-visibility) ;; markdown-mode declares keymap, help-echo, and mouse-face as ;; font-lock-extra-managed-props, which causes font-lock-ensure to diff --git a/eca-table.el b/eca-table.el index 5e4cf91..4c63559 100644 --- a/eca-table.el +++ b/eca-table.el @@ -23,7 +23,8 @@ (require 'seq) (require 'eca-util) -;; Forward-declare defcustom defined in eca-chat.el. +;; Forward-declare defcustoms defined in eca-chat.el. +(defvar eca-chat-hide-markdown-markup t) (defvar eca-chat-table-beautify) (defvar eca-chat-parent-mode) @@ -96,6 +97,12 @@ is on it while leaving `o' untouched everywhere else." (defvar eca-table--fontlock-buffer nil "Reusable buffer for font-lock based width measurement.") +(defun eca-table--apply-markdown-markup-visibility () + "Apply ECA chat markdown markup visibility in current buffer." + (if eca-chat-hide-markdown-markup + (add-to-invisibility-spec 'markdown-markup) + (remove-from-invisibility-spec 'markdown-markup))) + (defun eca-table--get-fontlock-buffer () "Return a reusable buffer configured for width measurement. Uses the chat parent mode and markdown-hide-markup to match @@ -107,9 +114,9 @@ the actual display rendering." (mode (or eca-chat-parent-mode 'gfm-mode))) (with-current-buffer buf (unless (derived-mode-p mode) - (funcall mode) - (setq-local markdown-hide-markup t) - (add-to-invisibility-spec 'markdown-markup))) + (funcall mode)) + (setq-local markdown-hide-markup t) + (eca-table--apply-markdown-markup-visibility)) buf)) (defun eca-table--display-width (str) @@ -255,7 +262,7 @@ the mouse click on the action bar." (erase-buffer) (gfm-mode) (setq-local markdown-hide-markup t) - (add-to-invisibility-spec 'markdown-markup) + (eca-table--apply-markdown-markup-visibility) (setq-local markdown-fontify-code-blocks-natively t) (face-remap-add-relative 'markdown-table-face '(:inherit fixed-pitch)) (insert text) diff --git a/test/eca-chat-test.el b/test/eca-chat-test.el index 9ed6945..d246cac 100644 --- a/test/eca-chat-test.el +++ b/test/eca-chat-test.el @@ -66,6 +66,22 @@ does not treat the first line as metadata. Returns FN's value." ;; Tests ;; --------------------------------------------------------------------------- +(describe "eca-chat--apply-markdown-markup-visibility" + (it "keeps the historical hidden-markup default" + (with-temp-buffer + (let ((eca-chat-hide-markdown-markup t)) + (eca-chat--apply-markdown-markup-visibility) + (expect (memq 'markdown-markup buffer-invisibility-spec) + :to-be-truthy)))) + + (it "allows markdown markup to stay visible" + (with-temp-buffer + (add-to-invisibility-spec 'markdown-markup) + (let ((eca-chat-hide-markdown-markup nil)) + (eca-chat--apply-markdown-markup-visibility) + (expect (memq 'markdown-markup buffer-invisibility-spec) + :to-be nil))))) + (describe "eca-chat--key-pressed-deletion" (describe "multi-line prompt" diff --git a/test/eca-table-test.el b/test/eca-table-test.el index 3f9710e..1c4d17f 100644 --- a/test/eca-table-test.el +++ b/test/eca-table-test.el @@ -28,6 +28,11 @@ (expect (eca-table--display-width "**bold**") :to-equal 4)) + (it "can keep markup visible in display width calculations" + (let ((eca-chat-hide-markdown-markup nil)) + (expect (eca-table--display-width "**bold**") + :to-equal 8))) + (it "returns display width for italic with asterisks" (expect (eca-table--display-width "*italic*") :to-equal 6)) @@ -222,6 +227,25 @@ (expect (string-match-p "Setting" (buffer-string)) :to-be-truthy)) (kill-buffer buf))) + (it "can keep markdown markup visible in the table buffer" + (when (get-buffer "*eca-table*") (kill-buffer "*eca-table*")) + (let ((eca-chat-hide-markdown-markup nil)) + (with-temp-buffer + (gfm-mode) + (setq-local markdown-hide-markup t) + (insert "| Setting | What it does |\n") + (insert "|---|---|\n") + (insert "| alpha | **bold** things |\n") + (goto-char (point-min)) + (forward-line 2) + (eca-table-open))) + (let ((buf (get-buffer "*eca-table*"))) + (expect buf :to-be-truthy) + (with-current-buffer buf + (expect (memq 'markdown-markup buffer-invisibility-spec) + :to-be nil)) + (kill-buffer buf))) + (it "errors when point is not on a table" (with-temp-buffer (gfm-mode)