From d056b610b797be02762e71dac7524c4e3c63ba16 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Sat, 25 Apr 2026 20:16:50 -0500 Subject: [PATCH] fix: avoid prompt_toolkit complex tempfile bug and prefer nvim first Setting buffer.tempfile = 'prompt.md' pushed prompt_toolkit into its complex-tempfile path, which creates a temp dir and then calls os.makedirs() on that same path when no subdirectory is present. That raises EEXIST before the editor can launch. Keep prompt_toolkit on the simple tempfile path with .md suffix, and make the editor fallback chain explicit on both surfaces: $VISUAL -> $EDITOR -> nvim -> vim -> vi -> nano. --- cli.py | 16 +++++++++------- ui-tui/src/lib/editor.test.ts | 7 ++++--- ui-tui/src/lib/editor.ts | 4 ++-- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/cli.py b/cli.py index 4690ebc325..eb56695efc 100644 --- a/cli.py +++ b/cli.py @@ -9783,23 +9783,24 @@ class HermesCLI: completer=_completer, ), ) - # Match the TUI's editor handoff: the temp file lands at - # /prompt.md so vim/nano/helix pick up markdown syntax - # highlighting from the .md extension and the title bar reads - # "prompt.md" instead of a random "tmpXXXXXX". prompt_toolkit's - # complex-tempfile path takes care of cleanup via shutil.rmtree. - input_area.buffer.tempfile = 'prompt.md' + # Keep prompt_toolkit on its simple tempfile path. Setting + # buffer.tempfile = "prompt.md" triggers its complex-tempfile branch, + # which tries to mkdir() the mkdtemp() directory again and raises + # EEXIST. The suffix keeps markdown highlighting without that bug. + input_area.buffer.tempfile_suffix = '.md' # prompt_toolkit's default fallback chain prefers /usr/bin/nano over # /usr/bin/vi when neither $VISUAL nor $EDITOR is set. The TUI's - # resolveEditor() prefers vim → vi → nano. Override this single + # resolveEditor() prefers nvim → vim → vi → nano. Override this single # buffer's resolver so both surfaces pick the same editor. import shlex import subprocess + def _hermes_pick_editor(filename: str) -> bool: chosen = ( os.environ.get('VISUAL') or os.environ.get('EDITOR') + or shutil.which('nvim') or shutil.which('vim') or shutil.which('vi') or shutil.which('nano') @@ -9810,6 +9811,7 @@ class HermesCLI: return subprocess.call(shlex.split(chosen) + [filename]) == 0 except OSError: return False + input_area.buffer._open_file_in_editor = _hermes_pick_editor # Dynamic height: accounts for both explicit newlines AND visual diff --git a/ui-tui/src/lib/editor.test.ts b/ui-tui/src/lib/editor.test.ts index 6bcccc0b74..4262de1988 100644 --- a/ui-tui/src/lib/editor.test.ts +++ b/ui-tui/src/lib/editor.test.ts @@ -33,12 +33,13 @@ describe('resolveEditor', () => { expect(resolveEditor({ EDITOR: 'nvim', PATH: dir })).toBe('nvim') }) - it('prefers vim over vi over nano on $PATH', () => { + it('prefers nvim over vim over vi over nano on $PATH', () => { exe('nano') exe('vi') - const vim = exe('vim') + exe('vim') + const nvim = exe('nvim') - expect(resolveEditor({ PATH: dir })).toBe(vim) + expect(resolveEditor({ PATH: dir })).toBe(nvim) }) it('falls back to vi when only vi and nano exist', () => { diff --git a/ui-tui/src/lib/editor.ts b/ui-tui/src/lib/editor.ts index d05e76851b..6898b7cb42 100644 --- a/ui-tui/src/lib/editor.ts +++ b/ui-tui/src/lib/editor.ts @@ -6,7 +6,7 @@ import { delimiter, join } from 'node:path' * * Order of preference: * 1. $VISUAL / $EDITOR (user's explicit choice) - * 2. first executable found on $PATH from `vim` → `vi` → `nano` + * 2. first executable found on $PATH from `nvim` → `vim` → `vi` → `nano` * 3. literal `'vi'` so spawnSync still has something to try * * Mirrors the override on `input_area.buffer._open_file_in_editor` in cli.py @@ -14,7 +14,7 @@ import { delimiter, join } from 'node:path' * doesn't surprise the user with nano in one and vim in the other. */ export function resolveEditor(env: NodeJS.ProcessEnv = process.env): string { - return env.VISUAL || env.EDITOR || findExecutable(env.PATH ?? '', 'vim', 'vi', 'nano') || 'vi' + return env.VISUAL || env.EDITOR || findExecutable(env.PATH ?? '', 'nvim', 'vim', 'vi', 'nano') || 'vi' } function findExecutable(path: string, ...names: string[]): null | string {