{{ message }}
Repository navigation
Expand file tree
/
Copy pathai-code-backends-infra.el
More file actions
370 lines (321 loc) · 16 KB
/
Copy pathai-code-backends-infra.el
File metadata and controls
370 lines (321 loc) · 16 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
;;; ai-code-backends-infra.el --- Infrastructure for AI Code Terminals -*- lexical-binding: t; -*-
;; Author: Yoav Orot, Kang Tu, AI Agent
;; SPDX-License-Identifier: Apache-2.0
;; Keywords: ai, terminal, vterm, eat
;;; Commentary:
;; This library provides common infrastructure for AI-powered terminal interfaces,
;; including terminal backend abstraction (vterm/eat), window management,
;; and performance optimizations like anti-flicker and reflow glitch prevention.
;; Code was generated by AI Agent, using
;; https://github.com/manzaltu/claude-code-ide.el as reference
;;; Code:
(require 'cl-lib)
(require 'project)
;; Silence native-compiler warnings.
(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))
(declare-function eat-term-send-string "eat" (&rest args))
(declare-function eat--adjust-process-window-size "eat" (&rest args))
(declare-function eat-mode "eat" ())
(declare-function eat-exec "eat" (&rest args))
;; Declare vterm dynamic variables for let-binding to work with lexical-binding
(defvar vterm-shell)
(defvar vterm-environment)
;;; Customization
(defgroup ai-code-backends-infra nil
"Infrastructure for AI Code terminals."
:group 'tools)
(defcustom ai-code-backends-infra-terminal-backend 'vterm
"Terminal backend to use for sessions.
Can be either `vterm' or `eat'."
:type '(choice (const :tag "vterm" vterm)
(const :tag "eat" eat))
:group 'ai-code-backends-infra)
(defcustom ai-code-backends-infra-window-side 'right
"Side of the frame where the window should appear."
:type '(choice (const :tag "Left" left)
(const :tag "Right" right)
(const :tag "Top" top)
(const :tag "Bottom" bottom))
:group 'ai-code-backends-infra)
(defcustom ai-code-backends-infra-window-width 80
"Width of the side window when opened on left or right."
:type 'integer
:group 'ai-code-backends-infra)
(defcustom ai-code-backends-infra-window-height 20
"Height of the side window when opened on top or bottom."
:type 'integer
:group 'ai-code-backends-infra)
(defcustom ai-code-backends-infra-use-side-window t
"Whether to display the terminal in a side window."
:type 'boolean
:group 'ai-code-backends-infra)
(defcustom ai-code-backends-infra-focus-on-open t
"Whether to focus the terminal window when it opens."
:type 'boolean
:group 'ai-code-backends-infra)
(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.005
"Rendering optimization delay for batched terminal updates."
:type 'number
:group 'ai-code-backends-infra)
(defcustom ai-code-backends-infra-terminal-initialization-delay 0.1
"Initialization delay for terminal stability."
:type 'number
:group 'ai-code-backends-infra)
(defcustom ai-code-backends-infra-prevent-reflow-glitch t
"Workaround for terminal scrolling bug #1422."
:type 'boolean
:group 'ai-code-backends-infra)
(defcustom ai-code-backends-infra-eat-preserve-position t
"Maintain terminal scroll position when switching windows in eat."
:type 'boolean
:group 'ai-code-backends-infra)
;;; Variables
(defvar ai-code-backends-infra--processes (make-hash-table :test 'equal)
"Hash table mapping directory roots to their processes.")
(defvar ai-code-backends-infra--last-accessed-buffer nil
"The most recently accessed AI Code buffer.")
;;; Vterm Rendering Optimization
(defvar-local ai-code-backends-infra--vterm-render-queue nil)
(defvar-local ai-code-backends-infra--vterm-render-timer nil)
(defun ai-code-backends-infra--vterm-smart-renderer (orig-fun process input)
"Smart rendering filter for optimized vterm display updates."
(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 "\033\\[[0-9]*A.*\033\\[K.*\033\\[[0-9]*A.*\033\\[K" input))
(clear-count (cl-count-if (lambda (s) (string= s "\033[K"))
(split-string input "\033\\[K" t)))
(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
(and (> escape-density 0.3) (>= clear-count 2))
ai-code-backends-infra--vterm-render-queue)
(progn
(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
(lambda (buf)
(when (buffer-live-p buf)
(with-current-buffer buf
(when ai-code-backends-infra--vterm-render-queue
(let ((inhibit-redisplay t)
(data ai-code-backends-infra--vterm-render-queue))
(setq ai-code-backends-infra--vterm-render-queue nil
ai-code-backends-infra--vterm-render-timer nil)
(funcall orig-fun (get-buffer-process buf) data))))))
(current-buffer))))
(funcall orig-fun process input))))))
(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))
(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)))
(when ai-code-backends-infra-vterm-anti-flicker
(advice-add 'vterm--filter :around #'ai-code-backends-infra--vterm-smart-renderer)))
;;; Terminal Backend Abstraction
(defun ai-code-backends-infra--terminal-ensure-backend ()
"Ensure the selected terminal backend is available."
(cond
((eq ai-code-backends-infra-terminal-backend 'vterm)
(unless (featurep 'vterm) (require 'vterm nil t))
(unless (featurep 'vterm)
(user-error "The package vterm is not installed")))
((eq ai-code-backends-infra-terminal-backend 'eat)
(unless (featurep 'eat) (require 'eat nil t))
(unless (featurep 'eat)
(user-error "The package eat is not installed")))
(t (user-error "Invalid terminal backend: %s" ai-code-backends-infra-terminal-backend))))
(defun ai-code-backends-infra--terminal-send-string (string)
"Send STRING to the terminal in the current buffer."
(cond
((eq ai-code-backends-infra-terminal-backend 'vterm)
(vterm-send-string string))
((eq ai-code-backends-infra-terminal-backend 'eat)
(when (bound-and-true-p eat-terminal)
(eat-term-send-string eat-terminal string)))
(t (error "Unknown terminal backend: %s" ai-code-backends-infra-terminal-backend))))
(defun ai-code-backends-infra--terminal-send-escape ()
"Send escape key to the terminal in the current buffer."
(cond
((eq ai-code-backends-infra-terminal-backend 'vterm) (vterm-send-escape))
((eq ai-code-backends-infra-terminal-backend 'eat)
(when (bound-and-true-p eat-terminal)
(eat-term-send-string eat-terminal "\e")))
(t (error "Unknown terminal backend: %s" ai-code-backends-infra-terminal-backend))))
(defun ai-code-backends-infra--terminal-send-return ()
"Send return key to the terminal in the current buffer."
(cond
((eq ai-code-backends-infra-terminal-backend 'vterm) (vterm-send-return))
((eq ai-code-backends-infra-terminal-backend 'eat)
(when (bound-and-true-p eat-terminal)
(eat-term-send-string eat-terminal "\r")))
(t (error "Unknown terminal backend: %s" ai-code-backends-infra-terminal-backend))))
;;; Reflow and Window Management
(defun ai-code-backends-infra--terminal-resize-handler ()
"Retrieve the terminal's resize handling function based on backend."
(pcase ai-code-backends-infra-terminal-backend
('vterm #'vterm--window-adjust-process-window-size)
('eat #'eat--adjust-process-window-size)
(_ (error "Unsupported terminal backend"))))
(defun ai-code-backends-infra--session-buffer-p (buffer)
"Check if BUFFER belongs to an AI session."
(when-let ((name (if (stringp buffer) buffer (buffer-name buffer))))
(or (string-prefix-p "*claude-code[" name)
(string-prefix-p "*codex[" name))))
(defun ai-code-backends-infra--terminal-reflow-filter (original-fn &rest args)
"Filter terminal reflows to prevent height-only resize triggers."
(let* ((base-result (apply original-fn args))
(dimensions-stable t))
(dolist (win (window-list))
(when-let* ((buf (window-buffer win))
((ai-code-backends-infra--session-buffer-p buf)))
(let* ((new-width (window-width win))
(cached-width (window-parameter win 'ai-code-backends-infra-cached-width)))
(unless (eql new-width cached-width)
(setq dimensions-stable nil)
(set-window-parameter win 'ai-code-backends-infra-cached-width new-width)))))
(if (and ai-code-backends-infra-prevent-reflow-glitch dimensions-stable)
nil
base-result)))
(defun ai-code-backends-infra--display-buffer-in-side-window (buffer)
"Display BUFFER in a side window."
(let ((window
(if ai-code-backends-infra-use-side-window
(let* ((side ai-code-backends-infra-window-side)
(display-buffer-alist
`((,(regexp-quote (buffer-name buffer))
(display-buffer-in-side-window)
(side . ,side)
(slot . 0)
,@(when (memq side '(left right))
`((window-width . ,ai-code-backends-infra-window-width)))
,@(when (memq side '(top bottom))
`((window-height . ,ai-code-backends-infra-window-height)))
(window-parameters . ((no-delete-other-windows . t)))))))
(display-buffer buffer))
(display-buffer buffer))))
(setq ai-code-backends-infra--last-accessed-buffer buffer)
(when (and window ai-code-backends-infra-focus-on-open)
(select-window window))
window))
;;; Session Helpers
(defun ai-code-backends-infra--session-working-directory ()
"Return the working directory, preferring the current project root."
(if-let ((project (project-current)))
(expand-file-name (project-root project))
(expand-file-name default-directory)))
(defun ai-code-backends-infra--session-buffer-name (prefix directory)
"Return a session buffer name for PREFIX in DIRECTORY."
(format "*%s[%s]*"
prefix
(file-name-nondirectory (directory-file-name directory))))
(defun ai-code-backends-infra--cleanup-session (directory buffer-name process-table)
"Clean up a session for DIRECTORY using BUFFER-NAME and PROCESS-TABLE."
(remhash directory process-table)
(when-let ((buffer (get-buffer buffer-name)))
(when (buffer-live-p buffer)
(kill-buffer buffer))))
(defun ai-code-backends-infra--toggle-or-create-session (working-dir buffer-name process-table command
&optional escape-fn cleanup-fn)
"Toggle or create a terminal session.
WORKING-DIR is the directory for the session.
BUFFER-NAME is the terminal buffer name.
PROCESS-TABLE maps directories to processes.
COMMAND is the shell command to run.
ESCAPE-FN is bound to `C-<escape>' inside the session buffer when non-nil.
CLEANUP-FN is called with no arguments when the process exits."
(ai-code-backends-infra--cleanup-dead-processes process-table)
(let ((existing-process (gethash working-dir process-table))
(buffer (get-buffer buffer-name)))
(if (and existing-process (process-live-p existing-process) buffer)
(if (get-buffer-window buffer)
(delete-window (get-buffer-window buffer))
(ai-code-backends-infra--display-buffer-in-side-window buffer))
(let* ((buffer-and-process
(ai-code-backends-infra--create-terminal-session
buffer-name working-dir command nil))
(new-buffer (car buffer-and-process))
(process (cdr buffer-and-process)))
(puthash working-dir process process-table)
(when cleanup-fn
(set-process-sentinel process
(lambda (_proc _event)
(funcall cleanup-fn))))
(when escape-fn
(with-current-buffer new-buffer
(local-set-key (kbd "C-<escape>") escape-fn)))
(sleep-for ai-code-backends-infra-terminal-initialization-delay)
(ai-code-backends-infra--display-buffer-in-side-window new-buffer)))))
(defun ai-code-backends-infra--switch-to-session-buffer (buffer-name missing-message)
"Switch to BUFFER-NAME or signal MISSING-MESSAGE."
(if-let ((buffer (get-buffer buffer-name)))
(if-let ((window (get-buffer-window buffer)))
(select-window window)
(ai-code-backends-infra--display-buffer-in-side-window buffer))
(user-error "%s" missing-message)))
(defun ai-code-backends-infra--send-line-to-session (buffer-name missing-message line)
"Send LINE to BUFFER-NAME or signal MISSING-MESSAGE."
(if-let ((buffer (get-buffer buffer-name)))
(with-current-buffer buffer
(ai-code-backends-infra--terminal-send-string line)
(sit-for 0.1)
(ai-code-backends-infra--terminal-send-return))
(user-error "%s" missing-message)))
;;; Generic Session Creation
(defun ai-code-backends-infra--create-terminal-session (buffer-name working-dir command env-vars)
"Generic function to create a terminal session.
BUFFER-NAME is the name for the buffer.
WORKING-DIR is the directory.
COMMAND is the shell command to run.
ENV-VARS is a list of environment variables."
(ai-code-backends-infra--terminal-ensure-backend)
(let ((default-directory working-dir))
(cond
((eq ai-code-backends-infra-terminal-backend 'vterm)
(let* ((vterm-shell command)
(vterm-environment (append env-vars (bound-and-true-p vterm-environment))))
(let ((buffer (save-window-excursion (vterm buffer-name))))
(with-current-buffer buffer
(ai-code-backends-infra--configure-vterm-buffer))
(cons buffer (get-buffer-process buffer)))))
((eq ai-code-backends-infra-terminal-backend 'eat)
(let* ((buffer (get-buffer-create buffer-name))
(parts (split-string-shell-command command))
(program (car parts))
(args (cdr parts)))
(with-current-buffer buffer
(unless (eq major-mode 'eat-mode) (eat-mode))
(setq-local process-environment (append env-vars process-environment))
(eat-exec buffer buffer-name program nil args)
(cons buffer (get-buffer-process buffer)))))
(t (error "Unknown backend")))))
(defun ai-code-backends-infra--cleanup-dead-processes (table)
"Clean up dead processes from TABLE."
(maphash (lambda (dir proc)
(unless (process-live-p proc)
(remhash dir table)))
table))
(provide 'ai-code-backends-infra)
;;; ai-code-backends-infra.el ends here
You can’t perform that action at this time.
