From ae42bdb2c09daba95de3625dad73a8388b7bf48e Mon Sep 17 00:00:00 2001 From: Hunter Bown Date: Tue, 12 May 2026 01:37:32 -0500 Subject: [PATCH] refactor(tui): clarify startup empty state with version / model / cwd context The center of the startup welcome view used to repeat information already shown in the header and footer (active model and mode names). It now shows three pieces of context that first-time users don't otherwise see at a glance: - the build version (so users on stale installs notice it before reaching `deepseek doctor`) - the active model with a `/model` hint so the picker is discoverable from the empty state - the current working directory so users can confirm the workspace deepseek-tui anchored at The header and footer continue to show the running model and mode for the active session; this change is only about the center "empty transcript" panel that sits in the gap before the first user message lands. Harvested from PR #1444 by @reidliu41 Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 7 +++++ crates/tui/src/tui/widgets/mod.rs | 48 ++++++++++++++++++++++--------- 2 files changed, 42 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2784e012..5b4f1aee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -153,6 +153,13 @@ real world uses." ### Added +- **Startup empty-state shows useful context instead of + repeating the header** (harvested from PR #1444 by + **@reidliu41**). The center of the welcome view used to repeat + information already displayed in the header and footer. It now + shows the build version, the active model with a `/model` + hint, and the current working directory so first-time users + have somewhere to look while they decide what to type. - **Opt-in `v4-best-practices` bundled skill** (harvested from PR #1448 by **@SamhandsomeLee**). A single 50-line `SKILL.md` encoding three V4-specific workflow rules for multi-step diff --git a/crates/tui/src/tui/widgets/mod.rs b/crates/tui/src/tui/widgets/mod.rs index da7bb9d6..1250e2c6 100644 --- a/crates/tui/src/tui/widgets/mod.rs +++ b/crates/tui/src/tui/widgets/mod.rs @@ -1864,24 +1864,23 @@ fn build_empty_state_lines(app: &App, area: Rect) -> Vec> { return Vec::new(); } - let workspace_name = app - .workspace - .file_name() - .and_then(|value| value.to_str()) - .filter(|value| !value.is_empty()) - .map(std::string::ToString::to_string) - .unwrap_or_else(|| app.workspace.to_string_lossy().into_owned()); + let workspace = crate::utils::display_path(&app.workspace); let body_width = usize::from(area.width.saturating_sub(8).clamp(24, 72)); let left_padding = usize::from(area.width.saturating_sub(body_width as u16) / 2); let inset = " ".repeat(left_padding); let body = vec![ Line::from(Span::styled( - format!("{inset}DeepSeek TUI"), + format!("{inset}>_ DeepSeek TUI (v{})", env!("CARGO_PKG_VERSION")), Style::default().fg(palette::DEEPSEEK_BLUE).bold(), )), + Line::from(""), Line::from(Span::styled( - format!("{inset}{workspace_name} ยท {}", app.model), + format!("{inset}model: {} /model to switch", app.model), + Style::default().fg(palette::TEXT_MUTED), + )), + Line::from(Span::styled( + format!("{inset}directory: {workspace}"), Style::default().fg(palette::TEXT_MUTED), )), ]; @@ -2186,10 +2185,10 @@ fn wrap_text(text: &str, width: usize) -> Vec { mod tests { use super::{ ApprovalWidget, COMPOSER_PANEL_HEIGHT, ChatWidget, ComposerWidget, Renderable, - SlashMenuEntry, apply_selection_to_line, composer_height, composer_max_height, - composer_min_input_rows, composer_top_padding, compute_takeover_area, cursor_row_col, - layout_input, pad_lines_to_bottom, placeholder_visual_lines, should_render_empty_state, - slash_completion_hints, wrap_input_lines, wrap_text, + SlashMenuEntry, apply_selection_to_line, build_empty_state_lines, composer_height, + composer_max_height, composer_min_input_rows, composer_top_padding, compute_takeover_area, + cursor_row_col, layout_input, pad_lines_to_bottom, placeholder_visual_lines, + should_render_empty_state, slash_completion_hints, wrap_input_lines, wrap_text, }; use crate::config::Config; use crate::localization::Locale; @@ -2699,6 +2698,29 @@ mod tests { assert!(!should_render_empty_state(&app)); } + #[test] + fn empty_state_shows_startup_context() { + let mut app = create_test_app(); + app.workspace = PathBuf::from("/tmp/deepseek-test-workspace"); + app.model = "deepseek-v4-pro".to_string(); + + let lines = build_empty_state_lines(&app, Rect::new(0, 0, 100, 20)); + let rendered = lines + .iter() + .map(|line| { + line.spans + .iter() + .map(|span| span.content.as_ref()) + .collect::() + }) + .collect::>() + .join("\n"); + + assert!(rendered.contains(&format!(">_ DeepSeek TUI (v{})", env!("CARGO_PKG_VERSION")))); + assert!(rendered.contains("model: deepseek-v4-pro /model to switch")); + assert!(rendered.contains("directory: /tmp/deepseek-test-workspace")); + } + /// Probe: confirm `cell.lines_with_motion` returns no Line whose total /// visual width exceeds the requested area width, even for pathological /// long single-line tool results.