diff --git a/crates/tui/src/palette.rs b/crates/tui/src/palette.rs index 6fbd13e0..b56a6393 100644 --- a/crates/tui/src/palette.rs +++ b/crates/tui/src/palette.rs @@ -82,6 +82,9 @@ pub const SURFACE_TOOL_ACTIVE: Color = Color::Rgb(29, 48, 73); // #1D3049 pub const SURFACE_SUCCESS: Color = Color::Rgb(22, 56, 63); // #16383F #[allow(dead_code)] pub const SURFACE_ERROR: Color = Color::Rgb(63, 27, 36); // #3F1B24 +pub const DIFF_ADDED_BG: Color = Color::Rgb(18, 52, 38); // #123426 dark green tint +pub const DIFF_DELETED_BG: Color = Color::Rgb(52, 22, 28); // #34161C dark red tint +pub const DIFF_ADDED: Color = Color::Rgb(87, 199, 133); // #57C785 pub const ACCENT_REASONING_LIVE: Color = Color::Rgb(146, 198, 248); // #92C6F8 pub const ACCENT_TOOL_LIVE: Color = Color::Rgb(133, 184, 234); // #85B8EA pub const ACCENT_TOOL_ISSUE: Color = Color::Rgb(192, 143, 153); // #C08F99 diff --git a/crates/tui/src/tui/diff_render.rs b/crates/tui/src/tui/diff_render.rs index e05b4029..7303c4fd 100644 --- a/crates/tui/src/tui/diff_render.rs +++ b/crates/tui/src/tui/diff_render.rs @@ -54,7 +54,9 @@ pub fn render_diff(diff: &str, width: u16) -> Vec> { old_line, new_line, '+', - Style::default().fg(palette::STATUS_SUCCESS), + Style::default() + .fg(palette::DIFF_ADDED) + .bg(palette::DIFF_ADDED_BG), )); if let Some(line) = new_line.as_mut() { *line = line.saturating_add(1); @@ -70,7 +72,9 @@ pub fn render_diff(diff: &str, width: u16) -> Vec> { old_line, new_line, '-', - Style::default().fg(palette::STATUS_ERROR), + Style::default() + .fg(palette::STATUS_ERROR) + .bg(palette::DIFF_DELETED_BG), )); if let Some(line) = old_line.as_mut() { *line = line.saturating_add(1); diff --git a/crates/tui/src/tui/history.rs b/crates/tui/src/tui/history.rs index 0e7d107d..6d9572c2 100644 --- a/crates/tui/src/tui/history.rs +++ b/crates/tui/src/tui/history.rs @@ -6,7 +6,7 @@ use std::time::Instant; use ratatui::style::{Color, Modifier, Style, Stylize}; use ratatui::text::{Line, Span}; use serde_json::Value; -use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; +use unicode_width::UnicodeWidthStr; use crate::deepseek_theme::active_theme; use crate::models::{ContentBlock, Message}; @@ -37,6 +37,11 @@ const USER_GLYPH: &str = "\u{258E}"; // ▎ /// Visual marker for the assistant role. Solid bullet that pulses at 2s /// cycle while the response is streaming, holds full brightness when idle. const ASSISTANT_GLYPH: &str = "\u{25CF}"; // ● +/// Transcript body left rail. Solid 1/8 block (`▏`) followed by a space — +/// used as a visual left-margin anchor for continuation lines, tool-card +/// detail rows, and affordance lines. Dimmed so it guides the eye without +/// competing with content. +const TRANSCRIPT_RAIL: &str = "\u{258F} "; // ▏ + space /// Reasoning header opener. Replaces the spinner glyph on thinking cells — /// reasoning is a slow exhale, not a tool spin. const REASONING_OPENER: &str = "\u{2026}"; // … @@ -478,7 +483,10 @@ fn render_archived_context( let rendered = crate::tui::markdown_render::render_markdown(&body, content_width, body_style); for (idx, line) in rendered.into_iter().enumerate() { if idx == 0 { - let mut spans = vec![Span::styled("▏ ", Style::default().fg(palette::TEXT_DIM))]; + let mut spans = vec![Span::styled( + TRANSCRIPT_RAIL.to_string(), + Style::default().fg(palette::TEXT_DIM), + )]; spans.extend(line.spans); lines.push(Line::from(spans)); } else { @@ -1608,7 +1616,7 @@ fn render_checklist_change_card( let (marker, marker_color) = checklist_status_marker(&change.status); let prefix = format!("{marker} "); let prefix_width = - UnicodeWidthStr::width("\u{258F} ") + UnicodeWidthStr::width(prefix.as_str()); + UnicodeWidthStr::width(TRANSCRIPT_RAIL) + UnicodeWidthStr::width(prefix.as_str()); let id_label = format!("Todo #{}", change.id); let arrow = " \u{2192} "; let status_label = change.status.clone(); @@ -1702,7 +1710,7 @@ fn render_checklist_card( let prefix = format!("{marker} "); // Reserve room for the rail + marker prefix when wrapping content. let prefix_width = - UnicodeWidthStr::width("\u{258F} ") + UnicodeWidthStr::width(prefix.as_str()); + UnicodeWidthStr::width(TRANSCRIPT_RAIL) + UnicodeWidthStr::width(prefix.as_str()); let content_width = usize::from(width).saturating_sub(prefix_width).max(1); for (idx, part) in wrap_text(item.content.trim(), content_width) .into_iter() @@ -2044,7 +2052,7 @@ fn render_thinking( // 12% reasoning surface tint over the app ink — the only deliberately // warm element in the transcript. Dropped on Ansi-16 terminals where the // tint would distort the named palette. - let depth = palette::ColorDepth::detect(); + let depth = cached_color_depth(); let body_bg = palette::reasoning_surface_tint(depth); let body_style = match body_bg { Some(bg) => style.italic().bg(bg), @@ -2153,9 +2161,13 @@ fn render_message( let indent = if prefix.is_empty() { String::new() } else { - " ".repeat(prefix_width + 1) + let mut s = String::with_capacity(prefix_width + 1); + s.push('\u{258F}'); + s.extend(std::iter::repeat_n(' ', prefix_width)); + s }; - let mut spans = vec![Span::raw(indent)]; + let rail_style = Style::default().fg(palette::TEXT_DIM); + let mut spans = vec![Span::styled(indent, rail_style)]; spans.extend(line.spans); lines.push(Line::from(spans)); } @@ -2485,16 +2497,16 @@ fn file_line_style(text: &str) -> Option