refactor(hermes_cli): tuple routing compare, debounce-timer helper, pack signatures across g6 files
This commit is contained in:
@@ -144,9 +144,10 @@ class CLIAgentSetupMixin:
|
||||
api_key = runtime.get("api_key")
|
||||
base_url = runtime.get("base_url")
|
||||
resolved_provider = runtime.get("provider", "openrouter")
|
||||
resolved_api_mode = runtime.get("api_mode", self.api_mode)
|
||||
resolved_acp_command = runtime.get("command")
|
||||
resolved_acp_args = list(runtime.get("args") or [])
|
||||
resolved_routing = (
|
||||
resolved_provider, runtime.get("api_mode", self.api_mode), runtime.get("command"),
|
||||
list(runtime.get("args") or []),
|
||||
)
|
||||
# A callable api_key is a bearer-token provider (Azure Entra ID): the OpenAI SDK
|
||||
# invokes it per request, so skip string validation / placeholder substitution.
|
||||
_is_callable_provider = callable(api_key) and not isinstance(api_key, str)
|
||||
@@ -174,16 +175,8 @@ class CLIAgentSetupMixin:
|
||||
return False
|
||||
|
||||
credentials_changed = api_key != self.api_key or base_url != self.base_url
|
||||
routing_changed = (
|
||||
resolved_provider != self.provider
|
||||
or resolved_api_mode != self.api_mode
|
||||
or resolved_acp_command != self.acp_command
|
||||
or resolved_acp_args != self.acp_args
|
||||
)
|
||||
self.provider = resolved_provider
|
||||
self.api_mode = resolved_api_mode
|
||||
self.acp_command = resolved_acp_command
|
||||
self.acp_args = resolved_acp_args
|
||||
routing_changed = resolved_routing != (self.provider, self.api_mode, self.acp_command, self.acp_args)
|
||||
self.provider, self.api_mode, self.acp_command, self.acp_args = resolved_routing
|
||||
self._credential_pool = runtime.get("credential_pool")
|
||||
self._provider_source = runtime.get("source")
|
||||
self.api_key = api_key
|
||||
@@ -315,12 +308,9 @@ class CLIAgentSetupMixin:
|
||||
from hermes_cli.config import load_config
|
||||
_model_cfg = (load_config().get("model") or {})
|
||||
if isinstance(_model_cfg, dict):
|
||||
_new_provider = (_model_cfg.get("provider") or "").strip()
|
||||
if _new_provider:
|
||||
self.requested_provider = _new_provider
|
||||
self.requested_provider = (_model_cfg.get("provider") or "").strip() or self.requested_provider
|
||||
_new_model = (_model_cfg.get("default") or _model_cfg.get("model") or "").strip()
|
||||
if _new_model:
|
||||
self.model = _new_model
|
||||
self.model = _new_model or self.model
|
||||
except Exception as exc:
|
||||
logger.debug("first-run config re-sync failed: %s", exc)
|
||||
# Force credential re-resolution + agent rebuild on next use.
|
||||
@@ -466,7 +456,6 @@ class CLIAgentSetupMixin:
|
||||
self._session_db = SessionDB()
|
||||
except Exception as e:
|
||||
logger.warning("SQLite session store not available — session will NOT be indexed: %s", e)
|
||||
|
||||
if (
|
||||
self._resumed and self._session_db and not self.conversation_history
|
||||
and not self._load_resumed_history_late()
|
||||
@@ -671,7 +660,6 @@ class CLIAgentSetupMixin:
|
||||
display_kind = msg.get("display_kind")
|
||||
content = msg.get("content")
|
||||
tool_calls = msg.get("tool_calls") or []
|
||||
|
||||
if display_kind == "hidden":
|
||||
continue
|
||||
if display_kind in _RESUME_EVENT_TEXT:
|
||||
@@ -702,9 +690,8 @@ class CLIAgentSetupMixin:
|
||||
text = text[:MAX_ASST_LEN] + "..."
|
||||
parts.append(text)
|
||||
if tool_calls:
|
||||
tc_summary = _tool_calls_summary(tool_calls)
|
||||
parts.append(tc_summary)
|
||||
full_parts.append(tc_summary)
|
||||
parts.append(_tool_calls_summary(tool_calls))
|
||||
full_parts.append(parts[-1])
|
||||
# Skip pure-reasoning messages with no visible output, and tool-call-only
|
||||
# entries when SKIP_TOOL_ONLY is enabled.
|
||||
if not text and (SKIP_TOOL_ONLY or not tool_calls):
|
||||
@@ -716,10 +703,8 @@ class CLIAgentSetupMixin:
|
||||
if not entries:
|
||||
return
|
||||
|
||||
skipped = 0
|
||||
if len(entries) > MAX_DISPLAY_EXCHANGES * 2:
|
||||
skipped = len(entries) - MAX_DISPLAY_EXCHANGES * 2
|
||||
entries = entries[skipped:]
|
||||
skipped = max(0, len(entries) - MAX_DISPLAY_EXCHANGES * 2)
|
||||
entries = entries[skipped:]
|
||||
|
||||
# Show the last assistant entry in full so the user sees where they left off.
|
||||
if _last_asst_idx is not None and _last_asst_full:
|
||||
|
||||
@@ -270,6 +270,21 @@ class CLITerminalMixin:
|
||||
original_on_resize()
|
||||
self._schedule_status_bar_unsuppress(app)
|
||||
|
||||
def _restart_debounce_timer(self, attr: str, delay: float, fn) -> None:
|
||||
"""Cancel the daemon Timer stored on ``self.<attr>`` (if any) and start a new one.
|
||||
|
||||
``fn`` receives the new Timer so it can detect being superseded."""
|
||||
old_timer = getattr(self, attr, None)
|
||||
if old_timer is not None:
|
||||
try:
|
||||
old_timer.cancel()
|
||||
except Exception:
|
||||
pass
|
||||
timer = threading.Timer(delay, lambda: fn(timer))
|
||||
timer.daemon = True
|
||||
setattr(self, attr, timer)
|
||||
timer.start()
|
||||
|
||||
def _schedule_status_bar_unsuppress(self, app, delay: float = 0.35) -> None:
|
||||
"""Clear the post-resize status-bar suppression after the reflow settles.
|
||||
|
||||
@@ -277,13 +292,6 @@ class CLITerminalMixin:
|
||||
the bar only once it stops.
|
||||
"""
|
||||
try:
|
||||
old_timer = getattr(self, "_status_bar_unsuppress_timer", None)
|
||||
if old_timer is not None:
|
||||
try:
|
||||
old_timer.cancel()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _clear():
|
||||
self._status_bar_suppressed_after_resize = False
|
||||
try:
|
||||
@@ -291,10 +299,9 @@ class CLITerminalMixin:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
timer = threading.Timer(delay, lambda: _run_on_app_loop(app, _clear))
|
||||
timer.daemon = True
|
||||
self._status_bar_unsuppress_timer = timer
|
||||
timer.start()
|
||||
self._restart_debounce_timer(
|
||||
"_status_bar_unsuppress_timer", delay, lambda _t: _run_on_app_loop(app, _clear)
|
||||
)
|
||||
except Exception:
|
||||
# Fail open: never leave the bar stuck hidden.
|
||||
self._status_bar_suppressed_after_resize = False
|
||||
@@ -302,7 +309,6 @@ class CLITerminalMixin:
|
||||
def _schedule_resize_recovery(self, app, original_on_resize, delay: float = 0.12) -> None:
|
||||
"""Debounce resize redraws so footer chrome is not stamped into scrollback."""
|
||||
try:
|
||||
old_timer = getattr(self, "_resize_recovery_timer", None)
|
||||
lock = getattr(self, "_resize_recovery_lock", None)
|
||||
if lock is None:
|
||||
lock = threading.Lock()
|
||||
@@ -312,7 +318,7 @@ class CLITerminalMixin:
|
||||
def _run_recovery():
|
||||
with lock:
|
||||
if getattr(self, "_resize_recovery_timer", None) is not timer_ref:
|
||||
return
|
||||
return # superseded by a newer resize
|
||||
self._resize_recovery_timer = None
|
||||
self._resize_recovery_pending = False
|
||||
self._recover_after_resize(app, original_on_resize)
|
||||
@@ -320,16 +326,8 @@ class CLITerminalMixin:
|
||||
_run_on_app_loop(app, _run_recovery)
|
||||
|
||||
with lock:
|
||||
if old_timer is not None:
|
||||
try:
|
||||
old_timer.cancel()
|
||||
except Exception:
|
||||
pass
|
||||
self._resize_recovery_pending = True
|
||||
timer = threading.Timer(delay, lambda: _timer_fired(timer))
|
||||
timer.daemon = True
|
||||
self._resize_recovery_timer = timer
|
||||
timer.start()
|
||||
self._restart_debounce_timer("_resize_recovery_timer", delay, _timer_fired)
|
||||
except Exception:
|
||||
self._resize_recovery_pending = False
|
||||
self._recover_after_resize(app, original_on_resize)
|
||||
|
||||
@@ -170,8 +170,7 @@ def _quote_key(key: str) -> str:
|
||||
|
||||
|
||||
def render_codex_toml_section(
|
||||
servers: dict[str, dict],
|
||||
plugins: Optional[list[dict]] = None,
|
||||
servers: dict[str, dict], plugins: Optional[list[dict]] = None,
|
||||
default_permission_profile: Optional[str] = None,
|
||||
) -> str:
|
||||
"""Render the managed [mcp_servers.<n>] / [plugins.<id>] / default_permissions block.
|
||||
@@ -288,8 +287,7 @@ def _strip_existing_managed_block(toml_text: str) -> str:
|
||||
|
||||
|
||||
def _query_codex_plugins(
|
||||
codex_home: Optional[Path] = None,
|
||||
timeout: float = 8.0,
|
||||
codex_home: Optional[Path] = None, timeout: float = 8.0
|
||||
) -> tuple[list[dict], Optional[str]]:
|
||||
"""Query codex's `plugin/list` for installed curated plugins.
|
||||
|
||||
@@ -410,12 +408,8 @@ def _write_atomic(target: Path, text: str) -> None:
|
||||
|
||||
|
||||
def migrate(
|
||||
hermes_config: dict,
|
||||
*,
|
||||
codex_home: Optional[Path] = None,
|
||||
dry_run: bool = False,
|
||||
discover_plugins: bool = True,
|
||||
default_permission_profile: Optional[str] = ":workspace",
|
||||
hermes_config: dict, *, codex_home: Optional[Path] = None, dry_run: bool = False,
|
||||
discover_plugins: bool = True, default_permission_profile: Optional[str] = ":workspace",
|
||||
expose_hermes_tools: bool = True,
|
||||
) -> MigrationReport:
|
||||
"""Translate Hermes mcp_servers config + Codex curated plugins into ~/.codex/config.toml.
|
||||
|
||||
Reference in New Issue
Block a user