mirror of
https://github.com/stablyai/orca.git
synced 2026-09-21 16:02:20 +00:00
* fix(mobile): route external mouse click and drag to the terminal The terminal WebView suppresses mousedown/click at capture so xterm's own mouse handling stays inert (its onData bytes are dropped by the mobile bridge). That left hardware mouse clicks and drags with no path at all: touch taps reached mouse-aware TUIs and drove selection, while a Bluetooth mouse or trackpad click did nothing (#8818; wheel half landed in #11247). Add a pointer-event router on the terminal surface (pointerType 'mouse', left button only) that mirrors touch semantics: - plain click: same pipeline as a touch tap (links/file paths first, then tracking-mode press+release reports, else keyboard focus), and a click on an active selection dismisses it like touch does - drag with mouse tracking: press at the anchor, per-cell motion reports (drag/any modes), release on pointerup or pointercancel - drag without tracking: character-anchored selection reusing the touch handle-drag plumbing (edge scroll, handles, copy pill) Widen the RN gesture-input grammar to pass left-drag motion reports (SGR button 32, default-encoding byte 64) through the existing validation and rate limiting. Mock server: echo the subscribe viewport and serialize scrollback so the session screen leaves the resubscribe loop, serve the session-tabs subscribe stream, and add a MOCK_TUI=1 mouse-tracking scenario plus a [SEND] byte log - the rig used to reproduce and verify this fix on an Android emulator. Fixes #8818 * fix(mobile): capture the mouse pointer and clear stale gestures on pointerdown A drag leaving the terminal surface dropped pointermove/pointerup without pointer capture, stranding the gesture; a pointerup lost outside the WebView could leave a tracked press latched until the next gesture. * fix(mobile): end mouse gestures whose pointerup never reached the surface Capture the mouse pointer on pointerdown so a drag that leaves the surface keeps delivering pointermove/pointerup; when capture is unavailable and the release is lost anyway, synthesize the release from the next buttons==0 pointermove or the next pointerdown, so a tracking TUI is never left with the left button latched down. * fix(mock-server): clear the terminal stream interval on resubscribe and unsubscribe * fix(mobile): synthesize the lost-pointerup release at the pointer's current cell * test(mobile): split terminal mouse click and drag coverage * test(mobile): satisfy changed-line quality checks * fix(mobile): cancel stale mock terminal callbacks * refactor(mobile): extract mouse report cell mapping
84 lines
3.3 KiB
TypeScript
84 lines
3.3 KiB
TypeScript
import { randomUUID } from 'node:crypto'
|
|
import type { RuntimeMobileSessionTabsResult } from '../../src/shared/runtime-types'
|
|
import type { RpcRequest, RpcResponse } from './mock-server-rpc-handlers'
|
|
|
|
// Why: the client's snapshot-acceptance gate keys on the publisher epoch, so it
|
|
// must stay stable for the process and change on restart like a real publisher —
|
|
// hence a uuid, not a clock read two restarts could land on.
|
|
// The `mobile-local:` prefix is reserved for phone-local writes — never use it.
|
|
const PUBLICATION_EPOCH = `mock-server:${randomUUID()}`
|
|
const GROUP_ID = 'group-1'
|
|
const PARENT_TAB_ID = 'tab-1'
|
|
// The host only ever publishes terminal-layout UUIDs here; pane-key parsing
|
|
// rejects any other shape, so a placeholder would mask pane-attribution bugs.
|
|
const LEAF_ID = 'f47ac10b-58cc-4372-a567-0e02b2c3d479'
|
|
// The host publishes terminal surfaces as `${parentTabId}::${leafId}`.
|
|
const SURFACE_TAB_ID = `${PARENT_TAB_ID}::${LEAF_ID}`
|
|
|
|
/** One ready terminal tab bound to the `term-1` fixture. Mirrors the full
|
|
* `session.tabs.list` contract so mock-server repros of tab, split-pane, and
|
|
* pane-attribution bugs aren't shape-incomplete. */
|
|
function createMockSessionTabs(worktreeId: string): RuntimeMobileSessionTabsResult {
|
|
return {
|
|
worktree: worktreeId,
|
|
publicationEpoch: PUBLICATION_EPOCH,
|
|
snapshotVersion: 1,
|
|
activeGroupId: GROUP_ID,
|
|
activeTabId: SURFACE_TAB_ID,
|
|
activeTabType: 'terminal',
|
|
// Groups track top-level tabs, so they carry parentTabId, not surface ids.
|
|
tabGroups: [
|
|
{
|
|
id: GROUP_ID,
|
|
activeTabId: PARENT_TAB_ID,
|
|
tabOrder: [PARENT_TAB_ID],
|
|
recentTabIds: [PARENT_TAB_ID]
|
|
}
|
|
],
|
|
tabs: [
|
|
{
|
|
type: 'terminal',
|
|
id: SURFACE_TAB_ID,
|
|
title: 'zsh',
|
|
parentTabId: PARENT_TAB_ID,
|
|
leafId: LEAF_ID,
|
|
status: 'ready',
|
|
terminal: 'term-1',
|
|
isActive: true
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
/** Default session-tabs backend: without it the session screen hangs on
|
|
* 'Loading tabs'. Returns false for methods it does not own. */
|
|
export function handleMockSessionTabsRequest(
|
|
request: RpcRequest,
|
|
respond: (response: RpcResponse) => void,
|
|
success: (id: string, result: unknown, streaming?: boolean) => RpcResponse,
|
|
// Shared with `terminal.list` so both surfaces agree on which worktree an
|
|
// absent or `id:`-prefixed selector means.
|
|
resolveWorktreeId: (selector: unknown) => string | undefined
|
|
): boolean {
|
|
if (request.method === 'session.tabs.list') {
|
|
const worktreeId = resolveWorktreeId(request.params?.worktree) ?? 'mock'
|
|
respond(success(request.id, createMockSessionTabs(worktreeId)))
|
|
return true
|
|
}
|
|
if (request.method === 'session.tabs.subscribe') {
|
|
// Without a live stream the client's health loop keeps invalidating list
|
|
// fetches mid-flight (barrier bump on the failed probe), so the session
|
|
// screen never leaves 'Loading tabs'. snapshot then updated => 'live'.
|
|
const worktreeId = resolveWorktreeId(request.params?.worktree) ?? 'mock'
|
|
const snapshot = createMockSessionTabs(worktreeId)
|
|
respond(success(request.id, { type: 'snapshot', ...snapshot }, true))
|
|
respond(success(request.id, { type: 'updated', ...snapshot }, true))
|
|
return true
|
|
}
|
|
if (request.method === 'session.tabs.unsubscribe') {
|
|
respond(success(request.id, { unsubscribed: true }))
|
|
return true
|
|
}
|
|
return false
|
|
}
|