Probe Windows terminal colors without risking startup input (#40598)

## Why

Windows Terminal and xterm.js can render a theme that differs from the
underlying console color table. Querying that visible theme through OSC shares
the console input queue with keystrokes, including input for security-sensitive
startup prompts.

## What changed

- Defer the Windows OSC color probe until protected startup decisions and the
  world-writable workspace scan have completed.
- Preserve and replay unrelated console input records around the probe, while
  removing only valid OSC 10 and 11 replies and retaining the console palette
  as a fallback.
- Require an explicit confirmation key after shortcuts select trust, sandbox,
  or world-writable warning choices, so a fragmented terminal reply cannot
  accept them.
- Accept one- through four-digit OSC RGB components so the visible terminal
  palette is applied across supported response formats.

## Testing

Added coverage for delayed startup scans, protected prompt confirmation,
console-record replay, bracketed paste and malformed OSC preservation, RGB
response parsing, and light-theme composer rendering.

GitOrigin-RevId: b8ae456fe32c701ba3af31e9cc452fc4e8bffa22
This commit is contained in:
Felipe Coury authored and copyberry committed 2026-08-25 12:20:41 +00:00
1 parent 1f5c9ba183
commit 70b5cfc73b
19 files changed
+1162 -149

No files matched your search

@@ -5090,6 +5090,29 @@ mod tests {
);
}
#[test]
fn light_terminal_palette_renders_light_composer_snapshot() {
let colors = crate::terminal_probe::DefaultColors {
fg: (0x55, 0x57, 0x53),
bg: (0xff, 0xff, 0xff),
};
crate::terminal_palette::with_test_default_colors(colors, || {
let (composer, _rx) = new_test_composer();
let area = Rect::new(
/*x*/ 0, /*y*/ 0, /*width*/ 48, /*height*/ 6,
);
let mut buffer = Buffer::empty(area);
composer.render(area, &mut buffer);
assert_eq!(
buffer[(0, 1)].bg,
crate::terminal_palette::rgb_color((244, 244, 244))
);
insta::assert_snapshot!("light_terminal_palette_composer", format!("{buffer:?}"));
});
}
#[test]
fn footer_hint_row_is_separated_from_composer() {
let (tx, _rx) = unbounded_channel::<AppEvent>();
@@ -143,6 +143,8 @@ pub(crate) struct SelectionItem {
pub is_disabled: bool,
pub actions: Vec<SelectionAction>,
pub dismiss_on_select: bool,
/// Require an explicit accept key after a direct shortcut highlights this sensitive item.
pub require_explicit_confirmation: bool,
pub dismiss_parent_on_child_accept: bool,
pub search_value: Option<String>,
pub disabled_reason: Option<String>,
@@ -807,6 +809,22 @@ impl ListSelectionView {
}
}
fn select_shortcut(&mut self, actual_idx: usize) {
let previously_selected = self.selected_actual_idx();
self.state.selected_idx = Some(actual_idx);
if self
.active_items()
.get(actual_idx)
.is_some_and(|item| item.require_explicit_confirmation)
{
if self.selected_actual_idx() != previously_selected {
self.fire_selection_changed();
}
} else {
self.accept();
}
}
#[cfg(test)]
pub(crate) fn set_search_query(&mut self, query: String) {
self.search_query = query;
@@ -1047,8 +1065,7 @@ impl BottomPaneView for ListSelectionView {
.is_some_and(|shortcut| shortcut.is_press(key_event))
&& Self::item_is_enabled(item)
}) {
self.state.selected_idx = Some(idx);
self.accept();
self.select_shortcut(idx);
return;
}
if let Some(idx) = c
@@ -1056,8 +1073,7 @@ impl BottomPaneView for ListSelectionView {
.map(|d| d as usize)
.and_then(|number| self.actual_idx_for_enabled_number(number))
{
self.state.selected_idx = Some(idx);
self.accept();
self.select_shortcut(idx);
}
}
_ => {}
@@ -0,0 +1,27 @@
---
source: tui/src/bottom_pane/chat_composer.rs
expression: "format!(\"{buffer:?}\")"
---
Buffer {
area: Rect { x: 0, y: 0, width: 48, height: 6 },
content: [
" ",
"› Ask Codex to do anything ",
" ",
" ",
" ",
" ? for shortcuts 100% context left ",
],
styles: [
x: 0, y: 0, fg: Reset, bg: Rgb(244, 244, 244), underline: Reset, modifier: NONE,
x: 0, y: 1, fg: Reset, bg: Rgb(244, 244, 244), underline: Reset, modifier: BOLD,
x: 1, y: 1, fg: Reset, bg: Rgb(244, 244, 244), underline: Reset, modifier: NONE,
x: 2, y: 1, fg: Reset, bg: Rgb(244, 244, 244), underline: Reset, modifier: DIM,
x: 26, y: 1, fg: Reset, bg: Rgb(244, 244, 244), underline: Reset, modifier: NONE,
x: 0, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
x: 2, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
x: 17, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
x: 29, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
x: 46, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
]
}