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:
grokkybara[bot] 2026-07-19 18:40:33 +01:00
commit ba76b0a683
143 changed files with 9465 additions and 3419 deletions

View file

@ -1885,6 +1885,7 @@ mod tests {
created_at: std::time::Instant::now(),
next_fire_at: None,
tag: "loop".into(),
last_subagent_id: None,
}
}
/// A turn-idle agent with a RUNNING background task is `Working`, not

View file

@ -1146,7 +1146,13 @@ fn highlight_to_ratatui_line(
if piece.is_empty() {
continue;
}
let fg = syntect_to_ratatui_color(style.foreground);
// Shared path: polarity-safe under the terminal-native lock, else
// normal theme quantize (see xai_grok_pager_render::syntax).
let fg = crate::syntax::syntect_rgb_to_fg(
style.foreground.r,
style.foreground.g,
style.foreground.b,
);
spans.push(Span::styled(piece, Style::default().fg(fg)));
}
@ -1156,13 +1162,6 @@ fn highlight_to_ratatui_line(
Line::from(spans)
}
/// Convert a syntect RGBA color to a ratatui Color.
///
/// Quantizes the RGB value to the terminal's supported color level.
fn syntect_to_ratatui_color(c: syntect::highlighting::Color) -> ratatui::style::Color {
crate::theme::quantize(ratatui::style::Color::Rgb(c.r, c.g, c.b))
}
/// Count digits in a number (for line number padding).
fn digit_count(n: usize) -> usize {
if n == 0 {

View file

@ -238,6 +238,7 @@ pub enum TaskEntry {
label: String,
styled: Line<'static>,
started_at: Instant,
linked_subagent: Option<String>,
},
/// Collapsible group header row (e.g. `▾ Subagents 2`). Not a task —
/// selecting it and pressing Enter (or clicking it) toggles the group's
@ -448,7 +449,9 @@ impl TaskEntry {
info: &ScheduledTaskInfo,
current_cron: Option<&str>,
is_queued: bool,
linked: Option<(String, bool)>,
) -> Self {
let linked_running = linked.as_ref().is_some_and(|(_, running)| *running);
let theme = Theme::current();
let prompt_preview = if info.prompt.chars().count() > 60 {
info.prompt.chars().take(57).collect::<String>() + "..."
@ -469,7 +472,7 @@ impl TaskEntry {
}
};
let is_provisional = info.task_id.starts_with("provisional-");
let suffix = if current_cron == Some(&info.task_id) {
let suffix = if current_cron == Some(&info.task_id) || linked_running {
" (running)".to_string()
} else if is_queued {
" (queued)".to_string()
@ -501,7 +504,7 @@ impl TaskEntry {
}
};
let label = format!(
"{} {} {}{}",
"{} {} \u{b7} {}{}",
tag_display, info.human_schedule, &prompt_preview, &suffix
);
@ -510,11 +513,11 @@ impl TaskEntry {
// neutral secondary text color so the row reads calmly with a single
// point of color. No surrounding `[ ]` brackets: the color alone
// sets the tag apart from the schedule that follows it.
let schedule_style = format!("{} ", info.human_schedule);
let schedule_style = format!("{} \u{b7} ", info.human_schedule);
let neutral = Style::default().fg(theme.text_secondary);
let styled = Line::from(vec![
Span::styled(
format!("{} ", tag_display),
format!("{} ", tag_display),
Style::default().fg(theme.accent_system),
),
Span::styled(schedule_style, neutral),
@ -537,6 +540,7 @@ impl TaskEntry {
label,
styled,
started_at: info.created_at,
linked_subagent: linked.map(|(sid, _)| sid),
}
}
@ -660,7 +664,7 @@ impl ListItem for TaskEntry {
enum OverlayEntryData {
BgTask(String),
Agent(String, String),
Scheduled(String),
Scheduled(String, Option<String>),
}
const MAX_TASKS_HEIGHT: u16 = 8;
@ -834,10 +838,17 @@ impl TasksPane {
// Add scheduled task items (always "running")
for info in scheduled.values() {
let linked = info.last_subagent_id.as_deref().and_then(|sid| {
subagents
.values()
.find(|s| s.subagent_id.as_ref() == sid)
.map(|s| (sid.to_string(), s.is_running()))
});
self.items.push(TaskEntry::from_scheduled(
info,
current_cron_task_id,
queued_cron_ids.contains(info.task_id.as_str()),
linked,
));
}
@ -1307,9 +1318,11 @@ impl TasksPane {
child_session_id,
..
} => OverlayEntryData::Agent(subagent_id.clone(), child_session_id.clone()),
TaskEntry::Scheduled { task_id, .. } => {
OverlayEntryData::Scheduled(task_id.clone())
}
TaskEntry::Scheduled {
task_id,
linked_subagent,
..
} => OverlayEntryData::Scheduled(task_id.clone(), linked_subagent.clone()),
// Group headers have no kill/view buttons; they still
// occupy a row (vis_row is enumerated before this filter),
// so the y offsets for following items stay correct.
@ -1333,8 +1346,15 @@ impl TasksPane {
};
self.render_agent_overlay(area, buf, y, subagent_id, info, &theme);
}
OverlayEntryData::Scheduled(ref task_id) => {
self.render_scheduled_overlay(area, buf, y, task_id, &theme);
OverlayEntryData::Scheduled(ref task_id, ref linked_subagent) => {
self.render_scheduled_overlay(
area,
buf,
y,
task_id,
linked_subagent.as_deref(),
&theme,
);
}
}
}
@ -1643,6 +1663,7 @@ impl TasksPane {
buf: &mut Buffer,
y: u16,
task_id: &str,
linked_subagent: Option<&str>,
theme: &Theme,
) {
let frames = crate::glyphs::dot_spinner_frames();
@ -1654,8 +1675,8 @@ impl TasksPane {
2,
);
// Clear overlay area (kill button + separator = 4 cols).
clear_overlay_area(buf, area, y, 4);
let overlay_cols = if linked_subagent.is_some() { 7 } else { 4 };
clear_overlay_area(buf, area, y, overlay_cols);
let mut rx = area.x + area.width;
@ -1681,6 +1702,29 @@ impl TasksPane {
Rect::new(rx, y, 3, 1),
));
if linked_subagent.is_some() {
rx = rx.saturating_sub(3);
let is_view_hovered = matches!(
&self.hovered_view,
Some(TaskEntryId::Scheduled(tid)) if tid == task_id
);
let view_style = if is_view_hovered {
Style::default().fg(theme.text_primary)
} else {
Style::default().fg(theme.gray)
};
buf.set_span(
rx,
y,
&Span::styled(crate::glyphs::enlarge_button(), view_style),
3,
);
self.view_button_rects.push((
TaskEntryId::Scheduled(task_id.to_string()),
Rect::new(rx, y, 3, 1),
));
}
if rx > area.x {
buf.set_span(rx - 1, y, &Span::raw(" "), 1);
}
@ -2899,6 +2943,7 @@ mod tests {
created_at: std::time::Instant::now(),
next_fire_at: next.map(|s| s.to_string()),
tag: "loop".into(),
last_subagent_id: None,
}
}