Skip to content

Commit 03d91d6

Browse files
authored
Fix vterm resize rendering for AI sessions (#352)
Avoid installing the height-only reflow advice for vterm, and route vterm dimension sync through its native resize handler. Flush queued vterm redraw output during resize instead of dropping it, so Codex-style TUIs keep their terminal state consistent without losing pending output. Add regression coverage for vterm resize behavior and stale advice cleanup.
1 parent 99f2cf4 commit 03d91d6

3 files changed

Lines changed: 181 additions & 19 deletions

File tree

ai-code-backends-infra-vterm.el

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,24 @@ applications write to the normal screen buffer (preserving scrollback)."
164164
(lambda ()
165165
(funcall orig-fun process data)))))))))
166166

167+
(defun ai-code-backends-infra-vterm-flush-render-queue (&optional buffer)
168+
"Immediately render delayed vterm output queued for BUFFER.
169+
When BUFFER is nil, flush the queue for the current buffer."
170+
(when (or (null buffer) (buffer-live-p buffer))
171+
(with-current-buffer (or buffer (current-buffer))
172+
(when ai-code-backends-infra--vterm-render-timer
173+
(cancel-timer ai-code-backends-infra--vterm-render-timer))
174+
(setq ai-code-backends-infra--vterm-render-timer nil)
175+
(when ai-code-backends-infra--vterm-render-queue
176+
(let ((data ai-code-backends-infra--vterm-render-queue))
177+
(setq ai-code-backends-infra--vterm-render-queue nil)
178+
(when-let* ((process (get-buffer-process (current-buffer)))
179+
((process-live-p process)))
180+
(let ((ai-code-backends-infra-vterm-anti-flicker nil))
181+
(ai-code-backends-infra--vterm-render-preserving-copy-mode-view
182+
(lambda ()
183+
(vterm--filter process data))))))))))
184+
167185
(defun ai-code-backends-infra--vterm-smart-renderer (orig-fun process input)
168186
"Update vterm display via smart rendering around ORIG-FUN.
169187
Activity tracking for notifications is handled separately by

ai-code-backends-infra.el

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -486,9 +486,10 @@ MULTILINE-INPUT-SEQUENCE configures `S-<return>' and `C-<return>' when non-nil."
486486

487487
;;; Reflow and Window Management
488488

489-
(defun ai-code-backends-infra--terminal-resize-handler ()
490-
"Retrieve the terminal's resize handling function based on backend."
491-
(pcase ai-code-backends-infra-terminal-backend
489+
(defun ai-code-backends-infra--terminal-resize-handler (&optional backend)
490+
"Retrieve the resize handling function for BACKEND.
491+
When BACKEND is nil, use `ai-code-backends-infra-terminal-backend'."
492+
(pcase (or backend ai-code-backends-infra-terminal-backend)
492493
('vterm (ai-code-backends-infra-vterm-resize-handler))
493494
('eat (ai-code-backends-infra-eat-resize-handler))
494495
('ghostel (ai-code-backends-infra-ghostel-resize-handler))
@@ -502,7 +503,8 @@ MULTILINE-INPUT-SEQUENCE configures `S-<return>' and `C-<return>' when non-nil."
502503
(defun ai-code-backends-infra--terminal-reflow-filter (original-fn &rest args)
503504
"Filter terminal reflows to prevent height-only resize triggers.
504505
Suppress reflow when terminal width is unchanged or when the session
505-
buffer is in scroll/copy mode, working around bug #1422."
506+
buffer is in scroll/copy mode, working around bug #1422.
507+
ORIGINAL-FN and ARGS are the resize handler and arguments."
506508
(let* ((base-result (apply original-fn args))
507509
(dimensions-stable t))
508510
(dolist (win (window-list))
@@ -533,7 +535,7 @@ buffer is in scroll/copy mode, working around bug #1422."
533535
"Add or remove terminal reflow advice according to current settings."
534536
(let* ((resize-handler (ai-code-backends-infra--terminal-resize-handler))
535537
(enabled (and ai-code-backends-infra-prevent-reflow-glitch
536-
(memq ai-code-backends-infra-terminal-backend '(vterm eat)))))
538+
(eq ai-code-backends-infra-terminal-backend 'eat))))
537539
(dolist (handler (cl-copy-list ai-code-backends-infra--reflow-advised-handlers))
538540
(unless (and enabled (eq handler resize-handler))
539541
(when (advice-member-p #'ai-code-backends-infra--terminal-reflow-filter
@@ -542,6 +544,13 @@ buffer is in scroll/copy mode, working around bug #1422."
542544
#'ai-code-backends-infra--terminal-reflow-filter))
543545
(setq ai-code-backends-infra--reflow-advised-handlers
544546
(delq handler ai-code-backends-infra--reflow-advised-handlers))))
547+
(unless enabled
548+
(when (advice-member-p #'ai-code-backends-infra--terminal-reflow-filter
549+
resize-handler)
550+
(advice-remove resize-handler
551+
#'ai-code-backends-infra--terminal-reflow-filter))
552+
(setq ai-code-backends-infra--reflow-advised-handlers
553+
(delq resize-handler ai-code-backends-infra--reflow-advised-handlers)))
545554
(when (and enabled resize-handler)
546555
(unless (advice-member-p #'ai-code-backends-infra--terminal-reflow-filter
547556
resize-handler)
@@ -594,9 +603,22 @@ from the window where it was initially created."
594603
(when (and buffer window (buffer-live-p buffer) (window-live-p window))
595604
(with-current-buffer buffer
596605
(when-let ((proc (get-buffer-process buffer)))
597-
(let ((height (window-body-height window))
598-
(width (window-body-width window)))
599-
(set-process-window-size proc height width))))))
606+
(let ((backend (ai-code-backends-infra--current-terminal-backend))
607+
(windows (or (get-buffer-window-list buffer nil t)
608+
(list window))))
609+
(pcase backend
610+
('vterm
611+
(let ((result
612+
(funcall (ai-code-backends-infra--terminal-resize-handler
613+
'vterm)
614+
proc windows)))
615+
(when result
616+
(ai-code-backends-infra-vterm-flush-render-queue buffer))
617+
result))
618+
(_
619+
(set-process-window-size proc
620+
(window-body-height window)
621+
(window-body-width window)))))))))
600622

601623
;;; Session Helpers
602624

test/test_ai-code-backends-infra.el

Lines changed: 133 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -255,19 +255,22 @@
255255
(should scheduled)))))
256256

257257
(ert-deftest test-ai-code-backends-infra-sync-reflow-filter-advice-vterm ()
258-
"Enable and disable reflow advice for vterm according to toggle."
259-
(let ((handler 'ai-code-backends-infra--test-resize-vterm))
258+
"Do not install height-only reflow advice for vterm."
259+
(let ((handler 'ai-code-backends-infra--test-resize-vterm)
260+
(ai-code-backends-infra--reflow-advised-handlers nil))
260261
(fset handler (lambda (&rest args) args))
261262
(unwind-protect
262263
(cl-letf (((symbol-function 'ai-code-backends-infra--terminal-resize-handler)
263264
(lambda () handler)))
264265
(let ((ai-code-backends-infra-terminal-backend 'vterm)
265266
(ai-code-backends-infra-prevent-reflow-glitch t))
266267
(ai-code-backends-infra--sync-reflow-filter-advice)
267-
(should (advice-member-p #'ai-code-backends-infra--terminal-reflow-filter
268-
handler)))
268+
(should-not (advice-member-p #'ai-code-backends-infra--terminal-reflow-filter
269+
handler)))
270+
(advice-add handler :around #'ai-code-backends-infra--terminal-reflow-filter)
271+
(setq ai-code-backends-infra--reflow-advised-handlers nil)
269272
(let ((ai-code-backends-infra-terminal-backend 'vterm)
270-
(ai-code-backends-infra-prevent-reflow-glitch nil))
273+
(ai-code-backends-infra-prevent-reflow-glitch t))
271274
(ai-code-backends-infra--sync-reflow-filter-advice)
272275
(should-not (advice-member-p #'ai-code-backends-infra--terminal-reflow-filter
273276
handler))))
@@ -317,17 +320,16 @@
317320
('vterm vterm-handler)
318321
('eat eat-handler)
319322
(_ (error "Unexpected backend"))))))
320-
(let ((ai-code-backends-infra-terminal-backend 'vterm)
321-
(ai-code-backends-infra-prevent-reflow-glitch t))
322-
(ai-code-backends-infra--sync-reflow-filter-advice)
323-
(should (advice-member-p #'ai-code-backends-infra--terminal-reflow-filter
324-
vterm-handler)))
323+
(advice-add vterm-handler :around #'ai-code-backends-infra--terminal-reflow-filter)
324+
(setq ai-code-backends-infra--reflow-advised-handlers (list vterm-handler))
325325
(let ((ai-code-backends-infra-terminal-backend 'eat)
326326
(ai-code-backends-infra-prevent-reflow-glitch t)
327327
(ai-code-backends-infra-eat-preserve-position nil))
328328
(ai-code-backends-infra--sync-reflow-filter-advice))
329329
(should-not (advice-member-p #'ai-code-backends-infra--terminal-reflow-filter
330-
vterm-handler)))
330+
vterm-handler))
331+
(should (advice-member-p #'ai-code-backends-infra--terminal-reflow-filter
332+
eat-handler)))
331333
(dolist (handler (list vterm-handler eat-handler))
332334
(when (advice-member-p #'ai-code-backends-infra--terminal-reflow-filter handler)
333335
(advice-remove handler #'ai-code-backends-infra--terminal-reflow-filter))
@@ -357,6 +359,126 @@
357359
(funcall (cdr (assq 'window-width captured-entry)) 'fake-window)
358360
(should (equal resize-call '(fake-window 4 t)))))))
359361

362+
(ert-deftest test-ai-code-backends-infra-terminal-reflow-filter-ignores-non-ai-vterm-buffer ()
363+
"The reflow filter should pass through non-session vterm buffers."
364+
(with-temp-buffer
365+
(rename-buffer "*vterm*" t)
366+
(let ((ai-code-backends-infra-terminal-backend 'vterm)
367+
(ai-code-backends-infra-prevent-reflow-glitch t)
368+
(original-called nil))
369+
(cl-letf (((symbol-function 'window-list)
370+
(lambda (&rest _args) (list 'fake-window)))
371+
((symbol-function 'window-buffer)
372+
(lambda (_window) (current-buffer))))
373+
(should (eq (ai-code-backends-infra--terminal-reflow-filter
374+
(lambda (&rest _args)
375+
(setq original-called t)
376+
'native-result)
377+
'fake-arg)
378+
'native-result))
379+
(should original-called)))))
380+
381+
(ert-deftest test-ai-code-backends-infra-sync-terminal-dimensions-vterm-height-change ()
382+
"Vterm height changes should go through the native resize handler."
383+
(let* ((buffer (get-buffer-create "*codex[vterm-height-resize]*"))
384+
(process (start-process "ai-code-vterm-resize-test"
385+
buffer
386+
"sleep"
387+
"5"))
388+
(window 'fake-window)
389+
(height 24)
390+
(width 80)
391+
resize-calls
392+
rendered
393+
cancelled-timer)
394+
(unwind-protect
395+
(cl-letf (((symbol-function 'window-live-p)
396+
(lambda (candidate) (eq candidate window)))
397+
((symbol-function 'get-buffer-window-list)
398+
(lambda (_buffer &rest _args) (list window)))
399+
((symbol-function 'window-body-height)
400+
(lambda (_window) height))
401+
((symbol-function 'window-body-width)
402+
(lambda (_window) width))
403+
((symbol-function 'set-process-window-size)
404+
(lambda (&rest _args)
405+
(ert-fail "vterm sync should use the native resize handler")))
406+
((symbol-function 'ai-code-backends-infra--terminal-resize-handler)
407+
(lambda (&optional backend)
408+
(should (eq backend 'vterm))
409+
(lambda (proc windows)
410+
(push (list proc windows height width) resize-calls)
411+
(cons width height))))
412+
((symbol-function 'cancel-timer)
413+
(lambda (timer) (setq cancelled-timer timer)))
414+
((symbol-function 'vterm--filter)
415+
(lambda (proc data)
416+
(push (list proc data ai-code-backends-infra-vterm-anti-flicker)
417+
rendered))))
418+
(with-current-buffer buffer
419+
(setq-local ai-code-backends-infra--session-terminal-backend 'vterm)
420+
(setq-local ai-code-backends-infra--vterm-render-timer 'mock-timer)
421+
(setq-local ai-code-backends-infra--vterm-render-queue "old-redraw-1")
422+
(ai-code-backends-infra--sync-terminal-dimensions buffer window)
423+
(setq height 18)
424+
(setq-local ai-code-backends-infra--vterm-render-queue "old-redraw-2")
425+
(ai-code-backends-infra--sync-terminal-dimensions buffer window)
426+
(should (null ai-code-backends-infra--vterm-render-timer))
427+
(should-not ai-code-backends-infra--vterm-render-queue))
428+
(should (eq cancelled-timer 'mock-timer))
429+
(should (equal (nreverse resize-calls)
430+
`((,process (,window) 24 80)
431+
(,process (,window) 18 80)))))
432+
(should (equal (nreverse rendered)
433+
`((,process "old-redraw-1" nil)
434+
(,process "old-redraw-2" nil))))
435+
(when (process-live-p process)
436+
(delete-process process))
437+
(when (buffer-live-p buffer)
438+
(kill-buffer buffer)))))
439+
440+
(ert-deftest test-ai-code-backends-infra-sync-terminal-dimensions-vterm-width-change ()
441+
"Vterm width changes should go through the native resize handler."
442+
(let* ((buffer (get-buffer-create "*codex[vterm-width-resize]*"))
443+
(process (start-process "ai-code-vterm-width-resize-test"
444+
buffer
445+
"sleep"
446+
"5"))
447+
(window 'fake-window)
448+
(height 24)
449+
(width 80)
450+
calls)
451+
(unwind-protect
452+
(cl-letf (((symbol-function 'window-live-p)
453+
(lambda (candidate) (eq candidate window)))
454+
((symbol-function 'get-buffer-window-list)
455+
(lambda (_buffer &rest _args) (list window)))
456+
((symbol-function 'window-body-height)
457+
(lambda (_window) height))
458+
((symbol-function 'window-body-width)
459+
(lambda (_window) width))
460+
((symbol-function 'set-process-window-size)
461+
(lambda (&rest _args)
462+
(ert-fail "vterm sync should not call generic process sizing")))
463+
((symbol-function 'ai-code-backends-infra--terminal-resize-handler)
464+
(lambda (&optional backend)
465+
(should (eq backend 'vterm))
466+
(lambda (proc windows)
467+
(push (list proc windows height width) calls)
468+
(cons width height)))))
469+
(with-current-buffer buffer
470+
(setq-local ai-code-backends-infra--session-terminal-backend 'vterm)
471+
(ai-code-backends-infra--sync-terminal-dimensions buffer window)
472+
(setq width 100)
473+
(ai-code-backends-infra--sync-terminal-dimensions buffer window))
474+
(should (equal (nreverse calls)
475+
`((,process (,window) 24 80)
476+
(,process (,window) 24 100)))))
477+
(when (process-live-p process)
478+
(delete-process process))
479+
(when (buffer-live-p buffer)
480+
(kill-buffer buffer)))))
481+
360482
(ert-deftest test-ai-code-backends-infra-sync-terminal-cursor-vterm-copy-mode ()
361483
"Show an Emacs cursor in vterm copy mode and restore terminal cursor on exit."
362484
(with-temp-buffer

0 commit comments

Comments
 (0)