mirror of
https://github.com/stablyai/orca.git
synced 2026-10-21 00:03:46 +00:00
fix(terminal): keep a live TUI's mouse modes across daemon reattach
On daemon reattach Orca replayed the snapshot and then immediately wrote a mode reset that wiped the mouse modes the snapshot had just rehydrated. xterm no longer knew the still-running TUI had mouse tracking on, so it fell back to row-wise selection over the TUI — the reporter saw drag selection appear after an in-app update restart. Re-arm the modes the replayed payload actually left on, instead of resetting them. A TUI that died leaving modes armed is still cleaned up by the confirmed-shell-foreground reset, so this only preserves modes that were genuinely on. Composed with the newer cold-restore branch from #12101: when the owner process ended, the full reset still wins. Fixes #8291 Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { buildMouseModeRearmSequence } from '../../shared/terminal-mouse-mode-sequences'
|
||||
import type { TerminalModes } from './types'
|
||||
|
||||
// Why no kitty flags here: rehydrateSequences feeds renderer xterms, and
|
||||
@@ -20,28 +21,13 @@ export function buildRehydrateSequences(modes: TerminalModes): string {
|
||||
}
|
||||
// Why: mobile alt-screen scroll gestures need xterm's mouse mode restored
|
||||
// from cold snapshots; OpenCode/OpenTUI enables scrollable panes this way.
|
||||
switch (modes.mouseTracking ? (modes.mouseTrackingMode ?? 'vt200') : 'none') {
|
||||
case 'x10':
|
||||
seqs.push('\x1b[?9h')
|
||||
break
|
||||
case 'vt200':
|
||||
seqs.push('\x1b[?1000h')
|
||||
break
|
||||
case 'drag':
|
||||
seqs.push('\x1b[?1002h')
|
||||
break
|
||||
case 'any':
|
||||
seqs.push('\x1b[?1003h')
|
||||
break
|
||||
case 'none':
|
||||
break
|
||||
}
|
||||
// Why: xterm tracks the mouse protocol and SGR encoding as independent
|
||||
// modes, so snapshots must preserve the encoding even when reporting is off.
|
||||
if (modes.sgrMousePixelsMode) {
|
||||
seqs.push('\x1b[?1016h')
|
||||
} else if (modes.sgrMouseMode) {
|
||||
seqs.push('\x1b[?1006h')
|
||||
}
|
||||
// Why shared: the renderer re-arms the same bytes after a reattach reset.
|
||||
seqs.push(
|
||||
buildMouseModeRearmSequence({
|
||||
mouseTrackingMode: modes.mouseTracking ? (modes.mouseTrackingMode ?? 'vt200') : 'none',
|
||||
sgrMouseMode: modes.sgrMouseMode === true,
|
||||
sgrMousePixelsMode: modes.sgrMousePixelsMode === true
|
||||
})
|
||||
)
|
||||
return seqs.join('')
|
||||
}
|
||||
|
||||
@@ -1,119 +1,3 @@
|
||||
import type { TerminalModes } from './types'
|
||||
|
||||
type MouseTrackingMode = NonNullable<TerminalModes['mouseTrackingMode']>
|
||||
|
||||
// Why: PTY/SSH chunks can split a long combined DECSET before the final h/l.
|
||||
// Keep parser state far beyond normal mode lists while still bounding memory.
|
||||
const PRIVATE_MODE_SCAN_TAIL_LIMIT = 4096
|
||||
|
||||
/**
|
||||
* Mirrors DECSET mouse-protocol/encoding state from the raw byte stream.
|
||||
* xterm's public modes API does not expose which mouse protocol is active,
|
||||
* so snapshots track it independently of the headless terminal; callers
|
||||
* must feed `scan()` the same bytes the terminal parsed, in order.
|
||||
*/
|
||||
export class TerminalMouseModeMirror {
|
||||
private scanTail = ''
|
||||
private trackingModeState: MouseTrackingMode = 'none'
|
||||
private sgrMouseModeState = false
|
||||
private sgrMousePixelsModeState = false
|
||||
|
||||
get mouseTrackingMode(): MouseTrackingMode {
|
||||
return this.trackingModeState
|
||||
}
|
||||
|
||||
get sgrMouseMode(): boolean {
|
||||
return this.sgrMouseModeState
|
||||
}
|
||||
|
||||
get sgrMousePixelsMode(): boolean {
|
||||
return this.sgrMousePixelsModeState
|
||||
}
|
||||
|
||||
scan(data: string): void {
|
||||
// Why the pre-filter: this runs on the daemon's per-chunk hot path for
|
||||
// every session; a flood chunk with no private-mode/reset introducer
|
||||
// must not pay the regex pass (measured share of a 2.2x ingest
|
||||
// regression — findings log 2026-07-03). Split sequences stay correct:
|
||||
// an introducer split across chunks either left a non-empty scanTail
|
||||
// (previous partial) or ends this chunk, which extractScanTail retains.
|
||||
if (
|
||||
this.scanTail.length === 0 &&
|
||||
!data.includes('\x1b[?') &&
|
||||
!data.includes('\x1bc') &&
|
||||
!data.includes('\x9b')
|
||||
) {
|
||||
this.scanTail = this.extractScanTail(data)
|
||||
return
|
||||
}
|
||||
const input = this.scanTail.length === 0 ? data : this.scanTail + data
|
||||
this.scanTail = this.extractScanTail(input)
|
||||
// oxlint-disable-next-line no-control-regex -- terminal escape sequences require control chars
|
||||
const privateModeRe = /\x1bc|\x1b\[\?([0-9;]+)([hl])|\x9b\?([0-9;]+)([hl])/g
|
||||
let match: RegExpExecArray | null
|
||||
while ((match = privateModeRe.exec(input)) !== null) {
|
||||
if (match[0] === '\x1bc') {
|
||||
this.trackingModeState = 'none'
|
||||
this.sgrMouseModeState = false
|
||||
this.sgrMousePixelsModeState = false
|
||||
continue
|
||||
}
|
||||
const params = match[1] ?? match[3]
|
||||
const enabled = (match[2] ?? match[4]) === 'h'
|
||||
for (const rawParam of params.split(';')) {
|
||||
if (rawParam === '') {
|
||||
continue
|
||||
}
|
||||
const param = Number(rawParam)
|
||||
if (!Number.isInteger(param)) {
|
||||
continue
|
||||
}
|
||||
if (param === 9) {
|
||||
this.trackingModeState = enabled ? 'x10' : 'none'
|
||||
}
|
||||
if (param === 1000) {
|
||||
this.trackingModeState = enabled ? 'vt200' : 'none'
|
||||
}
|
||||
if (param === 1002) {
|
||||
this.trackingModeState = enabled ? 'drag' : 'none'
|
||||
}
|
||||
if (param === 1003) {
|
||||
this.trackingModeState = enabled ? 'any' : 'none'
|
||||
}
|
||||
if (param === 1006) {
|
||||
this.sgrMouseModeState = enabled
|
||||
this.sgrMousePixelsModeState = false
|
||||
}
|
||||
if (param === 1016) {
|
||||
this.sgrMouseModeState = false
|
||||
this.sgrMousePixelsModeState = enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extractScanTail(input: string): string {
|
||||
const start = Math.max(input.lastIndexOf('\x1b'), input.lastIndexOf('\x9b'))
|
||||
if (start === -1) {
|
||||
return ''
|
||||
}
|
||||
const tail = input.slice(start)
|
||||
if (tail.length > PRIVATE_MODE_SCAN_TAIL_LIMIT) {
|
||||
return ''
|
||||
}
|
||||
if (tail === '\x1b' || tail === '\x1b[' || tail === '\x9b') {
|
||||
return tail
|
||||
}
|
||||
if (tail.startsWith('\x1b[?')) {
|
||||
return this.isIncompleteParams(tail.slice(3)) ? tail : ''
|
||||
}
|
||||
if (tail.startsWith('\x9b?')) {
|
||||
return this.isIncompleteParams(tail.slice(2)) ? tail : ''
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
private isIncompleteParams(params: string): boolean {
|
||||
return /^[0-9;]*$/.test(params)
|
||||
}
|
||||
}
|
||||
// Moved to src/shared so the renderer's reattach re-arm scans with the same
|
||||
// implementation the daemon mirrors from (see terminal-mouse-mode-sequences).
|
||||
export { TerminalMouseModeMirror } from '../../shared/terminal-mouse-mode-sequences'
|
||||
|
||||
@@ -9231,6 +9231,44 @@ describe('connectPanePty', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('re-arms mouse reporting a reattach snapshot restored for a live third-party TUI', async () => {
|
||||
// Why: issue #8291 — the reattach reset wiped the modes the daemon snapshot just
|
||||
// rehydrated, so xterm re-enabled its row-wise selection over a live TUI.
|
||||
const { connectPanePty } = await import('./pty-connection')
|
||||
const transport = createMockTransport('tab-pty')
|
||||
transport.connect.mockImplementation(async ({ sessionId }: { sessionId?: string }) => {
|
||||
if (sessionId) {
|
||||
return { id: sessionId, snapshot: '\x1b[?1002h\x1b[?1006hthird-party tui session' }
|
||||
}
|
||||
return null
|
||||
})
|
||||
transportFactoryQueue.push(transport)
|
||||
setReattachPaneTitle('zsh')
|
||||
|
||||
const pane = createPane(1)
|
||||
const textarea = {} as HTMLTextAreaElement
|
||||
configureTerminalFocusMode(pane, textarea)
|
||||
await withMockedDocumentActiveElement(textarea, async () => {
|
||||
const manager = createManager(1)
|
||||
const deps = createDeps({
|
||||
restoredLeafId: LEAF_1,
|
||||
restoredPtyIdByLeafId: { [LEAF_1]: 'tab-pty' }
|
||||
})
|
||||
|
||||
connectPanePty(pane as never, manager as never, deps as never)
|
||||
await flushAsyncTicks(20)
|
||||
|
||||
expect(pane.terminal.write).toHaveBeenCalledWith(
|
||||
`${POST_REPLAY_REATTACH_RESET}\x1b[?1002h\x1b[?1006h`,
|
||||
expect.any(Function)
|
||||
)
|
||||
expect(pane.terminal.write).not.toHaveBeenCalledWith(
|
||||
POST_REPLAY_REATTACH_RESET,
|
||||
expect.any(Function)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('does not treat persisted tab launchAgent metadata as a live agent reattach', async () => {
|
||||
const { connectPanePty } = await import('./pty-connection')
|
||||
const transport = createMockTransport('tab-pty')
|
||||
|
||||
@@ -117,6 +117,7 @@ import {
|
||||
} from '@/lib/pane-manager/pane-pty-resize-hold'
|
||||
import {
|
||||
buildPostReplayLiveAgentReattachReset,
|
||||
buildReattachMouseModeRearm,
|
||||
POST_REPLAY_LIVE_AGENT_SNAPSHOT_RESET,
|
||||
POST_REPLAY_LIVE_SNAPSHOT_RESET,
|
||||
POST_REPLAY_MODE_RESET,
|
||||
@@ -5501,9 +5502,14 @@ export function connectPanePty(
|
||||
if (ownerProcessEnded) {
|
||||
return POST_REPLAY_MODE_RESET
|
||||
}
|
||||
return shouldPreserveAgentReattachModes()
|
||||
? buildPostReplayLiveAgentReattachReset(payload)
|
||||
: POST_REPLAY_REATTACH_RESET
|
||||
if (shouldPreserveAgentReattachModes()) {
|
||||
return buildPostReplayLiveAgentReattachReset(payload)
|
||||
}
|
||||
// Why the re-arm: a third-party TUI Orca does not recognise as an agent is still live,
|
||||
// and the daemon snapshot just rehydrated its mouse modes; wiping them re-enables
|
||||
// xterm's row-wise selection over the TUI (#8291). A dead TUI's stale modes are still
|
||||
// cleared by the confirmed-shell-foreground reset above.
|
||||
return `${POST_REPLAY_REATTACH_RESET}${buildReattachMouseModeRearm(payload)}`
|
||||
}
|
||||
|
||||
const consumeRestoredViewportBlankingMarker = (): boolean => {
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
// into an xterm) and the daemon (seeding a cold-restored session) must clear
|
||||
// the same mode bits, and duplicating the literals drifted them apart (#12101).
|
||||
|
||||
import { scanReplayedMouseModeRearm } from './terminal-mouse-mode-sequences'
|
||||
|
||||
// Why: SerializeAddon replays mode bits assuming reattach to a live TUI, but Orca restores against a fresh shell with none, so stale bits (e.g. focus reporting rings the bell on click) must be reset.
|
||||
export const RESET_TERMINAL_CURSOR_STYLE = '\x1b[0 q'
|
||||
export const RESET_KITTY_KEYBOARD_PROTOCOL = '\x1b[<99u\x1b[=0u'
|
||||
@@ -18,6 +20,15 @@ export const POST_REPLAY_LIVE_SNAPSHOT_RESET = `${RESET_TERMINAL_CURSOR_STYLE}\x
|
||||
// Why: daemon reattach hits a live session, so skip the full reset; still clear cursor/focus/mouse/Kitty bits harmful to a plain shell after a bad TUI exit — safe for live TUIs since the post-reattach SIGWINCH repaints the cursor.
|
||||
export const POST_REPLAY_REATTACH_RESET = `${RESET_TERMINAL_CURSOR_STYLE}${RESET_KITTY_KEYBOARD_PROTOCOL}\x1b[?25h${RESET_MOUSE_REPORTING}\x1b[?1004l`
|
||||
|
||||
// Why: the reattach reset must not outlive the daemon's authoritative mode mirror — the
|
||||
// snapshot rehydrates a live TUI's mouse protocol/encoding and RESET_MOUSE_REPORTING would
|
||||
// disarm it one write later, re-enabling xterm's row-wise selection over the TUI (#8291).
|
||||
// A TUI that died leaving modes armed is still cleaned up by the confirmed-shell-foreground
|
||||
// reset in pty-connection, so this only preserves modes the replayed payload really left on.
|
||||
export function buildReattachMouseModeRearm(payload: string): string {
|
||||
return scanReplayedMouseModeRearm(payload)
|
||||
}
|
||||
|
||||
// Why: a live agent owns focus reporting; resetting ?1004h suppresses the focus-in it needs to re-anchor its cursor (IME).
|
||||
export const POST_REPLAY_LIVE_AGENT_REATTACH_RESET = `${RESET_TERMINAL_CURSOR_STYLE}${RESET_KITTY_KEYBOARD_PROTOCOL}\x1b[?25h`
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
buildMouseModeRearmSequence,
|
||||
scanReplayedMouseModeRearm,
|
||||
TerminalMouseModeMirror
|
||||
} from './terminal-mouse-mode-sequences'
|
||||
|
||||
describe('scanReplayedMouseModeRearm', () => {
|
||||
it('re-arms the protocol and encoding a payload left on', () => {
|
||||
expect(scanReplayedMouseModeRearm('boot\x1b[?1002h\x1b[?1006hlive tui')).toBe(
|
||||
'\x1b[?1002h\x1b[?1006h'
|
||||
)
|
||||
})
|
||||
|
||||
it('re-arms nothing when the payload turned mouse reporting back off', () => {
|
||||
expect(scanReplayedMouseModeRearm('\x1b[?1002h\x1b[?1006h\x1b[?1002l\x1b[?1006l')).toBe('')
|
||||
})
|
||||
|
||||
it('treats RIS as clearing every mouse mode', () => {
|
||||
expect(scanReplayedMouseModeRearm('\x1b[?1003h\x1b[?1016h\x1bcfresh')).toBe('')
|
||||
})
|
||||
|
||||
it('prefers the pixel encoding over SGR', () => {
|
||||
expect(scanReplayedMouseModeRearm('\x1b[?1000h\x1b[?1016h')).toBe('\x1b[?1000h\x1b[?1016h')
|
||||
})
|
||||
})
|
||||
|
||||
describe('TerminalMouseModeMirror', () => {
|
||||
it('detects a DECSET split across two scan calls', () => {
|
||||
const mirror = new TerminalMouseModeMirror()
|
||||
mirror.scan('output\x1b[?10')
|
||||
mirror.scan('02h\x1b[?1006h')
|
||||
expect(mirror.mouseTrackingMode).toBe('drag')
|
||||
expect(mirror.sgrMouseMode).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildMouseModeRearmSequence', () => {
|
||||
it('returns nothing when no protocol or encoding is armed', () => {
|
||||
expect(
|
||||
buildMouseModeRearmSequence({
|
||||
mouseTrackingMode: 'none',
|
||||
sgrMouseMode: false,
|
||||
sgrMousePixelsMode: false
|
||||
})
|
||||
).toBe('')
|
||||
})
|
||||
|
||||
it('keeps the encoding when reporting itself is off', () => {
|
||||
expect(
|
||||
buildMouseModeRearmSequence({
|
||||
mouseTrackingMode: 'none',
|
||||
sgrMouseMode: true,
|
||||
sgrMousePixelsMode: false
|
||||
})
|
||||
).toBe('\x1b[?1006h')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,168 @@
|
||||
export type TerminalMouseTrackingMode = 'none' | 'x10' | 'vt200' | 'drag' | 'any'
|
||||
|
||||
export type TerminalMouseModeState = {
|
||||
mouseTrackingMode: TerminalMouseTrackingMode
|
||||
sgrMouseMode: boolean
|
||||
sgrMousePixelsMode: boolean
|
||||
}
|
||||
|
||||
// Why: PTY/SSH chunks can split a long combined DECSET before the final h/l.
|
||||
// Keep parser state far beyond normal mode lists while still bounding memory.
|
||||
const PRIVATE_MODE_SCAN_TAIL_LIMIT = 4096
|
||||
|
||||
/**
|
||||
* Mirrors DECSET mouse-protocol/encoding state from the raw byte stream.
|
||||
* xterm's public modes API does not expose which mouse protocol is active,
|
||||
* so snapshots track it independently of the headless terminal; callers
|
||||
* must feed `scan()` the same bytes the terminal parsed, in order.
|
||||
*
|
||||
* Lives in shared so the daemon (authoritative mirror) and the renderer
|
||||
* (reattach re-arm) can never disagree about what a payload left armed.
|
||||
*/
|
||||
export class TerminalMouseModeMirror {
|
||||
private scanTail = ''
|
||||
private trackingModeState: TerminalMouseTrackingMode = 'none'
|
||||
private sgrMouseModeState = false
|
||||
private sgrMousePixelsModeState = false
|
||||
|
||||
get mouseTrackingMode(): TerminalMouseTrackingMode {
|
||||
return this.trackingModeState
|
||||
}
|
||||
|
||||
get sgrMouseMode(): boolean {
|
||||
return this.sgrMouseModeState
|
||||
}
|
||||
|
||||
get sgrMousePixelsMode(): boolean {
|
||||
return this.sgrMousePixelsModeState
|
||||
}
|
||||
|
||||
scan(data: string): void {
|
||||
// Why the pre-filter: this runs on the daemon's per-chunk hot path for
|
||||
// every session; a flood chunk with no private-mode/reset introducer
|
||||
// must not pay the regex pass (measured share of a 2.2x ingest
|
||||
// regression — findings log 2026-07-03). Split sequences stay correct:
|
||||
// an introducer split across chunks either left a non-empty scanTail
|
||||
// (previous partial) or ends this chunk, which extractScanTail retains.
|
||||
if (
|
||||
this.scanTail.length === 0 &&
|
||||
!data.includes('\x1b[?') &&
|
||||
!data.includes('\x1bc') &&
|
||||
!data.includes('\x9b')
|
||||
) {
|
||||
this.scanTail = this.extractScanTail(data)
|
||||
return
|
||||
}
|
||||
const input = this.scanTail.length === 0 ? data : this.scanTail + data
|
||||
this.scanTail = this.extractScanTail(input)
|
||||
// oxlint-disable-next-line no-control-regex -- terminal escape sequences require control chars
|
||||
const privateModeRe = /\x1bc|\x1b\[\?([0-9;]+)([hl])|\x9b\?([0-9;]+)([hl])/g
|
||||
let match: RegExpExecArray | null
|
||||
while ((match = privateModeRe.exec(input)) !== null) {
|
||||
if (match[0] === '\x1bc') {
|
||||
this.trackingModeState = 'none'
|
||||
this.sgrMouseModeState = false
|
||||
this.sgrMousePixelsModeState = false
|
||||
continue
|
||||
}
|
||||
const params = match[1] ?? match[3]
|
||||
const enabled = (match[2] ?? match[4]) === 'h'
|
||||
for (const rawParam of params.split(';')) {
|
||||
if (rawParam === '') {
|
||||
continue
|
||||
}
|
||||
const param = Number(rawParam)
|
||||
if (!Number.isInteger(param)) {
|
||||
continue
|
||||
}
|
||||
if (param === 9) {
|
||||
this.trackingModeState = enabled ? 'x10' : 'none'
|
||||
}
|
||||
if (param === 1000) {
|
||||
this.trackingModeState = enabled ? 'vt200' : 'none'
|
||||
}
|
||||
if (param === 1002) {
|
||||
this.trackingModeState = enabled ? 'drag' : 'none'
|
||||
}
|
||||
if (param === 1003) {
|
||||
this.trackingModeState = enabled ? 'any' : 'none'
|
||||
}
|
||||
if (param === 1006) {
|
||||
this.sgrMouseModeState = enabled
|
||||
this.sgrMousePixelsModeState = false
|
||||
}
|
||||
if (param === 1016) {
|
||||
this.sgrMouseModeState = false
|
||||
this.sgrMousePixelsModeState = enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extractScanTail(input: string): string {
|
||||
const start = Math.max(input.lastIndexOf('\x1b'), input.lastIndexOf('\x9b'))
|
||||
if (start === -1) {
|
||||
return ''
|
||||
}
|
||||
const tail = input.slice(start)
|
||||
if (tail.length > PRIVATE_MODE_SCAN_TAIL_LIMIT) {
|
||||
return ''
|
||||
}
|
||||
if (tail === '\x1b' || tail === '\x1b[' || tail === '\x9b') {
|
||||
return tail
|
||||
}
|
||||
if (tail.startsWith('\x1b[?')) {
|
||||
return this.isIncompleteParams(tail.slice(3)) ? tail : ''
|
||||
}
|
||||
if (tail.startsWith('\x9b?')) {
|
||||
return this.isIncompleteParams(tail.slice(2)) ? tail : ''
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
private isIncompleteParams(params: string): boolean {
|
||||
return /^[0-9;]*$/.test(params)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DECSET bytes that re-arm the given mouse protocol + encoding.
|
||||
* Why the encoding is independent: xterm tracks protocol and SGR encoding as
|
||||
* separate modes, so the encoding must be preserved even when reporting is off.
|
||||
*/
|
||||
export function buildMouseModeRearmSequence(state: TerminalMouseModeState): string {
|
||||
const seqs: string[] = []
|
||||
switch (state.mouseTrackingMode) {
|
||||
case 'x10':
|
||||
seqs.push('\x1b[?9h')
|
||||
break
|
||||
case 'vt200':
|
||||
seqs.push('\x1b[?1000h')
|
||||
break
|
||||
case 'drag':
|
||||
seqs.push('\x1b[?1002h')
|
||||
break
|
||||
case 'any':
|
||||
seqs.push('\x1b[?1003h')
|
||||
break
|
||||
case 'none':
|
||||
break
|
||||
}
|
||||
if (state.sgrMousePixelsMode) {
|
||||
seqs.push('\x1b[?1016h')
|
||||
} else if (state.sgrMouseMode) {
|
||||
seqs.push('\x1b[?1006h')
|
||||
}
|
||||
return seqs.join('')
|
||||
}
|
||||
|
||||
/** Mouse modes a replayed payload leaves armed, as DECSET bytes ('' when none). */
|
||||
export function scanReplayedMouseModeRearm(payload: string): string {
|
||||
const mirror = new TerminalMouseModeMirror()
|
||||
mirror.scan(payload)
|
||||
return buildMouseModeRearmSequence({
|
||||
mouseTrackingMode: mirror.mouseTrackingMode,
|
||||
sgrMouseMode: mirror.sgrMouseMode,
|
||||
sgrMousePixelsMode: mirror.sgrMousePixelsMode
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user