Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions codex-rs/tui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ use crate::history_cell;
use crate::history_cell::HistoryCell;
#[cfg(not(debug_assertions))]
use crate::history_cell::UpdateAvailableHistoryCell;
use crate::hooks_rpc::HookTrustUpdate;
use crate::key_hint::KeyBindingListExt;
use crate::keymap::RuntimeKeymap;
use crate::legacy_core::config::Config;
Expand Down Expand Up @@ -91,8 +92,7 @@ use codex_app_server_protocol::ConfigWriteResponse;
use codex_app_server_protocol::FeedbackUploadParams;
use codex_app_server_protocol::FeedbackUploadResponse;
use codex_app_server_protocol::GetAccountRateLimitsResponse;
use codex_app_server_protocol::HooksListParams;
use codex_app_server_protocol::HooksListResponse;
use codex_app_server_protocol::HooksListEntry;
use codex_app_server_protocol::ListMcpServerStatusParams;
use codex_app_server_protocol::ListMcpServerStatusResponse;
#[cfg(test)]
Expand Down Expand Up @@ -618,6 +618,7 @@ impl App {
remote_app_server_auth_token: Option<String>,
state_db: Option<StateDbHandle>,
environment_manager: Arc<EnvironmentManager>,
startup_hooks_browser: Option<HooksListEntry>,
) -> Result<AppExitInfo> {
use tokio_stream::StreamExt;
let (app_event_tx, mut app_event_rx) = unbounded_channel();
Expand Down Expand Up @@ -914,6 +915,9 @@ See the Codex keymap documentation for supported actions and examples."
pending_plugin_enabled_writes: HashMap::new(),
pending_hook_enabled_writes: HashMap::new(),
};
if let Some(entry) = startup_hooks_browser {
app.chat_widget.open_hooks_browser(entry);
}
if let Some(started) = initial_started_thread {
let thread_id = started.session.thread_id;
app.enqueue_primary_thread_session(started.session, started.turns)
Expand Down Expand Up @@ -957,7 +961,6 @@ See the Codex keymap documentation for supported actions and examples."

tui.frame_requester().schedule_frame();
app.refresh_startup_skills(&app_server);
app.refresh_startup_hooks(&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 {
Expand Down
104 changes: 19 additions & 85 deletions codex-rs/tui/src/app/background_requests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
//! the main event loop remains single-threaded.

use super::*;
use codex_app_server_protocol::HookTrustStatus;
use codex_app_server_protocol::MarketplaceAddParams;
use codex_app_server_protocol::MarketplaceAddResponse;
use codex_app_server_protocol::MarketplaceRemoveParams;
Expand All @@ -15,6 +14,9 @@ use codex_app_server_protocol::MarketplaceUpgradeResponse;

use codex_app_server_protocol::RequestId;

use crate::hooks_rpc::fetch_hooks_list;
use crate::hooks_rpc::write_hook_trust;
use crate::hooks_rpc::write_hook_trusts;
use codex_utils_absolute_path::AbsolutePathBuf;

impl App {
Expand Down Expand Up @@ -89,47 +91,6 @@ impl App {
});
}

/// Emits the initial hook review warning without delaying the first interactive frame.
pub(super) fn refresh_startup_hooks(&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_hooks_list(request_handle, cwd.clone()).await;
let response = match result {
Ok(response) => response,
Err(err) => {
tracing::warn!("failed to load startup hook review state: {err:#}");
return;
}
};
let hooks_needing_review = response
.data
.into_iter()
.find(|entry| entry.cwd.as_path() == cwd.as_path())
.map(|entry| {
entry
.hooks
.into_iter()
.filter(|hook| {
matches!(
hook.trust_status,
HookTrustStatus::Untrusted | HookTrustStatus::Modified
)
})
.count()
})
.unwrap_or_default();
if let Some(message) =
startup_prompts::hooks_needing_review_warning(hooks_needing_review)
{
app_event_tx.send(AppEvent::InsertHistoryCell(Box::new(
history_cell::new_warning_event(message),
)));
}
});
}

pub(super) 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();
Expand Down Expand Up @@ -381,6 +342,22 @@ impl App {
});
}

pub(super) fn trust_hooks(
&mut self,
app_server: &AppServerSession,
updates: Vec<HookTrustUpdate>,
) {
let request_handle = app_server.request_handle();
let app_event_tx = self.app_event_tx.clone();
tokio::spawn(async move {
let result = write_hook_trusts(request_handle, updates)
.await
.map(|_| ())
.map_err(|err| format!("Failed to trust hooks: {err}"));
app_event_tx.send(AppEvent::HookTrusted { result });
});
}

