fix(cli): try bundled TUI before requiring ui-tui workspace (#67116)

_make_tui_argv() called _ensure_tui_workspace(tui_dir) unconditionally
before checking for a prebuilt bundle. That function sys.exit(1)s when
ui-tui/ doesn't exist, which it never does on a pip/pipx install — the
wheel ships hermes_cli/tui_dist/entry.js but never ships ui-tui/ at all
(that directory only exists in a git checkout).

Every dashboard Chat tab connection on a pip/pipx install therefore
hard-exited before ever reaching _find_bundled_tui(), surfacing as the
unhelpful "Chat unavailable: 1" banner despite having a fully valid
bundled entry.js on disk.

Move the bundled-wheel/HERMES_TUI_DIR shortcut ahead of the workspace
check. --dev is unaffected (it never uses the bundled path and still
requires the workspace), and the checkout-without-bundle path is
unaffected (bundled lookup returns None, falls through to the existing
git-restore/npm-install/build flow).

Adds a contributors/emails mapping for the original author.

Fixes #56665

Co-authored-by: lucaskvasirr <lucaskvasir@duck.com>
This commit is contained in:
Austin Pickett 2026-07-18 17:17:25 -04:00 committed by GitHub
parent 11cb9e571f
commit ca0703feae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 87 additions and 3 deletions

View file

@ -0,0 +1,2 @@
lucaskvasirr
# PR #56672 salvage (fix(cli): try bundled TUI before requiring ui-tui workspace; #56665)

View file

@ -1766,10 +1766,14 @@ def _make_tui_argv(tui_dir: Path, tui_dev: bool) -> tuple[list[str], Path]:
)
sys.exit(1)
if not ext_dir:
_ensure_tui_workspace(tui_dir)
# 1. Prebuilt bundle (nix / packaged release): just run it.
#
# This must run BEFORE _ensure_tui_workspace() below. A pip/pipx install
# ships hermes_cli/tui_dist/entry.js in the wheel but never ships ui-tui/
# at all (that directory only exists in a git checkout) — so requiring
# the workspace to exist first made every pip/pipx dashboard Chat tab
# connection hard-exit before it ever got a chance to try the bundled
# entry.js it already has. See #56665.
if not tui_dev:
if ext_dir:
p = Path(ext_dir)
@ -1783,6 +1787,11 @@ def _make_tui_argv(tui_dir: Path, tui_dev: bool) -> tuple[list[str], Path]:
node = _node_bin("node")
return [node, "--expose-gc", str(bundled)], bundled.parent
# No prebuilt bundle available (or --dev, which never uses one) — we're
# about to npm install/build from source, so the workspace must exist.
if not ext_dir:
_ensure_tui_workspace(tui_dir)
# 2. Normal flow: npm install if needed, always esbuild, then node dist/entry.js.
# --dev flow: npm install if needed, then tsx src/entry.tsx.
# Existing desktop behaviour runs npm from the workspace root. Termux

View file

@ -361,6 +361,79 @@ def test_make_tui_argv_decodes_dev_prebuild_with_utf8_replace(
_assert_utf8_replace_capture(calls[0][1])
def test_make_tui_argv_uses_bundled_tui_when_workspace_missing(
tmp_path: Path, main_mod, monkeypatch
) -> None:
"""pip/pipx install regression (#56665): the wheel ships
hermes_cli/tui_dist/entry.js but never ships ui-tui/ (that directory only
exists in a git checkout). Before this fix, _make_tui_argv called
_ensure_tui_workspace() unconditionally before checking for the bundled
entry.js, so every pip/pipx dashboard Chat tab connection hard-exited
with `sys.exit(1)` surfaced to the user as the unhelpful "Chat
unavailable: 1" — despite having a perfectly runnable bundled TUI on
disk. The bundled-wheel shortcut must be tried first and succeed without
ever touching the (missing) ui-tui workspace or git.
"""
monkeypatch.delenv("HERMES_TUI_DIR", raising=False)
monkeypatch.setattr(main_mod, "_ensure_tui_node", lambda: None)
bundled_entry = tmp_path / "bundled" / "entry.js"
bundled_entry.parent.mkdir(parents=True)
bundled_entry.write_text("// bundled TUI")
monkeypatch.setattr(main_mod, "_find_bundled_tui", lambda: bundled_entry)
def which(name: str) -> str | None:
if name == "node":
return "/usr/bin/node"
raise AssertionError(f"unexpected shutil.which({name!r}) call — bundled path must not need npm/git")
monkeypatch.setattr(main_mod.shutil, "which", which)
def fail_run(*_args, **_kwargs):
raise AssertionError("bundled TUI path must not spawn any subprocess (no npm install/build, no git restore)")
monkeypatch.setattr(main_mod.subprocess, "run", fail_run)
# ui-tui/ deliberately does not exist under tmp_path, and there is no
# .git either — this mirrors a pip/pipx install exactly.
tui_dir = tmp_path / "ui-tui"
assert not tui_dir.exists()
argv, cwd = main_mod._make_tui_argv(tui_dir, tui_dev=False)
assert argv == ["/usr/bin/node", "--expose-gc", str(bundled_entry)]
assert cwd == bundled_entry.parent
def test_make_tui_argv_dev_mode_still_requires_workspace_even_with_bundle(
tmp_path: Path, main_mod, monkeypatch, capsys
) -> None:
"""--dev never uses the prebuilt bundle (there's no source to hot-reload
from a bundled entry.js), so it must still hit the workspace guard when
ui-tui/ is missing the bundled-first reordering must not weaken --dev.
"""
monkeypatch.delenv("HERMES_TUI_DIR", raising=False)
monkeypatch.setattr(main_mod, "_ensure_tui_node", lambda: None)
bundled_entry = tmp_path / "bundled" / "entry.js"
bundled_entry.parent.mkdir(parents=True)
bundled_entry.write_text("// bundled TUI")
monkeypatch.setattr(main_mod, "_find_bundled_tui", lambda: bundled_entry)
def which(name: str) -> str | None:
if name == "git":
return "/usr/bin/git"
raise AssertionError("node/npm lookup must not run when ui-tui is missing")
monkeypatch.setattr(main_mod.shutil, "which", which)
with pytest.raises(SystemExit) as exc:
main_mod._make_tui_argv(tmp_path / "ui-tui", tui_dev=True)
assert exc.value.code == 1
assert "TUI workspace is missing" in capsys.readouterr().err
def test_make_tui_argv_exits_with_recovery_hint_when_workspace_unrecoverable(
tmp_path: Path, main_mod, monkeypatch, capsys
) -> None: