fix(tui): show the blank Custom provider slot as setup, not a legacy route

On a fresh install the first-run provider picker listed the blank Custom
slot as `custom (custom)    legacy · legacy` with the footer
`Enter unavailable`, although Enter opens the endpoint form. The slot fails
route admission by design (nothing is configured), and the unadmitted
branch skipped its catalog presentation. Keep its descriptor label
"Custom (OpenAI-compatible)", show `needs endpoint`, and name the real
Enter action. Configured custom tables and bundled descriptor rows are
unchanged.

Also make all six published website frames reproducible from the opt-in
`website_current_terminal_capture` test: it now captures the empty Fleet
and Tasks work bar at 100x32 instead of an unrecorded manual sequence.

Evidence: found by re-capturing the website frames from a 3a788990 build
in a sealed HOME (PTY probe frame: `▸ custom (custom)    legacy · legacy`,
footer `Enter unavailable`; Enter opened "Custom provider" form).
`cargo test -p codewhale-tui --lib -- tui::provider_picker::tests::`
test result: ok. 145 passed; 0 failed (incl. new
blank_custom_slot_reads_as_setup_not_legacy). Capture test with
--features long-running-tests: 1 passed; 0 failed. rustfmt --check and
git diff --check passed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: CodeWhale Bot <[email protected]>
This commit is contained in:
CodeWhale BotandClaude Opus 5.5 committed 2026-10-04 12:24:41 -07:00
1 parent 3a788990d0
commit 8ba2bbf805
2 files changed
+71 -2

No files matched your search

+44 -1
View File
@@ -597,7 +597,18 @@ impl ProviderDashboardRow {
provider,
identity: None,
provider_id: provider_id.clone(),
// The blank `Custom` catalog slot is not a configured route,
// so admission fails by design; it keeps its catalog label.
display_name: presentation
.or_else(|| {
(provider == ProviderKind::Custom && configured.is_none())
.then(|| {
codewhale_config::descriptors::compatibility_for_id(
&provider_id,
)
})
.flatten()
})
.map(|row| row.label.to_string())
.unwrap_or_else(|| format!("{provider_id} (custom)")),
kind: configured
@@ -1027,6 +1038,9 @@ impl ProviderDashboardRow {
// A row you cannot use yet says what it needs, once. The
// bundled-model count beside it only repeated itself down a
// fifty-row list; the Details pane still carries it.
if self.is_custom_placeholder() {
return "needs endpoint".to_string();
}
match self.readiness {
ResolvedProviderReadiness::MissingKey => return "needs key".to_string(),
ResolvedProviderReadiness::MissingLogin => return "needs sign-in".to_string(),
@@ -1043,6 +1057,14 @@ impl ProviderDashboardRow {
}
}
/// The blank `Custom` slot: Enter opens the endpoint form (see
/// `activate_selected_row`), so it is neither a broken route nor legacy.
fn is_custom_placeholder(&self) -> bool {
self.provider == ProviderKind::Custom
&& !self.is_configured
&& provider_descriptor(&self.provider_id).is_none()
}
fn catalog_label(&self) -> String {
match self.catalog_status {
ProviderCatalogStatus::Bundled => format!("{} bundled", self.available_model_count),
@@ -2721,7 +2743,9 @@ impl ProviderPickerView {
}
fn render_list(&self, area: Rect, buf: &mut Buffer) {
let enter_action = if !self.selected_route_is_valid() {
let enter_action = if self.rows[self.selected_idx].is_custom_placeholder() {
self.tr(MessageId::PickerActionCustom)
} else if !self.selected_route_is_valid() {
self.tr(MessageId::PickerActionUnavailable)
} else if self.selected_has_key() {
self.tr(MessageId::PickerActionApply)
@@ -5735,6 +5759,25 @@ mod tests {
}
}
/// First-run catalog: the blank Custom slot fails route admission by
/// design, but it is the way into the endpoint form, not a legacy route.
#[test]
fn blank_custom_slot_reads_as_setup_not_legacy() {
let config = Config::default();
let picker = ProviderPickerView::new(ProviderKind::Deepseek, &config);
let row = picker
.rows
.iter()
.find(|row| row.provider_id == ProviderKind::Custom.as_str())
.expect("blank custom slot");
assert_eq!(row.display_name, "Custom (OpenAI-compatible)");
assert!(row.is_custom_placeholder());
assert_eq!(
row.list_row_hint(ProviderListView::Catalog),
"needs endpoint"
);
}
#[test]
fn default_view_shows_only_configured_providers() {
// #3830: with nothing but the active provider set up, the default
+27 -1
View File
@@ -16,7 +16,8 @@ const SAVED_TEXT: &str = "Restored conversation proof";
/// Published codewhale.net terminal media. Default Underwater theme, no
/// motion, a home-relative workspace, and only states a fresh install really
/// reaches: the first-run provider picker, home, a typed draft, and /help.
/// reaches: the first-run provider picker, home, a typed draft, /help, and
/// the empty Fleet and Tasks work bar.
#[test]
#[ignore = "opt-in website media; empty isolated session, no provider calls"]
fn website_current_terminal_capture() {
@@ -59,6 +60,31 @@ fn website_current_terminal_capture() {
wait(&mut tui, "/model");
capture(&mut tui, "website-help");
tui.shutdown();
// The work bar at the taller size the site shows it, opened by the
// advertised chords on an emptied composer: Fleet (Ctrl+]), then Tasks.
let workspace = make_sealed_workspace_in_home("my-project").unwrap();
let (_workspace, mut tui) = start_in(
workspace,
Launch {
rows: 32,
cols: 100,
keep_composer: true,
..Launch::default()
},
|_| {},
);
tui.wait_for_idle(Duration::from_secs(1), WAIT).unwrap();
tui.send(keys::key::backspaces(80)).unwrap();
wait(&mut tui, "Type a message");
assert_website_frame(&mut tui);
tui.send([0x1d]).unwrap();
wait(&mut tui, "no agents have run this session");
capture(&mut tui, "website-workbar-fleet");
tui.send(b"\x1b[9;6u").unwrap();
wait(&mut tui, "no to-dos yet");
capture(&mut tui, "website-workbar");
tui.shutdown();
}
/// The published frame names the project the way a user's shell would and