@@ -512,7 +512,14 @@ async fn run_streaming_polish(
512512 #[ cfg( target_os = "windows" ) ]
513513 let sendinput_options = windows_sendinput_options_from_prefs ( & inner. prefs . get ( ) ) ;
514514 #[ cfg( target_os = "macos" ) ]
515- let macos_newline_mode = inner. prefs . get ( ) . macos_newline_mode ;
515+ let macos_newline_mode = {
516+ let configured = inner. prefs . get ( ) . macos_newline_mode ;
517+ let resolved = resolve_macos_newline_mode ( configured, front_app) ;
518+ log:: info!(
519+ "[coord] streaming_insert: macOS newline mode configured={configured:?} resolved={resolved:?}"
520+ ) ;
521+ resolved
522+ } ;
516523 let typer_handle = tokio:: task:: spawn_blocking ( move || {
517524 #[ cfg( target_os = "windows" ) ]
518525 {
@@ -699,6 +706,33 @@ async fn run_streaming_polish(
699706 }
700707}
701708
709+ /// 把 Auto 解析成单次听写实际使用的模式。前台应用在听写开始时已经捕获,整个流式上屏
710+ /// 过程使用同一结果;未知应用保守使用 Shift+Return,避免聊天框换行时误发送。
711+ #[ cfg_attr( not( any( target_os = "macos" , test) ) , allow( dead_code) ) ]
712+ fn resolve_macos_newline_mode (
713+ configured : crate :: types:: MacosNewlineMode ,
714+ front_app : Option < & str > ,
715+ ) -> crate :: types:: MacosNewlineMode {
716+ use crate :: types:: MacosNewlineMode ;
717+
718+ if configured != MacosNewlineMode :: Auto {
719+ return configured;
720+ }
721+
722+ let bundle_id = front_app. and_then ( |label| {
723+ let front = crate :: types:: split_front_app_label ( label, true ) ;
724+ front. bundle_id . or ( front. name )
725+ } ) ;
726+ if bundle_id
727+ . as_deref ( )
728+ . is_some_and ( crate :: host_document:: is_terminal_bundle_id)
729+ {
730+ MacosNewlineMode :: LineFeed
731+ } else {
732+ MacosNewlineMode :: ShiftReturn
733+ }
734+ }
735+
702736#[ cfg( target_os = "windows" ) ]
703737pub ( super ) fn windows_sendinput_options_from_prefs (
704738 prefs : & crate :: types:: UserPreferences ,
@@ -5363,19 +5397,71 @@ mod tests {
53635397 append_typed_prefix, batch_asr_chunk_limit_ms, build_transcribe_failed_session,
53645398 coding_agent_mode_from_pref, default_done_message, drain_streaming_insert_deltas_with,
53655399 eligible_polish_context_turns, finalize_polished_text, flush_streaming_insert_buffer_with,
5366- pcm_duration_ms , pcm_from_wav_bytes , retry_error_outcome , should_arm_edit_watch ,
5367- should_attempt_silent_retry , should_read_cursor_context , insert_delivery_failed ,
5368- resolve_less_computer_run_outcome , streaming_insert_eligible ,
5369- SilentRetryOutcome ,
5400+ insert_delivery_failed , pcm_duration_ms , pcm_from_wav_bytes ,
5401+ resolve_less_computer_run_outcome , resolve_macos_newline_mode , retry_error_outcome ,
5402+ should_arm_edit_watch , should_attempt_silent_retry , should_read_cursor_context ,
5403+ streaming_insert_eligible , SilentRetryOutcome ,
53705404 } ;
53715405 #[ cfg( any( target_os = "macos" , target_os = "linux" ) ) ]
53725406 use super :: { desktop_keyless_dictation_provider, DesktopKeylessDictationProvider } ;
53735407 use crate :: coordinator:: RetranscribeError ;
53745408 use crate :: types:: {
5375- ChineseScriptPreference , CorrectionRule , DictationSession , InsertStatus , PolishMode ,
5409+ ChineseScriptPreference , CorrectionRule , DictationSession , InsertStatus , MacosNewlineMode ,
5410+ PolishMode ,
53765411 } ;
53775412 use uuid:: Uuid ;
53785413
5414+ #[ test]
5415+ fn macos_auto_newline_uses_line_feed_in_known_terminals ( ) {
5416+ for front_app in [
5417+ "Terminal (com.apple.Terminal)" ,
5418+ "iTerm2 (com.googlecode.iterm2)" ,
5419+ "Warp (dev.warp.Warp-Stable)" ,
5420+ "WezTerm (com.github.wez.wezterm)" ,
5421+ "Alacritty (io.alacritty)" ,
5422+ "Alacritty (org.alacritty)" ,
5423+ "kitty (net.kovidgoyal.kitty)" ,
5424+ "Hyper (co.zeit.hyper)" ,
5425+ "Tabby (org.tabby)" ,
5426+ "Tabby (com.tabby)" ,
5427+ "Ghostty (com.mitchellh.ghostty)" ,
5428+ ] {
5429+ assert_eq ! (
5430+ resolve_macos_newline_mode( MacosNewlineMode :: Auto , Some ( front_app) ) ,
5431+ MacosNewlineMode :: LineFeed ,
5432+ "{front_app} should use U+000A"
5433+ ) ;
5434+ }
5435+ }
5436+
5437+ #[ test]
5438+ fn macos_auto_newline_uses_shift_return_outside_known_terminals ( ) {
5439+ for front_app in [
5440+ None ,
5441+ Some ( "Terminal" ) ,
5442+ Some ( "Safari (com.apple.Safari)" ) ,
5443+ Some ( "Slack (com.tinyspeck.slackmacgap)" ) ,
5444+ ] {
5445+ assert_eq ! (
5446+ resolve_macos_newline_mode( MacosNewlineMode :: Auto , front_app) ,
5447+ MacosNewlineMode :: ShiftReturn ,
5448+ "{front_app:?} should use the chat-safe fallback"
5449+ ) ;
5450+ }
5451+ }
5452+
5453+ #[ test]
5454+ fn macos_explicit_newline_modes_override_target_app_detection ( ) {
5455+ let terminal = Some ( "Terminal (com.apple.Terminal)" ) ;
5456+ for configured in [
5457+ MacosNewlineMode :: ShiftReturn ,
5458+ MacosNewlineMode :: LineFeed ,
5459+ MacosNewlineMode :: Return ,
5460+ ] {
5461+ assert_eq ! ( resolve_macos_newline_mode( configured, terminal) , configured) ;
5462+ }
5463+ }
5464+
53795465 #[ test]
53805466 fn sandbox_providers_legacy_permission_modes_fail_closed_to_read_only ( ) {
53815467 use crate :: coding_agent:: { CodingAgentPermissionMode as M , CodingAgentProvider as P } ;
0 commit comments