Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions crates/tui/src/core/engine/lsp_hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ impl Engine {
return;
}
let paths = edited_paths_for_tool(tool_name, tool_input);
let mut found = 0usize;
let mut files = 0usize;
for path in paths {
let absolute = if path.is_absolute() {
path.clone()
Expand All @@ -64,9 +66,21 @@ impl Engine {
// batch by sequence.
let seq = self.turn_counter;
if let Some(block) = self.lsp_manager.diagnostics_for(&absolute, seq).await {
found = found.saturating_add(block.items.len());
files = files.saturating_add(1);
self.pending_lsp_blocks.push(block);
}
}
if found > 0 {
let _ = self
.tx_event
.send(Event::LspRepairUpdate {
diagnostics_found: found,
files,
injected: false,
})
.await;
}
}

/// Drain `pending_lsp_blocks` into a single synthetic user message so the
Expand All @@ -79,6 +93,8 @@ impl Engine {
return;
}
let blocks = std::mem::take(&mut self.pending_lsp_blocks);
let found: usize = blocks.iter().map(|b| b.items.len()).sum();
let files = blocks.len();
let rendered = crate::lsp::render_blocks(&blocks);
if rendered.is_empty() {
return;
Expand All @@ -88,5 +104,13 @@ impl Engine {
crate::core::ops::UserInputProvenance::Runtime,
))
.await;
let _ = self
.tx_event
.send(Event::LspRepairUpdate {
diagnostics_found: found,
files,
injected: true,
})
.await;
}
}
8 changes: 8 additions & 0 deletions crates/tui/src/core/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,14 @@ pub enum Event {
blocked_write: bool,
},

/// Observable LSP repair-loop update for the Turn Inspector (#4107).
/// Carries only summary counts/state — never raw prompt internals.
LspRepairUpdate {
diagnostics_found: usize,
files: usize,
injected: bool,
},

