Vectrum
Sign In

Model picker: other providers greyed out once a thread started; /model opens it

As in T3 Code, a started thread can't move to another provider: those
models are greyed out with T3's explanation, and Ctrl+1-9 skips them. A
new thread still switches. Typing /model opens the model picker.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
JjohannSocommitted 1 parent 25a21cdcommit1efac74

Showing 3 changed files with 37 additions and 5 deletions.

+3−2README.mdView File
@@ -66,8 +66,9 @@ Gruvbox, Rosé Pine, Solarized and Dracula.
6666 Claude's models (current ones, plus legacy models folded away), every model Pi's configured
6767 providers offer, and every model route jcode's logins and provider profiles make available.
6868 Star models to keep them under Favorites, search across all of them, pick the first nine
69- with Ctrl+1–9, and switch providers with Ctrl+Shift+↑/↓. Picking another provider's model
70- starts a thread of that kind in its place.
69+ with Ctrl+1–9, and switch providers with Ctrl+Shift+↑/↓. On a new thread, picking another
70+ provider's model starts a thread of that kind in its place; once a thread has started,
71+ other providers' models are greyed out, as in T3 Code. `/model` opens the picker too.
7172 - A reasoning menu (Ctrl+Shift+E): Low to Max (as the model supports, its default marked),
7273 Claude's Ultracode and Ultrathink, and Fast Mode on models that have it. Pi threads list Pi's thinking levels;
7374 jcode threads offer effort levels on its Anthropic, OpenAI and OpenRouter routes.
+14−0src/ui/chat.rsView File
@@ -938,6 +938,20 @@ impl ChatView {
938938
939939 /// Puts the picked item in the composer in place of the word typed.
940940 fn accept_menu_item(&mut self, menu: &CommandMenu, item: &MenuItem, window: &mut Window, cx: &mut Context<Self>) {
941+ // `/model` opens the model picker, as in T3 Code.
942+ if let MenuItem::Command(command) = item
943+ && command.name == "model"
944+ && menu.trigger == '/'
945+ {
946+ let start = menu.start;
947+ self.composer.update(cx, |composer, cx| {
948+ let cursor = composer.cursor();
949+ composer.set_selected_range(start..cursor, cx);
950+ composer.replace("", window, cx);
951+ });
952+ self.set_picker(Picker::Model, true, window, cx);
953+ return;
954+ }
941955 let replacement = match item {
942956 MenuItem::Command(command) => format!("{}{} ", if menu.trigger == '$' { '$' } else { '/' }, command.name),
943957 MenuItem::Path { path, .. } => format!("@{path} "),
+20−3src/ui/pickers.rsView File
@@ -487,6 +487,9 @@ impl ModelPicker {
487487 let pick = model.clone();
488488 let star = model.clone();
489489 let instance = catalog.label(&model.instance);
490+ // As in T3 Code, a thread can't move to another provider once it started.
491+ let state = self.session.read(cx);
492+ let locked = !state.items.is_empty() && model.instance != state.instance;
490493 let detail = match model.provider {
491494 Provider::Claude => instance,
492495 Provider::Pi | Provider::Jcode => format!("{instance} · {}", model.detail),
@@ -499,10 +502,21 @@ impl ModelPicker {
499502 .px(px(8.))
500503 .py(px(6.))
501504 .rounded(px(8.))
502- .cursor_pointer()
503505 .when(selected, |this| this.bg(p.ink.opacity(0.08)))
504- .hover(|style| style.bg(p.menu_row_hover))
505- .on_click(cx.listener(move |this, _, _, cx| this.pick(pick.clone(), cx)))
506+ .map(|this| {
507+ if locked {
508+ this.opacity(0.64).cursor_not_allowed().tooltip(|window, cx| {
509+ Tooltip::new(
510+ "This thread does not support switching providers after it has started. Start a new thread to use this model.",
511+ )
512+ .build(window, cx)
513+ })
514+ } else {
515+ this.cursor_pointer()
516+ .hover(|style| style.bg(p.menu_row_hover))
517+ .on_click(cx.listener(move |this, _, _, cx| this.pick(pick.clone(), cx)))
518+ }
519+ })
506520 .child(
507521 v_flex()
508522 .flex_1()
@@ -624,6 +638,8 @@ impl Render for ModelPicker {
624638 };
625639 let legacy_count = legacy.len();
626640 let empty = rows.is_empty() && legacy_count == 0;
641+ let state = self.session.read(cx);
642+ let (started, current_instance) = (!state.items.is_empty(), state.instance.clone());
627643 let setup = if searching { None } else { self.render_setup(cx) };
628644
629645 h_flex()
@@ -644,6 +660,7 @@ impl Render for ModelPicker {
644660 && !modifiers.shift
645661 && let Some(ix) = keystroke.key.parse::<usize>().ok().filter(|digit| (1..=9).contains(digit))
646662 && let Some(model) = shortcuts.get(ix - 1)
663+ && !(started && model.instance != current_instance)
647664 {
648665 cx.stop_propagation();
649666 this.pick(model.clone(), cx);