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) <noreply@anthropic.com>
This commit is contained in:
Hunter Bown
2026-05-12 01:37:32 -05:00
parent ebedd23807
commit ae42bdb2c0
2 changed files with 42 additions and 13 deletions
+7
View File
@@ -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
+35 -13
View File
@@ -1864,24 +1864,23 @@ fn build_empty_state_lines(app: &App, area: Rect) -> Vec<Line<'static>> {
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<String> {
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::<String>()
})
.collect::<Vec<_>>()
.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.