Skip to content

feat(plugins): add context_indicator with prompt circle + /context command#342

Open
mmcguff wants to merge 4 commits into
mpfaffenberger:mainfrom
mmcguff:feat/context-indicator
Open

feat(plugins): add context_indicator with prompt circle + /context command#342
mmcguff wants to merge 4 commits into
mpfaffenberger:mainfrom
mmcguff:feat/context-indicator

Conversation

@mmcguff
Copy link
Copy Markdown
Contributor

@mmcguff mmcguff commented May 15, 2026

Summary

Adds a tiny ergonomics plugin that surfaces context-window usage:

  • A colored circle in the terminal prompt right after 🐶:
    • 🟢 under 30% used
    • 🟡 30–60% used
    • 🔴 over 60% used
  • A new /context slash command that prints a token-usage breakdown
    (messages vs. system/tools overhead vs. total / capacity) plus a
    little progress bar.

Demo

🐶 🟡 puppy [code-puppy] [openai:gpt-5] (~/proj) >>>

> /context
🟡 Context usage: 50.0%
  [███████████████░░░░░░░░░░░░░░░]
  Messages : 4,500 tokens
  Overhead : 500 tokens (system prompt + tools)
  Total    : 5,000 / 10,000 tokens
  Buckets  : 🟢 <30%   🟡 30–60%   🔴 >60%

Design

  • Plugin-only. No edits to code_puppy/command_line/ or anywhere
    in core. Uses the documented startup, custom_command, and
    custom_command_help hooks per CONTRIBUTING.md.
  • Idempotent prompt patch on get_prompt_with_active_model
    original is stashed on the module so re-installation is a no-op
    (mirrors the pattern from prompt_newline).
  • Token math reuses the agent's own helpers
    (estimate_tokens_for_message, _estimate_context_overhead,
    _get_model_context_length) so the indicator stays in lockstep with
    the numbers the compaction subsystem already computes.
  • Defensive everywhere. Any exception inside usage computation
    hides the indicator instead of crashing the prompt — the colored
    circle is a status badge, not a load-bearing dependency.
  • Single Responsibility split: usage.py knows tokens,
    register_callbacks.py knows prompt-toolkit and slash commands. ~100
    lines each, well under the 600-line cap.

Tests

tests/plugins/test_context_indicator_plugin.py22 cases covering:

  • All three threshold buckets at edge values (0%, 29.9%, 30%, 59.9%, 60%, 150%)
  • ContextUsage proportion / percent / indicator math
  • Defensive None returns when no agent / capacity is zero / imports fail
  • Idempotent prompt patching (re-install is no-op, original is preserved)
  • Indicator placement (after the 🐶 tuple, before the puppy name)
  • /context command (success path, no-usage path, ignores unrelated names)
  • Progress-bar rendering

Files

code_puppy/plugins/context_indicator/
├── __init__.py              # docstring + sentinel
├── usage.py                 # token-counting + ContextUsage dataclass (~100 LOC)
└── register_callbacks.py    # prompt patch + /context command (~150 LOC)
tests/plugins/test_context_indicator_plugin.py

No new dependencies. No core file modifications.

Adds a small ergonomic plugin that surfaces context-window usage at a
glance:

* Inserts a colored circle into the terminal prompt right after the dog
  emoji 🐶 reflecting how full the active agent's context window is:
    - 🟢 under 30%
    - 🟡 30%–60%
    - 🔴 over 60%
* Adds a `/context` slash command that prints a token-usage breakdown
  (messages vs. overhead vs. total / capacity) plus a progress bar.

Implementation notes:

* Plugin-only — no edits to core. Hooks `startup`, `custom_command`,
  and `custom_command_help` per CONTRIBUTING.md.
* Idempotent prompt patch on `get_prompt_with_active_model`; original
  is stashed on the module so re-installation is a no-op.
* Token math reuses the agent's existing
  `estimate_tokens_for_message` + `_estimate_context_overhead` +
  `_get_model_context_length` so the indicator stays in lockstep with
  the numbers the compaction subsystem already computes.
* Defensive everywhere: any exception inside usage computation hides
  the indicator instead of crashing the prompt.

Tests:

* `tests/plugins/test_context_indicator_plugin.py` — 22 cases covering
  threshold buckets, defensive None paths, idempotent patching,
  indicator placement, and the `/context` command (success + empty).
Copilot AI review requested due to automatic review settings May 15, 2026 19:51
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new context_indicator plugin that surfaces context-window usage both inline in the prompt (🟢/🟡/🔴 badge) and via a /context slash command that prints a token breakdown and progress bar.

Changes:

  • Introduces code_puppy.plugins.context_indicator with token usage computation and callback registration (prompt patch + /context).
  • Adds a dedicated unit test suite covering thresholds, prompt injection behavior, and slash-command output.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
