Skip to content

feat: auto-mode UX — per-mode spinner, round counter, first-time warning (#97)#105

Merged
VforVitorio merged 4 commits intodevfrom
feat/auto-mode-ux
Apr 7, 2026
Merged

feat: auto-mode UX — per-mode spinner, round counter, first-time warning (#97)#105
VforVitorio merged 4 commits intodevfrom
feat/auto-mode-ux

Conversation

@VforVitorio
Copy link
Copy Markdown
Owner

Summary

Builds on #104's max_rounds safety core with the UX layer that makes the new boundary visible and teachable. This is PR 2 of 2 for #97.

  • Per-mode spinner colour — the in-turn spinner tracks the active permission mode (orange=ask, blue=auto, red=strict). New mode_color(mode) accessor in ui/status.py so agent/core.py doesn't reach into the private _MODE_COLORS dict. Applies to both the initial thinking. spinner and the live-updating _keepalive spinner.
  • Auto-mode round counter — the spinner label appends · round N/M in auto mode, driven by a new on_round_start callback wired into model.act(). Shown only in auto mode because ask blocks on user approval between tool calls (redraws are rare) and strict has no round concept. Gives real-time visibility into how much of the max_rounds budget the current turn has consumed.
  • First-time auto warning — first Tab-cycle into auto in a session prints a one-shot amber hint above the prompt via prompt_toolkit's run_in_terminal, so it lands cleanly without tearing the ghost-text completion layer. Session-scoped via self._auto_warned — never re-prints after cycling away and back. Format: auto mode — tools run without asking, up to N rounds per turn. Ctrl+C stops a running turn.
  • /status surfaces max rounds — new row in the session-state table so users can verify which safety boundary is currently in effect (config / env var / --max-rounds CLI flag all funnel into the same value).

Stacked on #104

This PR targets feat/auto-max-rounds (not dev) so the diff shown here is only the UX layer, not PR 1's safety core. Merge #104 into dev first, then re-target this PR to dev (GitHub will auto-rebase the base).

PR 1 (#104) delivers the actual safety guarantee; this PR turns it into something the user can see and learn from.

Implementation notes

  • max_prediction_rounds is now computed before asyncio.create_task(_keepalive()) so the closure sees a bound cell when it first runs. Previously it was defined after the task was created — worked by accident because the first await happened after assignment, but fragile.
  • The first-time warning uses run_in_terminal rather than a direct console.print because the Tab cycle fires from inside prompt_toolkit's keybinding loop. A direct print would tear the prompt; run_in_terminal temporarily pauses the prompt, writes above it, and restores.
  • _print_auto_warning() is a method on Agent rather than a closure so it's unit-testable without prompt_toolkit terminal plumbing.

Tests

  • test_run_turn_passes_on_round_start_to_act — verifies the on_round_start kwarg is wired as a callable and accepts a 0-based round index.
  • test_agent_auto_warned_initially_false — pins the fresh-agent default.
  • test_print_auto_warning_fires_once_per_session — calls the method twice, asserts console.print fires exactly once.
  • test_cycle_mode_preserves_always_allowed_tools — regression guard: a future refactor that adds _always_allowed_tools.clear() to the mode handler will be caught.
  • test_print_status_includes_max_rounds_line — asserts /status output contains the new row.

Test plan

  • uv run ruff check . && uv run ruff format --check . && uv run mypy src/ && uv run pytest — all green (213 passed)
  • Manual: uv run lmcode chat, Tab to auto, verify amber warning prints above the prompt once and never again
  • Manual: Tab back to ask, spinner becomes orange; Tab to auto, spinner becomes blue; Tab to strict, spinner becomes red
  • Manual: in auto mode, send a multi-step request and verify the spinner shows · round 2/10, · round 3/10, …
  • Manual: /status shows max rounds 10 (or whatever LMCODE_AGENT__MAX_ROUNDS / --max-rounds sets)

Closes #97 together with #104.

VforVitorio and others added 2 commits April 7, 2026 12:38
- AgentSettings.max_rounds default 50->10 with docstring explaining
  the trade-off and the 3 ways to override (config, env var, CLI flag)
- _run_turn passes max_prediction_rounds= to model.act() in the
  non-strict branch; strict stays on respond() which has no round
  concept
- Catch LMStudioPredictionError specifically when the message contains
  "final prediction round" and set self._last_turn_limit_reached. This
  prevents the run() top-level LMStudioServerError handler (parent
  class) from misreading the crash as an LM Studio disconnect
- Post-act check: ActResult.rounds >= cap also sets the flag (the
  well-behaved model case where the final round produced a text
  answer instead of a tool_call)
- run() prints an inline amber warning when the flag is set, matching
  the ctx window warning style
- CLI --max-rounds flag is now actually wired: mutates
  get_settings().agent.max_rounds for the session. Was previously
  accepted by Typer but silently ignored
- 5 regression tests + _make_mock_model fixed to return an
  ActResult-shaped mock with concrete rounds int (MagicMock >= int
  raises TypeError)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ing (#97)

Builds on PR 1's max_rounds safety core with the UX layer that makes the
new boundary visible and teachable.

- Per-mode spinner colour: the in-turn spinner tracks the active mode
  (orange=ask, blue=auto, red=strict) via a new mode_color() accessor
  in ui/status.py so the current mode is visible at a glance without
  reading the prompt line.
- Auto-mode round counter: in auto mode the spinner label appends
  " · round N/M" driven by an on_round_start callback wired into
  model.act(). Only shown in auto mode (ask blocks on user approval
  between tool calls, strict has no round concept).
- First-time auto warning: the first Tab-cycle into auto in a session
  prints an amber one-liner above the prompt via run_in_terminal,
  gated by self._auto_warned so it never repeats.
- /status surfaces the active max_rounds so users can verify which
  safety boundary is in effect after config / env / --max-rounds.

Tests cover: on_round_start kwarg wiring, _auto_warned initial state
and one-shot behaviour, mode-cycle preservation of
_always_allowed_tools (regression guard), /status max-rounds row.

Stacked on feat/auto-max-rounds — merge PR #104 first.
Base automatically changed from feat/auto-max-rounds to dev April 7, 2026 10:58
VforVitorio and others added 2 commits April 7, 2026 13:18
When dev (containing the squash-merged #104) was merged into this
branch, git's three-way merge dropped three pieces of PR 2 work
because it saw them as conflicts with the squashed version of #104:

- self._auto_warned init flag was removed from Agent.__init__,
  causing mypy `has-type` error in _print_auto_warning
- on_round_start=_on_round_start kwarg was dropped from the
  model.act() call, breaking the round counter test
- max_prediction_rounds was re-added in its original post-keepalive
  position, duplicating the moved-up declaration and causing the
  mypy `no-redef` error at line 1267

Also restores the 5 PR 2 tests and the 4 PR 2 CHANGELOG entries
that the merge silently dropped. Local ruff + mypy + pytest green.
@VforVitorio VforVitorio merged commit 4387569 into dev Apr 7, 2026
6 checks passed
@VforVitorio VforVitorio deleted the feat/auto-mode-ux branch April 7, 2026 11:41
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.

1 participant