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:
canvrno-oai
2026-05-26 12:09:07 -07:00
committed by GitHub
parent 833c19ed53
commit cd934c8bcb
8 changed files with 212 additions and 15 deletions
@@ -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;