fix(tui): drive the composer send cue from the draft, not the paste-burst window

The `[↵]` chip read `composer_enter_would_submit`, which also consults the
paste-burst window. That window is re-extended on every fast keystroke, so on
terminals outside the bracketed-paste allowlist (tmux, VS Code, kitty,
Alacritty, GNOME Terminal, SSH) the chip strobed `[↵]`/`[·]` while typing.

Add `App::composer_draft_is_submittable` (time-independent, trimmed-empty
check) and paint the chip from it. Enter routing, mouse submit and hover
registration stay on the timing predicate. Lands the preserved patch from the
fix/issue-sweep-0.10.0 worktree, with its placement corrected so
`composer_enter_would_submit` keeps its own doc comment and `#[must_use]`.
The pinned test at ui/tests.rs:6641 now asserts a steady `[↵]` while the
burst window is open and Enter routing still waits.

Refs #6397

Checks (run from a HEAD snapshot + these three files, because the shared
checkout's lib-test build was broken by other lanes' in-flight edits in
git_history.rs, context_report/, core/engine/tests.rs and debug/cache.rs):
- cargo test -p codewhale-tui --lib -- paste_safety composer_submit
  composer_enter submit_cue composer_draft: 6 passed, 0 failed
- rustfmt --check composer.rs: clean
Not run: manual tmux / VS Code terminal check.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
CodeWhale BotandClaude Opus 5.5 committed 2026-09-22 13:49:05 -07:00
1 parent 2fce1aad0e
commit 389619a693
3 files changed
+28 -4

No files matched your search

+15
View File
@@ -1870,6 +1870,21 @@ impl App {
!self.input.trim().is_empty()
}
/// Whether the *draft* is in a submittable state, for display only.
///
/// Deliberately time-independent. [`Self::composer_enter_would_submit`]
/// additionally consults the paste-burst heuristic, whose suppression
/// window is re-extended on every fast keystroke -- correct for deciding
/// what a newline does mid-paste, wrong for a persistent affordance.
/// Driving the `[↵]` chip from it made the chip strobe `[↵]`/`[·]` for as
/// long as the user kept typing, because the window kept reopening
/// (#6397). Enter routing, mouse submit and hover registration stay on
/// the timing predicate.
#[must_use]
pub fn composer_draft_is_submittable(&self) -> bool {
!self.input.trim().is_empty()
}
/// Public wrapper around [`Self::consolidate_large_input`] that no-ops
/// when the current input fits inside the safety cap. Both the paste-
/// insert path (visible-before-submit) and the submit-time safety net
+7 -2
View File
@@ -6638,15 +6638,20 @@ fn raw_paste_beginning_with_space_preserves_payload_over_reasoning_action() {
}
#[test]
fn paste_safety_expiry_repaints_the_submit_cue_without_another_key() {
fn paste_safety_window_keeps_the_submit_cue_steady_while_routing_waits() {
// #6397: the `[↵]` chip follows the time-independent draft predicate,
// so an open paste-burst window (re-extended on every fast keystroke)
// must not flip it to `[·]`. Enter routing still waits on the window.
let mut app = create_test_app();
app.use_paste_burst_detection = true;
app.insert_str("/mcp");
let now = Instant::now();
app.paste_burst.extend_window(now);
assert!(!app.composer_enter_would_submit());
assert!(app.composer_draft_is_submittable());
let waiting = render_underwater_test_app(&mut app, 80, 24);
assert!(waiting.contains("[·]"), "{waiting}");
assert!(waiting.contains("[↵]"), "{waiting}");
assert!(!waiting.contains("[·]"), "{waiting}");
app.needs_redraw = false;
assert!(flush_paste_burst_before_composer(
&mut app,
+6 -2
View File
@@ -1942,7 +1942,11 @@ impl Renderable for ComposerWidget<'_> {
area,
buf,
&self.app.ui_theme,
self.app.composer_enter_would_submit(),
// Display state, not key-routing state: the paste-burst
// window reopens on every fast keystroke, so drawing from
// `composer_enter_would_submit` strobed the chip while
// typing (#6397).
self.app.composer_draft_is_submittable(),
crate::tui::color_compat::ascii_safe_enabled(),
);
}
@@ -6969,7 +6973,7 @@ mod tests {
let mut buf = Buffer::empty(area);
widget.render(area, &mut buf);
let submit = active_composer_submit_rect(&app, area).unwrap();
let ready = app.composer_enter_would_submit();
let ready = app.composer_draft_is_submittable();
let painted: String = (submit.x..submit.right())
.map(|x| buf[(x, submit.y)].symbol())
.collect();