From eef6f338041ac40233773f1c5094362b9f938c5a Mon Sep 17 00:00:00 2001 From: Mouhand-Kaddo Date: Tue, 8 Sep 2026 08:25:05 +0400 Subject: [PATCH] feat: show reasoning-level override in the statusline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /thinking (and /reasoning) now surface the current reasoning level in the bottom bar right after the model — model: · High · cost: … — only while it differs from the startup baseline (config-effective). Switching back to the baseline hides the label. Display-only: provider requests are unchanged. --- src/lecode/slash/handlers.py | 4 ++-- src/lecode/tui/app.py | 10 +++++++++- src/lecode/tui/statusline.py | 5 +++++ tests/test_slash_settings.py | 22 ++++++++++++++++++++++ tests/test_tui_statusline.py | 12 ++++++++++++ 5 files changed, 50 insertions(+), 3 deletions(-) diff --git a/src/lecode/slash/handlers.py b/src/lecode/slash/handlers.py index d598a86..e48dd5d 100644 --- a/src/lecode/slash/handlers.py +++ b/src/lecode/slash/handlers.py @@ -10,7 +10,7 @@ from __future__ import annotations from pathlib import Path -from typing import TYPE_CHECKING, get_args +from typing import TYPE_CHECKING, cast, get_args from lecode.config.models import PermissionMode, ThinkingLevel from lecode.context.resources import load_text @@ -480,7 +480,7 @@ async def cmd_thinking(app: TuiApp, args: list[str]) -> None: if level not in levels: app.feed.error(f"unknown thinking level: {level} (one of: {', '.join(levels)})") return - app.config.llm.thinking = level + app.set_thinking(cast(ThinkingLevel, level)) app.feed.info(f"thinking: {level} (applies from the next turn)") diff --git a/src/lecode/tui/app.py b/src/lecode/tui/app.py index 362bd7c..8c432b1 100644 --- a/src/lecode/tui/app.py +++ b/src/lecode/tui/app.py @@ -51,7 +51,7 @@ ToolCall, ToolResult, ) -from lecode.config.models import PermissionMode +from lecode.config.models import PermissionMode, ThinkingLevel from lecode.context.agents import parse_mentions from lecode.context.resources import load_text from lecode.extras.chain import run_chain @@ -222,6 +222,8 @@ def __init__( cwd=self._cwd, context_window=config.agent.context_window, ) + #: Startup reasoning level; the statusline only labels deviations. + self._baseline_thinking = config.llm.thinking self._git = CachedGitInfo() #: Chars-per-token ratio, calibrated per model from real usage (EMA). self._char_per_token = 4.0 @@ -312,6 +314,12 @@ def refresh(self) -> None: """Re-render the statusline after state changes.""" self._invalidate() + def set_thinking(self, level: ThinkingLevel) -> None: + """Apply the reasoning level; the statusline labels it until back at baseline.""" + self._config.llm.thinking = level + self._status.reasoning = None if level == self._baseline_thinking else level.title() + self.refresh() + @property def catalog(self) -> Any: """The model catalog (lazy default; ``/models-add`` merges into it).""" diff --git a/src/lecode/tui/statusline.py b/src/lecode/tui/statusline.py index b958ca0..489a51a 100644 --- a/src/lecode/tui/statusline.py +++ b/src/lecode/tui/statusline.py @@ -60,6 +60,8 @@ class StatusState: model: str cwd: Path | str git: GitInfo | None = None + #: Reasoning-level override label ("High", "None", …); None at baseline. + reasoning: str | None = None context_used: int = 0 context_window: int = 200_000 input_tokens: int = 0 @@ -146,6 +148,9 @@ def labelled(line: Text, label: str, value: str, style: str, *, first: bool = Fa bar, pct = context_meter(state.context_used, state.context_window) line2 = Text() labelled(line2, "model", state.model, theme.text, first=True) + if state.reasoning is not None: + line2.append_text(sep.copy()) + line2.append(state.reasoning, style=theme.muted) labelled(line2, "cost", format_cost(state.cost_usd), theme.muted) labelled( line2, diff --git a/tests/test_slash_settings.py b/tests/test_slash_settings.py index 63cc301..7e3b180 100644 --- a/tests/test_slash_settings.py +++ b/tests/test_slash_settings.py @@ -101,6 +101,28 @@ async def test_thinking_passed_as_reasoning_effort(tmp_path, monkeypatch): assert provider.requests[-1]["kwargs"]["reasoning_effort"] is None +async def test_thinking_updates_statusline_reasoning(tmp_path, monkeypatch): + app, _, _ = make_app(tmp_path, monkeypatch, []) + assert app.status.reasoning is None # at startup baseline (medium) + await app.handle_command("/thinking high") + assert app.status.reasoning == "High" + await app.handle_command("/thinking none") + assert app.status.reasoning == "None" + await app.handle_command("/thinking medium") # back to baseline hides it + assert app.status.reasoning is None + + +async def test_thinking_baseline_comes_from_startup_config(tmp_path, monkeypatch): + config = Config() + config.llm.thinking = "high" + app, _, _ = make_app(tmp_path, monkeypatch, [], config=config) + assert app.status.reasoning is None # starting at its own baseline + await app.handle_command("/thinking medium") + assert app.status.reasoning == "Medium" + await app.handle_command("/thinking high") # return to baseline + assert app.status.reasoning is None + + # -- /permissions /mode /toggle --------------------------------------------------------- diff --git a/tests/test_tui_statusline.py b/tests/test_tui_statusline.py index 8bae599..7907b67 100644 --- a/tests/test_tui_statusline.py +++ b/tests/test_tui_statusline.py @@ -61,6 +61,18 @@ def test_line2_model_cost_context(state, theme): assert line2 == "model: openai/gpt-5-mini · cost: $0.0123 · ctx: ▓▓▓░░ 84.0k/200.0k 42%" +def test_line2_shows_reasoning_override_after_model(state, theme): + state.reasoning = "High" + line2 = render_statusline(state, theme, width=200).plain.splitlines()[1] + assert line2 == "model: openai/gpt-5-mini · High · cost: $0.0123 · ctx: ▓▓▓░░ 84.0k/200.0k 42%" + + +def test_line2_omits_reasoning_at_baseline(state, theme): + line2 = render_statusline(state, theme, width=200).plain.splitlines()[1] + assert "High" not in line2 + assert line2 == "model: openai/gpt-5-mini · cost: $0.0123 · ctx: ▓▓▓░░ 84.0k/200.0k 42%" + + def test_line3_session_agent_tokens_state(state, theme): line3 = render_statusline(state, theme, width=200).plain.splitlines()[2] assert line3 == "session: my-session · agent: default · in: 1.2k · out: 0.4k · ready"