diff --git a/src/main/claw-runtime.test.ts b/src/main/claw-runtime.test.ts index 374a69237..2ebf3ea85 100644 --- a/src/main/claw-runtime.test.ts +++ b/src/main/claw-runtime.test.ts @@ -27,6 +27,7 @@ function buildSettings(): AppSettingsV1 { theme: 'system', uiFontScale: 0.82, chatContentMaxWidthPx: 896, + composerSendKey: 'enter', provider: defaultModelProviderSettings(), agents: { kun: defaultKunRuntimeSettings() diff --git a/src/main/claw-schedule-mcp-config.test.ts b/src/main/claw-schedule-mcp-config.test.ts index 47845134a..cf166c35b 100644 --- a/src/main/claw-schedule-mcp-config.test.ts +++ b/src/main/claw-schedule-mcp-config.test.ts @@ -36,6 +36,7 @@ function createSettings(patch: Partial = theme: 'system', uiFontScale: 0.82, chatContentMaxWidthPx: 896, + composerSendKey: 'enter', provider: defaultModelProviderSettings(), agents: { kun: defaultKunRuntimeSettings() diff --git a/src/main/claw-scheduled-task-detector.test.ts b/src/main/claw-scheduled-task-detector.test.ts index 219a3c99e..2dce02461 100644 --- a/src/main/claw-scheduled-task-detector.test.ts +++ b/src/main/claw-scheduled-task-detector.test.ts @@ -30,6 +30,7 @@ function settings(endpointFormat: ModelEndpointFormat): AppSettingsV1 { theme: 'system', uiFontScale: 0.82, chatContentMaxWidthPx: 896, + composerSendKey: 'enter', provider, agents: { kun: defaultKunRuntimeSettings() diff --git a/src/main/data-migration/application-state-migration.ts b/src/main/data-migration/application-state-migration.ts index d249840bf..e87ddb163 100644 --- a/src/main/data-migration/application-state-migration.ts +++ b/src/main/data-migration/application-state-migration.ts @@ -32,6 +32,9 @@ export function applyPortableSettingsMigration( ...(typeof value.chatContentMaxWidthPx === 'number' ? { chatContentMaxWidthPx: value.chatContentMaxWidthPx as AppSettingsV1['chatContentMaxWidthPx'] } : {}), + ...(value.composerSendKey === 'enter' || value.composerSendKey === 'shiftEnter' + ? { composerSendKey: value.composerSendKey } + : {}), ...(typeof value.cursorSpotlight === 'boolean' ? { cursorSpotlight: value.cursorSpotlight } : {}), ...(typeof value.cursorSpotlightColor === 'string' ? { cursorSpotlightColor: value.cursorSpotlightColor } : {}), notifications: { diff --git a/src/main/data-migration/export-inventory.test.ts b/src/main/data-migration/export-inventory.test.ts index 5c127d5c1..675d1b14f 100644 --- a/src/main/data-migration/export-inventory.test.ts +++ b/src/main/data-migration/export-inventory.test.ts @@ -39,6 +39,7 @@ function settings(workspaceRoot: string, nestedRoot = workspaceRoot): AppSetting theme: 'dark', uiFontScale: 1, chatContentMaxWidthPx: 896, + composerSendKey: 'enter', provider: { ...defaultModelProviderSettings(), apiKey: 'must-not-export' }, agents: { kun: { ...defaultKunRuntimeSettings(), runtimeToken: 'must-not-export' } }, workspaceRoot, diff --git a/src/main/data-migration/export-inventory.ts b/src/main/data-migration/export-inventory.ts index b3776fa20..98f20a5e3 100644 --- a/src/main/data-migration/export-inventory.ts +++ b/src/main/data-migration/export-inventory.ts @@ -219,6 +219,7 @@ export function portableSettingsForMigration(settings: AppSettingsV1): Record { locale: 'en', theme: 'system', uiFontScale: 0.82, - chatContentMaxWidthPx: 896, + chatContentMaxWidthPx: 896, + composerSendKey: 'enter', provider: defaultModelProviderSettings(), agents: { kun: defaultKunRuntimeSettings(19000) diff --git a/src/main/runtime-settings-apply-mode.test.ts b/src/main/runtime-settings-apply-mode.test.ts index 3e0b452c7..cb76bf950 100644 --- a/src/main/runtime-settings-apply-mode.test.ts +++ b/src/main/runtime-settings-apply-mode.test.ts @@ -25,6 +25,7 @@ function settings(): AppSettingsV1 { theme: 'system', uiFontScale: 0.82, chatContentMaxWidthPx: 896, + composerSendKey: 'enter', provider: defaultModelProviderSettings(), agents: { kun: defaultKunRuntimeSettings() diff --git a/src/main/runtime/kun-adapter.test.ts b/src/main/runtime/kun-adapter.test.ts index 2428351d9..effa681d3 100644 --- a/src/main/runtime/kun-adapter.test.ts +++ b/src/main/runtime/kun-adapter.test.ts @@ -24,6 +24,7 @@ function settingsForPort(port: number): AppSettingsV1 { theme: 'system', uiFontScale: 0.82, chatContentMaxWidthPx: 896, + composerSendKey: 'enter', provider: defaultModelProviderSettings(), agents: { kun: { diff --git a/src/main/runtime/managed-runtime-idle.test.ts b/src/main/runtime/managed-runtime-idle.test.ts index b334a09bc..428a3cbc2 100644 --- a/src/main/runtime/managed-runtime-idle.test.ts +++ b/src/main/runtime/managed-runtime-idle.test.ts @@ -22,7 +22,8 @@ const settings: AppSettingsV1 = { locale: 'en', theme: 'system', uiFontScale: 0.82, - chatContentMaxWidthPx: 896, + chatContentMaxWidthPx: 896, + composerSendKey: 'enter', provider: defaultModelProviderSettings(), agents: { kun: defaultKunRuntimeSettings() }, workspaceRoot: '/tmp/workspace', diff --git a/src/main/schedule-runtime.test.ts b/src/main/schedule-runtime.test.ts index e4cc03a27..8851232a7 100644 --- a/src/main/schedule-runtime.test.ts +++ b/src/main/schedule-runtime.test.ts @@ -92,6 +92,7 @@ function settingsWith( theme: 'system', uiFontScale: 0.82, chatContentMaxWidthPx: 896, + composerSendKey: 'enter', provider: defaultModelProviderSettings(), agents: { kun: { diff --git a/src/main/services/prompt-optimization-service.test.ts b/src/main/services/prompt-optimization-service.test.ts index b84fca9e0..6addea8cf 100644 --- a/src/main/services/prompt-optimization-service.test.ts +++ b/src/main/services/prompt-optimization-service.test.ts @@ -21,6 +21,7 @@ function createSettings(patch: Partial = {}): Ap theme: 'system', uiFontScale: 0.82, chatContentMaxWidthPx: 896, + composerSendKey: 'enter', provider: defaultModelProviderSettings(), agents: { kun: { diff --git a/src/main/services/skill-service.test.ts b/src/main/services/skill-service.test.ts index 6ca769681..9a62d9609 100644 --- a/src/main/services/skill-service.test.ts +++ b/src/main/services/skill-service.test.ts @@ -337,7 +337,8 @@ describe('skill-service', () => { locale: 'en', theme: 'system', uiFontScale: 0.82, - chatContentMaxWidthPx: 896, + chatContentMaxWidthPx: 896, + composerSendKey: 'enter', provider: defaultModelProviderSettings(), agents: { kun: defaultKunRuntimeSettings() }, workspaceRoot, diff --git a/src/main/services/write-inline-completion-service.test.ts b/src/main/services/write-inline-completion-service.test.ts index bead93d41..4875d7caa 100644 --- a/src/main/services/write-inline-completion-service.test.ts +++ b/src/main/services/write-inline-completion-service.test.ts @@ -32,6 +32,7 @@ function createSettings(patch: Partial ({ theme: 'system', uiFontScale: DEFAULT_UI_FONT_SCALE, chatContentMaxWidthPx: DEFAULT_CHAT_CONTENT_MAX_WIDTH_PX, + composerSendKey: DEFAULT_COMPOSER_SEND_KEY, cursorSpotlight: true, cursorSpotlightColor: DEFAULT_CURSOR_SPOTLIGHT_COLOR, provider: defaultModelProviderSettings(), diff --git a/src/main/upstream-models.test.ts b/src/main/upstream-models.test.ts index 4247d1ce9..4db2c9982 100644 --- a/src/main/upstream-models.test.ts +++ b/src/main/upstream-models.test.ts @@ -27,6 +27,7 @@ function settings(dataDir: string, model = 'settings-model'): AppSettingsV1 { theme: 'system', uiFontScale: 0.82, chatContentMaxWidthPx: 896, + composerSendKey: 'enter', provider: { ...provider, providers: [ diff --git a/src/main/workflow-runtime.nodes.test.ts b/src/main/workflow-runtime.nodes.test.ts index 0b53c6e00..8971618e4 100644 --- a/src/main/workflow-runtime.nodes.test.ts +++ b/src/main/workflow-runtime.nodes.test.ts @@ -122,6 +122,7 @@ function buildSettings( theme: 'system', uiFontScale: 0.82, chatContentMaxWidthPx: 896, + composerSendKey: 'enter', provider: defaultModelProviderSettings(), agents: { kun: { ...defaultKunRuntimeSettings(), model: 'test-model', apiKey: 'test-key' } }, workspaceRoot: workflowWorkspaceRoot, diff --git a/src/main/workflow-runtime.run.test.ts b/src/main/workflow-runtime.run.test.ts index 415cbc64d..7ca0bcf1c 100644 --- a/src/main/workflow-runtime.run.test.ts +++ b/src/main/workflow-runtime.run.test.ts @@ -54,6 +54,7 @@ function settingsWithWorkflows(workflows: WorkflowV1[], modules: WorkflowCustomM theme: 'system', uiFontScale: 0.82, chatContentMaxWidthPx: 896, + composerSendKey: 'enter', provider: defaultModelProviderSettings(), agents: { kun: { ...defaultKunRuntimeSettings(), model: 'test-model', apiKey: 'test-key' } }, workspaceRoot: workflowWorkspaceRoot, diff --git a/src/renderer/src/agent/kun-runtime.test.ts b/src/renderer/src/agent/kun-runtime.test.ts index 72c7fef23..df62f17d3 100644 --- a/src/renderer/src/agent/kun-runtime.test.ts +++ b/src/renderer/src/agent/kun-runtime.test.ts @@ -23,6 +23,7 @@ function settings(): AppSettingsV1 { theme: 'system', uiFontScale: 0.82, chatContentMaxWidthPx: 896, + composerSendKey: 'enter', provider: defaultModelProviderSettings(), agents: { kun: defaultKunRuntimeSettings() diff --git a/src/renderer/src/agent/runtime-client.test.ts b/src/renderer/src/agent/runtime-client.test.ts index 6661e65f1..4bb7754da 100644 --- a/src/renderer/src/agent/runtime-client.test.ts +++ b/src/renderer/src/agent/runtime-client.test.ts @@ -20,6 +20,7 @@ function settings(apiKey: string): AppSettingsV1 { theme: 'system', uiFontScale: 0.82, chatContentMaxWidthPx: 896, + composerSendKey: 'enter', provider: defaultModelProviderSettings(), agents: { kun: { diff --git a/src/renderer/src/components/chat/FloatingComposer.tsx b/src/renderer/src/components/chat/FloatingComposer.tsx index 955d2f999..e30b0c605 100644 --- a/src/renderer/src/components/chat/FloatingComposer.tsx +++ b/src/renderer/src/components/chat/FloatingComposer.tsx @@ -118,6 +118,8 @@ import { routeComposerFileDrop, type ComposerFileDropOptions } from './composer-file-drop' +import { useComposerSendKeySetting } from '../../lib/composer-send-key-settings' +import { isComposerSendHotkey } from '@shared/app-settings' export type { ComposerFileReference } from '../../lib/composer-file-references' export type { ComposerExecutionSettings } from './FloatingComposerExecutionPicker' @@ -339,6 +341,7 @@ export function FloatingComposer({ hideBtwCommand = false }: Props): ReactElement { const { t, i18n } = useTranslation('common') + const composerSendKey = useComposerSendKeySetting() const route = useChatStore((s) => s.route) const workspaceRoot = useChatStore((s) => s.workspaceRoot) const storeActiveThreadId = useChatStore((s) => s.activeThreadId) @@ -540,7 +543,9 @@ export function FloatingComposer({ : t('clawComposerHintNeedsInbound') : useWorktreePool ? t('composerWorktreeModeHint') - : t('composerShortcut') + : composerSendKey === 'shiftEnter' + ? t('composerShortcutShiftEnter') + : t('composerShortcut') const showTodoProgress = !compact && route === 'chat' && Boolean(activeThreadId) @@ -968,8 +973,7 @@ export function FloatingComposer({ dictationPrimaryActionRef.current = primaryActionDisabled ? null : handlePrimaryAction const handleComposerKeyDown = (event: ReactKeyboardEvent): void => { - const sendByEnter = - event.key === 'Enter' && !event.shiftKey && !event.metaKey && !event.ctrlKey + const sendByHotkey = isComposerSendHotkey(event, composerSendKey) const composing = draft.isComposingEvent(event) if (fileMentions.handleKeyDown(event, composing)) return @@ -987,7 +991,7 @@ export function FloatingComposer({ return } - if (!sendByEnter || composing) return + if (!sendByHotkey || composing) return event.preventDefault() handlePrimaryAction() diff --git a/src/renderer/src/components/settings-section-claw.test.ts b/src/renderer/src/components/settings-section-claw.test.ts index 50db163b2..a60eb1808 100644 --- a/src/renderer/src/components/settings-section-claw.test.ts +++ b/src/renderer/src/components/settings-section-claw.test.ts @@ -93,6 +93,7 @@ function buildSettings(): AppSettingsV1 { theme: 'system', uiFontScale: 0.88, chatContentMaxWidthPx: 896, + composerSendKey: 'enter', provider: defaultModelProviderSettings(), agents: { kun: defaultKunRuntimeSettings() }, workspaceRoot: '/tmp/workspace', diff --git a/src/renderer/src/components/settings-section-general.test.ts b/src/renderer/src/components/settings-section-general.test.ts index 2591a3f84..9d4ed5629 100644 --- a/src/renderer/src/components/settings-section-general.test.ts +++ b/src/renderer/src/components/settings-section-general.test.ts @@ -27,7 +27,8 @@ function baseCtx(): Record { locale: 'zh', theme: 'dark', uiFontScale: 0.88, - chatContentMaxWidthPx: 896, + chatContentMaxWidthPx: 896, + composerSendKey: 'enter', workspaceRoot: '~/data/code/python/Kook-Voices', cursorSpotlight: true, cursorSpotlightColor: '#3b82f6', diff --git a/src/renderer/src/components/settings-section-general.tsx b/src/renderer/src/components/settings-section-general.tsx index b70deb61e..bab1bfd50 100644 --- a/src/renderer/src/components/settings-section-general.tsx +++ b/src/renderer/src/components/settings-section-general.tsx @@ -16,6 +16,7 @@ import { WRITE_INLINE_COMPLETION_MODEL_IDS, isKunRuntimeInsecure, normalizeChatContentMaxWidth, + normalizeComposerSendKey, normalizeUiFontScale } from '@shared/app-settings' import type { SkillRootId } from '../lib/skill-root-preference' @@ -400,6 +401,24 @@ export function GeneralSettingsSection({ ctx }: { ctx: Record }): R } /> + + update({ + composerSendKey: normalizeComposerSendKey(e.target.value) + }) + } + > + + + + } + /> (DEFAULT_COMPOSER_SEND_KEY) + + useEffect(() => { + let cancelled = false + const apply = (settings: AppSettingsV1): void => { + if (!cancelled) setSendKey(normalizeComposerSendKey(settings.composerSendKey)) + } + + if (typeof window.kunGui?.getSettings === 'function') { + void window.kunGui.getSettings().then(apply).catch(() => undefined) + } + + const onSettingsChanged = (event: Event): void => { + apply((event as CustomEvent).detail) + } + window.addEventListener(SETTINGS_CHANGED_EVENT, onSettingsChanged) + return () => { + cancelled = true + window.removeEventListener(SETTINGS_CHANGED_EVENT, onSettingsChanged) + } + }, []) + + return sendKey +} diff --git a/src/renderer/src/lib/settings-home-paths.test.ts b/src/renderer/src/lib/settings-home-paths.test.ts index e0b5b065e..9e80b7d52 100644 --- a/src/renderer/src/lib/settings-home-paths.test.ts +++ b/src/renderer/src/lib/settings-home-paths.test.ts @@ -95,6 +95,7 @@ function settings(): AppSettingsV1 { theme: 'system', uiFontScale: 0.82, chatContentMaxWidthPx: 896, + composerSendKey: 'enter', cursorSpotlight: true, provider: defaultModelProviderSettings(), agents: { diff --git a/src/renderer/src/locales/en/common.json b/src/renderer/src/locales/en/common.json index 5fbeba6dc..7c51226bf 100644 --- a/src/renderer/src/locales/en/common.json +++ b/src/renderer/src/locales/en/common.json @@ -1536,6 +1536,7 @@ "autoLabel": "Auto", "composerHint": "Press Enter to send · Shift+Enter for a new line · AI-generated content, verify before use", "composerShortcut": "Enter to send · Shift+Enter for newline", + "composerShortcutShiftEnter": "Shift+Enter to send · Enter for newline", "newline": "newline", "composerVerifyHint": "AI-generated content, verify before use", "composerStartHint": "Describe the task, paste code, or ask the agent to work directly in the current workspace.", diff --git a/src/renderer/src/locales/en/settings.json b/src/renderer/src/locales/en/settings.json index 8e425a55a..4844b1d75 100644 --- a/src/renderer/src/locales/en/settings.json +++ b/src/renderer/src/locales/en/settings.json @@ -210,6 +210,10 @@ "chatContentMaxWidthWide": "Wide", "chatContentMaxWidthDecrease": "Decrease conversation text width", "chatContentMaxWidthIncrease": "Increase conversation text width", + "composerSendKey": "Send shortcut", + "composerSendKeyDesc": "Choose whether Enter or Shift+Enter sends a message; the other inserts a new line.", + "composerSendKey_enter": "Enter to send · Shift+Enter for new line", + "composerSendKey_shiftEnter": "Shift+Enter to send · Enter for new line", "cursorSpotlight": "Interactive effects", "cursorSpotlightDesc": "Show a soft cursor-follow spotlight on the title bar and sidebar.", "cursorSpotlightColor": "Interaction effect color", diff --git a/src/renderer/src/locales/hi/common.json b/src/renderer/src/locales/hi/common.json index dfda7c2d8..edd8ba983 100644 --- a/src/renderer/src/locales/hi/common.json +++ b/src/renderer/src/locales/hi/common.json @@ -1536,6 +1536,7 @@ "autoLabel": "ऑटो", "composerHint": "भेजने के लिए Enter · नई लाइन के लिए Shift+Enter · AI-जनरेटेड सामग्री, उपयोग से पहले सत्यापित करें", "composerShortcut": "भेजने के लिए Enter · नई लाइन के लिए Shift+Enter", + "composerShortcutShiftEnter": "भेजने के लिए Shift+Enter · नई लाइन के लिए Enter", "newline": "नई पंक्ति", "composerVerifyHint": "एआई-जनरेटेड सामग्री, उपयोग से पहले सत्यापित करें", "composerStartHint": "कार्य का वर्णन करें, कोड पेस्ट करें, या एजेंट को वर्तमान कार्यक्षेत्र में सीधे काम करने के लिए कहें।", diff --git a/src/renderer/src/locales/hi/settings.json b/src/renderer/src/locales/hi/settings.json index 8cc22ae72..a4fcb0d94 100644 --- a/src/renderer/src/locales/hi/settings.json +++ b/src/renderer/src/locales/hi/settings.json @@ -210,6 +210,10 @@ "chatContentMaxWidthWide": "चौड़ा", "chatContentMaxWidthDecrease": "वार्तालाप पाठ की चौड़ाई कम करें", "chatContentMaxWidthIncrease": "वार्तालाप पाठ की चौड़ाई बढ़ाएँ", + "composerSendKey": "भेजने का शॉर्टकट", + "composerSendKeyDesc": "संदेश भेजने के लिए Enter या Shift+Enter चुनें; दूसरा नया लाइन डालता है।", + "composerSendKey_enter": "भेजने के लिए Enter · नई लाइन के लिए Shift+Enter", + "composerSendKey_shiftEnter": "भेजने के लिए Shift+Enter · नई लाइन के लिए Enter", "cursorSpotlight": "इंटरैक्टिव प्रभाव", "cursorSpotlightDesc": "टाइटल बार और साइडबार पर एक सॉफ्ट कर्सर-फ़ॉलो स्पॉटलाइट दिखाएँ।", "cursorSpotlightColor": "इंटरेक्शन प्रभाव रंग", diff --git a/src/renderer/src/locales/ja/common.json b/src/renderer/src/locales/ja/common.json index cc71bfd4c..d9925f006 100644 --- a/src/renderer/src/locales/ja/common.json +++ b/src/renderer/src/locales/ja/common.json @@ -1536,6 +1536,7 @@ "autoLabel": "自動", "composerHint": "Enter で送信 · Shift+Enter で改行 · AI 生成コンテンツは使用前に確認してください", "composerShortcut": "Enter で送信 · Shift+Enter で改行", + "composerShortcutShiftEnter": "Shift+Enter で送信 · Enter で改行", "newline": "改行", "composerVerifyHint": "AI によって生成されたコンテンツ、使用前に検証", "composerStartHint": "タスクを説明するか、コードを貼り付けるか、現在のワークスペースで直接作業するようにエージェントに依頼します。", diff --git a/src/renderer/src/locales/ja/settings.json b/src/renderer/src/locales/ja/settings.json index 796b9f199..0c60fa461 100644 --- a/src/renderer/src/locales/ja/settings.json +++ b/src/renderer/src/locales/ja/settings.json @@ -210,6 +210,10 @@ "chatContentMaxWidthWide": "ワイド", "chatContentMaxWidthDecrease": "会話のテキスト幅を狭くする", "chatContentMaxWidthIncrease": "会話テキストの幅を広げる", + "composerSendKey": "送信ショートカット", + "composerSendKeyDesc": "Enter または Shift+Enter のどちらで送信するかを選びます。もう一方は改行に使います。", + "composerSendKey_enter": "Enter で送信 · Shift+Enter で改行", + "composerSendKey_shiftEnter": "Shift+Enter で送信 · Enter で改行", "cursorSpotlight": "インタラクティブな効果", "cursorSpotlightDesc": "タイトル バーとサイドバーにソフト カーソル追跡スポットライトを表示します。", "cursorSpotlightColor": "インタラクション効果の色", diff --git a/src/renderer/src/locales/ko/common.json b/src/renderer/src/locales/ko/common.json index 8377aadfd..8ba0374bf 100644 --- a/src/renderer/src/locales/ko/common.json +++ b/src/renderer/src/locales/ko/common.json @@ -1536,6 +1536,7 @@ "autoLabel": "자동", "composerHint": "Enter로 전송 · Shift+Enter로 줄바꿈 · AI 생성 콘텐츠는 사용 전 확인", "composerShortcut": "Enter로 전송 · Shift+Enter로 줄바꿈", + "composerShortcutShiftEnter": "Shift+Enter로 전송 · Enter로 줄바꿈", "newline": "개행", "composerVerifyHint": "AI 생성 콘텐츠, 사용 전 검증", "composerStartHint": "작업을 설명하고, 코드를 붙여넣거나, 상담원에게 현재 작업 공간에서 직접 작업하도록 요청하세요.", diff --git a/src/renderer/src/locales/ko/settings.json b/src/renderer/src/locales/ko/settings.json index 2ced78c58..22a3c9526 100644 --- a/src/renderer/src/locales/ko/settings.json +++ b/src/renderer/src/locales/ko/settings.json @@ -210,6 +210,10 @@ "chatContentMaxWidthWide": "와이드", "chatContentMaxWidthDecrease": "대화 텍스트 너비 줄이기", "chatContentMaxWidthIncrease": "대화 텍스트 너비 늘리기", + "composerSendKey": "전송 단축키", + "composerSendKeyDesc": "Enter 또는 Shift+Enter로 메시지를 보낼지 선택합니다. 다른 키는 줄바꿈에 사용됩니다.", + "composerSendKey_enter": "Enter로 전송 · Shift+Enter로 줄바꿈", + "composerSendKey_shiftEnter": "Shift+Enter로 전송 · Enter로 줄바꿈", "cursorSpotlight": "인터랙티브 효과", "cursorSpotlightDesc": "제목 표시줄과 사이드바에 소프트 커서 따라가는 스포트라이트를 표시합니다.", "cursorSpotlightColor": "상호작용 효과 색상", diff --git a/src/renderer/src/locales/ru/common.json b/src/renderer/src/locales/ru/common.json index 68917db80..581eae216 100644 --- a/src/renderer/src/locales/ru/common.json +++ b/src/renderer/src/locales/ru/common.json @@ -1536,6 +1536,7 @@ "autoLabel": "Авто", "composerHint": "Enter — отправить · Shift+Enter — новая строка · Проверяйте содержимое, созданное AI", "composerShortcut": "Enter — отправить · Shift+Enter — новая строка", + "composerShortcutShiftEnter": "Shift+Enter — отправить · Enter — новая строка", "newline": "новая строка", "composerVerifyHint": "Контент, созданный искусственным интеллектом, проверяйте перед использованием", "composerStartHint": "Опишите задачу, вставьте код или попросите агента работать непосредственно в текущей рабочей области.", diff --git a/src/renderer/src/locales/ru/settings.json b/src/renderer/src/locales/ru/settings.json index b78029994..a116c1e94 100644 --- a/src/renderer/src/locales/ru/settings.json +++ b/src/renderer/src/locales/ru/settings.json @@ -210,6 +210,10 @@ "chatContentMaxWidthWide": "Широкая", "chatContentMaxWidthDecrease": "Уменьшить ширину текста", "chatContentMaxWidthIncrease": "Увеличить ширину текста", + "composerSendKey": "Сочетание для отправки", + "composerSendKeyDesc": "Выберите, отправлять сообщение по Enter или Shift+Enter; другая клавиша вставляет новую строку.", + "composerSendKey_enter": "Enter — отправить · Shift+Enter — новая строка", + "composerSendKey_shiftEnter": "Shift+Enter — отправить · Enter — новая строка", "cursorSpotlight": "Интерактивные эффекты", "cursorSpotlightDesc": "Показывать мягкую подсветку, следующую за курсором на заголовке и боковой панели.", "cursorSpotlightColor": "Цвет интерактивного эффекта", diff --git a/src/renderer/src/locales/th/common.json b/src/renderer/src/locales/th/common.json index c43368bef..433d5713b 100644 --- a/src/renderer/src/locales/th/common.json +++ b/src/renderer/src/locales/th/common.json @@ -1536,6 +1536,7 @@ "autoLabel": "อัตโนมัติ", "composerHint": "Enter เพื่อส่ง · Shift+Enter เพื่อขึ้นบรรทัดใหม่ · เนื้อหาที่สร้างโดย AI ตรวจสอบก่อนใช้งาน", "composerShortcut": "Enter เพื่อส่ง · Shift+Enter เพื่อขึ้นบรรทัดใหม่", + "composerShortcutShiftEnter": "Shift+Enter เพื่อส่ง · Enter เพื่อขึ้นบรรทัดใหม่", "newline": "ขึ้นบรรทัดใหม่", "composerVerifyHint": "เนื้อหาที่สร้างโดย AI ตรวจสอบก่อนใช้งาน", "composerStartHint": "อธิบายงาน วางโค้ด หรือขอให้ตัวแทนทำงานโดยตรงในพื้นที่ทำงานปัจจุบัน", diff --git a/src/renderer/src/locales/th/settings.json b/src/renderer/src/locales/th/settings.json index 1ad4a22f8..e3c1237f5 100644 --- a/src/renderer/src/locales/th/settings.json +++ b/src/renderer/src/locales/th/settings.json @@ -210,6 +210,10 @@ "chatContentMaxWidthWide": "กว้าง", "chatContentMaxWidthDecrease": "ลดความกว้างของข้อความการสนทนา", "chatContentMaxWidthIncrease": "เพิ่มความกว้างของข้อความการสนทนา", + "composerSendKey": "ทางลัดส่งข้อความ", + "composerSendKeyDesc": "เลือกใช้ Enter หรือ Shift+Enter เพื่อส่งข้อความ ส่วนอีกปุ่มใช้ขึ้นบรรทัดใหม่", + "composerSendKey_enter": "Enter เพื่อส่ง · Shift+Enter เพื่อขึ้นบรรทัดใหม่", + "composerSendKey_shiftEnter": "Shift+Enter เพื่อส่ง · Enter เพื่อขึ้นบรรทัดใหม่", "cursorSpotlight": "เอฟเฟกต์แบบโต้ตอบ", "cursorSpotlightDesc": "แสดงสปอตไลต์แบบนุ่มนวลตามเคอร์เซอร์บนแถบชื่อเรื่องและแถบด้านข้าง", "cursorSpotlightColor": "สีเอฟเฟกต์การโต้ตอบ", diff --git a/src/renderer/src/locales/zh/common.json b/src/renderer/src/locales/zh/common.json index cc9cd7eea..4879a7c1d 100644 --- a/src/renderer/src/locales/zh/common.json +++ b/src/renderer/src/locales/zh/common.json @@ -1536,6 +1536,7 @@ "autoLabel": "自动", "composerHint": "Enter 发送 · Shift+Enter 换行 · AI 生成内容请自行核验", "composerShortcut": "Enter 发送 · Shift+Enter 换行", + "composerShortcutShiftEnter": "Shift+Enter 发送 · Enter 换行", "newline": "换行", "composerVerifyHint": "AI 生成内容请自行核验", "composerStartHint": "描述需求、贴入代码,或直接让智能体处理当前工作区。", diff --git a/src/renderer/src/locales/zh/settings.json b/src/renderer/src/locales/zh/settings.json index 73e87bf7f..0115a5219 100644 --- a/src/renderer/src/locales/zh/settings.json +++ b/src/renderer/src/locales/zh/settings.json @@ -210,6 +210,10 @@ "chatContentMaxWidthWide": "宽", "chatContentMaxWidthDecrease": "减小对话文字宽度", "chatContentMaxWidthIncrease": "增大对话文字宽度", + "composerSendKey": "发送快捷键", + "composerSendKeyDesc": "选择用 Enter 还是 Shift+Enter 发送消息;另一个键用于换行。", + "composerSendKey_enter": "Enter 发送 · Shift+Enter 换行", + "composerSendKey_shiftEnter": "Shift+Enter 发送 · Enter 换行", "cursorSpotlight": "交互特效", "cursorSpotlightDesc": "在置顶栏和侧边栏显示跟随鼠标的柔和高光。", "cursorSpotlightColor": "交互特效颜色", diff --git a/src/renderer/src/store/chat-store-navigation-actions.test.ts b/src/renderer/src/store/chat-store-navigation-actions.test.ts index 84c1c0bae..fc3aaac46 100644 --- a/src/renderer/src/store/chat-store-navigation-actions.test.ts +++ b/src/renderer/src/store/chat-store-navigation-actions.test.ts @@ -253,6 +253,7 @@ describe('chat-store navigation workspace selection', () => { theme: 'dark', uiFontScale: 1, chatContentMaxWidthPx: 896, + composerSendKey: 'enter', locale: 'en', agents: { kun: { apiKey: 'test-key', model: 'deepseek-v4-pro', baseUrl: '' } }, disabledSkillIds: [] @@ -592,7 +593,8 @@ describe('onClawChannelActivity routes through subscribeThreadEventsLive (not se }, theme: 'dark', uiFontScale: 1, - chatContentMaxWidthPx: 896, + chatContentMaxWidthPx: 896, + composerSendKey: 'enter', locale: 'en', agents: { kun: { apiKey: 'test-key', model: 'deepseek-v4-pro', baseUrl: '' } }, disabledSkillIds: [] diff --git a/src/renderer/src/store/chat-store-thread-actions.test.ts b/src/renderer/src/store/chat-store-thread-actions.test.ts index 553ae7f72..9153b706e 100644 --- a/src/renderer/src/store/chat-store-thread-actions.test.ts +++ b/src/renderer/src/store/chat-store-thread-actions.test.ts @@ -1404,7 +1404,8 @@ describe('chat-store-thread-actions createThread conversation mode', () => { locale: 'en', theme: 'system', uiFontScale: 0.82, - chatContentMaxWidthPx: 896, + chatContentMaxWidthPx: 896, + composerSendKey: 'enter', provider: { providers: [], apiKey: '', baseUrl: '', proxy: { enabled: false } }, agents: { kun: { model: 'deepseek-v4-pro', apiKey: 'k', baseUrl: '' } }, workspaceRoot: '/tmp/workspace', diff --git a/src/shared/app-settings-domain.ts b/src/shared/app-settings-domain.ts index daa503ab8..8ff49e6fb 100644 --- a/src/shared/app-settings-domain.ts +++ b/src/shared/app-settings-domain.ts @@ -7,7 +7,7 @@ export type SettingsFieldOwner = /** Compile-time complete inventory of every persisted top-level settings field. */ export const APP_SETTINGS_FIELD_OWNERS: { readonly [K in keyof AppSettingsV1]-?: SettingsFieldOwner } = { version: 'core', initialSetupCompleted: 'core', locale: 'core', theme: 'core', uiFontScale: 'core', chatContentMaxWidthPx: 'core', - cursorSpotlight: 'core', cursorSpotlightColor: 'core', provider: 'provider', agents: 'kun', + composerSendKey: 'core', cursorSpotlight: 'core', cursorSpotlightColor: 'core', provider: 'provider', agents: 'kun', workspaceRoot: 'core', conversationWorkspaceRoot: 'core', log: 'core', checkpointCleanup: 'core', gitBranchPrefix: 'core', notifications: 'core', appBehavior: 'core', keyboardShortcuts: 'keyboard', write: 'write', claw: 'claw', schedule: 'schedule', workflow: 'workflow', design: 'design', diff --git a/src/shared/app-settings-normalize.ts b/src/shared/app-settings-normalize.ts index e01903e02..5561976e9 100644 --- a/src/shared/app-settings-normalize.ts +++ b/src/shared/app-settings-normalize.ts @@ -9,6 +9,7 @@ import { DEFAULT_LOG_RETENTION_DAYS, normalizeGuiUpdateChannel, normalizeChatContentMaxWidth, + normalizeComposerSendKey, normalizeUiFontScale, type AppBehaviorConfigV1, type AppSettingsV1, @@ -108,6 +109,7 @@ export function normalizeAppSettings(settings: AppSettingsV1): AppSettingsV1 { : 'system', uiFontScale: normalizeUiFontScale(maybeSettings.uiFontScale), chatContentMaxWidthPx: normalizeChatContentMaxWidth(maybeSettings.chatContentMaxWidthPx), + composerSendKey: normalizeComposerSendKey(maybeSettings.composerSendKey), cursorSpotlight: maybeSettings.cursorSpotlight !== false, cursorSpotlightColor: normalizeCursorSpotlightColor(maybeSettings.cursorSpotlightColor), provider: providerSettings, diff --git a/src/shared/app-settings-provider.test.ts b/src/shared/app-settings-provider.test.ts index df3314599..6522e306a 100644 --- a/src/shared/app-settings-provider.test.ts +++ b/src/shared/app-settings-provider.test.ts @@ -399,6 +399,7 @@ function settings(): AppSettingsV1 { theme: 'system', uiFontScale: 0.82, chatContentMaxWidthPx: 896, + composerSendKey: 'enter', provider: { ...defaultModelProviderSettings(), providers: [ diff --git a/src/shared/app-settings-types.ts b/src/shared/app-settings-types.ts index f01633c5e..33b6ae5c3 100644 --- a/src/shared/app-settings-types.ts +++ b/src/shared/app-settings-types.ts @@ -70,6 +70,20 @@ export function normalizeChatContentMaxWidth(value: unknown): ChatContentMaxWidt Math.max(CHAT_CONTENT_MAX_WIDTH_MIN, Math.round(num / 8) * 8) ) } +/** Which key combination sends from the chat composer. The other inserts a newline. */ +export const COMPOSER_SEND_KEYS = ['enter', 'shiftEnter'] as const +export type ComposerSendKey = (typeof COMPOSER_SEND_KEYS)[number] +export const DEFAULT_COMPOSER_SEND_KEY: ComposerSendKey = 'enter' +export function normalizeComposerSendKey(value: unknown): ComposerSendKey { + return value === 'shiftEnter' ? 'shiftEnter' : DEFAULT_COMPOSER_SEND_KEY +} +export function isComposerSendHotkey( + event: { key: string; shiftKey: boolean; metaKey: boolean; ctrlKey: boolean }, + sendKey: ComposerSendKey = DEFAULT_COMPOSER_SEND_KEY +): boolean { + if (event.key !== 'Enter' || event.metaKey || event.ctrlKey) return false + return sendKey === 'shiftEnter' ? event.shiftKey : !event.shiftKey +} export type ScheduleRunMode = 'agent' | 'plan' export type ScheduleKind = 'manual' | 'interval' | 'daily' | 'at' export type ScheduleTaskStatus = 'idle' | 'queued' | 'running' | 'success' | 'error' @@ -2085,6 +2099,8 @@ export type AppSettingsV1 = { theme: 'system' | 'light' | 'dark' uiFontScale: UiFontScale chatContentMaxWidthPx: ChatContentMaxWidthPx + /** Enter sends (default) or Shift+Enter sends; the other key inserts a newline. */ + composerSendKey: ComposerSendKey cursorSpotlight?: boolean cursorSpotlightColor?: string provider: ModelProviderSettingsV1 diff --git a/src/shared/app-settings.test.ts b/src/shared/app-settings.test.ts index 8ba3b1c0a..3dec33fa2 100644 --- a/src/shared/app-settings.test.ts +++ b/src/shared/app-settings.test.ts @@ -38,6 +38,8 @@ import { migrateLegacyAppSettings, normalizeAppSettings, normalizeChatContentMaxWidth, + normalizeComposerSendKey, + isComposerSendHotkey, normalizeGitBranchPrefix, applyGitBranchPrefix, parseClawUserPromptForDisplay, @@ -61,6 +63,7 @@ function settings(): AppSettingsV1 { theme: 'system', uiFontScale: 0.82, chatContentMaxWidthPx: 896, + composerSendKey: 'enter', provider: defaultModelProviderSettings(), agents: { kun: defaultKunRuntimeSettings() @@ -467,6 +470,42 @@ describe('runtime model provider selection', () => { }) }) +describe('composer send key settings', () => { + it('defaults to enter send', () => { + const raw = { + ...settings(), + composerSendKey: undefined + } as unknown as AppSettingsV1 + + expect(normalizeAppSettings(raw).composerSendKey).toBe('enter') + }) + + it('keeps shiftEnter when configured', () => { + expect(normalizeAppSettings({ + ...settings(), + composerSendKey: 'shiftEnter' + }).composerSendKey).toBe('shiftEnter') + }) + + it('rejects unknown values', () => { + expect(normalizeComposerSendKey('ctrlEnter')).toBe('enter') + expect(normalizeComposerSendKey(null)).toBe('enter') + }) + + it('matches Enter or Shift+Enter send hotkeys', () => { + const enter = { key: 'Enter', shiftKey: false, metaKey: false, ctrlKey: false } + const shiftEnter = { key: 'Enter', shiftKey: true, metaKey: false, ctrlKey: false } + const metaEnter = { key: 'Enter', shiftKey: false, metaKey: true, ctrlKey: false } + + expect(isComposerSendHotkey(enter, 'enter')).toBe(true) + expect(isComposerSendHotkey(shiftEnter, 'enter')).toBe(false) + expect(isComposerSendHotkey(enter, 'shiftEnter')).toBe(false) + expect(isComposerSendHotkey(shiftEnter, 'shiftEnter')).toBe(true) + expect(isComposerSendHotkey(metaEnter, 'enter')).toBe(false) + expect(isComposerSendHotkey(metaEnter, 'shiftEnter')).toBe(false) + }) +}) + describe('app behavior settings', () => { it('defaults desktop behavior to off', () => { const raw = {