Fix shell/terminal PTY using hardcoded 80-column width instead of actual buffer width

ober

71934188c5c0e736150dab57afcb088d376cb4eb

diff --git a/lib/jerboa-emacs/qt/app.sls b/lib/jerboa-emacs/qt/app.sls
index 946a5a5..3264d53 100644
--- a/lib/jerboa-emacs/qt/app.sls
+++ b/lib/jerboa-emacs/qt/app.sls
@@ -1777,6 +1777,68 @@
                        (when (shell-buffer? buf)
                          (let ([ss (hash-get *shell-state* buf)])
                            (when (and ss (shell-pty-busy? ss))
+                             (let ([vt (shell-state-vtscreen ss)])
+                               (when vt
+                                 (let ed-loop ([wins (qt-frame-windows
+                                                       fr)])
+                                   (when (pair? wins)
+                                     (if (eq? (qt-edit-window-buffer
+                                                (car wins))
+                                              buf)
+                                         (let* ([ed (qt-edit-window-editor
+                                                      (car wins))]
+                                                [new-rows (max 2
+                                                               (sci-send
+                                                                 ed
+                                                                 2370
+                                                                 0))]
+                                                [widget-w (qt-widget-width
+                                                            ed)]
+                                                [margin-w (sci-send
+                                                            ed
+                                                            SCI_GETMARGINWIDTHN
+                                                            0)]
+                                                [text-w (- widget-w
+                                                           margin-w
+                                                           16)]
+                                                [char-w (let ([w (sci-send/string
+                                                                   ed
+                                                                   2276
+                                                                   "M"
+                                                                   STYLE_DEFAULT)])
+                                                          (if (> w 0)
+                                                              w
+                                                              8))]
+                                                [new-cols (max 20
+                                                               (quotient
+                                                                 text-w
+                                                                 char-w))]
+                                                [old-rows (vtscreen-rows
+                                                            vt)]
+                                                [old-cols (vtscreen-cols
+                                                            vt)])
+                                           (when (or (not (= new-rows
+                                                             old-rows))
+                                                     (not (= new-cols
+                                                             old-cols)))
+                                             (verbose-log! "SHELL-PTY-RESIZE: "
+                                               (number->string old-rows)
+                                               "x"
+                                               (number->string old-cols)
+                                               " -> "
+                                               (number->string new-rows)
+                                               "x"
+                                               (number->string new-cols))
+                                             (vtscreen-resize!
+                                               vt
+                                               new-rows
+                                               new-cols)
+                                             (shell-pty-resize!
+                                               ss
+                                               new-rows
+                                               new-cols)))
+                                         (ed-loop (cdr wins))))))))
+                           (when (and ss (shell-pty-busy? ss))
                              (let drain ()
                                (let ([msg (shell-poll-output ss)])
                                  (when msg
@@ -1803,15 +1865,24 @@
                                                                  0))]
                                                 [widget-w (qt-widget-width
                                                             ed)]
-                                                [char-w (max 1
-                                                             (sci-send/string
-                                                               ed
-                                                               2275
-                                                               "0"
-                                                               0))]
+                                                [margin-w (sci-send
+                                                            ed
+                                                            SCI_GETMARGINWIDTHN
+                                                            0)]
+                                                [text-w (- widget-w
+                                                           margin-w
+                                                           16)]
+                                                [char-w (let ([w (sci-send/string
+                                                                   ed
+                                                                   2276
+                                                                   "M"
+                                                                   STYLE_DEFAULT)])
+                                                          (if (> w 0)
+                                                              w
+                                                              8))]
                                                 [new-cols (max 20
                                                                (quotient
-                                                                 widget-w
+                                                                 text-w
                                                                  char-w))]
                                                 [old-rows (vtscreen-rows
                                                             vt)]
diff --git a/lib/jerboa-emacs/qt/commands-edit.sls b/lib/jerboa-emacs/qt/commands-edit.sls
index 47031bf..4072337 100644
--- a/lib/jerboa-emacs/qt/commands-edit.sls
+++ b/lib/jerboa-emacs/qt/commands-edit.sls
@@ -930,67 +930,89 @@
                       [input (if (> end-pos prompt-pos)
                                  (substring all-text prompt-pos end-pos)
                                  "")])
-                 (let ([trimmed-input (safe-string-trim-both input)])
-                   (when (> (string-length trimmed-input) 0)
-                     (gsh-history-add! trimmed-input (current-directory))))
-                 (qt-plain-text-edit-move-cursor! ed QT_CURSOR_END)
-                 (qt-plain-text-edit-insert-text! ed "\n")
-                 (let-values ([(mode output new-cwd)
-                               (shell-execute-async! input ss)])
-                   (case mode
-                     [(sync)
-                      (when (and (string? output)
-                                 (> (string-length output) 0))
-                        (qt-plain-text-edit-move-cursor! ed QT_CURSOR_END)
-                        (qt-plain-text-edit-insert-text! ed output)
-                        (unless (char=?
-                                  (string-ref
-                                    output
-                                    (- (string-length output) 1))
-                                  #\newline)
-                          (qt-plain-text-edit-insert-text! ed "\n")))
-                      (when (hash-get *shell-state* buf)
-                        (let ([prompt (shell-prompt ss)])
+                 (let* ([rows (max 2 (sci-send ed 2370 0))]
+                        [widget-w (qt-widget-width ed)]
+                        [margin-w (sci-send ed SCI_GETMARGINWIDTHN 0)]
+                        [text-w (- widget-w margin-w 16)]
+                        [char-w (let ([w (sci-send/string
+                                           ed
+                                           2276
+                                           "M"
+                                           STYLE_DEFAULT)])
+                                  (if (> w 0) w 8))]
+                        [cols (max 20 (quotient text-w char-w))])
+                   (let ([trimmed-input (safe-string-trim-both input)])
+                     (when (> (string-length trimmed-input) 0)
+                       (gsh-history-add!
+                         trimmed-input
+                         (current-directory))))
+                   (qt-plain-text-edit-move-cursor! ed QT_CURSOR_END)
+                   (qt-plain-text-edit-insert-text! ed "\n")
+                   (let-values ([(mode output new-cwd)
+                                 (shell-execute-async!
+                                   input
+                                   ss
+                                   rows
+                                   cols)])
+                     (case mode
+                       [(sync)
+                        (when (and (string? output)
+                                   (> (string-length output) 0))
                           (qt-plain-text-edit-move-cursor!
                             ed
                             QT_CURSOR_END)
-                          (qt-plain-text-edit-insert-text! ed prompt)
-                          (shell-state-prompt-pos-set!
-                            ss
-                            (string-length (qt-plain-text-edit-text ed)))
-                          (qt-plain-text-edit-ensure-cursor-visible! ed)))]
-                     [(async)
-                      (qt-plain-text-edit-move-cursor! ed QT_CURSOR_END)
-                      (qt-plain-text-edit-ensure-cursor-visible! ed)]
-                     [(special)
-                      (cond
-                        [(eq? output 'clear)
-                         (qt-plain-text-edit-set-text! ed "")
-                         (let ([prompt (shell-prompt ss)])
-                           (qt-plain-text-edit-insert-text! ed prompt)
-                           (shell-state-prompt-pos-set!
-                             ss
-                             (string-length (qt-plain-text-edit-text ed)))
-                           (qt-plain-text-edit-ensure-cursor-visible! ed))]
-                        [(eq? output 'exit)
-                         (shell-stop! ss)
-                         (let* ([fr (app-state-frame app)]
-                                [other (let loop ([bs (buffer-list)])
-                                         (cond
-                                           [(null? bs) #f]
-                                           [(eq? (car bs) buf)
-                                            (loop (cdr bs))]
-                                           [else (car bs)]))])
-                           (when other
-                             (qt-buffer-attach! ed other)
-                             (qt-edit-window-buffer-set!
-                               (qt-current-window fr)
-                               other))
-                           (hash-remove! *shell-state* buf)
-                           (qt-buffer-kill! buf)
-                           (echo-message!
-                             (app-state-echo app)
-                             "Shell exited"))])])))))))
+                          (qt-plain-text-edit-insert-text! ed output)
+                          (unless (char=?
+                                    (string-ref
+                                      output
+                                      (- (string-length output) 1))
+                                    #\newline)
+                            (qt-plain-text-edit-insert-text! ed "\n")))
+                        (when (hash-get *shell-state* buf)
+                          (let ([prompt (shell-prompt ss)])
+                            (qt-plain-text-edit-move-cursor!
+                              ed
+                              QT_CURSOR_END)
+                            (qt-plain-text-edit-insert-text! ed prompt)
+                            (shell-state-prompt-pos-set!
+                              ss
+                              (string-length (qt-plain-text-edit-text ed)))
+                            (qt-plain-text-edit-ensure-cursor-visible!
+                              ed)))]
+                       [(async)
+                        (qt-plain-text-edit-move-cursor! ed QT_CURSOR_END)
+                        (qt-plain-text-edit-ensure-cursor-visible! ed)]
+                       [(special)
+                        (cond
+                          [(eq? output 'clear)
+                           (qt-plain-text-edit-set-text! ed "")
+                           (let ([prompt (shell-prompt ss)])
+                             (qt-plain-text-edit-insert-text! ed prompt)
+                             (shell-state-prompt-pos-set!
+                               ss
+                               (string-length
+                                 (qt-plain-text-edit-text ed)))
+                             (qt-plain-text-edit-ensure-cursor-visible!
+                               ed))]
+                          [(eq? output 'exit)
+                           (shell-stop! ss)
+                           (let* ([fr (app-state-frame app)]
+                                  [other (let loop ([bs (buffer-list)])
+                                           (cond
+                                             [(null? bs) #f]
+                                             [(eq? (car bs) buf)
+                                              (loop (cdr bs))]
+                                             [else (car bs)]))])
+                             (when other
+                               (qt-buffer-attach! ed other)
+                               (qt-edit-window-buffer-set!
+                                 (qt-current-window fr)
+                                 other))
+                             (hash-remove! *shell-state* buf)
+                             (qt-buffer-kill! buf)
+                             (echo-message!
+                               (app-state-echo app)
+                               "Shell exited"))])]))))))))
   (def qt-chat-buffer-name "*AI Chat*")
   (def qt-chat-prompt "\n\nYou: ")
   (def (cmd-chat app)
diff --git a/src/jerboa-emacs/qt/app.ss b/src/jerboa-emacs/qt/app.ss
index 7c7f722..f465471 100644
--- a/src/jerboa-emacs/qt/app.ss
+++ b/src/jerboa-emacs/qt/app.ss
@@ -1381,6 +1381,32 @@
             (lambda (buf)
               (when (shell-buffer? buf)
                 (let ((ss (hash-get *shell-state* buf)))
+                  ;; Resize shell PTY + vtscreen when editor dimensions change
+                  (when (and ss (shell-pty-busy? ss))
+                    (let ((vt (shell-state-vtscreen ss)))
+                      (when vt
+                        (let ed-loop ((wins (qt-frame-windows fr)))
+                          (when (pair? wins)
+                            (if (eq? (qt-edit-window-buffer (car wins)) buf)
+                              (let* ((ed (qt-edit-window-editor (car wins)))
+                                     (new-rows (max 2 (sci-send ed 2370 0)))
+                                     (widget-w (qt-widget-width ed))
+                                     (margin-w (sci-send ed SCI_GETMARGINWIDTHN 0))
+                                     (text-w (- widget-w margin-w 16))
+                                     (char-w (let ((w (sci-send/string ed 2276 "M" STYLE_DEFAULT)))
+                                               (if (> w 0) w 8)))
+                                     (new-cols (max 20 (quotient text-w char-w)))
+                                     (old-rows (vtscreen-rows vt))
+                                     (old-cols (vtscreen-cols vt)))
+                                (when (or (not (= new-rows old-rows))
+                                          (not (= new-cols old-cols)))
+                                  (verbose-log! "SHELL-PTY-RESIZE: "
+                                    (number->string old-rows) "x" (number->string old-cols)
+                                    " -> "
+                                    (number->string new-rows) "x" (number->string new-cols))
+                                  (vtscreen-resize! vt new-rows new-cols)
+                                  (shell-pty-resize! ss new-rows new-cols)))
+                              (ed-loop (cdr wins))))))))
                   (when (and ss (shell-pty-busy? ss))
                     (let drain ()
                       (let ((msg (shell-poll-output ss)))
@@ -1403,10 +1429,11 @@
                               (let* ((ed (qt-edit-window-editor (car wins)))
                                      (new-rows (max 2 (sci-send ed 2370 0)))
                                      (widget-w (qt-widget-width ed))
-                                     ;; Measure actual character width using Scintilla
-                                     ;; SCI_TEXTWIDTH(style, text) = 2275
-                                     (char-w (max 1 (sci-send/string ed 2275 "0" 0)))
-                                     (new-cols (max 20 (quotient widget-w char-w)))
+                                     (margin-w (sci-send ed SCI_GETMARGINWIDTHN 0))
+                                     (text-w (- widget-w margin-w 16))
+                                     (char-w (let ((w (sci-send/string ed 2276 "M" STYLE_DEFAULT)))
+                                               (if (> w 0) w 8)))
+                                     (new-cols (max 20 (quotient text-w char-w)))
                                      (old-rows (vtscreen-rows vt))
                                      (old-cols (vtscreen-cols vt)))
                                 (when (or (not (= new-rows old-rows))
diff --git a/src/jerboa-emacs/qt/commands-edit.ss b/src/jerboa-emacs/qt/commands-edit.ss
index 76264c5..643a15c 100644
--- a/src/jerboa-emacs/qt/commands-edit.ss
+++ b/src/jerboa-emacs/qt/commands-edit.ss
@@ -845,6 +845,14 @@
                (input (if (> end-pos prompt-pos)
                         (substring all-text prompt-pos end-pos)
                         "")))
+          ;; Compute actual terminal dimensions from editor widget
+          (let* ((rows (max 2 (sci-send ed 2370 0))) ; SCI_LINESONSCREEN
+                 (widget-w (qt-widget-width ed))
+                 (margin-w (sci-send ed SCI_GETMARGINWIDTHN 0))
+                 (text-w (- widget-w margin-w 16)) ; 16px for scrollbar
+                 (char-w (let ((w (sci-send/string ed 2276 "M" STYLE_DEFAULT)))
+                            (if (> w 0) w 8)))
+                 (cols (max 20 (quotient text-w char-w))))
           ;; Record in shell history
           (let ((trimmed-input (safe-string-trim-both input)))
             (when (> (string-length trimmed-input) 0)
@@ -852,7 +860,7 @@
           ;; Append newline after user input
           (qt-plain-text-edit-move-cursor! ed QT_CURSOR_END)
           (qt-plain-text-edit-insert-text! ed "\n")
-          (let-values (((mode output new-cwd) (shell-execute-async! input ss)))
+          (let-values (((mode output new-cwd) (shell-execute-async! input ss rows cols)))
           (case mode
             ((sync)
              (when (and (string? output) (> (string-length output) 0))
@@ -893,7 +901,7 @@
                     (set! (qt-edit-window-buffer (qt-current-window fr)) other))
                   (hash-remove! *shell-state* buf)
                   (qt-buffer-kill! buf)
-                  (echo-message! (app-state-echo app) "Shell exited"))))))))))))
+                  (echo-message! (app-state-echo app) "Shell exited")))))))))))))
 ;;;============================================================================
 ;;; AI Chat commands (Claude CLI integration)
 ;;;============================================================================
