feat(tui): compact live thinking by default

This commit is contained in:
Hunter Bown
2026-05-08 14:13:50 -05:00
parent 69862467c7
commit 4de726abc5
5 changed files with 102 additions and 8 deletions
+25
View File
@@ -180,6 +180,31 @@ pub fn status_line(_app: &mut App) -> CommandResult {
CommandResult::action(AppAction::OpenStatusPicker)
}
/// Toggle whether the live transcript renders full thinking detail.
pub fn verbose(app: &mut App, arg: Option<&str>) -> CommandResult {
let next = match arg.map(str::trim).filter(|s| !s.is_empty()) {
None => !app.verbose_transcript,
Some(raw) => match raw.to_ascii_lowercase().as_str() {
"on" | "true" | "1" | "yes" => true,
"off" | "false" | "0" | "no" => false,
"toggle" => !app.verbose_transcript,
_ => {
return CommandResult::error(
"Usage: /verbose [on|off]. Compact thinking remains available when verbose is off.",
);
}
},
};
app.verbose_transcript = next;
app.mark_history_updated();
CommandResult::message(if next {
"Verbose transcript on: live thinking renders in full."
} else {
"Verbose transcript off: live thinking stays compact."
})
}
/// Persist `tui.status_items` to `~/.deepseek/config.toml` without disturbing
/// the rest of the file. We round-trip through `toml::Value` so any keys we
/// don't know about (provider blocks, MCP, etc.) survive the write
+23
View File
@@ -347,6 +347,12 @@ pub const COMMANDS: &[CommandInfo] = &[
usage: "/theme",
description_id: MessageId::CmdThemeDescription,
},
CommandInfo {
name: "verbose",
aliases: &[],
usage: "/verbose [on|off]",
description_id: MessageId::CmdVerboseDescription,
},
CommandInfo {
name: "trust",
aliases: &[],
@@ -542,6 +548,7 @@ pub fn execute(cmd: &str, app: &mut App) -> CommandResult {
"agent" => config::agent_mode(app),
"plan" => config::plan_mode(app),
"theme" => config::theme(app),
"verbose" => config::verbose(app, arg),
"trust" => config::trust(app, arg),
"logout" => config::logout(app),
@@ -922,6 +929,22 @@ mod tests {
assert!(matches!(result.action, Some(AppAction::OpenConfigView)));
}
#[test]
fn execute_verbose_toggles_live_transcript_detail() {
let mut app = create_test_app();
assert!(!app.verbose_transcript);
let result = execute("/verbose on", &mut app);
assert!(!result.is_error);
assert!(app.verbose_transcript);
assert!(result.message.unwrap().contains("on"));
let result = execute("/verbose off", &mut app);
assert!(!result.is_error);
assert!(!app.verbose_transcript);
assert!(result.message.unwrap().contains("off"));
}
#[test]
fn execute_links_and_aliases_return_links_message() {
let mut app = create_test_app();
+6
View File
@@ -270,6 +270,7 @@ pub enum MessageId {
CmdLspDescription,
CmdShareDescription,
CmdUndoDescription,
CmdVerboseDescription,
CmdYoloDescription,
CmdCacheAdvice,
CmdCacheFootnote,
@@ -460,6 +461,7 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
MessageId::CmdLspDescription,
MessageId::CmdShareDescription,
MessageId::CmdUndoDescription,
MessageId::CmdVerboseDescription,
MessageId::CmdYoloDescription,
MessageId::CmdCacheAdvice,
MessageId::CmdCacheFootnote,
@@ -800,6 +802,7 @@ fn english(id: MessageId) -> &'static str {
"Manage workspace trust and per-path allowlist (`/trust add <path>`, `/trust list`, `/trust on|off`)"
}
MessageId::CmdUndoDescription => "Remove last message pair",
MessageId::CmdVerboseDescription => "Toggle full live thinking in the transcript",
MessageId::CmdYoloDescription => "Enable YOLO mode (shell + trust + auto-approve)",
MessageId::CmdCacheAdvice => {
"Hit/miss ratios over ~70% after the third turn indicate a stable cache prefix; \n\
@@ -1086,6 +1089,7 @@ fn japanese(id: MessageId) -> Option<&'static str> {
"ワークスペースの信頼設定とパス別許可リストを管理(`/trust add <path>`、`/trust list`、`/trust on|off`"
}
MessageId::CmdUndoDescription => "最後のメッセージ対を削除",
MessageId::CmdVerboseDescription => "ライブ思考表示の詳細モードを切り替え",
MessageId::CmdYoloDescription => "YOLO モードを有効化(shell + 信頼 + 自動承認)",
MessageId::CmdCacheAdvice => {
"3 ターン目以降にヒット率が ~70% 以上で安定していれば、プレフィックスキャッシュは健全。\n\
@@ -1344,6 +1348,7 @@ fn chinese_simplified(id: MessageId) -> Option<&'static str> {
"管理工作区信任与按路径的白名单(`/trust add <path>`、`/trust list`、`/trust on|off`"
}
MessageId::CmdUndoDescription => "移除最后一组消息对",
MessageId::CmdVerboseDescription => "切换实时思考内容的完整显示",
MessageId::CmdYoloDescription => "启用 YOLO 模式(shell + 信任 + 自动批准)",
MessageId::CmdCacheAdvice => {
"第 3 轮起命中率稳定在 ~70% 以上即表示前缀缓存稳定;\n\
@@ -1618,6 +1623,7 @@ fn portuguese_brazil(id: MessageId) -> Option<&'static str> {
"Gerenciar a confiança do workspace e a allowlist por caminho (`/trust add <path>`, `/trust list`, `/trust on|off`)"
}
MessageId::CmdUndoDescription => "Remover o último par de mensagens",
MessageId::CmdVerboseDescription => "Alternar pensamento ao vivo completo no transcript",
MessageId::CmdYoloDescription => {
"Ativar o modo YOLO (shell + confiança + aprovação automática)"
}
+3
View File
@@ -724,6 +724,7 @@ pub struct App {
#[allow(dead_code)]
pub fancy_animations: bool,
pub show_thinking: bool,
pub verbose_transcript: bool,
pub show_tool_details: bool,
pub ui_locale: Locale,
pub cost_currency: CostCurrency,
@@ -1308,6 +1309,7 @@ impl App {
low_motion,
fancy_animations,
show_thinking,
verbose_transcript: false,
show_tool_details,
ui_locale,
cost_currency,
@@ -2438,6 +2440,7 @@ impl App {
pub fn transcript_render_options(&self) -> TranscriptRenderOptions {
TranscriptRenderOptions {
show_thinking: self.show_thinking,
verbose: self.verbose_transcript,
show_tool_details: self.show_tool_details,
calm_mode: self.calm_mode,
low_motion: self.low_motion,
+45 -8
View File
@@ -3,7 +3,7 @@
use std::path::{Path, PathBuf};
use std::time::Instant;
use ratatui::style::{Color, Modifier, Style, Stylize};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use serde_json::Value;
use unicode_width::UnicodeWidthStr;
@@ -149,6 +149,7 @@ impl SubAgentCell {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TranscriptRenderOptions {
pub show_thinking: bool,
pub verbose: bool,
pub show_tool_details: bool,
pub calm_mode: bool,
pub low_motion: bool,
@@ -159,6 +160,7 @@ impl Default for TranscriptRenderOptions {
fn default() -> Self {
Self {
show_thinking: true,
verbose: false,
show_tool_details: true,
calm_mode: false,
low_motion: false,
@@ -239,7 +241,7 @@ impl HistoryCell {
width,
*streaming,
*duration_secs,
!*streaming,
!options.verbose,
options.low_motion,
),
HistoryCell::Tool(cell) if !options.show_tool_details => {
@@ -2081,12 +2083,18 @@ fn render_thinking(
lines.push(Line::from(header_spans));
let content_width = width.saturating_sub(3).max(1);
let body_text = if collapsed {
let body_text = if collapsed && streaming {
String::new()
} else if collapsed {
extract_reasoning_summary(content).unwrap_or_else(|| content.trim().to_string())
} else {
content.to_string()
};
let mut rendered = markdown_render::render_markdown(&body_text, content_width, body_style);
let mut rendered = if body_text.trim().is_empty() {
Vec::new()
} else {
markdown_render::render_markdown(&body_text, content_width, body_style)
};
let mut truncated = false;
if collapsed && rendered.len() > THINKING_SUMMARY_LINE_LIMIT {
rendered.truncate(THINKING_SUMMARY_LINE_LIMIT);
@@ -2098,10 +2106,7 @@ fn render_thinking(
if rendered.is_empty() && streaming {
let mut spans = vec![Span::styled(REASONING_RAIL.to_string(), rail_style)];
spans.push(Span::styled(
"reasoning in progress...",
body_style.italic(),
));
spans.push(Span::styled("thinking...", body_style.italic()));
if !low_motion {
spans.push(Span::styled(format!(" {REASONING_CURSOR}"), cursor_style));
}
@@ -3910,6 +3915,38 @@ mod tests {
);
}
#[test]
fn streaming_thinking_live_collapses_unless_verbose() {
let cell = HistoryCell::Thinking {
content: "private step one\nprivate step two".to_string(),
streaming: true,
duration_secs: None,
};
let compact = cell.lines_with_options(
80,
TranscriptRenderOptions {
low_motion: true,
..TranscriptRenderOptions::default()
},
);
let compact_text = lines_text(&compact);
assert!(compact_text.contains("thinking..."));
assert!(!compact_text.contains("private step one"));
let verbose = cell.lines_with_options(
80,
TranscriptRenderOptions {
verbose: true,
low_motion: true,
..TranscriptRenderOptions::default()
},
);
let verbose_text = lines_text(&verbose);
assert!(verbose_text.contains("private step one"));
assert!(verbose_text.contains("private step two"));
}
// === Theme parity tests ===
//
// These lock the visible color/style choices for one plan cell and one