Add legal link to TUI /plugin details (#15692)

- Adds language and "[learn
more](https://help.openai.com/en/articles/11487775-apps-in-chatgpt)"
link to plugin details pages.
-  Message is hidden when plugin is installed

<img width="1970" height="498" alt="image"
src="https://github.com/user-attachments/assets/f14330f7-661e-4860-8538-6dc9e8bbd90a"
/>
This commit is contained in:
canvrno-oai
2026-03-24 17:40:26 -07:00
committed by GitHub
Unverified
parent 05b967c79a
commit 38b638d89d
10 changed files with 308 additions and 12 deletions
+53 -3
View File
@@ -8,6 +8,7 @@ use crate::bottom_pane::ColumnWidthMode;
use crate::bottom_pane::SelectionItem;
use crate::bottom_pane::SelectionViewParams;
use crate::history_cell;
use crate::onboarding::mark_url_hyperlink;
use crate::render::renderable::ColumnRenderable;
use crate::render::renderable::Renderable;
use crate::shimmer::shimmer_spans;
@@ -24,10 +25,12 @@ use codex_features::Feature;
use codex_utils_absolute_path::AbsolutePathBuf;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::prelude::Widget;
use ratatui::style::Stylize;
use ratatui::text::Line;
use ratatui::widgets::Paragraph;
use ratatui::widgets::WidgetRef;
use ratatui::widgets::Wrap;
const PLUGINS_SELECTION_VIEW_ID: &str = "plugins-selection";
const LOADING_ANIMATION_DELAY: Duration = Duration::from_secs(1);
@@ -93,6 +96,29 @@ impl Renderable for DelayedLoadingHeader {
}
}
const APPS_HELP_ARTICLE_URL: &str = "https://help.openai.com/en/articles/11487775-apps-in-chatgpt";
struct PluginDisclosureLine {
line: Line<'static>,
}
impl Renderable for PluginDisclosureLine {
fn render(&self, area: Rect, buf: &mut Buffer) {
Paragraph::new(self.line.clone())
.wrap(Wrap { trim: false })
.render(area, buf);
mark_url_hyperlink(buf, area, APPS_HELP_ARTICLE_URL);
}
fn desired_height(&self, width: u16) -> u16 {
Paragraph::new(self.line.clone())
.wrap(Wrap { trim: false })
.line_count(width)
.try_into()
.unwrap_or(u16::MAX)
}
}
#[derive(Debug, Clone, Default)]
pub(super) enum PluginsCacheState {
#[default]
@@ -812,13 +838,37 @@ impl ChatWidget {
) -> SelectionViewParams {
let marketplace_label = plugin.marketplace_name.clone();
let display_name = plugin_display_name(&plugin.summary);
let status_label = plugin_status_label(&plugin.summary);
let detail_status_label = if plugin.summary.installed {
if plugin.summary.enabled {
"Installed"
} else {
"Installed · Disabled"
}
} else {
match plugin.summary.install_policy {
PluginInstallPolicy::NotAvailable => "Not installable",
PluginInstallPolicy::Available => "Can be installed",
PluginInstallPolicy::InstalledByDefault => "Available by default",
}
};
let mut header = ColumnRenderable::new();
header.push(Line::from("Plugins".bold()));
header.push(Line::from(
format!("{display_name} · {marketplace_label}").bold(),
format!("{display_name} · {detail_status_label} · {marketplace_label}").bold(),
));
header.push(Line::from(status_label.dim()));
if !plugin.summary.installed {
header.push(PluginDisclosureLine {
line: Line::from(vec![
"Data shared with this app is subject to the app's ".into(),
"terms of service".bold(),
" and ".into(),
"privacy policy".bold(),
". ".into(),
"Learn more".cyan().underlined(),
".".into(),
]),
});
}
if let Some(description) = plugin_detail_description(plugin) {
header.push(Line::from(description.dim()));
}
@@ -3,8 +3,9 @@ source: tui/src/chatwidget/tests.rs
expression: popup
---
Plugins
Figma · ChatGPT Marketplace
Available
Figma · Can be installed · ChatGPT Marketplace
Data shared with this app is subject to the app's terms of service and privacy policy. Learn
more.
Turn Figma files into implementation context.
1. Back to plugins Return to the plugin list.
@@ -0,0 +1,15 @@
---
source: tui/src/chatwidget/tests.rs
expression: popup
---
Plugins
Figma · Installed · ChatGPT Marketplace
Turn Figma files into implementation context.
1. Back to plugins Return to the plugin list.
2. Uninstall plugin Remove this plugin now.
Skills design-review, extract-copy
Apps Figma, Slack
MCP Servers figma-mcp, docs-mcp
Press esc to close.
+82 -1
View File
@@ -7083,6 +7083,40 @@ fn render_bottom_popup(chat: &ChatWidget, width: u16) -> String {
lines.join("\n")
}
fn strip_osc8_for_snapshot(text: &str) -> String {
// Snapshots should assert the visible popup text, not terminal hyperlink escapes.
let bytes = text.as_bytes();
let mut stripped = String::with_capacity(text.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i..].starts_with(b"\x1B]8;;") {
i += 5;
while i < bytes.len() {
if bytes[i] == b'\x07' {
i += 1;
break;
}
if i + 1 < bytes.len() && bytes[i] == b'\x1B' && bytes[i + 1] == b'\\' {
i += 2;
break;
}
i += 1;
}
continue;
}
let ch = text[i..]
.chars()
.next()
.expect("slice should always contain a char");
stripped.push(ch);
i += ch.len_utf8();
}
stripped
}
fn plugins_test_absolute_path(path: &str) -> AbsolutePathBuf {
AbsolutePathBuf::try_from(
std::env::temp_dir()
@@ -7338,7 +7372,54 @@ async fn plugin_detail_popup_snapshot_shows_install_actions_and_capability_summa
);
let popup = render_bottom_popup(&chat, 100);
assert_snapshot!("plugin_detail_popup_installable", popup);
assert_snapshot!(
"plugin_detail_popup_installable",
strip_osc8_for_snapshot(&popup)
);
}
#[tokio::test]
async fn plugin_detail_popup_hides_disclosure_for_installed_plugins() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await;
chat.set_feature_enabled(Feature::Plugins, true);
let summary = plugins_test_summary(
"plugin-figma",
"figma",
Some("Figma"),
Some("Design handoff."),
true,
true,
PluginInstallPolicy::Available,
);
let response = plugins_test_response(vec![plugins_test_curated_marketplace(vec![
summary.clone(),
])]);
let cwd = chat.config.cwd.clone();
chat.on_plugins_loaded(cwd.clone(), Ok(response));
chat.add_plugins_output();
chat.on_plugin_detail_loaded(
cwd,
Ok(PluginReadResponse {
plugin: plugins_test_detail(
summary,
Some("Turn Figma files into implementation context."),
&["design-review", "extract-copy"],
&[("Figma", true), ("Slack", false)],
&["figma-mcp", "docs-mcp"],
),
}),
);
let popup = render_bottom_popup(&chat, 100);
assert!(
!popup.contains("Data shared with this app is subject to the app's"),
"expected installed plugin details to hide the disclosure line, got:\n{popup}"
);
assert_snapshot!(
"plugin_detail_popup_installed",
strip_osc8_for_snapshot(&popup)
);
}
#[tokio::test]
+1
View File
@@ -1,5 +1,6 @@
mod auth;
pub mod onboarding_screen;
mod trust_directory;
pub(crate) use auth::mark_url_hyperlink;
pub use trust_directory::TrustDirectorySelection;
mod welcome;
@@ -8,6 +8,7 @@ use crate::bottom_pane::ColumnWidthMode;
use crate::bottom_pane::SelectionItem;
use crate::bottom_pane::SelectionViewParams;
use crate::history_cell;
use crate::onboarding::mark_url_hyperlink;
use crate::render::renderable::ColumnRenderable;
use crate::render::renderable::Renderable;
use crate::shimmer::shimmer_spans;
@@ -24,10 +25,12 @@ use codex_features::Feature;
use codex_utils_absolute_path::AbsolutePathBuf;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::prelude::Widget;
use ratatui::style::Stylize;
use ratatui::text::Line;
use ratatui::widgets::Paragraph;
use ratatui::widgets::WidgetRef;
use ratatui::widgets::Wrap;
const PLUGINS_SELECTION_VIEW_ID: &str = "plugins-selection";
const LOADING_ANIMATION_DELAY: Duration = Duration::from_secs(1);
@@ -93,6 +96,29 @@ impl Renderable for DelayedLoadingHeader {
}
}
const APPS_HELP_ARTICLE_URL: &str = "https://help.openai.com/en/articles/11487775-apps-in-chatgpt";
struct PluginDisclosureLine {
line: Line<'static>,
}
impl Renderable for PluginDisclosureLine {
fn render(&self, area: Rect, buf: &mut Buffer) {
Paragraph::new(self.line.clone())
.wrap(Wrap { trim: false })
.render(area, buf);
mark_url_hyperlink(buf, area, APPS_HELP_ARTICLE_URL);
}
fn desired_height(&self, width: u16) -> u16 {
Paragraph::new(self.line.clone())
.wrap(Wrap { trim: false })
.line_count(width)
.try_into()
.unwrap_or(u16::MAX)
}
}
#[derive(Debug, Clone, Default)]
pub(super) enum PluginsCacheState {
#[default]
@@ -812,13 +838,37 @@ impl ChatWidget {
) -> SelectionViewParams {
let marketplace_label = plugin.marketplace_name.clone();
let display_name = plugin_display_name(&plugin.summary);
let status_label = plugin_status_label(&plugin.summary);
let detail_status_label = if plugin.summary.installed {
if plugin.summary.enabled {
"Installed"
} else {
"Installed · Disabled"
}
} else {
match plugin.summary.install_policy {
PluginInstallPolicy::NotAvailable => "Not installable",
PluginInstallPolicy::Available => "Can be installed",
PluginInstallPolicy::InstalledByDefault => "Available by default",
}
};
let mut header = ColumnRenderable::new();
header.push(Line::from("Plugins".bold()));
header.push(Line::from(
format!("{display_name} · {marketplace_label}").bold(),
format!("{display_name} · {detail_status_label} · {marketplace_label}").bold(),
));
header.push(Line::from(status_label.dim()));
if !plugin.summary.installed {
header.push(PluginDisclosureLine {
line: Line::from(vec![
"Data shared with this app is subject to the app's ".into(),
"terms of service".bold(),
" and ".into(),
"privacy policy".bold(),
". ".into(),
"Learn more".cyan().underlined(),
".".into(),
]),
});
}
if let Some(description) = plugin_detail_description(plugin) {
header.push(Line::from(description.dim()));
}
@@ -3,8 +3,9 @@ source: tui_app_server/src/chatwidget/tests.rs
expression: popup
---
Plugins
Figma · ChatGPT Marketplace
Available
Figma · Can be installed · ChatGPT Marketplace
Data shared with this app is subject to the app's terms of service and privacy policy. Learn
more.
Turn Figma files into implementation context.
1. Back to plugins Return to the plugin list.
@@ -0,0 +1,15 @@
---
source: tui_app_server/src/chatwidget/tests.rs
expression: popup
---
Plugins
Figma · Installed · ChatGPT Marketplace
Turn Figma files into implementation context.
1. Back to plugins Return to the plugin list.
2. Uninstall plugin Remove this plugin now.
Skills design-review, extract-copy
Apps Figma, Slack
MCP Servers figma-mcp, docs-mcp
Press esc to close.
@@ -7680,6 +7680,40 @@ fn render_bottom_popup(chat: &ChatWidget, width: u16) -> String {
lines.join("\n")
}
fn strip_osc8_for_snapshot(text: &str) -> String {
// Snapshots should assert the visible popup text, not terminal hyperlink escapes.
let bytes = text.as_bytes();
let mut stripped = String::with_capacity(text.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i..].starts_with(b"\x1B]8;;") {
i += 5;
while i < bytes.len() {
if bytes[i] == b'\x07' {
i += 1;
break;
}
if i + 1 < bytes.len() && bytes[i] == b'\x1B' && bytes[i + 1] == b'\\' {
i += 2;
break;
}
i += 1;
}
continue;
}
let ch = text[i..]
.chars()
.next()
.expect("slice should always contain a char");
stripped.push(ch);
i += ch.len_utf8();
}
stripped
}
fn plugins_test_absolute_path(path: &str) -> AbsolutePathBuf {
AbsolutePathBuf::try_from(
std::env::temp_dir()
@@ -7935,7 +7969,54 @@ async fn plugin_detail_popup_snapshot_shows_install_actions_and_capability_summa
);
let popup = render_bottom_popup(&chat, 100);
assert_snapshot!("plugin_detail_popup_installable", popup);
assert_snapshot!(
"plugin_detail_popup_installable",
strip_osc8_for_snapshot(&popup)
);
}
#[tokio::test]
async fn plugin_detail_popup_hides_disclosure_for_installed_plugins() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await;
chat.set_feature_enabled(Feature::Plugins, true);
let summary = plugins_test_summary(
"plugin-figma",
"figma",
Some("Figma"),
Some("Design handoff."),
true,
true,
PluginInstallPolicy::Available,
);
let response = plugins_test_response(vec![plugins_test_curated_marketplace(vec![
summary.clone(),
])]);
let cwd = chat.config.cwd.clone();
chat.on_plugins_loaded(cwd.clone(), Ok(response));
chat.add_plugins_output();
chat.on_plugin_detail_loaded(
cwd,
Ok(PluginReadResponse {
plugin: plugins_test_detail(
summary,
Some("Turn Figma files into implementation context."),
&["design-review", "extract-copy"],
&[("Figma", true), ("Slack", false)],
&["figma-mcp", "docs-mcp"],
),
}),
);
let popup = render_bottom_popup(&chat, 100);
assert!(
!popup.contains("Data shared with this app is subject to the app's"),
"expected installed plugin details to hide the disclosure line, got:\n{popup}"
);
assert_snapshot!(
"plugin_detail_popup_installed",
strip_osc8_for_snapshot(&popup)
);
}
#[tokio::test]
@@ -1,5 +1,6 @@
mod auth;
pub mod onboarding_screen;
mod trust_directory;
pub(crate) use auth::mark_url_hyperlink;
pub use trust_directory::TrustDirectorySelection;
mod welcome;