Skip to content

Commit e4120d1

Browse files
committed
Merge remote-tracking branch 'origin/main' into fix/markdown
Made-with: Cursor # Conflicts: # ui-tui/src/components/markdown.tsx
2 parents 3379f88 + cd7150a commit e4120d1

82 files changed

Lines changed: 3565 additions & 491 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.dockerignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55

66
# Dependencies
77
node_modules
8+
**/node_modules
89
.venv
10+
**/.venv
911

1012
# CI/CD
1113
.github

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,7 @@ branding:
494494
agent_name: "My Agent"
495495
welcome: "Welcome message"
496496
response_label: " ⚔ Agent "
497-
prompt_symbol: "⚔"
497+
prompt_symbol: "⚔"
498498
499499
tool_prefix: "╎" # Tool output line prefix
500500
```

Dockerfile

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ ENV PLAYWRIGHT_BROWSERS_PATH=/opt/hermes/.playwright
1414
# that would otherwise accumulate when hermes runs as PID 1. See #15012.
1515
RUN apt-get update && \
1616
apt-get install -y --no-install-recommends \
17-
build-essential nodejs npm python3 ripgrep ffmpeg gcc python3-dev libffi-dev procps git openssh-client docker-cli tini && \
17+
build-essential nodejs npm python3 ripgrep ffmpeg gcc python3-dev libffi-dev procps git openssh-client docker-cli tini && \
1818
rm -rf /var/lib/apt/lists/*
1919

2020
# Non-root user for runtime; UID can be overridden via HERMES_UID at runtime
@@ -45,7 +45,13 @@ COPY --chown=hermes:hermes . .
4545

4646
# Build browser dashboard and terminal UI assets.
4747
RUN cd web && npm run build && \
48-
cd ../ui-tui && npm run build
48+
cd ../ui-tui && npm run build && \
49+
rm -rf node_modules/@hermes/ink && \
50+
rm -rf packages/hermes-ink/node_modules && \
51+
cp -R packages/hermes-ink node_modules/@hermes/ink && \
52+
npm install --omit=dev --prefer-offline --no-audit --prefix node_modules/@hermes/ink && \
53+
rm -rf node_modules/@hermes/ink/node_modules/react && \
54+
node --input-type=module -e "await import('@hermes/ink')"
4955

5056
# ---------- Permissions ----------
5157
# Make install dir world-readable so any HERMES_UID can read it at runtime.

cli-config.yaml.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -927,7 +927,7 @@ display:
927927
# agent_name: "My Agent" # Banner title and branding
928928
# welcome: "Welcome message" # Shown at CLI startup
929929
# response_label: " ⚔ Agent " # Response box header label
930-
# prompt_symbol: "⚔ ❯ " # Prompt symbol
930+
# prompt_symbol: "⚔" # Prompt symbol (bare token; renderers add trailing space)
931931
# tool_prefix: "╎" # Tool output line prefix (default: ┊)
932932
#
933933
skin: default

hermes_cli/commands.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,9 @@ class CommandDef:
128128
subcommands=("normal", "fast", "status", "on", "off")),
129129
CommandDef("skin", "Show or change the display skin/theme", "Configuration",
130130
cli_only=True, args_hint="[name]"),
131+
CommandDef("indicator", "Pick the TUI busy-indicator style", "Configuration",
132+
cli_only=True, args_hint="[kaomoji|emoji|unicode|ascii]",
133+
subcommands=("kaomoji", "emoji", "unicode", "ascii")),
131134
CommandDef("voice", "Toggle voice mode", "Configuration",
132135
args_hint="[on|off|tts|status]", subcommands=("on", "off", "tts", "status")),
133136
CommandDef("busy", "Control what Enter does while Hermes is working", "Configuration",

hermes_cli/config.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -703,13 +703,21 @@ def _ensure_hermes_home_managed(home: Path):
703703
"personality": "kawaii",
704704
"resume_display": "full",
705705
"busy_input_mode": "interrupt", # interrupt | queue | steer
706+
# When true, `hermes --tui` auto-resumes the most recent human-
707+
# facing session on launch instead of forging a fresh one.
708+
# Mirrors `hermes -c` muscle memory. Default off so existing
709+
# users aren't surprised. HERMES_TUI_RESUME=<id> always wins.
710+
"tui_auto_resume_recent": False,
706711
"bell_on_complete": False,
707712
"show_reasoning": False,
708713
"streaming": False,
709714
"final_response_markdown": "strip", # render | strip | raw
710715
"inline_diffs": True, # Show inline diff previews for write actions (write_file, patch, skill_manage)
711716
"show_cost": False, # Show $ cost in the status bar (off by default)
712717
"skin": "default",
718+
# TUI busy indicator style: kaomoji (default), emoji, unicode (braille
719+
# spinner), or ascii. Live-swappable via `/indicator <style>`.
720+
"tui_status_indicator": "kaomoji",
713721
"user_message_preview": { # CLI: how many submitted user-message lines to echo back in scrollback
714722
"first_lines": 2,
715723
"last_lines": 2,

hermes_cli/doctor.py

Lines changed: 67 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -293,15 +293,23 @@ def run_doctor(args):
293293

294294
known_providers: set = set()
295295
try:
296-
from hermes_cli.auth import PROVIDER_REGISTRY
296+
from hermes_cli.auth import (
297+
PROVIDER_REGISTRY,
298+
resolve_provider as _resolve_auth_provider,
299+
)
297300
known_providers = set(PROVIDER_REGISTRY.keys()) | {"openrouter", "custom", "auto"}
298301
except Exception:
302+
_resolve_auth_provider = None
299303
pass
300304
try:
301305
from hermes_cli.config import get_compatible_custom_providers as _compatible_custom_providers
302-
from hermes_cli.providers import resolve_provider_full as _resolve_provider_full
306+
from hermes_cli.providers import (
307+
normalize_provider as _normalize_catalog_provider,
308+
resolve_provider_full as _resolve_provider_full,
309+
)
303310
except Exception:
304311
_compatible_custom_providers = None
312+
_normalize_catalog_provider = None
305313
_resolve_provider_full = None
306314

307315
custom_providers = []
@@ -321,17 +329,43 @@ def run_doctor(args):
321329
if name:
322330
known_providers.add("custom:" + name.lower().replace(" ", "-"))
323331

324-
canonical_provider = provider
332+
valid_provider_ids = set(known_providers)
333+
provider_ids_to_accept = {provider} if provider else set()
334+
if _normalize_catalog_provider is not None:
335+
for known_provider in known_providers:
336+
try:
337+
valid_provider_ids.add(_normalize_catalog_provider(known_provider))
338+
except Exception:
339+
continue
340+
341+
runtime_provider = provider
342+
if (
343+
provider
344+
and _resolve_auth_provider is not None
345+
and provider not in ("auto", "custom")
346+
):
347+
try:
348+
runtime_provider = _resolve_auth_provider(provider)
349+
provider_ids_to_accept.add(runtime_provider)
350+
except Exception:
351+
runtime_provider = provider
352+
353+
catalog_provider = provider
325354
if (
326355
provider
327356
and _resolve_provider_full is not None
328357
and provider not in ("auto", "custom")
329358
):
330359
provider_def = _resolve_provider_full(provider, user_providers, custom_providers)
331-
canonical_provider = provider_def.id if provider_def is not None else None
360+
catalog_provider = provider_def.id if provider_def is not None else None
361+
if catalog_provider is not None:
362+
provider_ids_to_accept.add(catalog_provider)
332363

333364
if provider and provider != "auto":
334-
if canonical_provider is None or (known_providers and canonical_provider not in known_providers):
365+
if catalog_provider is None or (
366+
known_providers
367+
and not (provider_ids_to_accept & valid_provider_ids)
368+
):
335369
known_list = ", ".join(sorted(known_providers)) if known_providers else "(unavailable)"
336370
check_fail(
337371
f"model.provider '{provider_raw}' is not a recognised provider",
@@ -344,7 +378,24 @@ def run_doctor(args):
344378
)
345379

346380
# Warn if model is set to a provider-prefixed name on a provider that doesn't use them
347-
if default_model and "/" in default_model and canonical_provider and canonical_provider not in ("openrouter", "custom", "auto", "ai-gateway", "kilocode", "opencode-zen", "huggingface", "nous", "lmstudio"):
381+
provider_for_policy = runtime_provider or catalog_provider
382+
providers_accepting_vendor_slugs = {
383+
"openrouter",
384+
"custom",
385+
"auto",
386+
"ai-gateway",
387+
"kilocode",
388+
"opencode-zen",
389+
"huggingface",
390+
"lmstudio",
391+
"nous",
392+
}
393+
if (
394+
default_model
395+
and "/" in default_model
396+
and provider_for_policy
397+
and provider_for_policy not in providers_accepting_vendor_slugs
398+
):
348399
check_warn(
349400
f"model.default '{default_model}' uses a vendor/model slug but provider is '{provider_raw}'",
350401
"(vendor-prefixed slugs belong to aggregators like openrouter)",
@@ -360,20 +411,24 @@ def run_doctor(args):
360411
# own env-var checks elsewhere in doctor, and get_auth_status()
361412
# returns a bare {logged_in: False} for anything it doesn't
362413
# explicitly dispatch, which would produce false positives.
363-
if canonical_provider and canonical_provider not in ("auto", "custom", "openrouter"):
414+
if runtime_provider and runtime_provider not in ("auto", "custom", "openrouter"):
364415
try:
365416
from hermes_cli.auth import PROVIDER_REGISTRY, get_auth_status
366-
pconfig = PROVIDER_REGISTRY.get(canonical_provider)
417+
pconfig = PROVIDER_REGISTRY.get(runtime_provider)
367418
if pconfig and getattr(pconfig, "auth_type", "") == "api_key":
368-
status = get_auth_status(canonical_provider) or {}
369-
configured = bool(status.get("configured") or status.get("logged_in") or status.get("api_key"))
419+
status = get_auth_status(runtime_provider) or {}
420+
configured = bool(
421+
status.get("configured")
422+
or status.get("logged_in")
423+
or status.get("api_key")
424+
)
370425
if not configured:
371426
check_fail(
372-
f"model.provider '{canonical_provider}' is set but no API key is configured",
427+
f"model.provider '{runtime_provider}' is set but no API key is configured",
373428
"(check ~/.hermes/.env or run 'hermes setup')",
374429
)
375430
issues.append(
376-
f"No credentials found for provider '{canonical_provider}'. "
431+
f"No credentials found for provider '{runtime_provider}'. "
377432
f"Run 'hermes setup' or set the provider's API key in {_DHH}/.env, "
378433
f"or switch providers with 'hermes config set model.provider <name>'"
379434
)

hermes_cli/skin_engine.py

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868
welcome: "Welcome message" # Shown at CLI startup
6969
goodbye: "Goodbye! ⚕" # Shown on exit
7070
response_label: " ⚕ Hermes " # Response box header label
71-
prompt_symbol: "❯ " # Input prompt symbol
71+
prompt_symbol: "❯" # Input prompt symbol (bare token; renderers add trailing space)
7272
help_header: "(^_^)? Commands" # /help header text
7373
7474
# Tool prefix: character for tool output lines (default: ┊)
@@ -190,7 +190,7 @@ def get_branding(self, key: str, fallback: str = "") -> str:
190190
"welcome": "Welcome to Hermes Agent! Type your message or /help for commands.",
191191
"goodbye": "Goodbye! ⚕",
192192
"response_label": " ⚕ Hermes ",
193-
"prompt_symbol": "❯ ",
193+
"prompt_symbol": "❯",
194194
"help_header": "(^_^)? Available Commands",
195195
},
196196
"tool_prefix": "┊",
@@ -242,7 +242,7 @@ def get_branding(self, key: str, fallback: str = "") -> str:
242242
"welcome": "Welcome to Ares Agent! Type your message or /help for commands.",
243243
"goodbye": "Farewell, warrior! ⚔",
244244
"response_label": " ⚔ Ares ",
245-
"prompt_symbol": "⚔",
245+
"prompt_symbol": "⚔",
246246
"help_header": "(⚔) Available Commands",
247247
},
248248
"tool_prefix": "╎",
@@ -301,7 +301,7 @@ def get_branding(self, key: str, fallback: str = "") -> str:
301301
"welcome": "Welcome to Hermes Agent! Type your message or /help for commands.",
302302
"goodbye": "Goodbye! ⚕",
303303
"response_label": " ⚕ Hermes ",
304-
"prompt_symbol": "❯ ",
304+
"prompt_symbol": "❯",
305305
"help_header": "[?] Available Commands",
306306
},
307307
"tool_prefix": "┊",
@@ -340,7 +340,7 @@ def get_branding(self, key: str, fallback: str = "") -> str:
340340
"welcome": "Welcome to Hermes Agent! Type your message or /help for commands.",
341341
"goodbye": "Goodbye! ⚕",
342342
"response_label": " ⚕ Hermes ",
343-
"prompt_symbol": "❯ ",
343+
"prompt_symbol": "❯",
344344
"help_header": "(^_^)? Available Commands",
345345
},
346346
"tool_prefix": "┊",
@@ -377,7 +377,7 @@ def get_branding(self, key: str, fallback: str = "") -> str:
377377
"welcome": "Welcome to Hermes Agent! Type your message or /help for commands.",
378378
"goodbye": "Goodbye! ⚕",
379379
"response_label": " ⚕ Hermes ",
380-
"prompt_symbol": "❯ ",
380+
"prompt_symbol": "❯",
381381
"help_header": "[?] Available Commands",
382382
},
383383
"tool_prefix": "│",
@@ -414,7 +414,7 @@ def get_branding(self, key: str, fallback: str = "") -> str:
414414
"welcome": "Welcome to Hermes Agent! Type your message or /help for commands.",
415415
"goodbye": "Goodbye! \u2695",
416416
"response_label": " \u2695 Hermes ",
417-
"prompt_symbol": "\u276f ",
417+
"prompt_symbol": "\u276f",
418418
"help_header": "(^_^)? Available Commands",
419419
},
420420
"tool_prefix": "\u250a",
@@ -467,7 +467,7 @@ def get_branding(self, key: str, fallback: str = "") -> str:
467467
"welcome": "Welcome to Poseidon Agent! Type your message or /help for commands.",
468468
"goodbye": "Fair winds! Ψ",
469469
"response_label": " Ψ Poseidon ",
470-
"prompt_symbol": ",
470+
"prompt_symbol": "Ψ",
471471
"help_header": "(Ψ) Available Commands",
472472
},
473473
"tool_prefix": "│",
@@ -539,7 +539,7 @@ def get_branding(self, key: str, fallback: str = "") -> str:
539539
"welcome": "Welcome to Sisyphus Agent! Type your message or /help for commands.",
540540
"goodbye": "The boulder waits. ◉",
541541
"response_label": " ◉ Sisyphus ",
542-
"prompt_symbol": "◉",
542+
"prompt_symbol": "◉",
543543
"help_header": "(◉) Available Commands",
544544
},
545545
"tool_prefix": "│",
@@ -612,7 +612,7 @@ def get_branding(self, key: str, fallback: str = "") -> str:
612612
"welcome": "Welcome to Charizard Agent! Type your message or /help for commands.",
613613
"goodbye": "Flame out! ✦",
614614
"response_label": " ✦ Charizard ",
615-
"prompt_symbol": "✦",
615+
"prompt_symbol": "✦",
616616
"help_header": "(✦) Available Commands",
617617
},
618618
"tool_prefix": "│",
@@ -780,12 +780,21 @@ def init_skin_from_config(config: dict) -> None:
780780
# =============================================================================
781781

782782

783-
def get_active_prompt_symbol(fallback: str = "❯ ") -> str:
784-
"""Get the interactive prompt symbol from the active skin."""
783+
def get_active_prompt_symbol(fallback: str = "❯") -> str:
784+
"""Return the interactive prompt symbol with a single trailing space.
785+
786+
Skins store ``prompt_symbol`` as a bare token (no spaces). The trailing
787+
space is appended here so callers can drop it straight into a rendered
788+
prompt without hand-rolling whitespace.
789+
"""
785790
try:
786-
return get_active_skin().get_branding("prompt_symbol", fallback)
791+
raw = get_active_skin().get_branding("prompt_symbol", fallback)
787792
except Exception:
788-
return fallback
793+
raw = fallback
794+
795+
cleaned = (raw or fallback).strip()
796+
797+
return f"{cleaned or fallback.strip()} "
789798

790799

791800

0 commit comments

Comments
 (0)