mirror of
https://github.com/stablyai/orca.git
synced 2026-10-24 08:03:58 +00:00
fix(mobile): keep accessory repeats on the pressed terminal
This commit is contained in:
@@ -117,7 +117,10 @@ import {
|
||||
} from '../../../../src/terminal/terminal-accessory-layout'
|
||||
import { createTerminalLiveAccessoryInput } from '../../../../src/terminal/terminal-live-accessory-input'
|
||||
import { sendTerminalLiveAccessoryRawBytes } from '../../../../src/terminal/terminal-live-accessory-raw-send'
|
||||
import { createTerminalAccessoryRepeatController } from '../../../../src/terminal/terminal-accessory-repeat'
|
||||
import {
|
||||
createTerminalAccessoryRepeatController,
|
||||
createTerminalAccessoryRepeatSender
|
||||
} from '../../../../src/terminal/terminal-accessory-repeat'
|
||||
import {
|
||||
clearTerminalLiveInputFocusTimer,
|
||||
isTerminalLiveInputWithinByteLimit,
|
||||
@@ -2994,11 +2997,13 @@ export default function SessionScreen() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAccessoryKey(input: ReturnType<typeof createTerminalLiveAccessoryInput>) {
|
||||
if (!client || !activeHandle || !canSend) {
|
||||
async function handleAccessoryKey(
|
||||
input: ReturnType<typeof createTerminalLiveAccessoryInput>,
|
||||
targetHandle = activeHandleRef.current
|
||||
) {
|
||||
if (!client || !targetHandle || targetHandle !== activeHandleRef.current || !canSend) {
|
||||
return false
|
||||
}
|
||||
const targetHandle = activeHandle
|
||||
const accessoryCommit = await handleLiveInputAccessoryBytes(input)
|
||||
if (accessoryCommit.kind !== 'allow-raw') {
|
||||
return accessoryCommit.kind === 'handled'
|
||||
@@ -3403,7 +3408,7 @@ export default function SessionScreen() {
|
||||
const accessoryRepeatRef = useRef(
|
||||
createTerminalAccessoryRepeatController<ReturnType<typeof createTerminalLiveAccessoryInput>>()
|
||||
)
|
||||
// Why: ref keeps repeat firing the current callback; else a mid-hold tab switch/reconnect routes bytes to a stale terminal.
|
||||
// Why: the current callback observes reconnect state while the sender pins the press to its original terminal.
|
||||
const handleAccessoryKeyRef = useRef(handleAccessoryKey)
|
||||
handleAccessoryKeyRef.current = handleAccessoryKey
|
||||
const stopAccessoryRepeat = useCallback(() => {
|
||||
@@ -3411,8 +3416,13 @@ export default function SessionScreen() {
|
||||
}, [])
|
||||
const startAccessoryRepeat = useCallback(
|
||||
(input: ReturnType<typeof createTerminalLiveAccessoryInput>) => {
|
||||
accessoryRepeatRef.current.start(input, (nextInput) =>
|
||||
handleAccessoryKeyRef.current(nextInput)
|
||||
accessoryRepeatRef.current.start(
|
||||
input,
|
||||
createTerminalAccessoryRepeatSender(
|
||||
activeHandleRef.current,
|
||||
() => activeHandleRef.current,
|
||||
(nextInput, targetHandle) => handleAccessoryKeyRef.current(nextInput, targetHandle)
|
||||
)
|
||||
)
|
||||
},
|
||||
[]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
createTerminalAccessoryRepeatController,
|
||||
createTerminalAccessoryRepeatSender,
|
||||
TERMINAL_ACCESSORY_REPEAT_DELAY_MS,
|
||||
TERMINAL_ACCESSORY_REPEAT_INTERVAL_MS
|
||||
} from './terminal-accessory-repeat'
|
||||
@@ -80,4 +81,21 @@ describe('terminal accessory repeat', () => {
|
||||
|
||||
expect(send).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('stops sending when the terminal active at press time is no longer active', async () => {
|
||||
let activeTerminal = 'terminal-a'
|
||||
const sendToTerminal = vi.fn(async () => true)
|
||||
const send = createTerminalAccessoryRepeatSender(
|
||||
activeTerminal,
|
||||
() => activeTerminal,
|
||||
sendToTerminal
|
||||
)
|
||||
|
||||
await expect(send('down')).resolves.toBe(true)
|
||||
activeTerminal = 'terminal-b'
|
||||
await expect(send('down')).resolves.toBe(false)
|
||||
|
||||
expect(sendToTerminal).toHaveBeenCalledTimes(1)
|
||||
expect(sendToTerminal).toHaveBeenCalledWith('down', 'terminal-a')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,6 +3,19 @@ export const TERMINAL_ACCESSORY_REPEAT_INTERVAL_MS = 45
|
||||
|
||||
type TerminalAccessoryRepeatSender<TInput> = (input: TInput) => Promise<boolean>
|
||||
|
||||
export function createTerminalAccessoryRepeatSender<TInput>(
|
||||
targetHandle: string | null,
|
||||
getActiveHandle: () => string | null,
|
||||
sendToTerminal: (input: TInput, targetHandle: string) => Promise<boolean>
|
||||
): TerminalAccessoryRepeatSender<TInput> {
|
||||
return (input) => {
|
||||
if (!targetHandle || getActiveHandle() !== targetHandle) {
|
||||
return Promise.resolve(false)
|
||||
}
|
||||
return sendToTerminal(input, targetHandle)
|
||||
}
|
||||
}
|
||||
|
||||
export function createTerminalAccessoryRepeatController<TInput>() {
|
||||
let generation = 0
|
||||
let timer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { sendTerminalLiveAccessoryRawBytes } from './terminal-live-accessory-raw-send'
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
import type { RpcResponse } from '../transport/types'
|
||||
|
||||
function captureClient(result: Promise<unknown> = Promise.resolve({ ok: true })) {
|
||||
function captureClient(
|
||||
result: Promise<unknown> = Promise.resolve({
|
||||
id: 'send',
|
||||
ok: true,
|
||||
result: { send: { handle: 'terminal-a', accepted: true, bytesWritten: 1 } },
|
||||
_meta: { runtimeId: 'test-runtime' }
|
||||
})
|
||||
) {
|
||||
const sendRequest = vi.fn(() => result)
|
||||
return { client: { sendRequest } as unknown as Pick<RpcClient, 'sendRequest'>, sendRequest }
|
||||
}
|
||||
@@ -55,4 +63,16 @@ describe('terminal live accessory raw send', () => {
|
||||
|
||||
await expect(sendTerminalLiveAccessoryRawBytes({ ...BASE_ARGS, client })).resolves.toBe(false)
|
||||
})
|
||||
|
||||
it('reports a fulfilled RPC failure as a failed send', async () => {
|
||||
const response: RpcResponse = {
|
||||
id: 'send',
|
||||
ok: false,
|
||||
error: { code: 'terminal_error', message: 'failed' },
|
||||
_meta: { runtimeId: 'test-runtime' }
|
||||
}
|
||||
const { client } = captureClient(Promise.resolve(response))
|
||||
|
||||
await expect(sendTerminalLiveAccessoryRawBytes({ ...BASE_ARGS, client })).resolves.toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getTerminalLiveAccessoryRawSendTarget } from './terminal-live-accessory-raw-send-target'
|
||||
import { isTerminalSendRpcAccepted } from './terminal-send-rpc-response'
|
||||
import { buildTerminalSendParams, TERMINAL_INPUT_SEND_OPTIONS } from './terminal-send-request'
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
import type { ConnectionState } from '../transport/types'
|
||||
@@ -36,8 +37,5 @@ export async function sendTerminalLiveAccessoryRawBytes(
|
||||
}),
|
||||
TERMINAL_INPUT_SEND_OPTIONS
|
||||
)
|
||||
.then(
|
||||
() => true,
|
||||
() => false
|
||||
)
|
||||
.then(isTerminalSendRpcAccepted, () => false)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user