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>
Showing 3 changed files with 37 additions and 5 deletions.
| @@ -66,8 +66,9 @@ Gruvbox, Rosé Pine, Solarized and Dracula. | ||
| 66 | 66 | Claude's models (current ones, plus legacy models folded away), every model Pi's configured |
| 67 | 67 | providers offer, and every model route jcode's logins and provider profiles make available. |
| 68 | 68 | 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. | |
| 71 | 72 | - A reasoning menu (Ctrl+Shift+E): Low to Max (as the model supports, its default marked), |
| 72 | 73 | Claude's Ultracode and Ultrathink, and Fast Mode on models that have it. Pi threads list Pi's thinking levels; |
| 73 | 74 | jcode threads offer effort levels on its Anthropic, OpenAI and OpenRouter routes. |
| @@ -938,6 +938,20 @@ impl ChatView { | ||
| 938 | 938 | |
| 939 | 939 | /// Puts the picked item in the composer in place of the word typed. |
| 940 | 940 | 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 | + } | |
| 941 | 955 | let replacement = match item { |
| 942 | 956 | MenuItem::Command(command) => format!("{}{} ", if menu.trigger == '$' { '$' } else { '/' }, command.name), |
| 943 | 957 | MenuItem::Path { path, .. } => format!("@{path} "), |
| @@ -487,6 +487,9 @@ impl ModelPicker { | ||
| 487 | 487 | let pick = model.clone(); |
| 488 | 488 | let star = model.clone(); |
| 489 | 489 | 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; | |
| 490 | 493 | let detail = match model.provider { |
| 491 | 494 | Provider::Claude => instance, |
| 492 | 495 | Provider::Pi | Provider::Jcode => format!("{instance} · {}", model.detail), |
| @@ -499,10 +502,21 @@ impl ModelPicker { | ||
| 499 | 502 | .px(px(8.)) |
| 500 | 503 | .py(px(6.)) |
| 501 | 504 | .rounded(px(8.)) |
| 502 | - .cursor_pointer() | |
| 503 | 505 | .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 | + }) | |
| 506 | 520 | .child( |
| 507 | 521 | v_flex() |
| 508 | 522 | .flex_1() |
| @@ -624,6 +638,8 @@ impl Render for ModelPicker { | ||
| 624 | 638 | }; |
| 625 | 639 | let legacy_count = legacy.len(); |
| 626 | 640 | 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()); | |
| 627 | 643 | let setup = if searching { None } else { self.render_setup(cx) }; |
| 628 | 644 | |
| 629 | 645 | h_flex() |
| @@ -644,6 +660,7 @@ impl Render for ModelPicker { | ||
| 644 | 660 | && !modifiers.shift |
| 645 | 661 | && let Some(ix) = keystroke.key.parse::<usize>().ok().filter(|digit| (1..=9).contains(digit)) |
| 646 | 662 | && let Some(model) = shortcuts.get(ix - 1) |
| 663 | + && !(started && model.instance != current_instance) | |
| 647 | 664 | { |
| 648 | 665 | cx.stop_propagation(); |
| 649 | 666 | this.pick(model.clone(), cx); |