diff --git a/dev-notes/context-usage-display-config-plan.md b/dev-notes/context-usage-display-config-plan.md new file mode 100644 index 000000000..755f05bf8 --- /dev/null +++ b/dev-notes/context-usage-display-config-plan.md @@ -0,0 +1,275 @@ +# Context usage display config plan + +## Goal + +Add a user-configurable context usage display for Tau's Textual TUI so users can choose whether context usage is shown as token counts, percentage, both, or hidden. + +Current compact TUI chrome shows token counts like: + +```text +12k/200k context +``` + +It does not show percentage usage. This plan keeps the feature inside `tau_coding` because context accounting and TUI presentation are coding-app behavior, not reusable agent-harness behavior. + +## Current code locations + +- `src/tau_coding/context_window.py` + - Rough token estimation and compaction thresholds. + - Important items: `ContextUsageEstimate`, `estimate_context_usage`, `estimate_context_tokens`, `auto_compaction_threshold_for_context_window`. + +- `src/tau_coding/session.py` + - `CodingSession` exposes active context information. + - Important properties: `context_usage`, `context_token_estimate`, `context_window_tokens`, `auto_compact_token_threshold`. + +- `src/tau_coding/tui/widgets.py` + - Renders compact session info and context usage text. + - Important function: `_context_usage(session)`. + +- `src/tau_coding/tui/config.py` + - Durable TUI settings stored in `~/.tau/tui.json`. + - Important items: `TuiSettings`, `tui_settings_from_json`, `save_tui_settings`, `load_tui_settings`. + +## Proposed setting + +Add a durable TUI setting named `context_usage_display`. + +Suggested type: + +```python +type ContextUsageDisplay = Literal["tokens", "percent", "both", "off"] +``` + +Suggested default: + +```python +context_usage_display: ContextUsageDisplay = "tokens" +``` + +The default preserves current behavior for existing users. + +## Display modes + +| Value | Compact TUI output | +| --- | --- | +| `tokens` | `12k/200k context` | +| `percent` | `6% context` | +| `both` | `12k/216k context · 6%` | +| `off` | hides context usage | + +Open design point: current token rendering sometimes uses `auto_compact_token_threshold` as the denominator. For percentage, prefer using the full model `context_window_tokens`, because users usually interpret percentage as total context window usage. + +## Implementation steps + +1. Add percentage helper in `src/tau_coding/context_window.py`. + + Example: + + ```python + def context_usage_percent(used_tokens: int, context_window_tokens: int) -> int: + if context_window_tokens <= 0: + raise ValueError("context_window_tokens must be positive") + return min(999, round((used_tokens / context_window_tokens) * 100)) + ``` + +2. Expose percentage from `CodingSession` in `src/tau_coding/session.py`. + + Example: + + ```python + @property + def context_usage_percent(self) -> int | None: + return context_usage_percent( + self.context_token_estimate, + self.context_window_tokens, + ) + ``` + +3. Add config support in `src/tau_coding/tui/config.py`. + + - Add `ContextUsageDisplay` type. + - Add `context_usage_display` field to `TuiSettings`. + - Include it in `TuiSettings.to_json()`. + - Allow and validate it in `tui_settings_from_json(...)`. + +4. Update TUI rendering in `src/tau_coding/tui/widgets.py`. + + - Extend `SessionSummarySource` with `context_usage_percent`. + - Pass the setting into `render_compact_session_info(...)`. + - Update `_context_usage(...)` to render based on `context_usage_display`. + +5. Update `src/tau_coding/tui/app.py`. + + - When refreshing chrome, pass `self.tui_settings.context_usage_display` into compact session info rendering. + - Keep sidebar behavior unchanged unless we intentionally decide to show context usage there too. + +6. Optional: update `/session` in `src/tau_coding/commands.py`. + + Since `/session` is not TUI chrome, it can always include the percentage as additional information, or it can stay token-only for this first change. + +## Tests to add or update + +- `tests/test_context_window.py` + - Percentage helper computes deterministic values. + - Invalid or zero context window raises `ValueError`. + +- `tests/test_tui_config.py` + - Default is `tokens`. + - `context_usage_display` round-trips through JSON. + - Invalid values raise `TuiConfigError`. + +- `tests/test_tui_app.py` + - Existing compact session info still renders token mode by default. + - Percent mode renders `6% context` for the fake session. + - Both mode renders tokens and percent. + - Off mode hides context usage. + +## Compatibility + +This should be backwards-compatible because: + +- the default remains token display; +- missing `context_usage_display` in existing `~/.tau/tui.json` files falls back to `tokens`; +- the reusable `tau_agent` package is untouched. + +## Suggested branch + +```bash +git checkout -b context-usage-display-config +``` + +Suggested focused checks: + +```bash +uv run pytest tests/test_context_window.py tests/test_tui_config.py tests/test_tui_app.py +uv run ruff check . +``` + +## Task breakdown + +### Task 1 — Add context percentage accounting + +**Goal:** Provide a deterministic percentage helper in the context accounting layer. + +Files: + +- `src/tau_coding/context_window.py` +- `tests/test_context_window.py` + +Acceptance criteria: + +- `context_usage_percent(12_034, 216_384)` returns `6`. +- `context_usage_percent(..., 0)` raises `ValueError`. +- The helper is exported from `tau_coding.__init__` if the surrounding context helpers are exported there. +- Focused test command passes: + + ```bash + uv run pytest tests/test_context_window.py + ``` + +### Task 2 — Expose percentage from `CodingSession` + +**Goal:** Make context usage percentage available to TUI renderers through the session object. + +Files: + +- `src/tau_coding/session.py` +- `src/tau_coding/commands.py` protocol only if needed later +- `src/tau_coding/tui/widgets.py` protocol only if needed later +- relevant fake sessions in tests + +Acceptance criteria: + +- `CodingSession.context_usage_percent` returns the helper result using `context_token_estimate` and `context_window_tokens`. +- The property is cache-friendly and reuses existing context usage invalidation indirectly through `context_token_estimate`. + +### Task 3 — Add durable TUI config setting + +**Goal:** Let users choose their preferred context display in `~/.tau/tui.json`. + +Files: + +- `src/tau_coding/tui/config.py` +- `tests/test_tui_config.py` + +Acceptance criteria: + +- `TuiSettings().context_usage_display == "tokens"`. +- `TuiSettings(context_usage_display="both").to_json()` includes `"context_usage_display": "both"`. +- `tui_settings_from_json({"context_usage_display": "percent"})` parses successfully. +- Invalid values like `"bar"`, `123`, or `""` raise `TuiConfigError` mentioning `context_usage_display`. +- Existing config files without the setting continue to parse. + +Focused test command: + +```bash +uv run pytest tests/test_tui_config.py +``` + +### Task 4 — Render configurable compact context usage + +**Goal:** Update compact TUI chrome rendering while preserving the existing default. + +Files: + +- `src/tau_coding/tui/widgets.py` +- `tests/test_tui_app.py` + +Acceptance criteria: + +- Default/token mode still renders `12k/200k context` for the current fake session. +- Percent mode renders `6% context`. +- Both mode renders token count plus percentage, e.g. `12k/200k context · 6%`. +- Off mode omits context usage but still renders provider/model/thinking details. +- Percentage is always available for a valid coding session because unknown context windows fall back to Tau's default. + +Focused test command: + +```bash +uv run pytest tests/test_tui_app.py -k "compact_session_info or context_usage" +``` + +### Task 5 — Wire TUI settings into app refresh + +**Goal:** Ensure the configured display mode is used in the live Textual app. + +Files: + +- `src/tau_coding/tui/app.py` +- `tests/test_tui_app.py` + +Acceptance criteria: + +- `_refresh_chrome(...)` passes `self.tui_settings.context_usage_display` to compact session info rendering. +- A TUI app test with `TuiSettings(context_usage_display="percent")` shows percent mode in `#compact-session-info` after refresh. + +### Task 6 — Optional `/session` command enhancement + +**Goal:** Decide whether slash-command status should include percentage independently of TUI chrome config. + +Files, if implemented: + +- `src/tau_coding/commands.py` +- `tests/test_commands.py` + +Recommended acceptance criteria: + +- `/session` includes a stable line like `Context usage: 123 / 584 tokens (21%)`. +- This command is not controlled by `context_usage_display`, because the setting is specifically TUI chrome presentation. + +### Task 7 — Documentation and final checks + +**Goal:** Document the user-facing setting and run focused quality checks. + +Files: + +- `website/src/content/docs/` appropriate TUI/config guide, if one exists +- this dev note if implementation choices change + +Checks: + +```bash +uv run pytest tests/test_context_window.py tests/test_tui_config.py tests/test_tui_app.py tests/test_commands.py +uv run ruff check . +uv run ruff format --check . +``` diff --git a/src/tau_coding/__init__.py b/src/tau_coding/__init__.py index 92d33585b..b82e431f1 100644 --- a/src/tau_coding/__init__.py +++ b/src/tau_coding/__init__.py @@ -19,6 +19,7 @@ SUMMARIZATION_SYSTEM_PROMPT, auto_compaction_threshold_for_context_window, build_compaction_summary_prompt, + context_usage_percent, estimate_context_tokens, estimate_message_tokens, estimate_text_tokens, @@ -250,6 +251,7 @@ "DEFAULT_COMPACTION_RESERVE_TOKENS", "DEFAULT_CONTEXT_WINDOW_TOKENS", "auto_compaction_threshold_for_context_window", + "context_usage_percent", "estimate_context_tokens", "estimate_message_tokens", "estimate_text_tokens", diff --git a/src/tau_coding/commands.py b/src/tau_coding/commands.py index c090a2458..ba797bfd0 100644 --- a/src/tau_coding/commands.py +++ b/src/tau_coding/commands.py @@ -62,6 +62,9 @@ def auto_compact_token_threshold(self) -> int | None: ... @property def context_window_tokens(self) -> int: ... + @property + def context_usage_percent(self) -> int: ... + @property def thinking_level(self) -> str: ... @@ -401,6 +404,7 @@ def _status_command(context: CommandContext) -> CommandResult: f"Context files: {len(session.context_files)}", f"Estimated context tokens: {session.context_token_estimate}", f"Context window: {session.context_window_tokens}", + _context_usage_status_line(session), ] context_window_source = getattr(session, "context_window_source", None) if context_window_source: @@ -426,6 +430,14 @@ def _status_command(context: CommandContext) -> CommandResult: return CommandResult(handled=True, message="\n".join(lines)) +def _context_usage_status_line(session: CommandSession) -> str: + return ( + "Context usage: " + f"{session.context_token_estimate} / {session.context_window_tokens} tokens " + f"({session.context_usage_percent}%)" + ) + + def _system_command(context: CommandContext) -> CommandResult: if context.args: return CommandResult(handled=True, message="Usage: /system") diff --git a/src/tau_coding/context_window.py b/src/tau_coding/context_window.py index 23d04b146..e4ddaa2d9 100644 --- a/src/tau_coding/context_window.py +++ b/src/tau_coding/context_window.py @@ -170,6 +170,13 @@ def auto_compaction_threshold_for_context_window(context_window_tokens: int) -> return max(1, context_window_tokens - DEFAULT_COMPACTION_RESERVE_TOKENS) +def context_usage_percent(used_tokens: int, context_window_tokens: int) -> int: + """Return whole-number context usage percent for a model context window.""" + if context_window_tokens <= 0: + raise ValueError("context_window_tokens must be positive") + return min(999, round((used_tokens / context_window_tokens) * 100)) + + def estimate_context_usage( *, system: str, diff --git a/src/tau_coding/session.py b/src/tau_coding/session.py index b7a4383d7..e5831b867 100644 --- a/src/tau_coding/session.py +++ b/src/tau_coding/session.py @@ -50,6 +50,7 @@ ContextUsageEstimate, auto_compaction_threshold_for_context_window, build_compaction_summary_prompt, + context_usage_percent, estimate_context_usage, estimate_message_tokens, summarize_messages_for_compaction, @@ -648,6 +649,11 @@ def context_usage(self) -> ContextUsageEstimate: ) return self._context_usage_cache + @property + def context_usage_percent(self) -> int: + """Return estimated active context usage as a whole-window percentage.""" + return context_usage_percent(self.context_token_estimate, self.context_window_tokens) + @property def system_prompt(self) -> str: """Return the effective system prompt sent to the model.""" diff --git a/src/tau_coding/tui/app.py b/src/tau_coding/tui/app.py index 902a52533..eb15ebf20 100644 --- a/src/tau_coding/tui/app.py +++ b/src/tau_coding/tui/app.py @@ -4948,7 +4948,11 @@ def _refresh_chrome(self, *, theme: TuiTheme | None = None) -> None: sidebar = self.query_one("#sidebar", SessionSidebar) sidebar.update_from_session(self.session, theme=theme) compact_info = self.query_one("#compact-session-info", CompactSessionInfo) - compact_info.update_from_session(self.session, theme=theme) + compact_info.update_from_session( + self.session, + theme=theme, + context_usage_display=self.tui_settings.context_usage_display, + ) queued_messages = self.query_one("#queued-messages", Static) queue_render_key = ( self.state.queued_steering, diff --git a/src/tau_coding/tui/config.py b/src/tau_coding/tui/config.py index 09ea7ecf7..a22acffbb 100644 --- a/src/tau_coding/tui/config.py +++ b/src/tau_coding/tui/config.py @@ -21,6 +21,7 @@ __all__ = [ "BUILTIN_TUI_THEME_NAMES", + "ContextUsageDisplay", "HIGH_CONTRAST_THEME", "TAU_DARK_THEME", "TAU_LIGHT_THEME", @@ -38,6 +39,9 @@ ] +type ContextUsageDisplay = Literal["tokens", "percent", "both", "off"] + + class TuiConfigError(ValueError): """Raised when Tau TUI configuration is invalid.""" @@ -87,11 +91,13 @@ class TuiSettings: theme: TuiThemeName = "tau-dark" auto_copy_selection: bool = False sidebar_position: Literal["left", "right", "off"] = "right" + context_usage_display: ContextUsageDisplay = "tokens" def to_json(self) -> dict[str, Any]: """Serialize these settings to JSON-compatible data.""" return { "auto_copy_selection": self.auto_copy_selection, + "context_usage_display": self.context_usage_display, "keybindings": self.keybindings.to_json(), "sidebar_position": self.sidebar_position, "theme": self.theme, @@ -132,7 +138,13 @@ def save_tui_settings(settings: TuiSettings, paths: TauPaths | None = None) -> P def tui_settings_from_json(data: dict[str, Any]) -> TuiSettings: """Parse TUI settings from JSON-compatible data.""" - allowed_fields = {"auto_copy_selection", "keybindings", "sidebar_position", "theme"} + allowed_fields = { + "auto_copy_selection", + "context_usage_display", + "keybindings", + "sidebar_position", + "theme", + } unknown_fields = set(data) - allowed_fields if unknown_fields: raise TuiConfigError(f"Unknown TUI settings field: {sorted(unknown_fields)[0]}") @@ -151,6 +163,7 @@ def tui_settings_from_json(data: dict[str, Any]) -> TuiSettings: "auto_copy_selection", ), sidebar_position=cast(Literal["left", "right", "off"], raw_sidebar), + context_usage_display=_context_usage_display(data.get("context_usage_display", "tokens")), ) @@ -160,6 +173,12 @@ def _bool_setting(value: object, field_name: str) -> bool: raise TuiConfigError(f"TUI setting must be a boolean: {field_name}") +def _context_usage_display(value: object) -> ContextUsageDisplay: + if isinstance(value, str) and value in {"tokens", "percent", "both", "off"}: + return cast(ContextUsageDisplay, value) + raise TuiConfigError("context_usage_display must be 'tokens', 'percent', 'both', or 'off'") + + def _keybindings_from_json(data: dict[str, Any]) -> TuiKeybindings: defaults = TuiKeybindings() allowed_fields = set(defaults.to_json()) diff --git a/src/tau_coding/tui/widgets.py b/src/tau_coding/tui/widgets.py index 9734897da..b9d265ce4 100644 --- a/src/tau_coding/tui/widgets.py +++ b/src/tau_coding/tui/widgets.py @@ -36,7 +36,12 @@ from tau_coding.skills import Skill from tau_coding.system_prompt import ProjectContextFile from tau_coding.tui.autocomplete import CompletionState -from tau_coding.tui.config import TAU_DARK_THEME, TuiRoleStyle, TuiTheme +from tau_coding.tui.config import ( + TAU_DARK_THEME, + ContextUsageDisplay, + TuiRoleStyle, + TuiTheme, +) from tau_coding.tui.state import ChatItem, TuiState from tau_coding.version import current_version @@ -83,6 +88,9 @@ def auto_compact_token_threshold(self) -> int | None: ... @property def context_window_tokens(self) -> int: ... + @property + def context_usage_percent(self) -> int: ... + @property def thinking_level(self) -> str: ... @@ -136,13 +144,23 @@ def update_from_session( session: SessionSummarySource, *, theme: TuiTheme = TAU_DARK_THEME, + context_usage_display: ContextUsageDisplay = "tokens", ) -> None: """Redraw compact session metadata only when its inputs changed.""" - fingerprint = _session_summary_fingerprint(session, theme=theme) + fingerprint = ( + *_session_summary_fingerprint(session, theme=theme), + context_usage_display, + ) if fingerprint == self._summary_fingerprint: return self._summary_fingerprint = fingerprint - self.update(render_compact_session_info(session, theme=theme)) + self.update( + render_compact_session_info( + session, + theme=theme, + context_usage_display=context_usage_display, + ) + ) def _session_summary_fingerprint( @@ -1571,6 +1589,7 @@ def render_compact_session_info( session: SessionSummarySource, *, theme: TuiTheme = TAU_DARK_THEME, + context_usage_display: ContextUsageDisplay = "tokens", ) -> RenderableType: """Render the session facts below the prompt.""" left = _styled_cwd(session.cwd, theme=theme) @@ -1579,8 +1598,10 @@ def render_compact_session_info( right.append(f":{session.model}", style=theme.prompt_text) right.append(" ") right.append(f"({_thinking_level(session)})", style=theme.completion_description) - right.append("\n") - right.append(_context_usage(session), style=theme.completion_description) + context_usage = _context_usage(session, display=context_usage_display) + if context_usage: + right.append("\n") + right.append(context_usage, style=theme.completion_description) table = Table.grid(expand=True) table.add_column(ratio=1) @@ -1987,7 +2008,23 @@ def _plain_text(text: str, *, body_style: str) -> Text: return Text(text, style=body_style, overflow="fold", no_wrap=False) -def _context_usage(session: SessionSummarySource) -> str: +def _context_usage(session: SessionSummarySource, *, display: ContextUsageDisplay) -> str: + if display == "off": + return "" + + tokens = _context_usage_tokens(session) + percent_text = f"{session.context_usage_percent}%" + + if display == "tokens": + return tokens + if display == "percent": + return percent_text + if display == "both": + return f"{tokens} · {percent_text}" + return tokens + + +def _context_usage_tokens(session: SessionSummarySource) -> str: threshold = session.auto_compact_token_threshold limit = session.context_window_tokens if threshold is None or threshold <= 0 else threshold return f"{_compact_token_count(session.context_token_estimate)}/{_compact_token_count(limit)}" diff --git a/tests/test_coding_session.py b/tests/test_coding_session.py index 3cdb08081..1252e0f7d 100644 --- a/tests/test_coding_session.py +++ b/tests/test_coding_session.py @@ -32,6 +32,7 @@ from tau_ai import CancellationToken, FakeProvider, ModelProvider, RuntimeModelLimits from tau_ai.events import AssistantMessageEvent from tau_coding import ( + DEFAULT_CONTEXT_WINDOW_TOKENS, CodingSession, CodingSessionConfig, FileCredentialStore, @@ -802,6 +803,23 @@ async def test_tree_branching_preserves_active_model(tmp_path: Path) -> None: ) +@pytest.mark.anyio +async def test_context_usage_percent_uses_active_context_window(tmp_path: Path) -> None: + storage = JsonlSessionStorage(tmp_path / "session.jsonl") + session = await CodingSession.load( + CodingSessionConfig( + provider=FakeProvider([]), + model="fake", + system="x" * 30_720, + storage=storage, + cwd=tmp_path, + ) + ) + + assert session.context_window_tokens == DEFAULT_CONTEXT_WINDOW_TOKENS + assert session.context_usage_percent == 6 + + @pytest.mark.anyio async def test_context_usage_is_cached_until_session_context_changes( monkeypatch: pytest.MonkeyPatch, tmp_path: Path diff --git a/tests/test_commands.py b/tests/test_commands.py index 9baa8b541..d94e84418 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -38,6 +38,7 @@ def __init__(self, tmp_path: Path, manager: SessionManager | None = None) -> Non self.context_token_estimate = 123 self.auto_compact_token_threshold = 200 self.context_window_tokens = 584 + self.context_usage_percent = 21 self.thinking_level = "medium" self.available_thinking_levels = ("off", "minimal", "low", "medium", "high", "xhigh") self.thinking_unavailable_reason: str | None = None @@ -214,6 +215,7 @@ def test_session_command_includes_session_details(tmp_path: Path) -> None: assert "Context files: 1" in result.message assert "Estimated context tokens: 123" in result.message assert "Context window: 584" in result.message + assert "Context usage: 123 / 584 tokens (21%)" in result.message assert "Thinking mode: medium" in result.message assert "Auto compact threshold: 200" in result.message assert "Resource diagnostics: 0" in result.message diff --git a/tests/test_context_window.py b/tests/test_context_window.py index e48d00024..d2101b0e5 100644 --- a/tests/test_context_window.py +++ b/tests/test_context_window.py @@ -1,11 +1,15 @@ from pathlib import Path +import pytest + +import tau_coding from tau_agent import AssistantMessage, TextContent, ToolCall, ToolResultMessage, UserMessage from tau_agent.messages import assistant_content from tau_coding.context_window import ( ContextUsageEstimate, auto_compaction_threshold_for_context_window, build_compaction_summary_prompt, + context_usage_percent, estimate_context_tokens, estimate_context_usage, estimate_message_tokens, @@ -59,6 +63,24 @@ def test_auto_compaction_threshold_keeps_pi_style_reserve() -> None: assert auto_compaction_threshold_for_context_window(0) is None +def test_context_usage_percent_reports_whole_model_window_percentage() -> None: + assert context_usage_percent(12_034, 216_384) == 6 + assert context_usage_percent(50, 200) == 25 + assert context_usage_percent(1, 3) == 33 + assert context_usage_percent(0, 200) == 0 + assert context_usage_percent(500_000, 100_000) == 500 + assert context_usage_percent(1_000_000, 1) == 999 + assert tau_coding.context_usage_percent(12_034, 216_384) == 6 + + +@pytest.mark.parametrize("context_window_tokens", [0, -1]) +def test_context_usage_percent_rejects_non_positive_context_windows( + context_window_tokens: int, +) -> None: + with pytest.raises(ValueError, match="context_window_tokens must be positive"): + context_usage_percent(12_034, context_window_tokens) + + def test_context_usage_estimate_reports_breakdown(tmp_path: Path) -> None: tools = tuple(create_coding_tools(cwd=tmp_path)) messages = (UserMessage(content="hello"), AssistantMessage(content="hi")) diff --git a/tests/test_tui_app.py b/tests/test_tui_app.py index 0d5f7b606..e4a28b592 100644 --- a/tests/test_tui_app.py +++ b/tests/test_tui_app.py @@ -108,6 +108,7 @@ from tau_coding.tui.widgets import ( TRANSCRIPT_WINDOW_ITEMS, TRANSCRIPT_WINDOW_OVERSCAN_ITEMS, + CompactSessionInfo, LeftAlignedMarkdownHeading, StreamingTranscriptMessageWidget, TauMarkdownBlock, @@ -174,6 +175,7 @@ def __init__(self, messages=(), events=()) -> None: self.context_token_estimate = 12034 self.auto_compact_token_threshold = 200000 self.context_window_tokens = 216384 + self.context_usage_percent = 6 self.thinking_level = "medium" self.available_thinking_levels = ("off", "minimal", "low", "medium", "high", "xhigh") self.state = FakeSessionState() @@ -596,6 +598,39 @@ def test_compact_session_info_styles_parent_path_as_metadata() -> None: assert str(cwd.spans[2].style) == TAU_DARK_THEME.completion_description +def test_compact_session_info_can_render_context_usage_percent() -> None: + console = Console(record=True, width=120) + + console.print(render_compact_session_info(FakeSession(), context_usage_display="percent")) + + output = console.export_text() + assert "6%" in output + assert "12k/200k context" not in output + assert "openai:fake-model" in output + + +def test_compact_session_info_can_render_context_usage_tokens_and_percent() -> None: + console = Console(record=True, width=120) + + console.print(render_compact_session_info(FakeSession(), context_usage_display="both")) + + output = console.export_text() + assert "12k/200k · 6%" in output + assert "openai:fake-model" in output + + +def test_compact_session_info_can_hide_context_usage() -> None: + console = Console(record=True, width=120) + + console.print(render_compact_session_info(FakeSession(), context_usage_display="off")) + + output = console.export_text() + assert "12k/200k context" not in output + assert "6% context" not in output + assert "openai:fake-model" in output + assert "(medium)" in output + + def test_compact_token_count_uses_thousands_suffix() -> None: assert _compact_token_count(0) == "0k" assert _compact_token_count(499) == "<1k" @@ -2413,6 +2448,45 @@ async def test_tui_sidebar_is_visible_on_medium_windows() -> None: assert not app.has_class("-hide-sidebar") +@pytest.mark.anyio +async def test_tui_app_uses_configured_context_usage_display( + monkeypatch: pytest.MonkeyPatch, +) -> None: + captured_displays: list[str] = [] + + def capture_update_from_session( + self: CompactSessionInfo, + session: object, + *, + theme: object = None, + context_usage_display: str = "tokens", + ) -> None: + del self, session, theme + captured_displays.append(context_usage_display) + + monkeypatch.setattr(CompactSessionInfo, "update_from_session", capture_update_from_session) + app = TauTuiApp(FakeSession(), tui_settings=TuiSettings(context_usage_display="percent")) + + async with app.run_test(size=(120, 30)): + app._refresh_chrome() + + assert captured_displays + assert all(display == "percent" for display in captured_displays) + + +def test_compact_session_info_redraws_when_context_usage_display_changes() -> None: + widget = CompactSessionInfo() + session = FakeSession() + renders: list[object] = [] + widget.update = renders.append # type: ignore[method-assign] + + widget.update_from_session(session, context_usage_display="tokens") + widget.update_from_session(session, context_usage_display="tokens") + widget.update_from_session(session, context_usage_display="percent") + + assert len(renders) == 2 + + @pytest.mark.anyio async def test_tui_sidebar_fills_workspace_height() -> None: app = TauTuiApp(FakeSession()) diff --git a/tests/test_tui_config.py b/tests/test_tui_config.py index e434621ae..6f26d27e3 100644 --- a/tests/test_tui_config.py +++ b/tests/test_tui_config.py @@ -180,6 +180,23 @@ def test_tui_sidebar_position_defaults_to_right() -> None: assert tui_settings_from_json({}).sidebar_position == "right" +def test_tui_context_usage_display_defaults_to_tokens() -> None: + assert TuiSettings().context_usage_display == "tokens" + + +def test_tui_context_usage_display_roundtrips() -> None: + for value in ("tokens", "percent", "both", "off"): + settings = tui_settings_from_json({"context_usage_display": value}) + assert settings.context_usage_display == value + assert settings.to_json()["context_usage_display"] == value + + +def test_tui_context_usage_display_rejects_invalid() -> None: + for value in ("bar", "", 123): + with pytest.raises(TuiConfigError, match="context_usage_display"): + tui_settings_from_json({"context_usage_display": value}) + + def test_tui_sidebar_position_roundtrips() -> None: for value in ("left", "right", "off"): settings = tui_settings_from_json({"sidebar_position": value}) diff --git a/website/content/reference/configuration.md b/website/content/reference/configuration.md index fb5e0f548..0f4d1b5db 100644 --- a/website/content/reference/configuration.md +++ b/website/content/reference/configuration.md @@ -224,6 +224,7 @@ The built-in frontend reads optional settings from `~/.tau/tui.json`: { "theme": "high-contrast", "sidebar_position": "right", + "context_usage_display": "both", "keybindings": { "cancel": "escape", "command_palette": "ctrl+k", @@ -254,6 +255,10 @@ keybinding names, empty keys, and duplicate assignments. - `sidebar_position`: `"right"` (default), `"left"`, or `"off"`. Controls placement of the session metadata sidebar. `"off"` hides the sidebar entirely; the compact session info row below the prompt still works. +- `context_usage_display`: `"tokens"` (default), `"percent"`, `"both"`, or + `"off"`. Controls the compact context meter below the prompt. Token mode uses + Tau's rough token estimate and compaction threshold; percent mode uses the + active model's full context window. Full list in [Keyboard shortcuts]({{< relref "./keybindings.md" >}}). @@ -278,6 +283,7 @@ Resource discovery order (later overrides earlier) is documented in ## Context -`/session` reports a rough context estimate and breakdown. Auto-compaction -triggers near the model's context window minus a reserve; override per run with +`/session` reports rough context usage as tokens and as a percentage of the +active model's context window, plus a token breakdown. Auto-compaction triggers +near the model's context window minus a reserve; override per run with `--auto-compact-threshold`. Details in [Managing context]({{< relref "../guides/context.md" >}}).