From 2add182b0b1210fa0e85e97e02651ede841cf7c8 Mon Sep 17 00:00:00 2001 From: Val Alexander <68980965+BunsDev@users.noreply.github.com> Date: Fri, 29 May 2026 20:48:01 -0500 Subject: [PATCH 1/2] fix(query): keep tool turns active until final stop --- src-rust/crates/query/src/lib.rs | 46 +++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/src-rust/crates/query/src/lib.rs b/src-rust/crates/query/src/lib.rs index 34d3cb1..926f7ba 100644 --- a/src-rust/crates/query/src/lib.rs +++ b/src-rust/crates/query/src/lib.rs @@ -689,6 +689,14 @@ const MAX_TOKENS_RECOVERY_MSG: &str = you were doing. Pick up mid-thought if that is where the cut happened. \ Break remaining work into smaller pieces."; +fn should_emit_turn_complete(stop: &str, max_tokens_recovery_count: u32) -> bool { + match stop { + "tool_use" => false, + "max_tokens" => max_tokens_recovery_count >= MAX_TOKENS_RECOVERY_LIMIT, + _ => true, + } +} + // Spinner verbs are imported from claurst_core::spinner /// Run the agentic query loop. @@ -1700,12 +1708,14 @@ pub async fn run_query_loop( } } - if let Some(ref tx) = event_tx { - let _ = tx.send(QueryEvent::TurnComplete { - turn, - stop_reason: stop.to_string(), - usage: Some(usage.clone()), - }); + if should_emit_turn_complete(stop, max_tokens_recovery_count) { + if let Some(ref tx) = event_tx { + let _ = tx.send(QueryEvent::TurnComplete { + turn, + stop_reason: stop.to_string(), + usage: Some(usage.clone()), + }); + } } // Helper closure for firing the Stop hook. @@ -2459,6 +2469,30 @@ mod tests { serde_json::json!(10_000) ); } + + #[test] + fn turn_complete_emission_skips_intermediate_tool_turns() { + assert!(!should_emit_turn_complete("tool_use", 0)); + } + + #[test] + fn turn_complete_emission_skips_recoverable_max_tokens_turns() { + assert!(!should_emit_turn_complete("max_tokens", 0)); + assert!(!should_emit_turn_complete("max_tokens", 1)); + assert!(!should_emit_turn_complete("max_tokens", 2)); + assert!(should_emit_turn_complete( + "max_tokens", + MAX_TOKENS_RECOVERY_LIMIT + )); + } + + #[test] + fn turn_complete_emission_keeps_terminal_stop_reasons() { + assert!(should_emit_turn_complete("end_turn", 0)); + assert!(should_emit_turn_complete("stop_sequence", 0)); + assert!(should_emit_turn_complete("content_filtered", 0)); + assert!(should_emit_turn_complete("unknown_stop", 0)); + } } /// Stream handler that forwards events to an unbounded channel. From eb05e68c3a6822c362d89ce824bdcb609cbda001 Mon Sep 17 00:00:00 2001 From: Val Alexander <68980965+BunsDev@users.noreply.github.com> Date: Sat, 30 May 2026 16:32:04 -0500 Subject: [PATCH 2/2] feat(tui): color-coded diffs in Claude Code style Full-row red/green tint for removed/added lines (gutter, marker, and content), brighter saturated bg on inline word-level changes, soft brand-consistent fg colors, and a single-line slate hunk header that no longer doubles the @@ prefix. Co-Authored-By: Claude Opus 4.7 (1M context) --- src-rust/crates/tui/src/diff_viewer.rs | 189 +++++++++++++++++++------ 1 file changed, 146 insertions(+), 43 deletions(-) diff --git a/src-rust/crates/tui/src/diff_viewer.rs b/src-rust/crates/tui/src/diff_viewer.rs index a02a32f..975ab38 100644 --- a/src-rust/crates/tui/src/diff_viewer.rs +++ b/src-rust/crates/tui/src/diff_viewer.rs @@ -27,6 +27,23 @@ use crate::overlays::{ static SYNTAX_SET: Lazy = Lazy::new(SyntaxSet::load_defaults_newlines); static THEME_SET: Lazy = Lazy::new(ThemeSet::load_defaults); +// --------------------------------------------------------------------------- +// Diff palette — tuned to match Claude Code's terminal diff: +// • dim red/green tint across the entire row for removed/added lines +// • brighter highlight bg on inline word-level changes inside that row +// • soft red/green foreground for markers so they pop on the tint +// • subtle slate bg for hunk headers +// --------------------------------------------------------------------------- +const DIFF_BG_REMOVED: Color = Color::Rgb( 52, 18, 24); // dim red row tint +const DIFF_BG_ADDED: Color = Color::Rgb( 14, 44, 22); // dim green row tint +const DIFF_BG_WORD_DEL: Color = Color::Rgb(150, 38, 52); // bright red — changed word +const DIFF_BG_WORD_INS: Color = Color::Rgb( 34, 120, 52); // bright green — changed word +const DIFF_FG_REMOVED: Color = Color::Rgb(255, 168, 178); // soft red text/marker +const DIFF_FG_ADDED: Color = Color::Rgb(168, 240, 184); // soft green text/marker +const DIFF_FG_GUTTER: Color = Color::Rgb(108, 108, 122); // dim line-number gutter +const DIFF_FG_HEADER: Color = Color::Rgb(167, 139, 250); // hunk header (@@ lines) +const DIFF_BG_HEADER: Color = Color::Rgb( 18, 18, 28); // subtle slate band + // --------------------------------------------------------------------------- // Data types // --------------------------------------------------------------------------- @@ -744,18 +761,20 @@ fn render_diff_detail(state: &DiffViewerState, area: Rect, buf: &mut Buffer) { return; } - // Build lines for rendering - let lines = build_diff_lines(file, inner.width); - let total_lines = lines.len(); - let scroll = (state.detail_scroll as usize).min(total_lines.saturating_sub(inner.height as usize)); - let visible = &lines[scroll..]; - - // Shrink inner width by 1 to leave room for scrollbar - let text_width = if total_lines > inner.height as usize { + // The diff lines are width-padded so the bg tint extends across the panel, + // so pre-compute the rendered width and pass it in (avoids re-padding past + // the scrollbar column). + let raw_line_count: usize = file.hunks.iter().map(|h| h.lines.len()).sum(); + let needs_scrollbar = raw_line_count > inner.height as usize; + let text_width = if needs_scrollbar { inner.width.saturating_sub(1) } else { inner.width }; + let lines = build_diff_lines(file, text_width); + let total_lines = lines.len(); + let scroll = (state.detail_scroll as usize).min(total_lines.saturating_sub(inner.height as usize)); + let visible = &lines[scroll..]; for (i, line) in visible.iter().enumerate() { if i as u16 >= inner.height { break; } @@ -853,13 +872,19 @@ fn build_inline_diff_spans(old: &str, new: &str) -> (Vec>, Vec { old_spans.push(Span::styled( s, - Style::default().fg(Color::White).bg(Color::Rgb(150, 30, 30)), + Style::default() + .fg(Color::White) + .bg(DIFF_BG_WORD_DEL) + .add_modifier(Modifier::BOLD), )); } ChangeTag::Insert => { new_spans.push(Span::styled( s, - Style::default().fg(Color::White).bg(Color::Rgb(30, 130, 30)), + Style::default() + .fg(Color::White) + .bg(DIFF_BG_WORD_INS) + .add_modifier(Modifier::BOLD), )); } } @@ -933,7 +958,8 @@ fn build_diff_lines(file: &FileDiffStats, width: u16) -> Vec> { // Gutter = 10 chars ("dddd dddd "), prefix marker = 3 chars ("+ " etc.) let gutter_width: usize = 10; let prefix_width: usize = 3; - let avail = (width as usize).saturating_sub(gutter_width + prefix_width); + let total_width = width as usize; + let avail = total_width.saturating_sub(gutter_width + prefix_width); for hunk in &file.hunks { let hunk_lines = &hunk.lines; @@ -951,21 +977,43 @@ fn build_diff_lines(file: &FileDiffStats, width: u16) -> Vec> { let mut removed_row = vec![ Span::styled( format_gutter(diff_line.old_line_no, None), - Style::default().fg(Color::DarkGray), + Style::default().fg(DIFF_FG_GUTTER).bg(DIFF_BG_REMOVED), ), - Span::styled("- ", Style::default().fg(Color::Red)), + Span::styled( + "- ", + Style::default() + .fg(DIFF_FG_REMOVED) + .bg(DIFF_BG_REMOVED) + .add_modifier(Modifier::BOLD), + ), + Span::styled(" ", Style::default().bg(DIFF_BG_REMOVED)), ]; - removed_row.extend(truncate_spans_to_width(old_spans, avail)); + let mut old_clipped = truncate_spans_to_width(old_spans, avail); + recolor_equal_spans(&mut old_clipped, DIFF_FG_REMOVED); + apply_row_bg(&mut old_clipped, DIFF_BG_REMOVED); + removed_row.extend(old_clipped); + pad_to_width(&mut removed_row, total_width, DIFF_BG_REMOVED); lines.push(Line::from(removed_row)); let mut added_row = vec![ Span::styled( format_gutter(None, next_line.new_line_no), - Style::default().fg(Color::DarkGray), + Style::default().fg(DIFF_FG_GUTTER).bg(DIFF_BG_ADDED), ), - Span::styled("+ ", Style::default().fg(Color::Green)), + Span::styled( + "+ ", + Style::default() + .fg(DIFF_FG_ADDED) + .bg(DIFF_BG_ADDED) + .add_modifier(Modifier::BOLD), + ), + Span::styled(" ", Style::default().bg(DIFF_BG_ADDED)), ]; - added_row.extend(truncate_spans_to_width(new_spans, avail)); + let mut new_clipped = truncate_spans_to_width(new_spans, avail); + recolor_equal_spans(&mut new_clipped, DIFF_FG_ADDED); + apply_row_bg(&mut new_clipped, DIFF_BG_ADDED); + added_row.extend(new_clipped); + pad_to_width(&mut added_row, total_width, DIFF_BG_ADDED); lines.push(Line::from(added_row)); i += 2; @@ -974,42 +1022,64 @@ fn build_diff_lines(file: &FileDiffStats, width: u16) -> Vec> { } } - // Standard single-line rendering - let (marker, content_style) = match diff_line.kind { - DiffLineKind::Header => ( - Span::styled("@@ ", Style::default().fg(Color::Rgb(167, 139, 250))), - Style::default().fg(Color::Rgb(167, 139, 250)), - ), - DiffLineKind::Added => ( - Span::styled("+ ", Style::default().fg(Color::Green)), - Style::default().fg(Color::Green), - ), - DiffLineKind::Removed => ( - Span::styled("- ", Style::default().fg(Color::Red)), - Style::default().fg(Color::Red), - ), - DiffLineKind::Context => ( - Span::styled(" ", Style::default().fg(Color::DarkGray)), - Style::default().fg(Color::White), - ), + // Hunk header: render the @@ line full-width on a slate band, no gutter/marker. + if diff_line.kind == DiffLineKind::Header { + let content: String = diff_line.content.chars().take(total_width).collect(); + let mut row = vec![Span::styled( + format!(" {} ", content), + Style::default() + .fg(DIFF_FG_HEADER) + .bg(DIFF_BG_HEADER) + .add_modifier(Modifier::BOLD), + )]; + pad_to_width(&mut row, total_width, DIFF_BG_HEADER); + lines.push(Line::from(row)); + i += 1; + continue; + } + + // Added / Removed / Context rows. + let (marker_text, marker_fg, row_bg, fallback_fg): (&str, Color, Option, Color) = + match diff_line.kind { + DiffLineKind::Added => ("+ ", DIFF_FG_ADDED, Some(DIFF_BG_ADDED), DIFF_FG_ADDED), + DiffLineKind::Removed => ("- ", DIFF_FG_REMOVED, Some(DIFF_BG_REMOVED), DIFF_FG_REMOVED), + DiffLineKind::Context => (" ", DIFF_FG_GUTTER, None, COVEN_CODE_TEXT), + DiffLineKind::Header => unreachable!(), + }; + + let gutter_style = match row_bg { + Some(bg) => Style::default().fg(DIFF_FG_GUTTER).bg(bg), + None => Style::default().fg(DIFF_FG_GUTTER), + }; + let marker_style = match row_bg { + Some(bg) => Style::default().fg(marker_fg).bg(bg).add_modifier(Modifier::BOLD), + None => Style::default().fg(marker_fg), }; let ln_str = format_gutter(diff_line.old_line_no, diff_line.new_line_no); let content: String = diff_line.content.chars().take(avail).collect(); let mut row = vec![ - Span::styled(ln_str, Style::default().fg(Color::DarkGray)), - marker, + Span::styled(ln_str, gutter_style), + Span::styled(marker_text.to_string(), marker_style), + Span::styled(" ", row_bg.map(|bg| Style::default().bg(bg)).unwrap_or_default()), ]; - // Apply syntax highlighting for code lines (not headers) - if diff_line.kind == DiffLineKind::Header { - row.push(Span::styled(content, content_style)); - } else { - let highlighted = highlight_code_line(&content, &file.path, content_style); - row.extend(highlighted); + // Syntax-highlight the content. The fallback fg is used only for ranges + // where syntect returned a near-default color (so the diff tint reads cleanly). + let mut highlighted = highlight_code_line( + &content, + &file.path, + Style::default().fg(fallback_fg), + ); + if let Some(bg) = row_bg { + apply_row_bg(&mut highlighted, bg); } + row.extend(highlighted); + if let Some(bg) = row_bg { + pad_to_width(&mut row, total_width, bg); + } lines.push(Line::from(row)); i += 1; @@ -1019,6 +1089,39 @@ fn build_diff_lines(file: &FileDiffStats, width: u16) -> Vec> { lines } +/// Add `bg` to every span that doesn't already declare a background. +/// Preserves word-level inline highlight bgs (which are already set). +fn apply_row_bg(spans: &mut Vec>, bg: Color) { + for span in spans.iter_mut() { + if span.style.bg.is_none() { + span.style = span.style.bg(bg); + } + } +} + +/// Pad `spans` out to `width` characters by appending a single bg-only space span. +fn pad_to_width(spans: &mut Vec>, width: usize, bg: Color) { + let used: usize = spans.iter().map(|s| s.content.chars().count()).sum(); + if used < width { + spans.push(Span::styled( + " ".repeat(width - used), + Style::default().bg(bg), + )); + } +} + +/// Inside an inline-diff row, the COVEN_CODE_TEXT fg used by `build_inline_diff_spans` +/// for the unchanged-equal segments is too neutral against the dim red/green row tint. +/// Recolor those plain-text spans with `fg` so the row reads as a single coherent +/// removed/added block, while leaving the highlighted word spans alone. +fn recolor_equal_spans(spans: &mut Vec>, fg: Color) { + for span in spans.iter_mut() { + if span.style.bg.is_none() { + span.style = span.style.fg(fg); + } + } +} + // --------------------------------------------------------------------------- // Tests // ---------------------------------------------------------------------------