mirror of
https://github.com/openai/codex.git
synced 2026-11-01 19:33:35 +00:00
update
This commit is contained in:
@@ -1596,18 +1596,22 @@ impl App {
|
||||
tui.frame_requester().schedule_frame();
|
||||
}
|
||||
AppEvent::OpenAppLink {
|
||||
app_id,
|
||||
title,
|
||||
description,
|
||||
instructions,
|
||||
url,
|
||||
is_installed,
|
||||
is_enabled,
|
||||
} => {
|
||||
self.chat_widget.open_app_link_view(
|
||||
app_id,
|
||||
title,
|
||||
description,
|
||||
instructions,
|
||||
url,
|
||||
is_installed,
|
||||
is_enabled,
|
||||
);
|
||||
}
|
||||
AppEvent::OpenUrlInBrowser { url } => {
|
||||
@@ -2214,6 +2218,51 @@ impl App {
|
||||
}
|
||||
}
|
||||
}
|
||||
AppEvent::SetAppEnabled { id, enabled } => {
|
||||
let edits = if enabled {
|
||||
vec![
|
||||
ConfigEdit::ClearPath {
|
||||
segments: vec!["apps".to_string(), id.clone(), "enabled".to_string()],
|
||||
},
|
||||
ConfigEdit::ClearPath {
|
||||
segments: vec![
|
||||
"apps".to_string(),
|
||||
id.clone(),
|
||||
"disabled_reason".to_string(),
|
||||
],
|
||||
},
|
||||
]
|
||||
} else {
|
||||
vec![
|
||||
ConfigEdit::SetPath {
|
||||
segments: vec!["apps".to_string(), id.clone(), "enabled".to_string()],
|
||||
value: false.into(),
|
||||
},
|
||||
ConfigEdit::SetPath {
|
||||
segments: vec![
|
||||
"apps".to_string(),
|
||||
id.clone(),
|
||||
"disabled_reason".to_string(),
|
||||
],
|
||||
value: "user".into(),
|
||||
},
|
||||
]
|
||||
};
|
||||
match ConfigEditsBuilder::new(&self.config.codex_home)
|
||||
.with_edits(edits)
|
||||
.apply()
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
self.chat_widget.update_connector_enabled(&id, enabled);
|
||||
}
|
||||
Err(err) => {
|
||||
self.chat_widget.add_error_message(format!(
|
||||
"Failed to update app config for {id}: {err}"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
AppEvent::OpenPermissionsPopup => {
|
||||
self.chat_widget.open_permissions_popup();
|
||||
}
|
||||
|
||||
@@ -107,11 +107,13 @@ pub(crate) enum AppEvent {
|
||||
|
||||
/// Open the app link view in the bottom pane.
|
||||
OpenAppLink {
|
||||
app_id: String,
|
||||
title: String,
|
||||
description: Option<String>,
|
||||
instructions: String,
|
||||
url: String,
|
||||
is_installed: bool,
|
||||
is_enabled: bool,
|
||||
},
|
||||
|
||||
/// Open the provided URL in the user's browser.
|
||||
@@ -278,6 +280,12 @@ pub(crate) enum AppEvent {
|
||||
enabled: bool,
|
||||
},
|
||||
|
||||
/// Enable or disable an app by connector ID.
|
||||
SetAppEnabled {
|
||||
id: String,
|
||||
enabled: bool,
|
||||
},
|
||||
|
||||
/// Notify that the manage skills popup was closed.
|
||||
ManageSkillsClosed,
|
||||
|
||||
|
||||
@@ -33,11 +33,13 @@ enum AppLinkScreen {
|
||||
}
|
||||
|
||||
pub(crate) struct AppLinkView {
|
||||
app_id: String,
|
||||
title: String,
|
||||
description: Option<String>,
|
||||
instructions: String,
|
||||
url: String,
|
||||
is_installed: bool,
|
||||
is_enabled: bool,
|
||||
app_event_tx: AppEventSender,
|
||||
screen: AppLinkScreen,
|
||||
selected_action: usize,
|
||||
@@ -46,19 +48,23 @@ pub(crate) struct AppLinkView {
|
||||
|
||||
impl AppLinkView {
|
||||
pub(crate) fn new(
|
||||
app_id: String,
|
||||
title: String,
|
||||
description: Option<String>,
|
||||
instructions: String,
|
||||
url: String,
|
||||
is_installed: bool,
|
||||
is_enabled: bool,
|
||||
app_event_tx: AppEventSender,
|
||||
) -> Self {
|
||||
Self {
|
||||
app_id,
|
||||
title,
|
||||
description,
|
||||
instructions,
|
||||
url,
|
||||
is_installed,
|
||||
is_enabled,
|
||||
app_event_tx,
|
||||
screen: AppLinkScreen::Link,
|
||||
selected_action: 0,
|
||||
@@ -66,16 +72,24 @@ impl AppLinkView {
|
||||
}
|
||||
}
|
||||
|
||||
fn action_labels(&self) -> [&'static str; 2] {
|
||||
fn action_labels(&self) -> Vec<&'static str> {
|
||||
match self.screen {
|
||||
AppLinkScreen::Link => {
|
||||
if self.is_installed {
|
||||
["Manage on ChatGPT", "Back"]
|
||||
vec![
|
||||
"Manage on ChatGPT",
|
||||
if self.is_enabled {
|
||||
"Disable app"
|
||||
} else {
|
||||
"Enable app"
|
||||
},
|
||||
"Back",
|
||||
]
|
||||
} else {
|
||||
["Install on ChatGPT", "Back"]
|
||||
vec!["Install on ChatGPT", "Back"]
|
||||
}
|
||||
}
|
||||
AppLinkScreen::InstallConfirmation => ["I already Installed it", "Back"],
|
||||
AppLinkScreen::InstallConfirmation => vec!["I already Installed it", "Back"],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,42 +101,47 @@ impl AppLinkView {
|
||||
self.selected_action = (self.selected_action + 1).min(self.action_labels().len() - 1);
|
||||
}
|
||||
|
||||
fn handle_primary_action(&mut self) {
|
||||
match self.screen {
|
||||
AppLinkScreen::Link => {
|
||||
self.app_event_tx.send(AppEvent::OpenUrlInBrowser {
|
||||
url: self.url.clone(),
|
||||
});
|
||||
if !self.is_installed {
|
||||
self.screen = AppLinkScreen::InstallConfirmation;
|
||||
self.selected_action = 0;
|
||||
}
|
||||
}
|
||||
AppLinkScreen::InstallConfirmation => {
|
||||
self.app_event_tx.send(AppEvent::RefreshConnectors {
|
||||
force_refetch: true,
|
||||
});
|
||||
self.complete = true;
|
||||
}
|
||||
fn open_chatgpt_link(&mut self) {
|
||||
self.app_event_tx.send(AppEvent::OpenUrlInBrowser {
|
||||
url: self.url.clone(),
|
||||
});
|
||||
if !self.is_installed {
|
||||
self.screen = AppLinkScreen::InstallConfirmation;
|
||||
self.selected_action = 0;
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_secondary_action(&mut self) {
|
||||
match self.screen {
|
||||
AppLinkScreen::Link => {
|
||||
self.complete = true;
|
||||
}
|
||||
AppLinkScreen::InstallConfirmation => {
|
||||
self.screen = AppLinkScreen::Link;
|
||||
self.selected_action = 0;
|
||||
}
|
||||
}
|
||||
fn refresh_connectors_and_close(&mut self) {
|
||||
self.app_event_tx.send(AppEvent::RefreshConnectors {
|
||||
force_refetch: true,
|
||||
});
|
||||
self.complete = true;
|
||||
}
|
||||
|
||||
fn back_to_link_screen(&mut self) {
|
||||
self.screen = AppLinkScreen::Link;
|
||||
self.selected_action = 0;
|
||||
}
|
||||
|
||||
fn toggle_enabled(&mut self) {
|
||||
self.is_enabled = !self.is_enabled;
|
||||
self.app_event_tx.send(AppEvent::SetAppEnabled {
|
||||
id: self.app_id.clone(),
|
||||
enabled: self.is_enabled,
|
||||
});
|
||||
}
|
||||
|
||||
fn activate_selected_action(&mut self) {
|
||||
match self.selected_action {
|
||||
0 => self.handle_primary_action(),
|
||||
_ => self.handle_secondary_action(),
|
||||
match self.screen {
|
||||
AppLinkScreen::Link => match self.selected_action {
|
||||
0 => self.open_chatgpt_link(),
|
||||
1 if self.is_installed => self.toggle_enabled(),
|
||||
_ => self.complete = true,
|
||||
},
|
||||
AppLinkScreen::InstallConfirmation => match self.selected_action {
|
||||
0 => self.refresh_connectors_and_close(),
|
||||
_ => self.back_to_link_screen(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,20 +327,19 @@ impl BottomPaneView for AppLinkView {
|
||||
..
|
||||
} => self.move_selection_next(),
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('1'),
|
||||
code: KeyCode::Char(c),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
} => {
|
||||
self.selected_action = 0;
|
||||
self.activate_selected_action();
|
||||
}
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('2'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
} => {
|
||||
self.selected_action = 1;
|
||||
self.activate_selected_action();
|
||||
if let Some(index) = c
|
||||
.to_digit(10)
|
||||
.and_then(|digit| digit.checked_sub(1))
|
||||
.map(|index| index as usize)
|
||||
&& index < self.action_labels().len()
|
||||
{
|
||||
self.selected_action = index;
|
||||
self.activate_selected_action();
|
||||
}
|
||||
}
|
||||
KeyEvent {
|
||||
code: KeyCode::Enter,
|
||||
@@ -402,3 +420,63 @@ impl crate::render::renderable::Renderable for AppLinkView {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::app_event::AppEvent;
|
||||
use tokio::sync::mpsc::unbounded_channel;
|
||||
|
||||
#[test]
|
||||
fn installed_app_has_toggle_action() {
|
||||
let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx_raw);
|
||||
let view = AppLinkView::new(
|
||||
"connector_1".to_string(),
|
||||
"Notion".to_string(),
|
||||
None,
|
||||
"Manage app".to_string(),
|
||||
"https://example.test/notion".to_string(),
|
||||
true,
|
||||
true,
|
||||
tx,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
view.action_labels(),
|
||||
vec!["Manage on ChatGPT", "Disable app", "Back"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toggle_action_sends_set_app_enabled_and_updates_label() {
|
||||
let (tx_raw, mut rx) = unbounded_channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx_raw);
|
||||
let mut view = AppLinkView::new(
|
||||
"connector_1".to_string(),
|
||||
"Notion".to_string(),
|
||||
None,
|
||||
"Manage app".to_string(),
|
||||
"https://example.test/notion".to_string(),
|
||||
true,
|
||||
true,
|
||||
tx,
|
||||
);
|
||||
|
||||
view.handle_key_event(KeyEvent::new(KeyCode::Char('2'), KeyModifiers::NONE));
|
||||
|
||||
match rx.try_recv() {
|
||||
Ok(AppEvent::SetAppEnabled { id, enabled }) => {
|
||||
assert_eq!(id, "connector_1");
|
||||
assert!(!enabled);
|
||||
}
|
||||
Ok(other) => panic!("unexpected app event: {other:?}"),
|
||||
Err(err) => panic!("missing app event: {err}"),
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
view.action_labels(),
|
||||
vec!["Manage on ChatGPT", "Enable app", "Back"]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3099,7 +3099,7 @@ impl ChatComposer {
|
||||
&& let Some(snapshot) = self.connectors_snapshot.as_ref()
|
||||
{
|
||||
for connector in &snapshot.connectors {
|
||||
if !connector.is_accessible {
|
||||
if !connector.is_accessible || !connector.is_enabled {
|
||||
continue;
|
||||
}
|
||||
let display_name = connectors::connector_display_label(connector);
|
||||
@@ -4342,6 +4342,7 @@ mod tests {
|
||||
distribution_channel: None,
|
||||
install_url: Some("https://example.test/notion".to_string()),
|
||||
is_accessible: true,
|
||||
is_enabled: true,
|
||||
}];
|
||||
composer.set_connector_mentions(Some(ConnectorsSnapshot { connectors }));
|
||||
|
||||
@@ -4355,6 +4356,36 @@ mod tests {
|
||||
assert_eq!(mention.path, Some("app://connector_1".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_connector_mentions_excludes_disabled_apps_from_mention_popup() {
|
||||
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||
let sender = AppEventSender::new(tx);
|
||||
let mut composer = ChatComposer::new(
|
||||
true,
|
||||
sender,
|
||||
false,
|
||||
"Ask Codex to do anything".to_string(),
|
||||
false,
|
||||
);
|
||||
composer.set_connectors_enabled(true);
|
||||
composer.set_text_content("$".to_string(), Vec::new(), Vec::new());
|
||||
|
||||
let connectors = vec![AppInfo {
|
||||
id: "connector_1".to_string(),
|
||||
name: "Notion".to_string(),
|
||||
description: Some("Workspace docs".to_string()),
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
distribution_channel: None,
|
||||
install_url: Some("https://example.test/notion".to_string()),
|
||||
is_accessible: true,
|
||||
is_enabled: false,
|
||||
}];
|
||||
composer.set_connector_mentions(Some(ConnectorsSnapshot { connectors }));
|
||||
|
||||
assert!(matches!(composer.active_popup, ActivePopup::None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shortcut_overlay_persists_while_task_running() {
|
||||
use crossterm::event::KeyCode;
|
||||
|
||||
@@ -1149,18 +1149,22 @@ impl ChatWidget {
|
||||
|
||||
pub(crate) fn open_app_link_view(
|
||||
&mut self,
|
||||
app_id: String,
|
||||
title: String,
|
||||
description: Option<String>,
|
||||
instructions: String,
|
||||
url: String,
|
||||
is_installed: bool,
|
||||
is_enabled: bool,
|
||||
) {
|
||||
let view = crate::bottom_pane::AppLinkView::new(
|
||||
app_id,
|
||||
title,
|
||||
description,
|
||||
instructions,
|
||||
url,
|
||||
is_installed,
|
||||
is_enabled,
|
||||
self.app_event_tx.clone(),
|
||||
);
|
||||
self.bottom_pane.show_view(Box::new(view));
|
||||
@@ -3747,7 +3751,7 @@ impl ChatWidget {
|
||||
if !selected_app_ids.insert(app_id.to_string()) {
|
||||
continue;
|
||||
}
|
||||
if let Some(app) = apps.iter().find(|app| app.id == app_id) {
|
||||
if let Some(app) = apps.iter().find(|app| app.id == app_id && app.is_enabled) {
|
||||
items.push(UserInput::Mention {
|
||||
name: app.name.clone(),
|
||||
path: binding.path.clone(),
|
||||
@@ -6435,6 +6439,7 @@ impl ChatWidget {
|
||||
let connector_title = connector_label.clone();
|
||||
let link_description = Self::connector_description(connector);
|
||||
let description = Self::connector_brief_description(connector);
|
||||
let status_label = Self::connector_status_label(connector);
|
||||
let search_value = format!("{connector_label} {}", connector.id);
|
||||
let mut item = SelectionItem {
|
||||
name: connector_label,
|
||||
@@ -6443,42 +6448,47 @@ impl ChatWidget {
|
||||
..Default::default()
|
||||
};
|
||||
let is_installed = connector.is_accessible;
|
||||
let (selected_label, missing_label, instructions) = if connector.is_accessible {
|
||||
(
|
||||
"Press Enter to view the app link.",
|
||||
"App link unavailable.",
|
||||
"Manage this app in your browser.",
|
||||
let selected_label = if is_installed {
|
||||
format!(
|
||||
"{status_label}. Press Enter to open the app page to install, manage, or enable/disable this app."
|
||||
)
|
||||
} else {
|
||||
(
|
||||
"Press Enter to view the install link.",
|
||||
"Install link unavailable.",
|
||||
"Install this app in your browser, then reload Codex.",
|
||||
)
|
||||
format!("{status_label}. Press Enter to open the app page to install this app.")
|
||||
};
|
||||
let missing_label = format!("{status_label}. App link unavailable.");
|
||||
let instructions = if connector.is_accessible {
|
||||
"Manage this app in your browser."
|
||||
} else {
|
||||
"Install this app in your browser, then reload Codex."
|
||||
};
|
||||
if let Some(install_url) = connector.install_url.clone() {
|
||||
let app_id = connector.id.clone();
|
||||
let is_enabled = connector.is_enabled;
|
||||
let title = connector_title.clone();
|
||||
let instructions = instructions.to_string();
|
||||
let description = link_description.clone();
|
||||
item.actions = vec![Box::new(move |tx| {
|
||||
tx.send(AppEvent::OpenAppLink {
|
||||
app_id: app_id.clone(),
|
||||
title: title.clone(),
|
||||
description: description.clone(),
|
||||
instructions: instructions.clone(),
|
||||
url: install_url.clone(),
|
||||
is_installed,
|
||||
is_enabled,
|
||||
});
|
||||
})];
|
||||
item.dismiss_on_select = true;
|
||||
item.selected_description = Some(selected_label.to_string());
|
||||
item.selected_description = Some(selected_label);
|
||||
} else {
|
||||
let missing_label_for_action = missing_label.clone();
|
||||
item.actions = vec![Box::new(move |tx| {
|
||||
tx.send(AppEvent::InsertHistoryCell(Box::new(
|
||||
history_cell::new_info_event(missing_label.to_string(), None),
|
||||
history_cell::new_info_event(missing_label_for_action.clone(), None),
|
||||
)));
|
||||
})];
|
||||
item.dismiss_on_select = true;
|
||||
item.selected_description = Some(missing_label.to_string());
|
||||
item.selected_description = Some(missing_label);
|
||||
}
|
||||
items.push(item);
|
||||
}
|
||||
@@ -6511,17 +6521,25 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
fn connector_brief_description(connector: &connectors::AppInfo) -> String {
|
||||
let status_label = if connector.is_accessible {
|
||||
"Connected"
|
||||
} else {
|
||||
"Can be installed"
|
||||
};
|
||||
let status_label = Self::connector_status_label(connector);
|
||||
match Self::connector_description(connector) {
|
||||
Some(description) => format!("{status_label} · {description}"),
|
||||
None => status_label.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn connector_status_label(connector: &connectors::AppInfo) -> &'static str {
|
||||
if connector.is_accessible {
|
||||
if connector.is_enabled {
|
||||
"Installed"
|
||||
} else {
|
||||
"Installed · Disabled"
|
||||
}
|
||||
} else {
|
||||
"Can be installed"
|
||||
}
|
||||
}
|
||||
|
||||
fn connector_description(connector: &connectors::AppInfo) -> Option<String> {
|
||||
connector
|
||||
.description
|
||||
@@ -6767,6 +6785,29 @@ impl ChatWidget {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn update_connector_enabled(&mut self, connector_id: &str, enabled: bool) {
|
||||
let ConnectorsCacheState::Ready(mut snapshot) = self.connectors_cache.clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut changed = false;
|
||||
for connector in &mut snapshot.connectors {
|
||||
if connector.id == connector_id {
|
||||
changed = connector.is_enabled != enabled;
|
||||
connector.is_enabled = enabled;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !changed {
|
||||
return;
|
||||
}
|
||||
|
||||
self.refresh_connectors_popup_if_open(&snapshot.connectors);
|
||||
self.connectors_cache = ConnectorsCacheState::Ready(snapshot.clone());
|
||||
self.bottom_pane.set_connectors_snapshot(Some(snapshot));
|
||||
}
|
||||
|
||||
pub(crate) fn open_review_popup(&mut self) {
|
||||
let mut items: Vec<SelectionItem> = Vec::new();
|
||||
|
||||
|
||||
@@ -267,12 +267,12 @@ pub(crate) fn find_app_mentions(
|
||||
}
|
||||
|
||||
let mut slug_counts: HashMap<String, usize> = HashMap::new();
|
||||
for app in apps {
|
||||
for app in apps.iter().filter(|app| app.is_enabled) {
|
||||
let slug = connector_mention_slug(app);
|
||||
*slug_counts.entry(slug).or_insert(0) += 1;
|
||||
}
|
||||
|
||||
for app in apps {
|
||||
for app in apps.iter().filter(|app| app.is_enabled) {
|
||||
let slug = connector_mention_slug(app);
|
||||
let slug_count = slug_counts.get(&slug).copied().unwrap_or(0);
|
||||
if mentions.names.contains(&slug)
|
||||
@@ -285,7 +285,7 @@ pub(crate) fn find_app_mentions(
|
||||
}
|
||||
|
||||
apps.iter()
|
||||
.filter(|app| selected_ids.contains(&app.id))
|
||||
.filter(|app| app.is_enabled && selected_ids.contains(&app.id))
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -3731,6 +3731,7 @@ async fn apps_popup_refreshes_when_connectors_snapshot_updates() {
|
||||
distribution_channel: None,
|
||||
install_url: Some("https://example.test/notion".to_string()),
|
||||
is_accessible: true,
|
||||
is_enabled: true,
|
||||
}],
|
||||
}),
|
||||
false,
|
||||
@@ -3746,6 +3747,10 @@ async fn apps_popup_refreshes_when_connectors_snapshot_updates() {
|
||||
before.contains("Installed 1 of 1 available apps."),
|
||||
"expected initial apps popup snapshot, got:\n{before}"
|
||||
);
|
||||
assert!(
|
||||
before.contains("Installed. Press Enter to open the app page"),
|
||||
"expected selected app description to explain the app page action, got:\n{before}"
|
||||
);
|
||||
|
||||
chat.on_connectors_loaded(
|
||||
Ok(ConnectorsSnapshot {
|
||||
@@ -3759,6 +3764,7 @@ async fn apps_popup_refreshes_when_connectors_snapshot_updates() {
|
||||
distribution_channel: None,
|
||||
install_url: Some("https://example.test/notion".to_string()),
|
||||
is_accessible: true,
|
||||
is_enabled: true,
|
||||
},
|
||||
codex_chatgpt::connectors::AppInfo {
|
||||
id: "connector_2".to_string(),
|
||||
@@ -3769,6 +3775,7 @@ async fn apps_popup_refreshes_when_connectors_snapshot_updates() {
|
||||
distribution_channel: None,
|
||||
install_url: Some("https://example.test/linear".to_string()),
|
||||
is_accessible: true,
|
||||
is_enabled: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
@@ -3802,6 +3809,7 @@ async fn apps_refresh_failure_keeps_existing_full_snapshot() {
|
||||
distribution_channel: None,
|
||||
install_url: Some("https://example.test/notion".to_string()),
|
||||
is_accessible: true,
|
||||
is_enabled: true,
|
||||
},
|
||||
codex_chatgpt::connectors::AppInfo {
|
||||
id: "connector_2".to_string(),
|
||||
@@ -3812,6 +3820,7 @@ async fn apps_refresh_failure_keeps_existing_full_snapshot() {
|
||||
distribution_channel: None,
|
||||
install_url: Some("https://example.test/linear".to_string()),
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
},
|
||||
];
|
||||
chat.on_connectors_loaded(
|
||||
@@ -3832,6 +3841,7 @@ async fn apps_refresh_failure_keeps_existing_full_snapshot() {
|
||||
distribution_channel: None,
|
||||
install_url: Some("https://example.test/notion".to_string()),
|
||||
is_accessible: true,
|
||||
is_enabled: true,
|
||||
}],
|
||||
}),
|
||||
false,
|
||||
@@ -3851,6 +3861,76 @@ async fn apps_refresh_failure_keeps_existing_full_snapshot() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn apps_popup_shows_disabled_status_for_installed_but_disabled_apps() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await;
|
||||
chat.config.features.enable(Feature::Apps);
|
||||
chat.bottom_pane.set_connectors_enabled(true);
|
||||
|
||||
chat.on_connectors_loaded(
|
||||
Ok(ConnectorsSnapshot {
|
||||
connectors: vec![codex_chatgpt::connectors::AppInfo {
|
||||
id: "connector_1".to_string(),
|
||||
name: "Notion".to_string(),
|
||||
description: Some("Workspace docs".to_string()),
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
distribution_channel: None,
|
||||
install_url: Some("https://example.test/notion".to_string()),
|
||||
is_accessible: true,
|
||||
is_enabled: false,
|
||||
}],
|
||||
}),
|
||||
true,
|
||||
);
|
||||
|
||||
chat.add_connectors_output();
|
||||
let popup = render_bottom_popup(&chat, 80);
|
||||
assert!(
|
||||
popup.contains("Installed · Disabled. Press Enter to open the app page"),
|
||||
"expected selected app description to include disabled status, got:\n{popup}"
|
||||
);
|
||||
assert!(
|
||||
popup.contains("enable/disable this app."),
|
||||
"expected selected app description to mention enable/disable action, got:\n{popup}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn apps_popup_for_not_installed_app_uses_install_only_selected_description() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await;
|
||||
chat.config.features.enable(Feature::Apps);
|
||||
chat.bottom_pane.set_connectors_enabled(true);
|
||||
|
||||
chat.on_connectors_loaded(
|
||||
Ok(ConnectorsSnapshot {
|
||||
connectors: vec![codex_chatgpt::connectors::AppInfo {
|
||||
id: "connector_2".to_string(),
|
||||
name: "Linear".to_string(),
|
||||
description: Some("Project tracking".to_string()),
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
distribution_channel: None,
|
||||
install_url: Some("https://example.test/linear".to_string()),
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
}],
|
||||
}),
|
||||
true,
|
||||
);
|
||||
|
||||
chat.add_connectors_output();
|
||||
let popup = render_bottom_popup(&chat, 80);
|
||||
assert!(
|
||||
popup.contains("Can be installed. Press Enter to open the app page to install"),
|
||||
"expected selected app description to be install-only for not-installed apps, got:\n{popup}"
|
||||
);
|
||||
assert!(
|
||||
!popup.contains("enable/disable this app."),
|
||||
"did not expect enable/disable text for not-installed apps, got:\n{popup}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn experimental_features_popup_snapshot() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await;
|
||||
|
||||
Reference in New Issue
Block a user