mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 16:59:43 +02:00
fix(terminal): re-take the sticky-scroll baseline after a replay
A capture load now replays its queued tail, and that replay runs through `batchTerminalWrite`, which samples `_wasAtBottomBeforeWrite` before it queues. It runs inside `chunkedTerminalWrite`, before that promise resolves, with the terminal freshly reset and rewritten — so the sample is always true. The caller then restored the reader's position and the next `flushPendingWrites` scrolled straight back to the bottom off the latched flag, undoing it. The only thing in the way was `_hasRecentUserScrollUp()`, a 1500ms window a server-triggered refresh is usually past. `_syncStickyScrollBaseline()` re-takes the flag from wherever the viewport now sits, and the two paths that restore a position call it right after doing so: `_onSessionNeedsRefresh` and `_maybeRefetchFullHistory`. Those are the paths #259 and #205 exist for, and they are also where a non-empty queue is most likely, since a needsRefresh fires when output is flooding. Re-taking rather than suppressing the sampling: suppressing leaves whatever stale value the flag held from before the load, which on the full-history re-pull has no reason to be false. `selectSession` and `_onSessionClearTerminal` deliberately end at the bottom, so the sampled true is already the truth there and they do not call it. `_bufferLoadFinishOpts` gains the coverage the CI gate can see: both mux sources flush, `history` does not, and a payload naming no source does not. Its only coverage was the browser suite, which CI does not run. The JSDoc and the changeset now record the one duplicate window this cutoff cannot close. The server appends output to the byte buffer in the same tick it emits, but broadcasts on a batch timer — 8ms over WebSocket, 16 to 50ms over SSE — so a batch pending when `capture-pane` ran leaves the server after the reply and is replayed although the capture holds it. It is one batch interval wide against a recovery window spanning the whole chunked write, and closing it means flushing that batch server side before the capture. The second browser test asserts its session was created, so a failed create fails it instead of passing with zero hits. docs/architecture-invariants.md no longer claims the replay leaves the queued-event discard window alone. That clause now describes what decides how a load ends, the baseline rule, the batch window, and the three covering tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
c9515b1d4c
commit
75a028e825
@@ -322,6 +322,51 @@ describe('terminal flush budget', () => {
|
||||
expect(app._bufferLoadOwner).toBe(null);
|
||||
});
|
||||
|
||||
// ── Which payloads end their load by replaying the queue ──
|
||||
//
|
||||
// A pane capture is current only up to capture time, so the tail that arrived
|
||||
// after the response exists nowhere else and has to be replayed. The server's
|
||||
// accumulated byte history is current up to the response, so replaying on top
|
||||
// of it would duplicate output. `_bufferLoadFinishOpts` is the one place that
|
||||
// decides this, for all four paths that fetch a terminal buffer and write it.
|
||||
|
||||
it('replays the tail for a visible-pane capture', () => {
|
||||
const { CodemanApp } = loadAppHarness();
|
||||
const app = Object.create(CodemanApp.prototype) as any;
|
||||
|
||||
expect(app._bufferLoadFinishOpts({ source: 'mux-visible' }, 1234)).toEqual({
|
||||
flushQueued: true,
|
||||
since: 1234,
|
||||
});
|
||||
});
|
||||
|
||||
it('replays the tail for a full-history capture', () => {
|
||||
const { CodemanApp } = loadAppHarness();
|
||||
const app = Object.create(CodemanApp.prototype) as any;
|
||||
|
||||
expect(app._bufferLoadFinishOpts({ source: 'mux-full-history' }, 1234)).toEqual({
|
||||
flushQueued: true,
|
||||
since: 1234,
|
||||
});
|
||||
});
|
||||
|
||||
it('discards the queue for the accumulated byte history', () => {
|
||||
const { CodemanApp } = loadAppHarness();
|
||||
const app = Object.create(CodemanApp.prototype) as any;
|
||||
|
||||
expect(app._bufferLoadFinishOpts({ source: 'history' }, 1234).flushQueued).toBe(false);
|
||||
});
|
||||
|
||||
it('discards the queue for a payload that names no source', () => {
|
||||
// Fails toward the safe answer: a duplicated Ink redraw corrupts the screen,
|
||||
// while a dropped tail is repaired by the CLI's next full repaint.
|
||||
const { CodemanApp } = loadAppHarness();
|
||||
const app = Object.create(CodemanApp.prototype) as any;
|
||||
|
||||
expect(app._bufferLoadFinishOpts({}, 1234).flushQueued).toBe(false);
|
||||
expect(app._bufferLoadFinishOpts(undefined, 1234).flushQueued).toBe(false);
|
||||
});
|
||||
|
||||
it('does not snap back to bottom during Codex Working redraws right after the user scrolls up', () => {
|
||||
const { app } = loadTerminalUiHarness('codex');
|
||||
const scrollToBottom = vi.fn();
|
||||
|
||||
Reference in New Issue
Block a user