Skip to content

Shift+Tab reselects the current model forever when remote picker uses placeholder routes #807

Description

@rensburger

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

  1. 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
  2. Start a normal Jcode shared-server session.
  3. 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 (though it does seem to cycle the provider). 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:

  1. model_picker_is_favorite() identifies a favorite using the full key:
model + provider + api_method + effort
  1. The simplified remote picker initially exposes placeholder rows using api_method=remote-catalog.
  2. Persisted favorites use real methods such as openai-oauth, openrouter, or openai-compatible:deepseek.
  3. Therefore next_model_favorite_after_current() finds no entry.is_favorite row.
  4. cycle_model_favorite_after_current() reports that no favorite was found, but returns ().
  5. 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:

  1. 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.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    autonomous: likelyProbably hands-off: clearly worth fixing, agent can do it, minor judgment needed.bugSomething isn't workingtriage: reproducibleClear repro + clear fix path

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions