grok-build-upstream-mirror/crates/codegen/xai-grok-pager/src/views/privacy_banner.rs
grokkybara[bot] 5da6962e4a Synced from monorepo
Synced from monorepo

Changes:
- Workspace server: surface preview-proxy metrics through the hub metric pump
- Shell: reclaim a session’s retained state in one entry
- Shell: reclaim a session’s resident state in one entry
- Pager: withhold key event types from Alacritty builds that double keys
- Tools: cancel a session’s subagents when it closes
- Pager: keep the whole plan in scrollback and separate reasoning from output in minimal mode
- Pager: probe terminal version over DA2 and include it with feedback
- SuperGrok Plus: identity, CLI, and analytics tier surfaces
- Shell: inherit the session process scope into subagents
- Pager: build @-file-search matcher lazily on first use
- Tools: fix description and output contradictions in tool definitions
- Workspace: degrade @-file-search instead of aborting on thread exhaustion
- Tools: reap a session’s LSP servers when it closes
- Tools: fix contradictions and defects in tool descriptions, schemas, and harness pools
- MCP: reap stdio MCP children on session close
- Shell: reuse spawn-time skill discovery for session telemetry
- Tools: stop leaking shell-wrapper positional params into sourced scripts (fixes activate_conda under persistent/static shell)
- Shell: self-heal corrupt session-search SQLite cache
- Workspace: cap workspace-server tokio workers on many-core hosts
- Shell: reap a session’s child processes when it closes
- Crash handler: capture SIGABRT so panic-aborts leave crash reports
- CLI chat proxy: team-scoped Grok Code managed-config admin routes
- MCP: add CLI enable/disable for MCP servers
- Shell: cap tokio worker threads for startup thread demand
- Workspace: harden git_commit and add git_sync_base operation
- Circuit breaker: add feature-gated gRPC retry policy

Source-Revision: 2a818575225183d8ca915f5632a09b8067b5156a
2026-07-28 22:50:19 +00:00

426 lines
14 KiB
Rust

