fix(tui): first-run copy batch — sequential trust keys, path-safe wrapping, honest model width, goal-first Operate placeholder

Four first-run papercuts from the morning report:

Trust dialog keys read 1/3/2. The number keys now mirror the footer's
reading order — 1 trusts, 2 continues without trusting, 3 quits (Y/U/N/
Esc unchanged) — with the footer, the Enter hint, and all fourteen
translated locales updated together so copy and handling cannot drift.

Deep workspace paths hard-split mid-component because ratatui's Wrap
breaks only at whitespace. The trust prompt now wraps its location line
at '/' boundaries with indented continuations, using the panel's real
inner width.

The header's proportional width split let a long workspace squeeze the
model identity to a meaningless 'deep…' at 120 cols. The model — a trust
fact — now keeps up to 14 columns before the workspace takes the rest.

The Operate composer greeted first-run users with 'Coordinate parallel
tasks...'; it now says what to type: 'Describe the goal — Codewhale
keeps working until it's done'.

Verified: onboarding 52, trust 69, localization 31, widgets 284, header
127 suites green; cargo fmt clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hmbown
2026-08-02 01:56:30 -07:00
parent 23e279033d
commit bf04783951
19 changed files with 88 additions and 36 deletions
+1 -1
View File
@@ -479,7 +479,7 @@
"OnboardTrustFooterMiddle": " per confiar-hi i continuar, ",
"OnboardTrustFooterUntrustedMiddle": " per continuar sense confiar-hi, ",
"OnboardTrustFooterSuffix": " per sortir de Codewhale",
"OnboardTrustEnterHint": "Prem 1 o Y per confiar-hi, 3 o U per continuar sense confiar-hi, o 2, N o Esc per sortir de Codewhale.",
"OnboardTrustEnterHint": "Prem 1 o Y per confiar-hi, 2 o U per continuar sense confiar-hi, o 3, N o Esc per sortir de Codewhale.",
"OnboardTrustUntrustedNotice": "S'ha continuat sense confiança a l'espai de treball — les eines i els hooks continuen restringits.",
"OnboardMentalTitle": "Com funciona Codewhale",
"OnboardMentalModesLabel": "Modes",
+1 -1
View File
@@ -479,7 +479,7 @@
"OnboardTrustFooterMiddle": " zum Vertrauen und Fortfahren, ",
"OnboardTrustFooterUntrustedMiddle": " zum Fortfahren ohne Vertrauen, ",
"OnboardTrustFooterSuffix": " zum Beenden von Codewhale",
"OnboardTrustEnterHint": "Drücke 1 oder Y zum Vertrauen, 3 oder U, um ohne Vertrauen fortzufahren, oder 2, N oder Esc, um Codewhale zu beenden.",
"OnboardTrustEnterHint": "Drücke 1 oder Y zum Vertrauen, 2 oder U, um ohne Vertrauen fortzufahren, oder 3, N oder Esc, um Codewhale zu beenden.",
"OnboardTrustUntrustedNotice": "Fortfahren ohne Workspace-Vertrauen — Tools und Hooks bleiben eingeschränkt.",
"OnboardMentalTitle": "Wie Codewhale funktioniert",
"OnboardMentalModesLabel": "Modi",
+1 -1
View File
@@ -502,7 +502,7 @@
"OnboardTrustFooterMiddle": " to trust and continue, ",
"OnboardTrustFooterUntrustedMiddle": " to continue without trusting, ",
"OnboardTrustFooterSuffix": " to quit Codewhale",
"OnboardTrustEnterHint": "Press 1 or Y to trust, 3 or U to continue without trusting, or 2, N, or Esc to quit Codewhale.",
"OnboardTrustEnterHint": "Press 1 or Y to trust, 2 or U to continue without trusting, or 3, N, or Esc to quit Codewhale.",
"OnboardTrustUntrustedNotice": "Continuing without workspace trust — tools and hooks stay restricted.",
"OnboardMentalTitle": "How Codewhale works",
"OnboardMentalModesLabel": "Modes",
+1 -1
View File
@@ -502,7 +502,7 @@
"OnboardTrustFooterMiddle": " para confiar y continuar, ",
"OnboardTrustFooterUntrustedMiddle": " para continuar sin confiar, ",
"OnboardTrustFooterSuffix": " para salir de Codewhale",
"OnboardTrustEnterHint": "Pulsa 1 o Y para confiar, 3 o U para continuar sin confiar, o 2, N o Esc para salir de Codewhale.",
"OnboardTrustEnterHint": "Pulsa 1 o Y para confiar, 2 o U para continuar sin confiar, o 3, N o Esc para salir de Codewhale.",
"OnboardTrustUntrustedNotice": "Continuando sin confiar en el workspace: las herramientas y los hooks siguen restringidos.",
"OnboardMentalTitle": "Cómo funciona Codewhale",
"OnboardMentalModesLabel": "Modos",
+1 -1
View File
@@ -479,7 +479,7 @@
"OnboardTrustFooterMiddle": " pour faire confiance et continuer, ",
"OnboardTrustFooterUntrustedMiddle": " pour continuer sans confiance, ",
"OnboardTrustFooterSuffix": " pour quitter Codewhale",
"OnboardTrustEnterHint": "Appuyez sur 1 ou Y pour faire confiance, 3 ou U pour continuer sans confiance, ou 2, N ou Esc pour quitter Codewhale.",
"OnboardTrustEnterHint": "Appuyez sur 1 ou Y pour faire confiance, 2 ou U pour continuer sans confiance, ou 3, N ou Esc pour quitter Codewhale.",
"OnboardTrustUntrustedNotice": "Poursuite sans confiance du workspace — les outils et hooks restent restreints.",
"OnboardMentalTitle": "Comment fonctionne Codewhale",
"OnboardMentalModesLabel": "Modes",
+1 -1
View File
@@ -479,7 +479,7 @@
"OnboardTrustFooterMiddle": " विश्वास करके जारी रखें, ",
"OnboardTrustFooterUntrustedMiddle": " बिना विश्वास के जारी रखें, ",
"OnboardTrustFooterSuffix": " Codewhale बंद करें",
"OnboardTrustEnterHint": "विश्वास के लिए 1 या Y, बिना विश्वास के जारी रखने के लिए 3 या U, या Codewhale बंद करने के लिए 2, N या Esc दबाएँ।",
"OnboardTrustEnterHint": "विश्वास के लिए 1 या Y, बिना विश्वास के जारी रखने के लिए 2 या U, या Codewhale बंद करने के लिए 3, N या Esc दबाएँ।",
"OnboardTrustUntrustedNotice": "वर्कस्पेस ट्रस्ट के बिना जारी — टूल और हुक प्रतिबंधित रहेंगे।",
"OnboardMentalTitle": "Codewhale कैसे काम करता है",
"OnboardMentalModesLabel": "मोड",
+1 -1
View File
@@ -479,7 +479,7 @@
"OnboardTrustFooterMiddle": " untuk mempercayai dan lanjut, ",
"OnboardTrustFooterUntrustedMiddle": " untuk lanjut tanpa mempercayai, ",
"OnboardTrustFooterSuffix": " untuk keluar dari Codewhale",
"OnboardTrustEnterHint": "Tekan 1 atau Y untuk mempercayai, 3 atau U untuk lanjut tanpa mempercayai, atau 2, N, atau Esc untuk keluar dari Codewhale.",
"OnboardTrustEnterHint": "Tekan 1 atau Y untuk mempercayai, 2 atau U untuk lanjut tanpa mempercayai, atau 3, N, atau Esc untuk keluar dari Codewhale.",
"OnboardTrustUntrustedNotice": "Melanjutkan tanpa kepercayaan workspace — alat dan hook tetap dibatasi.",
"OnboardMentalTitle": "Cara kerja Codewhale",
"OnboardMentalModesLabel": "Mode",
+1 -1
View File
@@ -502,7 +502,7 @@
"OnboardTrustFooterMiddle": " で信頼して続行、",
"OnboardTrustFooterUntrustedMiddle": " で信頼せずに続行、",
"OnboardTrustFooterSuffix": " で Codewhale を終了",
"OnboardTrustEnterHint": "1 または Y で信頼、3 または U で信頼せずに続行、2・N・Esc で Codewhale を終了します。",
"OnboardTrustEnterHint": "1 または Y で信頼、2 または U で信頼せずに続行、3・N・Esc で Codewhale を終了します。",
"OnboardTrustUntrustedNotice": "ワークスペースを信頼せずに続行します — ツールとフックは制限されたままです。",
"OnboardMentalTitle": "Codewhale のしくみ",
"OnboardMentalModesLabel": "モード",
+1 -1
View File
@@ -502,7 +502,7 @@
"OnboardTrustFooterMiddle": " 신뢰하고 계속, ",
"OnboardTrustFooterUntrustedMiddle": " 신뢰하지 않고 계속, ",
"OnboardTrustFooterSuffix": " Codewhale 종료",
"OnboardTrustEnterHint": "1 또는 Y로 신뢰, 3 또는 U로 신뢰 없이 계속, 2·N·Esc로 Codewhale을 종료합니다.",
"OnboardTrustEnterHint": "1 또는 Y로 신뢰, 2 또는 U로 신뢰 없이 계속, 3·N·Esc로 Codewhale을 종료합니다.",
"OnboardTrustUntrustedNotice": "작업 공간을 신뢰하지 않고 계속합니다 — 도구와 훅은 제한된 상태로 유지됩니다.",
"OnboardMentalTitle": "Codewhale 작동 방식",
"OnboardMentalModesLabel": "모드",
+1 -1
View File
@@ -502,7 +502,7 @@
"OnboardTrustFooterMiddle": " para confiar e continuar, ",
"OnboardTrustFooterUntrustedMiddle": " para continuar sem confiar, ",
"OnboardTrustFooterSuffix": " para sair do Codewhale",
"OnboardTrustEnterHint": "Pressione 1 ou Y para confiar, 3 ou U para continuar sem confiar, ou 2, N ou Esc para sair do Codewhale.",
"OnboardTrustEnterHint": "Pressione 1 ou Y para confiar, 2 ou U para continuar sem confiar, ou 3, N ou Esc para sair do Codewhale.",
"OnboardTrustUntrustedNotice": "Continuando sem confiar no workspace — ferramentas e hooks permanecem restritos.",
"OnboardMentalTitle": "Como o Codewhale funciona",
"OnboardMentalModesLabel": "Modos",
+1 -1
View File
@@ -479,7 +479,7 @@
"OnboardTrustFooterMiddle": " — доверить и продолжить, ",
"OnboardTrustFooterUntrustedMiddle": " — продолжить без доверия, ",
"OnboardTrustFooterSuffix": " — выйти из Codewhale",
"OnboardTrustEnterHint": "Нажмите 1 или Y — доверить, 3 или U — продолжить без доверия, 2, N или Esc — выйти из Codewhale.",
"OnboardTrustEnterHint": "Нажмите 1 или Y — доверить, 2 или U — продолжить без доверия, 3, N или Esc — выйти из Codewhale.",
"OnboardTrustUntrustedNotice": "Продолжение без доверия к рабочей области — инструменты и хуки останутся ограниченными.",
"OnboardMentalTitle": "Как работает Codewhale",
"OnboardMentalModesLabel": "Режимы",
+1 -1
View File
@@ -479,7 +479,7 @@
"OnboardTrustFooterMiddle": " щоб довірити й продовжити, ",
"OnboardTrustFooterUntrustedMiddle": " щоб продовжити без довіри, ",
"OnboardTrustFooterSuffix": " щоб вийти з Codewhale",
"OnboardTrustEnterHint": "Натисніть 1 або Y, щоб довірити; 3 або U, щоб продовжити без довіри; або 2, N чи Esc, щоб вийти з Codewhale.",
"OnboardTrustEnterHint": "Натисніть 1 або Y, щоб довірити; 2 або U, щоб продовжити без довіри; або 3, N чи Esc, щоб вийти з Codewhale.",
"OnboardTrustUntrustedNotice": "Продовження без довіри до робочої області — інструменти та хуки лишаються обмеженими.",
"OnboardMentalTitle": "Як працює Codewhale",
"OnboardMentalModesLabel": "Режими",
+1 -1
View File
@@ -502,7 +502,7 @@
"OnboardTrustFooterMiddle": " để tin cậy và tiếp tục, ",
"OnboardTrustFooterUntrustedMiddle": " để tiếp tục mà không tin cậy, ",
"OnboardTrustFooterSuffix": " để thoát Codewhale",
"OnboardTrustEnterHint": "Nhấn 1 hoặc Y để tin cậy, 3 hoặc U để tiếp tục mà không tin cậy, hoặc 2, N, Esc để thoát Codewhale.",
"OnboardTrustEnterHint": "Nhấn 1 hoặc Y để tin cậy, 2 hoặc U để tiếp tục mà không tin cậy, hoặc 3, N, Esc để thoát Codewhale.",
"OnboardTrustUntrustedNotice": "Tiếp tục mà không tin cậy workspace — công cụ và hook vẫn bị hạn chế.",
"OnboardMentalTitle": "Cách Codewhale hoạt động",
"OnboardMentalModesLabel": "Chế độ",
+1 -1
View File
@@ -502,7 +502,7 @@
"OnboardTrustFooterMiddle": " 信任并继续,",
"OnboardTrustFooterUntrustedMiddle": " 不信任并继续,",
"OnboardTrustFooterSuffix": " 退出 Codewhale",
"OnboardTrustEnterHint": "按 1 或 Y 信任,按 3 或 U 在不信任的情况下继续,按 2、N 或 Esc 退出 Codewhale。",
"OnboardTrustEnterHint": "按 1 或 Y 信任,按 2 或 U 在不信任的情况下继续,按 3、N 或 Esc 退出 Codewhale。",
"OnboardTrustUntrustedNotice": "在未信任工作区的情况下继续 — 工具与钩子保持受限。",
"OnboardMentalTitle": "Codewhale 的工作方式",
"OnboardMentalModesLabel": "模式",
+6 -3
View File
@@ -39,7 +39,10 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
OnboardingState::Language => language::lines(app),
OnboardingState::Appearance => appearance_lines(app),
OnboardingState::Provider => provider_lines(app),
OnboardingState::TrustDirectory => trust_directory::lines(app),
OnboardingState::TrustDirectory => {
// Inner text width: panel borders (2) plus horizontal padding (4).
trust_directory::lines(app, usize::from(content_width.saturating_sub(6)))
}
OnboardingState::MentalModels => mental_models::lines(app),
OnboardingState::Tips => tips_lines(app),
OnboardingState::None => Vec::new(),
@@ -433,7 +436,7 @@ mod tests {
#[test]
fn trust_footer_advertises_only_explicit_trust_keys() {
let app = test_app_with_locale(Locale::En);
let lines = trust_directory::lines(&app);
let lines = trust_directory::lines(&app, 70);
let footer = lines
.last()
.expect("trust footer")
@@ -444,7 +447,7 @@ mod tests {
assert_eq!(
footer,
"Press 1/Y to trust and continue, 3/U to continue without trusting, 2/N/Esc to quit Codewhale"
"Press 1/Y to trust and continue, 2/U to continue without trusting, 3/N/Esc to quit Codewhale"
);
}
@@ -7,7 +7,44 @@ use crate::localization::MessageId;
use crate::palette;
use crate::tui::app::App;
pub fn lines(app: &App) -> Vec<Line<'static>> {
/// Wrap a path-bearing line at `/` boundaries so a deep workspace never
/// hard-splits mid-component under ratatui's whitespace-only `Wrap`.
/// Continuation lines are indented to read as one location.
fn wrap_on_path_separators(text: &str, width: usize) -> Vec<String> {
let width = width.max(8);
let mut out: Vec<String> = Vec::new();
let mut current = String::new();
let mut chunk = String::new();
let mut flush = |current: &mut String, chunk: &mut String, out: &mut Vec<String>| {
if chunk.is_empty() {
return;
}
let candidate_len = current.chars().count() + chunk.chars().count();
if candidate_len > width && !current.is_empty() {
out.push(std::mem::take(current));
current.push_str(" ");
}
current.push_str(chunk);
chunk.clear();
};
for ch in text.chars() {
chunk.push(ch);
if ch == '/' {
flush(&mut current, &mut chunk, &mut out);
}
}
flush(&mut current, &mut chunk, &mut out);
if !current.is_empty() {
out.push(current);
}
if out.is_empty() {
vec![String::new()]
} else {
out
}
}
pub fn lines(app: &App, content_width: usize) -> Vec<Line<'static>> {
let mut lines = Vec::new();
lines.push(Line::from(Span::styled(
app.tr(MessageId::OnboardTrustTitle).to_string(),
@@ -20,14 +57,17 @@ pub fn lines(app: &App) -> Vec<Line<'static>> {
app.tr(MessageId::OnboardTrustQuestion).to_string(),
Style::default().fg(palette::TEXT_PRIMARY),
)));
lines.push(Line::from(Span::styled(
format!(
"{}{}",
app.tr(MessageId::OnboardTrustLocationPrefix),
crate::utils::display_path(&app.workspace)
),
Style::default().fg(palette::TEXT_MUTED),
)));
let location = format!(
"{}{}",
app.tr(MessageId::OnboardTrustLocationPrefix),
crate::utils::display_path(&app.workspace)
);
for segment in wrap_on_path_separators(&location, content_width) {
lines.push(Line::from(Span::styled(
segment,
Style::default().fg(palette::TEXT_MUTED),
)));
}
lines.push(Line::from(""));
lines.push(Line::from(Span::styled(
app.tr(MessageId::OnboardTrustRiskHint).to_string(),
@@ -61,7 +101,7 @@ pub fn lines(app: &App) -> Vec<Line<'static>> {
Style::default().fg(palette::TEXT_MUTED),
),
Span::styled(
"3/U",
"2/U",
Style::default()
.fg(palette::TEXT_PRIMARY)
.add_modifier(Modifier::BOLD),
@@ -72,7 +112,7 @@ pub fn lines(app: &App) -> Vec<Line<'static>> {
Style::default().fg(palette::TEXT_MUTED),
),
Span::styled(
"2/N/Esc",
"3/N/Esc",
Style::default()
.fg(palette::TEXT_PRIMARY)
.add_modifier(Modifier::BOLD),
@@ -100,7 +140,7 @@ mod tests {
};
let mut app = App::new(options, &Config::default());
app.ui_locale = crate::localization::Locale::En;
let body = lines(&app)
let body = lines(&app, 70)
.into_iter()
.flat_map(|line| line.spans.into_iter().map(|span| span.content.to_string()))
.collect::<Vec<_>>()
@@ -111,9 +151,9 @@ mod tests {
assert!(body.contains("prompt injection"));
assert!(body.contains("tools and hooks"));
assert!(body.contains("1/Y"));
assert!(body.contains("3/U"));
assert!(body.contains("2/U"));
assert!(body.contains("continue without trusting"));
assert!(body.contains("2/N/Esc"));
assert!(body.contains("3/N/Esc"));
assert!(body.contains("quit Codewhale"));
}
}
+5 -2
View File
@@ -6071,12 +6071,15 @@ async fn run_event_loop(
app.status_message = Some(format!("Failed to trust workspace: {err}"));
}
}
KeyCode::Char('u') | KeyCode::Char('U') | KeyCode::Char('3')
// Number keys mirror the footer's reading order (1 trust,
// 2 continue untrusted, 3 quit) so the displayed digits
// are sequential instead of 1/3/2.
KeyCode::Char('u') | KeyCode::Char('U') | KeyCode::Char('2')
if app.onboarding == OnboardingState::TrustDirectory =>
{
continue_without_trusting_directory(app);
}
KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Char('2')
KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Char('3')
if app.onboarding == OnboardingState::TrustDirectory =>
{
let _ = engine_handle.send(Op::Shutdown).await;
+5 -1
View File
@@ -477,7 +477,11 @@ impl<'a> HeaderWidget<'a> {
let model_width = model.width();
let total_width = workspace_width + model_width;
let min_workspace = 4;
let min_model = 4;
// The model identity is a trust fact ("what am I talking to?"), so it
// keeps enough room to stay recognizable — "deepseek-v4-…" — before
// the proportional split lets a long workspace name squeeze it to a
// meaningless "deep…" (morning-report first-run copy batch).
let min_model = model_width.min(14).max(4);
let proportional_workspace =
((content_width as f64 * workspace_width as f64) / total_width as f64).round() as usize;
+4 -2
View File
@@ -3383,7 +3383,9 @@ pub(crate) fn composer_empty_hint_text(app: &App) -> Cow<'static, str> {
if app.is_history_search_active() {
app.tr(crate::localization::MessageId::HistorySearchPlaceholder)
} else if app.mode == crate::tui::app::AppMode::Operate {
Cow::Borrowed("Coordinate parallel tasks...")
// Operate is goal-driven; the empty composer says what to type, not
// orchestration jargon a first-run user has no model for.
Cow::Borrowed("Describe the goal — Codewhale keeps working until it's done")
} else {
app.tr(crate::localization::MessageId::ComposerPlaceholder)
}
@@ -5993,7 +5995,7 @@ mod tests {
assert_eq!(
composer_empty_hint_text(&app),
"Coordinate parallel tasks..."
"Describe the goal — Codewhale keeps working until it's done"
);
}