mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
fix(terminal): give the window and main's copy the daemon's ConPTY resize rule (#33584)
* fix(terminal): give the window and main's copy the daemon's ConPTY resize rule Every PTY a Windows machine owns is ConPTY, WSL shells included, and ConPTY repaints its own grid after a resize. The window applied xterm's ConPTY model only to native (non-WSL) panes, and main's copy for reveals and phones never did, so growing either pulled history into ConPTY's grid. Decide windowsPty from the PTY's owner backend everywhere, keeping the native-only cursor, CJK and keyboard workarounds behind isLocalNativeWindowsConpty. * fix(terminal): keep main's copy of an SSH relay PTY off the ConPTY model before its record exists Main's copy is built on the first byte, and a restored or adopted relay PTY can stream before its record carries a connection id.
This commit is contained in:
@@ -3,7 +3,7 @@ import { TerminalShellRecoveryBarrier } from './terminal-shell-recovery-barrier'
|
||||
import type { SubprocessHandle } from './session-subprocess-handle'
|
||||
import { release } from 'node:os'
|
||||
import type { PtyOwnerBackend } from '../../shared/pty-owner-backend'
|
||||
import { buildXtermConptyOptions, parseWindowsBuildNumber } from '../../shared/xterm-conpty-options'
|
||||
import { windowsPtyForOwnerBackend } from '../../shared/xterm-conpty-options'
|
||||
|
||||
/** Couple barrier emissions and snapshot ownership so neither can lag the output plane. */
|
||||
export function createSessionOutputPipeline(opts: {
|
||||
@@ -24,12 +24,7 @@ export function createSessionOutputPipeline(opts: {
|
||||
scrollback: opts.scrollback,
|
||||
wslDistro: opts.wslDistro,
|
||||
historySeedChunks: opts.historySeedChunks,
|
||||
// Every Windows daemon PTY is ConPTY, WSL shells included; ConPTY repaints its own grid after a
|
||||
// resize, so the emulator must not pull scrollback into it (xterm's windowsPty resize model).
|
||||
windowsPty:
|
||||
opts.ownerBackend && opts.ownerBackend !== 'posix-pty'
|
||||
? buildXtermConptyOptions(parseWindowsBuildNumber(release()))
|
||||
: undefined,
|
||||
windowsPty: windowsPtyForOwnerBackend(opts.ownerBackend, release()),
|
||||
getTerminalOwner: () => barrier?.getOwner()
|
||||
})
|
||||
const recoveryBarrier = new TerminalShellRecoveryBarrier({
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { withPlatform } from './orca-runtime-test-fixtures.spec'
|
||||
import { createHydrationRuntime, PTY_ID } from './headless-hydration-ownership-test-fixture'
|
||||
|
||||
const HISTORY = Array.from({ length: 60 }, (_, i) => `H${i}`).join('\r\n')
|
||||
|
||||
async function scrollbackAfterShrinkAndGrow(
|
||||
platform: NodeJS.Platform,
|
||||
ptyId = PTY_ID
|
||||
): Promise<number> {
|
||||
return withPlatform(platform, async () => {
|
||||
const runtime = createHydrationRuntime()
|
||||
runtime.onPtyData(ptyId, HISTORY, 1)
|
||||
const model = runtime.modelFor(ptyId)
|
||||
await model.writeChain
|
||||
const before = model.emulator.getSnapshot().scrollbackLines
|
||||
model.emulator.resize(80, 2)
|
||||
model.emulator.resize(80, 24)
|
||||
return model.emulator.getSnapshot().scrollbackLines - before
|
||||
})
|
||||
}
|
||||
|
||||
// awin verify-14: ConPTY repaints its own grid after a resize, so rows pulled back from history
|
||||
// into main's copy were overwritten before a reveal repainted the window from it.
|
||||
describe("main's copy of a local pane", () => {
|
||||
it("grows rows the ConPTY way on Windows, keeping history out of ConPTY's grid", async () => {
|
||||
expect(await scrollbackAfterShrinkAndGrow('win32')).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('keeps the Unix grow model elsewhere', async () => {
|
||||
expect(await scrollbackAfterShrinkAndGrow('darwin')).toBeLessThanOrEqual(0)
|
||||
})
|
||||
|
||||
// A restored or adopted relay PTY can stream before its record exists; its id still names SSH.
|
||||
it('keeps the Unix model for an SSH relay PTY with no record yet', async () => {
|
||||
expect(
|
||||
await scrollbackAfterShrinkAndGrow('win32', 'ssh:target-1@@relay-pty-9')
|
||||
).toBeLessThanOrEqual(0)
|
||||
})
|
||||
})
|
||||
@@ -13,7 +13,11 @@ export const RETIRED_SNAPSHOT = {
|
||||
|
||||
export class HydrationRuntime extends OrcaRuntimeService {
|
||||
model() {
|
||||
const state = this.headlessTerminals.get(PTY_ID)
|
||||
return this.modelFor(PTY_ID)
|
||||
}
|
||||
|
||||
modelFor(ptyId: string) {
|
||||
const state = this.headlessTerminals.get(ptyId)
|
||||
if (!state) {
|
||||
throw new Error('Expected headless model')
|
||||
}
|
||||
|
||||
@@ -6,8 +6,26 @@ import { isNativeWindowsConptyPty } from '../../terminal-model-query-authority'
|
||||
import { getTerminalViewAttributes } from '../../terminal-view-attribute-store'
|
||||
import { PtyShellOwnershipMirror } from '../../pty-shell-ownership-mirror'
|
||||
import { PROCESS_BOUNDARY_GROUND } from '../../../../shared/terminal-mode-reset-profiles'
|
||||
import { release } from 'node:os'
|
||||
import { resolvePtyOwnerBackend, type PtyOwnerBackend } from '../../../../shared/pty-owner-backend'
|
||||
import { isRemoteRuntimePtyId } from '../../../../shared/remote-runtime-pty-id'
|
||||
import { parseAppSshPtyId } from '../../../../shared/ssh-pty-id'
|
||||
import { windowsPtyForOwnerBackend } from '../../../../shared/xterm-conpty-options'
|
||||
|
||||
export abstract class OrcaRuntimeWithCreatePtyHeadlessTerminalState extends OrcaRuntimeWithMaybeHydrateHeadlessFromRenderer {
|
||||
/** SSH and paired-runtime panes run on their host's PTY; every PTY this Windows process owns is ConPTY. */
|
||||
private ownerBackendForHeadlessPty(ptyId: string): PtyOwnerBackend {
|
||||
// Why the id too: main's copy starts on the first byte, before a restored relay PTY has a record.
|
||||
if (
|
||||
this.ptysById.get(ptyId)?.connectionId ||
|
||||
parseAppSshPtyId(ptyId) !== null ||
|
||||
isRemoteRuntimePtyId(ptyId)
|
||||
) {
|
||||
return 'posix-pty'
|
||||
}
|
||||
return resolvePtyOwnerBackend({ platform: process.platform, shellPath: undefined })
|
||||
}
|
||||
|
||||
/** Create runtime emulators with query forwarding and ConPTY DA1 override for seed, hydration and live bytes; daemon emulators use another factory. */
|
||||
protected createPtyHeadlessTerminalState(
|
||||
ptyId: string,
|
||||
@@ -24,6 +42,7 @@ export abstract class OrcaRuntimeWithCreatePtyHeadlessTerminalState extends Orca
|
||||
wslDistro: this.ptysById.get(ptyId)?.connectionId
|
||||
? undefined
|
||||
: (this.wslDistroByPtyId.get(ptyId) ?? this.ptysById.get(ptyId)?.wslDistro ?? undefined),
|
||||
windowsPty: windowsPtyForOwnerBackend(this.ownerBackendForHeadlessPty(ptyId), release()),
|
||||
// Use provider input for query replies to retain shell-ready/SSH routing without interactive echo metering.
|
||||
onQueryReply: (reply) => {
|
||||
// Queued links can outlive disposal; only the current state may reply to a reused PTY id.
|
||||
|
||||
@@ -88,7 +88,8 @@ describe('buildWindowsPtyCompatibilityOptions', () => {
|
||||
).toEqual({})
|
||||
})
|
||||
|
||||
it('skips compatibility options for WSL cwd terminals', () => {
|
||||
// ConPTY hosts local WSL shells too, so their emulator needs its resize model (awin verify-14).
|
||||
it('uses the ConPTY model for local WSL cwd terminals', () => {
|
||||
for (const cwd of [
|
||||
'\\\\wsl.localhost\\Ubuntu\\home\\me\\repo',
|
||||
'\\\\wsl$\\Debian\\home\\me\\repo',
|
||||
@@ -104,11 +105,11 @@ describe('buildWindowsPtyCompatibilityOptions', () => {
|
||||
shellOverride: null,
|
||||
executionHostId: 'local'
|
||||
})
|
||||
).toEqual({})
|
||||
).toEqual({ windowsPty: { backend: 'conpty', buildNumber: 26100 } })
|
||||
}
|
||||
})
|
||||
|
||||
it('skips compatibility options when the shell override launches WSL', () => {
|
||||
it('uses the ConPTY model when the shell override launches WSL', () => {
|
||||
expect(
|
||||
buildWindowsPtyCompatibilityOptions({
|
||||
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
|
||||
@@ -118,7 +119,7 @@ describe('buildWindowsPtyCompatibilityOptions', () => {
|
||||
shellOverride: 'C:\\Windows\\System32\\wsl.exe',
|
||||
executionHostId: 'local'
|
||||
})
|
||||
).toEqual({})
|
||||
).toEqual({ windowsPty: { backend: 'conpty', buildNumber: 26100 } })
|
||||
})
|
||||
|
||||
it('returns no options outside Windows', () => {
|
||||
|
||||
@@ -3,8 +3,10 @@ import { isWslUncPath } from '../../../../shared/wsl-paths'
|
||||
import { LOCAL_EXECUTION_HOST_ID, type ExecutionHostId } from '../../../../shared/execution-host'
|
||||
import {
|
||||
buildXtermConptyOptions,
|
||||
parseWindowsBuildNumber
|
||||
parseWindowsBuildNumber,
|
||||
windowsPtyForOwnerBackend
|
||||
} from '../../../../shared/xterm-conpty-options'
|
||||
import { resolvePtyOwnerBackend, type PtyOwnerBackend } from '../../../../shared/pty-owner-backend'
|
||||
|
||||
export type WindowsPtyCompatibilityContext = {
|
||||
userAgent?: string
|
||||
@@ -26,14 +28,29 @@ function isWslShellOverride(shellOverride: string | null | undefined): boolean {
|
||||
return /(?:^|[/\\])wsl(?:\.exe)?$/i.test(shellOverride ?? '')
|
||||
}
|
||||
|
||||
/** Apply ConPTY options only to genuine local Windows panes. executionHostId excludes serve/runtime panes that look local to raw connectionId/cwd heuristics. */
|
||||
/**
|
||||
* xterm's ConPTY model for every pane this Windows machine's daemon owns, WSL shells included: the
|
||||
* same owner-backend rule as the daemon's and main's emulators. Native-only workarounds (cursor,
|
||||
* CJK, keyboard) stay behind `isLocalNativeWindowsConpty`. executionHostId excludes serve/runtime
|
||||
* panes that look local to raw connectionId/cwd heuristics.
|
||||
*/
|
||||
export function buildWindowsPtyCompatibilityOptions(
|
||||
context: WindowsPtyCompatibilityContext & { executionHostId: ExecutionHostId }
|
||||
): Partial<ITerminalOptions> {
|
||||
if (!isLocalNativeWindowsConpty(context)) {
|
||||
return {}
|
||||
const windowsPty = windowsPtyForOwnerBackend(localPaneOwnerBackend(context), context.osRelease)
|
||||
return windowsPty ? { windowsPty } : {}
|
||||
}
|
||||
|
||||
function localPaneOwnerBackend(
|
||||
context: WindowsPtyCompatibilityContext & { executionHostId: ExecutionHostId }
|
||||
): PtyOwnerBackend | undefined {
|
||||
if (context.executionHostId !== LOCAL_EXECUTION_HOST_ID || context.connectionId !== null) {
|
||||
return undefined
|
||||
}
|
||||
return buildLocalConptyTerminalOptions(context.osRelease)
|
||||
return resolvePtyOwnerBackend({
|
||||
platform: isWindowsUserAgent(context.userAgent) ? 'win32' : 'linux',
|
||||
shellPath: context.shellOverride ?? undefined
|
||||
})
|
||||
}
|
||||
|
||||
/** Reuse ConPTY options after dashboard preview has already resolved local native-Windows ownership. */
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { PtyOwnerBackend } from './pty-owner-backend'
|
||||
|
||||
/** xterm `windowsPty` for a ConPTY-backed terminal, shared by the window and the daemon's emulator. */
|
||||
export type XtermConptyOptions = { backend: 'conpty'; buildNumber?: number }
|
||||
|
||||
@@ -17,3 +19,17 @@ export function buildXtermConptyOptions(buildNumber: number | undefined): XtermC
|
||||
}
|
||||
return { backend: 'conpty', buildNumber }
|
||||
}
|
||||
|
||||
/**
|
||||
* ConPTY repaints its own grid after a resize, so every emulator of a ConPTY pane (the daemon's,
|
||||
* main's copy and the window's) must grow rows without pulling scrollback back into it.
|
||||
* Every PTY a Windows host owns is ConPTY, WSL shells included.
|
||||
*/
|
||||
export function windowsPtyForOwnerBackend(
|
||||
ownerBackend: PtyOwnerBackend | undefined,
|
||||
osRelease: string | null | undefined
|
||||
): XtermConptyOptions | undefined {
|
||||
return ownerBackend === undefined || ownerBackend === 'posix-pty'
|
||||
? undefined
|
||||
: buildXtermConptyOptions(parseWindowsBuildNumber(osRelease))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user