Skip to content

Commit 9c60994

Browse files
Show model discounts in Warp Agent CLI picker (#14614)
## Description Show server-provided model discounts in the Warp Agent CLI model picker. The TUI now reads each model's existing `discount_percentage` payload field and appends an italic green `x% off` label. Selected rows use the darker green treatment from the Figma mock. Discounts are hidden when inference is routed through a user/team API key, Bedrock, or Gemini Enterprise because Warp credits are not being billed. ## Linked Issue No linked issue. ## Testing - [x] Ran `./script/format` - [x] Ran all three `cargo clippy` configurations from `script/presubmit` - [x] Manually validated the picker in `warp-tui-oss` with a temporary 50% fallback - [x] Confirmed the live selected row used RGB `208,209,254` background and bold italic RGB `93,128,60` discount text No new automated tests were added because this is straightforward presentation wiring over an existing payload field and was validated through the production TUI renderer. ## Agent Mode - [x] Warp Agent Mode - This PR was created via Warp's AI Agent Mode Agent conversation: https://staging.warp.dev/conversation/9d8332ef-0bec-47cf-a241-f84d8b62786c CHANGELOG-TUI: Model discounts now appear in the Warp Agent CLI model picker. Co-Authored-By: Warp <agent@warp.dev> Co-authored-by: Warp <agent@warp.dev>
1 parent 52061d2 commit 9c60994

14 files changed

Lines changed: 65 additions & 3 deletions

app/src/tui_export.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,8 @@ pub use crate::ai::harness_availability::{
137137
HarnessAvailabilityModel, HarnessModelInfo,
138138
};
139139
pub use crate::ai::llms::{
140-
LLMId, LLMInfo, LLMPreferences, LLMPreferencesEvent, should_show_key_icon_for_model,
140+
LLMId, LLMInfo, LLMPreferences, LLMPreferencesEvent, should_show_bedrock_icon_for_model,
141+
should_show_gemini_enterprise_agent_platform_icon_for_model, should_show_key_icon_for_model,
141142
};
142143
pub use crate::ai::orchestration::{
143144
AuthSecretSelection, CloudAgentStartupAuthFlow, CloudAgentStartupBlocker,

crates/warp_tui/src/api_keys_menu.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,7 @@ impl TuiApiKeysMenuModel {
482482
prefix: None,
483483
description,
484484
state_suffix,
485+
promotional_suffix: None,
485486
is_selectable,
486487
style,
487488
}

crates/warp_tui/src/completion_menu.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ impl TuiCompletionMenuModel {
200200
prefix: None,
201201
description: row.description.clone(),
202202
state_suffix: None,
203+
promotional_suffix: None,
203204
is_selectable: true,
204205
style: TuiInlineMenuRowStyle::InlineMenuItem,
205206
})

crates/warp_tui/src/conversation_menu.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ impl TuiConversationMenuModel {
221221
prefix: None,
222222
description: None,
223223
state_suffix: None,
224+
promotional_suffix: None,
224225
is_selectable: true,
225226
style: TuiInlineMenuRowStyle::Default,
226227
})

crates/warp_tui/src/inline_menu.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ pub(crate) struct TuiInlineMenuRow {
172172
pub(crate) prefix: Option<TuiInlineMenuRowPrefix>,
173173
pub(crate) description: Option<String>,
174174
pub(crate) state_suffix: Option<String>,
175+
pub(crate) promotional_suffix: Option<String>,
175176
pub(crate) is_selectable: bool,
176177
pub(crate) style: TuiInlineMenuRowStyle,
177178
}
@@ -1368,7 +1369,11 @@ fn menu_result_row(
13681369
}
13691370
};
13701371
let show_description = match row.style {
1371-
TuiInlineMenuRowStyle::Default => row.description.is_some() || row.state_suffix.is_some(),
1372+
TuiInlineMenuRowStyle::Default => {
1373+
row.description.is_some()
1374+
|| row.state_suffix.is_some()
1375+
|| row.promotional_suffix.is_some()
1376+
}
13721377
TuiInlineMenuRowStyle::InlineMenuItem => {
13731378
slash_command_columns.show_second && row.description.is_some()
13741379
}
@@ -1458,6 +1463,14 @@ fn menu_result_row(
14581463
};
14591464
description_spans.push((format!(" {suffix}"), suffix_style));
14601465
}
1466+
if let Some(suffix) = &row.promotional_suffix {
1467+
let suffix_style = if is_selected {
1468+
builder.selection_promotional_suffix_style()
1469+
} else {
1470+
builder.promotional_suffix_style()
1471+
};
1472+
description_spans.push((format!(" {suffix}"), suffix_style));
1473+
}
14611474
if !description_spans.is_empty() {
14621475
content = content.child(TuiText::from_spans(description_spans).truncate().finish());
14631476
}

