feat(install): warn pip/Homebrew installs are unsupported (CLI, TUI, desktop) (#57225)

* feat(install): warn pip/Homebrew installs are unsupported (CLI, TUI, desktop)

pip and Homebrew are now Unsupported install methods per
website/docs/getting-started/platform-support.md. Surface a
warn-don't-block deprecation notice everywhere the install method is
already shown, pointing at the platform-support docs and noting these
installs will not receive further updates. NixOS (Tier 2) is untouched.

- hermes_cli/config.py: shared is_unsupported_install_method() /
  format_unsupported_install_warning() helpers so the wording and docs
  link stay consistent across every surface.
- hermes_cli/banner.py: generalize the existing pip-only banner
  warning to also cover Homebrew.
- hermes_cli/main.py: hermes update and hermes update --check print
  the warning before proceeding (still update; warn, don't block).
- tui_gateway/server.py: session.info gains install_warning.
- ui-tui: SessionPanel renders install_warning alongside the existing
  'N commits behind' notice.
- apps/desktop: SessionRuntimeInfo/GatewayEventPayload gain
  install_warning; applyRuntimeInfo + the live session.info event fire
  a snoozable warning toast via a new reportInstallMethodWarning(),
  mirroring the existing backend-contract-skew toast pattern. i18n
  strings added for en/zh/zh-hant/ja.
- Tests: updated pip banner assertions for the new wording, added a
  Homebrew banner test, and two tui_gateway session_info tests
  (install_warning present for pip, absent for git).

* fix(nix): make `hermes` in developement environment actually work

install modules as editable overlay with uv

* feat: print install method when running --version

* fix: correct detect install method when running from a subtree
This commit is contained in:
ethernet 2026-07-07 21:13:19 -07:00 committed by GitHub
parent 9de9c25f62
commit 4d7f8ade3e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 340 additions and 95 deletions

2
.envrc
View file

@ -1,4 +1,4 @@
watch_file pyproject.toml uv.lock
watch_file pyproject.toml uv.lock hermes
watch_file package-lock.json package.json web/package.json ui-tui/package.json website/package.json apps/shared/package.json apps/desktop/package.json ui-tui/packages/hermes-ink/package.json
watch_file flake.nix flake.lock nix/devShell.nix nix/tui.nix nix/package.nix nix/python.nix nix/hermes-agent.nix nix/desktop.nix

View file

@ -39,6 +39,7 @@ import {
import { clearSessionSubagents, pruneDelegateFallbackSubagents, upsertSubagent } from '@/store/subagents'
import { clearActiveSessionTodos } from '@/store/todos'
import { recordToolDiff } from '@/store/tool-diffs'
import { reportInstallMethodWarning } from '@/store/updates'
import { notifyWorkspaceChanged, toolMayMutateFiles } from '@/store/workspace-events'
import type { RpcEvent } from '@/types/hermes'
@ -216,6 +217,10 @@ export function useGatewayEventHandler(deps: GatewayEventDeps) {
requestDesktopOnboarding(payload.credential_warning)
}
if (apply) {
reportInstallMethodWarning(payload?.install_warning)
}
void refreshHermesConfig()
if (modelChanged || providerChanged) {

View file

@ -19,7 +19,7 @@ import {
setSessions,
setYoloActive
} from '@/store/session'
import { reportBackendContract } from '@/store/updates'
import { reportBackendContract, reportInstallMethodWarning } from '@/store/updates'
import type { SessionCreateResponse, SessionInfo, SessionRuntimeInfo } from '@/types/hermes'
import type { ClientSessionState } from '../../../types'
@ -270,6 +270,8 @@ export function applyRuntimeInfo(info: SessionRuntimeInfo | undefined): SessionR
requestDesktopOnboarding(info.credential_warning)
}
reportInstallMethodWarning(info.install_warning)
if (typeof info.model === 'string') {
setCurrentModel(info.model)
sessionState.model = info.model

View file

@ -119,6 +119,7 @@ export const en: Translations = {
backendOutOfDateTitle: 'Backend out of date',
backendOutOfDateMessage:
'Your Hermes backend is older than this desktop build and may not work correctly. Update to align them.',
installMethodUnsupportedTitle: 'Unsupported install method',
updateHermes: 'Update Hermes',
updateReadyTitle: 'Update ready',
updateReadyMessage: count => `${count} new change${count === 1 ? '' : 's'} available.`,

View file

@ -120,6 +120,7 @@ export const ja = defineLocale({
backendOutOfDateTitle: 'バックエンドが古いです',
backendOutOfDateMessage:
'Hermes バックエンドがこのデスクトップビルドより古く、正常に動作しない場合があります。更新して揃えてください。',
installMethodUnsupportedTitle: 'サポート対象外のインストール方法',
updateHermes: 'Hermes を更新',
updateReadyTitle: '更新の準備ができました',
updateReadyMessage: count => `${count} 件の新しい変更が利用可能です。`,

View file

@ -161,6 +161,7 @@ export interface Translations {
copyDetailFailed: string
backendOutOfDateTitle: string
backendOutOfDateMessage: string
installMethodUnsupportedTitle: string
updateHermes: string
updateReadyTitle: string
updateReadyMessage: (count: number) => string

View file

@ -116,6 +116,7 @@ export const zhHant = defineLocale({
copyDetailFailed: '無法複製通知詳情',
backendOutOfDateTitle: '後端版本過舊',
backendOutOfDateMessage: '您的 Hermes 後端早於目前的桌面版本,可能無法正常運作。請更新以保持一致。',
installMethodUnsupportedTitle: '不受支援的安裝方式',
updateHermes: '更新 Hermes',
updateReadyTitle: '有可用更新',
updateReadyMessage: count => `${count} 項新變更可用。`,

View file

@ -116,6 +116,7 @@ export const zh: Translations = {
copyDetailFailed: '无法复制通知详情',
backendOutOfDateTitle: '后端版本过旧',
backendOutOfDateMessage: '你的 Hermes 后端早于当前桌面构建,可能无法正常工作。请更新以保持一致。',
installMethodUnsupportedTitle: '不受支持的安装方式',
updateHermes: '更新 Hermes',
updateReadyTitle: '有可用更新',
updateReadyMessage: count => `${count} 项新更改可用。`,

View file

@ -51,6 +51,7 @@ export type GatewayEventPayload = {
cwd?: string
branch?: string
credential_warning?: string
install_warning?: string
personality?: string
usage?: Partial<UsageStats>
// agent.terminal.output — live chunk for a read-only agent terminal tab

View file

@ -111,6 +111,24 @@ function isSkewToastSnoozed(): boolean {
return Number.isFinite(until) && Date.now() < until
}
const INSTALL_METHOD_TOAST_ID = 'install-method-not-supported'
// Same time-based snooze pattern as the update/skew toasts: the warning is
// re-derived from every session.info (session.create/resume/activate all
// route through applyRuntimeInfo), so without a snooze it would re-pop on
// every session switch even right after the user dismissed it.
const INSTALL_METHOD_TOAST_SNOOZE_KEY = 'hermes:install-method-toast-snooze-until'
const INSTALL_METHOD_TOAST_COOLDOWN_MS = 24 * 60 * 60 * 1000
function snoozeInstallMethodToast(): void {
persistString(INSTALL_METHOD_TOAST_SNOOZE_KEY, String(Date.now() + INSTALL_METHOD_TOAST_COOLDOWN_MS))
}
function isInstallMethodToastSnoozed(): boolean {
const until = Number(storedString(INSTALL_METHOD_TOAST_SNOOZE_KEY) || 0)
return Number.isFinite(until) && Date.now() < until
}
/**
* Guard against a desktop GUI talking to a backend that predates its contract
* (e.g. a bb/gui-built app pointed at a `main` checkout). Rather than failing
@ -151,6 +169,27 @@ export function reportBackendContract(contract: number | undefined): void {
})
}
export function reportInstallMethodWarning(message: string | undefined): void {
if (!message) {
dismissNotification(INSTALL_METHOD_TOAST_ID)
return
}
if (isInstallMethodToastSnoozed()) {
return
}
notify({
durationMs: 0,
id: INSTALL_METHOD_TOAST_ID,
kind: 'warning',
message,
onDismiss: () => snoozeInstallMethodToast(),
title: translateNow('notifications.installMethodUnsupportedTitle')
})
}
/**
* Fire a toast when an update is available, at most once per cooldown window.
* Closing the toast dismissing it or opening the updates window from it

View file

@ -399,6 +399,7 @@ export interface SessionRuntimeInfo {
cwd?: string
desktop_contract?: number
fast?: boolean
install_warning?: string
model?: string
personality?: string
provider?: string

View file

@ -2,7 +2,6 @@
Pure display functions with no HermesCLI state dependency.
"""
import json
import logging
import os
@ -322,8 +321,8 @@ def check_for_updates() -> Optional[int]:
# both the Rich banner (build_welcome_banner) and the Ink badge
# (branding.tsx, guarded on `typeof === 'number' && > 0`) show nothing.
try:
from hermes_cli.config import detect_install_method
if detect_install_method() == "docker":
from hermes_cli.config import detect_install_method, get_project_root
if detect_install_method(get_project_root()) == "docker":
return None
except Exception:
pass
@ -889,17 +888,23 @@ def build_welcome_banner(console: "Console", model: str, cwd: str,
except Exception:
pass # Never break the banner over an update check
# Pip-install warning — `pip install hermes-agent` is not the supported
# install path (it exists on PyPI for internal/CI reasons, not end users).
# Such installs miss the git checkout + installer-managed deps, so updates,
# self-update, and issue triage don't behave correctly. Warn, don't block.
# Unsupported install-method warning — pip/PyPI and Homebrew are no
# longer an officially supported distribution method (see
# website/docs/getting-started/platform-support.md). Such installs miss
# the git checkout + installer-managed deps, so updates, self-update, and
# issue triage don't behave correctly. Warn, don't block. NixOS is fully
# supported and never hits this.
try:
from hermes_cli.config import detect_install_method
if detect_install_method() == "pip":
from hermes_cli.config import (
detect_install_method,
format_unsupported_install_warning,
is_unsupported_install_method,
get_project_root
)
_install_method = detect_install_method(get_project_root())
if is_unsupported_install_method(_install_method):
right_lines.append(
"[bold yellow]⚠ pip install not officially supported[/]"
"[dim yellow] — exists for reasons other than user install; "
"expect instability and an inability to support issues[/]"
f"[bold yellow]⚠ {format_unsupported_install_warning(_install_method)}[/]"
)
except Exception:
pass # Never break the banner over the install-method check

View file

@ -441,8 +441,20 @@ def detect_install_method(project_root: Optional[Path] = None) -> str:
managed = get_managed_system()
if managed:
return managed.lower().replace(" ", "-")
if (root / ".git").is_dir():
# detect git repo installs (normal installer, development env)
git_path = root / ".git"
if git_path.is_dir():
return "git"
# detect git repo installs from worktrees
if git_path.is_file():
try:
content = git_path.read_text(encoding="utf-8").strip()
if content.startswith("gitdir:"):
return "git"
except OSError:
pass
return "pip"
@ -528,10 +540,54 @@ def recommended_update_command() -> str:
managed_cmd = get_managed_update_command()
if managed_cmd:
return managed_cmd
method = detect_install_method()
method = detect_install_method(get_project_root())
return recommended_update_command_for_method(method)
# =============================================================================
# Unsupported install methods (pip, Homebrew) — deprecation notice
# =============================================================================
#
# pip/PyPI and Homebrew are NOT an officially supported distribution method
# (see website/docs/getting-started/platform-support.md, "Unsupported"
# section). pip exists on PyPI for internal/CI reasons, not end-user installs;
# Homebrew is a legacy packaging path. Unlike NixOS/Homebrew "managed mode"
# (which hard-blocks config writes), this is a warn-don't-block deprecation
# notice surfaced everywhere the user might see install-method state: the CLI
# banner, the TUI/desktop session info panel, and ``hermes update``. NixOS
# stays fully supported (Tier 2) and must never hit this path.
PLATFORM_SUPPORT_DOCS_URL = "https://hermes-agent.nousresearch.com/docs/getting-started/platform-support"
_UNSUPPORTED_INSTALL_METHODS = frozenset({"pip", "homebrew"})
def is_unsupported_install_method(method: str) -> bool:
"""Whether ``method`` (from ``detect_install_method()``) is deprecated."""
return method in _UNSUPPORTED_INSTALL_METHODS
def unsupported_install_method_label(method: str) -> str:
"""Human-readable name for an unsupported install method."""
return "pip" if method == "pip" else "Homebrew"
def format_unsupported_install_warning(method: str) -> str:
"""Plain-text (no markup) deprecation notice for pip/Homebrew installs.
Shared verbatim across the CLI banner, TUI/desktop ``session.info``, and
``hermes update`` / ``hermes update --check`` so the wording and the
docs link stays consistent across every surface instead of drifting
into three slightly different warnings.
"""
label = unsupported_install_method_label(method)
return (
f"{label} installs are no longer an officially supported platform and "
f"will not receive further updates. See {PLATFORM_SUPPORT_DOCS_URL} "
"for supported install methods."
)
# Long-form text for ``hermes update`` / ``--check`` when running inside the
# Docker image. Surfaced by ``cmd_update`` and ``_cmd_update_check`` in
# hermes_cli/main.py; lives here so the wording stays consistent and we

View file

@ -229,9 +229,9 @@ def _read_openai_version_fast() -> str | None:
def _print_fast_version_info() -> None:
from hermes_cli import __release_date__, __version__
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
print(f"Hermes Agent v{__version__} ({__release_date__})")
print(f"Project: {project_root}")
print(f"Install directory: {PROJECT_ROOT}")
print(f"Python: {sys.version.split()[0]}")
openai_version = _read_openai_version_fast()
@ -4329,10 +4329,12 @@ def cmd_import(args):
def _print_version_info(*, check_updates: bool = True) -> None:
from hermes_cli.config import detect_install_method
from hermes_cli.banner import format_banner_version_label
print(format_banner_version_label())
print(f"Project: {PROJECT_ROOT}")
print(f"Install directory: {PROJECT_ROOT}")
print(f"Install method: {detect_install_method(PROJECT_ROOT)}")
# Show Python version
print(f"Python: {sys.version.split()[0]}")
@ -8371,8 +8373,14 @@ def _cmd_update_check(branch: str = "main", *, branch_explicit: bool = False):
on a PyPI install we surface a one-line notice instead of silently
dropping the flag.
"""
from hermes_cli.config import detect_install_method
from hermes_cli.config import (
detect_install_method,
format_unsupported_install_warning,
is_unsupported_install_method,
)
method = detect_install_method(PROJECT_ROOT)
if is_unsupported_install_method(method):
print(f"{format_unsupported_install_warning(method)}")
if method == "docker":
# Docker can't ``git fetch`` from within the container. Surface the
# same long-form ``docker pull`` guidance ``hermes update`` (apply
@ -9255,10 +9263,21 @@ def cmd_update(args):
from hermes_cli.config import (
detect_install_method,
format_docker_update_message,
format_unsupported_install_warning,
is_managed,
is_unsupported_install_method,
managed_error,
)
# Deprecation notice for pip/Homebrew installs — printed before the
# managed-mode early-return below so Homebrew users (who are blocked from
# applying the update here) still see it. Warn, don't block: the update
# itself still proceeds (except Homebrew, which is managed-mode blocked
# for an unrelated reason — brew owns its own upgrade path).
_install_method_for_warning = detect_install_method(PROJECT_ROOT)
if is_unsupported_install_method(_install_method_for_warning):
print(f"{format_unsupported_install_warning(_install_method_for_warning)}")
if is_managed():
managed_error("update Hermes Agent")
return

View file

@ -28,13 +28,20 @@
packages =
with pkgs;
[
(pkgs.runCommand "hermes" { } ''
mkdir -p $out/bin
install -Dm755 ${../hermes} $out/bin/hermes
'')
uv
]
++ self'.packages.default.passthru.devDeps;
shellHook = ''
echo "Hermes Agent dev shell"
${combinedNonNpm}
${hermesNpmLib.mkNpmDevShellHook npmPackageJsonPaths}
# for the devshell to pick up the src
export HERMES_PYTHON_SRC_ROOT=$(git rev-parse --show-toplevel)
echo "Hermes Agent dev shell in $HERMES_PYTHON_SRC_ROOT"
echo "Ready. Run 'hermes' to start."
'';
};

View file

@ -44,7 +44,7 @@ let
dependency-groups = [ "all" ] ++ extraDependencyGroups;
};
hermesVenv = mkHermesVenv extraDependencyGroups;
hermesVenv = (mkHermesVenv extraDependencyGroups).venv;
hermesNpmLib = callPackage ./lib.nix {
inherit npm-lockfile-fix nodejs;
@ -200,33 +200,37 @@ stdenv.mkDerivation (finalAttrs: {
runHook postInstall
'';
passthru = {
inherit
hermesTui
hermesWeb
hermesNpmLib
hermesVenv
;
passthru =
let
devPython = (mkHermesVenv (extraDependencyGroups ++ [ "dev" ])).editableVenv;
in
{
inherit
hermesTui
hermesWeb
hermesNpmLib
hermesVenv
;
# `hermesDesktop` references `finalAttrs.finalPackage` (this whole
# derivation, after all overrides are applied) so the desktop wrapper
# can prepend its `/bin` to PATH. The desktop's resolver step 4
# ("existing hermes on PATH") then picks up the fully wrapped
# `hermes` binary — venv with all deps, bundled skills/plugins,
# runtime PATH (ripgrep/git/ffmpeg/etc). No re-implementation
# of the agent resolution in the desktop wrapper.
hermesDesktop = callPackage ./desktop.nix {
inherit hermesNpmLib electron;
hermesAgent = finalAttrs.finalPackage;
# `hermesDesktop` references `finalAttrs.finalPackage` (this whole
# derivation, after all overrides are applied) so the desktop wrapper
# can prepend its `/bin` to PATH. The desktop's resolver step 4
# ("existing hermes on PATH") then picks up the fully wrapped
# `hermes` binary — venv with all deps, bundled skills/plugins,
# runtime PATH (ripgrep/git/ffmpeg/etc). No re-implementation
# of the agent resolution in the desktop wrapper.
hermesDesktop = callPackage ./desktop.nix {
inherit hermesNpmLib electron;
hermesAgent = finalAttrs.finalPackage;
};
devShellHook = ''
export HERMES_PYTHON=${devPython}/bin/python3
'';
devDeps = runtimeDeps ++ [ devPython ];
};
devShellHook = ''
export HERMES_PYTHON=${hermesVenv}/bin/python3
'';
devDeps = runtimeDeps ++ [ (mkHermesVenv (extraDependencyGroups ++ [ "dev" ])) ];
};
meta = with lib; {
description = "AI agent with advanced tool-calling capabilities";
homepage = "https://github.com/NousResearch/hermes-agent";

View file

@ -27,7 +27,8 @@ let
dependency-groups = { };
};
mkPrebuiltOverride = final: from: dependencies:
mkPrebuiltOverride =
final: from: dependencies:
hacks.nixpkgsPrebuilt {
inherit from;
prev = {
@ -38,64 +39,100 @@ let
# Legacy alibabacloud packages ship only sdists with setup.py/setup.cfg
# and no pyproject.toml, so setuptools isn't declared as a build dep.
buildSystemOverrides = final: prev: builtins.mapAttrs
(name: _: prev.${name}.overrideAttrs (old: {
nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [ final.setuptools ];
}))
(lib.genAttrs [
"alibabacloud-credentials-api"
"alibabacloud-endpoint-util"
"alibabacloud-gateway-dingtalk"
"alibabacloud-gateway-spi"
"alibabacloud-tea"
] (_: null));
buildSystemOverrides =
final: prev:
builtins.mapAttrs
(
name: _:
prev.${name}.overrideAttrs (old: {
nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [ final.setuptools ];
})
)
(
lib.genAttrs [
"alibabacloud-credentials-api"
"alibabacloud-endpoint-util"
"alibabacloud-gateway-dingtalk"
"alibabacloud-gateway-spi"
"alibabacloud-tea"
] (_: null)
);
pythonPackageOverrides = final: _prev:
if isAarch64Darwin then {
numpy = mkPrebuiltOverride final python312.pkgs.numpy { };
pythonPackageOverrides =
final: _prev:
if isAarch64Darwin then
{
numpy = mkPrebuiltOverride final python312.pkgs.numpy { };
pyarrow = mkPrebuiltOverride final python312.pkgs.pyarrow { };
pyarrow = mkPrebuiltOverride final python312.pkgs.pyarrow { };
av = mkPrebuiltOverride final python312.pkgs.av { };
av = mkPrebuiltOverride final python312.pkgs.av { };
humanfriendly = mkPrebuiltOverride final python312.pkgs.humanfriendly { };
humanfriendly = mkPrebuiltOverride final python312.pkgs.humanfriendly { };
coloredlogs = mkPrebuiltOverride final python312.pkgs.coloredlogs {
humanfriendly = [ ];
};
coloredlogs = mkPrebuiltOverride final python312.pkgs.coloredlogs {
humanfriendly = [ ];
};
onnxruntime = mkPrebuiltOverride final python312.pkgs.onnxruntime {
coloredlogs = [ ];
numpy = [ ];
packaging = [ ];
};
onnxruntime = mkPrebuiltOverride final python312.pkgs.onnxruntime {
coloredlogs = [ ];
numpy = [ ];
packaging = [ ];
};
ctranslate2 = mkPrebuiltOverride final python312.pkgs.ctranslate2 {
numpy = [ ];
pyyaml = [ ];
};
ctranslate2 = mkPrebuiltOverride final python312.pkgs.ctranslate2 {
numpy = [ ];
pyyaml = [ ];
};
faster-whisper = mkPrebuiltOverride final python312.pkgs.faster-whisper {
av = [ ];
ctranslate2 = [ ];
huggingface-hub = [ ];
onnxruntime = [ ];
tokenizers = [ ];
tqdm = [ ];
};
} else {};
faster-whisper = mkPrebuiltOverride final python312.pkgs.faster-whisper {
av = [ ];
ctranslate2 = [ ];
huggingface-hub = [ ];
onnxruntime = [ ];
tokenizers = [ ];
tqdm = [ ];
};
}
else
{ };
pythonSet =
(callPackage pyproject-nix.build.packages {
python = python312;
}).overrideScope
(lib.composeManyExtensions [
pyproject-build-systems.overlays.default
overlay
buildSystemOverrides
pythonPackageOverrides
]);
(
lib.composeManyExtensions [
pyproject-build-systems.overlays.default
overlay
buildSystemOverrides
pythonPackageOverrides
]
);
editableOverlay = workspace.mkEditablePyprojectOverlay {
root = "$HERMES_PYTHON_SRC_ROOT"; # resolved at shellHook time
};
workspaceRoot = ./..;
editableSet = pythonSet.overrideScope (
lib.composeManyExtensions [
editableOverlay
(final: prev: {
hermes-agent = prev.hermes-agent.overrideAttrs (old: {
# point straight at the real source instead of the filtered nix store copy
src = workspaceRoot;
nativeBuildInputs = old.nativeBuildInputs ++ final.resolveBuildSystem { editables = [ ]; };
});
})
]
);
in
pythonSet.mkVirtualEnv "hermes-agent-env" {
hermes-agent = dependency-groups;
{
venv = pythonSet.mkVirtualEnv "hermes-agent-env" {
hermes-agent = dependency-groups;
};
editableVenv = editableSet.mkVirtualEnv "hermes-agent-editable-env" {
hermes-agent = dependency-groups;
};
}

View file

@ -195,7 +195,33 @@ def test_banner_warns_on_pip_install(tmp_path):
out = buf.getvalue()
assert "officially" in out
assert "instability" in out
assert "platform-support" in out
def test_banner_warns_on_homebrew_install(tmp_path):
"""The welcome banner surfaces a warning when the install method is homebrew."""
import io
from rich.console import Console
from hermes_cli import banner
hh = tmp_path / ".hermes"
hh.mkdir()
(hh / ".install_method").write_text("homebrew\n")
with patch("hermes_cli.config.get_hermes_home", return_value=hh), \
patch("hermes_constants.get_hermes_home", return_value=hh):
buf = io.StringIO()
console = Console(file=buf, width=400, force_terminal=False, color_system=None)
banner.build_welcome_banner(
console, model="m", cwd="/tmp",
tools=[{"function": {"name": "terminal"}}],
enabled_toolsets=["terminal"],
)
out = buf.getvalue()
assert "officially" in out
assert "Homebrew" in out
assert "platform-support" in out
def test_banner_no_pip_warning_on_git_install(tmp_path):

View file

@ -408,7 +408,7 @@ def test_termux_ultrafast_version_runs_before_heavy_startup(
out = capsys.readouterr().out
assert "Hermes Agent v" in out
assert "Project:" in out
assert "Install directory:" in out
assert "Python:" in out
assert "OpenAI SDK:" in out

View file

@ -4727,6 +4727,25 @@ def test_session_info_includes_session_title(monkeypatch):
assert info["title"] == "Dashboard title"
def test_session_info_includes_install_warning_for_pip(monkeypatch):
"""pip installs surface install_warning; git installs don't (issue: pip/brew deprecation)."""
monkeypatch.setattr("hermes_cli.config.detect_install_method", lambda: "pip")
info = server._session_info(types.SimpleNamespace(tools=[], model="", provider=""))
assert "install_warning" in info
assert "pip" in info["install_warning"]
assert "platform-support" in info["install_warning"]
def test_session_info_omits_install_warning_for_git(monkeypatch):
monkeypatch.setattr("hermes_cli.config.detect_install_method", lambda: "git")
info = server._session_info(types.SimpleNamespace(tools=[], model="", provider=""))
assert "install_warning" not in info
# ---------------------------------------------------------------------------
# History-mutating commands must reject while session.running is True.
# Without these guards, prompt.submit's post-run history write either

View file

@ -3238,6 +3238,18 @@ def _session_info(agent, session: dict | None = None) -> dict:
"usage": _get_usage(agent),
"profile_name": _current_profile_name(),
}
try:
from hermes_cli.config import (
detect_install_method,
format_unsupported_install_warning,
is_unsupported_install_method,
)
_install_method = detect_install_method()
if is_unsupported_install_method(_install_method):
info["install_warning"] = format_unsupported_install_warning(_install_method)
except Exception:
pass
try:
from hermes_cli import __version__, __release_date__

View file

@ -412,6 +412,12 @@ export function SessionPanel({ info, maxWidth, sid, t }: SessionPanelProps) {
</Text>
</Text>
)}
{info.install_warning && (
<Text bold color={t.color.warn} wrap="wrap">
! {info.install_warning}
</Text>
)}
</Box>
</Box>
)

View file

@ -150,6 +150,7 @@ export interface McpServerStatus {
export interface SessionInfo {
cwd?: string
fast?: boolean
install_warning?: string
lazy?: boolean
mcp_servers?: McpServerStatus[]
model: string