Skip to content
Open
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
6 changes: 6 additions & 0 deletions gateway/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -9408,13 +9408,19 @@ async def _handle_message_with_agent(self, event, source, _quick_key: str, run_g
_footer_line = ""
try:
from gateway.runtime_footer import build_footer_line as _bfl
_turn_count = None
try:
_turn_count = sum(1 for _m in (history or []) if _m.get("role") == "user") + 1
except Exception:
_turn_count = None
_footer_line = _bfl(
user_config=_load_gateway_config(),
platform_key=_platform_config_key(source.platform),
model=agent_result.get("model"),
context_tokens=agent_result.get("last_prompt_tokens", 0) or 0,
context_length=agent_result.get("context_length") or None,
cwd=os.environ.get("TERMINAL_CWD", ""),
turn_count=_turn_count,
)
except Exception as _footer_err:
logger.debug("runtime_footer build failed: %s", _footer_err)
Expand Down
30 changes: 29 additions & 1 deletion gateway/runtime_footer.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
runtime_footer:
enabled: true # off by default
fields: [model, context_pct, cwd] # order shown; drop any to hide
# Optional: include ``turn_count`` for a subtle ``12/100`` session tracker.
turn_limit: 100 # denominator for turn_count

Per-platform overrides live under ``display.platforms.<platform>.runtime_footer``.
Users can toggle the global setting with ``/footer on|off`` from both the CLI
Expand Down Expand Up @@ -53,6 +55,17 @@ def _model_short(model: Optional[str]) -> str:
return model.rsplit("/", 1)[-1]


def _coerce_positive_int(value: Any) -> Optional[int]:
"""Return *value* as a positive int, or ``None`` when invalid/missing."""
try:
coerced = int(value)
except (TypeError, ValueError):
return None
if coerced <= 0:
return None
return coerced


def resolve_footer_config(
user_config: dict[str, Any] | None,
platform_key: str | None = None,
Expand All @@ -64,7 +77,7 @@ def resolve_footer_config(
2. ``display.runtime_footer``
3. ``display.platforms.<platform_key>.runtime_footer``
"""
resolved = {"enabled": False, "fields": list(_DEFAULT_FIELDS)}
resolved = {"enabled": False, "fields": list(_DEFAULT_FIELDS), "turn_limit": 100}
cfg = (user_config or {}).get("display") or {}

global_cfg = cfg.get("runtime_footer")
Expand All @@ -73,6 +86,9 @@ def resolve_footer_config(
resolved["enabled"] = bool(global_cfg.get("enabled"))
if isinstance(global_cfg.get("fields"), list) and global_cfg["fields"]:
resolved["fields"] = [str(f) for f in global_cfg["fields"]]
turn_limit = _coerce_positive_int(global_cfg.get("turn_limit"))
if turn_limit is not None:
resolved["turn_limit"] = turn_limit

if platform_key:
platforms = cfg.get("platforms") or {}
Expand All @@ -84,6 +100,9 @@ def resolve_footer_config(
resolved["enabled"] = bool(plat_footer.get("enabled"))
if isinstance(plat_footer.get("fields"), list) and plat_footer["fields"]:
resolved["fields"] = [str(f) for f in plat_footer["fields"]]
turn_limit = _coerce_positive_int(plat_footer.get("turn_limit"))
if turn_limit is not None:
resolved["turn_limit"] = turn_limit

return resolved

Expand All @@ -94,6 +113,8 @@ def format_runtime_footer(
context_tokens: int,
context_length: Optional[int],
cwd: Optional[str] = None,
turn_count: Optional[int] = None,
turn_limit: int = 100,
fields: Iterable[str] = _DEFAULT_FIELDS,
) -> str:
"""Render the footer line, or return "" if no fields have data.
Expand All @@ -115,6 +136,10 @@ def format_runtime_footer(
rel = _home_relative_cwd(cwd or os.environ.get("TERMINAL_CWD", ""))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please also handle the /footer on preview path: it calls format_runtime_footer without turn_count (gateway/slash_commands.py:3073-3078), so configuring only this new field would render an empty preview.

if rel:
parts.append(rel)
elif field == "turn_count":
if turn_count is not None and turn_limit > 0:
shown_count = max(1, int(turn_count))
parts.append(f"{shown_count}/{int(turn_limit)}")
# Unknown field names are silently ignored.

if not parts:
Expand All @@ -130,6 +155,7 @@ def build_footer_line(
context_tokens: int,
context_length: Optional[int],
cwd: Optional[str] = None,
turn_count: Optional[int] = None,
) -> str:
"""Top-level entry point used by gateway/run.py.

Expand All @@ -145,5 +171,7 @@ def build_footer_line(
context_tokens=context_tokens,
context_length=context_length,
cwd=cwd,
turn_count=turn_count,
turn_limit=int(cfg.get("turn_limit") or 100),
fields=cfg.get("fields") or _DEFAULT_FIELDS,
)
72 changes: 71 additions & 1 deletion tests/gateway/test_runtime_footer.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,13 +147,36 @@ def test_format_footer_unknown_field_silently_ignored():
assert out == "gpt-5.4 · 50%"


def test_format_footer_turn_count_only():
out = format_runtime_footer(
model="openai/gpt-5.4",
context_tokens=50, context_length=100,
cwd="/x",
turn_count=12,
fields=("turn_count",),
)
assert out == "12/100"


def test_format_footer_turn_count_custom_limit():
out = format_runtime_footer(
model="openai/gpt-5.4",
context_tokens=50, context_length=100,
cwd="/x",
turn_count=7,
turn_limit=50,
fields=("turn_count",),
)
assert out == "7/50"


# ---------------------------------------------------------------------------
# resolve_footer_config
# ---------------------------------------------------------------------------

def test_resolve_defaults_off_empty_config():
cfg = resolve_footer_config({}, "telegram")
assert cfg == {"enabled": False, "fields": ["model", "context_pct", "cwd"]}
assert cfg == {"enabled": False, "fields": ["model", "context_pct", "cwd"], "turn_limit": 100}


def test_resolve_global_enable():
Expand All @@ -163,6 +186,12 @@ def test_resolve_global_enable():
assert cfg["fields"] == ["model", "context_pct", "cwd"]


def test_resolve_global_turn_limit():
user = {"display": {"runtime_footer": {"enabled": True, "turn_limit": 75}}}
cfg = resolve_footer_config(user, "telegram")
assert cfg["turn_limit"] == 75


def test_resolve_platform_override_wins():
user = {
"display": {
Expand Down Expand Up @@ -231,6 +260,47 @@ def test_build_footer_returns_rendered_when_enabled(monkeypatch, tmp_path):
assert "25%" in out


def test_build_footer_turn_count_platform_override():
out = build_footer_line(
user_config={
"display": {
"platforms": {
"discord": {
"runtime_footer": {"enabled": True, "fields": ["turn_count"]}
}
}
}
},
platform_key="discord",
model="openai/gpt-5.4",
context_tokens=25,
context_length=100,
cwd="/tmp",
turn_count=42,
)
assert out == "42/100"


def test_build_footer_turn_count_uses_configured_platform_limit():
out = build_footer_line(
user_config={
"display": {
"runtime_footer": {"enabled": True, "fields": ["turn_count"], "turn_limit": 100},
"platforms": {
"discord": {"runtime_footer": {"turn_limit": 250}}
},
}
},
platform_key="discord",
model="openai/gpt-5.4",
context_tokens=25,
context_length=100,
cwd="/tmp",
turn_count=42,
)
assert out == "42/250"


def test_build_footer_per_platform_off_suppresses():
user = {
"display": {
Expand Down