diff --git a/tests/agent/test_context_file_sources.py b/tests/agent/test_context_file_sources.py new file mode 100644 index 0000000000..aa2b9e06fe --- /dev/null +++ b/tests/agent/test_context_file_sources.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +"""Per-file context source manifest (`list_context_file_sources`). + +Read-only mirror of ``build_context_files_prompt`` discovery: one entry per +candidate context/instruction file with size, token estimate, and whether it +was loaded, truncated, or shadowed by a higher-priority context type. + +Inspired by: GitHub Copilot CLI 1.0.81 — "Show each user instruction file +separately in /instructions". +""" + +import pytest + +from agent.prompt_builder import list_context_file_sources + + +@pytest.fixture() +def project(tmp_path): + (tmp_path / ".git").mkdir() + return tmp_path + + +def _by_label(sources): + return {s["label"]: s for s in sources} + + +class TestDiscovery: + def test_agents_md_loaded(self, project): + (project / "AGENTS.md").write_text("# rules\n" * 10) + sources = list_context_file_sources(cwd=str(project)) + entry = _by_label(sources)["AGENTS.md"] + assert entry["loaded"] is True + assert entry["status"] == "loaded" + assert entry["chars"] > 0 + assert entry["est_tokens"] == (entry["chars"] + 3) // 4 + + def test_empty_project_returns_empty(self, tmp_path, tmp_path_factory): + (tmp_path / ".git").mkdir() + empty_home = tmp_path_factory.mktemp("empty_home") + assert ( + list_context_file_sources(cwd=str(tmp_path), home_override=empty_home) + == [] + ) + + def test_priority_shadowing_hermes_md_over_agents_md(self, project): + (project / ".hermes.md").write_text("hermes rules") + (project / "AGENTS.md").write_text("agents rules") + entries = _by_label(list_context_file_sources(cwd=str(project))) + assert entries[".hermes.md"]["status"] == "loaded" + assert entries["AGENTS.md"]["status"] == "shadowed" + assert entries["AGENTS.md"]["loaded"] is False + + def test_claude_md_shadowed_by_agents_md(self, project): + (project / "AGENTS.md").write_text("agents rules") + (project / "CLAUDE.md").write_text("claude rules") + entries = _by_label(list_context_file_sources(cwd=str(project))) + assert entries["AGENTS.md"]["status"] == "loaded" + assert entries["CLAUDE.md"]["status"] == "shadowed" + + def test_cursorrules_listed(self, project): + (project / ".cursorrules").write_text("cursor rules") + rules_dir = project / ".cursor" / "rules" + rules_dir.mkdir(parents=True) + (rules_dir / "a.mdc").write_text("rule a") + entries = _by_label(list_context_file_sources(cwd=str(project))) + assert entries[".cursorrules"]["status"] == "loaded" + assert entries[".cursor/rules/a.mdc"]["status"] == "loaded" + + def test_agents_override_wins_per_directory(self, project): + (project / "AGENTS.md").write_text("committed") + (project / "AGENTS.override.md").write_text("personal override") + labels = [s["label"] for s in list_context_file_sources(cwd=str(project))] + assert "AGENTS.override.md" in labels + assert "AGENTS.md" not in labels # first name wins per directory + + def test_directory_chain_lists_both_agents_files(self, project): + (project / "AGENTS.md").write_text("root rules") + sub = project / "pkg" + sub.mkdir() + (sub / "AGENTS.md").write_text("pkg rules") + sources = list_context_file_sources(cwd=str(sub)) + agents_entries = [s for s in sources if "AGENTS.md" in s["label"]] + assert len(agents_entries) == 2 + assert all(s["loaded"] for s in agents_entries) + + def test_truncated_status_when_over_cap(self, project, monkeypatch): + import agent.prompt_builder as pb + + monkeypatch.setattr(pb, "_get_context_file_max_chars", lambda *_a: 10) + (project / "AGENTS.md").write_text("x" * 100) + entries = _by_label(list_context_file_sources(cwd=str(project))) + assert entries["AGENTS.md"]["status"] == "truncated" + assert entries["AGENTS.md"]["loaded"] is True + + def test_soul_md_from_home_override(self, project, tmp_path_factory): + home = tmp_path_factory.mktemp("hermes_home") + (home / "SOUL.md").write_text("identity") + entries = _by_label( + list_context_file_sources(cwd=str(project), home_override=home) + ) + assert entries["SOUL.md"]["status"] == "loaded" + + def test_read_only_no_side_effects(self, project): + (project / "AGENTS.md").write_text("rules") + before = (project / "AGENTS.md").read_text() + list_context_file_sources(cwd=str(project)) + assert (project / "AGENTS.md").read_text() == before + + def test_accepts_path_object(self, project): + (project / "AGENTS.md").write_text("rules") + sources = list_context_file_sources(cwd=project) + assert _by_label(sources)["AGENTS.md"]["loaded"] is True diff --git a/website/docs/reference/slash-commands.md b/website/docs/reference/slash-commands.md index 80b1405cbf..5459da6f3c 100644 --- a/website/docs/reference/slash-commands.md +++ b/website/docs/reference/slash-commands.md @@ -63,7 +63,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. | +| `/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, followed by a per-file **Context files** listing (AGENTS.md chain, .hermes.md, CLAUDE.md, .cursorrules, SOUL.md) with each file's token estimate and whether it was loaded, truncated over `context_file_max_chars`, or shadowed by a higher-priority context type. 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. | | `/bg ` | 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). | | `/btw ` | Ask a quick side question **about the current conversation** without interrupting it. A one-shot auxiliary LLM call answers from a read-only snapshot of the transcript — the live session's history and prompt cache are untouched, and the current turn keeps running. For independent work with a fresh context, use `/bg`. |