1212//! provider/model picker that will churn most of this text. The command entry
1313//! (`CmdFleetDescription`) is already localized.
1414
15+ use std:: borrow:: Cow ;
1516use std:: path:: { Path , PathBuf } ;
1617
1718use crossterm:: event:: { KeyCode , KeyEvent } ;
@@ -36,9 +37,9 @@ const PROFILE_DIR: &str = ".codewhale/agents";
3637/// A selectable choice in a wizard step: a short identifier `label`, a one-line
3738/// `summary`, and a longer `description` shown (wrapped) in the detail pane.
3839struct Choice {
39- label : & ' static str ,
40- summary : & ' static str ,
41- description : & ' static str ,
40+ label : Cow < ' static , str > ,
41+ summary : Cow < ' static , str > ,
42+ description : Cow < ' static , str > ,
4243}
4344
4445const CHOICE_LIST_WIDTH : u16 = 22 ;
@@ -49,54 +50,55 @@ const CHOICE_TWO_COLUMN_MIN_WIDTH: u16 = CHOICE_LIST_WIDTH + CHOICE_DETAIL_MIN_W
4950/// so these strings are part of the generated-profile contract.
5051const ROLES : [ Choice ; 8 ] = [
5152 Choice {
52- label : "manager" ,
53- summary : "Plan & split queued work" ,
54- description : "Coordinates the Fleet run: plans the work, splits it into bounded tasks, and dispatches workers." ,
53+ label : Cow :: Borrowed ( "manager" ) ,
54+ summary : Cow :: Borrowed ( "Plan & split queued work" ) ,
55+ description : Cow :: Borrowed ( "Coordinates the Fleet run: plans the work, splits it into bounded tasks, and dispatches workers." ) ,
5556 } ,
5657 Choice {
57- label : "scout" ,
58- summary : "Read-first research" ,
59- description : "Research and repo reconnaissance. Reads and summarizes before anything is written." ,
58+ label : Cow :: Borrowed ( "scout" ) ,
59+ summary : Cow :: Borrowed ( "Read-first research" ) ,
60+ description : Cow :: Borrowed ( "Research and repo reconnaissance. Reads and summarizes before anything is written." ) ,
6061 } ,
6162 Choice {
62- label : "builder" ,
63- summary : "Implements bounded changes" ,
64- description : "Implements changes strictly inside its assigned task scope; writes only what the slice needs." ,
63+ label : Cow :: Borrowed ( "builder" ) ,
64+ summary : Cow :: Borrowed ( "Implements bounded changes" ) ,
65+ description : Cow :: Borrowed ( "Implements changes strictly inside its assigned task scope; writes only what the slice needs." ) ,
6566 } ,
6667 Choice {
67- label : "reviewer" ,
68- summary : "Read-only review" ,
69- description : "Checks regressions, tests, and diffs. Read-only — it never writes." ,
68+ label : Cow :: Borrowed ( "reviewer" ) ,
69+ summary : Cow :: Borrowed ( "Read-only review" ) ,
70+ description : Cow :: Borrowed ( "Checks regressions, tests, and diffs. Read-only — it never writes." ) ,
7071 } ,
7172 Choice {
72- label : "verifier" ,
73- summary : "Runs focused validation" ,
74- description : "Runs targeted validation and reports receipts back to the orchestrator." ,
73+ label : Cow :: Borrowed ( "verifier" ) ,
74+ summary : Cow :: Borrowed ( "Runs focused validation" ) ,
75+ description : Cow :: Borrowed ( "Runs targeted validation and reports receipts back to the orchestrator." ) ,
7576 } ,
7677 Choice {
77- label : "synthesizer" ,
78- summary : "Reduce receipts to handoff" ,
79- description : "Turns worker receipts into bounded handoff state instead of raw transcript replay." ,
78+ label : Cow :: Borrowed ( "synthesizer" ) ,
79+ summary : Cow :: Borrowed ( "Reduce receipts to handoff" ) ,
80+ description : Cow :: Borrowed ( "Turns worker receipts into bounded handoff state instead of raw transcript replay." ) ,
8081 } ,
8182 Choice {
82- label : "general" ,
83- summary : "General-purpose worker" ,
84- description : "A flexible worker with no specialized posture — use it when the task doesn't fit a named role." ,
83+ label : Cow :: Borrowed ( "general" ) ,
84+ summary : Cow :: Borrowed ( "General-purpose worker" ) ,
85+ description : Cow :: Borrowed ( "A flexible worker with no specialized posture — use it when the task doesn't fit a named role." ) ,
8586 } ,
8687 Choice {
87- label : "custom" ,
88- summary : "Author a profile by hand" ,
89- description : "Define the posture yourself in a workspace agent TOML profile under .codewhale/agents/." ,
88+ label : Cow :: Borrowed ( "custom" ) ,
89+ summary : Cow :: Borrowed ( "Author a profile by hand" ) ,
90+ description : Cow :: Borrowed ( "Define the posture yourself in a workspace agent TOML profile under .codewhale/agents/." ) ,
9091 } ,
9192] ;
9293
9394/// The `inherit` row shown first in the Model step (#3167). Concrete provider
94- /// models follow it, built per-run from the active provider's catalog, so the
95- /// user picks a real model instead of an abstract class.
95+ /// models follow it, built per-run from EVERY configured provider's catalog
96+ /// (#4093), so the user picks a real route — including cross-provider ones —
97+ /// instead of an abstract class or only the active provider's models.
9698const MODEL_INHERIT : Choice = Choice {
97- label : "inherit" ,
98- summary : "Same model as now" ,
99- description : "Reuse the active provider, model, and reasoning for this worker — the operator's route. Recommended default." ,
99+ label : Cow :: Borrowed ( "inherit" ) ,
100+ summary : Cow :: Borrowed ( "Same model as now" ) ,
101+ description : Cow :: Borrowed ( "Reuse the active provider, model, and reasoning for this worker — the operator's route. Recommended default." ) ,
100102} ;
101103
102104#[ derive( Debug , Clone ) ]
@@ -122,9 +124,11 @@ pub struct FleetSetupSnapshot {
122124 /// config / project), so the wizard can say when a chosen role would
123125 /// override an existing roster member.
124126 roster_members : Vec < ( String , String ) > ,
125- /// Concrete model ids selectable for a worker on the active provider,
126- /// shown after `inherit` in the Model step.
127- available_models : Vec < & ' static str > ,
127+ /// `(provider display name, model id)` pairs selectable for a worker,
128+ /// drawn from ALL configured providers — not only the active one (#4093).
129+ /// Shown after `inherit` in the Model step so a Fleet worker can be pinned
130+ /// to a route independent of the parent/current provider.
131+ available_models : Vec < ( String , String ) > ,
128132}
129133
130134impl FleetSetupSnapshot {
@@ -170,11 +174,29 @@ impl FleetSetupSnapshot {
170174 heartbeat_timeout_secs : config
171175 . subagent_heartbeat_timeout_secs_for_provider ( app. api_provider ) ,
172176 roster_members,
173- available_models : crate :: config :: model_completion_names_for_provider ( app. api_provider ) ,
177+ available_models : cross_provider_model_routes ( config , app. api_provider ) ,
174178 }
175179 }
176180}
177181
182+ /// Build the `(provider display name, model id)` pairs selectable for a worker
183+ /// from EVERY configured provider — not only the active one (#4093). Fleet
184+ /// workers can be pinned to a route independent of the parent/current provider,
185+ /// so the Model step must offer the same cross-provider catalog the model
186+ /// picker does, instead of the active provider's models alone.
187+ fn cross_provider_model_routes (
188+ config : & Config ,
189+ active : crate :: config:: ApiProvider ,
190+ ) -> Vec < ( String , String ) > {
191+ let mut routes = Vec :: new ( ) ;
192+ for provider in crate :: provider_lake:: configured_providers ( config, active) {
193+ for model in crate :: provider_lake:: models_for_provider ( config, active, provider) {
194+ routes. push ( ( provider. display_name ( ) . to_string ( ) , model) ) ;
195+ }
196+ }
197+ routes
198+ }
199+
178200/// Which focused screen of the wizard is showing.
179201#[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
180202enum Step {
@@ -198,8 +220,13 @@ pub struct FleetSetupView {
198220 model_draft : Option < Box < crate :: fleet:: profile:: FleetProfileDraft > > ,
199221 /// Display label of the model that authored `model_draft`.
200222 model_draft_label : Option < String > ,
201- /// Model-step rows: `inherit` followed by the active provider's models.
223+ /// Model-step rows: `inherit` followed by one row per concrete model from
224+ /// every configured provider (#4093).
202225 model_choices : Vec < Choice > ,
226+ /// `(provider, model)` aligned with `model_choices`. Index 0 is `inherit`
227+ /// (the active route); later rows pin a concrete, possibly cross-provider
228+ /// route. Drives the review/copy so a pinned route names its own provider.
229+ model_routes : Vec < ( String , String ) > ,
203230}
204231
205232impl FleetSetupView {
@@ -210,12 +237,18 @@ impl FleetSetupView {
210237
211238 fn from_snapshot ( snapshot : FleetSetupSnapshot ) -> Self {
212239 let mut model_choices = vec ! [ MODEL_INHERIT ] ;
213- for & name in & snapshot. available_models {
240+ // `inherit` (index 0) maps to the active route; every later row pins a
241+ // concrete (provider, model) drawn from all configured providers.
242+ let mut model_routes = vec ! [ ( snapshot. provider. clone( ) , snapshot. model. clone( ) ) ] ;
243+ for ( provider, model) in & snapshot. available_models {
214244 model_choices. push ( Choice {
215- label : name,
216- summary : "Pin this model" ,
217- description : "Route this worker to this specific model on the active provider instead of inheriting the session route." ,
245+ label : Cow :: Owned ( model. clone ( ) ) ,
246+ summary : Cow :: Owned ( format ! ( "Pin this model ({provider})" ) ) ,
247+ description : Cow :: Owned ( format ! (
248+ "Route this worker to {model} on {provider} instead of inheriting the session route."
249+ ) ) ,
218250 } ) ;
251+ model_routes. push ( ( provider. clone ( ) , model. clone ( ) ) ) ;
219252 }
220253 Self {
221254 snapshot,
@@ -226,6 +259,7 @@ impl FleetSetupView {
226259 model_draft : None ,
227260 model_draft_label : None ,
228261 model_choices,
262+ model_routes,
229263 }
230264 }
231265
@@ -260,8 +294,8 @@ impl FleetSetupView {
260294 }
261295
262296 /// The planner role chosen (drives the profile file name and `role_hint`).
263- fn selected_role ( & self ) -> & ' static str {
264- ROLES [ self . role_idx . min ( ROLES . len ( ) - 1 ) ] . label
297+ fn selected_role ( & self ) -> String {
298+ ROLES [ self . role_idx . min ( ROLES . len ( ) - 1 ) ] . label . to_string ( )
265299 }
266300
267301 /// Copy note when the chosen role would override an existing roster
@@ -276,13 +310,20 @@ impl FleetSetupView {
276310 . map ( |( id, origin) | format ! ( "Overrides the {origin} '{id}' roster member." ) )
277311 }
278312
279- /// The concrete model chosen for this worker, or `None` for `inherit`
280- /// (reuse the session route). Written to the profile `model` field .
313+ /// The concrete model chosen for this worker, written to the profile
314+ /// `model` field. `None` means `inherit` (reuse the session route) .
281315 fn selected_model ( & self ) -> Option < String > {
282- match self . model_choices . get ( self . model_idx ) {
283- Some ( choice) if choice. label != "inherit" => Some ( choice. label . to_string ( ) ) ,
284- _ => None ,
316+ self . selected_route ( ) . map ( |( _, model) | model)
317+ }
318+
319+ /// The concrete `(provider, model)` chosen for this worker — a pinned route
320+ /// independent of the parent/current provider (#4093) — or `None` when
321+ /// `inherit` is selected (reuse the session route).
322+ fn selected_route ( & self ) -> Option < ( String , String ) > {
323+ if self . model_idx == 0 {
324+ return None ;
285325 }
326+ self . model_routes . get ( self . model_idx ) . cloned ( )
286327 }
287328
288329 /// Number of selectable rows on the current step (0 on the review step).
0 commit comments