crates/warp_tui/src/inline_menu_tests.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ fn rows_snapshot(
196196
prefix: None,
197197
description: None,
198198
state_suffix: None,
199+
promotional_suffix: None,
199200
is_selectable: true,
200201
style: TuiInlineMenuRowStyle::Default,
201202
})
@@ -332,6 +333,7 @@ fn conversation_like_snapshot_reuses_header_tabs_rows_and_selection() {
332333
prefix: None,
333334
description: Some("2 minutes ago".to_owned()),
334335
state_suffix: None,
336+
promotional_suffix: None,
335337
is_selectable: true,
336338
style: TuiInlineMenuRowStyle::Default,
337339
},
@@ -340,6 +342,7 @@ fn conversation_like_snapshot_reuses_header_tabs_rows_and_selection() {
340342
prefix: None,
341343
description: None,
342344
state_suffix: None,
345+
promotional_suffix: None,
343346
is_selectable: false,
344347
style: TuiInlineMenuRowStyle::Default,
345348
},
@@ -371,6 +374,7 @@ fn default_row_state_suffix_is_muted_and_italic() {
371374
prefix: None,
372375
description: None,
373376
state_suffix: Some("(key connected)".to_owned()),
377+
promotional_suffix: None,
374378
is_selectable: true,
375379
style: TuiInlineMenuRowStyle::Default,
376380
}],
@@ -425,6 +429,7 @@ fn conversation_like_snapshot_keeps_selection_visible_within_production_height()
425429
prefix: None,
426430
description: None,
427431
state_suffix: None,
432+
promotional_suffix: None,
428433
is_selectable: true,
429434
style: TuiInlineMenuRowStyle::Default,
430435
})
@@ -462,6 +467,7 @@ fn slash_command_rows_match_figma_layout_and_colors() {
462467
prefix: None,
463468
description: Some("Start a new agent conversation".to_owned()),
464469
state_suffix: Some("(currently on)".to_owned()),
470+
promotional_suffix: None,
465471
is_selectable: true,
466472
style: TuiInlineMenuRowStyle::InlineMenuItem,
467473
},
@@ -470,6 +476,7 @@ fn slash_command_rows_match_figma_layout_and_colors() {
470476
prefix: None,
471477
description: Some("Create a plan".to_owned()),
472478
state_suffix: Some("(currently off)".to_owned()),
479+
promotional_suffix: None,
473480
is_selectable: true,
474481
style: TuiInlineMenuRowStyle::InlineMenuItem,
475482
},
@@ -561,6 +568,7 @@ fn long_slash_command_titles_are_ellipsized_before_the_description() {
561568
prefix: None,
562569
description: Some("Walk users through PR review comments".to_owned()),
563570
state_suffix: None,
571+
promotional_suffix: None,
564572
is_selectable: true,
565573
style: TuiInlineMenuRowStyle::InlineMenuItem,
566574
}],
@@ -584,6 +592,7 @@ fn wide_slash_command_rows_expand_to_show_long_titles() {
584592
prefix: None,
585593
description: Some("Walk users through PR review comments".to_owned()),
586594
state_suffix: None,
595+
promotional_suffix: None,
587596
is_selectable: true,
588597
style: TuiInlineMenuRowStyle::InlineMenuItem,
589598
}],
@@ -614,6 +623,7 @@ fn boundary_width_preserves_useful_title_and_description_columns() {
614623
prefix: None,
615624
description: Some("Start a new agent conversation".to_owned()),
616625
state_suffix: None,
626+
promotional_suffix: None,
617627
is_selectable: true,
618628
style: TuiInlineMenuRowStyle::InlineMenuItem,
619629
}],
@@ -640,6 +650,7 @@ fn narrow_slash_command_rows_use_the_full_width_for_titles() {
640650
prefix: None,
641651
description: Some("Description hidden at narrow widths".to_owned()),
642652
state_suffix: None,
653+
promotional_suffix: None,
643654
is_selectable: true,
644655
style: TuiInlineMenuRowStyle::InlineMenuItem,
645656
}],

