Synced from monorepo
Changes: - Stop hooks for session lifecycle - Add x.ai/session/state and x.ai/session/import ACP methods - Deny-and-continue for auto-mode classifier blocks with denial limits - Drop codebase-upload from dhat soak test - scheduler_create upsert via task_id; retire one-shot tasks - Clipboard: copy file fallback + honest toasts for SSH/Apple Terminal - Polarity-safe syntax colors in minimal mode - Auto mode classifies unvetted env prefixes instead of hard-prompting - Add GROK_CLIPBOARD_NO_OSC52 kill switch to force OSC 52 off
This commit is contained in:
parent
7cfcb20d2b
commit
ba76b0a683
143 changed files with 9465 additions and 3419 deletions
|
|
@ -133,12 +133,32 @@ fn terminal_supports_truecolor() -> bool {
|
|||
/// `ColorLevel` declaration-order discriminant.
|
||||
static COLOR_LEVEL_CAP: AtomicU8 = AtomicU8::new(ColorLevel::TrueColor as u8);
|
||||
|
||||
/// When set, RGB syntax colors are remapped with [`polarity_safe_syntax_ansi`]
|
||||
/// instead of nearest-ANSI16. Used by pager minimal mode: the canvas is the
|
||||
/// terminal's own bg, so night-theme pastels quantized to White vanish on
|
||||
/// light profiles. See `xai-grok-pager-render` syntax docs.
|
||||
static POLARITY_SAFE_SYNTAX: AtomicU8 = AtomicU8::new(0);
|
||||
|
||||
/// Set the process-wide upper bound on the effective color level. Pass
|
||||
/// [`ColorLevel::TrueColor`] to remove the cap.
|
||||
pub fn set_color_level_cap(cap: ColorLevel) {
|
||||
COLOR_LEVEL_CAP.store(cap as u8, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Engage dual-polarity-safe syntax color remapping (minimal / terminal-native).
|
||||
///
|
||||
/// When enabled, [`adapt_color`] maps near-gray RGB to "no color" (inherit
|
||||
/// terminal default fg) and chromatic RGB to base ANSI accents — never White.
|
||||
pub fn set_polarity_safe_syntax(enabled: bool) {
|
||||
POLARITY_SAFE_SYNTAX.store(u8::from(enabled), Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Whether polarity-safe syntax remapping is active.
|
||||
#[must_use]
|
||||
pub fn polarity_safe_syntax() -> bool {
|
||||
POLARITY_SAFE_SYNTAX.load(Ordering::Relaxed) != 0
|
||||
}
|
||||
|
||||
fn color_level_cap() -> ColorLevel {
|
||||
match COLOR_LEVEL_CAP.load(Ordering::Relaxed) {
|
||||
0 => ColorLevel::None,
|
||||
|
|
@ -169,7 +189,14 @@ pub fn set_color_level(level: ColorLevel) -> Result<(), ColorLevel> {
|
|||
/// - 256-color terminals: RGB colors are converted to closest ANSI 256 color
|
||||
/// - Basic terminals: colors are converted to closest ANSI 16 color
|
||||
/// - No color: returns None
|
||||
///
|
||||
/// When [`polarity_safe_syntax`] is enabled (minimal mode), RGB tokens take
|
||||
/// the dual-polarity path instead of nearest-ANSI16.
|
||||
pub fn adapt_color(color: Color) -> Option<Color> {
|
||||
if polarity_safe_syntax() {
|
||||
return adapt_color_polarity_safe(color);
|
||||
}
|
||||
|
||||
let level = get_color_level();
|
||||
|
||||
match level {
|
||||
|
|
@ -187,6 +214,106 @@ pub fn adapt_color(color: Color) -> Option<Color> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Polarity-safe remap for syntax tokens painted on a transparent canvas.
|
||||
///
|
||||
/// - Near-gray RGB → `None` (inherit terminal default fg)
|
||||
/// - Chromatic RGB → base ANSI Red/Green/Yellow/Blue/Magenta/Cyan
|
||||
/// - Existing ANSI → demote bright white / white body slots to `None`; keep accents
|
||||
fn adapt_color_polarity_safe(color: Color) -> Option<Color> {
|
||||
match color {
|
||||
Color::Rgb(rgb) => polarity_safe_syntax_ansi(rgb.0, rgb.1, rgb.2).map(Color::Ansi),
|
||||
Color::Ansi256(idx) => {
|
||||
// Expand xterm index to an approximate RGB then re-map.
|
||||
let (r, g, b) = ansi256_to_rgb(idx.index());
|
||||
polarity_safe_syntax_ansi(r, g, b).map(Color::Ansi)
|
||||
}
|
||||
Color::Ansi(ansi) => match ansi {
|
||||
// Body-ish slots that flip polarity → inherit default fg.
|
||||
AnsiColor::Black
|
||||
| AnsiColor::White
|
||||
| AnsiColor::BrightBlack
|
||||
| AnsiColor::BrightWhite => None,
|
||||
// Demote bright accents to base (brights can wash out on light).
|
||||
AnsiColor::BrightRed => Some(Color::Ansi(AnsiColor::Red)),
|
||||
AnsiColor::BrightGreen => Some(Color::Ansi(AnsiColor::Green)),
|
||||
AnsiColor::BrightYellow => Some(Color::Ansi(AnsiColor::Yellow)),
|
||||
AnsiColor::BrightBlue => Some(Color::Ansi(AnsiColor::Blue)),
|
||||
AnsiColor::BrightMagenta => Some(Color::Ansi(AnsiColor::Magenta)),
|
||||
AnsiColor::BrightCyan => Some(Color::Ansi(AnsiColor::Cyan)),
|
||||
other => Some(Color::Ansi(other)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Dual-polarity-safe ANSI mapping for syntax tokens (minimal mode).
|
||||
///
|
||||
/// Returns `None` for near-gray (caller inherits terminal default fg).
|
||||
/// Chromatic hues map to base ANSI colors only — never White/Black.
|
||||
pub fn polarity_safe_syntax_ansi(r: u8, g: u8, b: u8) -> Option<AnsiColor> {
|
||||
let max = r.max(g).max(b) as i32;
|
||||
let min = r.min(g).min(b) as i32;
|
||||
let chroma = max - min;
|
||||
if chroma < 40 {
|
||||
return None;
|
||||
}
|
||||
let (ri, gi, bi) = (r as i32, g as i32, b as i32);
|
||||
let h = if max == ri {
|
||||
let mut h = (gi - bi) * 60 / chroma;
|
||||
if h < 0 {
|
||||
h += 360;
|
||||
}
|
||||
h
|
||||
} else if max == gi {
|
||||
(bi - ri) * 60 / chroma + 120
|
||||
} else {
|
||||
(ri - gi) * 60 / chroma + 240
|
||||
};
|
||||
// Magenta starts at 255° so Tokyo Night purple (#bb9af7, ~261°) lands
|
||||
// Magenta rather than Blue; pure blues (~221°) stay Blue.
|
||||
Some(match h {
|
||||
0..30 | 330..=360 => AnsiColor::Red,
|
||||
30..90 => AnsiColor::Yellow,
|
||||
90..150 => AnsiColor::Green,
|
||||
150..210 => AnsiColor::Cyan,
|
||||
210..255 => AnsiColor::Blue,
|
||||
_ => AnsiColor::Magenta,
|
||||
})
|
||||
}
|
||||
|
||||
/// Approximate RGB for an xterm 256-color index (cube + grayscale).
|
||||
fn ansi256_to_rgb(idx: u8) -> (u8, u8, u8) {
|
||||
match idx {
|
||||
0 => (0, 0, 0),
|
||||
1 => (128, 0, 0),
|
||||
2 => (0, 128, 0),
|
||||
3 => (128, 128, 0),
|
||||
4 => (0, 0, 128),
|
||||
5 => (128, 0, 128),
|
||||
6 => (0, 128, 128),
|
||||
7 => (192, 192, 192),
|
||||
8 => (128, 128, 128),
|
||||
9 => (255, 0, 0),
|
||||
10 => (0, 255, 0),
|
||||
11 => (255, 255, 0),
|
||||
12 => (0, 0, 255),
|
||||
13 => (255, 0, 255),
|
||||
14 => (0, 255, 255),
|
||||
15 => (255, 255, 255),
|
||||
16..=231 => {
|
||||
let n = idx - 16;
|
||||
let r = n / 36;
|
||||
let g = (n / 6) % 6;
|
||||
let b = n % 6;
|
||||
let level = |c: u8| if c == 0 { 0 } else { 55 + 40 * c };
|
||||
(level(r), level(g), level(b))
|
||||
}
|
||||
232..=255 => {
|
||||
let v = 8 + (idx - 232) * 10;
|
||||
(v, v, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert an `anstyle::Style` to the appropriate color level.
|
||||
pub fn adapt_style(style: anstyle::Style) -> anstyle::Style {
|
||||
let fg = style.get_fg_color().and_then(adapt_color);
|
||||
|
|
@ -284,4 +411,41 @@ mod tests {
|
|||
assert!(ColorLevel::Basic < ColorLevel::Ansi256);
|
||||
assert!(ColorLevel::Ansi256 < ColorLevel::TrueColor);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn polarity_safe_grays_inherit_default() {
|
||||
assert_eq!(polarity_safe_syntax_ansi(0xc8, 0xc8, 0xc8), None);
|
||||
assert_eq!(polarity_safe_syntax_ansi(0x6c, 0x6c, 0x6c), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn polarity_safe_never_white() {
|
||||
for (r, g, b) in [
|
||||
(0xbb, 0x9a, 0xf7),
|
||||
(0x7d, 0xcf, 0xff),
|
||||
(0x7a, 0xa2, 0xf7),
|
||||
(0xff, 0x9e, 0x64),
|
||||
(0xf7, 0x76, 0x8e),
|
||||
(0xc8, 0xc8, 0xc8),
|
||||
] {
|
||||
let mapped = polarity_safe_syntax_ansi(r, g, b);
|
||||
assert!(
|
||||
!matches!(
|
||||
mapped,
|
||||
Some(AnsiColor::White | AnsiColor::BrightWhite | AnsiColor::Black)
|
||||
),
|
||||
"#{r:02x}{g:02x}{b:02x} -> {mapped:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adapt_color_polarity_safe_flag_drops_gray_rgb() {
|
||||
set_polarity_safe_syntax(true);
|
||||
let out = adapt_color(Color::Rgb(RgbColor(0xc8, 0xc8, 0xc8)));
|
||||
assert_eq!(out, None, "gray body must inherit default fg");
|
||||
let magenta = adapt_color(Color::Rgb(RgbColor(0xbb, 0x9a, 0xf7)));
|
||||
assert_eq!(magenta, Some(Color::Ansi(AnsiColor::Magenta)));
|
||||
set_polarity_safe_syntax(false);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,7 +47,8 @@ mod url_scan;
|
|||
pub use buffers::MarkdownBuffers;
|
||||
pub use checkpoint::{Checkpoint, CheckpointKind};
|
||||
pub use colors::{
|
||||
ColorLevel, adapt_color, adapt_style, detect_color_level, get_color_level, set_color_level_cap,
|
||||
ColorLevel, adapt_color, adapt_style, detect_color_level, get_color_level,
|
||||
polarity_safe_syntax, polarity_safe_syntax_ansi, set_color_level_cap, set_polarity_safe_syntax,
|
||||
};
|
||||
pub use latex_delimiters::{LatexDelimiterNormalizer, normalize_latex_delimiters};
|
||||
pub use output::{CodeBlockSpan, HyperlinkTarget, MarkdownRenderOutput, MarkdownRenderView};
|
||||
|
|
|
|||
Loading…
Reference in a new issue