mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Add model availability NUX tooltips (#13021)
- override startup tooltips with model availability NUX and persist per-model show counts in config - stop showing each model after four exposures and fall back to normal tooltips
This commit is contained in:
+206
-5
@@ -46,6 +46,7 @@ use codex_core::config::ConfigBuilder;
|
||||
use codex_core::config::ConfigOverrides;
|
||||
use codex_core::config::edit::ConfigEdit;
|
||||
use codex_core::config::edit::ConfigEditsBuilder;
|
||||
use codex_core::config::types::ModelAvailabilityNuxConfig;
|
||||
use codex_core::config_loader::ConfigLayerStackOrdering;
|
||||
use codex_core::features::Feature;
|
||||
use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig;
|
||||
@@ -61,6 +62,7 @@ use codex_protocol::config_types::Personality;
|
||||
#[cfg(target_os = "windows")]
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::items::TurnItem;
|
||||
use codex_protocol::openai_models::ModelAvailabilityNux;
|
||||
use codex_protocol::openai_models::ModelPreset;
|
||||
use codex_protocol::openai_models::ModelUpgrade;
|
||||
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
|
||||
@@ -451,12 +453,77 @@ fn target_preset_for_upgrade<'a>(
|
||||
.find(|preset| preset.model == target_model && preset.show_in_picker)
|
||||
}
|
||||
|
||||
const MODEL_AVAILABILITY_NUX_MAX_SHOW_COUNT: u32 = 4;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct StartupTooltipOverride {
|
||||
model_slug: String,
|
||||
message: String,
|
||||
}
|
||||
|
||||
fn select_model_availability_nux(
|
||||
available_models: &[ModelPreset],
|
||||
nux_config: &ModelAvailabilityNuxConfig,
|
||||
) -> Option<StartupTooltipOverride> {
|
||||
available_models.iter().find_map(|preset| {
|
||||
let ModelAvailabilityNux { message } = preset.availability_nux.as_ref()?;
|
||||
let shown_count = nux_config
|
||||
.shown_count
|
||||
.get(&preset.model)
|
||||
.copied()
|
||||
.unwrap_or_default();
|
||||
(shown_count < MODEL_AVAILABILITY_NUX_MAX_SHOW_COUNT).then(|| StartupTooltipOverride {
|
||||
model_slug: preset.model.clone(),
|
||||
message: message.clone(),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async fn prepare_startup_tooltip_override(
|
||||
config: &mut Config,
|
||||
available_models: &[ModelPreset],
|
||||
is_first_run: bool,
|
||||
) -> Option<String> {
|
||||
if is_first_run || !config.show_tooltips {
|
||||
return None;
|
||||
}
|
||||
|
||||
let tooltip_override =
|
||||
select_model_availability_nux(available_models, &config.model_availability_nux)?;
|
||||
|
||||
let shown_count = config
|
||||
.model_availability_nux
|
||||
.shown_count
|
||||
.get(&tooltip_override.model_slug)
|
||||
.copied()
|
||||
.unwrap_or_default();
|
||||
let next_count = shown_count.saturating_add(1);
|
||||
let mut updated_shown_count = config.model_availability_nux.shown_count.clone();
|
||||
updated_shown_count.insert(tooltip_override.model_slug.clone(), next_count);
|
||||
|
||||
if let Err(err) = ConfigEditsBuilder::new(&config.codex_home)
|
||||
.set_model_availability_nux_count(&updated_shown_count)
|
||||
.apply()
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
error = %err,
|
||||
model = %tooltip_override.model_slug,
|
||||
"failed to persist model availability nux count"
|
||||
);
|
||||
return Some(tooltip_override.message);
|
||||
}
|
||||
|
||||
config.model_availability_nux.shown_count = updated_shown_count;
|
||||
Some(tooltip_override.message)
|
||||
}
|
||||
|
||||
async fn handle_model_migration_prompt_if_needed(
|
||||
tui: &mut tui::Tui,
|
||||
config: &mut Config,
|
||||
model: &str,
|
||||
app_event_tx: &AppEventSender,
|
||||
available_models: Vec<ModelPreset>,
|
||||
available_models: &[ModelPreset],
|
||||
) -> Option<AppExitInfo> {
|
||||
let upgrade = available_models
|
||||
.iter()
|
||||
@@ -481,13 +548,13 @@ async fn handle_model_migration_prompt_if_needed(
|
||||
model,
|
||||
&target_model,
|
||||
&config.notices.model_migrations,
|
||||
&available_models,
|
||||
available_models,
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let current_preset = available_models.iter().find(|preset| preset.model == model);
|
||||
let target_preset = target_preset_for_upgrade(&available_models, &target_model);
|
||||
let target_preset = target_preset_for_upgrade(available_models, &target_model);
|
||||
let target_preset = target_preset?;
|
||||
let target_display_name = target_preset.display_name.clone();
|
||||
let heading_label = if target_display_name == model {
|
||||
@@ -668,6 +735,7 @@ impl App {
|
||||
is_first_run: false,
|
||||
feedback_audience: self.feedback_audience,
|
||||
model: Some(self.chat_widget.current_model().to_string()),
|
||||
startup_tooltip_override: None,
|
||||
status_line_invalid_items_warned: self.status_line_invalid_items_warned.clone(),
|
||||
otel_manager: self.otel_manager.clone(),
|
||||
}
|
||||
@@ -1194,6 +1262,7 @@ impl App {
|
||||
is_first_run: false,
|
||||
feedback_audience: self.feedback_audience,
|
||||
model: Some(model),
|
||||
startup_tooltip_override: None,
|
||||
status_line_invalid_items_warned: self.status_line_invalid_items_warned.clone(),
|
||||
otel_manager: self.otel_manager.clone(),
|
||||
};
|
||||
@@ -1340,7 +1409,7 @@ impl App {
|
||||
&mut config,
|
||||
model.as_str(),
|
||||
&app_event_tx,
|
||||
available_models,
|
||||
&available_models,
|
||||
)
|
||||
.await;
|
||||
if let Some(exit_info) = exit_info {
|
||||
@@ -1349,7 +1418,6 @@ impl App {
|
||||
if let Some(updated_model) = config.model.clone() {
|
||||
model = updated_model;
|
||||
}
|
||||
|
||||
let auth = auth_manager.auth().await;
|
||||
let auth_ref = auth.as_ref();
|
||||
// Determine who should see internal Slack routing. We treat
|
||||
@@ -1393,6 +1461,9 @@ impl App {
|
||||
Self::should_wait_for_initial_session(&session_selection);
|
||||
let mut chat_widget = match session_selection {
|
||||
SessionSelection::StartFresh | SessionSelection::Exit => {
|
||||
let startup_tooltip_override =
|
||||
prepare_startup_tooltip_override(&mut config, &available_models, is_first_run)
|
||||
.await;
|
||||
let init = crate::chatwidget::ChatWidgetInit {
|
||||
config: config.clone(),
|
||||
frame_requester: tui.frame_requester(),
|
||||
@@ -1410,6 +1481,7 @@ impl App {
|
||||
is_first_run,
|
||||
feedback_audience,
|
||||
model: Some(model.clone()),
|
||||
startup_tooltip_override,
|
||||
status_line_invalid_items_warned: status_line_invalid_items_warned.clone(),
|
||||
otel_manager: otel_manager.clone(),
|
||||
};
|
||||
@@ -1444,6 +1516,7 @@ impl App {
|
||||
is_first_run,
|
||||
feedback_audience,
|
||||
model: config.model.clone(),
|
||||
startup_tooltip_override: None,
|
||||
status_line_invalid_items_warned: status_line_invalid_items_warned.clone(),
|
||||
otel_manager: otel_manager.clone(),
|
||||
};
|
||||
@@ -1480,6 +1553,7 @@ impl App {
|
||||
is_first_run,
|
||||
feedback_audience,
|
||||
model: config.model.clone(),
|
||||
startup_tooltip_override: None,
|
||||
status_line_invalid_items_warned: status_line_invalid_items_warned.clone(),
|
||||
otel_manager: otel_manager.clone(),
|
||||
};
|
||||
@@ -3413,8 +3487,10 @@ mod tests {
|
||||
use codex_core::CodexAuth;
|
||||
use codex_core::config::ConfigBuilder;
|
||||
use codex_core::config::ConfigOverrides;
|
||||
use codex_core::config::types::ModelAvailabilityNuxConfig;
|
||||
use codex_otel::OtelManager;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::openai_models::ModelAvailabilityNux;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::Event;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
@@ -3866,6 +3942,7 @@ mod tests {
|
||||
event,
|
||||
is_first,
|
||||
None,
|
||||
None,
|
||||
)) as Arc<dyn HistoryCell>
|
||||
};
|
||||
|
||||
@@ -4068,6 +4145,15 @@ mod tests {
|
||||
codex_core::test_support::all_model_presets().clone()
|
||||
}
|
||||
|
||||
fn model_availability_nux_config(shown_count: &[(&str, u32)]) -> ModelAvailabilityNuxConfig {
|
||||
ModelAvailabilityNuxConfig {
|
||||
shown_count: shown_count
|
||||
.iter()
|
||||
.map(|(model, count)| ((*model).to_string(), *count))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn model_migration_copy_to_plain_text(
|
||||
copy: &crate::model_migration::ModelMigrationCopy,
|
||||
) -> String {
|
||||
@@ -4124,6 +4210,120 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_model_availability_nux_picks_only_eligible_model() {
|
||||
let mut presets = all_model_presets();
|
||||
presets.iter_mut().for_each(|preset| {
|
||||
preset.availability_nux = None;
|
||||
});
|
||||
let target = presets
|
||||
.iter_mut()
|
||||
.find(|preset| preset.model == "gpt-5")
|
||||
.expect("target preset present");
|
||||
target.availability_nux = Some(ModelAvailabilityNux {
|
||||
message: "gpt-5 is available".to_string(),
|
||||
});
|
||||
|
||||
let selected = select_model_availability_nux(&presets, &model_availability_nux_config(&[]));
|
||||
|
||||
assert_eq!(
|
||||
selected,
|
||||
Some(StartupTooltipOverride {
|
||||
model_slug: "gpt-5".to_string(),
|
||||
message: "gpt-5 is available".to_string(),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_model_availability_nux_skips_missing_and_exhausted_models() {
|
||||
let mut presets = all_model_presets();
|
||||
presets.iter_mut().for_each(|preset| {
|
||||
preset.availability_nux = None;
|
||||
});
|
||||
let gpt_5 = presets
|
||||
.iter_mut()
|
||||
.find(|preset| preset.model == "gpt-5")
|
||||
.expect("gpt-5 preset present");
|
||||
gpt_5.availability_nux = Some(ModelAvailabilityNux {
|
||||
message: "gpt-5 is available".to_string(),
|
||||
});
|
||||
let gpt_5_2 = presets
|
||||
.iter_mut()
|
||||
.find(|preset| preset.model == "gpt-5.2")
|
||||
.expect("gpt-5.2 preset present");
|
||||
gpt_5_2.availability_nux = Some(ModelAvailabilityNux {
|
||||
message: "gpt-5.2 is available".to_string(),
|
||||
});
|
||||
|
||||
let selected = select_model_availability_nux(
|
||||
&presets,
|
||||
&model_availability_nux_config(&[("gpt-5", MODEL_AVAILABILITY_NUX_MAX_SHOW_COUNT)]),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
selected,
|
||||
Some(StartupTooltipOverride {
|
||||
model_slug: "gpt-5.2".to_string(),
|
||||
message: "gpt-5.2 is available".to_string(),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_model_availability_nux_uses_existing_model_order_as_priority() {
|
||||
let mut presets = all_model_presets();
|
||||
presets.iter_mut().for_each(|preset| {
|
||||
preset.availability_nux = None;
|
||||
});
|
||||
let first = presets
|
||||
.iter_mut()
|
||||
.find(|preset| preset.model == "gpt-5")
|
||||
.expect("gpt-5 preset present");
|
||||
first.availability_nux = Some(ModelAvailabilityNux {
|
||||
message: "first".to_string(),
|
||||
});
|
||||
let second = presets
|
||||
.iter_mut()
|
||||
.find(|preset| preset.model == "gpt-5.2")
|
||||
.expect("gpt-5.2 preset present");
|
||||
second.availability_nux = Some(ModelAvailabilityNux {
|
||||
message: "second".to_string(),
|
||||
});
|
||||
|
||||
let selected = select_model_availability_nux(&presets, &model_availability_nux_config(&[]));
|
||||
|
||||
assert_eq!(
|
||||
selected,
|
||||
Some(StartupTooltipOverride {
|
||||
model_slug: "gpt-5.2".to_string(),
|
||||
message: "second".to_string(),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_model_availability_nux_returns_none_when_all_models_are_exhausted() {
|
||||
let mut presets = all_model_presets();
|
||||
presets.iter_mut().for_each(|preset| {
|
||||
preset.availability_nux = None;
|
||||
});
|
||||
let target = presets
|
||||
.iter_mut()
|
||||
.find(|preset| preset.model == "gpt-5")
|
||||
.expect("target preset present");
|
||||
target.availability_nux = Some(ModelAvailabilityNux {
|
||||
message: "gpt-5 is available".to_string(),
|
||||
});
|
||||
|
||||
let selected = select_model_availability_nux(
|
||||
&presets,
|
||||
&model_availability_nux_config(&[("gpt-5", MODEL_AVAILABILITY_NUX_MAX_SHOW_COUNT)]),
|
||||
);
|
||||
|
||||
assert_eq!(selected, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn model_migration_prompt_respects_hide_flag_and_self_target() {
|
||||
let mut seen = BTreeMap::new();
|
||||
@@ -4363,6 +4563,7 @@ mod tests {
|
||||
event,
|
||||
is_first,
|
||||
None,
|
||||
None,
|
||||
)) as Arc<dyn HistoryCell>
|
||||
};
|
||||
|
||||
|
||||
@@ -462,6 +462,7 @@ pub(crate) struct ChatWidgetInit {
|
||||
pub(crate) is_first_run: bool,
|
||||
pub(crate) feedback_audience: FeedbackAudience,
|
||||
pub(crate) model: Option<String>,
|
||||
pub(crate) startup_tooltip_override: Option<String>,
|
||||
// Shared latch so we only warn once about invalid status-line item IDs.
|
||||
pub(crate) status_line_invalid_items_warned: Arc<AtomicBool>,
|
||||
pub(crate) otel_manager: OtelManager,
|
||||
@@ -603,6 +604,8 @@ pub(crate) struct ChatWidget {
|
||||
frame_requester: FrameRequester,
|
||||
// Whether to include the initial welcome banner on session configured
|
||||
show_welcome_banner: bool,
|
||||
// One-shot tooltip override for the primary startup session.
|
||||
startup_tooltip_override: Option<String>,
|
||||
// When resuming an existing session (selected via resume picker), avoid an
|
||||
// immediate redraw on SessionConfigured to prevent a gratuitous UI flicker.
|
||||
suppress_session_configured_redraw: bool,
|
||||
@@ -1144,11 +1147,13 @@ impl ChatWidget {
|
||||
);
|
||||
self.refresh_model_display();
|
||||
self.sync_personality_command_enabled();
|
||||
let startup_tooltip_override = self.startup_tooltip_override.take();
|
||||
let session_info_cell = history_cell::new_session_info(
|
||||
&self.config,
|
||||
&model_for_header,
|
||||
event,
|
||||
self.show_welcome_banner,
|
||||
startup_tooltip_override,
|
||||
self.auth_manager
|
||||
.auth_cached()
|
||||
.and_then(|auth| auth.account_plan_type()),
|
||||
@@ -2755,6 +2760,7 @@ impl ChatWidget {
|
||||
is_first_run,
|
||||
feedback_audience,
|
||||
model,
|
||||
startup_tooltip_override,
|
||||
status_line_invalid_items_warned,
|
||||
otel_manager,
|
||||
} = common;
|
||||
@@ -2852,6 +2858,7 @@ impl ChatWidget {
|
||||
queued_user_messages: VecDeque::new(),
|
||||
queued_message_edit_binding,
|
||||
show_welcome_banner: is_first_run,
|
||||
startup_tooltip_override,
|
||||
suppress_session_configured_redraw: false,
|
||||
pending_notification: None,
|
||||
quit_shortcut_expires_at: None,
|
||||
@@ -2933,6 +2940,7 @@ impl ChatWidget {
|
||||
is_first_run,
|
||||
feedback_audience,
|
||||
model,
|
||||
startup_tooltip_override,
|
||||
status_line_invalid_items_warned,
|
||||
otel_manager,
|
||||
} = common;
|
||||
@@ -3033,6 +3041,7 @@ impl ChatWidget {
|
||||
queued_user_messages: VecDeque::new(),
|
||||
queued_message_edit_binding,
|
||||
show_welcome_banner: is_first_run,
|
||||
startup_tooltip_override,
|
||||
suppress_session_configured_redraw: false,
|
||||
pending_notification: None,
|
||||
quit_shortcut_expires_at: None,
|
||||
@@ -3099,6 +3108,7 @@ impl ChatWidget {
|
||||
is_first_run: _,
|
||||
feedback_audience,
|
||||
model,
|
||||
startup_tooltip_override: _,
|
||||
status_line_invalid_items_warned,
|
||||
otel_manager,
|
||||
} = common;
|
||||
@@ -3195,6 +3205,7 @@ impl ChatWidget {
|
||||
queued_user_messages: VecDeque::new(),
|
||||
queued_message_edit_binding,
|
||||
show_welcome_banner: false,
|
||||
startup_tooltip_override: None,
|
||||
suppress_session_configured_redraw: true,
|
||||
pending_notification: None,
|
||||
quit_shortcut_expires_at: None,
|
||||
|
||||
@@ -1581,6 +1581,7 @@ async fn helpers_are_available_and_do_not_panic() {
|
||||
is_first_run: true,
|
||||
feedback_audience: FeedbackAudience::External,
|
||||
model: Some(resolved_model),
|
||||
startup_tooltip_override: None,
|
||||
status_line_invalid_items_warned: Arc::new(AtomicBool::new(false)),
|
||||
otel_manager,
|
||||
};
|
||||
@@ -1705,6 +1706,7 @@ async fn make_chatwidget_manual(
|
||||
forked_from: None,
|
||||
frame_requester: FrameRequester::test_dummy(),
|
||||
show_welcome_banner: true,
|
||||
startup_tooltip_override: None,
|
||||
queued_user_messages: VecDeque::new(),
|
||||
queued_message_edit_binding: crate::key_hint::alt(KeyCode::Up),
|
||||
suppress_session_configured_redraw: false,
|
||||
@@ -4488,6 +4490,7 @@ async fn collaboration_modes_defaults_to_code_on_startup() {
|
||||
is_first_run: true,
|
||||
feedback_audience: FeedbackAudience::External,
|
||||
model: Some(resolved_model.clone()),
|
||||
startup_tooltip_override: None,
|
||||
status_line_invalid_items_warned: Arc::new(AtomicBool::new(false)),
|
||||
otel_manager,
|
||||
};
|
||||
@@ -4537,6 +4540,7 @@ async fn experimental_mode_plan_is_ignored_on_startup() {
|
||||
is_first_run: true,
|
||||
feedback_audience: FeedbackAudience::External,
|
||||
model: Some(resolved_model.clone()),
|
||||
startup_tooltip_override: None,
|
||||
status_line_invalid_items_warned: Arc::new(AtomicBool::new(false)),
|
||||
otel_manager,
|
||||
};
|
||||
|
||||
@@ -1041,6 +1041,7 @@ pub(crate) fn new_session_info(
|
||||
requested_model: &str,
|
||||
event: SessionConfiguredEvent,
|
||||
is_first_event: bool,
|
||||
tooltip_override: Option<String>,
|
||||
auth_plan: Option<PlanType>,
|
||||
) -> SessionInfoCell {
|
||||
let SessionConfiguredEvent {
|
||||
@@ -1094,7 +1095,9 @@ pub(crate) fn new_session_info(
|
||||
parts.push(Box::new(PlainHistoryCell { lines: help_lines }));
|
||||
} else {
|
||||
if config.show_tooltips
|
||||
&& let Some(tooltips) = tooltips::get_tooltip(auth_plan).map(TooltipHistoryCell::new)
|
||||
&& let Some(tooltips) = tooltip_override
|
||||
.or_else(|| tooltips::get_tooltip(auth_plan))
|
||||
.map(TooltipHistoryCell::new)
|
||||
{
|
||||
parts.push(Box::new(tooltips));
|
||||
}
|
||||
@@ -2396,13 +2399,19 @@ mod tests {
|
||||
use codex_core::config::types::McpServerTransportConfig;
|
||||
use codex_otel::RuntimeMetricTotals;
|
||||
use codex_otel::RuntimeMetricsSummary;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::account::PlanType;
|
||||
use codex_protocol::models::WebSearchAction;
|
||||
use codex_protocol::parse_command::ParsedCommand;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::McpAuthStatus;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::protocol::SessionConfiguredEvent;
|
||||
use dirs::home_dir;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use codex_protocol::mcp::CallToolResult;
|
||||
use codex_protocol::mcp::Tool;
|
||||
@@ -2463,6 +2472,25 @@ mod tests {
|
||||
.expect("resource link content should serialize")
|
||||
}
|
||||
|
||||
fn session_configured_event(model: &str) -> SessionConfiguredEvent {
|
||||
SessionConfiguredEvent {
|
||||
session_id: ThreadId::new(),
|
||||
forked_from_id: None,
|
||||
thread_name: None,
|
||||
model: model.to_string(),
|
||||
model_provider_id: "test-provider".to_string(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
cwd: PathBuf::from("/tmp/project"),
|
||||
reasoning_effort: None,
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
initial_messages: None,
|
||||
network_proxy: None,
|
||||
rollout_path: Some(PathBuf::new()),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unified_exec_interaction_cell_renders_input() {
|
||||
let cell =
|
||||
@@ -2547,6 +2575,73 @@ mod tests {
|
||||
insta::assert_snapshot!(rendered);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_info_uses_availability_nux_tooltip_override() {
|
||||
let config = test_config().await;
|
||||
let cell = new_session_info(
|
||||
&config,
|
||||
"gpt-5",
|
||||
session_configured_event("gpt-5"),
|
||||
false,
|
||||
Some("Model just became available".to_string()),
|
||||
Some(PlanType::Free),
|
||||
);
|
||||
|
||||
let rendered = render_transcript(&cell).join("\n");
|
||||
assert!(rendered.contains("Model just became available"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_info_availability_nux_tooltip_snapshot() {
|
||||
let mut config = test_config().await;
|
||||
config.cwd = PathBuf::from("/tmp/project");
|
||||
let cell = new_session_info(
|
||||
&config,
|
||||
"gpt-5",
|
||||
session_configured_event("gpt-5"),
|
||||
false,
|
||||
Some("Model just became available".to_string()),
|
||||
Some(PlanType::Free),
|
||||
);
|
||||
|
||||
let rendered = render_transcript(&cell).join("\n");
|
||||
insta::assert_snapshot!(rendered);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_info_first_event_suppresses_tooltips_and_nux() {
|
||||
let config = test_config().await;
|
||||
let cell = new_session_info(
|
||||
&config,
|
||||
"gpt-5",
|
||||
session_configured_event("gpt-5"),
|
||||
true,
|
||||
Some("Model just became available".to_string()),
|
||||
Some(PlanType::Free),
|
||||
);
|
||||
|
||||
let rendered = render_transcript(&cell).join("\n");
|
||||
assert!(!rendered.contains("Model just became available"));
|
||||
assert!(rendered.contains("To get started"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_info_hides_tooltips_when_disabled() {
|
||||
let mut config = test_config().await;
|
||||
config.show_tooltips = false;
|
||||
let cell = new_session_info(
|
||||
&config,
|
||||
"gpt-5",
|
||||
session_configured_event("gpt-5"),
|
||||
false,
|
||||
Some("Model just became available".to_string()),
|
||||
Some(PlanType::Free),
|
||||
);
|
||||
|
||||
let rendered = render_transcript(&cell).join("\n");
|
||||
assert!(!rendered.contains("Model just became available"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ps_output_multiline_snapshot() {
|
||||
let cell = new_unified_exec_processes_output(vec![
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
---
|
||||
source: tui/src/history_cell.rs
|
||||
assertion_line: 2608
|
||||
expression: rendered
|
||||
---
|
||||
╭─────────────────────────────────────╮
|
||||
│ >_ OpenAI Codex (v0.0.0) │
|
||||
│ │
|
||||
│ model: gpt-5 /model to change │
|
||||
│ directory: /tmp/project │
|
||||
╰─────────────────────────────────────╯
|
||||
|
||||
Tip: Model just became available
|
||||
Reference in New Issue
Block a user