Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/main/build-initial-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
18 changes: 18 additions & 0 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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') => {
Expand Down
4 changes: 4 additions & 0 deletions src/main/persistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,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.).
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/build-backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,8 @@ export function buildBackend(
req('config:setChatPromotionDismissed', 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),
Expand Down
48 changes: 42 additions & 6 deletions src/renderer/components/JsonModeChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -858,8 +858,30 @@ export function JsonModeChat({ sessionId, worktreePath, mode = 'awake' }: JsonMo
const backend = useBackend()
const session = useJsonClaudeSession(sessionId)
const { pending, resolve } = useJsonClaudeApprovals(sessionId)
const { jsonModeChatDensity: density, defaultClaudeTabType } = useSettings()
const {
jsonModeChatDensity: density,
jsonModeSendOnEnter: sendOnEnter,
defaultClaudeTabType
} = useSettings()
const cameFromTerminalDefault = defaultClaudeTabType === 'xterm'
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
Expand Down Expand Up @@ -1820,15 +1842,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
Expand Down Expand Up @@ -1870,9 +1903,12 @@ export function JsonModeChat({ sessionId, worktreePath, mode = 'awake' }: JsonMo
<button
onClick={() => send()}
disabled={!draft.trim() && attachments.length === 0}
aria-label={`Send (${sendHotkeyAria})`}
title={`Send (${sendHotkeyAria})`}
className="px-2.5 py-0.5 bg-accent text-white rounded text-xs font-medium disabled:opacity-40 cursor-pointer disabled:cursor-not-allowed hover:opacity-90 transition-opacity"
>
Send
<span>Send</span>
<span className="opacity-60 ml-1">{sendHotkeyLabel}</span>
</button>
</div>
</div>
Expand Down
23 changes: 23 additions & 0 deletions src/renderer/components/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ export function Settings({ onClose, onOpenGuide, onOpenMyWeek, initialSection }:
wsTransportHost,
defaultClaudeTabType,
jsonModeChatDensity,
jsonModeSendOnEnter,
jsonModeDefaultPermissionMode,
autoSleepMinutes,
autoApprovePermissions,
Expand Down Expand Up @@ -1546,6 +1547,28 @@ export function Settings({ onClose, onOpenGuide, onOpenMyWeek, initialSection }:
</button>
</div>
</div>

<div className="mt-4 pt-3 border-t border-border">
<label className="flex items-start gap-3 cursor-pointer">
<input
type="checkbox"
checked={jsonModeSendOnEnter}
onChange={(e) => {
void backend.setJsonModeSendOnEnter(e.target.checked)
}}
className="mt-0.5 cursor-pointer"
/>
<div className="flex-1">
<div className="text-sm text-fg-bright">
Send messages with Enter
</div>
<div className="text-xs text-dim mt-0.5">
Plain Enter sends; Shift+Enter inserts a newline. When
off, Cmd/Ctrl+Enter sends.
</div>
</div>
</label>
</div>
</div>

</div>
Expand Down
1 change: 1 addition & 0 deletions src/renderer/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ export interface ElectronAPI {
setDefaultClaudeTabType(value: 'xterm' | 'json'): Promise<boolean>
setChatPromotionDismissed(value: boolean): Promise<boolean>
setJsonModeChatDensity(value: 'compact' | 'comfy'): Promise<boolean>
setJsonModeSendOnEnter(enabled: boolean): Promise<boolean>
setJsonModeDefaultPermissionMode(
value: 'default' | 'acceptEdits' | 'plan'
): Promise<boolean>
Expand Down
14 changes: 14 additions & 0 deletions src/shared/state/settings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
9 changes: 9 additions & 0 deletions src/shared/state/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -260,6 +266,7 @@ export const initialSettings: SettingsState = {
autoApproveSteerInstructions: '',
useSystemClaudeForJsonMode: false,
jsonModeChatDensity: 'compact',
jsonModeSendOnEnter: false,
jsonModeDefaultPermissionMode: 'acceptEdits',
autoSleepMinutes: 30,
snoozeDefaultDays: 7,
Expand Down Expand Up @@ -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':
Expand Down
Loading