fix(tui): protect multiline drafts on arrow navigation
This commit is contained in:
+7
-2
@@ -32,6 +32,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
DeepSeek routes keep the 128K fallback.
|
||||
- Fixed npm wrapper version output so `--version` prefers the installed binary
|
||||
version instead of stale package metadata when both are available.
|
||||
- Fixed multiline composer arrow navigation so holding Up/Down at the first or
|
||||
last line no longer replaces the current draft with prompt history.
|
||||
- Clarified the English DeepSeek account-balance footer chip from `bal` to
|
||||
`balance` so it is less likely to be mistaken for session spend.
|
||||
- Fixed truncated subagent tool calls and repeated truncated subagent responses
|
||||
so they return model-visible errors instead of silently failing.
|
||||
|
||||
@@ -41,8 +45,9 @@ Thanks to **@ZhulongNT** (#2045), **@cyq1017** (#2521, #2536, #2537, #2559,
|
||||
#2562, #2563, #2564), and **@HUQIANTAO** (#2527) for the work harvested into
|
||||
this release pass. Thanks also to issue reporters and verification helpers
|
||||
including **@New2Niu** (#2561), **@buko** (#2533, #2369), **@wywsoor**
|
||||
(#2494), **@ctxyao** (#2556), and **@Dr3259** (#2380) for reports and
|
||||
acceptance details that shaped these fixes.
|
||||
(#2494), **@ctxyao** (#2556), **@Dr3259** (#2380), and **@caiyilian**
|
||||
(#2567) for reports and acceptance details that shaped these fixes, plus the
|
||||
WeChat/Chinese UX reports relayed during the final triage pass.
|
||||
|
||||
## [0.8.49] - 2026-06-01
|
||||
|
||||
|
||||
@@ -32,6 +32,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
DeepSeek routes keep the 128K fallback.
|
||||
- Fixed npm wrapper version output so `--version` prefers the installed binary
|
||||
version instead of stale package metadata when both are available.
|
||||
- Fixed multiline composer arrow navigation so holding Up/Down at the first or
|
||||
last line no longer replaces the current draft with prompt history.
|
||||
- Clarified the English DeepSeek account-balance footer chip from `bal` to
|
||||
`balance` so it is less likely to be mistaken for session spend.
|
||||
- Fixed truncated subagent tool calls and repeated truncated subagent responses
|
||||
so they return model-visible errors instead of silently failing.
|
||||
|
||||
@@ -41,8 +45,9 @@ Thanks to **@ZhulongNT** (#2045), **@cyq1017** (#2521, #2536, #2537, #2559,
|
||||
#2562, #2563, #2564), and **@HUQIANTAO** (#2527) for the work harvested into
|
||||
this release pass. Thanks also to issue reporters and verification helpers
|
||||
including **@New2Niu** (#2561), **@buko** (#2533, #2369), **@wywsoor**
|
||||
(#2494), **@ctxyao** (#2556), and **@Dr3259** (#2380) for reports and
|
||||
acceptance details that shaped these fixes.
|
||||
(#2494), **@ctxyao** (#2556), **@Dr3259** (#2380), and **@caiyilian**
|
||||
(#2567) for reports and acceptance details that shaped these fixes, plus the
|
||||
WeChat/Chinese UX reports relayed during the final triage pass.
|
||||
|
||||
## [0.8.49] - 2026-06-01
|
||||
|
||||
|
||||
@@ -1121,7 +1121,7 @@ fn english(id: MessageId) -> &'static str {
|
||||
MessageId::FooterAgentsPlural => "{count} agents",
|
||||
MessageId::FooterPressCtrlCAgain => "Press Ctrl+C again to quit",
|
||||
MessageId::FooterWorking => "working",
|
||||
MessageId::FooterBalancePrefix => "bal",
|
||||
MessageId::FooterBalancePrefix => "balance",
|
||||
MessageId::HelpSectionActions => "Actions",
|
||||
MessageId::HelpSectionClipboard => "Clipboard",
|
||||
MessageId::HelpSectionEditing => "Input editing",
|
||||
|
||||
@@ -57,14 +57,19 @@ pub(crate) fn handle_composer_history_arrow(
|
||||
}
|
||||
|
||||
// When `composer_arrows_scroll` is enabled, plain Up/Down scroll the
|
||||
// transcript for single-line drafts. Multiline composers keep editor-like
|
||||
// line navigation, with history fallback at the first/last line.
|
||||
// transcript for single-line drafts. Multiline drafts keep editor-like
|
||||
// line navigation. If the user holds Up/Down at the first/last line, do
|
||||
// not replace their current draft with prompt history unless they are
|
||||
// already navigating history.
|
||||
let scroll_transcript = app.composer_arrows_scroll && !app.input.contains('\n');
|
||||
let protect_multiline_draft = app.input.contains('\n') && app.history_index.is_none();
|
||||
|
||||
match key.code {
|
||||
KeyCode::Up => {
|
||||
if scroll_transcript {
|
||||
app.scroll_up(COMPOSER_ARROW_SCROLL_LINES);
|
||||
} else if protect_multiline_draft && !cursor_has_previous_logical_line(app) {
|
||||
app.needs_redraw = true;
|
||||
} else {
|
||||
app.vim_move_up();
|
||||
}
|
||||
@@ -73,6 +78,8 @@ pub(crate) fn handle_composer_history_arrow(
|
||||
KeyCode::Down => {
|
||||
if scroll_transcript {
|
||||
app.scroll_down(COMPOSER_ARROW_SCROLL_LINES);
|
||||
} else if protect_multiline_draft && !cursor_has_next_logical_line(app) {
|
||||
app.needs_redraw = true;
|
||||
} else {
|
||||
app.vim_move_down();
|
||||
}
|
||||
@@ -82,6 +89,26 @@ pub(crate) fn handle_composer_history_arrow(
|
||||
}
|
||||
}
|
||||
|
||||
fn cursor_has_previous_logical_line(app: &App) -> bool {
|
||||
let cursor_byte = byte_index_at_char(&app.input, app.cursor_position);
|
||||
app.input[..cursor_byte].contains('\n')
|
||||
}
|
||||
|
||||
fn cursor_has_next_logical_line(app: &App) -> bool {
|
||||
let cursor_byte = byte_index_at_char(&app.input, app.cursor_position);
|
||||
app.input[cursor_byte..].contains('\n')
|
||||
}
|
||||
|
||||
fn byte_index_at_char(text: &str, char_index: usize) -> usize {
|
||||
if char_index == 0 {
|
||||
return 0;
|
||||
}
|
||||
text.char_indices()
|
||||
.nth(char_index)
|
||||
.map(|(idx, _)| idx)
|
||||
.unwrap_or(text.len())
|
||||
}
|
||||
|
||||
pub(crate) fn is_word_cursor_modifier(modifiers: KeyModifiers) -> bool {
|
||||
modifiers.contains(KeyModifiers::CONTROL) || modifiers.contains(KeyModifiers::ALT)
|
||||
}
|
||||
|
||||
@@ -6935,7 +6935,7 @@ fn footer_balance_spans_formats_cny() {
|
||||
};
|
||||
*app.balance_cell.lock().unwrap() = Some(info);
|
||||
let spans = footer_balance_spans(&app);
|
||||
assert_eq!(spans_text(&spans), "bal ¥123.5");
|
||||
assert_eq!(spans_text(&spans), "balance ¥123.5");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -6948,7 +6948,7 @@ fn footer_balance_spans_formats_usd() {
|
||||
};
|
||||
*app.balance_cell.lock().unwrap() = Some(info);
|
||||
let spans = footer_balance_spans(&app);
|
||||
assert_eq!(spans_text(&spans), "bal $0.50");
|
||||
assert_eq!(spans_text(&spans), "balance $0.50");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -6961,7 +6961,7 @@ fn footer_balance_spans_rounds_large_amount() {
|
||||
};
|
||||
*app.balance_cell.lock().unwrap() = Some(info);
|
||||
let spans = footer_balance_spans(&app);
|
||||
assert_eq!(spans_text(&spans), "bal $1235");
|
||||
assert_eq!(spans_text(&spans), "balance $1235");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -6974,7 +6974,7 @@ fn footer_balance_spans_treats_unknown_currency_as_usd() {
|
||||
};
|
||||
*app.balance_cell.lock().unwrap() = Some(info);
|
||||
let spans = footer_balance_spans(&app);
|
||||
assert_eq!(spans_text(&spans), "bal $10.0");
|
||||
assert_eq!(spans_text(&spans), "balance $10.0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -6987,7 +6987,7 @@ fn render_footer_from_with_balance_item_shows_balance() {
|
||||
};
|
||||
*app.balance_cell.lock().unwrap() = Some(info);
|
||||
let props = render_footer_from(&app, &[crate::config::StatusItem::Balance], None);
|
||||
assert_eq!(spans_text(&props.balance), "bal $42.5");
|
||||
assert_eq!(spans_text(&props.balance), "balance $42.5");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -7323,7 +7323,7 @@ fn composer_arrows_scroll_multiline_input_navigates_lines() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composer_arrow_up_at_first_line_falls_back_to_history_up() {
|
||||
fn composer_arrow_up_at_first_line_preserves_multiline_draft() {
|
||||
let mut app = create_test_app();
|
||||
app.composer_arrows_scroll = false;
|
||||
app.input = "line one\nline two".to_string();
|
||||
@@ -7337,7 +7337,29 @@ fn composer_arrow_up_at_first_line_falls_back_to_history_up() {
|
||||
false,
|
||||
));
|
||||
|
||||
assert_eq!(app.input, "previous prompt");
|
||||
assert_eq!(app.input, "line one\nline two");
|
||||
assert_eq!(app.cursor_position, 0);
|
||||
assert!(app.history_index.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composer_arrow_down_at_last_line_preserves_multiline_draft() {
|
||||
let mut app = create_test_app();
|
||||
app.composer_arrows_scroll = false;
|
||||
app.input = "line one\nline two".to_string();
|
||||
app.cursor_position = app.input.chars().count();
|
||||
app.input_history.push("next prompt".to_string());
|
||||
|
||||
assert!(handle_composer_history_arrow(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Down, KeyModifiers::NONE),
|
||||
false,
|
||||
false,
|
||||
));
|
||||
|
||||
assert_eq!(app.input, "line one\nline two");
|
||||
assert_eq!(app.cursor_position, app.input.chars().count());
|
||||
assert!(app.history_index.is_none());
|
||||
}
|
||||
|
||||
// #1443: when mouse capture is off (e.g. Windows CMD), arrow-scroll
|
||||
|
||||
Reference in New Issue
Block a user