Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/lecode/slash/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)")


Expand Down
10 changes: 9 additions & 1 deletion src/lecode/tui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)."""
Expand Down
5 changes: 5 additions & 0 deletions src/lecode/tui/statusline.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
22 changes: 22 additions & 0 deletions tests/test_slash_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ---------------------------------------------------------


Expand Down
12 changes: 12 additions & 0 deletions tests/test_tui_statusline.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading