Skip to content

vterm: claude-code--term-make aborts on a single-window frame (delete-window on sole window), so the keymap never installs and S-RET/RET both submit #143

Description

@ShiroiKuma0

Summary

Starting a Claude session from a single-window frame aborts mid-setup with
delete-window: Attempt to delete minibuffer or sole ordinary window.... The
session’s vterm buffer is left alive under the raw vterm-mode-map, so the
claude-code-newline-keybinding-style bindings are never installed — both RET
and S-RET just submit, and there is no way to insert a newline.

The error originates in claude-code--term-make (vterm backend), which calls
delete-window unconditionally after pop-to-buffer. On a single-window frame,
pop-to-buffer reuses the sole window rather than creating a new one, so there
is no window to delete and delete-window signals — aborting claude-code--start
before it reaches the buffer-setup block that installs the keymap (and the
vterm-copy-mode restore hook).

Environment

  • claude-code.el: 0.4.5 (main @ 03199df)
  • Backend: vterm (claude-code-terminal-backend = 'vterm)
  • claude-code-newline-keybinding-style: 'newline-on-shift-return
  • Emacs: GNU Emacs 29.3 (X toolkit / Xaw3d), Debian, running under Wayland (XWayland)
  • Launch context: Emacs server (server-start) + emacsclient; sessions started from a frame that currently shows a single window

Note: the package declares (emacs "30.0"); I’m on 29.3, but this is a pure
window-topology logic error independent of Emacs version — the same
sole-window delete-window signal occurs identically on Emacs 30.

Steps to reproduce

  1. Be in a frame with one window (no splits).
  2. M-x claude-code (vterm backend).

Expected

Session starts, the claude-code keymap is installed, and S-RET inserts a
newline while RET submits (per newline-on-shift-return).

Actual

*Backtrace* (with debug-on-error):

Debugger entered--Lisp error: (error "Attempt to delete minibuffer or sole ordinary window...")
  delete-window(#<window 7 on *claude:.../:default*>)
  (save-current-buffer (set-buffer buffer) (pop-to-buffer buffer) (vterm-mode)
                       (delete-window (get-buffer-window buffer)) buffer)
  ...
  claude-code--term-make(vterm "*claude:.../:default*" "claude" nil)
  ... claude-code--start(nil nil)
  claude-code(nil)

The buffer still exists with Claude running, but under the bare vterm-mode-map.
C-h k on S-RET in that buffer confirms the claude-code map was never applied:

<return> (translated from S-<return>) runs the command
vterm-send-return (found in vterm-mode-map)

(Both RET and S-RET resolve to vterm-send-return → submit.)

Root cause

claude-code--term-make for the vterm backend:

(pop-to-buffer buffer)
(vterm-mode)
(delete-window (get-buffer-window buffer))   ; <-- assumes pop-to-buffer made a new window
buffer

The comment explains pop-to-buffer is used so vterm has a live window to size
the PTY. But pop-to-buffer only creates a window when it can’t reuse one. On a
single-window frame it reuses the sole window, so delete-window on that window
signals “sole ordinary window”. Because term-make is called early in
claude-code--start, the signal aborts before the with-current-buffer block
that runs claude-code--term-configure and claude-code--term-setup-keymap — so
neither the keybindings nor the vterm-copy-mode restore hook are installed.

This is distinct from #69 (there the map installs and works, then copy-mode
clobbers it). Here it’s never installed. (The same unconditional delete-window
pattern also appears later in the file, ~L1951, and may be worth auditing.)

Suggested fix

Only delete the window if pop-to-buffer actually created one — i.e. when the
buffer’s window is part of a split, not the frame’s sole window:

(let ((win (get-buffer-window buffer)))
  (when (and win (window-parent win))   ; non-nil => it's a split, safe to delete
    (delete-window win)))

(window-parent returns nil for a frame’s sole/root window.) A minimal
alternative is to wrap just this call in ignore-errors.

Workaround (config-side)

(with-eval-after-load 'claude-code
  (advice-add 'claude-code--term-make :around
              (lambda (orig &rest args)
                (cl-letf* ((real-delete (symbol-function 'delete-window))
                           ((symbol-function 'delete-window)
                            (lambda (&optional window)
                              (condition-case nil (funcall real-delete window)
                                (error nil)))))
                  (apply orig args)))
              '((name . tolerate-sole-window))))

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions