Fix Qt tab bar staleness

ober

fc39fb15625ebad64bbd36288e87543898de841c

diff --git a/src/jerboa-emacs/qt/app.ss b/src/jerboa-emacs/qt/app.ss
index 594420f..bb5f829 100644
--- a/src/jerboa-emacs/qt/app.ss
+++ b/src/jerboa-emacs/qt/app.ss
@@ -521,10 +521,28 @@
 
 ;; Tab bar state — populated during qt-main, used by qt-tabbar-update!
 (def *tab-bar-layout* #f)
-(def *tab-bar-buttons* '())  ;; list of (buffer . button) pairs
-(def *tab-bar-last-state* #f)  ;; cache: (current-buf . buffer-count) to skip redundant updates
+(def *tab-bar-buttons* '())  ;; list of (buffer . tab-widget) pairs
+(def *tab-bar-last-state* #f)  ;; cache of current tab plus tab labels/modified flags
 (def *tab-bar-widget* #f)     ;; the tab bar widget itself (for show/hide)
 
+(def (qt-tabbar-buffer-state buf)
+  "Return the tab cache state for BUF."
+  (let ((doc (buffer-doc-pointer buf)))
+    (list buf
+          (buffer-name buf)
+          (and doc (qt-text-document-modified? doc)))))
+(def (qt-tabbar-switch-to-buffer! app buf)
+  "Display BUF in the current Qt window and refresh tab/UI state."
+  (let* ((fr (app-state-frame app))
+         (ed (qt-current-editor fr)))
+    (when ed
+      (qt-buffer-attach! ed buf)
+      (set! (qt-edit-window-buffer (qt-current-window fr)) buf)
+      (qt-update-visual-decorations! ed)
+      (qt-modeline-update! app)
+      (set! *tab-bar-last-state* #f)
+      (qt-tabbar-update! app))))
+
 (def (qt-tabbar-update! app)
   "Rebuild the tab bar to reflect current buffer list."
   ;; Show/hide the tab bar widget
@@ -536,24 +554,26 @@
     (let* ((fr (app-state-frame app))
            (current-buf (qt-edit-window-buffer (qt-current-window fr)))
            (bufs (buffer-list))
-           (new-state (cons current-buf (length bufs))))
-      ;; Skip update if nothing changed
+           (new-state (list current-buf (map qt-tabbar-buffer-state bufs))))
+      ;; Skip update if nothing visible changed.
       (unless (and *tab-bar-last-state*
-                   (eq? (car new-state) (car *tab-bar-last-state*))
-                   (= (cdr new-state) (cdr *tab-bar-last-state*)))
+                   (equal? new-state *tab-bar-last-state*))
         (set! *tab-bar-last-state* new-state)
-        ;; Destroy old buttons
+        ;; Destroy old tab widgets
         (for-each (lambda (pair) (qt-widget-destroy! (cdr pair))) *tab-bar-buttons*)
         (set! *tab-bar-buttons* '())
-        ;; Create new buttons for each buffer
+        ;; Create new tab widgets for each buffer
         (for-each
           (lambda (buf)
             (let* ((name (buffer-name buf))
-                   (mod? (and (buffer-doc-pointer buf)
-                              (qt-text-document-modified? (buffer-doc-pointer buf))))
+                   (doc (buffer-doc-pointer buf))
+                   (mod? (and doc (qt-text-document-modified? doc)))
                    (label (if mod? (string-append name " *") name))
-                   (btn (qt-push-button-create label)))
-              ;; Style: current buffer gets highlighted
+                   (tab-item (qt-widget-create *tab-bar-widget*))
+                   (tab-layout (qt-hbox-layout-create tab-item))
+                   (btn (qt-push-button-create label))
+                   (close-btn (qt-push-button-create "x")))
+              ;; Style: current buffer gets highlighted; close uses a compact hit target.
               (let ((font-css (string-append " font-family: " *default-font-family*
                                              "; font-size: " (number->string (max 1 (- *default-font-size* 2))) "pt;")))
                 (if (eq? buf current-buf)
@@ -561,20 +581,25 @@
                     (string-append "QPushButton { color: #ffffff; background: #404060; border: 1px solid #606080; border-radius: 3px; padding: 2px 8px;" font-css " }"))
                   (qt-widget-set-style-sheet! btn
                     (string-append "QPushButton { color: #a0a0a0; background: #252525; border: 1px solid #383838; border-radius: 3px; padding: 2px 8px;" font-css " }\n"
-                                   "                   QPushButton:hover { color: #d8d8d8; background: #353535; }"))))
-              ;; Click handler: switch to this buffer
+                                   "                   QPushButton:hover { color: #d8d8d8; background: #353535; }")))
+                (qt-widget-set-style-sheet! close-btn
+                  (string-append "QPushButton { color: #909090; background: #1c1c1c; border: 1px solid #303030; border-radius: 3px; padding: 2px 5px;" font-css " }\n"
+                                 "QPushButton:hover { color: #ffffff; background: #5a3030; border-color: #805050; }")))
+              ;; Click handlers: switch or close this buffer.
               (qt-on-clicked! btn
                 (lambda ()
-                  (let* ((ed (qt-current-editor fr)))
-                    (qt-buffer-attach! ed buf)
-                    (set! (qt-edit-window-buffer (qt-current-window fr)) buf)
-                    (qt-update-visual-decorations! ed)
-                    (qt-modeline-update! app)
-                    (set! *tab-bar-last-state* #f)  ;; force refresh
-                    (qt-tabbar-update! app))))
+                  (qt-tabbar-switch-to-buffer! app buf)))
+              (qt-on-clicked! close-btn
+                (lambda ()
+                  (qt-tabbar-switch-to-buffer! app buf)
+                  (execute-command! app 'kill-buffer-cmd)
+                  (set! *tab-bar-last-state* #f)
+                  (qt-tabbar-update! app)))
               ;; Add to layout
-              (qt-layout-add-widget! *tab-bar-layout* btn)
-              (set! *tab-bar-buttons* (cons (cons buf btn) *tab-bar-buttons*))))
+              (qt-layout-add-widget! tab-layout btn)
+              (qt-layout-add-widget! tab-layout close-btn)
+              (qt-layout-add-widget! *tab-bar-layout* tab-item)
+              (set! *tab-bar-buttons* (cons (cons buf tab-item) *tab-bar-buttons*))))
           bufs)
         ;; Add stretch at the end to push tabs left
         (qt-layout-add-stretch! *tab-bar-layout*)))))
@@ -2345,7 +2370,7 @@
                       (qt-frame-current-idx (app-state-frame app))))))))
       (qt-startup-trace "debug repl setup done")
       ;; Tree-sitter debounced re-highlight — re-parse when buffer content changes.
-      ;; Tracks last-known text length per buffer to detect modifications.
+      ;; Tracks a monotonic edit version so same-length edits still reparse.
       (qt-startup-trace "periodic treesitter begin")
       (schedule-periodic! 'treesitter-reparse 150
         (lambda ()