diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 0d87a34b3..725427213 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -93,6 +93,7 @@ use codex_app_server_protocol::PluginUninstallResponse; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::ServerNotification; use codex_app_server_protocol::ServerRequest; +use codex_app_server_protocol::SkillsListParams; use codex_app_server_protocol::SkillsListResponse; use codex_app_server_protocol::ThreadItem; use codex_app_server_protocol::ThreadLoadedListParams; @@ -2072,6 +2073,25 @@ impl App { }); } + /// Starts the initial skills refresh without delaying the first interactive frame. + /// + /// Startup only needs skill metadata to populate skill mentions and the skills UI; the prompt can be + /// rendered before that metadata arrives. The result is routed through the normal app event queue so + /// the same response handler updates the chat widget and emits invalid `SKILL.md` warnings once the + /// app-server RPC finishes. User-initiated skills refreshes still use the blocking app command path so + /// callers that explicitly asked for fresh skill state do not race ahead of their own refresh. + fn refresh_startup_skills(&mut self, app_server: &AppServerSession) { + let request_handle = app_server.request_handle(); + let app_event_tx = self.app_event_tx.clone(); + let cwd = self.config.cwd.to_path_buf(); + tokio::spawn(async move { + let result = fetch_skills_list(request_handle, cwd) + .await + .map_err(|err| err.to_string()); + app_event_tx.send(AppEvent::SkillsListLoaded { result }); + }); + } + fn fetch_plugins_list(&mut self, app_server: &AppServerSession, cwd: PathBuf) { let request_handle = app_server.request_handle(); let app_event_tx = self.app_event_tx.clone(); @@ -4036,16 +4056,6 @@ impl App { app.enqueue_primary_thread_session(started.session, started.turns) .await?; } - app.handle_skills_list_result( - app_server - .skills_list(codex_app_server_protocol::SkillsListParams { - cwds: vec![app.config.cwd.to_path_buf()], - force_reload: true, - per_cwd_extra_user_roots: None, - }) - .await, - "failed to load skills on startup", - ); // On startup, if Agent mode (workspace-write) or ReadOnly is active, warn about world-writable dirs on Windows. #[cfg(target_os = "windows")] @@ -4076,6 +4086,7 @@ impl App { tokio::pin!(tui_events); tui.frame_requester().schedule_frame(); + app.refresh_startup_skills(&app_server); // Kick off a non-blocking rate-limit prefetch so the first `/status` // already has data, without delaying the initial frame render. if requires_openai_auth && has_chatgpt_account { @@ -4774,6 +4785,12 @@ impl App { AppEvent::McpInventoryLoaded { result } => { self.handle_mcp_inventory_result(result); } + AppEvent::SkillsListLoaded { result } => { + self.handle_skills_list_result( + result.map_err(|err| color_eyre::eyre::eyre!(err)), + "failed to load skills on startup", + ); + } AppEvent::StartFileSearch(query) => { self.file_search.on_user_query(query); } @@ -6454,6 +6471,26 @@ async fn fetch_account_rate_limits( Ok(app_server_rate_limit_snapshots_to_core(response)) } +async fn fetch_skills_list( + request_handle: AppServerRequestHandle, + cwd: PathBuf, +) -> Result { + let request_id = RequestId::String(format!("startup-skills-list-{}", Uuid::new_v4())); + // Use the cloneable request handle so startup can issue this RPC from a background task without + // extending a borrow of `AppServerSession` across the first frame render. + request_handle + .request_typed(ClientRequest::SkillsList { + request_id, + params: SkillsListParams { + cwds: vec![cwd], + force_reload: true, + per_cwd_extra_user_roots: None, + }, + }) + .await + .wrap_err("skills/list failed in TUI") +} + async fn fetch_plugins_list( request_handle: AppServerRequestHandle, cwd: PathBuf, diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index c45ff2d5b..106bb7a91 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -17,6 +17,7 @@ use codex_app_server_protocol::PluginListResponse; use codex_app_server_protocol::PluginReadParams; use codex_app_server_protocol::PluginReadResponse; use codex_app_server_protocol::PluginUninstallResponse; +use codex_app_server_protocol::SkillsListResponse; use codex_file_search::FileMatch; use codex_protocol::ThreadId; use codex_protocol::openai_models::ModelPreset; @@ -306,6 +307,15 @@ pub(crate) enum AppEvent { result: Result, String>, }, + /// Result of the startup skills refresh that runs after the first frame is scheduled. + /// + /// This event is startup-only. Interactive skills refreshes are handled synchronously through the app + /// command path because those callers expect the visible skill state to be current when their command + /// completes. + SkillsListLoaded { + result: Result, + }, + InsertHistoryCell(Box), /// Apply rollback semantics to local transcript cells.