feat(tui): compact live thinking by default
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)"
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user