mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 00:03:52 +00:00
fix(terminal): stop the macOS IME forwarder from running on iPadOS
Korean typed into a terminal from an iPad web client arrives as loose jamo instead of composed syllables. The native-text forwarder is a macOS workaround: it claims a printable keydown and delivers the input method's substituted text from the input event alone. It stands aside for IME composition by checking isComposing and compositionstart. Touch iOS/iPadOS is unreliable about firing those for hardware-keyboard CJK input, so each jamo keydown is claimed as its own one-shot substitution rather than deferred to xterm's composition handling. It runs there at all because every iOS user agent contains "Mac" - "Macintosh" in iPad desktop mode, the default since iPadOS 13, and "like Mac OS X" in mobile mode. maxTouchPoints is the only signal a real Mac never sets. The Linux branch immediately below already makes the mirror-image exclusion for Android and CrOS; the Mac branch never got the same treatment. Gate the forwarder install only. isMac keeps its other five consumers - the Ctrl+C interrupt, clipboard bypass, JIS yen input and the standalone 229 keydown policy - which intentionally still treat an iPad with a hardware keyboard like macOS, matching iPadOS shortcut conventions. Without the forwarder, xterm's own composition path plus the deferred textarea diff is the sole delivery mechanism, which is already the arrangement on Linux. Not verified on hardware: the claim that composition events are absent on iPadOS is the mechanism the code supports and the only one matching the reported symptom, but no on-device capture confirms it. The platform detection stands on its own regardless. terminal-ime-input-context-refresh.ts has the same user-agent collision for its NSTextInputContext refresh. Narrower trigger surface, left for a follow-up. Refs #13345
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
clearQueuedInitialCwdAfterFirstPane,
|
||||
getPreviousVisibleForTerminalPane,
|
||||
isTerminalPaneVisibilityResume,
|
||||
isTouchIOSUserAgent,
|
||||
mapRestoredPaneTitlesByPaneId,
|
||||
resolvePaneLinkCwd,
|
||||
resolvePaneSeedCwd,
|
||||
@@ -549,3 +550,33 @@ describe('terminal pane visibility resume tracking', () => {
|
||||
expect(isTerminalPaneVisibilityResume({ previousIsVisible: false, isVisible: true })).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isTouchIOSUserAgent', () => {
|
||||
it('is false for a real Mac (Macintosh UA, no touch points)', () => {
|
||||
expect(
|
||||
isTouchIOSUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15', 0)
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('is true for iPadOS desktop-mode Safari (Macintosh UA plus touch points)', () => {
|
||||
expect(
|
||||
isTouchIOSUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15', 5)
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('is true for iPhone Safari ("like Mac OS X" UA plus touch points)', () => {
|
||||
expect(
|
||||
isTouchIOSUserAgent(
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15',
|
||||
5
|
||||
)
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('is false for non-Mac UAs regardless of touch points', () => {
|
||||
expect(isTouchIOSUserAgent('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36', 5)).toBe(false)
|
||||
expect(
|
||||
isTouchIOSUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 5)
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -451,6 +451,11 @@ export function resolvePaneSeedCwd(splitPaneCwd: string | undefined, fallbackCwd
|
||||
return splitPaneCwd ?? fallbackCwd
|
||||
}
|
||||
|
||||
/** iPadOS reports a desktop "Macintosh" UA and iOS reports "like Mac OS X" — maxTouchPoints is the only signal a real Mac never sets. */
|
||||
export function isTouchIOSUserAgent(userAgent: string, maxTouchPoints: number): boolean {
|
||||
return userAgent.includes('Mac') && maxTouchPoints > 0
|
||||
}
|
||||
|
||||
type SplitStartupPayload = { command: string; env?: Record<string, string> }
|
||||
|
||||
type SplitWithStartupDeps = {
|
||||
@@ -995,6 +1000,8 @@ export function useTerminalPaneLifecycle({
|
||||
!isMac &&
|
||||
navigator.userAgent.includes('Linux') &&
|
||||
!/Android|CrOS/.test(navigator.userAgent)
|
||||
// Why: gates the forwarder only — isMac stays as-is for the Ctrl+C, clipboard, JIS-yen and 229 policies.
|
||||
const isTouchIOS = isTouchIOSUserAgent(navigator.userAgent, navigator.maxTouchPoints)
|
||||
const linuxImeCandidateState = isLinux
|
||||
? installTerminalImeLinuxCandidateState(pane.terminal.element)
|
||||
: null
|
||||
@@ -1006,18 +1013,20 @@ export function useTerminalPaneLifecycle({
|
||||
}
|
||||
})
|
||||
// Why: macOS commits an input source's substituted text through the input event alone, so printable keydowns must not reach xterm's encoder.
|
||||
const imeNativeTextForwarder = isMac
|
||||
? installTerminalImeNativeTextForwarder({
|
||||
terminalElement: pane.terminal.element,
|
||||
isComposing: () => imeCompositionTracker.isActive(),
|
||||
sendInput: (data) => pane.terminal.input(data),
|
||||
getKittyKeyboardFlags: () =>
|
||||
paneKittyKeyboardModesRef.current.get(pane.id)?.flags ?? 0
|
||||
})
|
||||
: {
|
||||
claimKeyEvent: () => false,
|
||||
dispose: () => undefined
|
||||
}
|
||||
// Not on touch iOS/iPadOS: the forwarder stands aside for IME input via composition events, which iPad Hangul appears not to fire (#13345).
|
||||
const imeNativeTextForwarder =
|
||||
isMac && !isTouchIOS
|
||||
? installTerminalImeNativeTextForwarder({
|
||||
terminalElement: pane.terminal.element,
|
||||
isComposing: () => imeCompositionTracker.isActive(),
|
||||
sendInput: (data) => pane.terminal.input(data),
|
||||
getKittyKeyboardFlags: () =>
|
||||
paneKittyKeyboardModesRef.current.get(pane.id)?.flags ?? 0
|
||||
})
|
||||
: {
|
||||
claimKeyEvent: () => false,
|
||||
dispose: () => undefined
|
||||
}
|
||||
imeNativeTextForwarderDisposablesRef.current.set(pane.id, imeNativeTextForwarder)
|
||||
pane.terminal.attachCustomKeyEventHandler((e) => {
|
||||
const linuxCandidateClassification = linuxImeCandidateState?.classifyKeyboardEvent(e) ?? {
|
||||
|
||||
Reference in New Issue
Block a user