mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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:
committed by
GitHub
Unverified
parent
05b967c79a
commit
38b638d89d
@@ -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
-2
@@ -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.
|
||||
|
||||
+15
@@ -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.
|
||||
@@ -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,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
-2
@@ -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.
|
||||
|
||||
+15
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user