diff --git a/crates/tui/src/config.rs b/crates/tui/src/config.rs index f9c0c0bd..b0020af5 100644 --- a/crates/tui/src/config.rs +++ b/crates/tui/src/config.rs @@ -5415,6 +5415,28 @@ mod tests { ); } + #[test] + fn prompt_suggestion_defaults_to_false() { + let config = Config::default(); + assert_eq!( + config.prompt_suggestion, None, + "default Config must not opt in" + ); + assert!( + !config.prompt_suggestion_enabled(), + "prompt_suggestion must be opt-in (default off)" + ); + } + + #[test] + fn prompt_suggestion_enabled_when_set_true() { + let config = Config { + prompt_suggestion: Some(true), + ..Default::default() + }; + assert!(config.prompt_suggestion_enabled()); + } + #[test] fn warns_when_allow_shell_nested_under_general_section() { // #2589: the reporter's config nested top-level keys under sections that diff --git a/crates/tui/src/tui/widgets/mod.rs b/crates/tui/src/tui/widgets/mod.rs index 58f63525..5c966adc 100644 --- a/crates/tui/src/tui/widgets/mod.rs +++ b/crates/tui/src/tui/widgets/mod.rs @@ -4052,4 +4052,88 @@ mod tests { ); } } + + // ── Ghost-text prompt suggestion rendering ──────────────────────── + + #[test] + fn ghost_text_renders_when_suggestion_set_and_input_empty() { + let mut app = create_test_app(); + app.prompt_suggestion = Some("What about error handling?".to_string()); + let slash_menu_entries = Vec::::new(); + let mention_menu_entries = Vec::::new(); + let widget = ComposerWidget::new(&app, 5, &slash_menu_entries, &mention_menu_entries); + let area = Rect { + x: 0, + y: 0, + width: 80, + height: 5, + }; + let mut buf = Buffer::empty(area); + widget.render(area, &mut buf); + + let rendered: String = buf + .content + .iter() + .map(|c| c.symbol()) + .collect::>() + .join(""); + assert!( + rendered.contains("What about error handling?"), + "ghost text should render the suggestion. Got: {rendered}" + ); + } + + #[test] + fn ghost_text_hidden_when_input_not_empty() { + let mut app = create_test_app(); + app.prompt_suggestion = Some("A suggestion".to_string()); + app.input = "hello".to_string(); + app.cursor_position = 5; + let slash_menu_entries = Vec::::new(); + let mention_menu_entries = Vec::::new(); + let widget = ComposerWidget::new(&app, 5, &slash_menu_entries, &mention_menu_entries); + let area = Rect { + x: 0, + y: 0, + width: 80, + height: 5, + }; + let mut buf = Buffer::empty(area); + widget.render(area, &mut buf); + + let has_suggestion = buf + .content + .iter() + .any(|c| c.symbol().contains("A suggestion")); + assert!( + !has_suggestion, + "suggestion should not render when input is non-empty" + ); + } + + #[test] + fn ghost_text_hidden_when_no_suggestion() { + let mut app = create_test_app(); + app.prompt_suggestion = None; + let slash_menu_entries = Vec::::new(); + let mention_menu_entries = Vec::::new(); + let widget = ComposerWidget::new(&app, 5, &slash_menu_entries, &mention_menu_entries); + let area = Rect { + x: 0, + y: 0, + width: 80, + height: 5, + }; + let mut buf = Buffer::empty(area); + widget.render(area, &mut buf); + + // When no suggestion and input is empty, placeholder text should appear + // instead. The exact placeholder text is locale-dependent, so we check + // that the suggestion text is NOT present. + let has_placeholder_like_text = buf.content.iter().any(|c| !c.symbol().trim().is_empty()); + assert!( + has_placeholder_like_text, + "some non-empty text should render as placeholder" + ); + } }