Skip to content

Commit b807af0

Browse files
authored
Fix: vterm flickered a lot when using gemini cli as backend (#272)
* Improve Vterm rendering detection and add test * Add no-flicker option and pass env to Claude Code * add contributor * Refine vterm redraw regex and add CRLF test
1 parent 9c54616 commit b807af0

4 files changed

Lines changed: 96 additions & 7 deletions

File tree

ai-code-backends-infra.el

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ Can be either `vterm' or `eat'."
8686
:type 'boolean
8787
:group 'ai-code-backends-infra)
8888

89-
(defcustom ai-code-backends-infra-vterm-render-delay 0.005
89+
(defcustom ai-code-backends-infra-vterm-render-delay 0.01
9090
"Rendering optimization delay for batched terminal updates."
9191
:type 'number
9292
:group 'ai-code-backends-infra)
@@ -167,6 +167,12 @@ if the AI session buffer is not currently visible."
167167

168168
;;; Vterm Rendering Optimization
169169

170+
(defconst ai-code-backends-infra--vterm-redraw-regexp
171+
"\033\\[[0-9;?]*[A-GJKMH]"
172+
"Regexp to detect ANSI terminal redraw or movement sequences.
173+
Standalone carriage returns are intentionally excluded so simple CR-based
174+
updates are handled separately via carriage return counting.")
175+
170176
(defvar-local ai-code-backends-infra--vterm-render-queue nil)
171177
(defvar-local ai-code-backends-infra--vterm-render-timer nil)
172178

@@ -263,12 +269,14 @@ scrolling and copying are not disrupted by timer-driven redraws."
263269
(funcall orig-fun process input)
264270
(with-current-buffer (process-buffer process)
265271
(let* ((complex-redraw-detected
266-
(string-match-p "\033\\[[0-9]*A.*\033\\[K.*\033\\[[0-9]*A.*\033\\[K" input))
272+
(string-match-p ai-code-backends-infra--vterm-redraw-regexp input))
267273
(clear-count (1- (length (split-string input "\033\\[K"))))
274+
(cr-count (cl-count ?\15 input))
268275
(escape-count (cl-count ?\033 input))
269276
(input-length (length input))
270277
(escape-density (if (> input-length 0) (/ (float escape-count) input-length) 0)))
271278
(if (or complex-redraw-detected
279+
(>= cr-count 2)
272280
(and (> escape-density 0.3) (>= clear-count 2))
273281
ai-code-backends-infra--vterm-render-queue
274282
(bound-and-true-p vterm-copy-mode))
@@ -946,8 +954,8 @@ CLEANUP-FN is called with no arguments when the process exits.
946954
INSTANCE-NAME overrides instance selection when non-nil.
947955
PREFIX enables instance selection when BUFFER-NAME is nil.
948956
When FORCE-PROMPT is non-nil, always prompt for a new instance name.
949-
ENV-VARS is a list of additional environment variable strings (e.g., \"VAR=value\")
950-
passed to the terminal session on creation.
957+
ENV-VARS is a list of additional environment variable strings, for example
958+
\"VAR=value\", passed to the terminal session on creation.
951959
MULTILINE-INPUT-SEQUENCE configures `S-<return>' and `C-<return>' to send
952960
that sequence inside the session buffer.
953961
POST-START-FN is called with (BUFFER PROCESS INSTANCE-NAME) after a new

ai-code-claude-code.el

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
;;; ai-code-claude-code.el --- Thin wrapper for Claude Code CLI -*- lexical-binding: t; -*-
22

3-
;; Author: Kang Tu <tninja@gmail.com>
3+
;; Author: Kang Tu, Yoav Orot
44
;; SPDX-License-Identifier: Apache-2.0
55

66
;;; Commentary:
@@ -31,6 +31,11 @@
3131
:type '(repeat string)
3232
:group 'ai-code-claude-code)
3333

34+
(defcustom ai-code-claude-code-no-flicker t
35+
"Enable experimental flicker-free terminal renderer in Claude Code."
36+
:type 'boolean
37+
:group 'ai-code-claude-code)
38+
3439
(defcustom ai-code-claude-code-multiline-input-sequence "\e\r"
3540
"Terminal sequence used for multiline input in Claude Code sessions.
3641
This mirrors the newline sequence Claude Code expects from `/terminal-setup'."
@@ -59,7 +64,9 @@ With prefix ARG, prompt for CLI args using
5964
(mcp-launch (ai-code-mcp-agent-prepare-launch 'claude-code working-dir command))
6065
(launch-command (or (plist-get mcp-launch :command) command))
6166
(cleanup-fn (plist-get mcp-launch :cleanup-fn))
62-
(post-start-fn (plist-get mcp-launch :post-start-fn)))
67+
(post-start-fn (plist-get mcp-launch :post-start-fn))
68+
(env-vars (list (format "CLAUDE_CODE_NO_FLICKER=%s"
69+
(if ai-code-claude-code-no-flicker "1" "0")))))
6370
(ai-code-backends-infra--toggle-or-create-session
6471
working-dir
6572
nil
@@ -70,7 +77,7 @@ With prefix ARG, prompt for CLI args using
7077
nil
7178
ai-code-claude-code--session-prefix
7279
nil
73-
nil
80+
env-vars
7481
ai-code-claude-code-multiline-input-sequence
7582
post-start-fn)))
7683

test/test_ai-code-backends-infra.el

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
(require 'ai-code-backends-infra)
1414
(require 'ai-code-notifications)
1515

16+
(defvar vterm-copy-mode-hook)
17+
1618
(ert-deftest test-ai-code-backends-infra-output-meaningful-p-noise ()
1719
"Ensure terminal noise is not considered meaningful output."
1820
(should-not (ai-code-backends-infra--output-meaningful-p nil))
@@ -1246,6 +1248,56 @@
12461248
(when (buffer-live-p buffer)
12471249
(kill-buffer buffer)))))
12481250

1251+
(ert-deftest test-ai-code-backends-infra-vterm-smart-renderer-queues-on-carriage-return ()
1252+
"Incoming vterm data is queued when it contains multiple carriage returns."
1253+
(with-temp-buffer
1254+
(rename-buffer "*testgemini[test-dir]*" t)
1255+
(setq-local ai-code-backends-infra--vterm-render-queue nil)
1256+
(setq-local ai-code-backends-infra--vterm-render-timer nil)
1257+
(setq-local vterm-copy-mode nil)
1258+
(let* ((rendered nil)
1259+
(orig-fun (lambda (_process input) (push input rendered)))
1260+
(mock-process 'mock-proc))
1261+
(cl-letf (((symbol-function 'process-buffer)
1262+
(lambda (_proc) (current-buffer)))
1263+
((symbol-function 'run-at-time)
1264+
(lambda (&rest _args) 'mock-timer))
1265+
((symbol-function 'cancel-timer)
1266+
(lambda (&rest _args) nil)))
1267+
;; Send input with multiple \r (common in TUI progress bars/updates).
1268+
(ai-code-backends-infra--vterm-smart-renderer
1269+
orig-fun mock-process "Loading... 10%\rLoading... 20%\r")
1270+
;; It should NOT be rendered immediately.
1271+
(should (null rendered))
1272+
;; It should be in the queue.
1273+
(should (equal ai-code-backends-infra--vterm-render-queue "Loading... 10%\rLoading... 20%\r"))))))
1274+
1275+
(ert-deftest test-ai-code-backends-infra-vterm-smart-renderer-allows-crlf-pass-through ()
1276+
"Simple CRLF output should render immediately instead of being queued."
1277+
(with-temp-buffer
1278+
(rename-buffer "*testgemini[test-crlf]*" t)
1279+
(setq-local ai-code-backends-infra--vterm-render-queue nil)
1280+
(setq-local ai-code-backends-infra--vterm-render-timer nil)
1281+
(setq-local vterm-copy-mode nil)
1282+
(let* ((rendered nil)
1283+
(timer-scheduled nil)
1284+
(orig-fun (lambda (_process input) (push input rendered)))
1285+
(mock-process 'mock-proc))
1286+
(cl-letf (((symbol-function 'process-buffer)
1287+
(lambda (_proc) (current-buffer)))
1288+
((symbol-function 'run-at-time)
1289+
(lambda (&rest _args)
1290+
(setq timer-scheduled t)
1291+
'mock-timer))
1292+
((symbol-function 'cancel-timer)
1293+
(lambda (&rest _args) nil)))
1294+
(ai-code-backends-infra--vterm-smart-renderer
1295+
orig-fun mock-process "hello\r\n")
1296+
(should (equal rendered '("hello\r\n")))
1297+
(should-not timer-scheduled)
1298+
(should-not ai-code-backends-infra--vterm-render-queue)
1299+
(should-not ai-code-backends-infra--vterm-render-timer)))))
1300+
12491301
(provide 'test_ai-code-backends-infra)
12501302

12511303
;;; test_ai-code-backends-infra.el ends here

test/test_ai-code-claude-code.el

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,28 @@
4040
(ai-code-claude-code)
4141
(should (equal captured-sequence "\e\r"))))))
4242

43+
(ert-deftest ai-code-test-claude-code-start-passes-no-flicker-env ()
44+
"Starting Claude Code should pass CLAUDE_CODE_NO_FLICKER env var based on config."
45+
(let ((captured-env-vars :unset))
46+
(cl-letf (((symbol-function 'ai-code-backends-infra--session-working-directory)
47+
(lambda () "/tmp/test-claude"))
48+
((symbol-function 'ai-code-backends-infra--resolve-start-command)
49+
(lambda (&rest _args)
50+
(list :command "claude")))
51+
((symbol-function 'ai-code-mcp-agent-prepare-launch)
52+
(lambda (&rest _args) nil))
53+
((symbol-function 'ai-code-backends-infra--toggle-or-create-session)
54+
(lambda (&rest args)
55+
(let ((env-vars (nth 9 args)))
56+
(setq captured-env-vars env-vars))
57+
nil)))
58+
(let ((ai-code-claude-code-no-flicker t))
59+
(ai-code-claude-code)
60+
(should (member "CLAUDE_CODE_NO_FLICKER=1" captured-env-vars)))
61+
(let ((ai-code-claude-code-no-flicker nil))
62+
(ai-code-claude-code)
63+
(should (member "CLAUDE_CODE_NO_FLICKER=0" captured-env-vars))))))
64+
4365
(provide 'test_ai-code-claude-code)
4466

4567
;;; test_ai-code-claude-code.el ends here

0 commit comments

Comments
 (0)