-
Notifications
You must be signed in to change notification settings - Fork 28
Expand file tree
/
Copy pathai-code-backends-infra-vterm.el
More file actions
385 lines (352 loc) · 17.4 KB
/
Copy pathai-code-backends-infra-vterm.el
File metadata and controls
385 lines (352 loc) · 17.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
;;; ai-code-backends-infra-vterm.el --- Vterm support for AI Code terminals -*- lexical-binding: t; -*-
;; Author: Yoav Orot, Kang Tu, Silex, Steve Molitor, AI Agent
;; SPDX-License-Identifier: Apache-2.0
;;; Commentary:
;; Vterm-specific support for `ai-code-backends-infra'.
;;; Code:
(require 'cl-lib)
(require 'ai-code-session-link)
(defcustom ai-code-backends-infra-vterm-anti-flicker t
"Enable intelligent flicker reduction for vterm display."
:type 'boolean
:group 'ai-code-backends-infra)
(defcustom ai-code-backends-infra-vterm-render-delay 0.01
"Rendering optimization delay for batched terminal updates."
:type 'number
:group 'ai-code-backends-infra)
(declare-function ai-code-backends-infra--configure-session-input-shortcuts
"ai-code-backends-infra" ())
(declare-function ai-code-backends-infra--install-navigation-cursor-sync
"ai-code-backends-infra" ())
(declare-function ai-code-backends-infra--note-meaningful-output
"ai-code-backends-infra" ())
(declare-function ai-code-backends-infra--output-meaningful-p
"ai-code-backends-infra" (output))
(declare-function ai-code-backends-infra--session-buffer-p
"ai-code-backends-infra" (buffer))
(declare-function ai-code-backends-infra--set-session-directory
"ai-code-backends-infra" (buffer directory))
(declare-function ai-code-backends-infra--strip-alternate-screen-sequences
"ai-code-backends-infra" (str))
(declare-function ai-code-backends-infra--sync-terminal-cursor
"ai-code-backends-infra" ())
(declare-function ai-code-editor-viewport-filter-output
"ai-code-editor-viewport-transport" (process output))
(declare-function ai-code-notifications-response-ready
"ai-code-notifications" (&optional backend-name))
(declare-function vterm "vterm" (&optional buffer-name))
(declare-function vterm-send-string "vterm" (&rest args))
(declare-function vterm-send-escape "vterm" ())
(declare-function vterm-send-return "vterm" ())
(declare-function vterm--window-adjust-process-window-size "vterm" (&rest args))
(declare-function vterm--filter "vterm" (&rest args))
(defvar ai-code-backends-infra-strip-alternate-screen)
(defvar ai-code-backends-infra--session-terminal-backend)
(defvar vterm-copy-mode)
(defvar vterm-copy-mode-hook)
(defvar vterm-environment)
(defvar vterm-kill-buffer-on-exit)
(defvar vterm-scroll-to-bottom-on-output)
(defvar vterm-shell)
(defvar ai-code-backends-infra--last-meaningful-output-time)
(defvar ai-code-backends-infra--response-seen)
(defvar vterm--redraw-immididately)
(defconst ai-code-backends-infra--vterm-redraw-regexp
"\033\\[[0-9;?]*[A-GJKMH]"
"Regexp to detect ANSI terminal redraw or movement sequences.
Standalone carriage returns are intentionally excluded so simple CR-based
updates are handled separately via carriage return counting.")
(defvar-local ai-code-backends-infra--vterm-render-queue nil)
(defvar-local ai-code-backends-infra--vterm-render-timer nil)
(defvar-local ai-code-backends-infra--vterm-dim-foreground-active nil
"Non-nil when AI Code injected an ANSI gray foreground for SGR dim text.")
(defvar-local ai-code-backends-infra--vterm-foreground-state nil
"Current foreground state tracked for AI Code vterm SGR normalization.
The value is nil for the default foreground, `explicit' for a foreground
set by the subprocess, and `injected' for AI Code's ANSI gray foreground.")
(defvar ai-code-backends-infra--vterm-advices-installed nil
"Flag indicating whether vterm filter advices have been installed globally.")
(defun ai-code-backends-infra-vterm-ensure-backend ()
"Ensure the vterm backend is available."
(unless (featurep 'vterm) (require 'vterm nil t))
(unless (featurep 'vterm)
(user-error "The package vterm is not installed")))
(defun ai-code-backends-infra-vterm-navigation-mode-p ()
"Return non-nil when the current vterm buffer is in copy mode."
(bound-and-true-p vterm-copy-mode))
(defun ai-code-backends-infra-vterm-install-navigation-cursor-sync ()
"Install cursor synchronization for vterm copy mode."
(add-hook 'vterm-copy-mode-hook
#'ai-code-backends-infra--sync-terminal-cursor nil t))
(defun ai-code-backends-infra-vterm-send-string (string &optional paste)
"Send STRING to the current vterm buffer.
If PASTE is non-nil, send it as a pasted string."
(if paste
(vterm-send-string string paste)
(vterm-send-string string)))
(defun ai-code-backends-infra-vterm-send-escape ()
"Send escape to the current vterm buffer."
(vterm-send-escape))
(defun ai-code-backends-infra-vterm-send-return ()
"Send return to the current vterm buffer."
(vterm-send-return))
(defun ai-code-backends-infra-vterm-send-backspace ()
"Send backspace to the current vterm buffer."
(vterm-send-string "\177"))
(defun ai-code-backends-infra-vterm-resize-handler ()
"Return the vterm resize handler."
#'vterm--window-adjust-process-window-size)
(defun ai-code-backends-infra--vterm-skip-sgr-color-params (codes)
"Skip extended SGR color parameters from CODES."
(pcase (car codes)
("5" (nthcdr 2 codes))
("2" (nthcdr 4 codes))
(_ (cdr codes))))
(defun ai-code-backends-infra--vterm-sgr-code-number (code)
"Return the SGR number for CODE, treating an empty parameter as reset."
(string-to-number (if (string= code "") "0" code)))
(defun ai-code-backends-infra--vterm-sgr-effect (codes)
"Return the final foreground and intensity effect of SGR CODES."
(let ((dim nil)
(normal nil)
(explicit-foreground nil)
(foreground-reset nil)
(foreground-state ai-code-backends-infra--vterm-foreground-state)
(remaining codes))
(while remaining
(let* ((code (pop remaining))
(number (ai-code-backends-infra--vterm-sgr-code-number code)))
(cond
((= number 0)
(setq dim nil
normal t
foreground-reset t
foreground-state nil))
((= number 2)
(setq dim t
normal nil))
((= number 22)
(setq dim nil
normal t))
((= number 39)
(setq foreground-reset t
foreground-state nil))
((or (<= 30 number 37)
(<= 90 number 97))
(setq explicit-foreground t
foreground-state 'explicit))
((= number 38)
(setq explicit-foreground t
foreground-state 'explicit)
(setq remaining
(ai-code-backends-infra--vterm-skip-sgr-color-params
remaining)))
((= number 48)
(setq remaining
(ai-code-backends-infra--vterm-skip-sgr-color-params
remaining))))))
(list dim normal explicit-foreground foreground-reset foreground-state)))
(defun ai-code-backends-infra--vterm-normalize-dim-sgr (input)
"Render SGR dim text with the standard ANSI gray foreground in INPUT."
(let ((start 0)
(parts nil))
(while (string-match "\e\\[\\([0-9;]*\\)m" input start)
(let* ((match-beginning (match-beginning 0))
(match-end (match-end 0))
(sequence (match-string 0 input))
(params (match-string 1 input))
(codes (if (string= params "") '("0") (split-string params ";")))
(was-injected
(eq ai-code-backends-infra--vterm-foreground-state 'injected)))
(cl-destructuring-bind
(dim normal explicit-foreground foreground-reset foreground-state)
(ai-code-backends-infra--vterm-sgr-effect codes)
(push (substring input start match-beginning) parts)
(push
(cond
((and dim (null foreground-state))
(setq ai-code-backends-infra--vterm-foreground-state 'injected
ai-code-backends-infra--vterm-dim-foreground-active t)
(format "\e[%sm" (mapconcat #'identity (append codes '("90")) ";")))
((and normal
was-injected
(not foreground-reset)
(not explicit-foreground))
(setq ai-code-backends-infra--vterm-foreground-state nil
ai-code-backends-infra--vterm-dim-foreground-active nil)
(format "\e[%sm" (mapconcat #'identity (append codes '("39")) ";")))
(t
(setq ai-code-backends-infra--vterm-foreground-state foreground-state
ai-code-backends-infra--vterm-dim-foreground-active
(eq foreground-state 'injected))
sequence))
parts)
(setq start match-end))))
(push (substring input start) parts)
(apply #'concat (nreverse parts))))
(defun ai-code-backends-infra--vterm-notification-tracker (orig-fun process input)
"Track vterm activity for notification purposes, then call ORIG-FUN.
When `ai-code-backends-infra-strip-alternate-screen' is non-nil,
strip alternate screen buffer sequences from INPUT for PROCESS so that TUI
applications write to the normal screen buffer (preserving scrollback)."
(let* ((session-p
(ai-code-backends-infra--session-buffer-p (process-buffer process)))
(editor-filtered-input
(if session-p
(ai-code-editor-viewport-filter-output process input)
input))
(filtered-input
(if session-p
(with-current-buffer (process-buffer process)
(ai-code-backends-infra--vterm-normalize-dim-sgr
(ai-code-backends-infra--strip-alternate-screen-sequences
editor-filtered-input)))
editor-filtered-input)))
(when session-p
(with-current-buffer (process-buffer process)
(when (ai-code-backends-infra--output-meaningful-p filtered-input)
(ai-code-backends-infra--note-meaningful-output))))
(prog1
(funcall orig-fun process filtered-input)
(when session-p
(ai-code-session-link--schedule-linkify-recent-output
(process-buffer process)
filtered-input)))))
(defun ai-code-backends-infra--vterm-render-preserving-copy-mode-view (render-fn)
"Call RENDER-FN while keeping the user's `vterm-copy-mode' viewport stable."
(if (not (bound-and-true-p vterm-copy-mode))
(funcall render-fn)
(let ((point-marker (copy-marker (point) t))
(window-states
(mapcar (lambda (window)
(list window
(copy-marker (window-start window) t)
(copy-marker (window-point window) t)))
(get-buffer-window-list (current-buffer) nil t))))
(unwind-protect
(let ((inhibit-redisplay t))
(funcall render-fn))
(dolist (state window-states)
(pcase-let ((`(,window ,start-marker ,window-point-marker) state))
(when (window-live-p window)
(set-window-start window start-marker t)
(set-window-point window window-point-marker))
(set-marker start-marker nil)
(set-marker window-point-marker nil)))
(goto-char point-marker)
(set-marker point-marker nil)))))
(defun ai-code-backends-infra--vterm-render-queued-output (orig-fun buffer)
"Render queued vterm output for BUFFER using ORIG-FUN."
(when (buffer-live-p buffer)
(with-current-buffer buffer
(setq ai-code-backends-infra--vterm-render-timer nil)
(when ai-code-backends-infra--vterm-render-queue
(let ((data ai-code-backends-infra--vterm-render-queue))
(setq ai-code-backends-infra--vterm-render-queue nil)
(when-let* ((process (get-buffer-process buffer))
((process-live-p process)))
(ai-code-backends-infra--vterm-render-preserving-copy-mode-view
(lambda ()
(funcall orig-fun process data)))))))))
(defun ai-code-backends-infra-vterm-flush-render-queue (&optional buffer)
"Immediately render delayed vterm output queued for BUFFER.
When BUFFER is nil, flush the queue for the current buffer."
(when (or (null buffer) (buffer-live-p buffer))
(with-current-buffer (or buffer (current-buffer))
(when ai-code-backends-infra--vterm-render-timer
(cancel-timer ai-code-backends-infra--vterm-render-timer))
(setq ai-code-backends-infra--vterm-render-timer nil)
(when ai-code-backends-infra--vterm-render-queue
(let ((data ai-code-backends-infra--vterm-render-queue))
(setq ai-code-backends-infra--vterm-render-queue nil)
(when-let* ((process (get-buffer-process (current-buffer)))
((process-live-p process)))
(let ((ai-code-backends-infra-vterm-anti-flicker nil))
(ai-code-backends-infra--vterm-render-preserving-copy-mode-view
(lambda ()
(vterm--filter process data))))))))))
(defun ai-code-backends-infra--vterm-smart-renderer (orig-fun process input)
"Update vterm display via smart rendering around ORIG-FUN.
Activity tracking for notifications is handled separately by
`ai-code-backends-infra--vterm-notification-tracker'.
When PROCESS receives INPUT in `vterm-copy-mode', rendering preserves
the current viewport so scrollback continues updating without yanking
navigation."
(if (or (not ai-code-backends-infra-vterm-anti-flicker)
(not (ai-code-backends-infra--session-buffer-p (process-buffer process))))
(funcall orig-fun process input)
(with-current-buffer (process-buffer process)
(let* ((complex-redraw-detected
(string-match-p ai-code-backends-infra--vterm-redraw-regexp input))
(clear-count (1- (length (split-string input "\033\\[K"))))
(cr-count (cl-count ?\15 input))
(escape-count (cl-count ?\033 input))
(input-length (length input))
(escape-density (if (> input-length 0)
(/ (float escape-count) input-length)
0)))
(if (or complex-redraw-detected
(>= cr-count 2)
(and (> escape-density 0.3) (>= clear-count 2))
ai-code-backends-infra--vterm-render-queue)
(let ((buffer (current-buffer)))
(setq ai-code-backends-infra--vterm-render-queue
(concat ai-code-backends-infra--vterm-render-queue input))
(when ai-code-backends-infra--vterm-render-timer
(cancel-timer ai-code-backends-infra--vterm-render-timer))
(setq ai-code-backends-infra--vterm-render-timer
(run-at-time ai-code-backends-infra-vterm-render-delay nil
#'ai-code-backends-infra--vterm-render-queued-output
orig-fun
buffer)))
(ai-code-backends-infra--vterm-render-preserving-copy-mode-view
(lambda ()
(funcall orig-fun process input))))))))
(defun ai-code-backends-infra--vterm-flush-on-copy-mode-exit ()
"Flush any pending render queue when exiting `vterm-copy-mode'.
Added buffer-locally to `vterm-copy-mode-hook' so that terminal output
queued while copy mode was active is rendered immediately when the user
returns to normal terminal interaction."
(unless (bound-and-true-p vterm-copy-mode)
(when ai-code-backends-infra--vterm-render-queue
(when-let* ((proc (get-buffer-process (current-buffer))))
(let ((data ai-code-backends-infra--vterm-render-queue))
(setq ai-code-backends-infra--vterm-render-queue nil)
(vterm--filter proc data))))))
(defun ai-code-backends-infra--configure-vterm-buffer ()
"Configure vterm for enhanced performance."
(setq-local vterm-scroll-to-bottom-on-output nil)
(when (boundp 'vterm--redraw-immididately)
(setq-local vterm--redraw-immididately nil))
(ai-code-backends-infra--configure-session-input-shortcuts)
(setq-local cursor-in-non-selected-windows nil)
(setq-local blink-cursor-mode nil)
(setq-local cursor-type nil)
(when-let* ((proc (get-buffer-process (current-buffer))))
(set-process-query-on-exit-flag proc nil)
(when (fboundp 'process-put)
(process-put proc 'read-output-max 4096)))
(add-hook 'vterm-copy-mode-hook
#'ai-code-backends-infra--vterm-flush-on-copy-mode-exit nil t)
(ai-code-backends-infra--install-navigation-cursor-sync)
(unless ai-code-backends-infra--vterm-advices-installed
(advice-add 'vterm--filter :around #'ai-code-backends-infra--vterm-notification-tracker)
(when ai-code-backends-infra-vterm-anti-flicker
(advice-add 'vterm--filter :around #'ai-code-backends-infra--vterm-smart-renderer))
(setq ai-code-backends-infra--vterm-advices-installed t)))
(defun ai-code-backends-infra-vterm-create-session (buffer-name working-dir command env-vars)
"Create a vterm session named BUFFER-NAME in WORKING-DIR.
COMMAND is the shell command to run and ENV-VARS are extra environment
variables for the terminal process."
(let ((default-directory working-dir)
(vterm-shell command)
(vterm-kill-buffer-on-exit nil)
(vterm-environment (append env-vars (bound-and-true-p vterm-environment))))
(let ((buffer (save-window-excursion (vterm buffer-name))))
(ai-code-backends-infra--set-session-directory buffer working-dir)
(with-current-buffer buffer
(setq-local ai-code-backends-infra--session-terminal-backend 'vterm)
(ai-code-backends-infra--configure-vterm-buffer))
(cons buffer (get-buffer-process buffer)))))
(provide 'ai-code-backends-infra-vterm)
;;; ai-code-backends-infra-vterm.el ends here