Skip to content

Commit c001692

Browse files
author
CodeWhale Agent
committed
fix(tui): align Fleet setup with role/profile roster editing (#4093)
Agent WIP: reframe setup modal around profile roster rather than provider-scoped configuration.
1 parent 9a74825 commit c001692

1 file changed

Lines changed: 89 additions & 48 deletions

File tree

crates/tui/src/tui/views/fleet_setup.rs

Lines changed: 89 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
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;
1516
use std::path::{Path, PathBuf};
1617

1718
use 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.
3839
struct 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

4445
const 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.
5051
const 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.
9698
const 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

130134
impl 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)]
180202
enum 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

205232
impl 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

Comments
 (0)