Skip to content

fix(tui): add force-switch mode hotkey bypassing the mid-session lock - #1881

Merged
wqymi merged 2 commits into
mainfrom
fix/tui-agent-switch-orchestrator
Jul 27, 2026
Merged

fix(tui): add force-switch mode hotkey bypassing the mid-session lock#1881
wqymi merged 2 commits into
mainfrom
fix/tui-agent-switch-orchestrator

Conversation

@wqymi

@wqymi wqymi commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Problem

In the TUI's agent/mode picker (Select agent), the mid-session lock restricts switching to FREE_SWITCH_GROUP = ["build", "plan"] once a session has messages. There was no way to deliberately switch into any other primary mode mid-session, even when the user knows what they're doing.

Root cause

  • The agent picker DialogAgent (component/dialog-agent.tsx) selects via local.agent.userSwitch(name).
  • userSwitch is gated by canSwitchTo() in context/local.tsx: once sessionHasMessages is true, switching is restricted to FREE_SWITCH_GROUP = ["build", "plan"]. Any other primary mode hits the lock and shows a "cannot switch mode mid-session" toast.
  • The switch mechanism itself is sound: setting agentStore.current triggers the mode-entry effect in app.tsx (dir-switch + sync.bootstrap() for Orchestrator). Only the lock was blocking a deliberate switch.

Fix

  • context/local.tsx: add agent.forceSwitch(name) that bypasses canSwitchTo and calls set() directly (which still validates the name against the available agents). The mode-entry effect keys off agentStore.current, so the switch fires exactly like a fresh entry.
  • component/dialog-agent.tsx: add a force? prop — in force mode the picker uses forceSwitch and shows a distinct, generic title + hint ("Force switch mode" / "Bypasses the mid-session lock — switch to any available mode"). The normal picker still uses userSwitch (lock respected).
  • app.tsx: register an agent.force command (slash /force-agent) bound to the new keybind, opening <DialogAgent force />.
  • config/keybinds.ts: add agent_force = <leader>o (previously unused), described generically as "Force switch mode (bypass mid-session lock)".
  • i18n titles (en / zh / zht) use generic "Force switch mode (bypass mid-session lock)" phrasing — no mode is singled out.

This is a generic force-switch: it bypasses the mid-session lock to switch to any available mode. It does not special-case or name any particular mode.

Orchestrator visibility is flag-gated (unchanged)

Orchestrator is one of several modes and is registered behind Flag.MIMOCODE_EXPERIMENTAL_ORCHESTRATOR in agent/agent.ts. When the flag is OFF, orchestrator is not added to the agent set → absent from Agent.list() → absent from sync.data.agent → therefore absent from the picker, the Tab-cycle, and the new force-switch (which validates against the available agents). When the flag is ON, it appears and is selectable. Force-switch does not expose it when the flag is off.

Verify

  • bun typecheck → 12/12 successful (EXIT 0).
  • bun test test/agent/orchestrator.test.ts test/agent/agent.test.ts test/keybind.test.ts → all pass.
  • Added a subprocess-based regression test (test/agent/orchestrator.test.ts + test/agent/fixtures/list-agents-probe.ts): Orchestrator is absent from Agent.list() when MIMOCODE_EXPERIMENTAL_ORCHESTRATOR is OFF and present when ON. (A subprocess is required because test/preload.ts force-enables the flag for the suite and Flag is read once at import.)
  • Live TUI: in a session with messages, <leader>o (or /force-agent) opens the force picker and switches to the selected available mode; normal Tab-cycle and <leader>a picker remain locked to build/plan mid-session as before.

The agent picker (dialog-agent.tsx) switches via local.agent.userSwitch(),
gated by canSwitchTo() in context/local.tsx: once a session has messages,
switching is restricted to FREE_SWITCH_GROUP=[build,plan]. Any other primary
mode is silently blocked mid-session, so users can no longer switch into it.

Add agent.forceSwitch(name) that bypasses the lock and calls set() directly.
Expose it via a new keybind agent_force (<leader>o, previously unused) and an
agent.force command that opens DialogAgent in force mode; normal Tab-cycle and
the standard picker still respect the lock.

The hotkey is a generic 'force switch mode' — its name/description and i18n
titles no longer single out Orchestrator. Orchestrator remains one of several
modes and stays flag-gated: its registration in agent/agent.ts is behind
Flag.MIMOCODE_EXPERIMENTAL_ORCHESTRATOR, so when the flag is OFF it is absent
from Agent.list() and therefore from the picker, Tab-cycle, and force-switch.
Add a subprocess-based regression test asserting Orchestrator is absent when the
flag is OFF and present when ON.
@wqymi
wqymi force-pushed the fix/tui-agent-switch-orchestrator branch from 4d3544c to a75b941 Compare July 24, 2026 10:27
@wqymi wqymi changed the title fix(tui): add force-switch hotkey for agent mode + make Orchestrator reachable fix(tui): add force-switch mode hotkey bypassing the mid-session lock Jul 24, 2026
…gate test

Review follow-ups on the force-switch mode change:

- dialog-agent.tsx hardcoded the force-mode title and hint as English literals,
  so non-English users saw untranslated text while the command palette entry
  (tui.command.agent.force.title) was already localized. Route both through
  useLanguage().t() with new tui.dialog.agent.force.{title,hint} keys added to
  en/zh/zht. The pre-existing "Select agent" literal is left untouched.
- orchestrator.test.ts leaked one mkdtempSync directory per listAgentNames()
  call; afterEach only disposed Instances. Wrap the body in try/finally and
  rmSync the root.
- Drop the over-long forceSwitch comment in context/local.tsx and the redundant
  inline comment in dialog-agent.tsx's onSelect.

No behavior change: the lock bypass, the orchestrator flag gating, the
agent_force keybind, and the /force-agent slash command are unchanged.
@wqymi
wqymi merged commit a91a01a into main Jul 27, 2026
6 checks passed
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