mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Speed up TUI startup by reusing plugin discovery (#26469)
## Summary TUI startup loads related plugin data from `hooks/list`, session MCP initialization, and plugin skill warmup. These paths repeated filesystem discovery and emitted the same plugin warnings, while `hooks/list` and account/model bootstrap ran serially. This change: - Reuses one immutable plugin load outcome across startup consumers. - Keys the cache only on plugin-relevant configuration. - Single-flights concurrent plugin loads and prevents invalidated loads from repopulating the cache. - Runs hook discovery and account/model bootstrap concurrently. - Preserves configuration-migration ordering, hook review behavior, and accurate startup telemetry. In 10 alternating release-build launches in the Ruff repository with the existing `~/.codex` configuration, median time to the first editable composer decreased from 833ms to 504ms. The branch was faster in 9 of 10 pairs, with a paired median improvement of 312ms.
This commit is contained in:
committed by
GitHub
Unverified
parent
345cf6e8d0
commit
055c7a7c53
@@ -647,9 +647,11 @@ impl CatalogRequestProcessor {
|
||||
config.features.enabled(Feature::Plugins) && workspace_codex_plugins_enabled;
|
||||
let plugin_hooks = if plugins_enabled {
|
||||
let plugins_input = config.plugins_config_input();
|
||||
plugins_manager
|
||||
.plugin_hooks_for_layer_stack(&config.config_layer_stack, &plugins_input)
|
||||
.await
|
||||
let plugin_outcome = plugins_manager.plugins_for_config(&plugins_input).await;
|
||||
codex_core_plugins::PluginHookLoadOutcome {
|
||||
hook_sources: plugin_outcome.effective_plugin_hook_sources(),
|
||||
hook_load_warnings: plugin_outcome.effective_plugin_hook_warnings(),
|
||||
}
|
||||
} else {
|
||||
codex_core_plugins::PluginHookLoadOutcome::default()
|
||||
};
|
||||
|
||||
@@ -264,6 +264,84 @@ async fn hooks_list_shows_discovered_plugin_hook() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hooks_list_warms_plugin_capabilities_for_thread_start() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
write_plugin_hook_config(
|
||||
codex_home.path(),
|
||||
r#"{
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "echo plugin hook"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}"#,
|
||||
)?;
|
||||
let plugin_mcp_path = codex_home
|
||||
.path()
|
||||
.join("plugins/cache/test/demo/local/.mcp.json");
|
||||
std::fs::write(
|
||||
&plugin_mcp_path,
|
||||
r#"{
|
||||
"mcpServers": {
|
||||
"plugin-server": {
|
||||
"url": "http://127.0.0.1:1/mcp"
|
||||
}
|
||||
}
|
||||
}"#,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let hooks_list_id = mcp
|
||||
.send_hooks_list_request(HooksListParams {
|
||||
cwds: vec![cwd.path().to_path_buf()],
|
||||
})
|
||||
.await?;
|
||||
timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(hooks_list_id)),
|
||||
)
|
||||
.await??;
|
||||
|
||||
std::fs::remove_file(plugin_mcp_path)?;
|
||||
|
||||
let thread_start_id = mcp
|
||||
.send_thread_start_request(ThreadStartParams::default())
|
||||
.await?;
|
||||
let _: ThreadStartResponse = to_response(
|
||||
timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(thread_start_id)),
|
||||
)
|
||||
.await??,
|
||||
)?;
|
||||
timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_matching_notification("plugin MCP server starting", |notification| {
|
||||
notification.method == "mcpServer/startupStatus/updated"
|
||||
&& notification
|
||||
.params
|
||||
.as_ref()
|
||||
.and_then(|params| params.get("name"))
|
||||
.and_then(serde_json::Value::as_str)
|
||||
== Some("plugin-server")
|
||||
}),
|
||||
)
|
||||
.await??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hooks_list_shows_plugin_hook_load_warnings() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
@@ -57,8 +57,9 @@ use codex_config::apply_user_plugin_config_edits;
|
||||
use codex_config::clear_user_plugin;
|
||||
use codex_config::set_user_plugin_enabled;
|
||||
use codex_config::types::PluginConfig;
|
||||
use codex_config::version_for_toml;
|
||||
use codex_core_skills::SkillMetadata;
|
||||
use codex_core_skills::config_rules::SkillConfigRules;
|
||||
use codex_core_skills::config_rules::skill_config_rules_from_stack;
|
||||
use codex_hooks::plugin_hook_declarations;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::CodexAuth;
|
||||
@@ -403,7 +404,8 @@ pub struct PluginsManager {
|
||||
featured_plugin_ids_cache: RwLock<Option<CachedFeaturedPluginIds>>,
|
||||
configured_marketplace_upgrade_state: RwLock<ConfiguredMarketplaceUpgradeState>,
|
||||
non_curated_cache_refresh_state: RwLock<NonCuratedCacheRefreshState>,
|
||||
cached_enabled_outcome: RwLock<Option<CachedPluginLoadOutcome>>,
|
||||
enabled_outcome_cache: RwLock<EnabledOutcomeCache>,
|
||||
enabled_outcome_load_semaphore: Semaphore,
|
||||
remote_installed_plugins_cache: RwLock<Option<Vec<RemoteInstalledPlugin>>>,
|
||||
remote_installed_plugins_cache_refresh_state: RwLock<RemoteInstalledPluginsCacheRefreshState>,
|
||||
remote_sync_lock: Semaphore,
|
||||
@@ -413,10 +415,23 @@ pub struct PluginsManager {
|
||||
|
||||
#[derive(Clone)]
|
||||
struct CachedPluginLoadOutcome {
|
||||
config_version: String,
|
||||
key: PluginLoadCacheKey,
|
||||
outcome: PluginLoadOutcome,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct EnabledOutcomeCache {
|
||||
generation: u64,
|
||||
outcome: Option<CachedPluginLoadOutcome>,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
struct PluginLoadCacheKey {
|
||||
configured_plugins: HashMap<String, PluginConfig>,
|
||||
skill_config_rules: SkillConfigRules,
|
||||
remote_plugin_enabled: bool,
|
||||
}
|
||||
|
||||
impl PluginsManager {
|
||||
pub fn new(codex_home: PathBuf) -> Self {
|
||||
Self::new_with_restriction_product(codex_home, Some(Product::Codex))
|
||||
@@ -441,7 +456,8 @@ impl PluginsManager {
|
||||
ConfiguredMarketplaceUpgradeState::default(),
|
||||
),
|
||||
non_curated_cache_refresh_state: RwLock::new(NonCuratedCacheRefreshState::default()),
|
||||
cached_enabled_outcome: RwLock::new(None),
|
||||
enabled_outcome_cache: RwLock::new(EnabledOutcomeCache::default()),
|
||||
enabled_outcome_load_semaphore: Semaphore::new(/*permits*/ 1),
|
||||
remote_installed_plugins_cache: RwLock::new(None),
|
||||
remote_installed_plugins_cache_refresh_state: RwLock::new(
|
||||
RemoteInstalledPluginsCacheRefreshState::default(),
|
||||
@@ -484,11 +500,23 @@ impl PluginsManager {
|
||||
return PluginLoadOutcome::default();
|
||||
}
|
||||
|
||||
let config_version = version_for_toml(&config.config_layer_stack.effective_config());
|
||||
if !force_reload && let Some(outcome) = self.cached_enabled_outcome(&config_version) {
|
||||
let cache_key = PluginLoadCacheKey {
|
||||
configured_plugins: configured_plugins_from_stack(&config.config_layer_stack),
|
||||
skill_config_rules: skill_config_rules_from_stack(&config.config_layer_stack),
|
||||
remote_plugin_enabled: config.remote_plugin_enabled,
|
||||
};
|
||||
if !force_reload && let Some(outcome) = self.cached_enabled_outcome(&cache_key) {
|
||||
return outcome;
|
||||
}
|
||||
|
||||
let Ok(_load_permit) = self.enabled_outcome_load_semaphore.acquire().await else {
|
||||
warn!("plugin load semaphore closed");
|
||||
return PluginLoadOutcome::default();
|
||||
};
|
||||
if !force_reload && let Some(outcome) = self.cached_enabled_outcome(&cache_key) {
|
||||
return outcome;
|
||||
}
|
||||
let cache_generation = self.enabled_outcome_cache_generation();
|
||||
let outcome = load_plugins_from_layer_stack(
|
||||
&config.config_layer_stack,
|
||||
self.remote_installed_plugin_configs(),
|
||||
@@ -498,14 +526,7 @@ impl PluginsManager {
|
||||
)
|
||||
.await;
|
||||
log_plugin_load_errors(&outcome);
|
||||
let mut cache = match self.cached_enabled_outcome.write() {
|
||||
Ok(cache) => cache,
|
||||
Err(err) => err.into_inner(),
|
||||
};
|
||||
*cache = Some(CachedPluginLoadOutcome {
|
||||
config_version,
|
||||
outcome: outcome.clone(),
|
||||
});
|
||||
self.cache_enabled_outcome_if_current(cache_generation, cache_key, outcome.clone());
|
||||
outcome
|
||||
}
|
||||
|
||||
@@ -519,11 +540,12 @@ impl PluginsManager {
|
||||
}
|
||||
|
||||
fn clear_enabled_outcome_cache(&self) {
|
||||
let mut cached_enabled_outcome = match self.cached_enabled_outcome.write() {
|
||||
let mut cache = match self.enabled_outcome_cache.write() {
|
||||
Ok(cache) => cache,
|
||||
Err(err) => err.into_inner(),
|
||||
};
|
||||
*cached_enabled_outcome = None;
|
||||
cache.generation = cache.generation.wrapping_add(1);
|
||||
cache.outcome = None;
|
||||
}
|
||||
|
||||
/// Load plugins for a config layer stack without touching the plugins cache.
|
||||
@@ -574,20 +596,44 @@ impl PluginsManager {
|
||||
.effective_plugin_skill_roots()
|
||||
}
|
||||
|
||||
fn cached_enabled_outcome(&self, config_version: &str) -> Option<PluginLoadOutcome> {
|
||||
match self.cached_enabled_outcome.read() {
|
||||
fn cached_enabled_outcome(&self, key: &PluginLoadCacheKey) -> Option<PluginLoadOutcome> {
|
||||
match self.enabled_outcome_cache.read() {
|
||||
Ok(cache) => cache
|
||||
.outcome
|
||||
.as_ref()
|
||||
.filter(|cached| cached.config_version == config_version)
|
||||
.filter(|cached| cached.key == *key)
|
||||
.map(|cached| cached.outcome.clone()),
|
||||
Err(err) => err
|
||||
.into_inner()
|
||||
.outcome
|
||||
.as_ref()
|
||||
.filter(|cached| cached.config_version == config_version)
|
||||
.filter(|cached| cached.key == *key)
|
||||
.map(|cached| cached.outcome.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn enabled_outcome_cache_generation(&self) -> u64 {
|
||||
match self.enabled_outcome_cache.read() {
|
||||
Ok(cache) => cache.generation,
|
||||
Err(err) => err.into_inner().generation,
|
||||
}
|
||||
}
|
||||
|
||||
fn cache_enabled_outcome_if_current(
|
||||
&self,
|
||||
generation: u64,
|
||||
key: PluginLoadCacheKey,
|
||||
outcome: PluginLoadOutcome,
|
||||
) {
|
||||
let mut cache = match self.enabled_outcome_cache.write() {
|
||||
Ok(cache) => cache,
|
||||
Err(err) => err.into_inner(),
|
||||
};
|
||||
if cache.generation == generation {
|
||||
cache.outcome = Some(CachedPluginLoadOutcome { key, outcome });
|
||||
}
|
||||
}
|
||||
|
||||
fn remote_installed_plugin_configs(&self) -> HashMap<String, PluginConfig> {
|
||||
let cache = match self.remote_installed_plugins_cache.read() {
|
||||
Ok(cache) => cache,
|
||||
|
||||
@@ -1300,6 +1300,97 @@ async fn load_plugins_returns_empty_when_feature_disabled() {
|
||||
assert_eq!(outcome, PluginLoadOutcome::default());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_cache_ignores_unrelated_session_overrides() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
let plugin_root = codex_home
|
||||
.path()
|
||||
.join("plugins/cache")
|
||||
.join("test/sample/local");
|
||||
write_plugin(
|
||||
codex_home.path().join("plugins/cache/test").as_path(),
|
||||
"sample/local",
|
||||
"sample",
|
||||
);
|
||||
write_file(
|
||||
&plugin_root.join(".mcp.json"),
|
||||
r#"{
|
||||
"mcpServers": {
|
||||
"sample": {
|
||||
"url": "https://sample.example/mcp"
|
||||
}
|
||||
}
|
||||
}"#,
|
||||
);
|
||||
|
||||
let user_file = codex_home.path().join(CONFIG_TOML_FILE).abs();
|
||||
let user_config: toml::Value = toml::from_str(&plugin_config_toml(
|
||||
/*enabled*/ true, /*plugins_feature_enabled*/ true,
|
||||
))
|
||||
.expect("user config should parse");
|
||||
let stack = |session_config: &str| {
|
||||
ConfigLayerStack::new(
|
||||
vec![
|
||||
ConfigLayerEntry::new(
|
||||
ConfigLayerSource::User {
|
||||
file: user_file.clone(),
|
||||
profile: None,
|
||||
},
|
||||
user_config.clone(),
|
||||
),
|
||||
ConfigLayerEntry::new(
|
||||
ConfigLayerSource::SessionFlags,
|
||||
toml::from_str(session_config).expect("session config should parse"),
|
||||
),
|
||||
],
|
||||
ConfigRequirements::default(),
|
||||
ConfigRequirementsToml::default(),
|
||||
)
|
||||
.expect("config layer stack should build")
|
||||
};
|
||||
let config = |session_config| {
|
||||
PluginsConfigInput::new(
|
||||
stack(session_config),
|
||||
/*plugins_enabled*/ true,
|
||||
/*remote_plugin_enabled*/ false,
|
||||
"https://chatgpt.com".to_string(),
|
||||
)
|
||||
};
|
||||
let manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
|
||||
let first = manager
|
||||
.plugins_for_config(&config(r#"model = "first""#))
|
||||
.await;
|
||||
std::fs::remove_file(plugin_root.join(".mcp.json")).unwrap();
|
||||
let second = manager
|
||||
.plugins_for_config(&config(r#"model = "second""#))
|
||||
.await;
|
||||
|
||||
assert_eq!(second, first);
|
||||
assert_eq!(second.plugins()[0].mcp_servers.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_cache_invalidation_rejects_stale_load_completion() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
let manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
let cache_key = PluginLoadCacheKey {
|
||||
configured_plugins: HashMap::new(),
|
||||
skill_config_rules: SkillConfigRules::default(),
|
||||
remote_plugin_enabled: false,
|
||||
};
|
||||
let stale_generation = manager.enabled_outcome_cache_generation();
|
||||
|
||||
manager.clear_enabled_outcome_cache();
|
||||
manager.cache_enabled_outcome_if_current(
|
||||
stale_generation,
|
||||
cache_key.clone(),
|
||||
PluginLoadOutcome::default(),
|
||||
);
|
||||
|
||||
assert_eq!(manager.cached_enabled_outcome(&cache_key), None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_plugins_rejects_invalid_plugin_keys() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
|
||||
@@ -16,6 +16,7 @@ use crate::app_event::RealtimeAudioDeviceKind;
|
||||
#[cfg(target_os = "windows")]
|
||||
use crate::app_event::WindowsSandboxEnableMode;
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
use crate::app_server_session::AppServerBootstrap;
|
||||
use crate::app_server_session::AppServerSession;
|
||||
use crate::app_server_session::AppServerStartedThread;
|
||||
use crate::app_server_session::TurnPermissionsOverride;
|
||||
@@ -727,6 +728,8 @@ impl App {
|
||||
app_server_target: AppServerTarget,
|
||||
state_db: Option<StateDbHandle>,
|
||||
environment_manager: Arc<EnvironmentManager>,
|
||||
startup_elapsed_before_app: Duration,
|
||||
startup_bootstrap: Option<AppServerBootstrap>,
|
||||
startup_hooks_browser: Option<HooksListEntry>,
|
||||
) -> Result<AppExitInfo> {
|
||||
use tokio_stream::StreamExt;
|
||||
@@ -774,9 +777,11 @@ impl App {
|
||||
});
|
||||
}
|
||||
};
|
||||
let bootstrap_started_at = Instant::now();
|
||||
let bootstrap = app_server.bootstrap(&config).await?;
|
||||
let bootstrap_ms = bootstrap_started_at.elapsed().as_millis();
|
||||
let bootstrap = match startup_bootstrap {
|
||||
Some(bootstrap) => bootstrap,
|
||||
None => app_server.bootstrap(&config).await?,
|
||||
};
|
||||
let bootstrap_ms = bootstrap.duration.as_millis();
|
||||
let mut model = bootstrap.default_model;
|
||||
let available_models = bootstrap.available_models;
|
||||
let remote_connection = crate::status::remote_connection::remote_connection_status_value(
|
||||
@@ -1085,7 +1090,7 @@ See the Codex keymap documentation for supported actions and examples."
|
||||
|
||||
tui.frame_requester().schedule_frame();
|
||||
tracing::info!(
|
||||
duration_ms = %startup_started_at.elapsed().as_millis(),
|
||||
duration_ms = %(startup_elapsed_before_app + startup_started_at.elapsed()).as_millis(),
|
||||
bootstrap_ms = %bootstrap_ms,
|
||||
runtime_model_provider_ms = %runtime_model_provider_ms,
|
||||
thread_and_widget_ms = %thread_and_widget_ms,
|
||||
|
||||
@@ -127,6 +127,8 @@ use color_eyre::eyre::Result;
|
||||
use color_eyre::eyre::WrapErr;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use uuid::Uuid;
|
||||
|
||||
const JSONRPC_INVALID_REQUEST: i64 = -32600;
|
||||
@@ -150,6 +152,7 @@ fn is_thread_settings_update_unsupported(source: &JSONRPCErrorError) -> bool {
|
||||
/// fetched asynchronously after bootstrap returns so that the TUI can render
|
||||
/// its first frame without waiting for the rate-limit round-trip.
|
||||
pub(crate) struct AppServerBootstrap {
|
||||
pub(crate) duration: Duration,
|
||||
pub(crate) account_email: Option<String>,
|
||||
pub(crate) auth_mode: Option<TelemetryAuthMode>,
|
||||
pub(crate) status_account_display: Option<StatusAccountDisplay>,
|
||||
@@ -239,6 +242,7 @@ impl AppServerSession {
|
||||
}
|
||||
|
||||
pub(crate) async fn bootstrap(&mut self, config: &Config) -> Result<AppServerBootstrap> {
|
||||
let started_at = Instant::now();
|
||||
let account = self.read_account().await?;
|
||||
let model_request_id = self.next_request_id();
|
||||
let models: ModelListResponse = self
|
||||
@@ -314,6 +318,7 @@ impl AppServerSession {
|
||||
None => (None, None, None, None, FeedbackAudience::External, false),
|
||||
};
|
||||
Ok(AppServerBootstrap {
|
||||
duration: started_at.elapsed(),
|
||||
account_email,
|
||||
auth_mode,
|
||||
status_account_display,
|
||||
|
||||
@@ -25,7 +25,7 @@ pub(crate) enum ExternalAgentConfigMigrationStartupOutcome {
|
||||
ExitRequested,
|
||||
}
|
||||
|
||||
fn should_show_external_agent_config_migration_prompt(
|
||||
pub(crate) fn should_show_external_agent_config_migration_prompt(
|
||||
config: &Config,
|
||||
entered_trust_nux: bool,
|
||||
) -> bool {
|
||||
|
||||
@@ -71,6 +71,7 @@ use std::fs::OpenOptions;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
pub use token_usage::TokenUsage;
|
||||
use tracing::Level;
|
||||
use tracing::error;
|
||||
@@ -276,6 +277,7 @@ pub(crate) mod test_support;
|
||||
use crate::onboarding::onboarding_screen::OnboardingScreenArgs;
|
||||
use crate::onboarding::onboarding_screen::run_onboarding_app;
|
||||
use crate::startup_hooks_review::StartupHooksReviewOutcome;
|
||||
use crate::startup_hooks_review::load_startup_hooks_review_entry;
|
||||
use crate::startup_hooks_review::maybe_run_startup_hooks_review;
|
||||
use crate::tui::Tui;
|
||||
pub use cli::Cli;
|
||||
@@ -1804,11 +1806,33 @@ async fn run_ratatui_app(
|
||||
resume_picker::SessionSelection::Resume(_)
|
||||
);
|
||||
let bypass_hook_trust_for_startup_review = config.bypass_hook_trust && !is_persistent_resume;
|
||||
let hooks_request_handle = app_server.request_handle();
|
||||
let hooks_cwd = config.cwd.to_path_buf();
|
||||
let startup_prefetch_started_at = Instant::now();
|
||||
let should_defer_bootstrap =
|
||||
external_agent_config_migration_startup::should_show_external_agent_config_migration_prompt(
|
||||
&config,
|
||||
should_show_trust_screen_flag,
|
||||
);
|
||||
let (startup_bootstrap, startup_hooks_entry) = if should_defer_bootstrap {
|
||||
(
|
||||
None,
|
||||
load_startup_hooks_review_entry(hooks_request_handle, hooks_cwd).await,
|
||||
)
|
||||
} else {
|
||||
let (bootstrap, entry) = tokio::join!(
|
||||
app_server.bootstrap(&config),
|
||||
load_startup_hooks_review_entry(hooks_request_handle, hooks_cwd),
|
||||
);
|
||||
(Some(bootstrap?), entry)
|
||||
};
|
||||
let startup_elapsed_before_app = startup_prefetch_started_at.elapsed();
|
||||
let startup_hooks_browser = match maybe_run_startup_hooks_review(
|
||||
&mut app_server,
|
||||
&mut tui,
|
||||
&config,
|
||||
bypass_hook_trust_for_startup_review,
|
||||
startup_hooks_entry,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
@@ -1833,6 +1857,8 @@ async fn run_ratatui_app(
|
||||
app_server_target,
|
||||
state_db,
|
||||
environment_manager,
|
||||
startup_elapsed_before_app,
|
||||
startup_bootstrap,
|
||||
startup_hooks_browser,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -31,7 +31,9 @@ use crate::render::renderable::ColumnRenderable;
|
||||
use crate::render::renderable::Renderable;
|
||||
use crate::tui::Tui;
|
||||
use crate::tui::TuiEvent;
|
||||
use codex_app_server_client::AppServerRequestHandle;
|
||||
use codex_app_server_protocol::HooksListEntry;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub(crate) enum StartupHooksReviewOutcome {
|
||||
Continue,
|
||||
@@ -45,21 +47,32 @@ enum StartupHooksReviewSelection {
|
||||
ContinueWithoutTrusting,
|
||||
}
|
||||
|
||||
pub(crate) async fn load_startup_hooks_review_entry(
|
||||
request_handle: AppServerRequestHandle,
|
||||
cwd: PathBuf,
|
||||
) -> HooksListEntry {
|
||||
let response = match fetch_hooks_list(request_handle, cwd.clone()).await {
|
||||
Ok(response) => response,
|
||||
Err(err) => {
|
||||
tracing::warn!("failed to load startup hook review state: {err:#}");
|
||||
return HooksListEntry {
|
||||
cwd,
|
||||
hooks: Vec::new(),
|
||||
warnings: Vec::new(),
|
||||
errors: Vec::new(),
|
||||
};
|
||||
}
|
||||
};
|
||||
hooks_list_entry_for_cwd(response, &cwd)
|
||||
}
|
||||
|
||||
pub(crate) async fn maybe_run_startup_hooks_review(
|
||||
app_server: &mut AppServerSession,
|
||||
tui: &mut Tui,
|
||||
config: &Config,
|
||||
bypass_hook_trust: bool,
|
||||
entry: HooksListEntry,
|
||||
) -> Result<StartupHooksReviewOutcome> {
|
||||
let cwd = config.cwd.to_path_buf();
|
||||
let response = match fetch_hooks_list(app_server.request_handle(), cwd.clone()).await {
|
||||
Ok(response) => response,
|
||||
Err(err) => {
|
||||
tracing::warn!("failed to load startup hook review state: {err:#}");
|
||||
return Ok(StartupHooksReviewOutcome::Continue);
|
||||
}
|
||||
};
|
||||
let entry = hooks_list_entry_for_cwd(response, &cwd);
|
||||
if !review_is_needed(bypass_hook_trust, &entry) {
|
||||
return Ok(StartupHooksReviewOutcome::Continue);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user