diff --git a/apps/array/src/renderer/features/message-editor/tiptap/useTiptapEditor.ts b/apps/array/src/renderer/features/message-editor/tiptap/useTiptapEditor.ts index c57ba7566..0d34f07a6 100644 --- a/apps/array/src/renderer/features/message-editor/tiptap/useTiptapEditor.ts +++ b/apps/array/src/renderer/features/message-editor/tiptap/useTiptapEditor.ts @@ -1,4 +1,5 @@ import { toast } from "@renderer/utils/toast"; +import { useSettingsStore } from "@stores/settingsStore"; import { useEditor } from "@tiptap/react"; import { useCallback, useRef, useState } from "react"; import { usePromptHistoryStore } from "../stores/promptHistoryStore"; @@ -98,14 +99,24 @@ export function useTiptapEditor(options: UseTiptapEditorOptions) { editorProps: { attributes: { class: EDITOR_CLASS }, handleKeyDown: (view, event) => { - if (event.key === "Enter" && !event.shiftKey) { - if (!view.editable) return false; - const suggestionPopup = document.querySelector("[data-tippy-root]"); - if (suggestionPopup) return false; - event.preventDefault(); - historyActions.reset(); - submitRef.current(); - return true; + if (event.key === "Enter") { + const sendMessagesWith = + useSettingsStore.getState().sendMessagesWith; + const isCmdEnterMode = sendMessagesWith === "cmd+enter"; + const isSubmitKey = isCmdEnterMode + ? event.metaKey || event.ctrlKey + : !event.shiftKey; + + if (isSubmitKey) { + if (!view.editable) return false; + const suggestionPopup = + document.querySelector("[data-tippy-root]"); + if (suggestionPopup) return false; + event.preventDefault(); + historyActions.reset(); + submitRef.current(); + return true; + } } if ( diff --git a/apps/array/src/renderer/features/settings/components/SettingsView.tsx b/apps/array/src/renderer/features/settings/components/SettingsView.tsx index ccec761aa..7872eb07b 100644 --- a/apps/array/src/renderer/features/settings/components/SettingsView.tsx +++ b/apps/array/src/renderer/features/settings/components/SettingsView.tsx @@ -1,6 +1,9 @@ import { useAuthStore } from "@features/auth/stores/authStore"; import { FolderPicker } from "@features/folder-picker/components/FolderPicker"; -import { useSettingsStore } from "@features/settings/stores/settingsStore"; +import { + type SendMessagesWith, + useSettingsStore, +} from "@features/settings/stores/settingsStore"; import { useMeQuery } from "@hooks/useMeQuery"; import { useProjectQuery } from "@hooks/useProjectQuery"; import { useSetHeaderContent } from "@hooks/useSetHeaderContent"; @@ -56,10 +59,12 @@ export function SettingsView() { createPR, cursorGlow, desktopNotifications, + sendMessagesWith, setAutoRunTasks, setCreatePR, setCursorGlow, setDesktopNotifications, + setSendMessagesWith, } = useSettingsStore(); const terminalLayoutMode = useTerminalLayoutStore( (state) => state.terminalLayoutMode, @@ -160,6 +165,18 @@ export function SettingsView() { [terminalLayoutMode, setTerminalLayout], ); + const handleSendMessagesWithChange = useCallback( + (value: SendMessagesWith) => { + track(ANALYTICS_EVENTS.SETTING_CHANGED, { + setting_name: "send_messages_with", + new_value: value, + old_value: sendMessagesWith, + }); + setSendMessagesWith(value); + }, + [sendMessagesWith, setSendMessagesWith], + ); + const handleWorktreeLocationChange = async (newLocation: string) => { setLocalWorktreeLocation(newLocation); try { @@ -341,20 +358,48 @@ export function SettingsView() { Chat - - - - Desktop notifications - - - Show notifications when the agent finishes working on a task - + + + + + Desktop notifications + + + Show notifications when the agent finishes working on a + task + + + + + + + + + Send messages with + + + Choose which key combination sends messages. Use + Shift+Enter for new lines. + + + + handleSendMessagesWithChange(value as SendMessagesWith) + } + size="1" + > + + + Enter + ⌘ Enter + + - diff --git a/apps/array/src/renderer/features/settings/stores/settingsStore.ts b/apps/array/src/renderer/features/settings/stores/settingsStore.ts index 7051ae7ba..59f05807b 100644 --- a/apps/array/src/renderer/features/settings/stores/settingsStore.ts +++ b/apps/array/src/renderer/features/settings/stores/settingsStore.ts @@ -5,6 +5,7 @@ import { persist } from "zustand/middleware"; export type DefaultRunMode = "local" | "cloud" | "last_used"; export type LocalWorkspaceMode = "worktree" | "root"; +export type SendMessagesWith = "enter" | "cmd+enter"; interface SettingsStore { autoRunTasks: boolean; @@ -16,6 +17,7 @@ interface SettingsStore { defaultModel: string; desktopNotifications: boolean; cursorGlow: boolean; + sendMessagesWith: SendMessagesWith; setAutoRunTasks: (autoRun: boolean) => void; setDefaultRunMode: (mode: DefaultRunMode) => void; @@ -26,6 +28,7 @@ interface SettingsStore { setDefaultModel: (model: string) => void; setDesktopNotifications: (enabled: boolean) => void; setCursorGlow: (enabled: boolean) => void; + setSendMessagesWith: (mode: SendMessagesWith) => void; } export const useSettingsStore = create()( @@ -40,6 +43,7 @@ export const useSettingsStore = create()( defaultModel: DEFAULT_MODEL, desktopNotifications: true, cursorGlow: false, + sendMessagesWith: "enter", setAutoRunTasks: (autoRun) => set({ autoRunTasks: autoRun }), setDefaultRunMode: (mode) => set({ defaultRunMode: mode }), @@ -52,6 +56,7 @@ export const useSettingsStore = create()( setDesktopNotifications: (enabled) => set({ desktopNotifications: enabled }), setCursorGlow: (enabled) => set({ cursorGlow: enabled }), + setSendMessagesWith: (mode) => set({ sendMessagesWith: mode }), }), { name: "settings-storage", diff --git a/apps/array/src/renderer/stores/settingsStore.ts b/apps/array/src/renderer/stores/settingsStore.ts index 27340803c..a8b894f09 100644 --- a/apps/array/src/renderer/stores/settingsStore.ts +++ b/apps/array/src/renderer/stores/settingsStore.ts @@ -2,16 +2,21 @@ import { create } from "zustand"; import { trpcVanilla } from "../trpc"; export type TerminalLayoutMode = "split" | "tabbed"; +export type SendMessagesWith = "enter" | "cmd+enter"; interface SettingsState { terminalLayoutMode: TerminalLayoutMode; + sendMessagesWith: SendMessagesWith; isLoading: boolean; loadTerminalLayout: () => Promise; setTerminalLayout: (mode: TerminalLayoutMode) => Promise; + loadSendMessagesWith: () => Promise; + setSendMessagesWith: (mode: SendMessagesWith) => Promise; } export const useSettingsStore = create()((set) => ({ terminalLayoutMode: "split", + sendMessagesWith: "enter", isLoading: true, loadTerminalLayout: async () => { @@ -37,4 +42,27 @@ export const useSettingsStore = create()((set) => ({ set({ terminalLayoutMode: mode }); } catch (_error) {} }, + + loadSendMessagesWith: async () => { + try { + const mode = await trpcVanilla.secureStore.getItem.query({ + key: "sendMessagesWith", + }); + if (mode === "enter" || mode === "cmd+enter") { + set({ sendMessagesWith: mode }); + } + } catch (_error) { + // Keep default value + } + }, + + setSendMessagesWith: async (mode: SendMessagesWith) => { + try { + await trpcVanilla.secureStore.setItem.query({ + key: "sendMessagesWith", + value: mode, + }); + set({ sendMessagesWith: mode }); + } catch (_error) {} + }, }));