Synced from monorepo
Synced from monorepo Changes: - Workspace server: report `/ready` as failed with dwell on hub connect failure - Refresh OIDC token for the Grok agent in the shell - ACP terminal output recorder - Cross-platform provider auth commands in the shell - Default `/resume` to Grok sessions with a hint for hidden external sessions - Resume sessions by title with `--resume` - Limit app-builder archive size - Data-driven tag labels for slash commands - Doctor fixes for tmux - Custom provider gateways and subprocess environment policy in the shell - `/tutorial` — opt-in onboarding tour of Grok Build - Soft and required CLI version checks in the shell - Privacy banner env overrides survive live settings updates - Add remote flag to override the image-edit model - Return profile fields from auth info even when the access token is expired - Add edit control on queued prompt rows - Keep fail-closed policy when clearing orphans with no team - Setting to disable the Ctrl+Space/F8 voice shortcut - Pass `--raw` to pw-record so Linux dictation works on older PipeWire - Validate git URLs when adding marketplace entries - Stop shipping stale tool-doc parameter and tool names - Re-point dashboard attach after `/fork` only when the parent was attached - Surface Grok Computer media-generation results as file-path chunks - Clear web background-task tray on kill and keep the task description - Show privacy upsell banner in agent view until acted on - Add tools-server client callback surface - Protect persistent global hook sources Source-Revision: 95d84f443eddcbed6cbfd6eed22e2eafe6b3939d
This commit is contained in:
parent
a5727c5960
commit
69f0ba880a
286 changed files with 22939 additions and 9624 deletions
169
crates/codegen/xai-grok-pager/src/views/privacy_banner.rs
Normal file
169
crates/codegen/xai-grok-pager/src/views/privacy_banner.rs
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
//! 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};
|
||||
|
||||
/// Legal line copy — used for both render spans and mouse hit width.
|
||||
const PRIVACY_BANNER_LEGAL: &str = "Learn more and read Terms and Privacy Policy.";
|
||||
|
||||
/// Click target for the legal line links.
|
||||
pub(crate) const PRIVACY_BANNER_LEGAL_URL: &str = "https://x.ai/legal";
|
||||
|
||||
/// Hit rects returned by [`render`] for mouse handling.
|
||||
pub(crate) struct PrivacyBannerRects {
|
||||
pub accept: Rect,
|
||||
pub customize: Rect,
|
||||
pub legal: Rect,
|
||||
}
|
||||
|
||||
/// Render the banner: copy left, `[Customize in settings]` / `[Accept]`
|
||||
/// right, legal links on the second row. Needs `area.height >= 2`.
|
||||
/// Hover styling mirrors the plugin CTA buttons.
|
||||
pub(crate) fn render(
|
||||
area: Rect,
|
||||
buf: &mut Buffer,
|
||||
theme: &Theme,
|
||||
mouse_pos: Option<(u16, u16)>,
|
||||
) -> PrivacyBannerRects {
|
||||
let customize_label = "[Customize in settings]";
|
||||
let accept_label = "[Accept]";
|
||||
let right_w = (customize_label.len() + 1 + accept_label.len()) as u16;
|
||||
// Buttons render whole or not at all: a clipped/overflowing [Accept]
|
||||
// must never leave a click target in the blank margin (a stray click
|
||||
// there would silently opt the user in).
|
||||
let buttons_fit = area.width > right_w;
|
||||
let left_w = if buttons_fit {
|
||||
area.width - right_w - 1
|
||||
} else {
|
||||
area.width
|
||||
};
|
||||
|
||||
let left = Rect {
|
||||
x: area.x,
|
||||
y: area.y,
|
||||
width: left_w,
|
||||
height: area.height.min(2),
|
||||
};
|
||||
let right = Rect {
|
||||
x: area.x + left_w + 1,
|
||||
y: area.y,
|
||||
width: right_w,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
let hovered = |r: Rect| {
|
||||
mouse_pos.is_some_and(|(mx, my)| r.contains(ratatui::layout::Position::new(mx, my)))
|
||||
};
|
||||
|
||||
let legal_w = if left.width as usize >= PRIVACY_BANNER_LEGAL.len() {
|
||||
PRIVACY_BANNER_LEGAL.len()
|
||||
} else {
|
||||
"Learn more".len().min(left.width as usize)
|
||||
};
|
||||
// The legal line only exists when the slot really has a second row —
|
||||
// otherwise its rect would make the blank row below clickable.
|
||||
let legal_rect = if area.height >= 2 {
|
||||
Rect {
|
||||
x: left.x,
|
||||
y: left.y.saturating_add(1),
|
||||
width: legal_w as u16,
|
||||
height: 1,
|
||||
}
|
||||
} else {
|
||||
Rect::default()
|
||||
};
|
||||
|
||||
// Figma node 8698:3806: title fg/primary, description fg/secondary,
|
||||
// legal line fg/tertiary with underlined links in the same color.
|
||||
// The whole legal line is one click target, so its links brighten together.
|
||||
let link_fg = if hovered(legal_rect) {
|
||||
theme.gray_bright
|
||||
} else {
|
||||
theme.gray
|
||||
};
|
||||
let link = Style::default()
|
||||
.fg(link_fg)
|
||||
.add_modifier(Modifier::UNDERLINED);
|
||||
let gray = Style::default().fg(theme.gray);
|
||||
let title = Span::styled("Help improve Grok", Style::default().fg(theme.text_primary));
|
||||
let desc = "Allow your sessions to improve SpaceXAI's models.";
|
||||
// Drop trailing spans whole rather than clipping mid-word when narrow.
|
||||
let line1 = if left.width as usize >= "Help improve Grok ".len() + desc.len() {
|
||||
Line::from(vec![
|
||||
title,
|
||||
Span::raw(" "),
|
||||
Span::styled(desc, Style::default().fg(theme.gray_bright)),
|
||||
])
|
||||
} else {
|
||||
Line::from(title)
|
||||
};
|
||||
// Span pieces must reassemble to PRIVACY_BANNER_LEGAL.
|
||||
let line2 = if left.width as usize >= PRIVACY_BANNER_LEGAL.len() {
|
||||
Line::from(vec![
|
||||
Span::styled("Learn more", link),
|
||||
Span::styled(" and read ", gray),
|
||||
Span::styled("Terms", link),
|
||||
Span::styled(" and ", gray),
|
||||
Span::styled("Privacy Policy", link),
|
||||
Span::styled(".", gray),
|
||||
])
|
||||
} else {
|
||||
Line::from(Span::styled("Learn more", link))
|
||||
};
|
||||
Paragraph::new(vec![line1, line2]).render(left, buf);
|
||||
|
||||
if !buttons_fit {
|
||||
return PrivacyBannerRects {
|
||||
accept: Rect::default(),
|
||||
customize: Rect::default(),
|
||||
legal: legal_rect,
|
||||
};
|
||||
}
|
||||
let customize_rect = Rect {
|
||||
x: right.x,
|
||||
y: right.y,
|
||||
width: customize_label.len() as u16,
|
||||
height: 1,
|
||||
};
|
||||
let accept_rect = Rect {
|
||||
x: right.x + customize_label.len() as u16 + 1,
|
||||
y: right.y,
|
||||
width: accept_label.len() as u16,
|
||||
height: 1,
|
||||
};
|
||||
let customize_style = if hovered(customize_rect) {
|
||||
Style::default().fg(theme.text_primary).bg(theme.bg_hover)
|
||||
} else {
|
||||
Style::default().fg(theme.gray_bright)
|
||||
};
|
||||
let accept_style = if hovered(accept_rect) {
|
||||
Style::default().fg(theme.link_fg).bg(theme.bg_hover)
|
||||
} else {
|
||||
Style::default().fg(theme.text_primary)
|
||||
};
|
||||
buf.set_stringn(
|
||||
customize_rect.x,
|
||||
customize_rect.y,
|
||||
customize_label,
|
||||
customize_rect.width as usize,
|
||||
customize_style,
|
||||
);
|
||||
buf.set_stringn(
|
||||
accept_rect.x,
|
||||
accept_rect.y,
|
||||
accept_label,
|
||||
accept_rect.width as usize,
|
||||
accept_style,
|
||||
);
|
||||
PrivacyBannerRects {
|
||||
accept: accept_rect,
|
||||
customize: customize_rect,
|
||||
legal: legal_rect,
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue