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
+6 -41
View File
@@ -33,7 +33,6 @@ _AUDIO_EXTS = frozenset({'.ogg', '.opus', '.mp3', '.wav', '.m4a', '.flac'})
# delivered as a regular document.
_TELEGRAM_AUDIO_ATTACHMENT_EXTS = frozenset({'.mp3', '.m4a'})
_TELEGRAM_VOICE_EXTS = frozenset({'.ogg', '.opus'})
_POST_DELIVERY_CALLBACK_TIMEOUT_SECONDS = 30.0
def _platform_name(platform) -> str:
@@ -1545,13 +1544,6 @@ class SendResult:
message_id: Optional[str] = None
error: Optional[str] = None
raw_response: Any = None
# Adapter-specific metadata. Cross-layer contracts that affect delivery
# semantics must be documented at the producer and consumer sites. Current
# known contract: Telegram edit overflow partials set
# raw_response["partial_overflow"] with delivered_chunks, total_chunks,
# last_message_id, delivered_prefix, and continuation_message_ids so the
# stream consumer can send the missing tail instead of marking a clipped
# response complete.
retryable: bool = False # True for transient connection errors — base will retry automatically
# When the adapter had to split an oversized payload across multiple
# platform messages (e.g. Telegram edit_message overflow split-and-deliver),
@@ -1803,26 +1795,11 @@ class BasePlatformAdapter(ABC):
# Whether this platform renders triple-backtick fenced code blocks (i.e.
# ``format_message`` translates/preserves markdown fences into a real code
# block). Capability flag for markdown-aware presentation choices.
# block). Drives presentation choices like rendering a ``terminal`` tool
# call's command as a ```bash block instead of a flat preview line.
# Default False (plain-text platforms); markdown-rendering adapters set True.
# Tool-progress uses this to render a terminal command as a bare fenced code
# block (no language tag — Slack mrkdwn would print the tag as a literal
# first code line). Plain-text platforms fall back to the short truncated
# preview (see gateway/run.py progress_callback).
supports_code_blocks: bool = False
# The command prefix users can always TYPE on this platform to reach
# Hermes commands. Default "/" (most platforms deliver "/approve" etc.
# as plain message text). Platforms where typing a leading "/" is
# intercepted or restricted by the client (Slack blocks native slash
# commands inside threads; Matrix clients reserve "/" for client-local
# commands) ship a "!" alias rewrite in their adapter and set this to
# "!" so user-facing instruction text ("Reply `!approve` ...") tells
# users the form that actually works everywhere. Capability flag —
# shared prompt builders read it via getattr(adapter,
# "typed_command_prefix", "/"); no per-platform branching at call sites.
typed_command_prefix: str = "/"
def __init__(self, config: PlatformConfig, platform: Platform):
self.config = config
self.platform = platform
@@ -4482,15 +4459,6 @@ class BasePlatformAdapter(ABC):
except Exception:
pass # Last resort — don't let error reporting crash the handler
finally:
# Stop typing before any deferred callback work. Post-delivery
# callbacks may perform platform I/O; a stuck callback must not
# leave the typing refresh task running indefinitely.
await _stop_typing_task()
try:
if hasattr(self, "stop_typing"):
await self.stop_typing(event.source.chat_id)
except Exception:
pass
# Fire any one-shot post-delivery callback registered for this
# session (e.g. deferred background-review notifications).
#
@@ -4518,12 +4486,11 @@ class BasePlatformAdapter(ABC):
try:
_post_result = _post_cb()
if inspect.isawaitable(_post_result):
await asyncio.wait_for(
_post_result,
timeout=_POST_DELIVERY_CALLBACK_TIMEOUT_SECONDS,
)
except (asyncio.TimeoutError, Exception):
await _post_result
except Exception:
pass
# Stop typing indicator
await _stop_typing_task()
# Also cancel any platform-level persistent typing tasks (e.g. Discord)
# that may have been recreated by _keep_typing after the last stop_typing()
try:
@@ -4681,7 +4648,6 @@ class BasePlatformAdapter(ABC):
guild_id: Optional[str] = None,
parent_chat_id: Optional[str] = None,
message_id: Optional[str] = None,
role_authorized: bool = False,
) -> SessionSource:
"""Helper to build a SessionSource for this platform."""
# Normalize empty topic to None
@@ -4702,7 +4668,6 @@ class BasePlatformAdapter(ABC):
guild_id=str(guild_id) if guild_id else None,
parent_chat_id=str(parent_chat_id) if parent_chat_id else None,
message_id=str(message_id) if message_id else None,
role_authorized=role_authorized,
)
@abstractmethod