Revert "fix(tui): merged scrollback offer boundary repair"
This reverts commitae89b3ff08, reversing changes made to227874dcc6.
This commit is contained in:
@@ -51,6 +51,12 @@
|
||||
- Fixed models.yml schema validation to surface warnings for invalid custom provider configurations instead of silently ignoring them
|
||||
- Fixed potential network hangs in omp update, Hindsight recall, and Smithery registry lookups by adding fetch timeouts
|
||||
- Reduced TUI CPU overhead during streaming and idle waits by dropping the redundant pre-render on every session event, iterating shimmer text in place instead of allocating a code-point array per animation frame, and coalescing the live-tool spinner render cadence to its glyph-advance rate ([#4353](https://github.com/can1357/oh-my-pi/issues/4353)).
|
||||
- Improved reliability of edits when file snapshots share identical 16-bit hash tags
|
||||
- Fixed ACP `terminal/create` sending the bash tool's full shell line in `command` with no `args`, which broke spec-conformant clients that spawn `command`+`args` directly (no implicit shell) — any command containing a space, pipe, `&&`, redirect, or `$(...)` failed with `ENOENT` and the agent silently degraded to read-only tools. The bash tool now wraps the shell line before calling `clientBridge.createTerminal`, reusing the same shell binary + args the local `bash-executor` resolves via `settings.getShellConfig()` (Git Bash / `bash.exe` on Windows, `$SHELL` with `sh` fallback on POSIX) so bash semantics — `$VAR`, `$(...)`, `source`, POSIX quoting, `-l` — are preserved on both platforms. ([#4333](https://github.com/can1357/oh-my-pi/issues/4333))
|
||||
- Fixed inference worker subprocesses (TTS, STT, tiny-model, mnemopi embeddings) discarding stderr, which left every unexpected exit — most visibly the local Kokoro TTS worker's recurring `exit code 7` crash loop — undiagnosable from the parent's logs. `createWorkerSubprocess` now pipes stderr without starting a live read while the worker is idle, then drains the stream after `onExit`, emits captured lines to `logger.debug` under an `<exitLabel> stderr` message, and keeps the last 16 KiB in a bounded ring that gets appended to the `Error` surfaced through `onError`. The exit surface is synchronized with the post-exit drain via `SpawnedSubprocess.stderrDrained`, so the full native trace shows up on the `tts: worker error` line without reintroducing event-loop liveness from unref'd workers. ([#4324](https://github.com/can1357/oh-my-pi/issues/4324))
|
||||
### Fixed
|
||||
|
||||
- Fixed Windows session tail loss after atomic compaction rewrites by fencing append writers during full-file replacement and gating the atomic publish on a `commitGuard` that the storage backend checks synchronously before rename, so a concurrent `flushSync` (Ctrl+C / session-exit) is not overwritten by the stale body serialized before it ran. Covers post-compaction prompts, tool results, title changes, and exit diagnostics on the current JSONL path ([#4338](https://github.com/can1357/oh-my-pi/issues/4338)).
|
||||
|
||||
## [16.3.2] - 2026-07-02
|
||||
|
||||
|
||||
@@ -443,10 +443,6 @@ export class TranscriptContainer
|
||||
// drift after commit; the engine commits them audit-exempt. Provisional
|
||||
// (commit-unstable) blocks never extend it.
|
||||
#nativeScrollbackSnapshotSafeEnd: number | undefined;
|
||||
// Local line index through which lower finalized siblings are safe to OFFER to
|
||||
// native scrollback while still audited. Unlike snapshotSafeEnd, rows below a
|
||||
// live block are not durable: growth above them must repair stale history.
|
||||
#nativeScrollbackOfferSafeEnd: number | undefined;
|
||||
// Persistent assembled transcript rows. Rows before the stable floor are
|
||||
// byte-identical to the previous render; rows at/after it were re-pushed.
|
||||
#lines: string[] = [];
|
||||
@@ -495,10 +491,6 @@ export class TranscriptContainer
|
||||
return this.#nativeScrollbackSnapshotSafeEnd;
|
||||
}
|
||||
|
||||
getNativeScrollbackOfferSafeEnd(): number | undefined {
|
||||
return this.#nativeScrollbackOfferSafeEnd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether `component` sits below a still-mutating block — i.e. inside the
|
||||
* live region, where its rows cannot have been committed to native
|
||||
@@ -591,7 +583,6 @@ export class TranscriptContainer
|
||||
this.#nativeScrollbackLiveRegionStart = undefined;
|
||||
this.#nativeScrollbackCommitSafeEnd = undefined;
|
||||
this.#nativeScrollbackSnapshotSafeEnd = undefined;
|
||||
this.#nativeScrollbackOfferSafeEnd = undefined;
|
||||
|
||||
const count = this.children.length;
|
||||
|
||||
@@ -638,14 +629,6 @@ export class TranscriptContainer
|
||||
// liveStartIndex; empty leading blocks (or a separator) must not claim it
|
||||
// early.
|
||||
let liveRecorded = false;
|
||||
// Prefix boundary for finalized siblings rendered below the first live
|
||||
// block. These rows may be offered to native scrollback, but they cannot
|
||||
// extend snapshotSafeEnd because a live block above can still move them.
|
||||
let offerSafeEnd: number | undefined;
|
||||
// Offer rows must be a contiguous finalized run below the first live
|
||||
// block. A later live/provisional block can still push rows below it, so
|
||||
// finalized siblings after that barrier must stay forced-overflow.
|
||||
let offerSafeOpen = true;
|
||||
// Frame row cursor: rows emitted (reused or pushed) so far.
|
||||
let row = 0;
|
||||
let stableRows = 0;
|
||||
@@ -718,7 +701,6 @@ export class TranscriptContainer
|
||||
// everything below it.
|
||||
if (contribution.length === 0) {
|
||||
if (i >= liveStartIndex && commitSafeOpen && !finalized) commitSafeOpen = false;
|
||||
if (i > liveStartIndex && !finalized) offerSafeOpen = false;
|
||||
if (chainStable && !(reusable && previous.rowCount === 0 && previous.startRow === row)) {
|
||||
chainStable = false;
|
||||
lines.length = row;
|
||||
@@ -788,13 +770,6 @@ export class TranscriptContainer
|
||||
// rows around as it grows, so the run closes there.
|
||||
if (!(finalized && safeLength >= contribution.length)) commitSafeOpen = false;
|
||||
}
|
||||
if (i > liveStartIndex) {
|
||||
if (offerSafeOpen && finalized) {
|
||||
offerSafeEnd = blockStart + contribution.length;
|
||||
} else if (!finalized) {
|
||||
offerSafeOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
segments[i] = {
|
||||
component: child,
|
||||
@@ -813,7 +788,6 @@ export class TranscriptContainer
|
||||
// Trailing shrink: blocks removed from the tail leave stale rows behind
|
||||
// when every surviving segment was reused.
|
||||
if (lines.length !== row) lines.length = row;
|
||||
this.#nativeScrollbackOfferSafeEnd = offerSafeEnd;
|
||||
this.#segments = segments;
|
||||
this.#stableRowsFloor = Math.min(stableFloorBefore, stableRows, row);
|
||||
return lines;
|
||||
|
||||
@@ -2,16 +2,6 @@ import { describe, expect, it } from "bun:test";
|
||||
import { TranscriptContainer } from "@oh-my-pi/pi-coding-agent/modes/components/transcript-container";
|
||||
import type { Component } from "@oh-my-pi/pi-tui";
|
||||
|
||||
class FinalizedBlock implements Component {
|
||||
#lines: string[];
|
||||
constructor(lines: string[]) {
|
||||
this.#lines = [...lines];
|
||||
}
|
||||
render(width: number): string[] {
|
||||
return this.#lines.map(line => line.slice(0, width));
|
||||
}
|
||||
}
|
||||
|
||||
class MutableLiveBlock implements Component {
|
||||
#lines: string[];
|
||||
constructor(lines: string[]) {
|
||||
@@ -44,49 +34,4 @@ describe("transcript streaming commit (assistant text)", () => {
|
||||
// off head must be committable to native scrollback (tmux pane history).
|
||||
expect(chat.getNativeScrollbackCommitSafeEnd()).toBe(3);
|
||||
});
|
||||
|
||||
it("offers lower finalized siblings without making them durable", () => {
|
||||
const chat = new TranscriptContainer();
|
||||
const top = new FinalizedBlock(["top-0"]);
|
||||
const live = new MutableLiveBlock(["live-0"]);
|
||||
const tail = new FinalizedBlock(["tail-0", "tail-1"]);
|
||||
chat.addChild(top);
|
||||
chat.addChild(live);
|
||||
chat.addChild(tail);
|
||||
|
||||
expect(chat.render(80)).toEqual(["top-0", "", "live-0", "", "tail-0", "tail-1"]);
|
||||
expect(chat.getNativeScrollbackLiveRegionStart()).toBe(2);
|
||||
expect(chat.getNativeScrollbackSnapshotSafeEnd()).toBe(3);
|
||||
expect(chat.getNativeScrollbackOfferSafeEnd()).toBe(6);
|
||||
});
|
||||
|
||||
it("stops offered siblings at an intervening live block", () => {
|
||||
const chat = new TranscriptContainer();
|
||||
const top = new FinalizedBlock(["top-0"]);
|
||||
const firstLive = new MutableLiveBlock(["live-a"]);
|
||||
const firstTail = new FinalizedBlock(["tail-a-0", "tail-a-1"]);
|
||||
const secondLive = new MutableLiveBlock(["live-b"]);
|
||||
const laterTail = new FinalizedBlock(["tail-b-0"]);
|
||||
chat.addChild(top);
|
||||
chat.addChild(firstLive);
|
||||
chat.addChild(firstTail);
|
||||
chat.addChild(secondLive);
|
||||
chat.addChild(laterTail);
|
||||
|
||||
expect(chat.render(80)).toEqual([
|
||||
"top-0",
|
||||
"",
|
||||
"live-a",
|
||||
"",
|
||||
"tail-a-0",
|
||||
"tail-a-1",
|
||||
"",
|
||||
"live-b",
|
||||
"",
|
||||
"tail-b-0",
|
||||
]);
|
||||
expect(chat.getNativeScrollbackLiveRegionStart()).toBe(2);
|
||||
expect(chat.getNativeScrollbackSnapshotSafeEnd()).toBe(3);
|
||||
expect(chat.getNativeScrollbackOfferSafeEnd()).toBe(6);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
- Fixed a native scrollback rendering bug where finalized transcript rows below an active block would duplicate when the active block expanded.
|
||||
- Fixed autocomplete popups remaining active with stale suggestions after destructive text editing (such as Ctrl+W, Ctrl+U, Ctrl+K, Alt+Backspace, Alt+D, paste, or yank), preventing input corruption when pressing Tab or Enter.
|
||||
- Skipped Markdown re-lex + re-wrap when `setText` receives the identical text, mirroring the equality guard on `Text.setText` — cuts one of the top streaming CPU hotspots when providers re-emit unchanged content ([#4353](https://github.com/can1357/oh-my-pi/issues/4353)).
|
||||
- Fixed the modifyOtherKeys keyboard fallback enabling on unknown SSH terminals, avoiding broken Shift input in iOS SSH clients such as Redock ([#4325](https://github.com/can1357/oh-my-pi/issues/4325)).
|
||||
|
||||
## [16.3.0] - 2026-07-02
|
||||
|
||||
|
||||
+29
-100
@@ -212,11 +212,6 @@ export interface OverlayFocusOwner {
|
||||
* dropped row or an audit re-anchor spray. Provisional live blocks (collapsing
|
||||
* tool/edit previews whose head is a throwaway tail window) omit it. Defaults to
|
||||
* `commitSafeEnd ?? liveRegionStart` when absent.
|
||||
* `getNativeScrollbackOfferSafeEnd` optionally reports the deepest prefix row
|
||||
* that may physically enter native scrollback while still remaining audited.
|
||||
* This is for finalized lower siblings under a live block: the rows may scroll
|
||||
* off, but a later live-block insertion above them must trigger repair instead
|
||||
* of becoming durable audit-exempt history.
|
||||
*
|
||||
* When several root children report a seam in the same frame, the topmost
|
||||
* one (and its commit-safe / snapshot-safe extension) defines the boundary:
|
||||
@@ -227,7 +222,6 @@ export interface NativeScrollbackLiveRegion {
|
||||
getNativeScrollbackLiveRegionStart(): number | undefined;
|
||||
getNativeScrollbackCommitSafeEnd?(): number | undefined;
|
||||
getNativeScrollbackSnapshotSafeEnd?(): number | undefined;
|
||||
getNativeScrollbackOfferSafeEnd?(): number | undefined;
|
||||
}
|
||||
|
||||
export interface NativeScrollbackCommittedRows {
|
||||
@@ -257,10 +251,6 @@ function getNativeScrollbackSnapshotSafeEnd(component: Component): number | unde
|
||||
return (component as Component & Partial<NativeScrollbackLiveRegion>).getNativeScrollbackSnapshotSafeEnd?.();
|
||||
}
|
||||
|
||||
function getNativeScrollbackOfferSafeEnd(component: Component): number | undefined {
|
||||
return (component as Component & Partial<NativeScrollbackLiveRegion>).getNativeScrollbackOfferSafeEnd?.();
|
||||
}
|
||||
|
||||
/**
|
||||
* Opt-in stability report for components that mutate their returned render
|
||||
* array in place across frames (instead of returning a fresh array per
|
||||
@@ -638,7 +628,6 @@ interface FrameSegment {
|
||||
liveLocalStart?: number;
|
||||
commitLocalEnd?: number;
|
||||
snapshotLocalEnd?: number;
|
||||
offerLocalEnd?: number;
|
||||
}
|
||||
|
||||
/** Depth-first identity search through `Container`-shaped children. */
|
||||
@@ -1020,27 +1009,23 @@ export class TUI extends Container {
|
||||
// #auditCommittedPrefix). Holds references to component-cached strings, so
|
||||
// the audit is a pointer walk in the common case.
|
||||
#committedPrefix: string[] = [];
|
||||
// The committed prefix [0, committedRows) splits into four zones by three
|
||||
// monotone marks auditRows ≤ durableRows ≤ offerRows ≤ committedRows:
|
||||
// The committed prefix [0, committedRows) splits into three audit zones by
|
||||
// two monotone marks auditRows ≤ durableRows ≤ committedRows:
|
||||
// [0, auditRows) BYTE-STABLE — audited (re-anchor on any shift).
|
||||
// [auditRows, durableRows) DURABLE snapshot — exempt: rows may drift in
|
||||
// place (a streaming table widening) without re-anchoring, so their
|
||||
// expected drift never sprays duplicate snapshots.
|
||||
// [durableRows, offerRows) OFFERED — audited: rows were allowed into
|
||||
// native scrollback while a live block above could still shift them.
|
||||
// A mismatch here requires a destructive replay, not a duplicate tail.
|
||||
// [offerRows, committedRows) FORCED-overflow — audited: rows committed
|
||||
// [durableRows, committedRows) FORCED-overflow — audited: rows committed
|
||||
// only because they scrolled above the window under a commit-unstable
|
||||
// barrier; auditing them re-anchors (duplication, never loss) when the
|
||||
// barrier later shifts/finalizes/removes, instead of stranding a stale
|
||||
// prefix that silently drops the rows beneath it.
|
||||
// Marks re-base on a wholesale re-slice (full paint / shrink / geometry) and
|
||||
// otherwise advance per the persistence rules in #updateCommittedAuditRows.
|
||||
// Both marks re-base on a wholesale re-slice (full paint / shrink / geometry)
|
||||
// and otherwise advance per the persistence rules in #updateCommittedAuditRows.
|
||||
// #auditCommittedPrefix audits [0, committedRows) skipping the exempt window
|
||||
// [auditRows, durableRows).
|
||||
#committedPrefixAuditRows = 0;
|
||||
#committedPrefixDurableRows = 0;
|
||||
#committedPrefixOfferRows = 0;
|
||||
// Frame row currently mapped to screen row 0. Monotonic between full
|
||||
// paints: a shrink never re-exposes scrolled-off rows (they cannot be
|
||||
// un-scrolled without rewriting history); live rows repaint at fixed
|
||||
@@ -1051,7 +1036,6 @@ export class TUI extends Container {
|
||||
#nativeScrollbackLiveRegionStart: number | undefined;
|
||||
#nativeScrollbackCommitSafeEnd: number | undefined;
|
||||
#nativeScrollbackSnapshotSafeEnd: number | undefined;
|
||||
#nativeScrollbackOfferSafeEnd: number | undefined;
|
||||
#fullRedrawCount = 0;
|
||||
// Caps how many inline images render as live graphics; older ones fall back
|
||||
// to text via a purge + full redraw. Cap is configured by the host app.
|
||||
@@ -1171,7 +1155,6 @@ export class TUI extends Container {
|
||||
this.#nativeScrollbackLiveRegionStart = undefined;
|
||||
this.#nativeScrollbackCommitSafeEnd = undefined;
|
||||
this.#nativeScrollbackSnapshotSafeEnd = undefined;
|
||||
this.#nativeScrollbackOfferSafeEnd = undefined;
|
||||
const children = this.children;
|
||||
const previousSegments = this.#frameSegments;
|
||||
const segments: FrameSegment[] = new Array(children.length);
|
||||
@@ -1194,14 +1177,12 @@ export class TUI extends Container {
|
||||
let liveLocalStart: number | undefined;
|
||||
let commitLocalEnd: number | undefined;
|
||||
let snapshotLocalEnd: number | undefined;
|
||||
let offerLocalEnd: number | undefined;
|
||||
let reported: number | undefined;
|
||||
if (reuse) {
|
||||
childLines = previous.lines;
|
||||
liveLocalStart = previous.liveLocalStart;
|
||||
commitLocalEnd = previous.commitLocalEnd;
|
||||
snapshotLocalEnd = previous.snapshotLocalEnd;
|
||||
offerLocalEnd = previous.offerLocalEnd;
|
||||
} else {
|
||||
// Feed the engine's committed-row claim (from the previous frame's
|
||||
// emit) before rendering so the child can skip re-deriving blocks
|
||||
@@ -1230,13 +1211,6 @@ export class TUI extends Container {
|
||||
? Math.max(snapshotFloor, Math.min(childLines.length, Math.trunc(snapshotSafeEnd)))
|
||||
: childLines.length;
|
||||
}
|
||||
const offerSafeEnd = getNativeScrollbackOfferSafeEnd(child);
|
||||
if (offerSafeEnd !== undefined) {
|
||||
const offerFloor = snapshotLocalEnd ?? commitLocalEnd ?? liveLocalStart;
|
||||
offerLocalEnd = Number.isFinite(offerSafeEnd)
|
||||
? Math.max(offerFloor, Math.min(childLines.length, Math.trunc(offerSafeEnd)))
|
||||
: childLines.length;
|
||||
}
|
||||
}
|
||||
// Consume the stability report unconditionally for implementers:
|
||||
// reading re-bases the component's baseline to the state this
|
||||
@@ -1260,9 +1234,6 @@ export class TUI extends Container {
|
||||
if (snapshotLocalEnd !== undefined) {
|
||||
this.#nativeScrollbackSnapshotSafeEnd = offset + snapshotLocalEnd;
|
||||
}
|
||||
if (offerLocalEnd !== undefined) {
|
||||
this.#nativeScrollbackOfferSafeEnd = offset + offerLocalEnd;
|
||||
}
|
||||
}
|
||||
if (chainStable) {
|
||||
if (previous !== undefined && previous.component === child && previous.start === offset) {
|
||||
@@ -1293,7 +1264,6 @@ export class TUI extends Container {
|
||||
liveLocalStart,
|
||||
commitLocalEnd,
|
||||
snapshotLocalEnd,
|
||||
offerLocalEnd,
|
||||
};
|
||||
offset += childLines.length;
|
||||
}
|
||||
@@ -2694,7 +2664,6 @@ export class TUI extends Container {
|
||||
const liveRegionStart = this.#nativeScrollbackLiveRegionStart;
|
||||
const commitSafeEnd = this.#nativeScrollbackCommitSafeEnd;
|
||||
const snapshotSafeEnd = this.#nativeScrollbackSnapshotSafeEnd;
|
||||
const offerSafeEnd = this.#nativeScrollbackOfferSafeEnd;
|
||||
|
||||
// Commit boundaries (also used by the window/commit math in section 3),
|
||||
// hoisted above the audit gate because the resync needs byteStableBoundary
|
||||
@@ -2702,27 +2671,22 @@ export class TUI extends Container {
|
||||
// The commit floor is windowTop in every non-frozen path (see chunkTo), so
|
||||
// whatever scrolls above the window is committed — never committed nowhere
|
||||
// AND painted nowhere (the loss bug). The boundaries no longer gate the
|
||||
// commit; they define audit and repair spans. byteStableBoundary: rows below
|
||||
// commit; they define the audit-exempt span. byteStableBoundary: rows below
|
||||
// it are byte-stable (never re-layout), audited. durableBoundary: rows in
|
||||
// [byteStableBoundary, durableBoundary) are durable — permanent on scroll-off
|
||||
// but may drift in place (a streaming table re-aligning), committed
|
||||
// audit-EXEMPT. offerBoundary: rows in [durableBoundary, offerBoundary)
|
||||
// were explicitly allowed to enter native scrollback while remaining
|
||||
// audited; if they later shift, the stale physical history is repaired by a
|
||||
// destructive replay instead of duplicate recommit. Rows at/beyond
|
||||
// offerBoundary committed only because they scrolled above the window (a
|
||||
// commit-unstable barrier over a long tail) are forced-overflow rows:
|
||||
// audited, so a later shift/finalize/removal re-anchors (duplication, never
|
||||
// loss) instead of stranding a stale prefix. Built on the finalized prefix
|
||||
// (live-region start); the whole frame when the root reports no seam (shell
|
||||
// semantics: whatever scrolls is final).
|
||||
// audit-EXEMPT. Rows at/beyond durableBoundary committed only because they
|
||||
// scrolled above the window (a commit-unstable barrier over a long tail) are
|
||||
// forced-overflow rows: audited, so a later shift/finalize/removal re-anchors
|
||||
// (duplication, never loss) instead of stranding a stale prefix. Built on the
|
||||
// finalized prefix (live-region start); the whole frame when the root reports
|
||||
// no seam (shell semantics: whatever scrolls is final).
|
||||
const frameLength = rawFrame.length;
|
||||
const byteStableBoundary = Math.max(0, Math.min(frameLength, commitSafeEnd ?? liveRegionStart ?? frameLength));
|
||||
const durableBoundary = Math.max(
|
||||
byteStableBoundary,
|
||||
Math.min(frameLength, snapshotSafeEnd ?? byteStableBoundary),
|
||||
);
|
||||
const offerBoundary = Math.max(durableBoundary, Math.min(frameLength, offerSafeEnd ?? durableBoundary));
|
||||
|
||||
// 2. Transition state captured before any emitter runs.
|
||||
const prevWindowTop = this.#windowTopRow;
|
||||
@@ -2755,28 +2719,18 @@ export class TUI extends Container {
|
||||
let committedRowsResynced = false;
|
||||
// Audit covers [0, auditRows) and the forced suffix [durableRows,
|
||||
// committedRows); the durable middle [auditRows, durableRows) is exempt
|
||||
// (in-place drift). Three reasons to run the audit this frame:
|
||||
// (in-place drift). Two reasons to run the audit this frame:
|
||||
// - the stable prefix does not cover every audited row (auditUpper); or
|
||||
// - a forced-overflow row this frame became durable/permanent
|
||||
// (committedPrefixDurableRows < min(committed, durableBoundary)): the
|
||||
// barrier above it finalized, so its committed bytes must be re-checked
|
||||
// even though the stable prefix says nothing moved — a stale committed
|
||||
// copy there would silently drop the row; or
|
||||
// - a forced-overflow row this frame joined the offered zone
|
||||
// (committedPrefixOfferRows < min(committed, offerBoundary)): a
|
||||
// finalized sibling under a live block asks for destructive-replay
|
||||
// repair, so a single-row shift there must be caught even if
|
||||
// tail-sample tolerance would otherwise skip it.
|
||||
// The hard scan in findCommittedPrefixResync covers [durableRows,
|
||||
// hardAuditEnd) in full (no tail-sample miss).
|
||||
// (committedPrefixDurableRows < hardAuditEnd): the barrier above it
|
||||
// finalized, so its committed bytes must be re-checked even though the
|
||||
// stable prefix says nothing moved — a stale committed copy there would
|
||||
// silently drop the row. The hard scan in findCommittedPrefixResync
|
||||
// covers [durableRows, hardAuditEnd) in full (no tail-sample miss).
|
||||
const auditUpper =
|
||||
this.#committedPrefixDurableRows < this.#committedRows ? this.#committedRows : this.#committedPrefixAuditRows;
|
||||
const durableHardEnd = Math.min(this.#committedRows, durableBoundary);
|
||||
const offerHardEnd = Math.min(this.#committedRows, offerBoundary);
|
||||
const hardAuditEnd = Math.max(durableHardEnd, offerHardEnd);
|
||||
const needHardAudit =
|
||||
this.#committedPrefixDurableRows < durableHardEnd || this.#committedPrefixOfferRows < offerHardEnd;
|
||||
let repairOfferedScrollback = false;
|
||||
const hardAuditEnd = Math.min(this.#committedRows, durableBoundary);
|
||||
const needHardAudit = this.#committedPrefixDurableRows < hardAuditEnd;
|
||||
const auditRan =
|
||||
this.#hasEverRendered &&
|
||||
!geometryChanged &&
|
||||
@@ -2784,21 +2738,14 @@ export class TUI extends Container {
|
||||
(this.#renderStablePrefixRows < auditUpper || needHardAudit);
|
||||
if (auditRan) {
|
||||
const committedRowsBeforeAudit = this.#committedRows;
|
||||
const offeredRowsBeforeAudit = this.#committedPrefixOfferRows;
|
||||
const durableRowsBeforeAudit = this.#committedPrefixDurableRows;
|
||||
this.#auditCommittedPrefix(rawFrame, hardAuditEnd);
|
||||
this.#auditCommittedPrefix(rawFrame, durableBoundary);
|
||||
committedRowsResynced = this.#committedRows !== committedRowsBeforeAudit;
|
||||
repairOfferedScrollback =
|
||||
offeredRowsBeforeAudit > durableRowsBeforeAudit &&
|
||||
committedRowsResynced &&
|
||||
this.#committedRows < offeredRowsBeforeAudit;
|
||||
}
|
||||
// Committed-prefix state this frame's commit math extends from (post-audit).
|
||||
// Drives the audit-rows / durable-rows caps recomputed after the emit.
|
||||
const preCommitRows = this.#committedRows;
|
||||
const preCommitAuditRows = this.#committedPrefixAuditRows;
|
||||
const preCommitDurableRows = this.#committedPrefixDurableRows;
|
||||
const preCommitOfferRows = this.#committedPrefixOfferRows;
|
||||
|
||||
// 3. Window and commit math (lengths only; content prepared below).
|
||||
let hasVisibleOverlay = false;
|
||||
@@ -2816,7 +2763,7 @@ export class TUI extends Container {
|
||||
// place, because an ED3 rewrap is unsafe (pane scrollback / alt-screen
|
||||
// feedback loop), so committed history keeps its old wrap.
|
||||
const firstPaint = !this.#hasEverRendered;
|
||||
const replaceRequested = this.#clearScrollbackOnNextRender || repairOfferedScrollback;
|
||||
const replaceRequested = this.#clearScrollbackOnNextRender;
|
||||
const geometryRebuild = geometryChanged && !resizeRepaintsInPlace();
|
||||
const fullPaint = firstPaint || replaceRequested || geometryRebuild;
|
||||
let windowTop: number;
|
||||
@@ -2928,10 +2875,8 @@ export class TUI extends Container {
|
||||
preCommitRows,
|
||||
preCommitAuditRows,
|
||||
preCommitDurableRows,
|
||||
preCommitOfferRows,
|
||||
byteStableBoundary,
|
||||
durableBoundary,
|
||||
offerBoundary,
|
||||
false,
|
||||
);
|
||||
this.#clearScrollbackOnNextRender = false;
|
||||
@@ -2959,10 +2904,8 @@ export class TUI extends Container {
|
||||
preCommitRows,
|
||||
preCommitAuditRows,
|
||||
preCommitDurableRows,
|
||||
preCommitOfferRows,
|
||||
byteStableBoundary,
|
||||
durableBoundary,
|
||||
offerBoundary,
|
||||
auditRan,
|
||||
);
|
||||
}
|
||||
@@ -2989,7 +2932,6 @@ export class TUI extends Container {
|
||||
this.#committedRows = resyncTo;
|
||||
this.#committedPrefixAuditRows = Math.min(this.#committedPrefixAuditRows, resyncTo);
|
||||
this.#committedPrefixDurableRows = Math.min(this.#committedPrefixDurableRows, resyncTo);
|
||||
this.#committedPrefixOfferRows = Math.min(this.#committedPrefixOfferRows, resyncTo);
|
||||
prefix.length = resyncTo;
|
||||
if ($flag("PI_DEBUG_REDRAW")) {
|
||||
const msg = `[${new Date().toISOString()}] commit resync: committed prefix diverged at row ${resyncTo}; recommitting\n`;
|
||||
@@ -3002,24 +2944,21 @@ export class TUI extends Container {
|
||||
* #committedPrefixAuditRows field doc for the three audit zones).
|
||||
*
|
||||
* auditRows tracks the byte-stable boundary; durableRows the durable snapshot
|
||||
* boundary; offerRows the deepest explicit audited-offer boundary. A wholesale
|
||||
* re-slice (full paint / shrink / geometry) re-bases each mark from the
|
||||
* current frame. An incremental extend keeps a mark once a row past it has
|
||||
* committed (mark < committed): a later RISE in a boundary (a table finalizing)
|
||||
* must neither pull already-committed stale snapshots back under the
|
||||
* byte-stable cap nor retroactively exempt forced-overflow rows already
|
||||
* audited. durableRows is floored at auditRows; offerRows is floored at
|
||||
* durableRows.
|
||||
* boundary. A wholesale re-slice (full paint / shrink / geometry) re-bases
|
||||
* each mark from the current frame (min(committed, boundary)). An incremental
|
||||
* extend keeps a mark once a row past it has committed (mark < committed): a
|
||||
* later RISE in a boundary (a table finalizing) must neither pull
|
||||
* already-committed stale snapshots back under the byte-stable cap nor
|
||||
* retroactively exempt forced-overflow rows already audited. durableRows is
|
||||
* floored at auditRows so the exempt window can never invert.
|
||||
*/
|
||||
#updateCommittedAuditRows(
|
||||
resliced: boolean,
|
||||
preCommittedRows: number,
|
||||
preAuditRows: number,
|
||||
preDurableRows: number,
|
||||
preOfferRows: number,
|
||||
byteStableBoundary: number,
|
||||
durableBoundary: number,
|
||||
offerBoundary: number,
|
||||
hardAudited: boolean,
|
||||
): void {
|
||||
const committed = this.#committedRows;
|
||||
@@ -3036,18 +2975,8 @@ export class TUI extends Container {
|
||||
resliced || preDurableRows >= preCommittedRows || hardAudited
|
||||
? Math.min(committed, durableBoundary)
|
||||
: Math.min(preDurableRows, committed);
|
||||
// offerRows may EXTEND to include forced-overflow rows within a
|
||||
// newly-visible offerBoundary — unlike auditRows/durableRows, retroactive
|
||||
// promotion is safe: both offered and forced-overflow zones stay
|
||||
// audited; offered only switches divergence repair from tolerant recommit
|
||||
// to destructive replay (stronger, not weaker). A dropped offerBoundary
|
||||
// still keeps the durability rule via `preOfferRows` (never demote
|
||||
// already-offered rows to forced-overflow).
|
||||
const offerCap = Math.min(committed, offerBoundary);
|
||||
const offerRows = resliced ? offerCap : Math.max(Math.min(preOfferRows, committed), offerCap);
|
||||
this.#committedPrefixAuditRows = auditRows;
|
||||
this.#committedPrefixDurableRows = Math.max(auditRows, durableRows);
|
||||
this.#committedPrefixOfferRows = Math.max(this.#committedPrefixDurableRows, offerRows);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -645,7 +645,6 @@ class SeamComponent implements Component, NativeScrollbackLiveRegion {
|
||||
liveStart: number | undefined;
|
||||
commitSafe: number | undefined;
|
||||
snapSafe: number | undefined;
|
||||
offerSafe: number | undefined;
|
||||
|
||||
invalidate(): void {}
|
||||
|
||||
@@ -664,9 +663,6 @@ class SeamComponent implements Component, NativeScrollbackLiveRegion {
|
||||
getNativeScrollbackSnapshotSafeEnd(): number | undefined {
|
||||
return this.snapSafe;
|
||||
}
|
||||
getNativeScrollbackOfferSafeEnd(): number | undefined {
|
||||
return this.offerSafe;
|
||||
}
|
||||
}
|
||||
|
||||
/** Indices in `buffer` where `needle` begins as a contiguous run. */
|
||||
@@ -958,96 +954,6 @@ describe("scrollback commit gap — commit-unstable barriers", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("repairs offered finalized tail rows when a live block above grows", async () => {
|
||||
if (process.platform === "win32") return;
|
||||
const term = new VirtualTerminal(20, 5);
|
||||
overrideProbe(term, undefined);
|
||||
const tui = new TUI(term);
|
||||
const root = new SeamComponent();
|
||||
|
||||
try {
|
||||
tui.addChild(root);
|
||||
tui.start();
|
||||
await settle(term);
|
||||
const writes = capture(term);
|
||||
|
||||
const f1 = ["live-0", ...rows("tail-", 10)];
|
||||
root.lines = f1;
|
||||
root.liveStart = 0;
|
||||
root.snapSafe = 1;
|
||||
root.offerSafe = f1.length;
|
||||
tui.requestRender();
|
||||
await settle(term);
|
||||
|
||||
const f2 = ["live-0", "live-1", ...rows("tail-", 10)];
|
||||
root.lines = f2;
|
||||
root.offerSafe = f2.length;
|
||||
tui.requestRender();
|
||||
await settle(term);
|
||||
|
||||
const buffer = term.getScrollBuffer().map(line => line.trimEnd());
|
||||
expect(buffer).toEqual(f2);
|
||||
for (const row of rows("tail-", 10)) expect(buffer.filter(line => line === row)).toHaveLength(1);
|
||||
expect(eraseScrollbackCount(writes)).toBeGreaterThan(0);
|
||||
} finally {
|
||||
tui.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("promotes forced-overflow rows to offered when the offer boundary appears after commit", async () => {
|
||||
if (process.platform === "win32") return;
|
||||
const term = new VirtualTerminal(20, 4);
|
||||
overrideProbe(term, undefined);
|
||||
const tui = new TUI(term);
|
||||
const root = new SeamComponent();
|
||||
|
||||
try {
|
||||
tui.addChild(root);
|
||||
tui.start();
|
||||
await settle(term);
|
||||
const writes = capture(term);
|
||||
|
||||
// Phase 1: only the live barrier overflows the viewport. Rows scroll
|
||||
// off as forced-overflow (no offer boundary).
|
||||
const f1 = rows("live-", 10);
|
||||
root.lines = f1;
|
||||
root.liveStart = 0;
|
||||
root.offerSafe = undefined;
|
||||
tui.requestRender();
|
||||
await settle(term);
|
||||
const commitAfterF1 = term.getScrollBuffer().length;
|
||||
expect(commitAfterF1).toBeGreaterThan(0);
|
||||
|
||||
// Phase 2: finalized siblings are appended below the still-live block;
|
||||
// the component now reports an offer boundary covering the whole frame.
|
||||
// The engine must retroactively classify the already-committed rows
|
||||
// under the offer boundary as offered so the next divergence takes the
|
||||
// destructive-replay repair path, not tolerant recommit.
|
||||
const f2 = [...rows("live-", 10), ...rows("tail-", 5)];
|
||||
root.lines = f2;
|
||||
root.liveStart = 0;
|
||||
root.offerSafe = f2.length;
|
||||
tui.requestRender();
|
||||
await settle(term);
|
||||
|
||||
// Phase 3: live block grows by one row, shifting the whole tail up.
|
||||
const f3 = [...rows("live-", 11), ...rows("tail-", 5)];
|
||||
root.lines = f3;
|
||||
root.liveStart = 0;
|
||||
root.offerSafe = f3.length;
|
||||
tui.requestRender();
|
||||
await settle(term);
|
||||
|
||||
const buffer = term.getScrollBuffer().map(line => line.trimEnd());
|
||||
for (const row of rows("tail-", 5)) {
|
||||
expect(buffer.filter(line => line === row)).toHaveLength(1);
|
||||
}
|
||||
expect(eraseScrollbackCount(writes)).toBeGreaterThan(0);
|
||||
} finally {
|
||||
tui.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("does not lose a single-row finalize edit above an unchanged tail (reviewer repro)", async () => {
|
||||
if (process.platform === "win32") return;
|
||||
const term = new VirtualTerminal(20, 4);
|
||||
|
||||
Reference in New Issue
Block a user