@@ -922,11 +922,19 @@ impl ChatComposer {
922
922
. next ( )
923
923
. unwrap_or ( "" )
924
924
. starts_with ( '/' ) ;
925
- if self . paste_burst . is_active ( ) && !in_slash_context {
925
+ if !in_slash_context {
926
926
let now = Instant :: now ( ) ;
927
927
if self . paste_burst . append_newline_if_active ( now) {
928
928
return ( InputResult :: None , true ) ;
929
929
}
930
+ if self
931
+ . paste_burst
932
+ . newline_should_insert_instead_of_submit ( now)
933
+ {
934
+ self . textarea . insert_str ( "\n " ) ;
935
+ self . paste_burst . extend_window ( now) ;
936
+ return ( InputResult :: None , true ) ;
937
+ }
930
938
}
931
939
// If we have pending placeholder pastes, submit immediately to expand them.
932
940
if !self . pending_pastes . is_empty ( ) {
@@ -945,17 +953,6 @@ impl ChatComposer {
945
953
return ( InputResult :: Submitted ( text) , true ) ;
946
954
}
947
955
948
- // During a paste-like burst, treat Enter as a newline instead of submit.
949
- let now = Instant :: now ( ) ;
950
- if self
951
- . paste_burst
952
- . newline_should_insert_instead_of_submit ( now)
953
- && !in_slash_context
954
- {
955
- self . textarea . insert_str ( "\n " ) ;
956
- self . paste_burst . extend_window ( now) ;
957
- return ( InputResult :: None , true ) ;
958
- }
959
956
let mut text = self . textarea . text ( ) . to_string ( ) ;
960
957
let original_input = text. clone ( ) ;
961
958
self . textarea . set_text ( "" ) ;
@@ -1002,6 +999,7 @@ impl ChatComposer {
1002
999
match self . paste_burst . flush_if_due ( now) {
1003
1000
FlushResult :: Paste ( pasted) => {
1004
1001
self . handle_paste ( pasted) ;
1002
+ self . paste_burst . extend_window ( now) ;
1005
1003
true
1006
1004
}
1007
1005
FlushResult :: Typed ( ch) => {
@@ -3211,6 +3209,113 @@ mod tests {
3211
3209
assert ! ( composer. pending_pastes[ 0 ] . 1 . chars( ) . all( |c| c == 'x' ) ) ;
3212
3210
}
3213
3211
3212
+ fn test_composer ( ) -> ChatComposer {
3213
+ let ( tx, _rx) = unbounded_channel :: < AppEvent > ( ) ;
3214
+ let sender = AppEventSender :: new ( tx) ;
3215
+ ChatComposer :: new (
3216
+ true ,
3217
+ sender,
3218
+ false ,
3219
+ "Ask Codex to do anything" . to_string ( ) ,
3220
+ false ,
3221
+ )
3222
+ }
3223
+
3224
+ fn seed_large_paste ( composer : & mut ChatComposer ) -> String {
3225
+ use crossterm:: event:: KeyCode ;
3226
+ use crossterm:: event:: KeyEvent ;
3227
+ use crossterm:: event:: KeyModifiers ;
3228
+
3229
+ let count = LARGE_PASTE_CHAR_THRESHOLD + 25 ;
3230
+ for _ in 0 ..count {
3231
+ let _ =
3232
+ composer. handle_key_event ( KeyEvent :: new ( KeyCode :: Char ( 'x' ) , KeyModifiers :: NONE ) ) ;
3233
+ }
3234
+
3235
+ std:: thread:: sleep ( ChatComposer :: recommended_paste_flush_delay ( ) ) ;
3236
+ assert ! (
3237
+ composer. flush_paste_burst_if_due( ) ,
3238
+ "expected buffered paste to flush"
3239
+ ) ;
3240
+ assert_eq ! ( 1 , composer. pending_pastes. len( ) ) ;
3241
+ composer. pending_pastes [ 0 ] . 0 . clone ( )
3242
+ }
3243
+
3244
+ #[ test]
3245
+ fn burst_paste_large_newline_does_not_submit ( ) {
3246
+ use crossterm:: event:: KeyCode ;
3247
+ use crossterm:: event:: KeyEvent ;
3248
+ use crossterm:: event:: KeyModifiers ;
3249
+
3250
+ let mut composer = test_composer ( ) ;
3251
+ let placeholder = seed_large_paste ( & mut composer) ;
3252
+
3253
+ let ( result, _needs_redraw) =
3254
+ composer. handle_key_event ( KeyEvent :: new ( KeyCode :: Enter , KeyModifiers :: NONE ) ) ;
3255
+
3256
+ assert_eq ! ( InputResult :: None , result) ;
3257
+ assert_eq ! ( 1 , composer. pending_pastes. len( ) ) ;
3258
+ let text_after = composer. textarea . text ( ) . to_string ( ) ;
3259
+ assert ! (
3260
+ text_after. contains( & placeholder) ,
3261
+ "placeholder should remain after newline: {text_after:?}"
3262
+ ) ;
3263
+ assert ! (
3264
+ text_after. ends_with( '\n' ) ,
3265
+ "newline from paste should be preserved: {text_after:?}"
3266
+ ) ;
3267
+ }
3268
+
3269
+ #[ test]
3270
+ fn burst_paste_large_shift_enter_does_not_submit ( ) {
3271
+ use crossterm:: event:: KeyCode ;
3272
+ use crossterm:: event:: KeyEvent ;
3273
+ use crossterm:: event:: KeyModifiers ;
3274
+
3275
+ let mut composer = test_composer ( ) ;
3276
+ let placeholder = seed_large_paste ( & mut composer) ;
3277
+
3278
+ let ( result, _needs_redraw) =
3279
+ composer. handle_key_event ( KeyEvent :: new ( KeyCode :: Enter , KeyModifiers :: SHIFT ) ) ;
3280
+
3281
+ assert_eq ! ( InputResult :: None , result) ;
3282
+ assert_eq ! ( 1 , composer. pending_pastes. len( ) ) ;
3283
+ let text_after = composer. textarea . text ( ) . to_string ( ) ;
3284
+ assert ! (
3285
+ text_after. contains( & placeholder) ,
3286
+ "placeholder should remain after Shift+Enter: {text_after:?}"
3287
+ ) ;
3288
+ assert ! (
3289
+ text_after. ends_with( '\n' ) ,
3290
+ "Shift+Enter should insert newline: {text_after:?}"
3291
+ ) ;
3292
+ }
3293
+
3294
+ #[ test]
3295
+ fn burst_paste_large_ctrl_enter_does_not_submit ( ) {
3296
+ use crossterm:: event:: KeyCode ;
3297
+ use crossterm:: event:: KeyEvent ;
3298
+ use crossterm:: event:: KeyModifiers ;
3299
+
3300
+ let mut composer = test_composer ( ) ;
3301
+ let placeholder = seed_large_paste ( & mut composer) ;
3302
+
3303
+ let ( result, _needs_redraw) =
3304
+ composer. handle_key_event ( KeyEvent :: new ( KeyCode :: Enter , KeyModifiers :: CONTROL ) ) ;
3305
+
3306
+ assert_eq ! ( InputResult :: None , result) ;
3307
+ assert_eq ! ( 1 , composer. pending_pastes. len( ) ) ;
3308
+ let text_after = composer. textarea . text ( ) . to_string ( ) ;
3309
+ assert ! (
3310
+ text_after. contains( & placeholder) ,
3311
+ "placeholder should remain after Ctrl+Enter: {text_after:?}"
3312
+ ) ;
3313
+ assert ! (
3314
+ text_after. ends_with( '\n' ) ,
3315
+ "Ctrl+Enter should insert newline: {text_after:?}"
3316
+ ) ;
3317
+ }
3318
+
3214
3319
#[ test]
3215
3320
fn humanlike_typing_1000_chars_appears_live_no_placeholder ( ) {
3216
3321
let ( tx, _rx) = unbounded_channel :: < AppEvent > ( ) ;
0 commit comments