mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
Main seeds its terminal model from the renderer's xterm and queues live chunks behind the seed. The renderer keeps parsing those chunks while it serializes, so they were in the seed and then written again, doubling TUI frames and splitting escapes (literal ';242m'). Hidden-pane reveals repaint from this model. Skip queued chunks at or below the seed's seq and carry any unfinished escape into the next chunk.
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
import { expect, it } from 'vitest'
|
||||
import { deferred } from './orca-runtime-test-fixtures.spec'
|
||||
import { createHydrationRuntime, PTY_ID, SIZE } from './headless-hydration-ownership-test-fixture'
|
||||
|
||||
it('does not replay live bytes the renderer seed already painted', async () => {
|
||||
const runtime = createHydrationRuntime()
|
||||
const rendered = deferred<{ data: string; cols: number; rows: number; seq: number }>()
|
||||
runtime.setPtyController({
|
||||
write: () => true,
|
||||
kill: () => true,
|
||||
getForegroundProcess: async () => null,
|
||||
getSize: () => SIZE,
|
||||
hasRendererSerializer: () => true,
|
||||
serializeBuffer: () => rendered.promise
|
||||
})
|
||||
|
||||
// The renderer parses these while main awaits its serialization.
|
||||
runtime.onPtyData(PTY_ID, 'LIVE-ONE\r\n', 1)
|
||||
runtime.onPtyData(PTY_ID, '\x1b[38;5', 2)
|
||||
runtime.onPtyData(PTY_ID, ';242mLIVE-TWO\x1b[0m\r\n', 3)
|
||||
rendered.resolve({ data: 'HISTORY\r\nLIVE-ONE\r\n', ...SIZE, seq: 16 })
|
||||
await runtime.model().writeChain
|
||||
|
||||
const screen = runtime.model().emulator.getVisibleLines().join('\n')
|
||||
expect(screen.match(/LIVE-ONE/g)).toHaveLength(1)
|
||||
expect(screen).toContain('LIVE-TWO')
|
||||
expect(screen).not.toContain(';242m')
|
||||
})
|
||||
@@ -158,6 +158,10 @@ export type RuntimeHeadlessTerminal = {
|
||||
outputSequence: number
|
||||
writeChain: Promise<void>
|
||||
ownership: PtyShellOwnershipMirror
|
||||
/** Output seq a renderer hydration seed already painted; queued live chunks at/below it are not rewritten. */
|
||||
rendererSeedSeq?: number
|
||||
/** Unfinished escape from skipped seed-covered chunks, written before the next uncovered chunk. */
|
||||
rendererSeedEscapeTail?: string
|
||||
/** The grid a reattach reflowed the model onto, until a PTY resize off it repaints the TUI. */
|
||||
unrepaintedReflowGrid?: { cols: number; rows: number }
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
normalizeTerminalTitle
|
||||
} from '../../../../shared/agent-detection'
|
||||
import { shouldModelAnswerHiddenPtyQueries } from '../../terminal-model-query-authority'
|
||||
import { advancePartialEscapeTail } from '../../../../shared/terminal-partial-escape-tail'
|
||||
|
||||
export abstract class OrcaRuntimeWithMaybeHydrateHeadlessFromRenderer extends OrcaRuntimeWithSerializeMainTerminalBuffer {
|
||||
protected abstract getOrCreateHeadlessTerminal(ptyId: string): RuntimeHeadlessTerminal
|
||||
@@ -84,6 +85,10 @@ export abstract class OrcaRuntimeWithMaybeHydrateHeadlessFromRenderer extends Or
|
||||
if (this.headlessTerminals.get(ptyId) !== state) {
|
||||
return
|
||||
}
|
||||
// Why: the renderer parsed live bytes queued behind this seed while serializing; writing them again doubles TUI frames and splits escapes.
|
||||
if (typeof rendered.seq === 'number') {
|
||||
state.rendererSeedSeq = rendered.seq
|
||||
}
|
||||
const ptyDims = this.getTerminalSize(ptyId)
|
||||
if (ptyDims && (ptyDims.cols !== rendered.cols || ptyDims.rows !== rendered.rows)) {
|
||||
state.emulator.resize(ptyDims.cols, ptyDims.rows)
|
||||
@@ -187,6 +192,22 @@ export abstract class OrcaRuntimeWithMaybeHydrateHeadlessFromRenderer extends Or
|
||||
const completion = state.writeChain.then(async () => {
|
||||
// Capture reply ownership at ingestion, then scan inside the chain so live bytes and seeds retain total order.
|
||||
state.ownership.scan(data)
|
||||
if (state.rendererSeedSeq !== undefined) {
|
||||
if (outputSequence <= state.rendererSeedSeq) {
|
||||
state.rendererSeedEscapeTail = advancePartialEscapeTail(
|
||||
state.rendererSeedEscapeTail ?? '',
|
||||
data
|
||||
)
|
||||
state.outputSequence = outputSequence
|
||||
return
|
||||
}
|
||||
const escapeTail = state.rendererSeedEscapeTail
|
||||
state.rendererSeedSeq = undefined
|
||||
state.rendererSeedEscapeTail = undefined
|
||||
if (escapeTail) {
|
||||
await state.emulator.write(escapeTail)
|
||||
}
|
||||
}
|
||||
for (const chunk of splitFreebuffScreenUpdates(data, state.emulator.partialEscapeTailAnsi)) {
|
||||
await state.emulator.write(chunk, { forwardQueryReplies })
|
||||
const pty = this.ptysById.get(ptyId)
|
||||
|
||||
Reference in New Issue
Block a user