Skip to content

Commit 49f65f0

Browse files
committed
fix: improve slash command interactions
1 parent b4a72de commit 49f65f0

3 files changed

Lines changed: 85 additions & 12 deletions

File tree

crates/tui/src/bottom_pane/chat_composer.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1430,6 +1430,8 @@ impl ChatComposer {
14301430
modifiers: KeyModifiers::NONE,
14311431
..
14321432
} => {
1433+
let first_line = self.textarea.text().lines().next().unwrap_or("");
1434+
popup.on_composer_text_change(first_line.to_string());
14331435
if let Some(sel) = popup.selected_item() {
14341436
let CommandItem::Builtin(cmd) = sel;
14351437
// If the command supports inline args and there's already
@@ -4029,4 +4031,23 @@ mod reference_popup_tests {
40294031
assert_eq!(result, (InputResult::None, true));
40304032
assert!(matches!(composer.active_popup, ActivePopup::None));
40314033
}
4034+
4035+
/// Trace: L2-DES-TUI-003
4036+
/// Verifies: Enter confirms the focused slash-command popup row.
4037+
#[test]
4038+
fn enter_accepts_focused_slash_command() {
4039+
let (mut composer, _rx) = test_composer();
4040+
set_text_at_end(&mut composer, "/");
4041+
4042+
assert_eq!(
4043+
composer.handle_key_event(press(KeyCode::Down)),
4044+
(InputResult::None, true)
4045+
);
4046+
4047+
assert_eq!(
4048+
composer.handle_key_event(press(KeyCode::Enter)),
4049+
(InputResult::Command(SlashCommand::Model), true)
4050+
);
4051+
assert_eq!(composer.current_text(), "");
4052+
}
40324053
}

crates/tui/src/history_cell.rs

Lines changed: 62 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -344,28 +344,24 @@ impl HistoryCell for UserHistoryCell {
344344
user_message_style()
345345
};
346346
let element_style = style.fg(accent);
347+
let slash_command_style = style.fg(accent);
347348
let prefix_style = Style::default().fg(mode_color);
348349
let blank_prefixed_line = || Line::from(Span::styled(" ", style)).style(style);
349350

350351
let wrapped_message = if self.message.is_empty() && self.text_elements.is_empty() {
351352
None
352-
} else if self.text_elements.is_empty() {
353-
let message_without_trailing_newlines = self.message.trim_end_matches(['\r', '\n']);
354-
let wrapped = adaptive_wrap_lines(
355-
message_without_trailing_newlines
356-
.split('\n')
357-
.map(|line| Line::from(line).style(style)),
358-
RtOptions::new(usize::from(wrap_width))
359-
.wrap_algorithm(textwrap::WrapAlgorithm::FirstFit),
360-
);
361-
let wrapped = trim_trailing_blank_lines(wrapped);
362-
(!wrapped.is_empty()).then_some(wrapped)
363353
} else {
354+
let message = if self.text_elements.is_empty() {
355+
self.message.trim_end_matches(['\r', '\n'])
356+
} else {
357+
&self.message
358+
};
364359
let raw_lines = build_user_message_lines_with_elements(
365-
&self.message,
360+
message,
366361
&self.text_elements,
367362
style,
368363
element_style,
364+
slash_command_style,
369365
);
370366
let wrapped = adaptive_wrap_lines(
371367
raw_lines,
@@ -1725,9 +1721,40 @@ fn pluralize(count: u64, singular: &'static str, plural: &'static str) -> &'stat
17251721

17261722
#[cfg(test)]
17271723
mod tests {
1724+
use ratatui::style::Color;
1725+
1726+
use crate::bottom_pane::InputMode;
1727+
1728+
use super::HistoryCell;
17281729
use pretty_assertions::assert_eq;
17291730

17301731
use super::format_duration_hms;
1732+
use super::new_user_prompt;
1733+
1734+
fn content_spans_for_message(message: &str) -> Vec<(String, Option<Color>)> {
1735+
let cell = new_user_prompt(
1736+
message.to_string(),
1737+
Vec::new(),
1738+
Vec::new(),
1739+
Vec::new(),
1740+
Color::Yellow,
1741+
InputMode::Build,
1742+
);
1743+
cell.display_lines(80)
1744+
.into_iter()
1745+
.find(|line| {
1746+
line.spans
1747+
.iter()
1748+
.map(|span| span.content.as_ref())
1749+
.collect::<String>()
1750+
.contains(message)
1751+
})
1752+
.unwrap_or_else(|| panic!("missing rendered message line for {message:?}"))
1753+
.spans
1754+
.into_iter()
1755+
.map(|span| (span.content.to_string(), span.style.fg))
1756+
.collect()
1757+
}
17311758

17321759
#[test]
17331760
fn turn_summary_duration_uses_hour_minute_second_units() {
@@ -1738,4 +1765,27 @@ mod tests {
17381765
assert_eq!(format_duration_hms(3_601), "1h0m1s");
17391766
assert_eq!(format_duration_hms(3_723), "1h2m3s");
17401767
}
1768+
1769+
#[test]
1770+
fn user_prompt_highlights_leading_slash_command_without_text_element() {
1771+
assert_eq!(
1772+
content_spans_for_message("/btw check this"),
1773+
vec![
1774+
("▌ ".to_string(), Some(Color::Cyan)),
1775+
("/btw".to_string(), Some(Color::Yellow)),
1776+
(" check this".to_string(), None),
1777+
]
1778+
);
1779+
}
1780+
1781+
#[test]
1782+
fn user_prompt_does_not_highlight_unknown_slash_command() {
1783+
assert_eq!(
1784+
content_spans_for_message("/unknown check this"),
1785+
vec![
1786+
("▌ ".to_string(), Some(Color::Cyan)),
1787+
("/unknown check this".to_string(), None),
1788+
]
1789+
);
1790+
}
17411791
}

specs/L2/tui/L2-DES-TUI-003-composer-and-input-modes.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,7 @@ Slash-command inline rendering:
297297
- Parameters or placeholder text following the matched slash command use muted foreground color.
298298
- If the typed slash command does not match an existing command, the composer should not apply matched-command coloring.
299299
- Inline command coloring is presentational only. Command parsing and validation still happen when the user confirms or submits the command.
300+
- Submitted user history cells apply the same matched-command coloring to a leading recognized `/name` token, including rows restored or constructed without composer text-element metadata.
300301
- For `/goal`, free-form text after the command is the objective. Pressing Enter submits that objective directly to the goal command instead of opening a budget prompt or create wizard.
301302

302303
Example matched slash command with parameter hint:
@@ -388,3 +389,4 @@ Rules:
388389
| 3 | 2026-05-26 | Human | Refinement | Updated navigable slash-command lists so `>` marks the focused row and `` is reserved for enabled options in choice lists. |
389390
| 4 | 2026-05-27 | Human | Refinement | Removed `/onboard` from slash-command discovery because onboarding is entered through startup CLI arguments. |
390391
| 4 | 2026-06-08 | Assistant | Refinement | Defined `Shift+Tab` Build/Plan toggling, leftmost uppercase mode labels, Shell-only `!` entry behavior, one-shot PTY-backed Shell execution including sessionless startup commands, prompt-only Plan Mode v1 behavior, active-mode composer marker color, direct user-shell output, and `▣ Shell` process summaries. |
392+
| 5 | 2026-06-18 | Assistant | Refinement | Clarified that submitted user history cells also highlight a leading recognized slash-command token, even when no composer text-element metadata is available. |

0 commit comments

Comments
 (0)