mirror of
https://github.com/stablyai/orca.git
synced 2026-10-12 16:03:03 +00:00
fix(runtime): seed the model when a buffer read falls back to the pane
With no host model, a terminal buffer read answered with the desktop pane's screen at the pane's size. The phone's resize restream and pending-output recovery read through it, so a hidden pane could still hand the phone a desktop-sized screen. The fallback now seeds the model from that screen onto the PTY grid and answers from the model, unless output landed during the read. The desktop's own hidden-output recovery keeps the raw renderer fallback. Claude-Session: lane-host-hydrate
This commit is contained in:
@@ -23,7 +23,9 @@ export class OrcaRuntimeWithSerializeTerminalBufferFromAvailableState extends Or
|
|||||||
kittyKeyboardFlags?: number
|
kittyKeyboardFlags?: number
|
||||||
terminalOwner?: 'shell'
|
terminalOwner?: 'shell'
|
||||||
} | null> {
|
} | null> {
|
||||||
const restoredSnapshot = await this.serializePreferredRestoredTerminalBuffer(ptyId, opts)
|
const restoredSnapshot = await this.serializePreferredRestoredTerminalBuffer(ptyId, opts, {
|
||||||
|
seedModelFromRenderer: true
|
||||||
|
})
|
||||||
if (restoredSnapshot) {
|
if (restoredSnapshot) {
|
||||||
return restoredSnapshot
|
return restoredSnapshot
|
||||||
}
|
}
|
||||||
@@ -32,7 +34,7 @@ export class OrcaRuntimeWithSerializeTerminalBufferFromAvailableState extends Or
|
|||||||
return headlessSnapshot
|
return headlessSnapshot
|
||||||
}
|
}
|
||||||
|
|
||||||
const rendererSnapshot = await this.serializeRendererTerminalBuffer(ptyId, opts)
|
const rendererSnapshot = await this.serializeRendererScreenOntoModel(ptyId, opts)
|
||||||
if (!rendererSnapshot) {
|
if (!rendererSnapshot) {
|
||||||
return this.serializeProviderTerminalBuffer(ptyId, opts)
|
return this.serializeProviderTerminalBuffer(ptyId, opts)
|
||||||
}
|
}
|
||||||
@@ -51,7 +53,8 @@ export class OrcaRuntimeWithSerializeTerminalBufferFromAvailableState extends Or
|
|||||||
|
|
||||||
protected async serializePreferredRestoredTerminalBuffer(
|
protected async serializePreferredRestoredTerminalBuffer(
|
||||||
ptyId: string,
|
ptyId: string,
|
||||||
opts: { scrollbackRows?: number } = {}
|
opts: { scrollbackRows?: number } = {},
|
||||||
|
fallback: { seedModelFromRenderer?: boolean } = {}
|
||||||
) {
|
) {
|
||||||
if (!this.providerSnapshotPreferredPtys.has(ptyId)) {
|
if (!this.providerSnapshotPreferredPtys.has(ptyId)) {
|
||||||
return null
|
return null
|
||||||
@@ -59,10 +62,27 @@ export class OrcaRuntimeWithSerializeTerminalBufferFromAvailableState extends Or
|
|||||||
// Pre-attach bytes are only a suffix; older providers can fall back to the renderer.
|
// Pre-attach bytes are only a suffix; older providers can fall back to the renderer.
|
||||||
return (
|
return (
|
||||||
(await this.serializeProviderTerminalBuffer(ptyId, opts)) ??
|
(await this.serializeProviderTerminalBuffer(ptyId, opts)) ??
|
||||||
(await this.serializeRendererTerminalBuffer(ptyId, opts))
|
(await (fallback.seedModelFromRenderer
|
||||||
|
? this.serializeRendererScreenOntoModel(ptyId, opts)
|
||||||
|
: this.serializeRendererTerminalBuffer(ptyId, opts)))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Why: a pane answers at its own size; seed the model onto the PTY grid and serve that instead.
|
||||||
|
protected async serializeRendererScreenOntoModel(
|
||||||
|
ptyId: string,
|
||||||
|
opts: { scrollbackRows?: number }
|
||||||
|
) {
|
||||||
|
const outputSequence = this.getPtyOutputSequence(ptyId)
|
||||||
|
const screen = await this.serializeRendererTerminalBuffer(ptyId, opts)
|
||||||
|
// Why: output that landed during the read is in neither the screen nor a fresh model.
|
||||||
|
if (!screen?.data.length || this.getPtyOutputSequence(ptyId) !== outputSequence) {
|
||||||
|
return screen
|
||||||
|
}
|
||||||
|
await this.replaceHeadlessTerminalFromRendererSnapshotForRecovery(ptyId, screen)
|
||||||
|
return (await this.serializeHeadlessTerminalBuffer(ptyId, opts)) ?? screen
|
||||||
|
}
|
||||||
|
|
||||||
async serializeRendererTerminalBuffer(
|
async serializeRendererTerminalBuffer(
|
||||||
ptyId: string,
|
ptyId: string,
|
||||||
opts: { scrollbackRows?: number } = {}
|
opts: { scrollbackRows?: number } = {}
|
||||||
|
|||||||
@@ -52,7 +52,8 @@ describe('terminal side-effect fact channel', () => {
|
|||||||
runtime.ingestSyntheticTitleFrame('pty-1', '\x1b]0;⠋ Cursor Agent\x07')
|
runtime.ingestSyntheticTitleFrame('pty-1', '\x1b]0;⠋ Cursor Agent\x07')
|
||||||
|
|
||||||
const snapshot = await runtime.serializeTerminalBuffer('pty-1', { scrollbackRows: 10 })
|
const snapshot = await runtime.serializeTerminalBuffer('pty-1', { scrollbackRows: 10 })
|
||||||
expect(snapshot?.source).toBe('renderer')
|
// The renderer screen is seeded into the host model, which then answers.
|
||||||
|
expect(snapshot?.source).toBe('headless')
|
||||||
expect(snapshot?.lastTitle).toBe('⠋ Cursor Agent')
|
expect(snapshot?.lastTitle).toBe('⠋ Cursor Agent')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* With no host model, a terminal buffer read falls back to the desktop pane. A hidden pane answers
|
||||||
|
* at its own size, so the read must seed the model onto the PTY grid and answer from it. This read
|
||||||
|
* backs the phone's resize restream and pending-output recovery.
|
||||||
|
*/
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import {
|
||||||
|
DESKTOP,
|
||||||
|
EXPECTED_PHONE_ROWS,
|
||||||
|
PHONE,
|
||||||
|
PTY_ID,
|
||||||
|
internals,
|
||||||
|
paintedRows,
|
||||||
|
setupPhoneSubscribe
|
||||||
|
} from './mobile-phone-subscribe-test-fixture'
|
||||||
|
|
||||||
|
async function readOnPhoneGrid(opts: { providerPreferred: boolean }) {
|
||||||
|
const { runtime, sizes } = setupPhoneSubscribe({ paneMounted: true })
|
||||||
|
sizes.set(PTY_ID, { ...PHONE })
|
||||||
|
if (opts.providerPreferred) {
|
||||||
|
internals(runtime).providerSnapshotPreferredPtys.add(PTY_ID)
|
||||||
|
}
|
||||||
|
const snapshot = await runtime.serializeTerminalBuffer(PTY_ID, { scrollbackRows: 100 })
|
||||||
|
return { runtime, snapshot }
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('terminal buffer read with no host model and a desktop-sized pane', () => {
|
||||||
|
it('answers from a model seeded onto the PTY grid', async () => {
|
||||||
|
const { runtime, snapshot } = await readOnPhoneGrid({ providerPreferred: false })
|
||||||
|
|
||||||
|
expect(snapshot).toMatchObject({ source: 'headless', ...PHONE })
|
||||||
|
expect(snapshot && (await paintedRows(snapshot)).slice(0, 4)).toEqual(EXPECTED_PHONE_ROWS)
|
||||||
|
expect(runtime.hasHeadlessTerminalState(PTY_ID)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('answers from the model when a restored snapshot is preferred but the provider has none', async () => {
|
||||||
|
const { snapshot } = await readOnPhoneGrid({ providerPreferred: true })
|
||||||
|
|
||||||
|
expect(snapshot).toMatchObject({ source: 'headless', ...PHONE })
|
||||||
|
expect(snapshot?.cols).not.toBe(DESKTOP.cols)
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user