mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
tui: keep inaccessible apps out of mentions (#24625)
## Summary Fix the TUI `$` app mention paths so App Directory rows that are not accessible are not treated as usable apps. This includes the core preservation fix from #24104, but expands it to the other app mention paths: - preserve app-server `is_accessible` flags when partial `app/list/updated` snapshots reach the TUI - require apps to be both accessible and enabled when resolving exact `$slug` mentions - require restored/stale `app://...` bindings to point at accessible, enabled apps before emitting structured app mentions - remove the now-unused `codex-chatgpt` dependency from `codex-tui`, which addresses the `cargo shear` failure seen on #24104 ## Root Cause The app server already sends merged app snapshots with accessibility computed. The TUI handled app-server app list updates as partial app loads and re-ran the old accessible-app merge path. That path treated every notification row as accessible, so App Directory entries with `isAccessible=false` could appear in `$` suggestions. Regression source: #22914 routed app-list updates through the app server while reusing the old TUI partial-load handling. Related precursor: #14717 introduced the partial-load path, but #22914 made it user-visible for app-server updates. ## Issues Fixes #24145 Fixes #24205 Fixes #24319 ## Validation - `just fmt` - `git diff --check` - `just bazel-lock-update` - `just bazel-lock-check` - `just argument-comment-lint -p codex-tui` - `just test -p codex-tui chatwidget::tests::popups_and_settings::apps_notification_update_excludes_inaccessible_apps_from_mentions chatwidget::tests::composer_submission::submit_user_message_ignores_inaccessible_app_mentions_from_bindings chatwidget::skills::tests::find_app_mentions_requires_accessible_enabled_apps_for_bound_paths chatwidget::skills::tests::find_app_mentions_requires_accessible_enabled_apps_for_slugs`
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use crate::app_event::ConnectorsSnapshot;
|
||||
use codex_protocol::models::ManagedFileSystemPermissions;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
@@ -1172,6 +1173,61 @@ async fn enqueueing_history_prompt_multiple_times_is_stable() {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn submit_user_message_ignores_inaccessible_app_mentions_from_bindings() {
|
||||
let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
set_chatgpt_auth(&mut chat);
|
||||
chat.config
|
||||
.features
|
||||
.enable(Feature::Apps)
|
||||
.expect("test config should allow feature update");
|
||||
|
||||
chat.on_connectors_loaded(
|
||||
Ok(ConnectorsSnapshot {
|
||||
connectors: vec![AppInfo {
|
||||
id: "arabica_uae".to_string(),
|
||||
name: "% Arabica UAE".to_string(),
|
||||
description: Some("Directory-only app".to_string()),
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
distribution_channel: None,
|
||||
branding: None,
|
||||
app_metadata: None,
|
||||
labels: None,
|
||||
install_url: Some("https://example.test/arabica".to_string()),
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
}],
|
||||
}),
|
||||
/*is_final*/ false,
|
||||
);
|
||||
|
||||
chat.submit_user_message(UserMessage {
|
||||
text: "$arabica-uae".to_string(),
|
||||
local_images: Vec::new(),
|
||||
remote_image_urls: Vec::new(),
|
||||
text_elements: Vec::new(),
|
||||
mention_bindings: vec![MentionBinding {
|
||||
mention: "arabica-uae".to_string(),
|
||||
path: "app://arabica_uae".to_string(),
|
||||
}],
|
||||
});
|
||||
|
||||
let items = match next_submit_op(&mut op_rx) {
|
||||
Op::UserTurn { items, .. } => items,
|
||||
other => panic!("expected Op::UserTurn, got {other:?}"),
|
||||
};
|
||||
assert_eq!(
|
||||
items,
|
||||
vec![UserInput::Text {
|
||||
text: "$arabica-uae".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_message_display_from_inputs_matches_flattened_user_message_shape() {
|
||||
let local_image = PathBuf::from("/tmp/local.png");
|
||||
|
||||
@@ -1399,6 +1399,77 @@ async fn apps_popup_stays_loading_until_final_snapshot_updates() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn apps_notification_update_excludes_inaccessible_apps_from_mentions() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
set_chatgpt_auth(&mut chat);
|
||||
chat.config
|
||||
.features
|
||||
.enable(Feature::Apps)
|
||||
.expect("test config should allow feature update");
|
||||
chat.bottom_pane.set_connectors_enabled(/*enabled*/ true);
|
||||
chat.bottom_pane
|
||||
.set_composer_text("$".to_string(), Vec::new(), Vec::new());
|
||||
|
||||
chat.on_connectors_loaded(
|
||||
Ok(ConnectorsSnapshot {
|
||||
connectors: vec![
|
||||
AppInfo {
|
||||
id: "google_drive".to_string(),
|
||||
name: "Google Drive".to_string(),
|
||||
description: Some("Connected files".to_string()),
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
distribution_channel: None,
|
||||
branding: None,
|
||||
app_metadata: None,
|
||||
labels: None,
|
||||
install_url: Some("https://example.test/google-drive".to_string()),
|
||||
is_accessible: true,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
},
|
||||
AppInfo {
|
||||
id: "arabica_uae".to_string(),
|
||||
name: "% Arabica UAE".to_string(),
|
||||
description: Some("Directory-only app".to_string()),
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
distribution_channel: None,
|
||||
branding: None,
|
||||
app_metadata: None,
|
||||
labels: None,
|
||||
install_url: Some("https://example.test/arabica".to_string()),
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
},
|
||||
],
|
||||
}),
|
||||
/*is_final*/ false,
|
||||
);
|
||||
|
||||
assert_matches!(
|
||||
&chat.connectors.partial_snapshot,
|
||||
Some(snapshot)
|
||||
if snapshot
|
||||
.connectors
|
||||
.iter()
|
||||
.find(|connector| connector.id == "arabica_uae")
|
||||
.is_some_and(|connector| !connector.is_accessible)
|
||||
);
|
||||
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 80);
|
||||
assert!(
|
||||
popup.contains("Google Drive"),
|
||||
"expected accessible apps to appear in the mention popup, got:\n{popup}"
|
||||
);
|
||||
assert!(
|
||||
!popup.contains("% Arabica UAE"),
|
||||
"did not expect an inaccessible directory app in the mention popup, got:\n{popup}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn apps_refresh_failure_keeps_existing_full_snapshot() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
Reference in New Issue
Block a user