crates/warp_tui/src/mcp_install_flow.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,7 @@ impl TuiMcpInstallFlowModel {
285285
request.variables.len()
286286
)),
287287
state_suffix: None,
288+
promotional_suffix: None,
288289
is_selectable: true,
289290
style: TuiInlineMenuRowStyle::Default,
290291
})

crates/warp_tui/src/mcp_menu.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ impl TuiMcpMenuModel {
208208
prefix: None,
209209
description: row.description.clone(),
210210
state_suffix: None,
211+
promotional_suffix: None,
211212
is_selectable: row_is_selectable(row),
212213
style: TuiInlineMenuRowStyle::Default,
213214
})

crates/warp_tui/src/model_menu.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
use warp::editor::{CodeEditorModel, CodeEditorModelEvent};
44
use warp::tui_export::{
55
LLMId, LLMPreferences, LLMPreferencesEvent, ModelPickerChoice, query_model_picker_choices,
6-
should_show_key_icon_for_model,
6+
should_show_bedrock_icon_for_model,
7+
should_show_gemini_enterprise_agent_platform_icon_for_model, should_show_key_icon_for_model,
78
};
89
use warp_editor::model::CoreEditorModel;
910
use warpui_core::{AppContext, Entity, ModelContext, ModelHandle, SingletonEntity};
@@ -22,6 +23,7 @@ struct TuiModelMenuRow {
2223
title: String,
2324
is_selectable: bool,
2425
is_key_connected: bool,
26+
discount_percentage: Option<f32>,
2527
}
2628

2729
#[derive(Debug, Clone, Default)]
@@ -86,6 +88,7 @@ impl TuiModelMenuModel {
8688
id,
8789
is_selectable,
8890
is_key_connected: false,
91+
discount_percentage: None,
8992
})
9093
.collect(),
9194
false,
@@ -245,9 +248,16 @@ impl TuiModelMenuModel {
245248
}
246249

247250
fn model_menu_row(choice: ModelPickerChoice, app: &AppContext) -> TuiModelMenuRow {
251+
let uses_external_inference = should_show_key_icon_for_model(&choice.llm, app)
252+
|| should_show_bedrock_icon_for_model(&choice.llm, app)
253+
|| should_show_gemini_enterprise_agent_platform_icon_for_model(&choice.llm, app);
248254
TuiModelMenuRow {
249255
is_selectable: choice.is_selectable(),
250256
is_key_connected: should_show_key_icon_for_model(&choice.llm, app),
257+
discount_percentage: choice
258+
.llm
259+
.discount_percentage
260+
.filter(|_| !uses_external_inference),
251261
id: choice.llm.id,
252262
title: choice.llm.display_name,
253263
}
@@ -259,11 +269,18 @@ fn snapshot_row(row: &TuiModelMenuRow) -> TuiInlineMenuRow {
259269
prefix: None,
260270
description: (!row.is_selectable).then(|| "disabled".to_owned()),
261271
state_suffix: row.is_key_connected.then(|| "(key connected)".to_owned()),
272+
promotional_suffix: discount_label(row.discount_percentage),
262273
is_selectable: row.is_selectable,
263274
style: TuiInlineMenuRowStyle::Default,
264275
}
265276
}
266277

278+
fn discount_label(discount_percentage: Option<f32>) -> Option<String> {
279+
discount_percentage
280+
.filter(|percentage| *percentage > 0.)
281+
.map(|percentage| format!("{}% off", percentage.round() as u32))
282+
}
283+
267284
fn preferred_selection_index(
268285
rows: &[TuiModelMenuRow],
269286
active_id: &LLMId,

crates/warp_tui/src/model_menu_tests.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ fn row(id: &str, is_selectable: bool, is_key_connected: bool) -> TuiModelMenuRow
1313
title: id.to_owned(),
1414
is_selectable,
1515
is_key_connected,
16+
discount_percentage: None,
1617
}
1718
}
1819

0 commit comments

Comments
 (0)