pub(super) fn refresh_plugin_mentions(&mut self) {
let config = self.config.clone();
let app_event_tx = self.app_event_tx.clone();
Expand Down Expand Up @@ -674,20 +651,6 @@ pub(super) async fn fetch_plugins_list(
Ok(response)
}

pub(super) async fn fetch_hooks_list(
request_handle: AppServerRequestHandle,
cwd: PathBuf,
) -> Result<HooksListResponse> {
let request_id = RequestId::String(format!("hooks-list-{}", Uuid::new_v4()));
request_handle
.request_typed(ClientRequest::HooksList {
request_id,
params: HooksListParams { cwds: vec![cwd] },
})
.await
.wrap_err("hooks/list failed in TUI")
}

const CLI_HIDDEN_PLUGIN_MARKETPLACES: &[&str] = &["openai-bundled"];

pub(super) fn hide_cli_only_plugin_marketplaces(response: &mut PluginListResponse) {
Expand Down Expand Up @@ -864,35 +827,6 @@ pub(super) async fn write_hook_enabled(
.wrap_err("config/batchWrite failed while updating hook enablement in TUI")
}

pub(super) async fn write_hook_trust(
request_handle: AppServerRequestHandle,
key: String,
current_hash: String,
) -> Result<ConfigWriteResponse> {
let request_id = RequestId::String(format!("hooks-config-write-{}", Uuid::new_v4()));
let value = serde_json::json!({
key: {
"trusted_hash": current_hash,
}
});
request_handle
.request_typed(ClientRequest::ConfigBatchWrite {
request_id,
params: ConfigBatchWriteParams {
edits: vec![codex_app_server_protocol::ConfigEdit {
key_path: "hooks.state".to_string(),
value,
merge_strategy: MergeStrategy::Upsert,
}],
file_path: None,
expected_version: None,
reload_user_config: true,
},
})
.await
.wrap_err("config/batchWrite failed while updating hook trust in TUI")
}

pub(super) fn build_feedback_upload_params(
origin_thread_id: Option<ThreadId>,
rollout_path: Option<PathBuf>,
Expand Down
3 changes: 3 additions & 0 deletions codex-rs/tui/src/app/event_dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1713,6 +1713,9 @@ impl App {
AppEvent::TrustHook { key, current_hash } => {
self.trust_hook(app_server, key, current_hash);
}
AppEvent::TrustHooks { updates } => {
self.trust_hooks(app_server, updates);
}
AppEvent::HookEnabledSet {
key,
enabled,
Expand Down
10 changes: 0 additions & 10 deletions codex-rs/tui/src/app/startup_prompts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,16 +77,6 @@ pub(super) fn emit_system_bwrap_warning(app_event_tx: &AppEventSender, config: &
)));
}

pub(super) fn hooks_needing_review_warning(count: usize) -> Option<String> {
match count {
0 => None,
1 => Some("1 hook needs review before it can run. Open /hooks to review it.".to_string()),
count => Some(format!(
"{count} hooks need review before they can run. Open /hooks to review them."
)),
}
}

pub(super) fn should_show_model_migration_prompt(
current_model: &str,
target_model: &str,
Expand Down
11 changes: 0 additions & 11 deletions codex-rs/tui/src/app/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,17 +301,6 @@ async fn ignore_same_thread_resume_allows_reattaching_displayed_inactive_thread(
assert!(app.transcript_cells.is_empty());
}

#[test]
fn hooks_needing_review_startup_warning_snapshot() {
let message = startup_prompts::hooks_needing_review_warning(/*count*/ 2)
.expect("review-needed hooks should produce a startup warning");
let rendered = lines_to_single_string(
&history_cell::new_warning_event(message).display_lines(/*width*/ 80),
);

assert_app_snapshot!("hooks_needing_review_startup_warning", rendered);
}

#[tokio::test]
async fn enqueue_primary_thread_session_replays_buffered_approval_after_attach() -> Result<()> {
let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await;
Expand Down
5 changes: 5 additions & 0 deletions codex-rs/tui/src/app_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,11 @@ pub(crate) enum AppEvent {
current_hash: String,
},

/// Trust the current definitions for one or more hooks by stable hook key.
TrustHooks {
updates: Vec<crate::hooks_rpc::HookTrustUpdate>,
},

/// Result of persisting hook enabled state.
HookEnabledSet {
key: String,
Expand Down
Loading
Loading