@aragon-agent/cli 0.6.2 → 0.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +321 -0
- package/README.md +297 -2
- package/dist/agent/controller.d.ts +193 -1
- package/dist/agent/controller.js +475 -2
- package/dist/agent/headless.d.ts +8 -0
- package/dist/agent/headless.js +47 -0
- package/dist/agent/reducer.d.ts +189 -0
- package/dist/agent/reducer.js +301 -2
- package/dist/agent/system-prompt.d.ts +14 -0
- package/dist/agent/system-prompt.js +2 -0
- package/dist/cli.js +297 -16
- package/dist/commands/builtins.js +165 -24
- package/dist/commands/registry.d.ts +27 -0
- package/dist/compaction/archive.d.ts +175 -0
- package/dist/compaction/archive.js +267 -0
- package/dist/compaction/child.d.ts +97 -0
- package/dist/compaction/child.js +118 -0
- package/dist/compaction/command.d.ts +41 -0
- package/dist/compaction/command.js +355 -0
- package/dist/compaction/compactor.d.ts +302 -0
- package/dist/compaction/compactor.js +810 -0
- package/dist/compaction/digest.d.ts +62 -0
- package/dist/compaction/digest.js +164 -0
- package/dist/compaction/limits.d.ts +165 -0
- package/dist/compaction/limits.js +172 -0
- package/dist/compaction/pressure.d.ts +135 -0
- package/dist/compaction/pressure.js +167 -0
- package/dist/compaction/summarize-call.d.ts +70 -0
- package/dist/compaction/summarize-call.js +86 -0
- package/dist/compaction/summary-prompt.d.ts +85 -0
- package/dist/compaction/summary-prompt.js +197 -0
- package/dist/compaction/tail-budget.d.ts +52 -0
- package/dist/compaction/tail-budget.js +67 -0
- package/dist/compaction/types.d.ts +215 -0
- package/dist/compaction/types.js +22 -0
- package/dist/compaction/wiring.d.ts +264 -0
- package/dist/compaction/wiring.js +577 -0
- package/dist/config/app-paths.d.ts +14 -0
- package/dist/config/app-paths.js +16 -0
- package/dist/config/cli-commands.d.ts +41 -0
- package/dist/config/cli-commands.js +77 -1
- package/dist/config/env.d.ts +5 -2
- package/dist/config/env.js +140 -3
- package/dist/config/load.d.ts +42 -0
- package/dist/config/load.js +101 -1
- package/dist/config/schema.d.ts +244 -0
- package/dist/config/schema.js +170 -0
- package/dist/config/store.js +37 -1
- package/dist/config/ui-state.d.ts +28 -1
- package/dist/config/ui-state.js +9 -0
- package/dist/exec/events.d.ts +50 -1
- package/dist/exec/runner.js +42 -0
- package/dist/input/limits.d.ts +72 -0
- package/dist/input/limits.js +63 -0
- package/dist/input/mouse-events.d.ts +31 -5
- package/dist/input/mouse-events.js +86 -27
- package/dist/input/paste-parse.d.ts +71 -0
- package/dist/input/paste-parse.js +95 -0
- package/dist/input/stdin-filter.d.ts +81 -0
- package/dist/input/stdin-filter.js +379 -0
- package/dist/logging/install.d.ts +53 -1
- package/dist/logging/install.js +81 -1
- package/dist/logging/logger.d.ts +1 -1
- package/dist/proc/classify.d.ts +52 -0
- package/dist/proc/classify.js +143 -0
- package/dist/proc/kill-tree.d.ts +42 -0
- package/dist/proc/kill-tree.js +119 -0
- package/dist/proc/limits.d.ts +84 -0
- package/dist/proc/limits.js +84 -0
- package/dist/proc/log-ring.d.ts +68 -0
- package/dist/proc/log-ring.js +102 -0
- package/dist/proc/prompt.d.ts +24 -0
- package/dist/proc/prompt.js +49 -0
- package/dist/proc/readiness.d.ts +102 -0
- package/dist/proc/readiness.js +219 -0
- package/dist/proc/supervisor.d.ts +103 -0
- package/dist/proc/supervisor.js +465 -0
- package/dist/proc/types.d.ts +175 -0
- package/dist/proc/types.js +15 -0
- package/dist/session/persist.js +55 -1
- package/dist/team/report.js +7 -1
- package/dist/team/runtime.d.ts +10 -0
- package/dist/team/runtime.js +7 -0
- package/dist/team/subagent.d.ts +31 -1
- package/dist/team/subagent.js +42 -15
- package/dist/team/types.d.ts +8 -0
- package/dist/tools/bash-tool.d.ts +51 -2
- package/dist/tools/bash-tool.js +321 -51
- package/dist/tools/fs-tools.d.ts +17 -0
- package/dist/tools/index.d.ts +55 -2
- package/dist/tools/index.js +39 -1
- package/dist/tools/proc-tools.d.ts +25 -0
- package/dist/tools/proc-tools.js +102 -0
- package/dist/ui/ActivityLine.d.ts +19 -1
- package/dist/ui/ActivityLine.js +10 -2
- package/dist/ui/App.d.ts +62 -2
- package/dist/ui/App.js +546 -16
- package/dist/ui/Composer.d.ts +66 -1
- package/dist/ui/Composer.js +50 -6
- package/dist/ui/PromptInput.d.ts +82 -3
- package/dist/ui/PromptInput.js +232 -49
- package/dist/ui/StatusBar.d.ts +59 -0
- package/dist/ui/StatusBar.js +16 -4
- package/dist/ui/TeamPanel.js +7 -1
- package/dist/ui/Transcript.d.ts +21 -1
- package/dist/ui/Transcript.js +61 -3
- package/dist/ui/clipboard.d.ts +52 -0
- package/dist/ui/clipboard.js +91 -0
- package/dist/ui/composer-limits.d.ts +31 -0
- package/dist/ui/composer-limits.js +50 -0
- package/dist/ui/composer-rows.d.ts +73 -0
- package/dist/ui/composer-rows.js +201 -0
- package/dist/ui/editor-reducer.d.ts +37 -0
- package/dist/ui/editor-reducer.js +88 -12
- package/dist/ui/entries/CompactionCard.d.ts +75 -0
- package/dist/ui/entries/CompactionCard.js +158 -0
- package/dist/ui/entries/ServiceCard.d.ts +66 -0
- package/dist/ui/entries/ServiceCard.js +116 -0
- package/dist/ui/entries/UserEntry.d.ts +17 -3
- package/dist/ui/entries/UserEntry.js +34 -5
- package/dist/ui/frame-differ.d.ts +46 -0
- package/dist/ui/frame-differ.js +94 -10
- package/dist/ui/gauge.d.ts +16 -1
- package/dist/ui/gauge.js +25 -6
- package/dist/ui/glyphs.d.ts +11 -0
- package/dist/ui/glyphs.js +2 -0
- package/dist/ui/layout/ScrollViewport.d.ts +43 -6
- package/dist/ui/layout/ScrollViewport.js +128 -21
- package/dist/ui/layout/budget.d.ts +44 -9
- package/dist/ui/layout/budget.js +42 -10
- package/dist/ui/layout/follow-state.d.ts +137 -0
- package/dist/ui/layout/follow-state.js +150 -0
- package/dist/ui/layout/virtual-window.d.ts +11 -0
- package/dist/ui/layout/virtual-window.js +85 -1
- package/dist/ui/overlays/HelpOverlay.js +9 -1
- package/dist/ui/overlays/PlanReviewOverlay.js +10 -2
- package/dist/ui/overlays/QuestionOverlay.js +5 -1
- package/dist/ui/overlays/SettingsScreen.d.ts +47 -1
- package/dist/ui/overlays/SettingsScreen.js +74 -2
- package/dist/ui/palettes.d.ts +16 -0
- package/dist/ui/palettes.js +6 -0
- package/dist/ui/paste-frames.d.ts +87 -0
- package/dist/ui/paste-frames.js +112 -0
- package/dist/ui/paste-tokens.d.ts +90 -0
- package/dist/ui/paste-tokens.js +147 -0
- package/dist/ui/screen.d.ts +38 -0
- package/dist/ui/screen.js +113 -8
- package/dist/ui/selection/highlight.d.ts +39 -0
- package/dist/ui/selection/highlight.js +64 -0
- package/dist/ui/selection/screen-mirror.d.ts +24 -0
- package/dist/ui/selection/screen-mirror.js +35 -0
- package/dist/ui/selection/selection-controller.d.ts +88 -0
- package/dist/ui/selection/selection-controller.js +282 -0
- package/dist/ui/selection/selection.d.ts +90 -0
- package/dist/ui/selection/selection.js +223 -0
- package/dist/ui/stdout-frame-writer.d.ts +33 -0
- package/dist/ui/stdout-frame-writer.js +15 -0
- package/dist/ui/theme.d.ts +11 -0
- package/dist/ui/theme.js +2 -0
- package/dist/ui/transcript-text.js +72 -0
- package/dist/ui/use-startup-notices.d.ts +27 -1
- package/dist/ui/use-startup-notices.js +43 -11
- package/dist/ui/use-wheel-routing.d.ts +1 -1
- package/dist/ui/use-wheel-routing.js +8 -0
- package/package.json +4 -2
- package/dist/input/stdin-mouse-filter.d.ts +0 -42
- package/dist/input/stdin-mouse-filter.js +0 -153
|
@@ -11,11 +11,23 @@
|
|
|
11
11
|
* *display* number back. Holding the offset in the parent as well would create a
|
|
12
12
|
* measure → setState → re-render → measure loop whose convergence is provable
|
|
13
13
|
* but whose frame count is not — expensive inside a 33 ms streaming window.
|
|
14
|
+
*
|
|
15
|
+
* AS OF tui-selection-and-scroll-follow THIS IS AN ADAPTER, NOT A POLICY OWNER.
|
|
16
|
+
* The two follow rules are pure and live in `follow-state.ts`; what stays here is
|
|
17
|
+
* measure → compute a tail delta → apply → arm a timer. `scroll.ts` is
|
|
18
|
+
* unchanged, so every existing scroll test keeps passing (R-10).
|
|
19
|
+
*
|
|
20
|
+
* The "N new lines · PgDn" hint used to take this viewport's LAST ROW. It now
|
|
21
|
+
* renders as a chip inside the composer's bordered input row (§4.2), which costs
|
|
22
|
+
* zero rows and removes the feedback loop where the hint's own presence shrank
|
|
23
|
+
* the viewport, which changed `overflowLines`, which was the number the hint
|
|
24
|
+
* displayed. The data still leaves through `onScrolledLinesChange`.
|
|
14
25
|
*/
|
|
15
26
|
import React from 'react';
|
|
16
27
|
import type { Theme } from '../theme.js';
|
|
17
28
|
import type { TermCapabilities } from '../capabilities.js';
|
|
18
29
|
import { type ScrollIntent } from './scroll.js';
|
|
30
|
+
import { type TailSink } from './follow-state.js';
|
|
19
31
|
export interface ScrollViewportProps {
|
|
20
32
|
/**
|
|
21
33
|
* Scroll intent. `nonce` is monotonic so pressing the same key twice re-fires.
|
|
@@ -30,6 +42,31 @@ export interface ScrollViewportProps {
|
|
|
30
42
|
pinToBottomNonce?: number;
|
|
31
43
|
/** Derived display value for the status bar's `↑N`. Must be referentially stable. */
|
|
32
44
|
onScrolledLinesChange?: (lines: number) => void;
|
|
45
|
+
/**
|
|
46
|
+
* Rows hidden ABOVE the window, published on every change (§4.3.3).
|
|
47
|
+
*
|
|
48
|
+
* PUBLISHED, NOT LIFTED — the same shape as `onScrolledLinesChange`, so there
|
|
49
|
+
* is still exactly one owner of the offset. `shiftUp` changes exactly when the
|
|
50
|
+
* rows on screen move, which is the property the selection's clear-list was
|
|
51
|
+
* approximating by enumeration (I-9 / P1-4). Must be referentially stable.
|
|
52
|
+
*/
|
|
53
|
+
onViewportShiftChange?: (shiftUp: number) => void;
|
|
54
|
+
/**
|
|
55
|
+
* The transcript's tail row counter (§4.3.1a), written during ITS render and
|
|
56
|
+
* therefore already current when this component's layout effect runs.
|
|
57
|
+
*
|
|
58
|
+
* ABSENT ⇒ NO ANCHORING AT ALL, deliberately: every existing caller and every
|
|
59
|
+
* existing test that does not supply it behaves exactly as it did before this
|
|
60
|
+
* feature, because `tailDelta` is then permanently 0.
|
|
61
|
+
*/
|
|
62
|
+
tailRowsRef?: TailSink;
|
|
63
|
+
/**
|
|
64
|
+
* True while a selection drag is in progress (§4.4). It anchors the content
|
|
65
|
+
* even from a pinned viewport, and it suspends the resume timer.
|
|
66
|
+
*/
|
|
67
|
+
hold?: boolean;
|
|
68
|
+
/** Idle delay before returning to the newest line, in ms; `0` disables (AC-9). */
|
|
69
|
+
resumeMs?: number;
|
|
33
70
|
/** Reserve the right-edge scroll-position rail (mouse-wheel §4.8). */
|
|
34
71
|
showScrollIndicator?: boolean;
|
|
35
72
|
/**
|
|
@@ -37,15 +74,15 @@ export interface ScrollViewportProps {
|
|
|
37
74
|
* (todo-plan-execution §3.9 / C-7).
|
|
38
75
|
*
|
|
39
76
|
* This component is the ONLY one inside the viewport subtree that reads
|
|
40
|
-
* `stdout.columns` directly, and
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
77
|
+
* `stdout.columns` directly, and the place it uses the number — the
|
|
78
|
+
* `MIN_INDICATOR_COLS` gate — is about how much room THIS BOX has, not how
|
|
79
|
+
* wide the terminal is. Once the todo rail takes a fifth of the frame those two
|
|
80
|
+
* stop being the same number. Optional so every existing caller (and every
|
|
81
|
+
* test) is unchanged.
|
|
45
82
|
*/
|
|
46
83
|
cols?: number;
|
|
47
84
|
theme: Theme;
|
|
48
85
|
caps: TermCapabilities;
|
|
49
86
|
children: React.ReactNode;
|
|
50
87
|
}
|
|
51
|
-
export declare function ScrollViewport({ intent, pinToBottomNonce, onScrolledLinesChange, showScrollIndicator, cols: colsProp, theme, caps, children, }: ScrollViewportProps): React.ReactElement;
|
|
88
|
+
export declare function ScrollViewport({ intent, pinToBottomNonce, onScrolledLinesChange, onViewportShiftChange, tailRowsRef, hold, resumeMs, showScrollIndicator, cols: colsProp, theme, caps, children, }: ScrollViewportProps): React.ReactElement;
|
|
@@ -12,34 +12,132 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
12
12
|
* *display* number back. Holding the offset in the parent as well would create a
|
|
13
13
|
* measure → setState → re-render → measure loop whose convergence is provable
|
|
14
14
|
* but whose frame count is not — expensive inside a 33 ms streaming window.
|
|
15
|
+
*
|
|
16
|
+
* AS OF tui-selection-and-scroll-follow THIS IS AN ADAPTER, NOT A POLICY OWNER.
|
|
17
|
+
* The two follow rules are pure and live in `follow-state.ts`; what stays here is
|
|
18
|
+
* measure → compute a tail delta → apply → arm a timer. `scroll.ts` is
|
|
19
|
+
* unchanged, so every existing scroll test keeps passing (R-10).
|
|
20
|
+
*
|
|
21
|
+
* The "N new lines · PgDn" hint used to take this viewport's LAST ROW. It now
|
|
22
|
+
* renders as a chip inside the composer's bordered input row (§4.2), which costs
|
|
23
|
+
* zero rows and removes the feedback loop where the hint's own presence shrank
|
|
24
|
+
* the viewport, which changed `overflowLines`, which was the number the hint
|
|
25
|
+
* displayed. The data still leaves through `onScrolledLinesChange`.
|
|
15
26
|
*/
|
|
16
|
-
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
17
|
-
import { Box,
|
|
18
|
-
import { pickGlyphs } from '../glyphs.js';
|
|
27
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
28
|
+
import { Box, measureElement, useStdout } from 'ink';
|
|
19
29
|
import { ViewportGeometryContext } from './viewport-geometry.js';
|
|
20
30
|
import { applyScrollTimes, clampScroll, } from './scroll.js';
|
|
31
|
+
import { reduceFollow, shouldArmResume } from './follow-state.js';
|
|
21
32
|
import { thumbRange } from './scroll-indicator.js';
|
|
22
33
|
import { MIN_INDICATOR_COLS, ScrollIndicator } from './ScrollIndicator.js';
|
|
23
|
-
|
|
24
|
-
const TERSE_HINT_COLS = 42;
|
|
25
|
-
export function ScrollViewport({ intent, pinToBottomNonce = 0, onScrolledLinesChange, showScrollIndicator = false, cols: colsProp, theme, caps, children, }) {
|
|
34
|
+
export function ScrollViewport({ intent, pinToBottomNonce = 0, onScrolledLinesChange, onViewportShiftChange, tailRowsRef, hold = false, resumeMs = 0, showScrollIndicator = false, cols: colsProp, theme, caps, children, }) {
|
|
26
35
|
const { stdout } = useStdout();
|
|
27
36
|
const cols = colsProp ?? stdout?.columns ?? 80;
|
|
28
|
-
const glyphs = pickGlyphs(caps);
|
|
29
37
|
const clipRef = useRef(null);
|
|
30
38
|
const innerRef = useRef(null);
|
|
31
39
|
const [metrics, setMetrics] = useState({ viewport: 0, content: 0 });
|
|
32
40
|
const [scroll, setScroll] = useState({ offset: 0 });
|
|
41
|
+
const overflowLines = Math.max(0, metrics.content - metrics.viewport);
|
|
42
|
+
const offset = clampScroll(scroll.offset, overflowLines);
|
|
43
|
+
const shiftUp = overflowLines - offset;
|
|
33
44
|
const metricsRef = useRef(metrics);
|
|
34
45
|
metricsRef.current = metrics;
|
|
35
46
|
const intentRef = useRef(intent);
|
|
36
47
|
intentRef.current = intent;
|
|
48
|
+
/**
|
|
49
|
+
* The RENDERED (clamped) offset, assigned during render (P1-9).
|
|
50
|
+
*
|
|
51
|
+
* `scroll.offset` is raw state while everything on screen derives from
|
|
52
|
+
* `clampScroll(...)`; the two diverge after any content shrink (`/clear`, the
|
|
53
|
+
* retain ring, a re-wrap), and an accumulation against the raw value then yanks
|
|
54
|
+
* a viewport the user believes is pinned and shows a phantom chip.
|
|
55
|
+
*/
|
|
56
|
+
const offsetRef = useRef(offset);
|
|
57
|
+
offsetRef.current = offset;
|
|
58
|
+
/** Read at timer FIRE time, not only at ARM time (I-6). */
|
|
59
|
+
const holdRef = useRef(hold);
|
|
60
|
+
holdRef.current = hold;
|
|
61
|
+
const resumeMsRef = useRef(resumeMs);
|
|
62
|
+
resumeMsRef.current = resumeMs;
|
|
63
|
+
/** Rows that arrived while paused; resets to 0 whenever the offset reaches 0. */
|
|
64
|
+
const newLinesRef = useRef(0);
|
|
65
|
+
const prevTail = useRef(0);
|
|
66
|
+
const resumeTimer = useRef(null);
|
|
67
|
+
const clearResumeTimer = useCallback(() => {
|
|
68
|
+
if (!resumeTimer.current)
|
|
69
|
+
return;
|
|
70
|
+
clearTimeout(resumeTimer.current);
|
|
71
|
+
resumeTimer.current = null;
|
|
72
|
+
}, []);
|
|
73
|
+
/**
|
|
74
|
+
* Rule B. Called from exactly three places and no others: the intent effect
|
|
75
|
+
* (after applying a scroll), the anchoring effect (after a non-zero
|
|
76
|
+
* `tailDelta`), and every `hold` transition.
|
|
77
|
+
*
|
|
78
|
+
* The re-check inside the callback is the same class of bug the wheel router's
|
|
79
|
+
* flush already guards: the decision that goes stale is the one taken when the
|
|
80
|
+
* timer was ARMED, and 5 seconds is an eternity next to a 16 ms coalescing
|
|
81
|
+
* window.
|
|
82
|
+
*/
|
|
83
|
+
const armResume = useCallback(() => {
|
|
84
|
+
clearResumeTimer();
|
|
85
|
+
if (!shouldArmResume({
|
|
86
|
+
resumeMs: resumeMsRef.current,
|
|
87
|
+
offset: offsetRef.current,
|
|
88
|
+
newLinesWhilePaused: newLinesRef.current,
|
|
89
|
+
hold: holdRef.current,
|
|
90
|
+
})) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const timer = setTimeout(() => {
|
|
94
|
+
resumeTimer.current = null;
|
|
95
|
+
if (holdRef.current)
|
|
96
|
+
return; // I-6 — re-read at FIRE time.
|
|
97
|
+
setScroll({ offset: 0 });
|
|
98
|
+
}, resumeMsRef.current);
|
|
99
|
+
timer.unref?.();
|
|
100
|
+
resumeTimer.current = timer;
|
|
101
|
+
}, [clearResumeTimer]);
|
|
102
|
+
// A timer that outlives the component would fire `setScroll` on an unmounted
|
|
103
|
+
// tree; an overlay unmounts this component on every open.
|
|
104
|
+
useEffect(() => clearResumeTimer, [clearResumeTimer]);
|
|
37
105
|
// Ink calculates yoga layout in `onRender`, which React runs BEFORE layout
|
|
38
106
|
// effects, so the measurement below reflects the commit that just happened.
|
|
107
|
+
// The children have already rendered, so `tailRowsRef` is current too.
|
|
39
108
|
useLayoutEffect(() => {
|
|
40
109
|
const viewport = clipRef.current ? measureElement(clipRef.current).height : 0;
|
|
41
110
|
const content = innerRef.current ? measureElement(innerRef.current).height : 0;
|
|
42
111
|
setMetrics((prev) => (prev.viewport === viewport && prev.content === content ? prev : { viewport, content }));
|
|
112
|
+
// RULE A. The input is the transcript's TAIL counter and never `content`
|
|
113
|
+
// (P0-1 / I-7) — see the header of `follow-state.ts` for the four things
|
|
114
|
+
// that move a measured height and the one that must move the offset.
|
|
115
|
+
const tail = tailRowsRef?.current.rows ?? 0;
|
|
116
|
+
const tailDelta = tail - prevTail.current;
|
|
117
|
+
prevTail.current = tail;
|
|
118
|
+
if (tailDelta === 0)
|
|
119
|
+
return; // the steady state, and R-8's loop guard
|
|
120
|
+
const overflow = Math.max(0, content - viewport);
|
|
121
|
+
// THE DECISION IS TAKEN OUTSIDE THE UPDATER (P1-9). React is free to invoke
|
|
122
|
+
// an updater more than once, and a `newLinesWhilePaused += delta` inside one
|
|
123
|
+
// double-counts when it does.
|
|
124
|
+
const out = reduceFollow({
|
|
125
|
+
offset: clampScroll(offsetRef.current, overflow),
|
|
126
|
+
overflowLines: overflow,
|
|
127
|
+
tailDelta,
|
|
128
|
+
hold: holdRef.current,
|
|
129
|
+
newLinesWhilePaused: newLinesRef.current,
|
|
130
|
+
});
|
|
131
|
+
newLinesRef.current = out.newLinesWhilePaused;
|
|
132
|
+
if (out.offset !== offsetRef.current) {
|
|
133
|
+
// Assigned eagerly so the `armResume()` below decides against the offset
|
|
134
|
+
// this frame is about to render, not the one it replaced. `out.offset` is
|
|
135
|
+
// already clamped against the freshly measured overflow, which is exactly
|
|
136
|
+
// what the next render will clamp against.
|
|
137
|
+
offsetRef.current = out.offset;
|
|
138
|
+
setScroll({ offset: out.offset });
|
|
139
|
+
}
|
|
140
|
+
armResume();
|
|
43
141
|
});
|
|
44
142
|
// I-9: seeded from the INCOMING nonce during the first render, so the effect
|
|
45
143
|
// below cannot apply an intent on the mount that first observes it. A
|
|
@@ -61,6 +159,9 @@ export function ScrollViewport({ intent, pinToBottomNonce = 0, onScrolledLinesCh
|
|
|
61
159
|
lastAppliedNonce.current = current.nonce;
|
|
62
160
|
const repeat = current.repeat ?? 1;
|
|
63
161
|
setScroll((prev) => applyScrollTimes(prev, current.kind, metricsRef.current, repeat));
|
|
162
|
+
// T-21 — "用户没动" is measured from the last GESTURE, so every applied
|
|
163
|
+
// intent restarts the countdown.
|
|
164
|
+
armResume();
|
|
64
165
|
// Keyed on the nonce alone: `intent` is a fresh object on every parent
|
|
65
166
|
// render, so depending on it directly would re-scroll on every keystroke.
|
|
66
167
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -68,16 +169,28 @@ export function ScrollViewport({ intent, pinToBottomNonce = 0, onScrolledLinesCh
|
|
|
68
169
|
useEffect(() => {
|
|
69
170
|
setScroll({ offset: 0 });
|
|
70
171
|
}, [pinToBottomNonce]);
|
|
71
|
-
const overflowLines = Math.max(0, metrics.content - metrics.viewport);
|
|
72
|
-
const offset = clampScroll(scroll.offset, overflowLines);
|
|
73
|
-
const shiftUp = overflowLines - offset;
|
|
74
172
|
useEffect(() => {
|
|
75
173
|
onScrolledLinesChange?.(offset);
|
|
76
174
|
}, [offset, onScrolledLinesChange]);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
onViewportShiftChange?.(shiftUp);
|
|
177
|
+
}, [shiftUp, onViewportShiftChange]);
|
|
178
|
+
// Reaching the bottom BY ANY ROUTE — the timer, `pinToBottomNonce`, PgDn, or
|
|
179
|
+
// `applyScroll`'s bottom snap — resets the paused counter and disarms. The
|
|
180
|
+
// anchoring route expresses the same reset inside `reduceFollow`, so the rule
|
|
181
|
+
// is stated once per route rather than once per gesture.
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
if (offset !== 0)
|
|
184
|
+
return;
|
|
185
|
+
newLinesRef.current = 0;
|
|
186
|
+
armResume(); // clears; re-arming is refused at offset 0 by condition 1.
|
|
187
|
+
}, [offset, armResume]);
|
|
188
|
+
// Both `hold` edges. Rising suspends the countdown (condition 3); falling
|
|
189
|
+
// restarts it, which is what makes a released drag behave like any other
|
|
190
|
+
// finished gesture.
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
armResume();
|
|
193
|
+
}, [hold, armResume]);
|
|
81
194
|
// Suppressed below `MIN_INDICATOR_COLS`, where a column of content is worth
|
|
82
195
|
// more than the affordance. The routing never reads this flag, which is what
|
|
83
196
|
// keeps the indicator independently revertible (§13-Q3).
|
|
@@ -87,11 +200,5 @@ export function ScrollViewport({ intent, pinToBottomNonce = 0, onScrolledLinesCh
|
|
|
87
200
|
// value hands them down without adding a second owner or a second measure
|
|
88
201
|
// pass, which is exactly what this component's header (P1-7) forbids.
|
|
89
202
|
const geometry = useMemo(() => ({ viewportRows: metrics.viewport, offset, contentRows: metrics.content }), [metrics.viewport, metrics.content, offset]);
|
|
90
|
-
return (
|
|
91
|
-
// Not an overlay — Ink has no absolute positioning or z-index. This takes
|
|
92
|
-
// the viewport's last row, trading one line of content for a hint that
|
|
93
|
-
// cannot drift. Cheaper than stacking with negative margins.
|
|
94
|
-
_jsx(Box, { flexShrink: 0, justifyContent: "flex-end", children: _jsx(Text, { wrap: "truncate", color: theme.hintFg ?? theme.muted, children: cols < TERSE_HINT_COLS
|
|
95
|
-
? `${glyphs.arrowDown}${hiddenBelow}`
|
|
96
|
-
: `${glyphs.arrowDown} ${hiddenBelow} new ${hiddenBelow === 1 ? 'line' : 'lines'} ${glyphs.midDot} PgDn for the latest` }) }))] }));
|
|
203
|
+
return (_jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsxs(Box, { flexDirection: "row", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: [_jsx(Box, { ref: clipRef, flexDirection: "column", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsx(Box, { ref: innerRef, flexDirection: "column", flexShrink: 0, marginTop: -shiftUp, children: _jsx(ViewportGeometryContext.Provider, { value: geometry, children: children }) }) }), indicatorVisible && (_jsx(ScrollIndicator, { rows: metrics.viewport, thumb: thumbRange(metrics.viewport, metrics.content, offset), theme: theme, caps: caps }))] }) }));
|
|
97
204
|
}
|
|
@@ -12,10 +12,17 @@
|
|
|
12
12
|
* Deliberately NOT part of `frame.ts`: that file is the read-only foundation
|
|
13
13
|
* (invariant I-1) and this is content-layer policy.
|
|
14
14
|
*
|
|
15
|
-
* Scope: the STATIC chrome only.
|
|
16
|
-
*
|
|
17
|
-
* - `ScrollViewport`'s "N new lines" hint takes the viewport's own last row;
|
|
15
|
+
* Scope: the STATIC chrome only. One consumer is dynamic and is handled by the
|
|
16
|
+
* degradation ladder in §5.2 instead of being averaged in here:
|
|
18
17
|
* - `AutocompletePopup` expands the bottom chrome by up to 9 rows.
|
|
18
|
+
*
|
|
19
|
+
* `ScrollViewport`'s "N new lines" hint used to be listed here as a second
|
|
20
|
+
* dynamic consumer. It no longer exists: the number renders as a chip on the
|
|
21
|
+
* composer's input row (tui-selection-and-scroll-follow §4.2), which costs
|
|
22
|
+
* COLUMNS rather than rows. The arithmetic below is unchanged — the hint was
|
|
23
|
+
* never in the budget in the first place — but leaving the bullet in place would
|
|
24
|
+
* make this comment a true-sounding statement about a row that is gone (AC-12,
|
|
25
|
+
* and the `wheel-scrolls-transcript-only` RV-3 lesson).
|
|
19
26
|
*/
|
|
20
27
|
/** Below this many rows the composer's hint line is dropped to buy back a row. */
|
|
21
28
|
export declare const HINT_MIN_ROWS = 20;
|
|
@@ -23,11 +30,26 @@ export interface ChromeBudget {
|
|
|
23
30
|
/** Brand bar. Constant 1 in full-screen — this is what makes A-3 hold. */
|
|
24
31
|
header: number;
|
|
25
32
|
toast: number;
|
|
26
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* Round border (2) + the DRAFT's own rows, plus the hint row when it fits.
|
|
35
|
+
*
|
|
36
|
+
* The draft term used to be the constant 1 that the `3` / `4` below encode.
|
|
37
|
+
* That constant is what made a pasted 200-line draft draw the transcript 197
|
|
38
|
+
* rows shorter than every consumer of `viewportRows` believed -- the trap
|
|
39
|
+
* `BottomStatusRow.tsx:5-27` already documents at 1/200th the magnitude.
|
|
40
|
+
*/
|
|
27
41
|
composer: number;
|
|
28
42
|
status: number;
|
|
29
43
|
}
|
|
30
|
-
|
|
44
|
+
/**
|
|
45
|
+
* The composer's draft rows, clamped to what this terminal height allows.
|
|
46
|
+
*
|
|
47
|
+
* Exported so the reporter and the budget clamp with ONE function: I-8 requires
|
|
48
|
+
* the rendered row count and the number handed to `viewportRows` to be the same
|
|
49
|
+
* number, and two clamps are how they stop being.
|
|
50
|
+
*/
|
|
51
|
+
export declare function clampDraftRows(rows: number, draftRows: number): number;
|
|
52
|
+
export declare function chromeBudget(rows: number, draftRows?: number): ChromeBudget;
|
|
31
53
|
/**
|
|
32
54
|
* Rows available to the viewport at a given terminal height.
|
|
33
55
|
*
|
|
@@ -35,8 +57,21 @@ export declare function chromeBudget(rows: number): ChromeBudget;
|
|
|
35
57
|
* that argument is the guarantee that the first submitted message cannot make
|
|
36
58
|
* the layout jump (A-4). Do not add it back.
|
|
37
59
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
60
|
+
* `draftRows` IS NOT THAT ARGUMENT, and the distinction is the whole of D-12.
|
|
61
|
+
* A-4 forbids making the budget depend on TRANSCRIPT content, so that the first
|
|
62
|
+
* submitted message cannot move the layout. `draftRows` depends only on what the
|
|
63
|
+
* user is typing right now, and its whole purpose is that the jump is ALREADY
|
|
64
|
+
* HAPPENING -- Yoga takes the rows out of the transcript whether or not this
|
|
65
|
+
* function was told. `App` adds the hysteresis (grow now, shrink one tick later)
|
|
66
|
+
* that keeps it from becoming per-keystroke jitter.
|
|
67
|
+
*
|
|
68
|
+
* It DEFAULTS TO 1, i.e. to the constant this function used to assume, so every
|
|
69
|
+
* pre-existing caller and every pre-existing assertion is byte-identical -- and
|
|
70
|
+
* `viewportRows.length` stays 1, because a parameter with a default does not
|
|
71
|
+
* count toward it.
|
|
72
|
+
*
|
|
73
|
+
* Non-decreasing in `rows` at a fixed `draftRows = 1`: the one step where chrome
|
|
74
|
+
* grows (the hint row at `HINT_MIN_ROWS`) coincides with `rows` growing by one,
|
|
75
|
+
* so the net change is 0 rather than -1.
|
|
41
76
|
*/
|
|
42
|
-
export declare function viewportRows(rows: number): number;
|
|
77
|
+
export declare function viewportRows(rows: number, draftRows?: number): number;
|
package/dist/ui/layout/budget.js
CHANGED
|
@@ -12,19 +12,38 @@
|
|
|
12
12
|
* Deliberately NOT part of `frame.ts`: that file is the read-only foundation
|
|
13
13
|
* (invariant I-1) and this is content-layer policy.
|
|
14
14
|
*
|
|
15
|
-
* Scope: the STATIC chrome only.
|
|
16
|
-
*
|
|
17
|
-
* - `ScrollViewport`'s "N new lines" hint takes the viewport's own last row;
|
|
15
|
+
* Scope: the STATIC chrome only. One consumer is dynamic and is handled by the
|
|
16
|
+
* degradation ladder in §5.2 instead of being averaged in here:
|
|
18
17
|
* - `AutocompletePopup` expands the bottom chrome by up to 9 rows.
|
|
18
|
+
*
|
|
19
|
+
* `ScrollViewport`'s "N new lines" hint used to be listed here as a second
|
|
20
|
+
* dynamic consumer. It no longer exists: the number renders as a chip on the
|
|
21
|
+
* composer's input row (tui-selection-and-scroll-follow §4.2), which costs
|
|
22
|
+
* COLUMNS rather than rows. The arithmetic below is unchanged — the hint was
|
|
23
|
+
* never in the budget in the first place — but leaving the bullet in place would
|
|
24
|
+
* make this comment a true-sounding statement about a row that is gone (AC-12,
|
|
25
|
+
* and the `wheel-scrolls-transcript-only` RV-3 lesson).
|
|
19
26
|
*/
|
|
27
|
+
import { draftMaxRows } from '../composer-limits.js';
|
|
20
28
|
import { frameHeight, MIN_FULLSCREEN_ROWS } from './frame.js';
|
|
21
29
|
/** Below this many rows the composer's hint line is dropped to buy back a row. */
|
|
22
30
|
export const HINT_MIN_ROWS = 20;
|
|
23
|
-
|
|
31
|
+
/**
|
|
32
|
+
* The composer's draft rows, clamped to what this terminal height allows.
|
|
33
|
+
*
|
|
34
|
+
* Exported so the reporter and the budget clamp with ONE function: I-8 requires
|
|
35
|
+
* the rendered row count and the number handed to `viewportRows` to be the same
|
|
36
|
+
* number, and two clamps are how they stop being.
|
|
37
|
+
*/
|
|
38
|
+
export function clampDraftRows(rows, draftRows) {
|
|
39
|
+
const wanted = Number.isFinite(draftRows) ? Math.floor(draftRows) : 1;
|
|
40
|
+
return Math.max(1, Math.min(wanted, draftMaxRows(rows)));
|
|
41
|
+
}
|
|
42
|
+
export function chromeBudget(rows, draftRows = 1) {
|
|
24
43
|
return {
|
|
25
44
|
header: 1,
|
|
26
45
|
toast: 1,
|
|
27
|
-
composer: rows >= HINT_MIN_ROWS ?
|
|
46
|
+
composer: 2 + clampDraftRows(rows, draftRows) + (rows >= HINT_MIN_ROWS ? 1 : 0),
|
|
28
47
|
status: 1,
|
|
29
48
|
};
|
|
30
49
|
}
|
|
@@ -35,13 +54,26 @@ export function chromeBudget(rows) {
|
|
|
35
54
|
* that argument is the guarantee that the first submitted message cannot make
|
|
36
55
|
* the layout jump (A-4). Do not add it back.
|
|
37
56
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
57
|
+
* `draftRows` IS NOT THAT ARGUMENT, and the distinction is the whole of D-12.
|
|
58
|
+
* A-4 forbids making the budget depend on TRANSCRIPT content, so that the first
|
|
59
|
+
* submitted message cannot move the layout. `draftRows` depends only on what the
|
|
60
|
+
* user is typing right now, and its whole purpose is that the jump is ALREADY
|
|
61
|
+
* HAPPENING -- Yoga takes the rows out of the transcript whether or not this
|
|
62
|
+
* function was told. `App` adds the hysteresis (grow now, shrink one tick later)
|
|
63
|
+
* that keeps it from becoming per-keystroke jitter.
|
|
64
|
+
*
|
|
65
|
+
* It DEFAULTS TO 1, i.e. to the constant this function used to assume, so every
|
|
66
|
+
* pre-existing caller and every pre-existing assertion is byte-identical -- and
|
|
67
|
+
* `viewportRows.length` stays 1, because a parameter with a default does not
|
|
68
|
+
* count toward it.
|
|
69
|
+
*
|
|
70
|
+
* Non-decreasing in `rows` at a fixed `draftRows = 1`: the one step where chrome
|
|
71
|
+
* grows (the hint row at `HINT_MIN_ROWS`) coincides with `rows` growing by one,
|
|
72
|
+
* so the net change is 0 rather than -1.
|
|
41
73
|
*/
|
|
42
|
-
export function viewportRows(rows) {
|
|
74
|
+
export function viewportRows(rows, draftRows = 1) {
|
|
43
75
|
if (!Number.isFinite(rows) || rows < MIN_FULLSCREEN_ROWS)
|
|
44
76
|
return 0;
|
|
45
|
-
const c = chromeBudget(rows);
|
|
77
|
+
const c = chromeBudget(rows, draftRows);
|
|
46
78
|
return Math.max(0, frameHeight(rows) - c.header - c.toast - c.composer - c.status);
|
|
47
79
|
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Follow rules for the self-drawn viewport (tui-selection-and-scroll-follow
|
|
3
|
+
* §4.3). PURE, React-free, unit-tested — the shape `scroll.ts`,
|
|
4
|
+
* `virtual-window.ts` and `frame-differ.ts` all already use.
|
|
5
|
+
*
|
|
6
|
+
* It lives here rather than inside `ScrollViewport` because
|
|
7
|
+
* `ink-testing-library`'s stdout stub reports no height, so a MOUNTED viewport
|
|
8
|
+
* measures `content === viewport`, `overflowLines === 0`, and neither rule below
|
|
9
|
+
* ever engages (`__tests__/render-at-width.ts`, and `transcript-virtual.test.tsx`
|
|
10
|
+
* records the same limitation and the same workaround). A state machine that can
|
|
11
|
+
* only be exercised through a box the test cannot size is a state machine with
|
|
12
|
+
* no tests (P1-10).
|
|
13
|
+
*
|
|
14
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
15
|
+
* NOTHING IN THIS FILE MAY READ A MEASUREMENT (AC-17, and it is a `grep`).
|
|
16
|
+
*
|
|
17
|
+
* The gate is a `grep` for the name of Ink's element-measuring helper returning
|
|
18
|
+
* NOTHING from this file, so that name is deliberately left unspelled here and
|
|
19
|
+
* below, even where writing it would read more directly. A grep a COMMENT can
|
|
20
|
+
* trip is a grep whoever runs it learns to ignore, and then the import it exists
|
|
21
|
+
* to catch walks straight past it — the same reason `glyphs.test.ts` strips
|
|
22
|
+
* comments before judging a line.
|
|
23
|
+
*
|
|
24
|
+
* The transcript's MEASURED CONTENT HEIGHT — what `ScrollViewport` reads out of
|
|
25
|
+
* Ink's layout for the inner box — is the height of everything the transcript is
|
|
26
|
+
* currently drawing, and FOUR different things move it — only one of which is
|
|
27
|
+
* "the agent produced output":
|
|
28
|
+
*
|
|
29
|
+
* 1. rows appended at the tail <- Rule A must react
|
|
30
|
+
* 2. an entry ABOVE the reading position changing <- must NOT move
|
|
31
|
+
* height (Ctrl+T, Ctrl+O, a first real measurement
|
|
32
|
+
* of an entry that stood on an estimate)
|
|
33
|
+
* 3. the scroll horizon dropping entries off the <- must NOT move
|
|
34
|
+
* FRONT (`Transcript.tsx`, `entries.slice(-size)`)
|
|
35
|
+
* 4. a re-wrap after a resize <- must NOT move
|
|
36
|
+
*
|
|
37
|
+
* For (2) and (3) `virtual-window.ts`'s invariant V-4 is explicit: counting the
|
|
38
|
+
* offset FROM THE BOTTOM makes an above-the-viewport height change
|
|
39
|
+
* self-compensating, and that is what buys virtualisation its "no
|
|
40
|
+
* scroll-anchoring compensation pass required". Reacting to total content growth
|
|
41
|
+
* would make Rule A that compensation pass, applied to changes that never needed
|
|
42
|
+
* compensating — and it would additionally make (3) look like new output and arm
|
|
43
|
+
* the idle timer on a transcript that produced nothing, the exact failure G6
|
|
44
|
+
* exists to prevent.
|
|
45
|
+
*
|
|
46
|
+
* There is a second, sharper reason. `selectWindow` TAKES `offset` AS AN INPUT,
|
|
47
|
+
* so the mounted set — and therefore which entries are measured rather than
|
|
48
|
+
* estimated — is a function of the offset. An offset that is in turn a function
|
|
49
|
+
* of the measured height closes a loop: offset -> mounted set -> measured
|
|
50
|
+
* heights -> content height -> offset. The loop is broken by the INPUT rather
|
|
51
|
+
* than by a convergence argument: `advanceTailRows` reads the height table and
|
|
52
|
+
* never the geometry context (P0-1 / D-13).
|
|
53
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
54
|
+
*/
|
|
55
|
+
export interface FollowInput {
|
|
56
|
+
/** Current RENDERED (already clamped) offset — see the caller's P1-9 note. */
|
|
57
|
+
offset: number;
|
|
58
|
+
overflowLines: number;
|
|
59
|
+
/** Signed: rows appended at (or removed from) the tail since the last frame. */
|
|
60
|
+
tailDelta: number;
|
|
61
|
+
/** True while a selection drag holds the viewport (§4.4). */
|
|
62
|
+
hold: boolean;
|
|
63
|
+
newLinesWhilePaused: number;
|
|
64
|
+
}
|
|
65
|
+
export interface FollowOutput {
|
|
66
|
+
offset: number;
|
|
67
|
+
newLinesWhilePaused: number;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Rule A. When rows are appended AT THE TAIL while the user is not pinned to the
|
|
71
|
+
* bottom, grow the offset by the same amount: `shiftUp = overflowLines - offset`
|
|
72
|
+
* is then constant and the rows on screen do not move.
|
|
73
|
+
*
|
|
74
|
+
* SIGNED, NOT GROWTH-ONLY (I-7). A tail SHRINK is rows that no longer exist
|
|
75
|
+
* below the reading position, so shrinking the offset by the same amount is what
|
|
76
|
+
* keeps the screen still — and it is self-correcting when the source of the
|
|
77
|
+
* delta was an upper-bound height estimate that a later measurement brings down
|
|
78
|
+
* (`estimateEntryRows` is deliberately an upper bound). An unsigned rule
|
|
79
|
+
* over-counts once and never finds its way back.
|
|
80
|
+
*/
|
|
81
|
+
export declare function reduceFollow(input: FollowInput): FollowOutput;
|
|
82
|
+
export interface ResumeInput {
|
|
83
|
+
/** `scrollResumeMs`; `0` (or anything non-finite) disables auto-resume. */
|
|
84
|
+
resumeMs: number;
|
|
85
|
+
offset: number;
|
|
86
|
+
newLinesWhilePaused: number;
|
|
87
|
+
hold: boolean;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Rule B's three conditions, and they are three rather than one on purpose:
|
|
91
|
+
*
|
|
92
|
+
* 1. `offset > 0` — there is somewhere to return FROM;
|
|
93
|
+
* 2. `newLines > 0` — something arrived that is worth returning TO. THIS IS
|
|
94
|
+
* G6: a user reading a FINISHED transcript is never yanked to the bottom,
|
|
95
|
+
* which is the new bug that a timing-only rule would ship wearing the fix's
|
|
96
|
+
* clothes (D-3);
|
|
97
|
+
* 3. `!hold` — no selection drag is in progress (§4.4).
|
|
98
|
+
*
|
|
99
|
+
* AC-9 rides on the first line: with `scrollResumeMs: 0` this returns `false`
|
|
100
|
+
* before anything else is considered, so no timer is ever created.
|
|
101
|
+
*/
|
|
102
|
+
export declare function shouldArmResume(input: ResumeInput): boolean;
|
|
103
|
+
/**
|
|
104
|
+
* What the transcript publishes through a ref written during render.
|
|
105
|
+
*
|
|
106
|
+
* `lastHeight` is not decoration: §4.3.1a defines the per-frame delta as "heights
|
|
107
|
+
* of entries after the previously-last entry PLUS the delta of that entry's own
|
|
108
|
+
* height (a streaming entry grows in place)", and the second term cannot be
|
|
109
|
+
* computed without remembering what that height was.
|
|
110
|
+
*/
|
|
111
|
+
export interface TailState {
|
|
112
|
+
/** Monotonic-ish cumulative count of rows produced at the tail. Signed deltas. */
|
|
113
|
+
rows: number;
|
|
114
|
+
/** Identity anchor — the last entry's id when this state was written. */
|
|
115
|
+
lastId: string | null;
|
|
116
|
+
/** That entry's height when this state was written. */
|
|
117
|
+
lastHeight: number;
|
|
118
|
+
}
|
|
119
|
+
export interface TailSink {
|
|
120
|
+
current: TailState;
|
|
121
|
+
}
|
|
122
|
+
export declare function emptyTailState(): TailState;
|
|
123
|
+
/**
|
|
124
|
+
* Advance the tail counter by one frame.
|
|
125
|
+
*
|
|
126
|
+
* `ids` are the entries the transcript is drawing, in order; `heightOf(index)`
|
|
127
|
+
* is the transcript's own per-entry height table (measured where it exists, an
|
|
128
|
+
* upper-bound estimate where it does not). NO GEOMETRY IS READ HERE, which is
|
|
129
|
+
* what breaks P0-1's loop by construction.
|
|
130
|
+
*
|
|
131
|
+
* RE-ANCHORING CONTRIBUTES ZERO (R-11). When the scroll horizon drops the entry
|
|
132
|
+
* this counter was anchored on, the anchor is simply moved to the new last entry
|
|
133
|
+
* and the frame contributes a delta of 0. Losing one frame of anchoring is a row
|
|
134
|
+
* of drift at worst; treating a FRONT-DROP as tail motion is a visible jump, and
|
|
135
|
+
* an unbounded one.
|
|
136
|
+
*/
|
|
137
|
+
export declare function advanceTailRows(prev: TailState, ids: readonly string[], heightOf: (index: number) => number): TailState;
|