fix(runtime): keep desktop take-back from stale phone input

Invalidate unfinished phone input claims after desktop takeback or disconnect. Recheck presence after delayed layout and preserve fresh phone input.

Closes #23199.
Adapted from #23200.

Co-authored-by: Tauri-EPO <enrico.pin@gmail.com>
This commit is contained in:
Neil
2026-10-21 14:30:57 -07:00
committed by GitHub
co-authored by Tauri-EPO
parent 4a671db940
commit 2d9da847cb
3 changed files with 102 additions and 5 deletions
@@ -280,6 +280,60 @@ describe('mobile presence lock — driver state machine', () => {
expect(runtime.getDriver('pty-1')).toEqual({ kind: 'mobile', clientId: 'phone-B' })
})
it.each(['before commit', 'during commit', 'direct phone-fit'] as const)(
'desktop take-back supersedes mobile input %s',
async (timing) => {
const { runtime, driverEvents, ptySizes } = createRuntime()
await runtime.handleMobileSubscribe('pty-1', 'phone-A', { cols: 45, rows: 20 })
await runtime.reclaimTerminalForDesktop('pty-1')
const claim =
timing === 'direct phone-fit' ? null : runtime.beginMobileInputFloor('pty-1', 'phone-A')!
let finishLayout = () => {}
let committing: Promise<void> | undefined
if (timing !== 'before commit') {
vi.spyOn(runtime, 'applyMobileDisplayMode').mockImplementationOnce(
() =>
new Promise<boolean>((resolve) => {
finishLayout = () => resolve(true)
})
)
committing = claim ? claim.commit() : runtime.mobileTookFloor('pty-1', 'phone-A')
}
await runtime.reclaimTerminalForDesktop('pty-1')
expect(runtime.getDriver('pty-1')).toEqual({ kind: 'desktop' })
finishLayout()
await (committing ?? claim!.commit())
expect(runtime.getDriver('pty-1')).toEqual({ kind: 'desktop' })
expect(driverEvents.at(-1)?.driver).toEqual({ kind: 'desktop' })
expect(ptySizes.get('pty-1')).toEqual({ cols: 150, rows: 40 })
await runtime.beginMobileInputFloor('pty-1', 'phone-A')!.commit()
expect(runtime.getDriver('pty-1')).toEqual({ kind: 'mobile', clientId: 'phone-A' })
}
)
it.each(['commit', 'rollback'] as const)(
'a superseded mobile input %s preserves a newer phone claim',
async (settle) => {
const { runtime, driverEvents } = createRuntime()
await runtime.handleMobileSubscribe('pty-1', 'phone-A', { cols: 45, rows: 20 })
await runtime.handleMobileSubscribe('pty-1', 'phone-B', { cols: 60, rows: 25 })
await runtime.reclaimTerminalForDesktop('pty-1')
const stale = runtime.beginMobileInputFloor('pty-1', 'phone-A')!
await runtime.reclaimTerminalForDesktop('pty-1')
const fresh = runtime.beginMobileInputFloor('pty-1', 'phone-B')!
await stale[settle]()
await fresh.commit()
expect(runtime.getDriver('pty-1')).toEqual({ kind: 'mobile', clientId: 'phone-B' })
expect(driverEvents.at(-1)?.driver).toEqual({ kind: 'mobile', clientId: 'phone-B' })
}
)
it('mobile input without an active subscriber cannot create an orphaned floor lock', async () => {
const { runtime } = createRuntime()
await runtime.handleMobileSubscribe('pty-1', 'phone-A', { cols: 45, rows: 20 })
@@ -291,6 +345,37 @@ describe('mobile presence lock — driver state machine', () => {
expect(runtime.getDriver('pty-1')).toEqual({ kind: 'idle' })
})
it.each(['input', 'legacy'] as const)(
'a %s phone-fit completion after disconnect cannot recreate the mobile lock',
async (entry) => {
const { runtime, driverEvents } = createRuntime()
await runtime.handleMobileSubscribe('pty-1', 'phone-A', { cols: 45, rows: 20 })
await runtime.reclaimTerminalForDesktop('pty-1')
let finishLayout!: () => void
vi.spyOn(runtime, 'applyMobileDisplayMode').mockImplementationOnce(
() =>
new Promise<boolean>((resolve) => {
finishLayout = () => resolve(true)
})
)
const committing =
entry === 'input'
? runtime.beginMobileInputFloor('pty-1', 'phone-A')!.commit()
: runtime.mobileTookFloor('pty-1', 'phone-A')
runtime.handleMobileUnsubscribe('pty-1', 'phone-A')
await vi.advanceTimersByTimeAsync(250)
expect(runtime.getDriver('pty-1')).toEqual({ kind: 'idle' })
finishLayout()
await committing
await vi.advanceTimersByTimeAsync(10 * 60_000)
expect(runtime.getDriver('pty-1')).toEqual({ kind: 'idle' })
expect(driverEvents.at(-1)?.driver).toEqual({ kind: 'idle' })
}
)
it('admits a soft-leaving client to reserve the input floor within the grace window', async () => {
const { runtime } = createRuntime()
await runtime.handleMobileSubscribe('pty-1', 'phone-A', { cols: 45, rows: 20 })
@@ -37,6 +37,10 @@ export class RuntimeTerminalDriverController {
}
set(ptyId: string, next: DriverState): void {
// A desktop reclaim or disconnect supersedes unfinished mobile input.
if (next.kind !== 'mobile') {
this.inputFloorClaims.delete(ptyId)
}
const prev = this.get(ptyId)
if (prev.kind === next.kind) {
if (prev.kind === 'mobile' && next.kind === 'mobile' && prev.clientId === next.clientId) {
@@ -59,6 +63,7 @@ export class RuntimeTerminalDriverController {
}
clear(ptyId: string): boolean {
this.inputFloorClaims.delete(ptyId)
if (!this.current.delete(ptyId)) {
return false
}
@@ -104,7 +109,7 @@ export class RuntimeTerminalDriverController {
}
settled = true
state.pending.delete(token)
if (generation < state.committedGeneration) {
if (this.inputFloorClaims.get(ptyId) !== state || generation < state.committedGeneration) {
this.deleteSettledClaim(ptyId, state)
return
}
@@ -14,8 +14,12 @@ export abstract class OrcaRuntimeWithMobileTookFloor extends OrcaRuntimeWithMark
ptyId: string,
clientId: string,
previousFloor?: DriverState,
isCurrent: () => boolean = () => true
isCurrent?: () => boolean
): Promise<void> {
if (!isCurrent) {
await this.beginMobileInputFloor(ptyId, clientId)?.commit()
return
}
const inner = this.mobileSubscribers.get(ptyId)
const sub = inner?.get(clientId)
const softLeaver = this.pendingSoftLeavers.get(ptyId)
@@ -39,9 +43,12 @@ export abstract class OrcaRuntimeWithMobileTookFloor extends OrcaRuntimeWithMark
}
await this.applyMobileDisplayMode(ptyId)
}
// Why: display changes are async; a later PTY write must keep the floor
// when an older phone-fit operation eventually completes.
if (!isCurrent()) {
// Newer input or disconnect can supersede an asynchronous phone-fit.
if (
!isCurrent() ||
(!this.mobileSubscribers.get(ptyId)?.has(clientId) &&
this.pendingSoftLeavers.get(ptyId)?.clientId !== clientId)
) {
return
}
this.setDriver(ptyId, { kind: 'mobile', clientId })