mirror of
https://github.com/stablyai/orca.git
synced 2026-10-10 16:02:53 +00:00
fix(runtime): join a renderer seed the resize repaint already started
A TUI answering the phone fit's SIGWINCH can deliver a byte before the subscribe reaches its hydrate. That byte runs the same renderer seed, so the subscribe saw "already hydrating", fell back to adopting the pane's desktop-sized screen and seeded the model at desktop size again. Keep the model's renderer seed beside it so the subscribe joins that result. Claude-Session: lane-host-hydrate
This commit is contained in:
@@ -48,12 +48,19 @@ async function screenOn(grid: { cols: number; rows: number }): Promise<string> {
|
||||
}
|
||||
}
|
||||
|
||||
function setup(opts: { paneMounted: boolean; providerSnapshot?: boolean }) {
|
||||
function setup(opts: {
|
||||
paneMounted: boolean
|
||||
providerSnapshot?: boolean
|
||||
repaintOnResize?: boolean
|
||||
}) {
|
||||
const sizes = new Map([[PTY_ID, { ...DESKTOP }]])
|
||||
const serializeBuffer = vi.fn(async () =>
|
||||
opts.paneMounted ? { data: await screenOn(DESKTOP), ...DESKTOP } : null
|
||||
)
|
||||
const runtime = new OrcaRuntimeService()
|
||||
// The pane orders its screen against PTY output, as a mounted desktop xterm does.
|
||||
const serializeBuffer = vi.fn(async () =>
|
||||
opts.paneMounted
|
||||
? { data: await screenOn(DESKTOP), ...DESKTOP, seq: runtime.getPtyOutputSequence(PTY_ID) }
|
||||
: null
|
||||
)
|
||||
runtime.setPtyController({
|
||||
write: () => true,
|
||||
kill: () => true,
|
||||
@@ -61,6 +68,10 @@ function setup(opts: { paneMounted: boolean; providerSnapshot?: boolean }) {
|
||||
getSize: (ptyId: string) => sizes.get(ptyId) ?? null,
|
||||
resize: (ptyId: string, cols: number, rows: number) => {
|
||||
sizes.set(ptyId, { cols, rows })
|
||||
if (opts.repaintOnResize) {
|
||||
// A TUI answering SIGWINCH before the subscribe reaches its own hydrate.
|
||||
runtime.onPtyData(ptyId, '\x1b[?25h', Date.now())
|
||||
}
|
||||
return true
|
||||
},
|
||||
hasRendererSerializer: () => opts.paneMounted,
|
||||
@@ -178,6 +189,16 @@ describe('phone subscribe to an idle PTY whose hidden pane sits at desktop size'
|
||||
expect({ cols: resubscribed.cols, rows: resubscribed.rows }).toEqual(PHONE)
|
||||
})
|
||||
|
||||
it('joins a hydrate the resize repaint already started', async () => {
|
||||
const { runtime, handle, serializeBuffer } = setup({ paneMounted: true, repaintOnResize: true })
|
||||
|
||||
const snapshot = await firstSnapshot(runtime, handle)
|
||||
|
||||
expect({ cols: snapshot.cols, rows: snapshot.rows }).toEqual(PHONE)
|
||||
expect((await paintedRows(snapshot)).slice(0, 4)).toEqual(EXPECTED_PHONE_ROWS)
|
||||
expect(serializeBuffer).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('keeps serving the model on resubscribe without re-reading the pane', async () => {
|
||||
const { runtime, handle, serializeBuffer } = setup({ paneMounted: true })
|
||||
await firstSnapshot(runtime, handle)
|
||||
|
||||
@@ -130,6 +130,8 @@ export class OrcaRuntimeWithFitOverrideListeners extends OrcaRuntimeWithStopRequ
|
||||
// Absent → hydration has not been considered yet for this PTY.
|
||||
// See docs/mobile-prefer-renderer-scrollback.md.
|
||||
protected headlessHydrationState = new Map<string, 'pending' | 'done'>()
|
||||
/** The model's renderer seed, resolving true once written; lives as long as the model. */
|
||||
protected rendererSeedsByPtyId = new Map<string, Promise<boolean>>()
|
||||
|
||||
// Why: mobile-fit overrides are keyed by ptyId (not terminal handle) because
|
||||
// handles can be reissued while the PTY identity is stable. In-memory only —
|
||||
|
||||
@@ -110,13 +110,20 @@ export class OrcaRuntimeWithMaybeHydrateHeadlessFromRenderer extends OrcaRuntime
|
||||
}
|
||||
}
|
||||
})
|
||||
return state.writeChain.then(() => seeded)
|
||||
const seed = state.writeChain.then(() => seeded)
|
||||
this.rendererSeedsByPtyId.set(ptyId, seed)
|
||||
return seed
|
||||
}
|
||||
|
||||
/** Public: a phone fit resizes the PTY, but an idle TUI sends no byte to hydrate on. Resolves
|
||||
* true once the model holds the pane's screen reflowed onto the PTY's current grid. */
|
||||
hydrateHeadlessTerminalFromRenderer(ptyId: string): Promise<boolean> {
|
||||
return this.maybeHydrateHeadlessFromRenderer(ptyId) ?? Promise.resolve(false)
|
||||
// Why join: a resize repaint can run the on-data seed first; its result answers this caller too.
|
||||
return (
|
||||
this.maybeHydrateHeadlessFromRenderer(ptyId) ??
|
||||
this.rendererSeedsByPtyId.get(ptyId) ??
|
||||
Promise.resolve(false)
|
||||
)
|
||||
}
|
||||
|
||||
// Why: seed-derived agent status reflects historical state. Orchestration
|
||||
|
||||
@@ -75,6 +75,7 @@ export class OrcaRuntimeWithSerializeHeadlessTerminalBuffer extends OrcaRuntimeW
|
||||
|
||||
protected disposeHeadlessTerminal(ptyId: string): void {
|
||||
this.headlessHydrationState.delete(ptyId)
|
||||
this.rendererSeedsByPtyId.delete(ptyId)
|
||||
const state = this.headlessTerminals.get(ptyId)
|
||||
if (!state) {
|
||||
return
|
||||
|
||||
@@ -41,6 +41,10 @@ const CLEARED_BY_REAPER_HELPER: Record<string, { helper: string; module: string
|
||||
agentPromptPermissionSequenceByPtyId: {
|
||||
helper: 'advancePtyLifecycleGeneration',
|
||||
module: 'src/main/runtime/orca-runtime-record-agent-prompt-lifecycle-state.ts'
|
||||
},
|
||||
rendererSeedsByPtyId: {
|
||||
helper: 'disposeHeadlessTerminal',
|
||||
module: 'src/main/runtime/orca-runtime-serialize-headless-terminal-buffer.ts'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user