feat(cli,gateway): unify /context into a visual context-usage breakdown

Extends the cherry-picked /context command (PR #52184) and prompt-size
attribution helpers (PR #66656) into one visual context view across
surfaces, and absorbs the per-component budget-visibility goal of the
/tokens proposal (PR #48470):

- agent/context_breakdown.py: pure renderers over the existing payload —
  a 5x20 glyph block grid (1 cell ~= 1% of the model window), an
  'Estimated usage by category' table with free space, and expanded
  per-skill / per-toolset listings via compute_context_details(), which
  reuses the prompt-size attribution mechanism (skills index-line bytes +
  registry tool->toolset map) converted to the same chars/4 heuristic.
- cli.py: /context [all] renders grid + category table (+ expanded
  listings) from the live agent and in-memory conversation history.
- gateway/slash_commands.py: /context appends the plain-text category
  table (no grid — monospace not guaranteed on messaging platforms);
  /context all adds the expanded listings. Fail-open: breakdown errors
  never break the gauge.
- hermes_cli/commands.py: /context gains the 'all' subcommand; /version
  demoted to /hermes version on Slack to keep the 50-slash cap.
- tests: renderer unit tests against synthetic payloads, registry test,
  gateway /context + /context all + failure-degradation handler tests.
- docs: slash-commands reference + CLI guide entries.

Read-only and locally computed: no provider calls, no prompt-cache impact.

Co-authored-by: RemyFevry <29257684+RemyFevry@users.noreply.github.com>
Co-authored-by: joelbrilliant <joelbrilliant1@gmail.com>
Co-authored-by: CharlesMcquade <6466275+CharlesMcquade@users.noreply.github.com>
This commit is contained in:
teknium1 2026-07-26 14:22:50 -07:00 committed by Teknium
parent 8b3da145f1
commit 07370a9dba
9 changed files with 591 additions and 2 deletions

View file

@ -154,3 +154,207 @@ def compute_session_context_breakdown(
"estimated_total": estimated_total,
"model": getattr(agent, "model", "") or "",
}
# ── /context rendering (CLI + gateway) ──────────────────────────────────────
#
# Pure text renderers over the payload above. The CLI shows a glyph block-grid
# plus a category table; the gateway uses the same table without the grid
# (proportional monospace is not guaranteed on messaging platforms).
_CATEGORY_GLYPHS = {
"system_prompt": "",
"tool_definitions": "",
"rules": "",
"skills": "",
"mcp": "",
"subagent_definitions": "",
"memory": "",
"conversation": "",
}
_FREE_GLYPH = "·"
_GRID_COLUMNS = 20
_GRID_ROWS = 5 # 100 cells → 1 cell per percent of the context window
# Human-readable tables cap the expanded listings; nothing is dropped from
# the underlying data.
_DETAILS_TABLE_LIMIT = 15
def _bytes_to_tokens(size: Optional[int]) -> Optional[int]:
if size is None:
return None
return (int(size) + 3) // 4
def compute_context_details(agent: Any) -> Dict[str, Any]:
"""Expanded per-skill / per-toolset cost listing for ``/context all``.
Reuses the ``hermes prompt-size`` attribution mechanism (PR #66656):
per-skill index-line bytes parsed from the live ``<available_skills>``
block, and per-toolset schema bytes attributed via the tool registry's
canonical tooltoolset map. Byte figures are converted to the same
chars/4 token heuristic the categories above use.
"""
from hermes_cli.prompt_size import (
_compute_skills_breakdown,
_compute_toolsets_breakdown,
)
from agent.system_prompt import build_system_prompt_parts
parts = build_system_prompt_parts(agent)
stable = parts.get("stable", "") or ""
skills_match = _SKILLS_BLOCK_RE.search(stable)
skills_block = skills_match.group(0) if skills_match else ""
skills: List[Dict[str, Any]] = []
if skills_block:
for entry in _compute_skills_breakdown(skills_block):
skills.append({
"name": entry.get("name", ""),
"index_tokens": _bytes_to_tokens(entry.get("index_line_bytes")) or 0,
"skill_md_tokens": _bytes_to_tokens(entry.get("skill_md_bytes")),
})
toolsets: List[Dict[str, Any]] = []
tools = list(getattr(agent, "tools", None) or [])
if tools:
for group in _compute_toolsets_breakdown(tools):
toolsets.append({
"toolset": group.get("toolset", ""),
"tool_count": int(group.get("tool_count", 0) or 0),
"schema_tokens": _bytes_to_tokens(group.get("json_bytes")) or 0,
})
return {"skills": skills, "toolsets": toolsets}
def render_context_grid(payload: Dict[str, Any]) -> List[str]:
"""Render the payload as a Claude Code-style glyph block grid.
100 cells (5×20), each one percent of the model context window. Categories
fill in declaration order; the remainder renders as free space.
"""
context_max = int(payload.get("context_max") or 0)
categories = payload.get("categories") or []
total_cells = _GRID_COLUMNS * _GRID_ROWS
cells: List[str] = []
if context_max > 0:
for cat in categories:
tokens = int(cat.get("tokens") or 0)
n = round(tokens / context_max * total_cells)
if tokens > 0 and n == 0:
n = 1 # never render a nonzero category as invisible
glyph = _CATEGORY_GLYPHS.get(str(cat.get("id") or ""), "")
cells.extend([glyph] * n)
cells = cells[:total_cells]
cells.extend([_FREE_GLYPH] * (total_cells - len(cells)))
return [
" ".join(cells[row * _GRID_COLUMNS:(row + 1) * _GRID_COLUMNS])
for row in range(_GRID_ROWS)
]
def render_context_category_lines(payload: Dict[str, Any]) -> List[str]:
"""Render the 'Estimated usage by category' table as plain-text lines."""
categories = payload.get("categories") or []
context_max = int(payload.get("context_max") or 0)
estimated_total = int(payload.get("estimated_total") or 0)
denom = context_max or estimated_total
lines = ["Estimated usage by category"]
if not categories:
lines.append(" (no data yet — send a message first)")
return lines
width = max(len(str(cat.get("label") or "")) for cat in categories)
width = max(width, len("Free space"))
for cat in categories:
tokens = int(cat.get("tokens") or 0)
glyph = _CATEGORY_GLYPHS.get(str(cat.get("id") or ""), "")
pct = tokens / denom * 100 if denom else 0.0
label = str(cat.get("label") or cat.get("id") or "")
lines.append(f"{glyph} {label:<{width}} {tokens:>9,} tokens {pct:>5.1f}%")
if context_max > 0:
free = max(0, context_max - estimated_total)
pct = free / context_max * 100
lines.append(f"{_FREE_GLYPH} {'Free space':<{width}} {free:>9,} tokens {pct:>5.1f}%")
return lines
def render_context_details_lines(details: Dict[str, Any]) -> List[str]:
"""Render the expanded ``/context all`` per-skill / per-toolset tables."""
lines: List[str] = []
toolsets = details.get("toolsets") or []
if toolsets:
lines.append("Toolsets by schema cost (largest first)")
for group in toolsets[:_DETAILS_TABLE_LIMIT]:
lines.append(
f" {group['toolset']:<24} {group['tool_count']:>3} tools"
f" {group['schema_tokens']:>8,} tokens"
)
remaining = len(toolsets) - _DETAILS_TABLE_LIMIT
if remaining > 0:
lines.append(f" … and {remaining} more")
skills = details.get("skills") or []
if skills:
if lines:
lines.append("")
lines.append("Skills by cost (index = always-on; SKILL.md = cost when loaded)")
for entry in skills[:_DETAILS_TABLE_LIMIT]:
name = str(entry.get("name") or "")
if len(name) > 28:
name = name[:27] + ""
md = entry.get("skill_md_tokens")
md_str = f"{md:>8,}" if md is not None else f"{'n/a':>8}"
lines.append(
f" {name:<28} index {entry['index_tokens']:>6,}"
f" SKILL.md {md_str} tokens"
)
remaining = len(skills) - _DETAILS_TABLE_LIMIT
if remaining > 0:
lines.append(f" … and {remaining} more")
return lines
def render_context_breakdown_lines(
payload: Dict[str, Any],
*,
details: Optional[Dict[str, Any]] = None,
grid: bool = True,
) -> List[str]:
"""Render the full /context view as plain-text lines.
``grid=True`` (CLI) prepends the glyph block grid; the gateway passes
``grid=False`` and keeps its own gauge. ``details`` (from
:func:`compute_context_details`) appends the expanded listings.
"""
lines: List[str] = []
if grid:
lines.extend(render_context_grid(payload))
lines.append("")
lines.extend(render_context_category_lines(payload))
context_max = int(payload.get("context_max") or 0)
context_used = int(payload.get("context_used") or 0)
if context_max > 0:
pct = int(payload.get("context_percent") or 0)
lines.append("")
lines.append(
f"Context window: {context_used:,} / {context_max:,} tokens ({pct}%)"
)
if details is not None:
detail_lines = render_context_details_lines(details)
if detail_lines:
lines.append("")
lines.extend(detail_lines)
else:
lines.append("")
lines.append("Use /context all for per-skill and per-toolset costs.")
return lines

51
cli.py
View file

@ -9628,6 +9628,8 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
self._show_gateway_status()
elif canonical == "status":
self._show_session_status()
elif canonical == "context":
self._show_context_breakdown(cmd_original)
elif canonical == "egress":
from hermes_cli.proxy_cli import format_status_text
@ -10699,6 +10701,55 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
return
print(f" {result.message}")
def _show_context_breakdown(self, cmd_original: str = ""):
"""`/context [all]` — visual context-window usage breakdown.
Renders a 5×20 glyph block grid (each cell 1% of the model context
window) plus an estimated per-category table: system prompt, tool
definitions, rules, skills index, MCP, subagents, memory, and the
conversation itself versus free space. `/context all` appends the
expanded per-skill and per-toolset cost listings.
Read-only: same chars/4 estimation engine as the desktop context
popover (agent.context_breakdown) no provider calls, no prompt-cache
impact.
"""
if not self.agent:
print(" (._.) No active agent -- send a message first.")
return
args = cmd_original.split(maxsplit=1)[1].strip().lower() if " " in cmd_original else ""
expanded = args in {"all", "full", "details"}
from agent.context_breakdown import (
compute_context_details,
compute_session_context_breakdown,
render_context_breakdown_lines,
)
try:
payload = compute_session_context_breakdown(
self.agent, self.conversation_history
)
except Exception as e:
print(f" (._.) Could not compute context breakdown: {e}")
return
details = None
if expanded:
try:
details = compute_context_details(self.agent)
except Exception:
details = {"skills": [], "toolsets": []}
model = payload.get("model") or self.model
print()
print(f" 🧠 Context Usage — {model}")
print()
for line in render_context_breakdown_lines(payload, details=details, grid=True):
print(f" {line}")
print()
def _show_usage(self):
"""Rate limits + session token usage (when a live agent exists) + Nous credits.

View file

@ -726,12 +726,16 @@ class GatewaySlashCommandsMixin:
(between turns), then the SessionStore/SessionDB metadata for a gauge
even when no agent is resident. Falls back to a transcript estimate only
as a last resort.
``/context all`` appends the expanded per-skill / per-toolset cost
listings (requires a resident agent).
"""
from gateway.run import _AGENT_PENDING_SENTINEL
source = event.source
session_key = self._session_key_for_source(source)
session_entry = await self.async_session_store.get_or_create_session(source)
expanded = event.get_command_args().strip().lower() in {"all", "full", "details"}
# Try running agent first (mid-turn), then cached agent (between turns).
agent = self._running_agents.get(session_key)
@ -861,6 +865,18 @@ class GatewaySlashCommandsMixin:
lines.append("")
lines.append(t("gateway.context.detail_after_first"))
# Per-category estimated breakdown (+ optional expanded listings).
# Same chars/4 engine the desktop popover and /usage use; plain
# text (no glyph grid — monospace isn't guaranteed on messaging
# platforms). Fail-open: rendering errors never break /context.
if has_agent:
breakdown = await asyncio.to_thread(
self._context_breakdown_block, agent, source, expanded
)
if breakdown:
lines.append("")
lines.extend(breakdown)
return "\n".join(lines)
# Last resort: rough estimate from transcript
@ -4536,6 +4552,43 @@ class GatewaySlashCommandsMixin:
lines.append("Top up and manage billing in the browser — your balance updates here after.")
return "\n".join(lines)
def _context_breakdown_block(self, agent, source, expanded: bool) -> list[str]:
"""Render the /context per-category block (plain text, no grid).
Estimated (chars/4) same engine as the desktop popover and /usage.
``expanded`` appends the per-skill / per-toolset listings from the
prompt-size attribution mechanism. Runs in a thread (sync store reads);
returns [] and never raises so /context stays robust.
"""
try:
from agent.context_breakdown import (
compute_context_details,
compute_session_context_breakdown,
render_context_breakdown_lines,
)
history: list[dict] = []
try:
entry = self.session_store.get_or_create_session(source)
history = self.session_store.load_transcript(entry.session_id) or []
except Exception:
history = []
payload = compute_session_context_breakdown(agent, history)
if not (payload.get("categories") or []):
return []
details = None
if expanded:
try:
details = compute_context_details(agent)
except Exception:
details = {"skills": [], "toolsets": []}
return render_context_breakdown_lines(payload, details=details, grid=False)
except Exception:
return []
def _context_breakdown_lines(self, agent, source) -> list[str]:
"""Render the per-category context breakdown for /usage.

View file

@ -120,8 +120,8 @@ COMMAND_REGISTRY: list[CommandDef] = [
CommandDef("status", "Show session, model, token, and context info", "Session"),
CommandDef("egress", "Show Docker egress proxy status", "Session",
args_hint="[status]", subcommands=("status",)),
CommandDef("context", "Show detailed context window view with usage gauge, compression stats, and throughput", "Session",
aliases=("ctx",)),
CommandDef("context", "Show detailed context window view with usage gauge, category breakdown, compression stats, and throughput", "Session",
aliases=("ctx",), args_hint="[all]", subcommands=("all",)),
CommandDef("whoami", "Show your slash command access (admin / user)", "Info"),
CommandDef("profile", "Show active profile name and home directory", "Info"),
CommandDef("sethome", "Set this chat as the home channel", "Session",
@ -1180,6 +1180,10 @@ _SLACK_PRIORITY_ALIASES = ("btw", "bg")
# rare from Slack; reachable as /hermes init. Without this entry, adding
# /init clamps /version off the native list and breaks Telegram parity.
_SLACK_VIA_HERMES_ONLY = frozenset({"topup", "moa", "debug", "egress", "init"})
# - version: low-frequency info command; reachable as /hermes version on
# Slack. Demoted when /context claimed a native slot (context is a
# recurring inspection surface; version is a one-off lookup).
_SLACK_VIA_HERMES_ONLY = frozenset({"topup", "moa", "debug", "egress", "version"})
def _sanitize_slack_name(raw: str) -> str:

View file

@ -58,3 +58,147 @@ def test_breakdown_uses_measured_context_when_available():
assert data["context_used"] == 42_000
assert data["context_percent"] == 21
# ── /context renderers (pure functions over the payload) ────────────────────
from agent.context_breakdown import ( # noqa: E402
compute_context_details,
render_context_breakdown_lines,
render_context_category_lines,
render_context_details_lines,
render_context_grid,
)
def _payload(**overrides):
base = {
"categories": [
{"id": "system_prompt", "label": "System prompt", "tokens": 10_000},
{"id": "tool_definitions", "label": "Tool definitions", "tokens": 20_000},
{"id": "skills", "label": "Skills", "tokens": 5_000},
{"id": "conversation", "label": "Conversation", "tokens": 15_000},
],
"context_max": 200_000,
"context_percent": 25,
"context_used": 50_000,
"estimated_total": 50_000,
"model": "openai/gpt-test",
}
base.update(overrides)
return base
def test_grid_is_5x20_and_mostly_free():
rows = render_context_grid(_payload())
assert len(rows) == 5
cells = " ".join(rows).split(" ")
assert len(cells) == 100
# 50k / 200k → 25 used cells, 75 free
assert cells.count("·") == 75
# Category glyphs proportional: 10k→5, 20k→10, 5k→2-3, 15k→7-8 cells
assert cells.count("") == 5
assert cells.count("") == 10
def test_grid_nonzero_category_never_invisible():
payload = _payload(
categories=[{"id": "memory", "label": "Memory", "tokens": 10}],
estimated_total=10,
context_used=10,
)
rows = render_context_grid(payload)
assert "" in " ".join(rows)
def test_grid_without_context_max_is_all_free():
rows = render_context_grid(_payload(context_max=0))
cells = " ".join(rows).split(" ")
assert set(cells) == {"·"}
def test_category_lines_include_tokens_percent_and_free_space():
lines = render_context_category_lines(_payload())
text = "\n".join(lines)
assert "Estimated usage by category" in text
assert "System prompt" in text and "10,000 tokens" in text
assert "5.0%" in text # 10k / 200k
assert "Free space" in text and "150,000 tokens" in text
def test_category_lines_no_categories():
lines = render_context_category_lines(_payload(categories=[]))
assert any("no data yet" in line for line in lines)
def test_breakdown_lines_grid_toggle():
with_grid = render_context_breakdown_lines(_payload(), grid=True)
without = render_context_breakdown_lines(_payload(), grid=False)
assert any("·" in line for line in with_grid[:5])
assert not any("·" in line for line in without[:2])
# Both include the window summary and the expand hint
for lines in (with_grid, without):
text = "\n".join(lines)
assert "Context window: 50,000 / 200,000 tokens (25%)" in text
assert "/context all" in text
def test_breakdown_lines_with_details_omits_hint():
details = {
"skills": [
{"name": "alpha", "index_tokens": 25, "skill_md_tokens": 800},
{"name": "beta", "index_tokens": 30, "skill_md_tokens": None},
],
"toolsets": [
{"toolset": "terminal", "tool_count": 3, "schema_tokens": 4_000},
],
}
lines = render_context_breakdown_lines(_payload(), details=details, grid=False)
text = "\n".join(lines)
assert "Toolsets by schema cost" in text
assert "terminal" in text and "4,000 tokens" in text
assert "Skills by cost" in text
assert "alpha" in text and "beta" in text
assert "n/a" in text # unmapped SKILL.md renders n/a, not a crash
assert "Use /context all" not in text
def test_details_lines_caps_listing():
details = {
"skills": [
{"name": f"skill-{i}", "index_tokens": 10, "skill_md_tokens": 100}
for i in range(20)
],
"toolsets": [],
}
lines = render_context_details_lines(details)
assert any("… and 5 more" in line for line in lines)
def test_compute_context_details_maps_bytes_to_tokens():
agent, parts = _make_agent(
stable=(
"base\n<available_skills>\n demo:\n"
" - hello: a demo skill\n</available_skills>"
),
)
fake_skills = [{
"name": "hello",
"index_line_bytes": 40,
"index_line_total_bytes": 40,
"index_line_shared_bytes": 0,
"index_line_skill_count": 1,
"skill_md_bytes": 401,
"path": "/tmp/hello/SKILL.md",
}]
fake_toolsets = [{"toolset": "terminal", "tool_count": 2, "json_bytes": 399}]
with patch("agent.system_prompt.build_system_prompt_parts", return_value=parts), \
patch("hermes_cli.prompt_size._compute_skills_breakdown", return_value=fake_skills), \
patch("hermes_cli.prompt_size._compute_toolsets_breakdown", return_value=fake_toolsets):
details = compute_context_details(agent)
assert details["skills"] == [
{"name": "hello", "index_tokens": 10, "skill_md_tokens": 101},
]
assert details["toolsets"] == [
{"toolset": "terminal", "tool_count": 2, "schema_tokens": 100},
]

View file

@ -969,3 +969,124 @@ async def test_context_command_no_data():
result = await runner._handle_context_command(_make_event("/context"))
assert "No context data available yet" in result
@pytest.mark.asyncio
async def test_context_command_includes_category_breakdown():
"""/context with a live agent appends the per-category estimated
breakdown (plain text, no glyph grid) and the /context all hint."""
session_entry = SessionEntry(
session_key=build_session_key(_make_source()),
session_id="sess-5",
created_at=datetime.now(),
updated_at=datetime.now(),
platform=Platform.TELEGRAM,
chat_type="dm",
)
runner = _make_runner(session_entry)
agent = _stub_agent()
runner._running_agents[session_entry.session_key] = agent
fake_payload = {
"categories": [
{"id": "system_prompt", "label": "System prompt", "tokens": 9_000},
{"id": "tool_definitions", "label": "Tool definitions", "tokens": 21_000},
],
"context_max": 200_000,
"context_percent": 24,
"context_used": 47_231,
"estimated_total": 30_000,
"model": "openai/gpt-test",
}
from unittest.mock import patch as _patch
with _patch(
"agent.context_breakdown.compute_session_context_breakdown",
return_value=fake_payload,
):
result = await runner._handle_context_command(_make_event("/context"))
assert "Estimated usage by category" in result
assert "System prompt" in result
assert "9,000 tokens" in result
assert "Tool definitions" in result
assert "Use /context all" in result
# No glyph grid on the gateway (plain-text variant)
assert "· · ·" not in result
@pytest.mark.asyncio
async def test_context_all_appends_expanded_listings():
"""/context all appends per-toolset and per-skill cost listings."""
session_entry = SessionEntry(
session_key=build_session_key(_make_source()),
session_id="sess-6",
created_at=datetime.now(),
updated_at=datetime.now(),
platform=Platform.TELEGRAM,
chat_type="dm",
)
runner = _make_runner(session_entry)
agent = _stub_agent()
runner._running_agents[session_entry.session_key] = agent
fake_payload = {
"categories": [
{"id": "skills", "label": "Skills", "tokens": 2_000},
],
"context_max": 200_000,
"context_percent": 24,
"context_used": 47_231,
"estimated_total": 2_000,
"model": "openai/gpt-test",
}
fake_details = {
"skills": [
{"name": "hermes-agent", "index_tokens": 30, "skill_md_tokens": 2_500},
],
"toolsets": [
{"toolset": "terminal", "tool_count": 4, "schema_tokens": 5_100},
],
}
from unittest.mock import patch as _patch
with _patch(
"agent.context_breakdown.compute_session_context_breakdown",
return_value=fake_payload,
), _patch(
"agent.context_breakdown.compute_context_details",
return_value=fake_details,
):
result = await runner._handle_context_command(_make_event("/context all"))
assert "Toolsets by schema cost" in result
assert "terminal" in result and "5,100 tokens" in result
assert "Skills by cost" in result
assert "hermes-agent" in result
# Expanded view drops the hint
assert "Use /context all" not in result
@pytest.mark.asyncio
async def test_context_breakdown_failure_never_breaks_command():
"""A breakdown engine crash degrades gracefully — the gauge still renders."""
session_entry = SessionEntry(
session_key=build_session_key(_make_source()),
session_id="sess-7",
created_at=datetime.now(),
updated_at=datetime.now(),
platform=Platform.TELEGRAM,
chat_type="dm",
)
runner = _make_runner(session_entry)
agent = _stub_agent()
runner._running_agents[session_entry.session_key] = agent
from unittest.mock import patch as _patch
with _patch(
"agent.context_breakdown.compute_session_context_breakdown",
side_effect=RuntimeError("boom"),
):
result = await runner._handle_context_command(_make_event("/context"))
assert "🧠 **Context Window**" in result
assert "In use: 47,231 / 200,000 (24%)" in result
assert "Estimated usage by category" not in result

View file

@ -120,6 +120,16 @@ class TestResolveCommand:
assert topic.name == "topic"
assert "topic" in GATEWAY_KNOWN_COMMANDS
def test_context_command_registered_with_ctx_alias(self):
ctx = resolve_command("context")
assert ctx is not None
assert ctx.name == "context"
assert resolve_command("ctx").name == "context"
assert "all" in (ctx.subcommands or ())
# Available on both CLI and gateway surfaces
assert not ctx.cli_only and not ctx.gateway_only
assert "context" in GATEWAY_KNOWN_COMMANDS
def test_leading_slash_stripped(self):
assert resolve_command("/help").name == "help"
assert resolve_command("/bg").name == "background"

View file

@ -58,6 +58,7 @@ Type `/` in the CLI to open the autocomplete menu. Built-in commands are case-in
| `/egress [status]` | Show Docker egress proxy status — enabled/configured/running state, credential source, token mappings, uncovered providers, and next remediation step. Works in CLI, TUI, Desktop chat, and messaging gateway. |
| `/redraw` | Force a full UI repaint (recovers from terminal drift after tmux resize, mouse selection artifacts, etc.) |
| `/status` | Show session info — model, provider, profile, session ID, working directory, title, created/updated timestamps, token totals, agent-running state — followed by a local **Session recap** block (recent user/assistant turn counts, tool result count, top tools used, last few files touched, the latest user prompt, and the latest assistant reply). The recap is computed locally from the in-memory conversation; no LLM call, no prompt-cache impact. |
| `/context [all]` (alias: `/ctx`) | Visual context-window breakdown. On the CLI/TUI: a 5×20 glyph block grid (each cell ≈ 1% of the model window) plus an estimated per-category table — system prompt, tool definitions, rules, skills index, MCP, subagents, memory, conversation — versus free space. On messaging platforms: a usage gauge with auto-compression threshold/headroom, compression stats, cumulative throughput, and the same category table in plain text. `/context all` appends per-skill and per-toolset cost listings (index cost vs SKILL.md load cost; schema tokens per toolset). Read-only and computed locally — no LLM call, no prompt-cache impact. |
| `/agents` (alias: `/tasks`) | Show active agents and running tasks across the current session. |
| `/background <prompt>` (alias: `/bg`, `/btw`) | Run a prompt in a separate background session. The agent processes your prompt independently — your current session stays free for other work. Results appear as a panel when the task finishes. See [CLI Background Sessions](/user-guide/cli#background-sessions). |
| `/branch [name]` (alias: `/fork`) | Branch the current session (explore a different path) |

View file

@ -135,6 +135,7 @@ Common examples:
| `/reasoning high` | Increase reasoning effort |
| `/title My Session` | Name the current session |
| `/status` | Show session info — model/profile/tokens/duration — followed by a local **Session recap** block (recent turn counts, top tools used, files touched, latest user prompt + assistant reply). Pure local compute; no LLM call. |
| `/context [all]` | Visual context-usage breakdown — glyph block grid + per-category token table (system prompt / tools / skills / memory / conversation / free space). `/context all` adds per-skill and per-toolset costs. |
| `/sessions` | Open an interactive session picker right inside the classic CLI (same surface the TUI uses). Type to filter, arrow keys to navigate, Enter to resume. |
For the full built-in CLI and messaging lists, see [Slash Commands Reference](../reference/slash-commands.md).