From 12041b4d55dcdf74d2be7ffbe96257c2776f03ad Mon Sep 17 00:00:00 2001 From: Mike Lyons Date: Mon, 25 May 2026 12:35:12 -0600 Subject: [PATCH] feat(json-chat): add 'send on Enter' setting; show active send hotkey on Send button + placeholder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds jsonModeSendOnEnter (default off) to the settings slice with the full 5-file wiring (slice + test, persistence, initial state, IPC, backend, types). When on, plain Enter sends in the JSON-mode chat composer and Shift+Enter inserts a newline; when off the historical Cmd/Ctrl+Enter-to-send behavior is preserved. The Send button and placeholder now reflect the active hotkey (⌘↵ vs ↵ on Mac, Ctrl+Enter vs Enter elsewhere) with a screen-reader-friendly aria-label. The keydown handler guards IME composition so Enter during CJK input never sends. The @-mention picker path is untouched — Enter still picks the suggestion while the popover is open. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/main/build-initial-state.ts | 1 + src/main/index.ts | 18 ++++++++++ src/main/persistence.ts | 4 +++ src/renderer/build-backend.ts | 2 ++ src/renderer/components/JsonModeChat.tsx | 46 ++++++++++++++++++++---- src/renderer/components/Settings.tsx | 23 ++++++++++++ src/renderer/types.ts | 1 + src/shared/state/settings.test.ts | 14 ++++++++ src/shared/state/settings.ts | 9 +++++ 9 files changed, 112 insertions(+), 6 deletions(-) diff --git a/src/main/build-initial-state.ts b/src/main/build-initial-state.ts index c1ce179f..fec9ca86 100644 --- a/src/main/build-initial-state.ts +++ b/src/main/build-initial-state.ts @@ -94,6 +94,7 @@ export function buildInitialAppState( autoApproveSteerInstructions: config.autoApproveSteerInstructions || '', useSystemClaudeForJsonMode: config.useSystemClaudeForJsonMode === true, jsonModeChatDensity: config.jsonModeChatDensity === 'comfy' ? 'comfy' : 'compact', + jsonModeSendOnEnter: config.jsonModeSendOnEnter === true, jsonModeDefaultPermissionMode: config.jsonModeDefaultPermissionMode === 'default' || config.jsonModeDefaultPermissionMode === 'plan' diff --git a/src/main/index.ts b/src/main/index.ts index fe1ce574..a7b14574 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -2698,6 +2698,24 @@ function registerIpcHandlers(): void { } ) + transport.onRequest( + 'config:setJsonModeSendOnEnter', + (_ctx, value: boolean) => { + const next = value === true + if (!next) { + delete config.jsonModeSendOnEnter + } else { + config.jsonModeSendOnEnter = true + } + saveConfig(config) + store.dispatch({ + type: 'settings/jsonModeSendOnEnterChanged', + payload: next + }) + return true + } + ) + transport.onRequest( 'config:setJsonModeDefaultPermissionMode', (_ctx, value: 'default' | 'acceptEdits' | 'plan') => { diff --git a/src/main/persistence.ts b/src/main/persistence.ts index cc47a2f3..efc5f7cf 100644 --- a/src/main/persistence.ts +++ b/src/main/persistence.ts @@ -197,6 +197,10 @@ export interface Config { // historical look). 'comfy' bumps font sizes, padding, and corner // radius for newcomers / screen-sharing. jsonModeChatDensity?: 'compact' | 'comfy' + // When true, plain Enter sends a message in the JSON-mode chat + // composer (Shift+Enter inserts a newline). Default off — preserves + // the historical Cmd/Ctrl+Enter-to-send behavior. + jsonModeSendOnEnter?: boolean // Permission mode applied when a brand-new json-mode session spawns. // Existing sessions keep whatever mode they were last in. Default // 'acceptEdits' (auto-allow Edit/Write, still ask for Bash etc.). diff --git a/src/renderer/build-backend.ts b/src/renderer/build-backend.ts index 34ada6b6..5e774c03 100644 --- a/src/renderer/build-backend.ts +++ b/src/renderer/build-backend.ts @@ -244,6 +244,8 @@ export function buildBackend( req('config:setDefaultClaudeTabType', value), setJsonModeChatDensity: (value: 'compact' | 'comfy') => req('config:setJsonModeChatDensity', value), + setJsonModeSendOnEnter: (enabled: boolean) => + req('config:setJsonModeSendOnEnter', enabled), setJsonModeDefaultPermissionMode: (value: 'default' | 'acceptEdits' | 'plan') => req('config:setJsonModeDefaultPermissionMode', value), setAutoSleepMinutes: (value: number) => req('config:setAutoSleepMinutes', value), diff --git a/src/renderer/components/JsonModeChat.tsx b/src/renderer/components/JsonModeChat.tsx index adfdcac5..7b09e082 100644 --- a/src/renderer/components/JsonModeChat.tsx +++ b/src/renderer/components/JsonModeChat.tsx @@ -856,7 +856,27 @@ export function JsonModeChat({ sessionId, worktreePath, mode = 'awake' }: JsonMo const backend = useBackend() const session = useJsonClaudeSession(sessionId) const { pending, resolve } = useJsonClaudeApprovals(sessionId) - const density = useSettings().jsonModeChatDensity + const settings = useSettings() + const density = settings.jsonModeChatDensity + const sendOnEnter = settings.jsonModeSendOnEnter + const isMac = + typeof window !== 'undefined' && + (window.__HARNESS_PLATFORM__ + ? window.__HARNESS_PLATFORM__ === 'darwin' + : /Mac|iPhone|iPad/.test(navigator.platform || '')) + const modKeySymbol = isMac ? '⌘' : 'Ctrl+' + const modKeyWord = isMac ? 'Cmd' : 'Ctrl' + const sendHotkeyLabel = sendOnEnter + ? isMac + ? '↵' + : 'Enter' + : isMac + ? `${modKeySymbol}↵` + : `${modKeySymbol}Enter` + const sendHotkeyAria = sendOnEnter ? 'Enter' : `${modKeyWord}+Enter` + const composerPlaceholder = sendOnEnter + ? 'Message Claude — Enter to send, Shift+Enter for newline' + : `Message Claude — ${modKeyWord}+Enter to send` const [draft, setDraft] = useState('') // Mention/popover state. `dismissed` carries the draft text at which // the user pressed Escape — comparing against the live draft is how we @@ -1775,15 +1795,26 @@ export function JsonModeChat({ sessionId, worktreePath, mode = 'awake' }: JsonMo return } } - if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { - e.preventDefault() - send() + if (e.key === 'Enter') { + // IME composition guard — don't send while composing + // CJK input. + if (e.nativeEvent.isComposing || e.keyCode === 229) return + const wantsSend = sendOnEnter + ? !e.shiftKey && !e.metaKey && !e.ctrlKey && !e.altKey + : e.metaKey || e.ctrlKey + if (wantsSend) { + e.preventDefault() + send() + return + } + // sendOnEnter && Shift+Enter → fall through so the + // textarea inserts a newline as usual. } }} placeholder={ mode === 'asleep' ? 'Type to wake this session…' - : 'Message Claude — Cmd/Ctrl+Enter to send' + : composerPlaceholder } // text-base (16px) below sm: prevents iOS Safari from zooming // the viewport when the textarea takes focus. text-sm on @@ -1825,9 +1856,12 @@ export function JsonModeChat({ sessionId, worktreePath, mode = 'awake' }: JsonMo diff --git a/src/renderer/components/Settings.tsx b/src/renderer/components/Settings.tsx index 2e71ec01..b1fb0519 100644 --- a/src/renderer/components/Settings.tsx +++ b/src/renderer/components/Settings.tsx @@ -200,6 +200,7 @@ export function Settings({ onClose, onOpenGuide, onOpenMyWeek, initialSection }: jsonModeClaudeTabs, defaultClaudeTabType, jsonModeChatDensity, + jsonModeSendOnEnter, jsonModeDefaultPermissionMode, autoSleepMinutes, autoApprovePermissions, @@ -2554,6 +2555,28 @@ export function Settings({ onClose, onOpenGuide, onOpenMyWeek, initialSection }: + +
+ +
{/* Auto-approve safe tool calls sub-card */} diff --git a/src/renderer/types.ts b/src/renderer/types.ts index 71aeecdc..14cdd85d 100644 --- a/src/renderer/types.ts +++ b/src/renderer/types.ts @@ -302,6 +302,7 @@ export interface ElectronAPI { setJsonModeClaudeTabs(enabled: boolean): Promise setDefaultClaudeTabType(value: 'xterm' | 'json'): Promise setJsonModeChatDensity(value: 'compact' | 'comfy'): Promise + setJsonModeSendOnEnter(enabled: boolean): Promise setJsonModeDefaultPermissionMode( value: 'default' | 'acceptEdits' | 'plan' ): Promise diff --git a/src/shared/state/settings.test.ts b/src/shared/state/settings.test.ts index 3ca67b36..5fac831d 100644 --- a/src/shared/state/settings.test.ts +++ b/src/shared/state/settings.test.ts @@ -427,6 +427,20 @@ describe('settingsReducer', () => { expect(compact.jsonModeChatDensity).toBe('compact') }) + it('jsonModeSendOnEnterChanged toggles the send-on-enter flag', () => { + expect(initialSettings.jsonModeSendOnEnter).toBe(false) + const on = apply(initialSettings, { + type: 'settings/jsonModeSendOnEnterChanged', + payload: true + }) + expect(on.jsonModeSendOnEnter).toBe(true) + const off = apply(on, { + type: 'settings/jsonModeSendOnEnterChanged', + payload: false + }) + expect(off.jsonModeSendOnEnter).toBe(false) + }) + it('jsonModeDefaultPermissionModeChanged sets the default and preserves other settings', () => { expect(initialSettings.jsonModeDefaultPermissionMode).toBe('acceptEdits') const start: SettingsState = { diff --git a/src/shared/state/settings.ts b/src/shared/state/settings.ts index 4747787f..ade7c2ea 100644 --- a/src/shared/state/settings.ts +++ b/src/shared/state/settings.ts @@ -128,6 +128,11 @@ export interface SettingsState { * radius for newcomers / screen-sharing. Wired via CSS variables on * the chat root, so it's a pure styling switch. */ jsonModeChatDensity: JsonModeChatDensity + /** When true, plain Enter sends a message in the JSON-mode chat + * composer (Shift+Enter inserts a newline). When false (default), + * the historical behavior applies: Cmd/Ctrl+Enter sends and plain + * Enter inserts a newline. */ + jsonModeSendOnEnter: boolean /** Permission mode applied to a freshly-spawned json-mode session. * Existing sessions keep whatever mode they were in (set via the * statusline picker). Default 'acceptEdits' so first-time users @@ -205,6 +210,7 @@ export type SettingsEvent = | { type: 'settings/autoApproveSteerInstructionsChanged'; payload: string } | { type: 'settings/useSystemClaudeForJsonModeChanged'; payload: boolean } | { type: 'settings/jsonModeChatDensityChanged'; payload: JsonModeChatDensity } + | { type: 'settings/jsonModeSendOnEnterChanged'; payload: boolean } | { type: 'settings/jsonModeDefaultPermissionModeChanged' payload: JsonClaudePermissionMode @@ -260,6 +266,7 @@ export const initialSettings: SettingsState = { autoApproveSteerInstructions: '', useSystemClaudeForJsonMode: false, jsonModeChatDensity: 'compact', + jsonModeSendOnEnter: false, jsonModeDefaultPermissionMode: 'acceptEdits', autoSleepMinutes: 30, snoozeDefaultDays: 7, @@ -353,6 +360,8 @@ export function settingsReducer(state: SettingsState, event: SettingsEvent): Set return { ...state, useSystemClaudeForJsonMode: event.payload } case 'settings/jsonModeChatDensityChanged': return { ...state, jsonModeChatDensity: event.payload } + case 'settings/jsonModeSendOnEnterChanged': + return { ...state, jsonModeSendOnEnter: event.payload } case 'settings/jsonModeDefaultPermissionModeChanged': return { ...state, jsonModeDefaultPermissionMode: event.payload } case 'settings/autoSleepMinutesChanged':