Function: eca-chat--render-content
eca-chat--render-content is a natively compiled function defined in
eca-chat.el.
Signature
(eca-chat--render-content SESSION CHAT-BUFFER ROLE CONTENT ROOTS &optional PARENT-TOOL-CALL-ID CHAT-ID)
Documentation
Render CONTENT inside CHAT-BUFFER for SESSION.
ROLE is the message role. ROOTS is the list of workspace roots.
When PARENT-TOOL-CALL-ID is non-nil, renders as nested content inside
that expandable block (subagent mode).
CHAT-ID is the chat session the content belongs to, used for tool call
approval requests. Falls back to the buffer-local eca-chat--id.
Must be called with eca-chat--with-current-buffer or equivalent.
Source Code
;; Defined in /nix/store/jklqzw6pzz6j4jrbv8wy0iwgi7jlrbld-emacs-packages-deps/share/emacs/site-lisp/elpa/eca-20260928.1827/eca-chat.el
(defun eca-chat--render-content (session chat-buffer role content roots &optional parent-tool-call-id chat-id)
"Render CONTENT inside CHAT-BUFFER for SESSION.
ROLE is the message role. ROOTS is the list of workspace roots.
When PARENT-TOOL-CALL-ID is non-nil, renders as nested content inside
that expandable block (subagent mode).
CHAT-ID is the chat session the content belongs to, used for tool call
approval requests. Falls back to the buffer-local `eca-chat--id'.
Must be called with `eca-chat--with-current-buffer' or equivalent."
(let* ((content-id (plist-get content :contentId))
(content-type (plist-get content :type))
(tool-call-next-line-spacing (make-string (1+ (length eca-chat-expandable-block-open-symbol)) ?\s))
;; Whether the user acted on this approval from within its
;; block; checked now, before rendering drops the buttons.
(approval-acted-on? (and (member content-type '("toolCallRunning" "toolCalled"
"toolCallRejected"))
(eca-chat--approval-acted-on-p (plist-get content :id)))))
;; Tool call lifecycle events are the only content that adds or
;; removes approval buttons; drop the cached scan result so the
;; next status check rescans the buffer.
(when (member content-type '("toolCallRun" "toolCallRunning"
"toolCalled" "toolCallRejected"))
(eca-chat--invalidate-pending-approvals-cache)
(eca-chat--invalidate-tab-line-cache session))
(when (and (not parent-tool-call-id)
(or (not (equal content-type "text"))
(member role '("user" "system"))))
(eca-chat--stream-flush))
(pcase content-type
("metadata"
(unless parent-tool-call-id
(setq-local eca-chat--title (plist-get content :title))
(eca-chat--invalidate-tab-line-cache session)))
("text"
(when-let* ((text (plist-get content :text)))
(pcase role
("user"
(unless parent-tool-call-id
;; Capture the insertion point before adding the user
;; expandable so we can scope `font-lock-ensure' to just
;; the newly-inserted region instead of the whole buffer.
(let ((user-msg-start (eca-chat--content-insertion-point)))
(when eca-chat--steered-prompt
(setq-local eca-chat--steered-prompt nil)
(eca-chat--update-steer-area))
(eca-chat--ensure-empty-line-before user-msg-start)
(eca-chat--add-expandable-content
content-id
(propertize (string-trim text) 'font-lock-face 'eca-chat-user-messages-face)
(eca-buttonize
eca-chat-mode-map
(propertize "Rollback chat to before this message" 'font-lock-face 'eca-chat-rollback-face)
(lambda () (eca-chat--rollback session content-id (string-trim text)))))
(when-let* ((ov (eca-chat--get-expandable-content content-id)))
(overlay-put ov 'eca-chat--user-message-id content-id)
(overlay-put ov 'eca-chat--timestamp (float-time))
(eca-chat--paint-user-message ov)
(eca-chat--linkify-user-message session ov roots))
(setq-local eca-chat--last-response-copy-start nil)
(setq-local eca-chat--last-response-copy-kind nil)
(eca-chat--mark-header)
(font-lock-ensure user-msg-start (point-max))
;; The user was at the prompt to send, so keep it
;; visible even when the message is long: the guard in
;; `eca-chat--ensure-prompt-visible' cannot pass then,
;; as the insertion pushes the prompt below the window
;; end. Skip when prepending older history pages.
(unless eca-chat--insertion-point-override
(eca-chat--ensure-prompt-visible t)))))
("system"
(eca-chat--add-text-content
(propertize text
'font-lock-face 'eca-chat-system-messages-face
'line-height 20)))
(_
(if parent-tool-call-id
;; Subagent: append assistant text to the parent tool call content
(eca-chat--update-expandable-content
parent-tool-call-id nil text t)
(unless (eq eca-chat--last-response-copy-kind 'text)
(setq-local eca-chat--last-response-copy-start
(eca-chat--content-insertion-point)))
(setq-local eca-chat--last-response-copy-kind 'text)
(if (eca-chat--stream-buffering-enabled-p)
(eca-chat--stream-buffered-text text)
(eca-chat--add-text-content text)
;; Defer fontification: let jit-lock handle visible-area
;; updates and run a single final ensure in the
;; "finished" progress arm below.
(eca-chat--schedule-fontify)))))))
("url"
(unless parent-tool-call-id
(eca-chat--add-header
(concat "đ "
(eca-buttonize
eca-chat-mode-map
(plist-get content :title)
(lambda () (browse-url (plist-get content :url))))
"\n\n"))))
("image"
(eca-chat--render-image-content content parent-tool-call-id))
("flag"
(let* ((flag-text (plist-get content :text))
(flag-content-id (plist-get content :contentId))
(flag-str (propertize (concat "đŠī¸ī¸ " flag-text)
'font-lock-face 'eca-chat-flag-face))
(fork-btn (eca-buttonize
eca-chat-mode-map
(propertize "Fork from here" 'font-lock-face 'eca-chat-rollback-face)
(lambda () (eca-chat--fork-from-flag session flag-content-id))))
(remove-btn (eca-buttonize
eca-chat-mode-map
(propertize "Remove flag" 'font-lock-face 'eca-chat-rollback-face)
(lambda () (eca-chat--remove-flag session flag-content-id))))
(actions (concat fork-btn "\n" remove-btn)))
(eca-chat--add-expandable-content flag-content-id flag-str actions)
(when-let* ((ov (eca-chat--get-expandable-content flag-content-id)))
(overlay-put ov 'eca-chat--flag-text flag-text)
(overlay-put ov 'eca-chat--timestamp (float-time)))))
("reasonStarted"
(let ((id (plist-get content :id))
(label (propertize "Thinking..." 'font-lock-face 'eca-chat-reason-label-face)))
(eca-chat--add-expandable-content id label "" parent-tool-call-id)))
("reasonText"
(let ((id (plist-get content :id))
(label (propertize "Thinking..." 'font-lock-face 'eca-chat-reason-label-face))
(text (plist-get content :text)))
(eca-chat--update-expandable-content id label text t parent-tool-call-id)))
("reasonFinished"
(let* ((id (plist-get content :id))
(base (propertize "Thought" 'font-lock-face 'eca-chat-reason-label-face))
(time (when-let ((ms (plist-get content :totalTimeMs)))
(concat " " (eca-chat--time->presentable-time ms))))
(label (concat base time)))
(eca-chat--update-expandable-content id label "" t parent-tool-call-id)))
("hookActionStarted"
(let* ((id (plist-get content :id))
(name (plist-get content :name))
(label (propertize (format "Running hook '%s'..." name) 'font-lock-face 'eca-chat-hook-label-face)))
(eca-chat--add-expandable-content id label "" parent-tool-call-id)))
("hookActionFinished"
(let* ((id (plist-get content :id))
(name (plist-get content :name))
(status (number-to-string (plist-get content :status)))
(output (plist-get content :output))
(error (plist-get content :error))
(label (propertize (format "Executed hook '%s'" name) 'font-lock-face 'eca-chat-hook-label-face)))
(eca-chat--update-expandable-content id label (eca-chat--content-table
(append
`(("Name" . ,name)
("Status" . ,status))
(when output `(("Output" . ,output)))
(when error `(("Error" . ,error)))))
nil parent-tool-call-id)))
("toolCallPrepare"
(if (eca-chat--task-tool-call-p content)
(unless (eca-chat--get-expandable-content eca-chat--task-block-id)
(eca-chat--render-task-widget t))
(when-let* ((id (plist-get content :id))
(name (plist-get content :name))
(server (plist-get content :server)))
(let* ((argsText (plist-get content :argumentsText))
(details (plist-get content :details))
(subagent? (string= "subagent" (plist-get details :type)))
(label (or (plist-get content :summary)
(format "Preparing tool: %s__%s" server name)))
(current-count (gethash id eca-chat--tool-call-prepare-counters 0))
(cached-content (gethash id eca-chat--tool-call-prepare-content-cache ""))
(new-content (concat cached-content argsText))
(should-update-ui-p
(pcase eca-chat-tool-call-prepare-throttle
('all t)
('smart (or (= current-count 0)
(= (mod current-count eca-chat-tool-call-prepare-update-interval) 0))))))
;; Always cache the metadata and content
(puthash id (1+ current-count) eca-chat--tool-call-prepare-counters)
(puthash id new-content eca-chat--tool-call-prepare-content-cache)
;; Only update UI when throttling permits
(when should-update-ui-p
(let* ((label-face (if subagent?
'eca-chat-subagent-tool-call-label-face
'eca-chat-mcp-tool-call-label-face))
(label (concat (propertize label 'font-lock-face label-face)
" " eca-chat-mcp-tool-call-loading-symbol))
;; Tag body and accumulated content with `eca-no-fontify' so
;; the custom `font-lock-fontify-region-function' skips them
;; while the tool args are still streaming (#234). The
;; property travels through `eca-chat--insert' and
;; `eca-chat--update-expandable-content' to every code
;; path that lands the body in the buffer; the `toolCalled'
;; arm later reinserts un-tagged final content, so jit-lock
;; refontifies normally on the next redisplay.
(body (if subagent?
(eca-chat--content-table `())
(propertize
(eca-chat--content-table
`(("Tool" . ,name)
("Server" . ,server)
("Arguments" . ,new-content)))
'eca-no-fontify t)))
(update-content (if subagent?
body
(propertize new-content 'eca-no-fontify t))))
(if (eca-chat--get-expandable-content id)
;; Update with accumulated content, not just this chunk
(eca-chat--update-expandable-content
id label update-content nil parent-tool-call-id)
(eca-chat--add-expandable-content
id label body parent-tool-call-id))))))))
("toolCallRun"
(when (and (eca-chat--task-tool-call-p content)
(plist-get content :manualApproval))
(eca-chat--show-task-tool-call-approval session content chat-id tool-call-next-line-spacing))
(unless (or (eca-chat--task-tool-call-p content)
;; A stale run from an older history page must not
;; resurrect an already-answered approval prompt (#307).
(and (plist-get content :manualApproval)
(eca-chat--tool-call-approval-resolved-p
(plist-get content :id))))
(let* ((id (plist-get content :id))
(args (plist-get content :arguments))
(name (plist-get content :name))
(server (plist-get content :server))
(label (or (plist-get content :summary)
(format "Calling tool: %s__%s" server name)))
(manual? (plist-get content :manualApproval))
(status (if manual?
eca-chat-mcp-tool-call-pending-approval-symbol
eca-chat-mcp-tool-call-loading-symbol))
(details (plist-get content :details))
(approval-text (when manual?
(eca-chat--build-tool-call-approval-str-content session id tool-call-next-line-spacing chat-id details)))
;; Decide before rendering: the approval buttons and the
;; expansion below grow the block, which can push the
;; prompt out of the window and fail the check afterwards.
(viewing-bottom? (and manual?
eca-chat-expand-pending-approval-tools
(-some-> (get-buffer-window (current-buffer))
(eca-chat--viewing-bottom-p)))))
;; Register subagent mapping only for top-level tool calls
(when (and (not parent-tool-call-id)
(string= "subagent" (plist-get details :type)))
(puthash (plist-get details :subagentChatId) id eca-chat--subagent-chat-id->tool-call-id))
(pcase (plist-get details :type)
("fileChange" (eca-chat--tool-call-file-change-details content label approval-text nil status tool-call-next-line-spacing roots parent-tool-call-id))
("shellCommand" (eca-chat--tool-call-shell-command-details content label approval-text nil status parent-tool-call-id nil (not manual?)))
("subagent" (eca-chat--tool-call-subagent-details id args label approval-text nil status parent-tool-call-id details))
(_ (eca-chat--update-expandable-content
id
(concat (propertize label 'font-lock-face 'eca-chat-mcp-tool-call-label-face)
" " status
"\n"
approval-text)
(eca-chat--content-table
`(("Tool" . ,name)
("Server" . ,server)
("Arguments" . ,args)))
nil
parent-tool-call-id)))
;; Mark this ID as having received toolCallRun so that any late-arriving
;; toolCallPrepare events (still in-flight for long files) don't overwrite
;; the approval prompt we just rendered. Set this AFTER the pcase dispatch
;; so that if rendering errors, the flag doesn't poison subsequent prepare
;; events (which would prevent the block from ever being created).
(when (and eca-chat-expand-pending-approval-tools manual?)
(when parent-tool-call-id
(eca-chat--expandable-content-toggle parent-tool-call-id t nil))
(eca-chat--expandable-content-toggle id t nil)
(when viewing-bottom?
(eca-chat--ensure-tool-call-approval-visible id)))
;; Update parent subagent status to show pending approval
(when (and manual? parent-tool-call-id)
(eca-chat--update-parent-subagent-status
parent-tool-call-id eca-chat-mcp-tool-call-pending-approval-symbol)))))
("toolCallRunning"
;; Approved (possibly from another client): drop the prompt, the
;; task list follows with toolCalled.
(when (and (eca-chat--task-tool-call-p content)
(equal (plist-get content :id) (eca-chat--task-widget-approval-id)))
(eca-chat--render-task-widget t))
(unless (eca-chat--task-tool-call-p content)
(let* ((id (plist-get content :id))
(args (plist-get content :arguments))
(name (plist-get content :name))
(server (plist-get content :server))
(label (or (plist-get content :summary)
(format "Running tool: %s__%s" server name)))
(details (plist-get content :details))
(status eca-chat-mcp-tool-call-loading-symbol)
(elapsed-time (progn
(eca-chat--tool-call-elapsed-start id)
(eca-chat--elapsed-time-string
(gethash id eca-chat--tool-call-elapsed-times)))))
;; Register subagent mapping only for top-level tool calls
(when (and (not parent-tool-call-id)
(string= "subagent" (plist-get details :type)))
(let ((subagent-chat-id (plist-get details :subagentChatId)))
(unless (gethash subagent-chat-id eca-chat--subagent-chat-id->tool-call-id)
(puthash subagent-chat-id id eca-chat--subagent-chat-id->tool-call-id))))
(pcase (plist-get details :type)
("fileChange" (eca-chat--tool-call-file-change-details content label nil elapsed-time status tool-call-next-line-spacing roots parent-tool-call-id))
("shellCommand" (eca-chat--tool-call-shell-command-details content label nil elapsed-time status parent-tool-call-id nil t))
("subagent" (eca-chat--tool-call-subagent-details id args label nil elapsed-time status parent-tool-call-id details))
(_ (eca-chat--update-expandable-content
id
(concat (propertize label 'font-lock-face 'eca-chat-mcp-tool-call-label-face)
" " status elapsed-time)
(eca-chat--content-table
`(("Tool" . ,name)
("Server" . ,server)
("Arguments" . ,args)))
nil
parent-tool-call-id)))
(eca-chat--mark-tool-call-approval-resolved id)
;; Keep parent pending while sibling approvals remain pending
(eca-chat--restore-parent-subagent-status parent-tool-call-id))))
("toolCalled"
(if (eca-chat--task-tool-call-p content)
(eca-chat--update-task-state content)
(let* ((id (plist-get content :id))
(name (plist-get content :name))
(server (plist-get content :server))
(label (or (plist-get content :summary)
(format "Called tool: %s__%s" server name)))
(args (plist-get content :arguments))
(outputs (plist-get content :outputs))
(output-text (if outputs
(mapconcat (lambda (o) (or (plist-get o :text) "")) outputs "\n")
""))
(details (plist-get content :details))
(time (when-let ((ms (plist-get content :totalTimeMs)))
(concat " " (eca-chat--time->presentable-time ms))))
(bg? (plist-get details :background))
(status (cond
((plist-get content :error)
eca-chat-mcp-tool-call-error-symbol)
(bg? "đĄ")
(t eca-chat-mcp-tool-call-success-symbol))))
;; Cleanup counters for this tool-call id to avoid unbounded growth
(remhash id eca-chat--tool-call-prepare-counters)
(remhash id eca-chat--tool-call-prepare-content-cache)
;; Stop elapsed-time tracking for this tool call
(eca-chat--tool-call-elapsed-stop id)
;; Another client may have answered this ask_user question first,
;; resolving the tool call here; drop our now-stale prompt state.
(eca-chat--dismiss-pending-question-for-tool-call id)
;; Cleanup subagent mapping only for top-level tool calls
(when (and (not parent-tool-call-id)
(string= "subagent" (plist-get details :type)))
(remhash (plist-get details :subagentChatId) eca-chat--subagent-chat-id->tool-call-id))
(pcase (plist-get details :type)
("fileChange" (eca-chat--tool-call-file-change-details content label nil time status tool-call-next-line-spacing roots parent-tool-call-id))
("shellCommand" (eca-chat--tool-call-shell-command-details content label nil time status parent-tool-call-id output-text t))
("jsonOutputs" (eca-chat--tool-call-json-outputs-details content time status parent-tool-call-id))
("subagent" (eca-chat--tool-call-subagent-details id args label nil time status parent-tool-call-id details output-text))
(_ (eca-chat--update-expandable-content
id
(concat (propertize label 'font-lock-face 'eca-chat-mcp-tool-call-label-face)
" " status time)
(eca-chat--content-table
`(("Tool" . ,name)
("Server" . ,server)
("Arguments" . ,args)
("Output" . ,output-text)))
nil
parent-tool-call-id)))
(when eca-chat-shrink-called-tools
(eca-chat--expandable-content-toggle id t t)
(eca-chat--ensure-prompt-visible))
(eca-chat--mark-tool-call-approval-resolved id)
;; Keep parent pending while sibling approvals remain pending
(eca-chat--restore-parent-subagent-status parent-tool-call-id))))
("toolCallRejected"
(when (eca-chat--task-tool-call-p content)
(eca-chat--update-task-state content))
(unless (eca-chat--task-tool-call-p content)
(let* ((name (plist-get content :name))
(server (plist-get content :server))
(label (or (plist-get content :summary)
(format "Rejected tool: %s__%s" server name)))
(args (plist-get content :arguments))
(details (plist-get content :details))
(status eca-chat-mcp-tool-call-error-symbol)
(id (plist-get content :id)))
;; Cleanup counters for this tool-call id
(remhash id eca-chat--tool-call-prepare-counters)
(remhash id eca-chat--tool-call-prepare-content-cache)
;; Another client may have cancelled this ask_user question first,
;; rejecting the tool call here; drop our now-stale prompt state.
(eca-chat--dismiss-pending-question-for-tool-call id)
;; Cleanup subagent mapping only for top-level tool calls
(when (and (not parent-tool-call-id)
(string= "subagent" (plist-get details :type)))
(remhash (plist-get details :subagentChatId) eca-chat--subagent-chat-id->tool-call-id))
(pcase (plist-get details :type)
("fileChange" (eca-chat--tool-call-file-change-details content label nil nil status tool-call-next-line-spacing roots parent-tool-call-id))
("shellCommand" (eca-chat--tool-call-shell-command-details content label nil nil status parent-tool-call-id))
("subagent" (eca-chat--tool-call-subagent-details id args label nil nil status parent-tool-call-id details))
(_ (eca-chat--update-expandable-content
id
(concat (propertize label
'font-lock-face 'eca-chat-mcp-tool-call-label-face)
" "
eca-chat-mcp-tool-call-error-symbol)
(eca-chat--content-table `(("Tool" . ,name)
("Server" . ,server)
("Arguments" . ,args)))
nil
parent-tool-call-id)))
(eca-chat--mark-tool-call-approval-resolved id)
;; Keep parent pending while sibling approvals remain pending
(eca-chat--restore-parent-subagent-status parent-tool-call-id))))
("progress"
(unless parent-tool-call-id
(pcase (plist-get content :state)
("running"
(setq-local eca-chat--progress-text (plist-get content :text))
(unless eca-chat--spinner-timer
(eca-chat--spinner-start
(lambda ()
(eca-chat--refresh-progress chat-buffer))))
(eca-chat--refresh-progress chat-buffer))
("finished"
(pcase eca-chat--chat-loading
('stopping
;; Stopped prompt confirmed â minimal cleanup, no trailing newline
(setq-local eca-chat--progress-text "")
(eca-chat--spinner-stop)
(eca-chat--tool-call-elapsed-stop-all)
(when (timerp eca-chat--fontify-timer)
(cancel-timer eca-chat--fontify-timer)
(setq eca-chat--fontify-timer nil))
;; Region-scoped fontify: only the current turn needs
;; (re)fontification at end-of-stream; full-buffer
;; fontify is O(buffer size) and was the dominant
;; cost on long chats.
(eca-chat--font-lock-ensure
(or eca-chat--last-user-message-pos (point-min))
(point-max))
(eca-chat--refresh-copy-scopes)
(eca-chat--set-chat-loading session nil)
(eca-chat--refresh-progress chat-buffer)
(eca-chat--send-steered-prompt session)
(eca-chat--send-queued-prompt session)
(run-hooks 'eca-chat-finished-hook))
('t
;; Normal completion
(setq-local eca-chat--progress-text "")
(eca-chat--spinner-stop)
(eca-chat--tool-call-elapsed-stop-all)
(eca-chat--add-text-content "\n")
(when (timerp eca-chat--fontify-timer)
(cancel-timer eca-chat--fontify-timer)
(setq eca-chat--fontify-timer nil))
;; Final guaranteed fontify before table alignment so
;; the beautifier sees fully-fontified text. Scoped
;; to the current turn so cost does not grow with
;; chat history; previous turns were already
;; fontified at their own end-of-stream.
(eca-chat--font-lock-ensure
(or eca-chat--last-user-message-pos (point-min))
(point-max))
(eca-chat--refresh-copy-scopes)
;; Table align/beautify default to scanning from
;; `eca-chat--last-user-message-pos' when called with
;; no argument, scoping work to the current turn.
(eca-chat--align-tables)
(eca-chat--beautify-tables)
(eca-chat--set-chat-loading session nil)
(eca-chat--refresh-progress chat-buffer)
(eca-chat--send-steered-prompt session)
(eca-chat--send-queued-prompt session)
(run-hooks 'eca-chat-finished-hook))
(_
;; Idempotent UI cleanup for a `finished' arriving while
;; `chat-loading' is nil â e.g. server-driven progress not
;; started via `eca-chat--send-prompt', or after the 10s
;; stopping safety-timer already cleared the flag. We still
;; clear the visible spinner / progress text and stop the
;; elapsed timers, but intentionally skip the trailing
;; newline, fontify and queued/steered-prompt dispatch so a
;; duplicate `finished' (the case 19aa392 guarded against)
;; still cannot insert a second newline or re-trigger a
;; queued prompt.
(setq-local eca-chat--progress-text "")
(eca-chat--spinner-stop)
(eca-chat--tool-call-elapsed-stop-all)
(eca-chat--refresh-progress chat-buffer)))))))
("usage"
(progn
(if parent-tool-call-id
;; Subagent usage â store and refresh the tool call label
(let ((session-tokens (plist-get content :sessionTokens))
(context-limit (plist-get (plist-get content :limit) :context)))
(puthash parent-tool-call-id
(list :session-tokens session-tokens :context-limit context-limit)
eca-chat--subagent-usage)
(eca-chat--refresh-subagent-usage-label parent-tool-call-id))
(setq-local eca-chat--message-input-tokens (plist-get content :messageInputTokens))
(setq-local eca-chat--message-output-tokens (plist-get content :messageOutputTokens))
(setq-local eca-chat--session-tokens (plist-get content :sessionTokens))
(setq-local eca-chat--session-limit-context (plist-get (plist-get content :limit) :context))
(setq-local eca-chat--session-limit-output (plist-get (plist-get content :limit) :output))
(setq-local eca-chat--message-cost (plist-get content :messageCost))
(setq-local eca-chat--session-cost (plist-get content :sessionCost))
(setq-local eca-chat--session-auto-compact-percentage (plist-get content :autoCompactPercentage))
(setq-local eca-chat--context-breakdown (plist-get content :contextBreakdown)))
(force-mode-line-update)))
(_ nil))
(when approval-acted-on?
(eca-chat--move-on-from-approval))
(eca-chat--mark-response-copy-break
content-type parent-tool-call-id)))