diff --git a/src/renderer/src/components/NewWorkspaceComposerModal.tsx b/src/renderer/src/components/NewWorkspaceComposerModal.tsx index ea9b4e3f681..30c5230b505 100644 --- a/src/renderer/src/components/NewWorkspaceComposerModal.tsx +++ b/src/renderer/src/components/NewWorkspaceComposerModal.tsx @@ -4,6 +4,7 @@ import { Dialog, DialogContent, DialogDescription, DialogTitle } from '@/compone import NewWorkspaceComposerCard from '@/components/NewWorkspaceComposerCard' import { useComposerState } from '@/hooks/useComposerState' import type { LinkedWorkItemSummary } from '@/lib/new-workspace' +import { shouldSuppressEnterSubmit } from '@/lib/new-workspace-enter-guard' type ComposerModalData = { prefilledName?: string @@ -102,7 +103,7 @@ function ComposerModalBody({ if (createDisabled) { return } - if (target instanceof HTMLTextAreaElement && event.shiftKey) { + if (shouldSuppressEnterSubmit(event, target instanceof HTMLTextAreaElement)) { return } event.preventDefault() diff --git a/src/renderer/src/components/NewWorkspacePage.tsx b/src/renderer/src/components/NewWorkspacePage.tsx index 5dd155f91aa..848b1b0d807 100644 --- a/src/renderer/src/components/NewWorkspacePage.tsx +++ b/src/renderer/src/components/NewWorkspacePage.tsx @@ -34,6 +34,7 @@ import { getLinkedWorkItemSuggestedName, getTaskPresetQuery } from '@/lib/new-wo import type { LinkedWorkItemSummary } from '@/lib/new-workspace' import { isGitRepoKind } from '../../../shared/repo-kind' import type { GitHubWorkItem, TaskViewPresetId } from '../../../shared/types' +import { shouldSuppressEnterSubmit } from '@/lib/new-workspace-enter-guard' type TaskSource = 'github' | 'linear' type TaskQueryPreset = { @@ -332,6 +333,15 @@ export default function NewWorkspacePage(): React.JSX.Element { const handleTaskSearchKeyDown = useCallback( (event: React.KeyboardEvent): void => { if (event.key === 'Enter') { + // React SyntheticEvent does not expose isComposing; use nativeEvent. + if ( + shouldSuppressEnterSubmit( + { isComposing: event.nativeEvent.isComposing, shiftKey: event.shiftKey }, + false + ) + ) { + return + } event.preventDefault() handleApplyTaskSearch() } diff --git a/src/renderer/src/lib/new-workspace-enter-guard.test.ts b/src/renderer/src/lib/new-workspace-enter-guard.test.ts new file mode 100644 index 00000000000..cfed351a043 --- /dev/null +++ b/src/renderer/src/lib/new-workspace-enter-guard.test.ts @@ -0,0 +1,33 @@ +import { describe, it, expect } from 'vitest' +import { shouldSuppressEnterSubmit } from './new-workspace-enter-guard' + +function makeEvent(overrides: Partial<{ isComposing: boolean; shiftKey: boolean }>): { + isComposing: boolean + shiftKey: boolean +} { + return { isComposing: false, shiftKey: false, ...overrides } +} + +describe('shouldSuppressEnterSubmit', () => { + it('returns false for a plain Enter with no composition', () => { + expect(shouldSuppressEnterSubmit(makeEvent({}), false)).toBe(false) + }) + + it('returns true when IME composition is active', () => { + expect(shouldSuppressEnterSubmit(makeEvent({ isComposing: true }), false)).toBe(true) + }) + + it('returns true for Shift+Enter inside a textarea', () => { + expect(shouldSuppressEnterSubmit(makeEvent({ shiftKey: true }), true)).toBe(true) + }) + + it('returns false for Shift+Enter inside a non-textarea element', () => { + expect(shouldSuppressEnterSubmit(makeEvent({ shiftKey: true }), false)).toBe(false) + }) + + it('returns true when both isComposing and shiftKey are true (textarea)', () => { + expect(shouldSuppressEnterSubmit(makeEvent({ isComposing: true, shiftKey: true }), true)).toBe( + true + ) + }) +}) diff --git a/src/renderer/src/lib/new-workspace-enter-guard.ts b/src/renderer/src/lib/new-workspace-enter-guard.ts new file mode 100644 index 00000000000..69523c01386 --- /dev/null +++ b/src/renderer/src/lib/new-workspace-enter-guard.ts @@ -0,0 +1,19 @@ +/** + * Returns true when an Enter keydown event should be suppressed for submit actions. + * + * Two cases must be blocked: + * 1. IME composition is active — Enter only confirms the conversion candidate. + * 2. Shift+Enter inside a textarea — intended as a newline, not a submit. + */ +export function shouldSuppressEnterSubmit( + event: { isComposing: boolean; shiftKey: boolean }, + isTextarea: boolean +): boolean { + if (event.isComposing) { + return true + } + if (isTextarea && event.shiftKey) { + return true + } + return false +}