Skip to content
Open
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
66 changes: 56 additions & 10 deletions tui_gateway/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,14 @@ def __init__(self, session_key: str, model: str):
self._closed = False
from hermes_cli._subprocess_compat import windows_hide_flags

# start_new_session=True detaches the slash worker into its own
# process group / session. Without this, the worker inherits the
# gateway's pgid (= TUI parent PID). When mcp_tool's
# _kill_orphaned_mcp_children races with slash_worker spawn and sweeps
# the gateway's child set, it captures the worker PID, records the
# inherited pgid, and killpg() then kills the TUI parent itself.
# See agent/lsp/client.py for the symmetric LSP server fix and
# tools/mcp_tool.py _filter_mcp_children for defense-in-depth.
self.proc = subprocess.Popen(
argv,
stdin=subprocess.PIPE,
Expand All @@ -296,6 +304,7 @@ def __init__(self, session_key: str, model: str):
# Tier-1 secrets (gateway/GitHub/infra) are still stripped (#29157).
env=hermes_subprocess_env(inherit_credentials=True),
creationflags=windows_hide_flags(),
start_new_session=True,
)
threading.Thread(target=self._drain_stdout, daemon=True).start()
threading.Thread(target=self._drain_stderr, daemon=True).start()
Expand Down Expand Up @@ -2964,12 +2973,31 @@ def _get_usage(agent) -> dict:
}
comp = getattr(agent, "context_compressor", None)
if comp:
ctx_used = getattr(comp, "last_prompt_tokens", 0) or usage["total"] or 0
# context_used is the *current-window* occupancy. Do NOT fall back to
# usage["total"] (cumulative lifetime session_total_tokens): for an
# external context engine that doesn't report last_prompt_tokens that
# substitution showed lifetime totals as the live context fill, yielding
# impossible readings such as 1.9m/120k clamped to 100% (#50421).
#
# Per the issue, populate context_used/percent only from a *real*
# current-occupancy value and "leave it unknown otherwise" — so a falsy
# last_prompt_tokens (0 or missing, i.e. an engine that doesn't track
# per-window occupancy) intentionally emits no gauge rather than a
# fabricated 0% or the old cumulative reading. The built-in compressor
# always reports a real last_prompt_tokens once a turn runs, so it is
# unaffected.
# Clamp the -1 "compression just ran, awaiting real usage" sentinel
# (conversation_compression.py) to 0 so the transitional turn reads as
# unknown (no gauge) instead of leaking context_used=-1. Matches the
# CLI status-bar path (cli.py _get_status_bar_snapshot).
last_prompt = getattr(comp, "last_prompt_tokens", 0) or 0
if last_prompt < 0:
last_prompt = 0
ctx_max = getattr(comp, "context_length", 0) or 0
if ctx_max:
usage["context_used"] = ctx_used
if ctx_max and last_prompt:
usage["context_used"] = last_prompt
usage["context_max"] = ctx_max
usage["context_percent"] = max(0, min(100, round(ctx_used / ctx_max * 100)))
usage["context_percent"] = max(0, min(100, round(last_prompt / ctx_max * 100)))
usage["compressions"] = getattr(comp, "compression_count", 0) or 0
# Live count of background/async subagents still running (delegate_task
# batches + background single delegations). Mirrors the classic CLI status
Expand Down Expand Up @@ -8121,7 +8149,12 @@ def _(rid, params: dict) -> dict:
return _err(rid, 4004, "truncate_before_user_ordinal must be an integer")
history = session.get("history", [])
user_indices = [i for i, m in enumerate(history) if m.get("role") == "user"]
if ordinal >= len(user_indices):
# Reject out-of-range ordinals on BOTH ends. A negative value would
# otherwise sail past the upper-bound check and hit Python's negative
# indexing below (user_indices[-1] -> the LAST user turn), silently
# truncating history to everything before it and persisting that loss
# via replace_messages — an unrecoverable overwrite of the session DB.
if ordinal < 0 or ordinal >= len(user_indices):
return _err(rid, 4018, "target user message is no longer in session history")
truncated = history[: user_indices[ordinal]]
session["history"] = truncated
Expand Down Expand Up @@ -10134,7 +10167,7 @@ def _resolve_toggle(current: bool) -> bool:
)
return _ok(rid, {"key": key, "value": nv})

if key == "compact":
if key in ("compact", "compact-ui"):
raw = str(value or "").strip().lower()
cfg0 = _load_cfg()
d0 = cfg0.get("display") if isinstance(cfg0.get("display"), dict) else {}
Expand Down Expand Up @@ -10800,7 +10833,7 @@ def _(rid, params: dict) -> dict:
)
nv = "full" if dm == "expanded" else "collapsed"
return _ok(rid, {"value": nv})
if key == "compact":
if key in ("compact", "compact-ui"):
on = bool((_load_cfg().get("display") or {}).get("tui_compact", False))
return _ok(rid, {"value": "on" if on else "off"})
if key == "statusbar":
Expand Down Expand Up @@ -11089,7 +11122,7 @@ def _(rid, params: dict) -> dict:
)

_TUI_EXTRA: list[tuple[str, str, str]] = [
("/compact", "Toggle compact display mode", "TUI"),
("/compact-ui", "Toggle compact display mode", "TUI"),
("/logs", "Show recent gateway log lines", "TUI"),
(
"/mouse",
Expand Down Expand Up @@ -11156,6 +11189,10 @@ def _(rid, params: dict) -> dict:
cat_map[cat].append([c, desc])

for name, desc, cat in _TUI_EXTRA:
# Skip TUI-only entries whose name collides with a registered
# command or alias (e.g. /compact is now an alias of /compress).
if name.lower() in canon:
continue
all_pairs.append([name, desc])
if cat not in cat_map:
cat_map[cat] = []
Expand Down Expand Up @@ -11310,19 +11347,28 @@ def _(rid, params: dict) -> dict:
if name in qcmds:
qc = qcmds[name]
if qc.get("type") == "exec":
# Sanitize env to prevent credential leakage —
# quick commands run in the TUI server process which
# has all API keys in os.environ.
from tools.environments.local import _sanitize_subprocess_env
sanitized_env = _sanitize_subprocess_env(os.environ.copy())
r = subprocess.run(
qc.get("command", ""),
shell=True,
capture_output=True,
text=True,
timeout=30,
stdin=subprocess.DEVNULL,
env=sanitized_env,
)
output = (
(r.stdout or "")
+ ("\n" if r.stdout and r.stderr else "")
+ (r.stderr or "")
).strip()[:4000]
if output:
from agent.redact import redact_sensitive_text
output = redact_sensitive_text(output)
if r.returncode != 0:
return _err(
rid,
Expand Down Expand Up @@ -12158,8 +12204,8 @@ def _(rid, params: dict) -> dict:
text_lower = text.lower()
extras = [
{
"text": "/compact",
"display": "/compact",
"text": "/compact-ui",
"display": "/compact-ui",
"meta": "Toggle compact display mode",
},
{
Expand Down
Loading