Skip to content
Closed
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
21 changes: 19 additions & 2 deletions apps/desktop/src/app/chat/composer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,13 @@ import {
RICH_INPUT_SLOT
} from './rich-editor'
import { SkinSlashPopover } from './skin-slash-popover'
import { detectTrigger, extractClipboardImageBlobs, textBeforeCaret, type TriggerState } from './text-utils'
import {
detectTrigger,
extractClipboardImageBlobs,
isComposingKeyboardEvent,
textBeforeCaret,
type TriggerState
} from './text-utils'
import { ComposerTriggerPopover } from './trigger-popover'
import type { ChatBarProps } from './types'
import { UrlDialog } from './url-dialog'
Expand Down Expand Up @@ -567,6 +573,13 @@ export function ChatBar({
}

const handleEditorKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
// IMEs (Japanese, Chinese, Korean, etc.) use Enter/arrow keys while text is
// still composing. Let the browser finish conversion instead of treating
// that key press as a composer submit or slash-menu navigation.
if (isComposingKeyboardEvent(event)) {
return
}

if ((event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && event.key.toLowerCase() === 'k') {
event.preventDefault()

Expand Down Expand Up @@ -628,7 +641,11 @@ export function ChatBar({
}
}

const handleEditorKeyUp = () => {
const handleEditorKeyUp = (event: KeyboardEvent<HTMLDivElement>) => {
if (isComposingKeyboardEvent(event)) {
return
}

// If this keyup belongs to a key the open trigger popover already consumed
// in keydown (Arrow/Enter/Tab/Escape), skip the refresh. Those keys never
// edit text, and for Escape the keydown already closed the menu — a refresh
Expand Down
19 changes: 18 additions & 1 deletion apps/desktop/src/app/chat/composer/text-utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'

import { detectTrigger } from './text-utils'
import { detectTrigger, isComposingKeyboardEvent } from './text-utils'

describe('detectTrigger', () => {
it('detects a bare slash trigger with an empty query', () => {
Expand All @@ -23,3 +23,20 @@ describe('detectTrigger', () => {
expect(detectTrigger('hello there')).toBeNull()
})
})

describe('isComposingKeyboardEvent', () => {
it('recognizes browser IME composition flags so Enter can confirm conversion', () => {
expect(isComposingKeyboardEvent({ isComposing: true, key: 'Enter', nativeEvent: {} })).toBe(true)
expect(isComposingKeyboardEvent({ key: 'Enter', nativeEvent: { isComposing: true } })).toBe(true)
})

it('recognizes keyCode 229 IME process events from Electron/browser quirks', () => {
expect(isComposingKeyboardEvent({ key: 'Enter', keyCode: 229, nativeEvent: {} })).toBe(true)
expect(isComposingKeyboardEvent({ key: 'Enter', nativeEvent: { keyCode: 229 } })).toBe(true)
expect(isComposingKeyboardEvent({ key: 'Process', nativeEvent: {} })).toBe(true)
})

it('does not treat normal Enter as composition', () => {
expect(isComposingKeyboardEvent({ key: 'Enter', keyCode: 13, nativeEvent: { isComposing: false } })).toBe(false)
})
})
27 changes: 27 additions & 0 deletions apps/desktop/src/app/chat/composer/text-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,33 @@ export interface TriggerState {
}

const TRIGGER_RE = /(?:^|[\s])([@/])([^\s@/]*)$/
const IME_PROCESS_KEY_CODE = 229

interface KeyboardEventLike {
isComposing?: boolean
key?: string
keyCode?: number
nativeEvent?: {
isComposing?: boolean
keyCode?: number
which?: number
}
which?: number
}

export function isComposingKeyboardEvent(event: KeyboardEventLike): boolean {
const native = event.nativeEvent

return Boolean(
event.isComposing ||
native?.isComposing ||
event.key === 'Process' ||
event.keyCode === IME_PROCESS_KEY_CODE ||
event.which === IME_PROCESS_KEY_CODE ||
native?.keyCode === IME_PROCESS_KEY_CODE ||
native?.which === IME_PROCESS_KEY_CODE
)
}

export function extractClipboardImageBlobs(clipboard: DataTransfer): Blob[] {
const blobs: Blob[] = []
Expand Down