Summary
In the normal shared-server/remote TUI path, Shift+Tab can repeatedly reselect the current model instead of cycling through saved favorites.
This is distinct from #767 / #783. That fix makes the cycle order stable after favorites are recognized. In this case, the initial remote picker contains remote-catalog placeholder routes, while favorites are persisted with their full provider and API-method identities, so no picker row is recognized as a favorite.
The hotkey path then confirms the still-selected current row unconditionally, producing repeated messages such as:
✓ Switched to model: deepseek-v4-flash [x7]
Environment
- Jcode: v0.68.0 (
fcf53909f)
- OS: Windows 11, x86_64
- Terminal: Windows Terminal
- Runtime path: shared-server / remote TUI (
remote=true)
The key itself is received correctly as KeyCode::BackTab; this is not a Windows Terminal keybinding problem.
Reproduction
- Save several model favorites whose identities include real routes, for example:
gpt-5.6-sol / OpenAI / openai-oauth / medium
gpt-5.6-terra / OpenAI / openai-oauth / high
deepseek-v4-pro / DeepSeek / openai-compatible:deepseek / high
- Start a normal Jcode shared-server session.
- Before or while the remote model picker has only simplified/placeholder routes, press Shift+Tab repeatedly.
Expected
Each press selects the next saved favorite in the stable order introduced by #783.
Actual
Each press selects the current model again. The confirmation messages collapse into [xN].
Confirming log sequence
Privacy-sensitive paths and session IDs removed:
EVENT event=model_picker_open current_model=deepseek-v4-flash current_provider=OpenRouter entries=430 routes_in=430 simplified=true remote=true
EVENT event=model_picker_select api_method=remote-catalog effort=none entry=deepseek-v4-flash provider=OpenRouter remote=true
EVENT event=server_model_changed model=deepseek-v4-flash provider=DeepSeek
EVENT event=model_picker_open current_model=deepseek-v4-flash current_provider=DeepSeek entries=430 routes_in=430 simplified=true remote=true
EVENT event=model_picker_select api_method=remote-catalog effort=none entry=deepseek-v4-flash provider=DeepSeek remote=true
EVENT event=server_model_changed model=deepseek-v4-flash provider=OpenRouter
The sequence repeats on every Shift+Tab press. In this reproduction it also alternates the provider label while keeping the same model.
Root cause
crates/jcode-tui/src/tui/app/inline_interactive.rs:
model_picker_is_favorite() identifies a favorite using the full key:
model + provider + api_method + effort
- The simplified remote picker initially exposes placeholder rows using
api_method=remote-catalog.
- Persisted favorites use real methods such as
openai-oauth, openrouter, or openai-compatible:deepseek.
- Therefore
next_model_favorite_after_current() finds no entry.is_favorite row.
cycle_model_favorite_after_current() reports that no favorite was found, but returns ().
cycle_model_favorite_hotkey() then executes Enter unconditionally:
self.cycle_model_favorite_after_current();
let _ = self.handle_inline_interactive_key(KeyCode::Enter, KeyModifiers::NONE);
Because the current placeholder row remains selected, Enter switches to the current model again.
Suggested fix
Two parts:
- Safety fix: make
cycle_model_favorite_after_current() return whether it selected a favorite, and only send Enter when it did. This prevents silently reselecting the current model.
- Functional fix: for closed-picker Shift+Tab in remote mode, resolve/cycle persisted favorite identities using hydrated real routes, rather than the temporary
remote-catalog placeholder rows. The existing remote catalog cache may already provide the needed route identities.
The first part is only a guard. Without the second part, Shift+Tab would safely do nothing until route hydration completes instead of actually cycling.
Regression coverage
Suggested focused tests:
- A remote simplified picker containing only
remote-catalog placeholders plus persisted full-route favorites must not confirm the current row.
- Once real routes are available, repeated closed-picker Shift+Tab presses must visit every persisted favorite.
- The test should verify that
pending_model_switch is not set when no matching favorite was selected.
Relationship to #767
#767 fixed a different failure mode where recognized favorites became trapped in a two-model loop because picker ordering changed on every press. v0.68.0 already contains that fix. This report covers the earlier stage where remote placeholder rows prevent the favorites from being recognized at all.
Summary
In the normal shared-server/remote TUI path, Shift+Tab can repeatedly reselect the current model instead of cycling through saved favorites.
This is distinct from #767 / #783. That fix makes the cycle order stable after favorites are recognized. In this case, the initial remote picker contains
remote-catalogplaceholder routes, while favorites are persisted with their full provider and API-method identities, so no picker row is recognized as a favorite.The hotkey path then confirms the still-selected current row unconditionally, producing repeated messages such as:
Environment
fcf53909f)remote=true)The key itself is received correctly as
KeyCode::BackTab; this is not a Windows Terminal keybinding problem.Reproduction
gpt-5.6-sol / OpenAI / openai-oauth / mediumgpt-5.6-terra / OpenAI / openai-oauth / highdeepseek-v4-pro / DeepSeek / openai-compatible:deepseek / highExpected
Each press selects the next saved favorite in the stable order introduced by #783.
Actual
Each press selects the current model again. The confirmation messages collapse into
[xN].Confirming log sequence
Privacy-sensitive paths and session IDs removed:
The sequence repeats on every Shift+Tab press. In this reproduction it also alternates the provider label while keeping the same model.
Root cause
crates/jcode-tui/src/tui/app/inline_interactive.rs:model_picker_is_favorite()identifies a favorite using the full key:api_method=remote-catalog.openai-oauth,openrouter, oropenai-compatible:deepseek.next_model_favorite_after_current()finds noentry.is_favoriterow.cycle_model_favorite_after_current()reports that no favorite was found, but returns().cycle_model_favorite_hotkey()then executes Enter unconditionally:Because the current placeholder row remains selected, Enter switches to the current model again.
Suggested fix
Two parts:
cycle_model_favorite_after_current()return whether it selected a favorite, and only send Enter when it did. This prevents silently reselecting the current model.remote-catalogplaceholder rows. The existing remote catalog cache may already provide the needed route identities.The first part is only a guard. Without the second part, Shift+Tab would safely do nothing until route hydration completes instead of actually cycling.
Regression coverage
Suggested focused tests:
remote-catalogplaceholders plus persisted full-route favorites must not confirm the current row.pending_model_switchis not set when no matching favorite was selected.Relationship to #767
#767 fixed a different failure mode where recognized favorites became trapped in a two-model loop because picker ordering changed on every press. v0.68.0 already contains that fix. This report covers the earlier stage where remote placeholder rows prevent the favorites from being recognized at all.