diff --git a/src/jerboa-emacs/shell.ss b/src/jerboa-emacs/shell.ss
index 782c670..2624591 100644
--- a/src/jerboa-emacs/shell.ss
+++ b/src/jerboa-emacs/shell.ss
@@ -14,6 +14,7 @@
         shell-pty-busy?
         shell-interrupt!
         shell-send-input!
+        shell-pty-resize!
         shell-cleanup-pty!
         shell-stop!
         shell-prompt
@@ -266,7 +267,7 @@
                       (let ((status (shell-pty-waitpid-status pid #t)))
                         (channel-put ch (cons 'done status)))))))))))))))
 
-(def (shell-execute-async! input ss)
+(def (shell-execute-async! input ss (pty-rows 24) (pty-cols 80))
   "Execute command: builtins go through gsh-capture (sync),
    external commands go through PTY subprocess (async).
    Returns:
@@ -293,7 +294,7 @@
              (values 'sync output cwd))
            ;; External/compound: spawn PTY subprocess (async)
            (let* ((env-alist (env-exported-alist env))
-                  (rows 24) (cols 80))
+                  (rows pty-rows) (cols pty-cols))
              (let-values (((mfd pid) (with-catch
                                        (lambda (e)
                                          (jemacs-log! "PTY-SPAWN-ERROR: "
@@ -342,6 +343,12 @@
     (when mfd
       (pty-write mfd str))))
 
+(def (shell-pty-resize! ss rows cols)
+  "Notify PTY child of window size change."
+  (let ((mfd (shell-state-pty-master ss)))
+    (when (and mfd (integer? mfd))
+      (pty-resize! mfd rows cols))))
+
 (def (shell-cleanup-pty! ss)
   "Clean up PTY state after command finishes or on stop."
   (let ((mfd (shell-state-pty-master ss))