feat(macos): auto-select newline mode for terminal apps (#988)

* feat(macos): add line feed newline option

* feat(macos): auto-select newline mode by target app

* style(macos): localize newline fallback comment
This commit is contained in:
Chris233 authored and GitHub committed 2026-08-23 16:30:17 +08:00
1 parent 3f59b1352e
commit 6efc8396ea
12 files changed
+204 -39

No files matched your search

@@ -512,7 +512,14 @@ async fn run_streaming_polish(
#[cfg(target_os = "windows")]
let sendinput_options = windows_sendinput_options_from_prefs(&inner.prefs.get());
#[cfg(target_os = "macos")]
let macos_newline_mode = inner.prefs.get().macos_newline_mode;
let macos_newline_mode = {
let configured = inner.prefs.get().macos_newline_mode;
let resolved = resolve_macos_newline_mode(configured, front_app);
log::info!(
"[coord] streaming_insert: macOS newline mode configured={configured:?} resolved={resolved:?}"
);
resolved
};
let typer_handle = tokio::task::spawn_blocking(move || {
#[cfg(target_os = "windows")]
{
@@ -699,6 +706,33 @@ async fn run_streaming_polish(
}
}
/// 把 Auto 解析成单次听写实际使用的模式。前台应用在听写开始时已经捕获,整个流式上屏
/// 过程使用同一结果;未知应用保守使用 Shift+Return,避免聊天框换行时误发送。
#[cfg_attr(not(any(target_os = "macos", test)), allow(dead_code))]
fn resolve_macos_newline_mode(
configured: crate::types::MacosNewlineMode,
front_app: Option<&str>,
) -> crate::types::MacosNewlineMode {
use crate::types::MacosNewlineMode;
if configured != MacosNewlineMode::Auto {
return configured;
}
let bundle_id = front_app.and_then(|label| {
let front = crate::types::split_front_app_label(label, true);
front.bundle_id.or(front.name)
});
if bundle_id
.as_deref()
.is_some_and(crate::host_document::is_terminal_bundle_id)
{
MacosNewlineMode::LineFeed
} else {
MacosNewlineMode::ShiftReturn
}
}
#[cfg(target_os = "windows")]
pub(super) fn windows_sendinput_options_from_prefs(
prefs: &crate::types::UserPreferences,
@@ -5363,19 +5397,71 @@ mod tests {
append_typed_prefix, batch_asr_chunk_limit_ms, build_transcribe_failed_session,
coding_agent_mode_from_pref, default_done_message, drain_streaming_insert_deltas_with,
eligible_polish_context_turns, finalize_polished_text, flush_streaming_insert_buffer_with,
pcm_duration_ms, pcm_from_wav_bytes, retry_error_outcome, should_arm_edit_watch,
should_attempt_silent_retry, should_read_cursor_context, insert_delivery_failed,
resolve_less_computer_run_outcome, streaming_insert_eligible,
SilentRetryOutcome,
insert_delivery_failed, pcm_duration_ms, pcm_from_wav_bytes,
resolve_less_computer_run_outcome, resolve_macos_newline_mode, retry_error_outcome,
should_arm_edit_watch, should_attempt_silent_retry, should_read_cursor_context,
streaming_insert_eligible, SilentRetryOutcome,
};
#[cfg(any(target_os = "macos", target_os = "linux"))]
use super::{desktop_keyless_dictation_provider, DesktopKeylessDictationProvider};
use crate::coordinator::RetranscribeError;
use crate::types::{
ChineseScriptPreference, CorrectionRule, DictationSession, InsertStatus, PolishMode,
ChineseScriptPreference, CorrectionRule, DictationSession, InsertStatus, MacosNewlineMode,
PolishMode,
};
use uuid::Uuid;
#[test]
fn macos_auto_newline_uses_line_feed_in_known_terminals() {
for front_app in [
"Terminal (com.apple.Terminal)",
"iTerm2 (com.googlecode.iterm2)",
"Warp (dev.warp.Warp-Stable)",
"WezTerm (com.github.wez.wezterm)",
"Alacritty (io.alacritty)",
"Alacritty (org.alacritty)",
"kitty (net.kovidgoyal.kitty)",
"Hyper (co.zeit.hyper)",
"Tabby (org.tabby)",
"Tabby (com.tabby)",
"Ghostty (com.mitchellh.ghostty)",
] {
assert_eq!(
resolve_macos_newline_mode(MacosNewlineMode::Auto, Some(front_app)),
MacosNewlineMode::LineFeed,
"{front_app} should use U+000A"
);
}
}
#[test]
fn macos_auto_newline_uses_shift_return_outside_known_terminals() {
for front_app in [
None,
Some("Terminal"),
Some("Safari (com.apple.Safari)"),
Some("Slack (com.tinyspeck.slackmacgap)"),
] {
assert_eq!(
resolve_macos_newline_mode(MacosNewlineMode::Auto, front_app),
MacosNewlineMode::ShiftReturn,
"{front_app:?} should use the chat-safe fallback"
);
}
}
#[test]
fn macos_explicit_newline_modes_override_target_app_detection() {
let terminal = Some("Terminal (com.apple.Terminal)");
for configured in [
MacosNewlineMode::ShiftReturn,
MacosNewlineMode::LineFeed,
MacosNewlineMode::Return,
] {
assert_eq!(resolve_macos_newline_mode(configured, terminal), configured);
}
}
#[test]
fn sandbox_providers_legacy_permission_modes_fail_closed_to_read_only() {
use crate::coding_agent::{CodingAgentPermissionMode as M, CodingAgentProvider as P};
@@ -183,19 +183,15 @@ pub struct GateInputs {
/// AX 里表示「密码输入框」的 role/subrole 值。
const AX_SECURE_TEXT_FIELD: &str = "axsecuretextfield";
/// 一律不读的 app(bundle id 前缀,小写比较)。
/// 一律不读的敏感 app(bundle id 前缀,小写比较)。
///
/// 不做 UI —— 黑名单 UI 会给用户「配一下就安全了」的错觉,而真正的防线是默认关闭
/// 加这里的硬编码。这份清单只覆盖「内容几乎必然敏感」的两类:
///
/// - **密码管理器 / 钥匙串**:正文就是凭据本身。
/// - **终端**:命令行里混着 token、私钥路径、内网地址,而且很多终端的 AX 会把整个
/// scrollback 当作一个文本元素返回 —— 一读就是几千行历史命令。
///
/// 前缀匹配,所以 `com.1password` 能同时盖住 `com.1password.1password` 和其
/// helper 进程。
const BLOCKED_BUNDLE_PREFIXES: &[&str] = &[
// 密码管理器 / 钥匙串
const SENSITIVE_BUNDLE_PREFIXES: &[&str] = &[
"com.1password",
"com.agilebits.onepassword",
"com.apple.keychainaccess",
@@ -207,7 +203,11 @@ const BLOCKED_BUNDLE_PREFIXES: &[&str] = &[
"in.sinew.enpass",
"com.sinew.enpass",
"com.apple.passwords",
// 终端
];
/// 终端 app 的 bundle id 前缀。除了禁止读取 scrollback,也供 macOS 自动换行模式判断:
/// 已知终端发送 U+000A,其它应用保守发送 Shift+Return。
const TERMINAL_BUNDLE_PREFIXES: &[&str] = &[
"com.apple.terminal",
"com.googlecode.iterm2",
"dev.warp.warp",
@@ -221,6 +221,15 @@ const BLOCKED_BUNDLE_PREFIXES: &[&str] = &[
"com.mitchellh.ghostty",
];
fn bundle_id_starts_with_any(bundle_id: &str, prefixes: &[&str]) -> bool {
let lowered = bundle_id.to_ascii_lowercase();
prefixes.iter().any(|prefix| lowered.starts_with(prefix))
}
pub(crate) fn is_terminal_bundle_id(bundle_id: &str) -> bool {
bundle_id_starts_with_any(bundle_id, TERMINAL_BUNDLE_PREFIXES)
}
/// 闸门判定。返回 `Some(reason)` 表示拦下,`None` 表示放行。
///
/// 判定顺序按「代价从低到高」:Secure Input 和 bundle 前缀不需要 AX,先判;
@@ -230,10 +239,8 @@ pub fn evaluate_gate(inputs: &GateInputs) -> Option<BlockReason> {
return Some(BlockReason::SecureInput);
}
if let Some(bundle) = inputs.bundle_id.as_deref() {
let lowered = bundle.to_ascii_lowercase();
if BLOCKED_BUNDLE_PREFIXES
.iter()
.any(|prefix| lowered.starts_with(prefix))
if bundle_id_starts_with_any(bundle, SENSITIVE_BUNDLE_PREFIXES)
|| is_terminal_bundle_id(bundle)
{
return Some(BlockReason::BlockedApp);
}
+25 -4
View File
@@ -132,17 +132,22 @@ pub enum WindowsSendInputNewlineMode {
/// macOS 逐字上屏时换行符怎么发。仅流式插入路径生效。
///
/// 默认 `ShiftReturn`:macOS 把 U+000A 当 Return 键,而聊天框里 Return 就是「发送」——
/// 一条带空行的两段话会被从中间劈开发出去。Shift+Return 在聊天框是软换行,在编辑器 /
/// 终端 / 网页输入框里就是普通换行。
/// 默认 `Auto`:已知终端应用发送 U+000A,其它或未知应用发送 Shift+Return。
///
/// Terminal.app 无法区分 Shift+Return 和 Return,里面的 Codex / Claude Code 等 TUI
/// 会把它当成「提交」。`LineFeed` 恢复发送 U+000A,让这些 TUI 将其识别为 Ctrl+J 软换行。
///
/// 保留 `Return` 是因为风格市场里有靠换行发多条消息的风格包,那种效果需要真回车。
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "camelCase")]
pub enum MacosNewlineMode {
/// Shift+Return:聊天框软换行,不发送。
/// 按听写开始时捕获的前台应用自动选择;未知应用安全回退到 Shift+Return。
#[default]
Auto,
/// Shift+Return:聊天框软换行,不发送。
ShiftReturn,
/// U+000A:Terminal.app / CLI Agent 中作为 Ctrl+J 软换行。
LineFeed,
/// Return:聊天框里等于发送 —— 想要「一段话拆成多条消息」的风格包用这个。
Return,
}
@@ -3706,6 +3711,22 @@ mod tests {
assert!(!json.contains("windowsSendinputNewlineMode"));
}
#[test]
fn macos_newline_mode_defaults_to_auto() {
let prefs: UserPreferences = serde_json::from_str("{}").unwrap();
assert_eq!(prefs.macos_newline_mode, MacosNewlineMode::Auto);
}
#[test]
fn macos_newline_mode_round_trips_line_feed() {
let prefs: UserPreferences =
serde_json::from_str(r#"{"macosNewlineMode":"lineFeed"}"#).unwrap();
assert_eq!(prefs.macos_newline_mode, MacosNewlineMode::LineFeed);
let json = serde_json::to_string(&prefs).unwrap();
assert!(json.contains(r#""macosNewlineMode":"lineFeed""#));
}
#[test]
fn windows_sendinput_insertion_only_serializes_frontend_wire_key() {
let enabled = UserPreferences {
@@ -103,6 +103,8 @@ mod macos_impl {
pub(super) enum MacKeystroke {
/// 换行:发真实的 Shift+Return 按键(聊天框软换行)。
ShiftReturn,
/// 换行:发送 Unicode U+000A(Terminal.app / CLI Agent 中作为 Ctrl+J 软换行)。
LineFeed,
/// 换行:发真实的 Return 按键(聊天框里等于发送)。
Return,
/// CR:不发任何键。`\r\n` 里它只是 `\n` 的前缀,发了会变成两个换行;
@@ -113,22 +115,29 @@ mod macos_impl {
Unicode,
}
/// **换行必须走真实按键,不能当普通 Unicode 字符发。**
/// 默认用真实 Shift+Return;Terminal.app / CLI Agent 可显式选择 Unicode U+000A。
///
/// macOS 的文本输入系统看到 U+000A 就当作 Return —— 在微信 / Slack / Telegram
/// 这类聊天框里等价于「发送」。曾经有一条带空行的两段话被逐字上屏,第一个 `\n`
/// 直接把上半句发了出去,下半句留在了输入框里。
///
/// 默认发 Shift+Return:在聊天框是「软换行」(不发送),在编辑器 / 终端 / 网页
/// textarea 里就是普通换行 —— 两边都对。Windows 侧早有同款结论(见
/// 默认发 Shift+Return:在聊天框是「软换行」(不发送),在编辑器 / 网页 textarea
/// 里就是普通换行。Windows 侧早有同款结论(见
/// `WindowsSendInputNewlineMode::ShiftEnter`,设置文案直接写着「聊天框选它」)。
///
/// Terminal.app 不区分 Shift+Return 和 Return,Codex / Claude Code 等 TUI 会把两者
/// 都当作「提交」。`LineFeed` 发送 U+000A,让这些 TUI 将其识别为 Ctrl+J 软换行。
///
/// 用户可以在设置里改成 `Return`:风格市场上有靠换行把一段话拆成多条消息的风格包,
/// 那种效果要的正是真回车。
pub(super) fn classify_mac_keystroke(ch: char, mode: MacosNewlineMode) -> MacKeystroke {
match ch {
'\n' => match mode {
// `Auto` 通常会在逐字上屏任务启动前解析;缺少前台应用上下文的调用方
// 使用对聊天框安全的 Shift+Return 兜底。
MacosNewlineMode::Auto => MacKeystroke::ShiftReturn,
MacosNewlineMode::ShiftReturn => MacKeystroke::ShiftReturn,
MacosNewlineMode::LineFeed => MacKeystroke::LineFeed,
MacosNewlineMode::Return => MacKeystroke::Return,
},
'\r' => MacKeystroke::Swallow,
@@ -162,6 +171,7 @@ mod macos_impl {
for ch in text.chars() {
let sent = match classify_mac_keystroke(ch, newline_mode) {
MacKeystroke::ShiftReturn => send_shift_return(),
MacKeystroke::LineFeed => send_line_feed(),
MacKeystroke::Return => send_return(),
// 吞掉的 char 也要计数:调用方(`flush_streaming_insert_buffer_with`)
// 拿 `typed_chars` 和 `delta.chars().count()` 比对,少一个就判定
@@ -204,6 +214,12 @@ mod macos_impl {
post_key_event(KEY_RETURN, KCG_EVENT_FLAG_MASK_SHIFT, None)
}
/// 发送 Unicode U+000A。Terminal.app 会把它转给 TUI,Codex / Claude Code 等将其
/// 识别为 Ctrl+J 软换行,而不是普通 Return 的「提交」。
fn send_line_feed() -> Result<(), TypeError> {
send_one_codepoint('\n')
}
/// 发一次不带修饰键的 Return。聊天框里这等于「发送」——只有用户在设置里明确选了
/// [`MacosNewlineMode::Return`] 才会走到这里。
fn send_return() -> Result<(), TypeError> {
@@ -666,16 +682,16 @@ mod linux_impl {
mod tests {
use super::TypeError;
/// 默认模式下换行走 Shift+Return —— macOS 把 U+000A 当 Return,聊天框里等于
/// 「发送」,一条带空行的两段话会被从中间劈开发出去。
/// 没有前台应用上下文时,未解析的 Auto 安全回退到 Shift+Return,避免聊天框里
/// U+000A 被当作 Return 后直接发送。
#[test]
#[cfg(target_os = "macos")]
fn newline_defaults_to_shift_return() {
fn unresolved_auto_mode_falls_back_to_shift_return() {
use super::macos_impl::{classify_mac_keystroke, MacKeystroke};
use crate::types::MacosNewlineMode;
let mode = MacosNewlineMode::default();
assert_eq!(mode, MacosNewlineMode::ShiftReturn, "默认必须是不发送的那个");
assert_eq!(mode, MacosNewlineMode::Auto);
assert_eq!(
classify_mac_keystroke('\n', mode),
MacKeystroke::ShiftReturn
@@ -710,6 +726,24 @@ mod tests {
);
}
/// Terminal.app 不区分 Shift+Return 和 Return;显式 LineFeed 模式必须改发
/// Unicode U+000A,供 Codex / Claude Code 等 TUI 识别为 Ctrl+J 软换行。
#[test]
#[cfg(target_os = "macos")]
fn line_feed_mode_sends_unicode_lf_for_terminal_cli_agents() {
use super::macos_impl::{classify_mac_keystroke, MacKeystroke};
use crate::types::MacosNewlineMode;
assert_eq!(
classify_mac_keystroke('\n', MacosNewlineMode::LineFeed),
MacKeystroke::LineFeed
);
assert_eq!(
classify_mac_keystroke('中', MacosNewlineMode::LineFeed),
MacKeystroke::Unicode
);
}
/// 计数契约:`type_unicode_chunk` 返回的 typed_chars 必须等于输入的 char 数,
/// 连被吞掉的 `\r` 也要算 —— 调用方拿它跟 `delta.chars().count()` 比对,
/// 少一个就判定「部分失败」并丢弃后面所有 delta。
@@ -719,7 +753,12 @@ mod tests {
use super::macos_impl::classify_mac_keystroke;
use crate::types::MacosNewlineMode;
for mode in [MacosNewlineMode::ShiftReturn, MacosNewlineMode::Return] {
for mode in [
MacosNewlineMode::Auto,
MacosNewlineMode::ShiftReturn,
MacosNewlineMode::LineFeed,
MacosNewlineMode::Return,
] {
let text = "上半句\r\n\r\n下半句";
// 每个 char 都会被分类成某一种处理方式,没有漏网的。
let counted = text
+3 -1
View File
@@ -879,8 +879,10 @@ export const en: typeof zhCN = {
windowsInsertionModeSendInput: 'SendInput keystroke simulation',
windowsInsertionModePaste: 'Clipboard paste (Ctrl+V, etc.)',
macosNewlineModeLabel: 'Line breaks',
macosNewlineModeDesc: 'Which key a line break sends while typing out text. A plain Return means "send" in chat apps — keep Shift+Return unless your style pack deliberately splits one dictation into several messages.',
macosNewlineModeDesc: 'Auto uses Line Feed (U+000A / Ctrl+J) in known terminal apps and Shift+Return elsewhere. Plain Return sends the message.',
macosNewlineModeAuto: 'Auto (Line Feed in terminals)',
macosNewlineModeShiftReturn: 'Shift+Return (newline in chat)',
macosNewlineModeLineFeed: 'Line Feed (terminal CLI / Ctrl+J)',
macosNewlineModeReturn: 'Return (split into messages)',
windowsSendInputNewlineModeLabel: 'SendInput newline simulation',
windowsSendInputNewlineModeDesc: 'How SendInput turns line breaks into keys. Use Shift+Enter for chat boxes; Enter for Notepad / VS Code and most editors.',
+3 -1
View File
@@ -881,8 +881,10 @@ export const ja: typeof zhCN = {
windowsInsertionModeSendInput: 'SendInput キー入力シミュレーション',
windowsInsertionModePaste: 'クリップボード貼り付け(Ctrl+V など)',
macosNewlineModeLabel: '改行の送り方',
macosNewlineModeDesc: '逐次入力時に改行をどのキーで送るか。チャットでは通常の Return は「送信」になります。改行で複数メッセージに分けるスタイルパックを使う場合以外は Shift+Return のままに。',
macosNewlineModeDesc: '自動では既知のターミナルアプリに Line Feed(U+000A / Ctrl+J)、それ以外に Shift+Return を送ります。通常の Return は送信になります。',
macosNewlineModeAuto: '自動(ターミナルでは Line Feed)',
macosNewlineModeShiftReturn: 'Shift+Return(チャットで改行)',
macosNewlineModeLineFeed: 'Line Feed(ターミナル CLI / Ctrl+J)',
macosNewlineModeReturn: 'Return(複数メッセージに分割)',
windowsSendInputNewlineModeLabel: 'SendInput 改行シミュレーション',
windowsSendInputNewlineModeDesc: 'SendInput で改行をどのキーとして送るか。チャット入力は Shift+Enter、メモ帳 / VS Code などは Enter。',
+3 -1
View File
@@ -881,8 +881,10 @@ export const ko: typeof zhCN = {
windowsInsertionModeSendInput: 'SendInput 키 입력 시뮬레이션',
windowsInsertionModePaste: '클립보드 붙여넣기(Ctrl+V 등)',
macosNewlineModeLabel: '줄바꿈 처리',
macosNewlineModeDesc: '한 글자씩 입력할 때 줄바꿈을 어느 키로 보낼지. 채팅에서 일반 Return은 전송을 의미합니다. 줄바꿈으로 여러 메시지를 나누는 스타일 팩을 쓰는 게 아니라면 Shift+Return을 유지하세요.',
macosNewlineModeDesc: '자동은 알려진 터미널 앱에서 Line Feed(U+000A / Ctrl+J)를, 그 밖의 앱에서는 Shift+Return을 사용합니다. 일반 Return은 메시지를 전송합니다.',
macosNewlineModeAuto: '자동(터미널에서는 Line Feed)',
macosNewlineModeShiftReturn: 'Shift+Return(채팅에서 줄바꿈)',
macosNewlineModeLineFeed: 'Line Feed(터미널 CLI / Ctrl+J)',
macosNewlineModeReturn: 'Return(여러 메시지로 분할)',
windowsSendInputNewlineModeLabel: 'SendInput 줄바꿈 시뮬레이션',
windowsSendInputNewlineModeDesc: 'SendInput 모드에서 줄바꿈을 어떤 키로 보낼지. 채팅 입력창은 Shift+Enter, 메모장 / VS Code 등은 Enter.',
+3 -1
View File
@@ -877,8 +877,10 @@ export const zhCN = {
windowsInsertionModeSendInput: 'SendInput 逐字模拟',
windowsInsertionModePaste: '剪贴板粘贴(Ctrl+V 等)',
macosNewlineModeLabel: '换行怎么落',
macosNewlineModeDesc: '逐字上屏时换行符发哪个按键。聊天框里真回车等于发送——除非你用的风格包就是靠换行拆成多条消息,否则保持 Shift+Return。',
macosNewlineModeDesc: '自动会在已知终端应用中使用 Line Feed(U+000A / Ctrl+J),其他应用使用 Shift+Return;Return 会直接发送。',
macosNewlineModeAuto: '自动(终端使用 Line Feed)',
macosNewlineModeShiftReturn: 'Shift+Return(聊天框换行)',
macosNewlineModeLineFeed: 'Line Feed(终端 CLI / Ctrl+J)',
macosNewlineModeReturn: 'Return(拆成多条消息)',
windowsSendInputNewlineModeLabel: 'SendInput 换行模拟',
windowsSendInputNewlineModeDesc: 'SendInput 模式下如何把换行符模拟成按键。聊天框通常选 Shift+Enter;记事本 / VS Code 等选 Enter。',
+3 -1
View File
@@ -879,8 +879,10 @@ export const zhTW: typeof zhCN = {
windowsInsertionModeSendInput: 'SendInput 逐字模擬',
windowsInsertionModePaste: '剪貼簿貼上(Ctrl+V 等)',
macosNewlineModeLabel: '換行怎麼落',
macosNewlineModeDesc: '逐字上屏時換行符發哪個按鍵。聊天框裡真回車等於發送——除非你用的風格包就是靠換行拆成多條訊息,否則保持 Shift+Return。',
macosNewlineModeDesc: '自動會在已知終端應用中使用 Line Feed(U+000A / Ctrl+J),其他應用使用 Shift+Return;Return 會直接發送。',
macosNewlineModeAuto: '自動(終端使用 Line Feed)',
macosNewlineModeShiftReturn: 'Shift+Return(聊天框換行)',
macosNewlineModeLineFeed: 'Line Feed(終端 CLI / Ctrl+J)',
macosNewlineModeReturn: 'Return(拆成多條訊息)',
windowsSendInputNewlineModeLabel: 'SendInput 換行模擬',
windowsSendInputNewlineModeDesc: 'SendInput 模式下如何把換行符模擬成按鍵。聊天框通常選 Shift+Enter;記事本 / VS Code 等選 Enter。',
+1 -1
View File
@@ -60,7 +60,7 @@ export let mockSettings: UserPreferences = {
allowNonTsfInsertionFallback: true,
windowsInsertionMode: "tsf",
windowsSendInputNewlineMode: "enter",
macosNewlineMode: "shiftReturn",
macosNewlineMode: "auto",
windowsSendInputInsertionOnly: false,
windowsShowOpenlessInKeyboardList: true,
workingLanguages: ["简体中文"],
+3 -3
View File
@@ -239,9 +239,9 @@ export type WindowsInsertionMode = 'tsf' | 'sendInput' | 'paste';
/** Windows SendInput 路径的换行模拟方式。 */
export type WindowsSendInputNewlineMode = 'enter' | 'shiftEnter' | 'crlf';
/** macOS 逐字上屏时换行符怎么发。`return` 在聊天框里等于发送 —— 靠换行拆多条消息的
* 风格包要的就是这个。 */
export type MacosNewlineMode = 'shiftReturn' | 'return';
/** macOS 逐字上屏时换行符怎么发。`auto` 按前台应用选择;`lineFeed` 供终端使用;
* `return` 在聊天框里等于发送 —— 靠换行拆多条消息的风格包要的就是这个。 */
export type MacosNewlineMode = 'auto' | 'shiftReturn' | 'lineFeed' | 'return';
export type WindowsImeInstallState =
| 'installed'
@@ -528,10 +528,12 @@ export function RecordingInputSection() {
desc={t('settings.recording.macosNewlineModeDesc')}
>
<SelectLite
value={prefs.macosNewlineMode ?? 'shiftReturn'}
value={prefs.macosNewlineMode ?? 'auto'}
onChange={next => onMacosNewlineModeChange(next as MacosNewlineMode)}
options={[
{ value: 'auto', label: t('settings.recording.macosNewlineModeAuto') },
{ value: 'shiftReturn', label: t('settings.recording.macosNewlineModeShiftReturn') },
{ value: 'lineFeed', label: t('settings.recording.macosNewlineModeLineFeed') },
{ value: 'return', label: t('settings.recording.macosNewlineModeReturn') },
]}
ariaLabel={t('settings.recording.macosNewlineModeLabel')}