From 67d3d3dccb60a465db126f096d9cdee1548a60fd Mon Sep 17 00:00:00 2001 From: Kang Tu Date: Wed, 1 Jul 2026 07:49:17 -0700 Subject: [PATCH 1/4] Track Ghostel lifecycle and progress metadata --- ai-code-backends-infra-ghostel.el | 86 +++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/ai-code-backends-infra-ghostel.el b/ai-code-backends-infra-ghostel.el index 5bbc4845..73c98dea 100644 --- a/ai-code-backends-infra-ghostel.el +++ b/ai-code-backends-infra-ghostel.el @@ -25,6 +25,8 @@ "ai-code-backends-infra" (buffer directory)) (declare-function ai-code-backends-infra--sync-terminal-cursor "ai-code-backends-infra" ()) +(declare-function ai-code-session-update-metadata + "ai-code-session" (id-or-buffer metadata)) (declare-function ghostel-exec "ghostel" (buffer program &optional args)) (declare-function ghostel-send-key "ghostel" (key-name &optional mods)) (declare-function ghostel-send-string "ghostel" (string)) @@ -34,11 +36,17 @@ (defvar ai-code-backends-infra--session-terminal-backend) (eval-when-compile + (defvar ghostel-command-finish-functions) + (defvar ghostel-command-start-functions) (defvar ghostel-kill-buffer-on-exit) + (defvar ghostel-progress-function) (defvar ghostel-set-title-function) (defvar ghostel--copy-mode-active) (defvar ghostel--input-mode)) +(defvar-local ai-code-backends-infra-ghostel--progress-function nil + "Original Ghostel progress function captured before AI Code wrapping.") + (defun ai-code-backends-infra-ghostel-ensure-backend () "Ensure the Ghostel backend is available." (unless (featurep 'ghostel) (require 'ghostel nil t)) @@ -55,6 +63,83 @@ (add-hook 'post-command-hook #'ai-code-backends-infra--sync-terminal-cursor nil t)) +(defun ai-code-backends-infra-ghostel--ai-session-buffer-p (&optional buffer) + "Return non-nil when BUFFER is an AI Code Ghostel session buffer." + (when (buffer-live-p (or buffer (current-buffer))) + (with-current-buffer (or buffer (current-buffer)) + (eq ai-code-backends-infra--session-terminal-backend 'ghostel)))) + +(defun ai-code-backends-infra-ghostel--update-session-metadata (buffer metadata) + "Merge METADATA into the AI Code session associated with BUFFER." + (when (and (buffer-live-p buffer) + (fboundp 'ai-code-session-update-metadata) + (ai-code-backends-infra-ghostel--ai-session-buffer-p buffer)) + (with-current-buffer buffer + (ai-code-session-update-metadata buffer metadata)))) + +(defun ai-code-backends-infra-ghostel--command-start (buffer) + "Record a Ghostel OSC 133 command-start event for BUFFER." + (ai-code-backends-infra-ghostel--update-session-metadata + buffer + (list :ghostel-command-state 'started + :ghostel-command-started-at (float-time) + :ghostel-command-finished-at nil + :ghostel-command-exit-status nil + :ghostel-status 'running))) + +(defun ai-code-backends-infra-ghostel--command-finish (buffer exit-status) + "Record a Ghostel OSC 133 command-finish event for BUFFER. +EXIT-STATUS is the status reported by Ghostel, or nil when unavailable." + (ai-code-backends-infra-ghostel--update-session-metadata + buffer + (list :ghostel-command-state 'finished + :ghostel-command-finished-at (float-time) + :ghostel-command-exit-status exit-status + :ghostel-status (if (and exit-status (/= exit-status 0)) + 'error + 'idle)))) + +(defun ai-code-backends-infra-ghostel--progress-status (state) + "Return an AI Code status symbol for Ghostel progress STATE." + (pcase state + ('remove 'idle) + ('error 'error) + ('pause 'paused) + ((or 'set 'indeterminate) 'running) + (_ 'running))) + +(defun ai-code-backends-infra-ghostel--progress (state progress) + "Record a Ghostel OSC 9;4 progress report and delegate to Ghostel. +STATE and PROGRESS use the signature of `ghostel-progress-function'." + (when (ai-code-backends-infra-ghostel--ai-session-buffer-p) + (ai-code-backends-infra-ghostel--update-session-metadata + (current-buffer) + (list :ghostel-progress-state state + :ghostel-progress-value progress + :ghostel-progress-updated-at (float-time) + :ghostel-status + (ai-code-backends-infra-ghostel--progress-status state)))) + (when (and ai-code-backends-infra-ghostel--progress-function + (not (eq ai-code-backends-infra-ghostel--progress-function + #'ai-code-backends-infra-ghostel--progress))) + (funcall ai-code-backends-infra-ghostel--progress-function state progress))) + +(defun ai-code-backends-infra-ghostel--install-lifecycle-hooks () + "Install Ghostel lifecycle hooks for the current AI Code session." + (when (boundp 'ghostel-command-start-functions) + (add-hook 'ghostel-command-start-functions + #'ai-code-backends-infra-ghostel--command-start nil t)) + (when (boundp 'ghostel-command-finish-functions) + (add-hook 'ghostel-command-finish-functions + #'ai-code-backends-infra-ghostel--command-finish nil t)) + (when (boundp 'ghostel-progress-function) + (unless (eq ghostel-progress-function + #'ai-code-backends-infra-ghostel--progress) + (setq-local ai-code-backends-infra-ghostel--progress-function + ghostel-progress-function)) + (setq-local ghostel-progress-function + #'ai-code-backends-infra-ghostel--progress))) + (defun ai-code-backends-infra-ghostel-send-string (string &optional paste) "Send STRING to the current Ghostel process. If PASTE is non-nil, send it as a pasted string." @@ -82,6 +167,7 @@ If PASTE is non-nil, send it as a pasted string." "Configure the current Ghostel buffer for AI Code sessions." (setq-local ghostel-set-title-function nil) (setq-local ghostel-kill-buffer-on-exit nil) + (ai-code-backends-infra-ghostel--install-lifecycle-hooks) (ai-code-backends-infra--configure-session-input-shortcuts) (ai-code-backends-infra--install-navigation-cursor-sync)) From 4df1d55a13a4ee02391bfcc506369dac4ada8e27 Mon Sep 17 00:00:00 2001 From: Kang Tu Date: Wed, 1 Jul 2026 07:50:03 -0700 Subject: [PATCH 2/4] Add tests for Ghostel lifecycle integration --- test/test_ai-code-backends-infra-ghostel.el | 111 ++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 test/test_ai-code-backends-infra-ghostel.el diff --git a/test/test_ai-code-backends-infra-ghostel.el b/test/test_ai-code-backends-infra-ghostel.el new file mode 100644 index 00000000..3aac7d6e --- /dev/null +++ b/test/test_ai-code-backends-infra-ghostel.el @@ -0,0 +1,111 @@ +;;; test_ai-code-backends-infra-ghostel.el --- Ghostel lifecycle tests -*- lexical-binding: t; -*- + +;; Author: Kang Tu +;; SPDX-License-Identifier: Apache-2.0 + +;;; Commentary: +;; Tests for Ghostel-specific lifecycle integration. + +;;; Code: + +(require 'ert) +(require 'cl-lib) +(require 'ai-code-backends-infra-ghostel) + +(defvar ghostel-command-finish-functions) +(defvar ghostel-command-start-functions) +(defvar ghostel-progress-function) +(defvar ghostel-set-title-function) +(defvar ghostel-kill-buffer-on-exit) + +(ert-deftest test-ai-code-backends-infra-ghostel-configures-lifecycle-hooks () + "Ghostel AI session configuration should install lifecycle hooks locally." + (with-temp-buffer + (setq-local ai-code-backends-infra--session-terminal-backend 'ghostel) + (let ((ghostel-command-start-functions nil) + (ghostel-command-finish-functions nil) + (ghostel-progress-function #'ignore)) + (cl-letf (((symbol-function 'ai-code-backends-infra--configure-session-input-shortcuts) + (lambda () nil)) + ((symbol-function 'ai-code-backends-infra--install-navigation-cursor-sync) + (lambda () nil))) + (ai-code-backends-infra--configure-ghostel-buffer)) + (should (memq #'ai-code-backends-infra-ghostel--command-start + ghostel-command-start-functions)) + (should (memq #'ai-code-backends-infra-ghostel--command-finish + ghostel-command-finish-functions)) + (should (eq ghostel-progress-function + #'ai-code-backends-infra-ghostel--progress)) + (should (eq ai-code-backends-infra-ghostel--progress-function + #'ignore))))) + +(ert-deftest test-ai-code-backends-infra-ghostel-command-lifecycle-updates-session-metadata () + "OSC 133 command hooks should write structured session metadata." + (let ((buffer (generate-new-buffer "*ai-code-ghostel-lifecycle*")) + (updates nil)) + (unwind-protect + (cl-letf (((symbol-function 'ai-code-session-update-metadata) + (lambda (target metadata) + (push (list target metadata) updates)))) + (with-current-buffer buffer + (setq-local ai-code-backends-infra--session-terminal-backend 'ghostel)) + (ai-code-backends-infra-ghostel--command-start buffer) + (ai-code-backends-infra-ghostel--command-finish buffer 2) + (let ((start (cadr (nth 1 updates))) + (finish (cadr (nth 0 updates)))) + (should (eq (plist-get start :ghostel-command-state) 'started)) + (should (eq (plist-get start :ghostel-status) 'running)) + (should (null (plist-get start :ghostel-command-exit-status))) + (should (eq (plist-get finish :ghostel-command-state) 'finished)) + (should (= (plist-get finish :ghostel-command-exit-status) 2)) + (should (eq (plist-get finish :ghostel-status) 'error)))) + (when (buffer-live-p buffer) + (kill-buffer buffer))))) + +(ert-deftest test-ai-code-backends-infra-ghostel-command-hooks-ignore-non-ai-buffers () + "Global Ghostel command hooks should not touch unrelated Ghostel buffers." + (let ((buffer (generate-new-buffer "*plain-ghostel*")) + (updated nil)) + (unwind-protect + (cl-letf (((symbol-function 'ai-code-session-update-metadata) + (lambda (&rest _args) + (setq updated t)))) + (ai-code-backends-infra-ghostel--command-start buffer) + (ai-code-backends-infra-ghostel--command-finish buffer 0) + (should-not updated)) + (when (buffer-live-p buffer) + (kill-buffer buffer))))) + +(ert-deftest test-ai-code-backends-infra-ghostel-progress-updates-session-and-delegates () + "OSC 9;4 progress should update metadata and preserve Ghostel's handler." + (let ((updates nil) + (delegated nil)) + (cl-letf (((symbol-function 'ai-code-session-update-metadata) + (lambda (target metadata) + (push (list target metadata) updates)))) + (with-temp-buffer + (setq-local ai-code-backends-infra--session-terminal-backend 'ghostel) + (setq-local ai-code-backends-infra-ghostel--progress-function + (lambda (state progress) + (setq delegated (list state progress)))) + (ai-code-backends-infra-ghostel--progress 'set 42) + (let ((metadata (cadar updates))) + (should (eq (plist-get metadata :ghostel-progress-state) 'set)) + (should (= (plist-get metadata :ghostel-progress-value) 42)) + (should (eq (plist-get metadata :ghostel-status) 'running))) + (should (equal delegated '(set 42))))))) + +(ert-deftest test-ai-code-backends-infra-ghostel-progress-remove-marks-idle () + "OSC 9;4 remove progress should mark the Ghostel status as idle." + (let ((updates nil)) + (cl-letf (((symbol-function 'ai-code-session-update-metadata) + (lambda (_target metadata) + (push metadata updates)))) + (with-temp-buffer + (setq-local ai-code-backends-infra--session-terminal-backend 'ghostel) + (ai-code-backends-infra-ghostel--progress 'remove nil) + (should (eq (plist-get (car updates) :ghostel-progress-state) 'remove)) + (should (eq (plist-get (car updates) :ghostel-status) 'idle)))))) + +(provide 'test_ai-code-backends-infra-ghostel) +;;; test_ai-code-backends-infra-ghostel.el ends here From 289327e6e6ac8305e02b887c035894f25aade903 Mon Sep 17 00:00:00 2001 From: Kang Tu Date: Sun, 5 Jul 2026 10:26:27 -0700 Subject: [PATCH 3/4] Fix Ghostel lifecycle hook test bindings --- test/test_ai-code-backends-infra-ghostel.el | 32 ++++++++++----------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/test/test_ai-code-backends-infra-ghostel.el b/test/test_ai-code-backends-infra-ghostel.el index 3aac7d6e..c2cecee8 100644 --- a/test/test_ai-code-backends-infra-ghostel.el +++ b/test/test_ai-code-backends-infra-ghostel.el @@ -22,22 +22,22 @@ "Ghostel AI session configuration should install lifecycle hooks locally." (with-temp-buffer (setq-local ai-code-backends-infra--session-terminal-backend 'ghostel) - (let ((ghostel-command-start-functions nil) - (ghostel-command-finish-functions nil) - (ghostel-progress-function #'ignore)) - (cl-letf (((symbol-function 'ai-code-backends-infra--configure-session-input-shortcuts) - (lambda () nil)) - ((symbol-function 'ai-code-backends-infra--install-navigation-cursor-sync) - (lambda () nil))) - (ai-code-backends-infra--configure-ghostel-buffer)) - (should (memq #'ai-code-backends-infra-ghostel--command-start - ghostel-command-start-functions)) - (should (memq #'ai-code-backends-infra-ghostel--command-finish - ghostel-command-finish-functions)) - (should (eq ghostel-progress-function - #'ai-code-backends-infra-ghostel--progress)) - (should (eq ai-code-backends-infra-ghostel--progress-function - #'ignore))))) + (setq-local ghostel-command-start-functions nil) + (setq-local ghostel-command-finish-functions nil) + (setq-local ghostel-progress-function #'ignore) + (cl-letf (((symbol-function 'ai-code-backends-infra--configure-session-input-shortcuts) + (lambda () nil)) + ((symbol-function 'ai-code-backends-infra--install-navigation-cursor-sync) + (lambda () nil))) + (ai-code-backends-infra--configure-ghostel-buffer)) + (should (memq #'ai-code-backends-infra-ghostel--command-start + ghostel-command-start-functions)) + (should (memq #'ai-code-backends-infra-ghostel--command-finish + ghostel-command-finish-functions)) + (should (eq ghostel-progress-function + #'ai-code-backends-infra-ghostel--progress)) + (should (eq ai-code-backends-infra-ghostel--progress-function + #'ignore)))) (ert-deftest test-ai-code-backends-infra-ghostel-command-lifecycle-updates-session-metadata () "OSC 133 command hooks should write structured session metadata." From 15caae8cd8ffe1abb0db0444d8f6025f0b9f1b41 Mon Sep 17 00:00:00 2001 From: tninja Date: Sun, 5 Jul 2026 16:27:04 -0700 Subject: [PATCH 4/4] Remove Ghostel private resize handler usage --- ai-code-backends-infra-ghostel.el | 7 +++---- ai-code-backends-infra.el | 7 +++---- test/test_ai-code-backends-infra.el | 25 ++++++++++--------------- 3 files changed, 16 insertions(+), 23 deletions(-) diff --git a/ai-code-backends-infra-ghostel.el b/ai-code-backends-infra-ghostel.el index 73c98dea..234e8c27 100644 --- a/ai-code-backends-infra-ghostel.el +++ b/ai-code-backends-infra-ghostel.el @@ -31,8 +31,6 @@ (declare-function ghostel-send-key "ghostel" (key-name &optional mods)) (declare-function ghostel-send-string "ghostel" (string)) (declare-function ghostel-paste-string "ghostel" (string)) -(declare-function ghostel--window-adjust-process-window-size - "ghostel" (process windows)) (defvar ai-code-backends-infra--session-terminal-backend) (eval-when-compile @@ -160,8 +158,9 @@ If PASTE is non-nil, send it as a pasted string." (ghostel-send-key "backspace")) (defun ai-code-backends-infra-ghostel-resize-handler () - "Return the Ghostel resize handler." - #'ghostel--window-adjust-process-window-size) + "Return the Ghostel resize handler. +Ghostel owns terminal-model resizing through its mode-local window hooks." + nil) (defun ai-code-backends-infra--configure-ghostel-buffer () "Configure the current Ghostel buffer for AI Code sessions." diff --git a/ai-code-backends-infra.el b/ai-code-backends-infra.el index c024a73b..7c4457c4 100644 --- a/ai-code-backends-infra.el +++ b/ai-code-backends-infra.el @@ -663,10 +663,9 @@ from the window where it was initially created." (windows (or (get-buffer-window-list buffer nil t) (list window)))) (if (eq backend 'ghostel) - (when-let ((size (funcall (ai-code-backends-infra-ghostel-resize-handler) - proc - windows))) - (set-process-window-size proc (cdr size) (car size))) + (set-process-window-size proc + (window-body-height window) + (window-body-width window)) (pcase backend ('vterm (let ((result diff --git a/test/test_ai-code-backends-infra.el b/test/test_ai-code-backends-infra.el index ec85d158..5504bd6b 100644 --- a/test/test_ai-code-backends-infra.el +++ b/test/test_ai-code-backends-infra.el @@ -14,8 +14,6 @@ (require 'ai-code-backends-infra) (require 'ai-code-notifications) -(declare-function ghostel--window-adjust-process-window-size "ghostel" (process windows)) - (defvar vterm-copy-mode-hook) (defvar eat-term-name) (defvar ghostel-set-title-function) @@ -478,18 +476,17 @@ The result is a cons of whether SYMBOL is bound and its default value." (advice-remove handler #'ai-code-backends-infra--terminal-reflow-filter)) (fmakunbound handler))))) -(ert-deftest test-ai-code-backends-infra-sync-terminal-dimensions-uses-ghostel-handler () - "Ghostel dimension sync should update Ghostel's terminal model before PTY size." - (let ((adjust-calls nil) - (set-size-calls nil)) +(ert-deftest test-ai-code-backends-infra-sync-terminal-dimensions-ghostel-uses-generic-pty-resize () + "Ghostel dimension sync should not call removed Ghostel private handlers." + (let ((set-size-calls nil)) (cl-letf (((symbol-function 'get-buffer-process) (lambda (_buffer) 'ghostel-proc)) ((symbol-function 'window-live-p) (lambda (_window) t)) - ((symbol-function 'ghostel--window-adjust-process-window-size) - (lambda (process windows) - (push (list process windows) adjust-calls) - '(90 . 24))) + ((symbol-function 'window-body-height) + (lambda (_window) 24)) + ((symbol-function 'window-body-width) + (lambda (_window) 90)) ((symbol-function 'set-process-window-size) (lambda (process height width) (push (list process height width) set-size-calls)))) @@ -498,7 +495,6 @@ The result is a cons of whether SYMBOL is bound and its default value." (ai-code-backends-infra--sync-terminal-dimensions (current-buffer) 'mock-window))) - (should (equal adjust-calls '((ghostel-proc (mock-window))))) (should (equal set-size-calls '((ghostel-proc 24 90)))))) (ert-deftest test-ai-code-backends-infra-display-buffer-in-side-window-uses-body-width () @@ -991,11 +987,10 @@ The result is a cons of whether SYMBOL is bound and its default value." (lambda () t))) (should (ai-code-backends-infra--terminal-navigation-mode-p))))) -(ert-deftest test-ai-code-backends-infra-terminal-resize-handler-supports-ghostel () - "Ghostel backend should expose its resize handler." +(ert-deftest test-ai-code-backends-infra-terminal-resize-handler-skips-ghostel () + "Ghostel backend should not expose removed private resize handlers." (let ((ai-code-backends-infra-terminal-backend 'ghostel)) - (should (eq (ai-code-backends-infra--terminal-resize-handler) - #'ghostel--window-adjust-process-window-size)))) + (should-not (ai-code-backends-infra--terminal-resize-handler)))) (ert-deftest test-ai-code-backends-infra-terminal-resize-handler-delegates-to-eat-module () "Resize handler lookup should delegate eat specifics to the eat module."