mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 00:03:45 +00:00
fix(mobile): focus terminal input after TUI touch
This commit is contained in:
@@ -179,9 +179,8 @@ export const TERMINAL_MOUSE_CLICK_DRAG_JS = `
|
||||
// Why: a dismissing tap only clears the selection (touch parity); it must
|
||||
// not also open a link or focus the keyboard underneath.
|
||||
if (gesture.dismissedSelection) return;
|
||||
// Plain click == touch tap: links and file paths win, then tracking-mode
|
||||
// click reports, then keyboard focus.
|
||||
notifyTerminalSurfaceTap(e.clientX, e.clientY);
|
||||
// Pointer clicks keep their current link, file, TUI mouse, and focus priority.
|
||||
notifyTerminalSurfaceTap(e.clientX, e.clientY, false);
|
||||
}, true);
|
||||
|
||||
targetSurface.addEventListener('pointercancel', function(e) {
|
||||
|
||||
@@ -18,6 +18,7 @@ describe('terminal WebView external mouse click', () => {
|
||||
expect(bytes.slice(0, 4)).toBe(`${ESC}[M `)
|
||||
expect(bytes.slice(6, 10)).toBe(`${ESC}[M#`)
|
||||
expect(bytes.slice(4, 6)).toBe(bytes.slice(10, 12))
|
||||
expect(mouse.postedMessages().filter((message) => message.type === 'terminal-tap')).toEqual([])
|
||||
})
|
||||
|
||||
it('dismisses an existing selection with a click without focusing the keyboard', () => {
|
||||
|
||||
@@ -212,10 +212,12 @@ describe('TerminalWebView scroll routing', () => {
|
||||
"document.addEventListener('touchend'",
|
||||
'}, { capture: true, passive: true });'
|
||||
)
|
||||
expect(touchEndBlock).toContain('notifyTerminalSurfaceTap(tapCandidate.x, tapCandidate.y)')
|
||||
expect(touchEndBlock).toContain(
|
||||
'notifyTerminalSurfaceTap(tapCandidate.x, tapCandidate.y, true)'
|
||||
)
|
||||
|
||||
const tapHandlerBlock = sliceBetween(
|
||||
'function notifyTerminalSurfaceTap(originX, originY)',
|
||||
'function notifyTerminalSurfaceTap(originX, originY, focusKeyboard)',
|
||||
"document.addEventListener('touchstart'"
|
||||
)
|
||||
expect(tapHandlerBlock.indexOf('oscLinkAtViewportPoint')).toBeLessThan(
|
||||
@@ -229,6 +231,9 @@ describe('TerminalWebView scroll routing', () => {
|
||||
)
|
||||
expect(tapHandlerBlock).toContain("notify({ type: 'open-url', url: tappedUrl });")
|
||||
expect(tapHandlerBlock).toContain("notify({ type: 'terminal-input', bytes: clickInput });")
|
||||
expect(tapHandlerBlock).toContain(
|
||||
'if (focusKeyboard || !isClickMouseTrackingMode(getMouseTrackingMode()))'
|
||||
)
|
||||
expect(tapHandlerBlock).toContain("notify({ type: 'terminal-tap' });")
|
||||
})
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@ export const TERMINAL_TAP_DISPATCH_JS = `
|
||||
selMode !== 'select' &&
|
||||
Date.now() - tapCandidate.t <= TAP_MAX_MS
|
||||
) {
|
||||
notifyTerminalSurfaceTap(tapCandidate.x, tapCandidate.y);
|
||||
notifyTerminalSurfaceTap(tapCandidate.x, tapCandidate.y, true);
|
||||
}
|
||||
clearLongPress();
|
||||
tapCandidate = null;
|
||||
|
||||
@@ -19,12 +19,12 @@ function bodyMarkup(): string {
|
||||
}
|
||||
|
||||
// Minimal xterm stub: one scrollback line containing a URL, fixed 8x15 cells.
|
||||
function makeTerminal(lineRef: { current: string }) {
|
||||
function makeTerminal(lineRef: { current: string }, mouseTrackingMode = 'none') {
|
||||
return {
|
||||
cols: 80,
|
||||
rows: 24,
|
||||
options: { fontSize: 13 },
|
||||
modes: {},
|
||||
modes: { mouseTrackingMode },
|
||||
element: { scrollWidth: 800, scrollHeight: 360 },
|
||||
_core: { _renderService: { dimensions: { css: { cell: { width: 8, height: 15 } } } } },
|
||||
buffer: {
|
||||
@@ -76,13 +76,14 @@ type OscLinkRange = { row: number; startCol: number; endCol: number; uri: string
|
||||
|
||||
function boot(
|
||||
line: string,
|
||||
oscLinks?: OscLinkRange[]
|
||||
oscLinks?: OscLinkRange[],
|
||||
mouseTrackingMode = 'none'
|
||||
): { posted: Posted; setLine: (line: string) => void } {
|
||||
const posted: Posted = []
|
||||
const lineRef = { current: line }
|
||||
const w = window as unknown as { Terminal: unknown; ReactNativeWebView: unknown }
|
||||
w.Terminal = function () {
|
||||
return makeTerminal(lineRef)
|
||||
return makeTerminal(lineRef, mouseTrackingMode)
|
||||
}
|
||||
w.ReactNativeWebView = {
|
||||
postMessage(s: string) {
|
||||
@@ -150,6 +151,17 @@ describe('terminal WebView tap routing', () => {
|
||||
expect(posted.find((m) => m.type === 'open-url')?.url).toBe('https://example.com/foo')
|
||||
})
|
||||
|
||||
it('focuses native input after reporting a touch tap to a mouse-tracking TUI', async () => {
|
||||
const { posted } = boot('interactive prompt', undefined, 'drag')
|
||||
await settle()
|
||||
|
||||
fireTouch('touchstart', [{ x: 20, y: tapY }])
|
||||
fireTouch('touchend', [])
|
||||
|
||||
expect(posted.find((message) => message.type === 'terminal-input')).toBeDefined()
|
||||
expect(posted.filter((message) => message.type === 'terminal-tap')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('opens the URL even right after a width-change reflow', async () => {
|
||||
// Why: scrollback reflow rewraps the local buffer; the tap path must keep
|
||||
// working afterward (the reflow message must not disturb tap routing).
|
||||
|
||||
@@ -205,7 +205,7 @@ export const URL_TAP_WEBVIEW_JS = `
|
||||
} catch (e) { return 0; }
|
||||
}
|
||||
|
||||
function notifyTerminalSurfaceTap(originX, originY) {
|
||||
function notifyTerminalSurfaceTap(originX, originY, focusKeyboard) {
|
||||
var tappedOscLink = oscLinkAtViewportPoint(originX, originY);
|
||||
if (tappedOscLink && tappedOscLink.kind === 'file') {
|
||||
notify({
|
||||
@@ -245,7 +245,9 @@ export const URL_TAP_WEBVIEW_JS = `
|
||||
var clickInput = buildMouseClickInput(originX, originY);
|
||||
if (clickInput) {
|
||||
notify({ type: 'terminal-input', bytes: clickInput });
|
||||
} else if (!isClickMouseTrackingMode(getMouseTrackingMode())) {
|
||||
}
|
||||
// Touch still needs native input focus after the TUI consumes its mouse click.
|
||||
if (focusKeyboard || !isClickMouseTrackingMode(getMouseTrackingMode())) {
|
||||
notify({ type: 'terminal-tap' });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user