mirror of
https://github.com/stablyai/orca.git
synced 2026-10-09 16:02:46 +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
|
||||
terminalOwner?: 'shell'
|
||||
} | null> {
|
||||
const restoredSnapshot = await this.serializePreferredRestoredTerminalBuffer(ptyId, opts)
|
||||
const restoredSnapshot = await this.serializePreferredRestoredTerminalBuffer(ptyId, opts, {
|
||||
seedModelFromRenderer: true
|
||||
})
|
||||
if (restoredSnapshot) {
|
||||
return restoredSnapshot
|
||||
}
|
||||
@@ -32,7 +34,7 @@ export class OrcaRuntimeWithSerializeTerminalBufferFromAvailableState extends Or
|
||||
return headlessSnapshot
|
||||
}
|
||||
|
||||
const rendererSnapshot = await this.serializeRendererTerminalBuffer(ptyId, opts)
|
||||
const rendererSnapshot = await this.serializeRendererScreenOntoModel(ptyId, opts)
|
||||
if (!rendererSnapshot) {
|
||||
return this.serializeProviderTerminalBuffer(ptyId, opts)
|
||||
}
|
||||
@@ -51,7 +53,8 @@ export class OrcaRuntimeWithSerializeTerminalBufferFromAvailableState extends Or
|
||||
|
||||
protected async serializePreferredRestoredTerminalBuffer(
|
||||
ptyId: string,
|
||||
opts: { scrollbackRows?: number } = {}
|
||||
opts: { scrollbackRows?: number } = {},
|
||||
fallback: { seedModelFromRenderer?: boolean } = {}
|
||||
) {
|
||||
if (!this.providerSnapshotPreferredPtys.has(ptyId)) {
|
||||
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.
|
||||
return (
|
||||
(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(
|
||||
ptyId: string,
|
||||
opts: { scrollbackRows?: number } = {}
|
||||
|
||||
@@ -52,7 +52,8 @@ describe('terminal side-effect fact channel', () => {
|
||||
runtime.ingestSyntheticTitleFrame('pty-1', '\x1b]0;⠋ Cursor Agent\x07')
|
||||
|
||||
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')
|
||||
})
|
||||
|
||||
|
||||
@@ -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