//! Coding-data sharing upsell banner (Figma "Data Sharing Upsell",
//! node 8698:3690). Shared by the welcome tip slot and the agent-view
//! banner slot; visibility is gated by `AppView::privacy_banner_should_show`.
use crate::theme::Theme;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Paragraph, Widget};
/// Shares its row with the buttons.
const PRIVACY_BANNER_TITLE: &str = "Help improve Grok";
const PRIVACY_BANNER_DESC: &str = "Off by default. Opt-in to allow SpaceXAI to retain coding \
data, e.g., prompts, traces, & metrics, for training and debugging purposes. Change \
anytime via settings.";
pub(crate) const PRIVACY_BANNER_TERMS_URL: &str = "https://x.ai/legal/terms-of-service";
pub(crate) const PRIVACY_BANNER_POLICY_URL: &str = "https://x.ai/legal/privacy-policy";
/// `(text, url_when_link)`.
type LegalSegment = (&'static str, Option<&'static str>);
/// Widest first; the first that fits *whole* wins. A clipped line would
/// leave hit rects over unreadable link text, and every variant keeps both
/// links so neither document becomes unreachable.
const PRIVACY_BANNER_LEGAL_VARIANTS: [&[LegalSegment]; 3] = [
&[
("Read ", None),
("Terms", Some(PRIVACY_BANNER_TERMS_URL)),
(" and ", None),
("Privacy Policy", Some(PRIVACY_BANNER_POLICY_URL)),
(".", None),
],
&[
("Terms", Some(PRIVACY_BANNER_TERMS_URL)),
(" and ", None),
("Privacy Policy", Some(PRIVACY_BANNER_POLICY_URL)),
],
&[
("Terms", Some(PRIVACY_BANNER_TERMS_URL)),
(" & ", None),
("Privacy", Some(PRIVACY_BANNER_POLICY_URL)),
],
];
const OPT_OUT_LABEL: &str = "[Opt out]";
const OPT_IN_LABEL: &str = "[Opt in]";
/// Title + legal.
const CHROME_ROWS: u16 = 2;
pub(crate) const MIN_HEIGHT: u16 = CHROME_ROWS + 1;
/// Caps banner growth on narrow terminals; overflow is elided with `…` so
/// the disclosure never looks complete when it isn't.
const MAX_BODY_ROWS: usize = 4;
/// Past this, the body abandons the button column for the full slot width:
/// a shorter banner beats a tidy right edge.
const PREFERRED_BODY_ROWS: usize = 3;
pub(crate) struct PrivacyBannerRects {
pub opt_in: Rect,
pub opt_out: Rect,
pub terms: Rect,
pub policy: Rect,
}
impl PrivacyBannerRects {
fn none() -> Self {
Self {
opt_in: Rect::default(),
opt_out: Rect::default(),
terms: Rect::default(),
policy: Rect::default(),
}
}
}
fn button_block_width() -> u16 {
(OPT_OUT_LABEL.len() + 1 + OPT_IN_LABEL.len()) as u16
}
fn legal_width(variant: &[LegalSegment]) -> u16 {
variant.iter().map(|(text, _)| text.len() as u16).sum()
}
/// Buttons render whole or not at all, and never at the cost of the title:
/// a clipped/overflowing `[Opt in]` must not leave a click target in the
/// blank margin (a stray click there would silently opt the user in).
fn buttons_fit(area_width: u16) -> bool {
area_width >= PRIVACY_BANNER_TITLE.len() as u16 + 1 + button_block_width()
}
fn title_width(area_width: u16) -> u16 {
if buttons_fit(area_width) {
area_width - button_block_width() - 1
} else {
area_width
}
}
fn wrap_to(width: usize) -> Vec<std::borrow::Cow<'static, str>> {
if width == 0 {
return vec![];
}
let opts = textwrap::Options::new(width).wrap_algorithm(textwrap::WrapAlgorithm::FirstFit);
textwrap::wrap(PRIVACY_BANNER_DESC, opts)
}
fn body_lines(area_width: u16) -> Vec<std::borrow::Cow<'static, str>> {
let column = wrap_to(title_width(area_width) as usize);
let mut lines = if column.len() <= PREFERRED_BODY_ROWS {
column
} else {
let full = wrap_to(area_width as usize);
if full.len() < column.len() {
full
} else {
column
}
};
if lines.len() > MAX_BODY_ROWS {
lines.truncate(MAX_BODY_ROWS);
if let Some(last) = lines.last_mut() {
let mut s = last.trim_end().to_string();
while s.chars().count() + 1 > area_width as usize {
s.pop();
}
s.push('\u{2026}');
*last = std::borrow::Cow::Owned(s);
}
}
lines
}
/// Rows needed at `width` — the body wraps, so both slot owners must size
/// from this rather than a constant.
pub(crate) fn height(width: u16) -> u16 {
CHROME_ROWS + (body_lines(width).len() as u16).max(1)
}
/// Needs `area.height >= MIN_HEIGHT`; give it [`height`] rows for the full
/// body.
pub(crate) fn render(
area: Rect,
buf: &mut Buffer,
theme: &Theme,
mouse_pos: Option<(u16, u16)>,
) -> PrivacyBannerRects {
if area.height < MIN_HEIGHT || area.width == 0 {
return PrivacyBannerRects::none();
}
let hovered = |r: Rect| {
mouse_pos.is_some_and(|(mx, my)| r.contains(ratatui::layout::Position::new(mx, my)))
};
// Figma node 8698:3806.
buf.set_stringn(
area.x,
area.y,
PRIVACY_BANNER_TITLE,
title_width(area.width) as usize,
Style::default().fg(theme.text_primary),
);
let body_style = Style::default().fg(theme.gray_bright);
let body_rows = area.height - CHROME_ROWS;
let body: Vec<Line> = body_lines(area.width)
.into_iter()
.take(body_rows as usize)
.map(|l| Line::styled(l.into_owned(), body_style))
.collect();
Paragraph::new(body).render(
Rect {
x: area.x,
y: area.y + 1,
width: area.width,
height: body_rows,
},
buf,
);
// Last row, so it gets the full width — no buttons to dodge.
let gray = Style::default().fg(theme.gray);
let legal_y = area.y + area.height - 1;
let mut terms_rect = Rect::default();
let mut policy_rect = Rect::default();
if let Some(variant) = PRIVACY_BANNER_LEGAL_VARIANTS
.into_iter()
.find(|v| legal_width(v) <= area.width)
{
let mut x = area.x;
let mut spans = Vec::with_capacity(variant.len());
for (text, url) in variant {
let w = text.len() as u16;
let style = match url {
None => gray,
Some(url) => {
let rect = Rect {
x,
y: legal_y,
width: w,
height: 1,
};
if *url == PRIVACY_BANNER_TERMS_URL {
terms_rect = rect;
} else {
policy_rect = rect;
}
let fg = if hovered(rect) {
theme.gray_bright
} else {
theme.gray
};
Style::default().fg(fg).add_modifier(Modifier::UNDERLINED)
}
};
spans.push(Span::styled(*text, style));
x += w;
}
Paragraph::new(Line::from(spans)).render(
Rect {
x: area.x,
y: legal_y,
width: x - area.x,
height: 1,
},
buf,
);
}
if !buttons_fit(area.width) {
return PrivacyBannerRects {
opt_in: Rect::default(),
opt_out: Rect::default(),
terms: terms_rect,
policy: policy_rect,
};
}
let opt_out_rect = Rect {
x: area.x + area.width - button_block_width(),
y: area.y,
width: OPT_OUT_LABEL.len() as u16,
height: 1,
};
let opt_in_rect = Rect {
x: opt_out_rect.x + opt_out_rect.width + 1,
y: area.y,
width: OPT_IN_LABEL.len() as u16,
height: 1,
};
let opt_out_style = if hovered(opt_out_rect) {
Style::default().fg(theme.text_primary).bg(theme.bg_hover)
} else {
Style::default().fg(theme.gray_bright)
};
let opt_in_style = if hovered(opt_in_rect) {
Style::default().fg(theme.link_fg).bg(theme.bg_hover)
} else {
Style::default().fg(theme.text_primary)
};
buf.set_stringn(
opt_out_rect.x,
opt_out_rect.y,
OPT_OUT_LABEL,
opt_out_rect.width as usize,
opt_out_style,
);
buf.set_stringn(
opt_in_rect.x,
opt_in_rect.y,
OPT_IN_LABEL,
opt_in_rect.width as usize,
opt_in_style,
);
PrivacyBannerRects {
opt_in: opt_in_rect,
opt_out: opt_out_rect,
terms: terms_rect,
policy: policy_rect,
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Render at `width` into a buffer sized by [`height`], returning the
/// rows (trailing blanks trimmed) and the hit rects.
fn draw(width: u16) -> (Vec<String>, PrivacyBannerRects) {
let h = height(width);
let area = Rect::new(0, 0, width, h);
let mut buf = Buffer::empty(area);
let rects = render(area, &mut buf, &Theme::current(), None);
let rows = (0..h)
.map(|y| {
(0..width)
.map(|x| buf.cell((x, y)).map(|c| c.symbol()).unwrap_or(" "))
.collect::<String>()
.trim_end()
.to_string()
})
.collect();
(rows, rects)
}
fn rows(width: u16) -> Vec<String> {
draw(width).0
}
/// The text a legal variant reassembles to.
fn legal_text(variant: &[LegalSegment]) -> String {
variant.iter().map(|(text, _)| *text).collect()
}
/// The buffer text under `rect` on its row.
fn text_at(rows: &[String], rect: Rect) -> String {
let row = &rows[rect.y as usize];
row.chars()
.skip(rect.x as usize)
.take(rect.width as usize)
.collect()
}
/// Slot owners reserve [`height`] rows, so the last one it promises must
/// be the legal line — not a body row pushed off the end.
#[test]
fn height_reserves_every_row_the_banner_paints() {
for width in [200, 117, 110, 100, 80, 72, 60, 45, 40, 36, 30, 24, 18] {
let rows = rows(width);
assert_eq!(rows.len(), height(width) as usize);
assert!(
rows[0].starts_with(PRIVACY_BANNER_TITLE),
"width {width}: title must never be clipped, got {:?}",
rows[0]
);
let legal = rows.last().expect("legal row");
assert!(
PRIVACY_BANNER_LEGAL_VARIANTS
.iter()
.any(|v| legal_text(v) == *legal),
"width {width}: legal line must survive whole, got {legal:?}"
);
assert!(
rows[1..rows.len() - 1].iter().all(|r| !r.is_empty()),
"width {width}: body rows must not be blank: {rows:?}"
);
}
}
/// The row cap's elision is a narrow-terminal fallback, not the norm.
#[test]
fn body_copy_is_complete_at_common_widths() {
for width in [200, 117, 100, 80, 60] {
let body = rows(width)[1..].join(" ");
let flattened: String = body.split_whitespace().collect::<Vec<_>>().join(" ");
assert!(
flattened.contains(PRIVACY_BANNER_DESC),
"width {width}: body copy was truncated: {flattened:?}"
);
}
}
#[test]
fn buttons_drop_whole_when_the_row_is_too_narrow() {
let width = PRIVACY_BANNER_TITLE.len() as u16 + button_block_width(); // one short
let h = height(width);
let mut buf = Buffer::empty(Rect::new(0, 0, width, h));
let rects = render(Rect::new(0, 0, width, h), &mut buf, &Theme::current(), None);
assert_eq!(rects.opt_in, Rect::default());
assert_eq!(rects.opt_out, Rect::default());
assert_ne!(rects.terms, Rect::default(), "terms link still clickable");
assert_ne!(rects.policy, Rect::default(), "policy link still clickable");
let rects = {
let width = width + 1;
let h = height(width);
let mut buf = Buffer::empty(Rect::new(0, 0, width, h));
render(Rect::new(0, 0, width, h), &mut buf, &Theme::current(), None)
};
assert_eq!(rects.opt_out.width, OPT_OUT_LABEL.len() as u16);
assert_eq!(rects.opt_in.width, OPT_IN_LABEL.len() as u16);
}
#[test]
fn slot_below_min_height_arms_no_hit_rects() {
let mut buf = Buffer::empty(Rect::new(0, 0, 100, MIN_HEIGHT));
let rects = render(
Rect::new(0, 0, 100, MIN_HEIGHT - 1),
&mut buf,
&Theme::current(),
None,
);
assert_eq!(rects.opt_in, Rect::default());
assert_eq!(rects.opt_out, Rect::default());
assert_eq!(rects.terms, Rect::default());
assert_eq!(rects.policy, Rect::default());
}
/// The two links open different documents, so an off-by-one rect sends
/// the user to the wrong page.
#[test]
fn each_legal_link_hits_its_own_words() {
for width in [200, 117, 80, 60, 40, 30, 24, 18] {
let (rows, rects) = draw(width);
assert_eq!(
text_at(&rows, rects.terms),
"Terms",
"width {width}: terms rect is off its word: {rows:?}"
);
let policy = text_at(&rows, rects.policy);
assert!(
policy == "Privacy Policy" || policy == "Privacy",
"width {width}: policy rect is off its word, got {policy:?}"
);
assert!(
rects.terms.right() <= rects.policy.x,
"width {width}: link rects must not overlap"
);
}
}
}