Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 82 additions & 1 deletion cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5597,6 +5597,34 @@ def _handle_resume_command(self, cmd_original: str) -> None:
else:
_cprint(f" ↻ Resumed session {target_id}{title_part} — no messages, starting fresh.")

def _handle_sessions_command(self) -> None:
"""Handle /sessions — list and browse saved sessions."""
# Fix for Issue #22951: /sessions unknown command in classic REPL mode
if not self._session_db:
from hermes_state import format_session_db_unavailable
_cprint(f" {format_session_db_unavailable()}")
return

try:
sessions = self._session_db.list_sessions()
except Exception as exc:
_cprint(f" Error reading sessions: {exc}")
return

if not sessions:
_cprint(" No saved sessions yet. Sessions are created automatically as you chat.")
return

try:
from hermes_cli.main import _session_browse_picker
selected = _session_browse_picker(sessions)
except Exception as exc:
_cprint(f" Error browsing sessions: {exc}")
return

if selected is not None:
self._relaunch(session_id=selected)

def _handle_branch_command(self, cmd_original: str) -> None:
"""Handle /branch [name] — fork the current session into a new independent copy.

Expand Down Expand Up @@ -5869,11 +5897,18 @@ def _ask():

if self._app:
from prompt_toolkit.application import run_in_terminal
import threading
was_visible = self._status_bar_visible
self._status_bar_visible = False
self._app.invalidate()
try:
run_in_terminal(_ask)
# Fix for Issue #22970: RuntimeWarning when slash commands run
# from background threads. run_in_terminal() returns a coroutine
# that can't be scheduled when not on the main thread.
if threading.current_thread() is threading.main_thread():
run_in_terminal(_ask)
else:
_ask()
finally:
self._status_bar_visible = was_visible
self._app.invalidate()
Expand Down Expand Up @@ -6922,8 +6957,12 @@ def process_command(self, command: str) -> bool:
self.new_session(title=title)
elif canonical == "resume":
self._handle_resume_command(cmd_original)
elif canonical == "sessions":
self._handle_sessions_command()
elif canonical == "model":
self._handle_model_switch(cmd_original)
elif canonical == "indicator":
self._handle_indicator_command(cmd_original)
elif canonical == "gquota":
self._handle_gquota_command(cmd_original)

Expand Down Expand Up @@ -7897,6 +7936,48 @@ def _toggle_yolo(self):
" — all commands auto-approved. Use with caution."
)

def _handle_indicator_command(self, cmd_original: str) -> None:
"""Pick the TUI busy-indicator style (kaomoji/emoji/unicode/ascii).

Usage:
/indicator → show current style
/indicator kaomoji → set to kaomoji (default)
/indicator emoji → set to emoji
/indicator unicode → set to unicode (braille spinner)
/indicator ascii → set to ascii
"""
from hermes_cli.config import load_config
from hermes_cli.colors import Colors as _Colors

VALID_STYLES = {"kaomoji", "emoji", "unicode", "ascii"}

# Parse arg
arg = ""
try:
parts = (cmd_original or "").strip().split(None, 1)
if len(parts) > 1:
arg = parts[1].strip().lower()
except Exception:
arg = ""

cfg = load_config() or {}
current = ((cfg.get("display") or {}).get("tui_status_indicator", "kaomoji"))

if arg in ("status", "?", ""):
_cprint(f" {_Colors.BOLD}Busy indicator:{_Colors.RESET} {current}")
_cprint(f" Available: {', '.join(sorted(VALID_STYLES))}")
return

if arg not in VALID_STYLES:
_cprint(f" Unknown indicator style: {arg}")
_cprint(f" Available: {', '.join(sorted(VALID_STYLES))}")
return

if save_config_value("display.tui_status_indicator", arg):
_cprint(f" Busy indicator set to: {_Colors.GREEN}{arg}{_Colors.RESET} (saved)")
else:
_cprint(f" Busy indicator set to: {arg}")

def _handle_reasoning_command(self, cmd: str):
"""Handle /reasoning — manage effort level and display toggle.

Expand Down
18 changes: 17 additions & 1 deletion hermes_cli/oneshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,23 @@ def _run_agent(
agent.stream_delta_callback = None
agent.tool_gen_callback = None

return agent.chat(prompt) or ""
response = agent.chat(prompt) or ""

# Fix for Issue #22975: oneshot returns empty stdout despite successful API response.
# When streaming is active, agent.chat() may return an empty string because the
# streaming path consumes the response content before it can be captured as a
# return value. We recover the response from the agent's internal state.
if not response and hasattr(agent, "_last_full_response"):
response = agent._last_full_response or ""

# Fallback: try to reconstruct from conversation history if still empty
if not response and hasattr(agent, "conversation_history") and agent.conversation_history:
for msg in reversed(agent.conversation_history):
if msg.get("role") == "assistant" and msg.get("content"):
response = msg["content"]
break

return response


def _oneshot_clarify_callback(question: str, choices=None) -> str:
Expand Down
2 changes: 1 addition & 1 deletion ui-tui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"scripts": {
"dev": "npm run build --prefix packages/hermes-ink && tsx --watch src/entry.tsx",
"start": "tsx src/entry.tsx",
"build": "npm run build --prefix packages/hermes-ink && tsc -p tsconfig.build.json && npm run build:compile && chmod +x dist/entry.js",
"build": "npm run build --prefix packages/hermes-ink && tsc -p tsconfig.build.json && npm run build:compile",
"build:compile": "babel dist --out-dir dist --config-file ./babel.compiler.config.cjs --extensions .js --keep-file-extension",
"type-check": "tsc --noEmit -p tsconfig.json",
"lint": "eslint src/ packages/",
Expand Down
6 changes: 6 additions & 0 deletions ui-tui/src/app/useMainApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,12 @@ export function useMainApp(gw: GatewayClient) {
clearTimeout(timer)
timer = setTimeout(() => {
timer = undefined
// Fix for Issue #22976: clear the terminal before re-rendering on resize
// to prevent ghost separator lines from accumulating. Ink's VDOM diff
// does not invalidate old output when the canvas geometry changes.
if (stdout && typeof stdout.write === 'function') {
stdout.write('\x1b[2J\x1b[H')
}
void rpc<TerminalResizeResponse>('terminal.resize', { cols: stdout.columns ?? 80, session_id: ui.sid })
}, 100)
}
Expand Down
8 changes: 7 additions & 1 deletion web/src/pages/SessionsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,13 @@ function MessageBubble({
},
};

const style = ROLE_STYLES[msg.role] ?? ROLE_STYLES.system;
// Fix for Issue #22961: vision_analyze tool results displayed as user messages.
// When a message has tool_name but role is incorrectly set to 'user' (happens
// for some vision tool results), override the display role to 'tool'.
const effectiveRole =
msg.tool_name && msg.role === "user" ? "tool" : msg.role;

const style = ROLE_STYLES[effectiveRole] ?? ROLE_STYLES.system;
const label = msg.tool_name
? `${t.sessions.roles.tool}: ${msg.tool_name}`
: style.label;
Expand Down