Fix QTerminalWidget double-free crashes and complete Phase 5 behavioral tests

ober

a053941ff3e592f57ea49a1e476b6048ed3079b2

diff --git a/lib/jerboa-emacs/async.sls b/lib/jerboa-emacs/async.sls
index aea2f02..772adc0 100644
--- a/lib/jerboa-emacs/async.sls
+++ b/lib/jerboa-emacs/async.sls
@@ -552,8 +552,8 @@
        (let ([v (hashtable-ref
                   cache
                   key
-                  '#{miss j40h7yge6hu7zd2spbldcd8pr-1})])
-         (if (eq? v '#{miss j40h7yge6hu7zd2spbldcd8pr-2})
+                  '#{miss bbhoe7u0vfcvf8wf5ezuvxl6i-1})])
+         (if (eq? v '#{miss bbhoe7u0vfcvf8wf5ezuvxl6i-2})
              (if (null? default) #f (car default))
              v)))
   (def (weak-cache-set! cache key value)
diff --git a/lib/jerboa-emacs/qt/app.sls b/lib/jerboa-emacs/qt/app.sls
index 2980e88..951c7a9 100644
--- a/lib/jerboa-emacs/qt/app.sls
+++ b/lib/jerboa-emacs/qt/app.sls
@@ -1553,8 +1553,15 @@
                                               [qt-term (and qt-buf
                                                             (hash-get
                                                               *terminal-widget-map*
-                                                              qt-buf))])
+                                                              qt-buf))]
+                                              [key-src-widget (qt-last-key-widget)]
+                                              [key-from-terminal? (and qt-term
+                                                                       (equal?
+                                                                         key-src-widget
+                                                                         (qt-terminal-widget
+                                                                           qt-term)))])
                                          (if (and qt-term
+                                                  key-from-terminal?
                                                   (null?
                                                     (key-state-prefix-keys
                                                       (app-state-key-state
@@ -1814,7 +1821,25 @@
                                  (hash-get *terminal-widget-map* buf))])
                  (if term
                      (qt-terminal-widget term)
-                     (qt-current-editor fr))))))
+                     (qt-current-editor fr)))))
+           (qt-window-set-pre-container-destroy-fn!
+             (lambda (container)
+               (let ([bufs-to-remove '()])
+                 (hash-for-each
+                   (lambda (buf stored-container)
+                     (when (equal? stored-container container)
+                       (let ([term (hash-get *terminal-widget-map* buf)])
+                         (when term
+                           (with-catch
+                             (lambda (e) #f)
+                             (lambda () (qt-terminal-destroy! term)))))
+                       (set! bufs-to-remove (cons buf bufs-to-remove))))
+                   *terminal-container-map*)
+                 (for-each
+                   (lambda (buf)
+                     (hash-remove! *terminal-widget-map* buf)
+                     (hash-remove! *terminal-container-map* buf))
+                   bufs-to-remove)))))
          (schedule-periodic!
            'repl-poll
            50
@@ -2521,16 +2546,25 @@
                   'test-terminal-running?
                   (lambda ()
                     (let* ([fr (app-state-frame app)]
-                           [buf (qt-current-buffer fr)])
-                      (and buf (hash-key? *terminal-widget-map* buf) #t))))
+                           [win (qt-current-window fr)]
+                           [buf (qt-edit-window-buffer win)]
+                           [term (and buf
+                                      (hash-get
+                                        *terminal-widget-map*
+                                        buf))])
+                      (and term
+                           (let* ([container (qt-edit-window-container
+                                               win)]
+                                  [count (qt-stacked-widget-count
+                                           container)])
+                             (and (> count 1)
+                                  (> (qt-stacked-widget-current-index
+                                       container)
+                                     0)))))))
                 (cons
                   'test-reset!
                   (lambda ()
                     (app-state-key-state-set! app (make-initial-key-state))
-                    (when (> (length
-                               (qt-frame-windows (app-state-frame app)))
-                             1)
-                      (execute-command! app 'delete-other-windows))
                     (let ([term-bufs (hash-keys *terminal-widget-map*)])
                       (for-each
                         (lambda (buf)
@@ -2541,8 +2575,15 @@
                               (with-catch
                                 (lambda (e) #f)
                                 (lambda () (qt-terminal-destroy! term)))
-                              (hash-remove! *terminal-widget-map* buf))))
+                              (hash-remove! *terminal-widget-map* buf)
+                              (hash-remove!
+                                *terminal-container-map*
+                                buf))))
                         term-bufs))
+                    (when (> (length
+                               (qt-frame-windows (app-state-frame app)))
+                             1)
+                      (execute-command! app 'delete-other-windows))
                     'ok))
                 (cons
                   'test-clear-buffer!
diff --git a/lib/jerboa-emacs/qt/commands-config.sls b/lib/jerboa-emacs/qt/commands-config.sls
index c512798..fa5aaa0 100644
--- a/lib/jerboa-emacs/qt/commands-config.sls
+++ b/lib/jerboa-emacs/qt/commands-config.sls
@@ -938,6 +938,7 @@
                             (qt-terminal-widget term))])
                  (qt-stacked-widget-set-current-index! container idx))
                (hash-put! *terminal-widget-map* buf term)
+               (hash-put! *terminal-container-map* buf container)
                ((app-state-key-handler app) (qt-terminal-widget term))
                (qt-terminal-spawn! term "")
                (qt-terminal-focus! term)
diff --git a/lib/jerboa-emacs/qt/commands-shell.sls b/lib/jerboa-emacs/qt/commands-shell.sls
index d2e0407..8ff7867 100644
--- a/lib/jerboa-emacs/qt/commands-shell.sls
+++ b/lib/jerboa-emacs/qt/commands-shell.sls
@@ -4,30 +4,30 @@
 
 (library (jerboa-emacs qt commands-shell)
   (export directory-exists? *terminal-widget-map*
-   apply-font-size-to-all-editors! cmd-increase-font-size
-   cmd-decrease-font-size cmd-reset-font-size
-   cmd-goto-first-non-blank cmd-goto-last-non-blank
-   cmd-move-to-window-top cmd-move-to-window-middle
-   cmd-move-to-window-bottom cmd-scroll-left cmd-scroll-right
-   cmd-insert-let cmd-insert-lambda cmd-insert-defun
-   cmd-insert-cond cmd-insert-when cmd-insert-unless
-   cmd-insert-match cmd-insert-import cmd-insert-export
-   cmd-insert-include cmd-insert-file-header
-   cmd-insert-header-guard cmd-insert-box-comment
-   cmd-insert-file-contents cmd-insert-register-string
-   *auto-indent* *backup-files* *version-control* *debug-mode*
-   *debug-on-quit* *visible-bell* *transient-mark*
-   *electric-indent* cmd-toggle-auto-indent
-   cmd-toggle-backup-files cmd-toggle-version-control
-   cmd-toggle-debug-mode cmd-toggle-debug-on-quit
-   cmd-toggle-visible-bell cmd-toggle-transient-mark
-   cmd-toggle-electric-indent cmd-toggle-auto-revert
-   cmd-toggle-auto-revert-global cmd-auto-revert-tail-mode
-   *view-mode-buffers* cmd-view-mode *so-long-threshold*
-   *so-long-buffers* check-so-long! cmd-so-long-mode
-   *follow-mode* cmd-follow-mode *command-history-file*
-   savehist-save! savehist-load! auto-fill-check!
-   qt-aggressive-indent-line!
+   *terminal-container-map* apply-font-size-to-all-editors!
+   cmd-increase-font-size cmd-decrease-font-size
+   cmd-reset-font-size cmd-goto-first-non-blank
+   cmd-goto-last-non-blank cmd-move-to-window-top
+   cmd-move-to-window-middle cmd-move-to-window-bottom
+   cmd-scroll-left cmd-scroll-right cmd-insert-let
+   cmd-insert-lambda cmd-insert-defun cmd-insert-cond
+   cmd-insert-when cmd-insert-unless cmd-insert-match
+   cmd-insert-import cmd-insert-export cmd-insert-include
+   cmd-insert-file-header cmd-insert-header-guard
+   cmd-insert-box-comment cmd-insert-file-contents
+   cmd-insert-register-string *auto-indent* *backup-files*
+   *version-control* *debug-mode* *debug-on-quit*
+   *visible-bell* *transient-mark* *electric-indent*
+   cmd-toggle-auto-indent cmd-toggle-backup-files
+   cmd-toggle-version-control cmd-toggle-debug-mode
+   cmd-toggle-debug-on-quit cmd-toggle-visible-bell
+   cmd-toggle-transient-mark cmd-toggle-electric-indent
+   cmd-toggle-auto-revert cmd-toggle-auto-revert-global
+   cmd-auto-revert-tail-mode *view-mode-buffers* cmd-view-mode
+   *so-long-threshold* *so-long-buffers* check-so-long!
+   cmd-so-long-mode *follow-mode* cmd-follow-mode
+   *command-history-file* savehist-save! savehist-load!
+   auto-fill-check! qt-aggressive-indent-line!
    cmd-toggle-delete-trailing-whitespace-on-save
    uniquify-parent-suffix uniquify-buffer-name!
    cmd-recentf-open-files cmd-toggle-frame-fullscreen
@@ -118,6 +118,8 @@
        (and (file-exists? path) (file-directory? path)))
   (define *terminal-widget-map*--cell
     (vector (make-hash-table-eq)))
+  (define *terminal-container-map*--cell
+    (vector (make-hash-table-eq)))
   (def (apply-font-size-to-all-editors! app)
        "Apply the current global font size to all open editors."
        (let ([fr (app-state-frame app)]
@@ -2280,6 +2282,13 @@
                        *terminal-widget-map*--cell
                        0
                        val)]))
+  (define-syntax *terminal-container-map*
+    (identifier-syntax
+      [id (vector-ref *terminal-container-map*--cell 0)]
+      [(set! id val) (vector-set!
+                       *terminal-container-map*--cell
+                       0
+                       val)]))
   (define-syntax *auto-indent*
     (identifier-syntax
       [id (vector-ref *auto-indent*--cell 0)]
diff --git a/lib/jerboa-emacs/qt/commands.sls b/lib/jerboa-emacs/qt/commands.sls
index 2574a49..4b579e2 100644
--- a/lib/jerboa-emacs/qt/commands.sls
+++ b/lib/jerboa-emacs/qt/commands.sls
@@ -5,10 +5,10 @@
 (library (jerboa-emacs qt commands)
   (export qt-register-all-commands! dired-open-directory!
    qt-open-image-inline! *qt-app-ptr* *terminal-widget-map*
-   qt-kill-ring-push! *isearch-active* isearch-handle-key!
-   *qreplace-active* qreplace-handle-key! recent-files-add!
-   recent-files-load! bookmarks-load! session-save!
-   session-restore-files *tab-bar-visible*
+   *terminal-container-map* qt-kill-ring-push! *isearch-active*
+   isearch-handle-key! *qreplace-active* qreplace-handle-key!
+   recent-files-add! recent-files-load! bookmarks-load!
+   session-save! session-restore-files *tab-bar-visible*
    *auto-revert-tail-buffers* *file-mtimes* file-mtime-record!
    file-mtime-changed? *eldoc-mode* eldoc-display!
    *current-theme* *themes* theme-stylesheet load-theme!
diff --git a/lib/jerboa-emacs/qt/sci-shim.sls b/lib/jerboa-emacs/qt/sci-shim.sls
index 2a3da87..048c041 100644
--- a/lib/jerboa-emacs/qt/sci-shim.sls
+++ b/lib/jerboa-emacs/qt/sci-shim.sls
@@ -77,13 +77,14 @@
    qt-label-set-alignment! qt-label-set-pixmap!
    qt-label-set-text! qt-label-text qt-last-key-code
    qt-last-key-modifiers qt-last-key-text
-   qt-last-key-autorepeat? qt-line-edit-create
-   qt-line-edit-set-completer! qt-line-edit-set-text!
-   qt-line-edit-text qt-list-widget-add-item!
-   qt-list-widget-clear! qt-list-widget-create
-   qt-list-widget-current-row qt-list-widget-set-current-row!
-   qt-main-window-add-toolbar! qt-main-window-create
-   qt-main-window-menu-bar qt-main-window-set-central-widget!
+   qt-last-key-autorepeat? qt-last-key-widget
+   qt-line-edit-create qt-line-edit-set-completer!
+   qt-line-edit-set-text! qt-line-edit-text
+   qt-list-widget-add-item! qt-list-widget-clear!
+   qt-list-widget-create qt-list-widget-current-row
+   qt-list-widget-set-current-row! qt-main-window-add-toolbar!
+   qt-main-window-create qt-main-window-menu-bar
+   qt-main-window-set-central-widget!
    qt-main-window-set-status-bar-text!
    qt-main-window-set-title! qt-menu-add-action!
    qt-menu-add-separator! qt-menu-bar-add-menu qt-on-clicked!
diff --git a/lib/jerboa-emacs/qt/window.sls b/lib/jerboa-emacs/qt/window.sls
index e53d9b0..2f9b73d 100644
--- a/lib/jerboa-emacs/qt/window.sls
+++ b/lib/jerboa-emacs/qt/window.sls
@@ -28,7 +28,8 @@
    qt-frame-other-window! qt-apply-editor-theme!
    split-tree-flatten split-tree-find-parent
    split-tree-find-leaf split-tree-collect-sub-splitters
-   qt-window-set-app-ptr!)
+   qt-window-set-app-ptr!
+   qt-window-set-pre-container-destroy-fn!)
   (import
     (except (chezscheme) make-hash-table hash-table? iota \x31;+ \x31;-
       getenv path-extension path-absolute? thread? make-mutex
@@ -40,6 +41,12 @@
   (def *qt-app-for-events* #f)
   (def (qt-window-set-app-ptr! app)
        (set! *qt-app-for-events* app))
+  (def *pre-container-destroy-fn* #f)
+  (def (qt-window-set-pre-container-destroy-fn! fn)
+       (set! *pre-container-destroy-fn* fn))
+  (def (qt-window-pre-container-destroy! container)
+       (when *pre-container-destroy-fn*
+         (*pre-container-destroy-fn* container)))
   (def (qt-window-process-events!)
        "Process pending Qt events if app pointer is available."
        (let ([app *qt-app-for-events*])
@@ -546,6 +553,7 @@
                      (set! container #f)
                      (qt-widget-destroy! parent-spl))))))
            (when container
+             (qt-window-pre-container-destroy! container)
              (qt-widget-hide! container)
              (qt-widget-destroy! container))
            (qt-frame-windows-set!
@@ -578,6 +586,7 @@
                (let ([ed (qt-edit-window-editor win)]
                      [container (qt-edit-window-container win)])
                  (hash-remove! *editor-window-map* ed)
+                 (qt-window-pre-container-destroy! container)
                  (qt-widget-hide! container)
                  (qt-widget-destroy! container))))
            all-wins)
diff --git a/src/jerboa-emacs/qt/app.ss b/src/jerboa-emacs/qt/app.ss
index ff3d440..db48c5f 100644
--- a/src/jerboa-emacs/qt/app.ss
+++ b/src/jerboa-emacs/qt/app.ss
@@ -1255,9 +1255,20 @@
                   ;; QTerminalWidget key forwarding: send non-command keys directly
                   ;; to the terminal widget, bypassing chord detection and self-insert.
                   ;; C-x prefix and M-x pass through to jemacs keymap.
+                  ;;
+                  ;; FOCUS GUARD: only forward to the terminal if the key event came
+                  ;; FROM the QTerminalWidget itself. If the user clicked a different
+                  ;; window (so the key came from that window's QScintilla), we must
+                  ;; NOT forward to the terminal even if qt-current-buffer is still
+                  ;; a terminal buffer (Chez state may lag the Qt focus change).
                   (let* ((qt-buf (qt-current-buffer (app-state-frame app)))
-                         (qt-term (and qt-buf (hash-get *terminal-widget-map* qt-buf))))
+                         (qt-term (and qt-buf (hash-get *terminal-widget-map* qt-buf)))
+                         (key-src-widget (qt-last-key-widget))
+                         (key-from-terminal? (and qt-term
+                                                   (equal? key-src-widget
+                                                           (qt-terminal-widget qt-term)))))
                     (if (and qt-term
+                             key-from-terminal?  ;; key must come FROM this terminal widget
                              ;; Not in a prefix key state (e.g. after C-x)
                              (null? (key-state-prefix-keys (app-state-key-state app)))
                              ;; Allow C-x to pass through to jemacs
@@ -1391,7 +1402,30 @@
                    (term (and buf (hash-get *terminal-widget-map* buf))))
               (if term
                 (qt-terminal-widget term)
-                (qt-current-editor fr))))))
+                (qt-current-editor fr)))))
+
+        ;; Install pre-container-destroy hook so that when any window container
+        ;; (QStackedWidget) is about to be destroyed (by delete-other-windows,
+        ;; C-x 0, kill-terminal-buffer, etc.), any terminal living inside it
+        ;; is detached and destroyed first — preventing the double-free crash
+        ;; that occurs when Qt auto-deletes the terminal as a child.
+        (qt-window-set-pre-container-destroy-fn!
+          (lambda (container)
+            (let ((bufs-to-remove '()))
+              (hash-for-each
+                (lambda (buf stored-container)
+                  (when (equal? stored-container container)
+                    (let ((term (hash-get *terminal-widget-map* buf)))
+                      (when term
+                        (with-catch (lambda (e) #f)
+                          (lambda () (qt-terminal-destroy! term)))))
+                    (set! bufs-to-remove (cons buf bufs-to-remove))))
+                *terminal-container-map*)
+              (for-each
+                (lambda (buf)
+                  (hash-remove! *terminal-widget-map* buf)
+                  (hash-remove! *terminal-container-map* buf))
+                bufs-to-remove)))))
 
       ;; ================================================================
       ;; Periodic tasks — registered with schedule-periodic!, driven
@@ -1925,20 +1959,31 @@
                       (not (null? (key-state-prefix-keys (app-state-key-state app))))))
               ;; Is the current buffer a QTerminalWidget terminal?
               (cons 'test-terminal-running?
+                    ;; True only if the current window's QStackedWidget is actually
+                    ;; showing the QTerminalWidget page (not just the buffer being a
+                    ;; terminal buffer — after C-x 2, new window shows editor page).
                     (lambda ()
-                      (let* ((fr (app-state-frame app))
-                             (buf (qt-current-buffer fr)))
-                        (and buf (hash-key? *terminal-widget-map* buf) #t))))
+                      (let* ((fr  (app-state-frame app))
+                             (win (qt-current-window fr))
+                             (buf (qt-edit-window-buffer win))
+                             (term (and buf (hash-get *terminal-widget-map* buf))))
+                        (and term
+                             (let* ((container (qt-edit-window-container win))
+                                    (count (qt-stacked-widget-count container)))
+                               ;; Terminal widget was added as the last page (index count-1).
+                               ;; If count > 1 and current index > 0, terminal is visible.
+                               (and (> count 1)
+                                    (> (qt-stacked-widget-current-index container) 0)))))))
               ;; Reset editor to a clean single-window state between tests.
               ;; Clears key prefix state, collapses to one window, destroys terminals.
               (cons 'test-reset!
                     (lambda ()
                       ;; Clear any pending prefix key (e.g. C-x)
                       (set! (app-state-key-state app) (make-initial-key-state))
-                      ;; Collapse to single window
-                      (when (> (length (qt-frame-windows (app-state-frame app))) 1)
-                        (execute-command! app 'delete-other-windows))
-                      ;; Destroy all QTerminalWidget instances
+                      ;; Destroy terminals BEFORE delete-other-windows: qt-terminal-destroy!
+                      ;; detaches the widget from its parent QStackedWidget, preventing the
+                      ;; double-free that occurs when delete-other-windows destroys the
+                      ;; container and Qt auto-deletes its children.
                       (let ((term-bufs (hash-keys *terminal-widget-map*)))
                         (for-each
                           (lambda (buf)
@@ -1946,8 +1991,12 @@
                               (when term
                                 (with-catch (lambda (e) #f)
                                   (lambda () (qt-terminal-destroy! term)))
-                                (hash-remove! *terminal-widget-map* buf))))
+                                (hash-remove! *terminal-widget-map* buf)
+                                (hash-remove! *terminal-container-map* buf))))
                           term-bufs))
+                      ;; Collapse to single window (safe now: terminals detached from containers)
+                      (when (> (length (qt-frame-windows (app-state-frame app))) 1)
+                        (execute-command! app 'delete-other-windows))
                       'ok))
               ;; Set current editor text directly (bypasses undo — for test isolation)
               (cons 'test-clear-buffer!
diff --git a/src/jerboa-emacs/qt/commands-config.ss b/src/jerboa-emacs/qt/commands-config.ss
index b9ee224..6ed5859 100644
--- a/src/jerboa-emacs/qt/commands-config.ss
+++ b/src/jerboa-emacs/qt/commands-config.ss
@@ -795,6 +795,9 @@ modified so the next save uses the new encoding."
             (qt-stacked-widget-set-current-index! container idx))
           ;; Store widget mapping for key forwarding and buffer switching
           (hash-put! *terminal-widget-map* buf term)
+          ;; Track which container this terminal lives in so the pre-destroy
+          ;; hook can detach it before the container is freed
+          (hash-put! *terminal-container-map* buf container)
           ;; Install consuming key filter so all keys go through Scheme first
           ;; (same pattern as image scroll widgets — without this, Qt sends keys
           ;; directly to QTerminalWidget::keyPressEvent, bypassing jemacs entirely)
diff --git a/src/jerboa-emacs/qt/commands-shell.ss b/src/jerboa-emacs/qt/commands-shell.ss
index 85ffb86..0c1e03d 100644
--- a/src/jerboa-emacs/qt/commands-shell.ss
+++ b/src/jerboa-emacs/qt/commands-shell.ss
@@ -65,6 +65,11 @@
 ;; so moving it here makes it visible to both without circular dependency.
 (def *terminal-widget-map* (make-hash-table-eq))
 
+;; Map buffer → QStackedWidget container for the terminal's host window.
+;; Lets the pre-container-destroy hook identify which terminal is in a container
+;; that is about to be destroyed, so it can detach/destroy the terminal first.
+(def *terminal-container-map* (make-hash-table-eq))
+
 ;; --- Font size ---
 ;; Note: Font size state is now in face.ss (*default-font-size*)
 
diff --git a/src/jerboa-emacs/qt/commands.ss b/src/jerboa-emacs/qt/commands.ss
index 1c573d4..a9d5245 100644
--- a/src/jerboa-emacs/qt/commands.ss
+++ b/src/jerboa-emacs/qt/commands.ss
@@ -7,6 +7,7 @@
         qt-open-image-inline!
         *qt-app-ptr*
         *terminal-widget-map*
+        *terminal-container-map*
         qt-kill-ring-push!
         *isearch-active*
         isearch-handle-key!
diff --git a/src/jerboa-emacs/qt/sci-shim.ss b/src/jerboa-emacs/qt/sci-shim.ss
index 8bf13c7..81cef68 100644
--- a/src/jerboa-emacs/qt/sci-shim.ss
+++ b/src/jerboa-emacs/qt/sci-shim.ss
@@ -74,7 +74,7 @@
   ;; Label
   qt-label-create qt-label-set-alignment! qt-label-set-pixmap! qt-label-set-text! qt-label-text
   ;; Key events
-  qt-last-key-code qt-last-key-modifiers qt-last-key-text qt-last-key-autorepeat?
+  qt-last-key-code qt-last-key-modifiers qt-last-key-text qt-last-key-autorepeat? qt-last-key-widget
   ;; Line edit
   qt-line-edit-create qt-line-edit-set-completer! qt-line-edit-set-text! qt-line-edit-text
   ;; List widget
diff --git a/src/jerboa-emacs/qt/window.ss b/src/jerboa-emacs/qt/window.ss
index ea3bd78..fecdf84 100644
--- a/src/jerboa-emacs/qt/window.ss
+++ b/src/jerboa-emacs/qt/window.ss
@@ -39,7 +39,9 @@
         split-tree-find-leaf
         split-tree-collect-sub-splitters
         ;; App pointer for process-events during splits
-        qt-window-set-app-ptr!)
+        qt-window-set-app-ptr!
+        ;; Hook called before any window container is destroyed
+        qt-window-set-pre-container-destroy-fn!)
 
 (import :std/sugar
         :chez-scintilla/constants
@@ -58,6 +60,17 @@
 (def (qt-window-set-app-ptr! app)
   (set! *qt-app-for-events* app))
 
+;; Called with (container) before qt-widget-destroy! on any window container.
+;; Registered by terminal code to detach QTerminalWidgets before parent is freed.
+(def *pre-container-destroy-fn* #f)
+
+(def (qt-window-set-pre-container-destroy-fn! fn)
+  (set! *pre-container-destroy-fn* fn))
+
+(def (qt-window-pre-container-destroy! container)
+  (when *pre-container-destroy-fn*
+    (*pre-container-destroy-fn* container)))
+
 (def (qt-window-process-events!)
   "Process pending Qt events if app pointer is available."
   (let ((app *qt-app-for-events*))
@@ -560,6 +573,7 @@
       ;; Destroy the deleted window's Qt container.
       ;; Skipped when container was set to #f above (already destroyed via parent-spl).
       (when container
+        (qt-window-pre-container-destroy! container)
         (qt-widget-hide! container)
         (qt-widget-destroy! container))
 
@@ -587,6 +601,7 @@
                   (let ((ed        (qt-edit-window-editor win))
                         (container (qt-edit-window-container win)))
                     (hash-remove! *editor-window-map* ed)
+                    (qt-window-pre-container-destroy! container)
                     (qt-widget-hide! container)
                     (qt-widget-destroy! container))))
               all-wins)
diff --git a/vendor/chez-qt/chez-qt/ffi.ss b/vendor/chez-qt/chez-qt/ffi.ss
index f47ebba..e096336 100644
--- a/vendor/chez-qt/chez-qt/ffi.ss
+++ b/vendor/chez-qt/chez-qt/ffi.ss
@@ -212,7 +212,7 @@
     ;; Keyboard Events
     ffi-qt-install-key-handler ffi-qt-install-key-handler-consuming
     ffi-qt-last-key-code ffi-qt-last-key-modifiers ffi-qt-last-key-text
-    ffi-qt-last-key-autorepeat
+    ffi-qt-last-key-autorepeat ffi-qt-last-key-widget
     ffi-qt-send-key-event
 
     ;; Pixmap
@@ -1656,6 +1656,8 @@
     (foreign-procedure "qt_last_key_text" () string))
   (define ffi-qt-last-key-autorepeat
     (foreign-procedure "qt_last_key_autorepeat" () int))
+  (define ffi-qt-last-key-widget
+    (foreign-procedure "qt_last_key_widget" () void*))
   (define ffi-qt-send-key-event
     (foreign-procedure "qt_send_key_event" (void* int int int string) void))
 
diff --git a/vendor/chez-qt/chez-qt/qt.ss b/vendor/chez-qt/chez-qt/qt.ss
index b5d5269..2cf8ec0 100644
--- a/vendor/chez-qt/chez-qt/qt.ss
+++ b/vendor/chez-qt/chez-qt/qt.ss
@@ -212,7 +212,7 @@
     ;; Keyboard Events
     qt-on-key-press! qt-on-key-press-consuming!
     qt-last-key-code qt-last-key-modifiers qt-last-key-text
-    qt-last-key-autorepeat?
+    qt-last-key-autorepeat? qt-last-key-widget
     qt-send-key-press! qt-send-key-release!
 
     ;; Pixmap
@@ -1565,6 +1565,7 @@
   (define (qt-last-key-modifiers) (ffi-qt-last-key-modifiers))
   (define (qt-last-key-text) (ffi-qt-last-key-text))
   (define (qt-last-key-autorepeat?) (not (zero? (ffi-qt-last-key-autorepeat))))
+  (define (qt-last-key-widget) (ffi-qt-last-key-widget))
 
   (define (qt-send-key-press! w key mods text)
     (ffi-qt-send-key-event w 0 key mods text))
diff --git a/vendor/qt_shim.cpp b/vendor/qt_shim.cpp
index 2ffb5cd..d9ef9b5 100644
--- a/vendor/qt_shim.cpp
+++ b/vendor/qt_shim.cpp
@@ -601,6 +601,7 @@ static int s_last_key_code = 0;
 static int s_last_key_modifiers = 0;
 static std::string s_last_key_text;
 static int s_last_key_autorepeat = 0;
+static QObject* s_last_key_widget = nullptr;  // which widget fired the last key event
 
 // Storage for QInputDialog ok/cancel flag
 static bool s_last_input_ok = false;
@@ -624,6 +625,7 @@ public:
             s_last_key_modifiers = static_cast<int>(ke->modifiers());
             s_last_key_text = ke->text().toUtf8().toStdString();
             s_last_key_autorepeat = ke->isAutoRepeat() ? 1 : 0;
+            s_last_key_widget = obj;
             m_callback(m_callback_id);
         }
         return QObject::eventFilter(obj, event);
@@ -649,6 +651,7 @@ public:
             s_last_key_modifiers = static_cast<int>(ke->modifiers());
             s_last_key_text = ke->text().toUtf8().toStdString();
             s_last_key_autorepeat = ke->isAutoRepeat() ? 1 : 0;
+            s_last_key_widget = obj;
             m_callback(m_callback_id);
             return true;  // consume the event — widget does NOT see it
         }
@@ -2470,6 +2473,12 @@ extern "C" int qt_last_key_autorepeat(void) {
     QT_RETURN(int, s_last_key_autorepeat);
 }
 
+// Returns the QObject* (widget pointer) that fired the last key event.
+// Lets Chez determine whether a key came from a terminal widget or an editor.
+extern "C" void* qt_last_key_widget(void) {
+    return s_last_key_widget;
+}
+
 extern "C" void qt_send_key_event(qt_widget_t w, int type, int key, int modifiers, const char* text) {
     QT_NULL_CHECK_VOID(w);
     // Capture primitives by value; construct QKeyEvent inside the lambda
@@ -7510,6 +7519,10 @@ public:
         if (m_vt) { vterm_free(m_vt); m_vt = nullptr; }
     }
 
+    // Public alias so qt_terminal_destroy can call PTY cleanup before
+    // the widget is actually deleted (via deleteLater).
+    void cleanupPtyPublic() { cleanupPty(); }
+
     // ── libvterm initialization ────────────────────────────────────────────
 
     void initVterm() {
@@ -7602,9 +7615,30 @@ public:
     void cleanupPty() {
         if (m_timer) m_timer->stop();
         if (m_child_pid > 0) {
-            kill(m_child_pid, SIGTERM);
+            // Close master fd first — this sends SIGHUP to the child, which
+            // is gentler than SIGTERM and causes most shells to exit.
+            if (m_master_fd >= 0) {
+                ::close(m_master_fd);
+                m_master_fd = -1;
+            }
+            // Non-blocking check — child may have already exited on SIGHUP.
             int status;
-            waitpid(m_child_pid, &status, 0);
+            pid_t result = waitpid(m_child_pid, &status, WNOHANG);
+            if (result != m_child_pid) {
+                // Child still alive — escalate to SIGTERM then SIGKILL.
+                kill(m_child_pid, SIGTERM);
+                // Poll for up to 200ms with WNOHANG before giving up.
+                for (int i = 0; i < 20 && result != m_child_pid; ++i) {
+                    struct timespec ts = {0, 10000000}; // 10ms
+                    nanosleep(&ts, nullptr);
+                    result = waitpid(m_child_pid, &status, WNOHANG);
+                }
+                if (result != m_child_pid) {
+                    kill(m_child_pid, SIGKILL);
+                    // One final non-blocking reap; orphan if still alive.
+                    waitpid(m_child_pid, &status, WNOHANG);
+                }
+            }
             m_child_pid = -1;
         }
         if (m_master_fd >= 0) {
@@ -7877,11 +7911,17 @@ private:
         if (m_master_fd < 0) return;
 
         char buf[8192];
-        ssize_t n;
+        ssize_t n = 0;
         bool got_data = false;
+        // Cap reads per poll to ~64KB so high-output commands (find / -ls, etc.)
+        // don't monopolise the Qt event loop and block M-x / other key events.
+        static const size_t MAX_BYTES_PER_POLL = 65536;
+        size_t total = 0;
 
-        while ((n = ::read(m_master_fd, buf, sizeof(buf))) > 0) {
+        while (total < MAX_BYTES_PER_POLL &&
+               (n = ::read(m_master_fd, buf, sizeof(buf))) > 0) {
             vterm_input_write(m_vt, buf, (size_t)n);
+            total += (size_t)n;
             got_data = true;
         }
 
@@ -7996,7 +8036,18 @@ extern "C" void qt_terminal_focus(qt_terminal_t term) {
 
 extern "C" void qt_terminal_destroy(qt_terminal_t term) {
     QT_NULL_CHECK_VOID(term);
-    QT_VOID(
-        delete static_cast<QTerminalWidget*>(term)
+    // 1. Detach from parent (QStackedWidget) so delete-other-windows cannot
+    //    double-free the widget when it later destroys the container.
+    // 2. Schedule deletion via the event loop (deleteLater) to avoid
+    //    "shared QObject deleted directly" crash from pending Qt events.
+    //    The destructor calls cleanupPty() which uses WNOHANG — no blocking.
+    QT_VOID(
+        auto* tw = static_cast<QTerminalWidget*>(term);
+        if (QWidget* p = tw->parentWidget()) {
+            if (auto* stacked = qobject_cast<QStackedWidget*>(p))
+                stacked->removeWidget(tw);
+            tw->setParent(nullptr);
+        }
+        tw->deleteLater()
     );
 }