opentui(phase3): launcher integration — HERMES_TUI_ENGINE dual-engine

hermes --tui launches the native OpenTUI engine (Bun) when
HERMES_TUI_ENGINE=opentui (env) or display.tui_engine=opentui (config);
Ink stays the default and the shipping path is untouched.

- _resolve_tui_engine() (env > config > ink); refuses opentui on
  Windows/Termux (no Bun) -> falls back to ink with a notice.
- _make_opentui_argv() -> [bun, src/entry.real.tsx] (no build step).
- _bun_bin() with HERMES_BUN override.
- Branch at top of _make_tui_argv BEFORE _ensure_tui_node (Bun-only host
  must not bootstrap Node).
- Gate _launch_tui NODE_OPTIONS/--max-old-space-size on engine==ink (Bun
  is JSC; the V8 flag errors/ignores).

Verified end-to-end via tmux: real hermes --tui -> Bun -> OpenTUI ->
real Python gateway streamed a real reply. No-flag default still ink.
This commit is contained in:
alt-glitch
2026-06-08 11:11:54 +00:00
parent 24f74eb888
commit 2bd9c9b881
741 changed files with 17733 additions and 79889 deletions
+15 -44
View File
@@ -91,6 +91,10 @@ class TestTelegramModelPicker:
query.answer = AsyncMock()
query.edit_message_text = AsyncMock()
update = MagicMock()
update.callback_query = query
context = MagicMock()
await adapter._handle_model_picker_callback(query, "mb", "12345")
edit_kwargs = query.edit_message_text.call_args[1]
@@ -129,11 +133,17 @@ class TestTelegramModelPicker:
await adapter._handle_model_picker_callback(query, "mm:0", "12345")
# The callback was invoked with the selected model
callback.assert_awaited_once()
# edit_message_text MUST be called on the success path (this is the
# regression we're guarding).
query.edit_message_text.assert_awaited()
edit_kwargs = query.edit_message_text.call_args[1]
assert "MARKDOWN_V2" in repr(edit_kwargs["parse_mode"])
# The dynamic result text was routed through format_message
# (backtick code blocks survive escaping).
assert "`gpt-5`" in edit_kwargs["text"]
# State is cleaned up after a successful switch.
assert "12345" not in adapter._model_picker_state
@pytest.mark.asyncio
@@ -174,7 +184,7 @@ class TestTelegramModelPicker:
providers = [
{"slug": "minimax", "name": "MiniMax", "total_models": 2},
{"slug": "minimax-cn", "name": "MiniMax (China)", "total_models": 3},
{"slug": "xai", "name": "xAI", "total_models": 1},
{"slug": "xai", "name": "xAI", "total_models": 1}, # lone group member
]
await adapter.send_model_picker(
@@ -187,11 +197,14 @@ class TestTelegramModelPicker:
metadata=None,
)
# Top-level keyboard: MiniMax family folded into one group button;
# xai (lone member) degraded to a direct provider button.
assert "mpg:minimax" in built
assert "mp:xai" in built
assert "mp:minimax" not in built
assert "mp:minimax-cn" not in built
# Drill into the MiniMax group → members appear as mp: buttons + back.
built.clear()
query = AsyncMock()
query.message = MagicMock()
@@ -203,49 +216,7 @@ class TestTelegramModelPicker:
assert "mp:minimax" in built
assert "mp:minimax-cn" in built
assert "mb" in built
@pytest.mark.asyncio
async def test_expensive_model_requires_confirmation(self, monkeypatch):
adapter = _make_adapter()
callback = AsyncMock(return_value="Switched to `openai/gpt-5.5-pro`")
adapter._model_picker_state["12345"] = {
"providers": [
{"slug": "openrouter", "name": "OpenRouter", "total_models": 1, "is_current": True}
],
"current_model": "model_1",
"current_provider": "openrouter",
"session_key": "s",
"on_model_selected": callback,
"selected_provider": "openrouter",
"model_list": ["openai/gpt-5.5-pro"],
"msg_id": 42,
}
monkeypatch.setattr(
"hermes_cli.model_cost_guard.expensive_model_warning",
lambda *_args, **_kwargs: SimpleNamespace(
message="!!! EXPENSIVE MODEL WARNING !!!\ndid you mean to select openai/gpt-5.5?"
),
)
query = AsyncMock()
query.message = MagicMock()
query.message.chat_id = 12345
query.answer = AsyncMock()
query.edit_message_text = AsyncMock()
await adapter._handle_model_picker_callback(query, "mm:0", "12345")
callback.assert_not_awaited()
assert "12345" in adapter._model_picker_state
first_edit = query.edit_message_text.call_args[1]
assert "EXPENSIVE MODEL WARNING" in first_edit["text"]
assert first_edit["reply_markup"] is not None
await adapter._handle_model_picker_callback(query, "mc:0", "12345")
callback.assert_awaited_once_with("12345", "openai/gpt-5.5-pro", "openrouter")
assert "12345" not in adapter._model_picker_state
assert "mb" in built # back-to-providers button present
@pytest.mark.asyncio
async def test_retries_without_thread_when_thread_not_found(self):