code_puppy/plugins/context_indicator/__init__.py Adds plugin package docstring describing the feature and /context command.
code_puppy/plugins/context_indicator/usage.py Implements ContextUsage, threshold→indicator mapping, and current-agent usage estimation.
code_puppy/plugins/context_indicator/register_callbacks.py Registers startup prompt patch and /context command; formats the usage report/progress bar.
tests/plugins/test_context_indicator_plugin.py Adds tests for indicator thresholds, defensive behavior, prompt insertion, and /context output.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +20 to +36
def _ensure_agent_manager_stub():
"""Stub ``code_puppy.agents.agent_manager`` so ``patch()`` can target it.

Importing the real module pulls in MCP dependencies that aren't installed
in the test environment. The plugin only ever calls
``get_current_agent`` — a stub with that attribute is plenty.
"""
if "code_puppy.agents.agent_manager" in sys.modules:
return
stub = MagicMock()
stub.get_current_agent = MagicMock(side_effect=RuntimeError("unstubbed"))
sys.modules["code_puppy.agents.agent_manager"] = stub
# Also ensure parent ``code_puppy.agents`` namespace knows about it.
agents_pkg = sys.modules.get("code_puppy.agents")
if agents_pkg is not None:
setattr(agents_pkg, "agent_manager", stub)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@copilot apply changes based on this feedback

Comment on lines +77 to +90
try:
history = agent.get_message_history() or []
except Exception:
history = []

try:
used = sum(agent.estimate_tokens_for_message(m) for m in history)
except Exception:
used = 0

try:
overhead = agent._estimate_context_overhead()
except Exception:
overhead = 0
Comment on lines +114 to +120
f"{usage.indicator} Context usage: {usage.percent:.1f}%\n"
f" [{bar}]\n"
f" Messages : {usage.used_tokens:,} tokens\n"
f" Overhead : {usage.overhead_tokens:,} tokens (system prompt + tools)\n"
f" Total : {usage.total_tokens:,} / {usage.capacity:,} tokens\n"
f" Buckets : 🟢 <30% 🟡 30–60% 🔴 >60%"
)
Comment on lines +49 to +55
def pick_indicator(proportion: float) -> str:
"""Pick the colored-circle emoji for a given usage proportion (0..1)."""
if proportion < GREEN_THRESHOLD:
return GREEN_CIRCLE
if proportion < YELLOW_THRESHOLD:
return YELLOW_CIRCLE
return RED_CIRCLE
…ys.modules leakage

The previous helper permanently injected a MagicMock as
code_puppy.agents.agent_manager into sys.modules at import time. Other
tests that import the real agent_manager later in the session would pick
up the stub instead, leading to order-dependent failures.

Replace it with a 'stub_agent_manager' fixture using
monkeypatch.setitem(sys.modules, ...), which is automatically torn down
when the test ends. Tests that don't need to fake out get_current_agent
no longer carry the stub at all.

Per review feedback on PR mpfaffenberger#342.
@mpfaffenberger
Copy link
Copy Markdown
Owner

can you make sure linters / tests pass?

@mmcguff
Copy link
Copy Markdown
Contributor Author

mmcguff commented May 19, 2026

can you make sure linters / tests pass?

I’ll be working on this today. I got pulled into a few other priorities earlier, but I’ll let you know as soon as it’s ready.

- Align bucket text/docs with actual threshold logic (≥60% is red,
  not >60%). Comment + /context legend now read '🟢 <30%   🟡 30–<60%
  🔴 ≥60%' to match pick_indicator()'s sharp '<' comparisons.
- get_current_usage() now returns None on ANY history/estimator/
  capacity exception instead of silently falling back to zeros, which
  could surface a misleading 🟢 indicator. Hiding the badge is the
  honest behavior.
- Add tests covering the new strict-None paths for estimator and
  overhead failures.
@mmcguff
Copy link
Copy Markdown
Contributor Author

mmcguff commented May 19, 2026

Addressed Copilot's review in 0c19ac3:

  • Threshold/legend mismatch (pick_indicator + /context report): kept the threshold logic as-is (60% → 🔴 was the dev's intent per the existing test case), but updated the module comment and /context legend to 🟢 <30% 🟡 30–<60% 🔴 ≥60% so the docs match the sharp < comparisons.
  • get_current_usage() partial-failure honesty: removed the per-call try/except fallbacks. Any exception while estimating history/used/overhead/capacity now returns None so we hide the badge instead of surfacing a misleading 🟢. Docstring updated accordingly.
  • sys.modules leakage (already handled in 417bae8 via the scoped stub_agent_manager fixture).
  • Tests: added two cases covering the new strict-None paths (estimator raises / overhead raises). ruff check . clean, ruff format applied, 24/24 plugin tests pass; full suite green except 3 pre-existing failures on main unrelated to this PR (test_model_factory × 2, browser_workflows).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants