fix(tui): add force-switch mode hotkey bypassing the mid-session lock - #1881
Merged
Conversation
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
force-pushed
the
fix/tui-agent-switch-orchestrator
branch
from
July 24, 2026 10:27
4d3544c to
a75b941
Compare
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
In the TUI's agent/mode picker (
Select agent), the mid-session lock restricts switching toFREE_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
DialogAgent(component/dialog-agent.tsx) selects vialocal.agent.userSwitch(name).userSwitchis gated bycanSwitchTo()incontext/local.tsx: oncesessionHasMessagesis true, switching is restricted toFREE_SWITCH_GROUP = ["build", "plan"]. Any other primary mode hits the lock and shows a "cannot switch mode mid-session" toast.agentStore.currenttriggers the mode-entry effect inapp.tsx(dir-switch +sync.bootstrap()for Orchestrator). Only the lock was blocking a deliberate switch.Fix
context/local.tsx: addagent.forceSwitch(name)that bypassescanSwitchToand callsset()directly (which still validates the name against the available agents). The mode-entry effect keys offagentStore.current, so the switch fires exactly like a fresh entry.component/dialog-agent.tsx: add aforce?prop — in force mode the picker usesforceSwitchand shows a distinct, generic title + hint ("Force switch mode" / "Bypasses the mid-session lock — switch to any available mode"). The normal picker still usesuserSwitch(lock respected).app.tsx: register anagent.forcecommand (slash/force-agent) bound to the new keybind, opening<DialogAgent force />.config/keybinds.ts: addagent_force=<leader>o(previously unused), described generically as "Force switch mode (bypass mid-session lock)".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_ORCHESTRATORinagent/agent.ts. When the flag is OFF, orchestrator is not added to the agent set → absent fromAgent.list()→ absent fromsync.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.test/agent/orchestrator.test.ts+test/agent/fixtures/list-agents-probe.ts): Orchestrator is absent fromAgent.list()whenMIMOCODE_EXPERIMENTAL_ORCHESTRATORis OFF and present when ON. (A subprocess is required becausetest/preload.tsforce-enables the flag for the suite andFlagis read once at import.)<leader>o(or/force-agent) opens the force picker and switches to the selected available mode; normal Tab-cycle and<leader>apicker remain locked to build/plan mid-session as before.