// === Prefix-Cache Stability Events ===
/// The prefix (system prompt + tool specs) changed between turns,
/// which invalidates DeepSeek's KV prefix cache. Carries diagnostics
Expand Down
62 changes: 45 additions & 17 deletions crates/tui/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -373,24 +373,25 @@ impl Default for Settings {
// making long-session continuity the default runtime behavior.
auto_compact: false,
auto_compact_threshold_percent: 80.0,
calm_mode: false,
// #4095: default presentation is compact/calm; verbose detail is opt-in.
calm_mode: true,
tool_collapse_mode: "compact".to_string(),
low_motion: false,
fancy_animations: true,
low_motion: true,
fancy_animations: false,
bracketed_paste: true,
paste_burst_detection: true,
mention_menu_limit: 128,
mention_walk_depth: 10,
mention_menu_behavior: "fuzzy".to_string(),
show_thinking: true,
show_tool_details: true,
show_tool_details: false,
locale: "auto".to_string(),
theme: "system".to_string(),
background_color: None,
composer_density: "comfortable".to_string(),
composer_border: true,
composer_vim_mode: "normal".to_string(),
transcript_spacing: "comfortable".to_string(),
transcript_spacing: "compact".to_string(),
default_mode: "agent".to_string(),
sidebar_width_percent: 28,
sidebar_focus: "pinned".to_string(),
Expand Down Expand Up @@ -421,7 +422,7 @@ impl Default for Settings {
pub const CALM_PRESET_FIELDS: &[(&str, &str)] = &[
("calm_mode", "true"),
("tool_collapse", "calm"),
("transcript_spacing", "comfortable"),
("transcript_spacing", "compact"),
("low_motion", "true"),
("fancy_animations", "false"),
("show_tool_details", "false"),
Expand Down Expand Up @@ -1522,11 +1523,22 @@ fn env_truthy(name: &str) -> bool {
mod tests {
use super::*;

/// Explicit animated baseline for env-force tests (#4095 flipped defaults to calm).
fn animated_settings() -> Settings {
let mut s = Settings::default();
s.calm_mode = false;
s.low_motion = false;
s.fancy_animations = true;
s.show_tool_details = true;
s.transcript_spacing = "comfortable".to_string();
s
}

#[test]
fn apply_preset_calm_sets_bundle_and_preserves_evidence() {
let mut settings = Settings::default();
// Defaults are the debug-visible posture.
assert!(!settings.calm_mode);
// Defaults are already the calm/compact posture (#4095).
assert!(settings.calm_mode);
assert!(settings.show_thinking);

let changed = settings.apply_preset("CALM").expect("calm preset applies");
Expand All @@ -1540,7 +1552,7 @@ mod tests {

assert!(settings.calm_mode);
assert_eq!(settings.tool_collapse_mode, "calm");
assert_eq!(settings.transcript_spacing, "comfortable");
assert_eq!(settings.transcript_spacing, "compact");
assert!(settings.low_motion);
assert!(!settings.fancy_animations);
assert!(!settings.show_tool_details);
Expand All @@ -1552,6 +1564,19 @@ mod tests {
);
}

#[test]
fn default_settings_are_compact_presentation() {
let settings = Settings::default();
assert!(settings.calm_mode);
assert!(!settings.show_tool_details);
assert!(settings.low_motion);
assert!(!settings.fancy_animations);
assert_eq!(settings.transcript_spacing, "compact");
assert_eq!(settings.tool_collapse_mode, "compact");
// Thinking stays visible — compact is not "hide evidence".
assert!(settings.show_thinking);
}

#[test]
fn apply_preset_rejects_unknown_name() {
let mut settings = Settings::default();
Expand Down Expand Up @@ -1595,7 +1620,10 @@ mod tests {
#[test]
fn default_settings_show_footer_water_strip() {
let settings = Settings::default();
assert!(settings.fancy_animations);
assert!(
!settings.fancy_animations,
"default presentation is calm (#4095)"
);
}

#[test]
Expand Down Expand Up @@ -1901,7 +1929,7 @@ mod tests {
unsafe {
std::env::set_var("NO_ANIMATIONS", "1");
}
let mut settings = Settings::default();
let mut settings = animated_settings();
assert!(!settings.low_motion, "default is animated");
assert!(settings.fancy_animations, "default shows the water strip");
settings.apply_env_overrides();
Expand Down Expand Up @@ -2053,7 +2081,7 @@ mod tests {
unsafe {
std::env::set_var("TERM_PROGRAM", "vscode");
}
let mut settings = Settings::default();
let mut settings = animated_settings();
assert!(!settings.low_motion, "default is animated");
settings.apply_env_overrides();
assert!(
Expand Down Expand Up @@ -2081,7 +2109,7 @@ mod tests {
unsafe {
std::env::set_var("TERM_PROGRAM", "Ghostty");
}
let mut settings = Settings::default();
let mut settings = animated_settings();
assert!(!settings.low_motion, "default is animated");
settings.apply_env_overrides();
assert!(
Expand Down Expand Up @@ -2219,7 +2247,7 @@ mod tests {
std::env::remove_var("TERMINATOR_UUID");
std::env::set_var(var, val);
}
let mut settings = Settings::default();
let mut settings = animated_settings();
assert!(!settings.low_motion, "default is animated");
settings.apply_env_overrides();
assert!(
Expand Down Expand Up @@ -2257,7 +2285,7 @@ mod tests {
unsafe {
std::env::set_var("TERM_PROGRAM", "Termius");
}
let mut settings = Settings::default();
let mut settings = animated_settings();
assert!(!settings.low_motion, "default is animated");
settings.apply_env_overrides();
assert!(
Expand Down Expand Up @@ -2345,7 +2373,7 @@ mod tests {
}
}

let mut settings = Settings::default();
let mut settings = animated_settings();
assert!(!settings.low_motion, "default is animated");
assert!(settings.fancy_animations, "default shows the water strip");
assert_eq!(settings.synchronized_output, "auto");
Expand Down Expand Up @@ -2451,7 +2479,7 @@ mod tests {
}
std::env::set_var(var, val);
}
let mut settings = Settings::default();
let mut settings = animated_settings();
assert!(!settings.low_motion, "default is animated");
assert!(settings.fancy_animations, "default shows the water strip");
settings.apply_env_overrides();
Expand Down
27 changes: 27 additions & 0 deletions crates/tui/src/tui/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,30 @@ pub struct AgentProgressMeta {
pub spawn_depth: u32,
}

/// Per-turn LSP repair-loop summary for the Turn Inspector (#4107).
/// Observable state only — no raw diagnostic text or prompt internals.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LspRepairState {
pub diagnostics_found: usize,
pub files_touched: usize,
pub injected: bool,
pub repair_attempted: bool,
/// "resolved" | "still_failing" | "unknown" | "unavailable"
pub latest: &'static str,
}

impl Default for LspRepairState {
fn default() -> Self {
Self {
diagnostics_found: 0,
files_touched: 0,
injected: false,
repair_attempted: false,
latest: "unavailable",
}
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ComposerDensity {
Compact,
Expand Down Expand Up @@ -2200,6 +2224,8 @@ pub struct App {
/// Whether LSP diagnostics are currently enabled. Mirrors the config file
/// `[lsp].enabled` setting. Toggled at runtime via `/lsp on|off`.
pub lsp_enabled: bool,
/// Current-turn LSP repair-loop summary for Ctrl-O Turn Inspector (#4107).
pub lsp_repair: LspRepairState,
/// Derived title for the current session shown in the composer border.
/// Updated when `EngineEvent::SessionUpdated` fires or a saved session is loaded.
pub session_title: Option<String>,
Expand Down Expand Up @@ -2944,6 +2970,7 @@ impl App {
collapsed_cell_map: Vec::new(),
edit_in_progress: false,
lsp_enabled: config.lsp.as_ref().and_then(|l| l.enabled).unwrap_or(true),
lsp_repair: LspRepairState::default(),
composer_arrows_scroll: config
.tui
.as_ref()
Expand Down
23 changes: 10 additions & 13 deletions crates/tui/src/tui/history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1319,19 +1319,16 @@ impl GenericToolCell {
}

// Sub-agent launch already gets a dedicated `DelegateCard`
// that owns the live action tree, status, and final summary. The
// generic tool block for the same call duplicates that signal at
// 3-4 lines per spawn — N parallel spawns multiply the noise. In
// live mode, render one compact summary line and let the
// DelegateCard be the source of truth. Transcript mode keeps the
// full block for spawns so session replay remains complete, but
// inspection calls (peek/status/wait) stay compact there too — a
// full projection dump per check is exactly the dogfood-A5 noise
// and carries no replay value (#4112).
if self.name == "agent"
&& (matches!(mode, RenderMode::Live) || agent_activity::is_agent_inspection(self))
{
return agent_activity::render_agent_compact(self, low_motion);
// that owns the live action tree, status, and final summary (#4133).
// Spawns therefore render nothing here in either mode — one visible
// artifact per delegated unit. Inspection/join calls (peek/status/
// wait) stay as a single compact line (#4112 dogfood A5).
if self.name == "agent" {
if agent_activity::is_agent_inspection(self) {
return agent_activity::render_agent_compact(self, low_motion);
}
// Spawn / start / run: suppress the generic tool card entirely.
return Vec::new();
}

// A call to a tool that doesn't exist carries exactly one useful
Expand Down
14 changes: 8 additions & 6 deletions crates/tui/src/tui/history/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2857,15 +2857,17 @@ fn agent_inspection_stays_compact_in_transcript_mode() {
}

#[test]
fn agent_spawn_keeps_full_block_in_transcript_mode() {
fn agent_spawn_suppresses_generic_card_in_favor_of_delegate_card() {
let cell = agent_cell(
Some("prompt: map the repo"),
ToolStatus::Success,
Some(r#"{"agent_id":"agent_scout_1","status":"running"}"#),
);
let lines = cell.lines_with_mode(120, true, super::RenderMode::Transcript);
assert!(
lines.len() > 1,
"spawns keep the full block for session replay: {lines:?}"
);
for mode in [super::RenderMode::Live, super::RenderMode::Transcript] {
let lines = cell.lines_with_mode(120, true, mode);
assert!(
lines.is_empty(),
"spawn generic tool card must yield to DelegateCard (#4133): {mode:?} {lines:?}"
);
}
}
30 changes: 30 additions & 0 deletions crates/tui/src/tui/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2500,6 +2500,7 @@ async fn run_event_loop(
app.is_loading = true;
app.offline_mode = false;
app.turn_error_posted = false;
app.lsp_repair = crate::tui::app::LspRepairState::default();
app.prompt_suggestion = None;
app.prompt_suggestion_gen
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
Expand Down Expand Up @@ -3000,6 +3001,35 @@ async fn run_event_loop(
}
}
}
EngineEvent::LspRepairUpdate {
diagnostics_found,
files,
injected,
} => {
let repair = &mut app.lsp_repair;
repair.diagnostics_found =
repair.diagnostics_found.saturating_add(diagnostics_found);
repair.files_touched = repair.files_touched.saturating_add(files);
if injected {
// Injection itself is not a repair attempt — the model
// has only been shown the diagnostics so far (#4107).
repair.injected = true;
if repair.latest == "unavailable" || repair.latest.is_empty() {
repair.latest = "unknown";
}
} else if repair.injected {
// Diagnostics after a prior injection imply the model
// edited again (a repair attempt). Zero findings = resolved.
repair.repair_attempted = true;
repair.latest = if diagnostics_found == 0 {
"resolved"
} else {
"still_failing"
};
} else {
repair.latest = "unknown";
}
}
EngineEvent::PauseEvents { ack } => {
if !event_broker.is_paused() {
pause_terminal(
Expand Down
Loading
Loading