@aragon-agent/cli 0.6.7 → 0.6.9
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 +27 -0
- package/README.md +107 -77
- package/dist/agent/controller.d.ts +11 -7
- package/dist/agent/controller.js +35 -48
- package/dist/agent/headless.d.ts +2 -1
- package/dist/agent/prompt-options.d.ts +15 -0
- package/dist/agent/prompt-options.js +1 -0
- package/dist/agent/reducer.d.ts +7 -113
- package/dist/agent/reducer.js +0 -62
- package/dist/cli.js +111 -182
- package/dist/commands/builtins.js +3 -1
- package/dist/commands/perf.d.ts +32 -2
- package/dist/commands/perf.js +32 -2
- package/dist/commands/registry.d.ts +4 -5
- package/dist/config/env.js +0 -130
- package/dist/config/load.d.ts +0 -2
- package/dist/config/load.js +2 -31
- package/dist/config/schema.d.ts +1 -14
- package/dist/config/schema.js +0 -1
- package/dist/config/store.js +2 -0
- package/dist/input/interrupt-gesture.d.ts +18 -0
- package/dist/input/interrupt-gesture.js +19 -0
- package/dist/input/pointer-router.d.ts +11 -0
- package/dist/input/pointer-router.js +14 -0
- package/dist/logging/install.d.ts +2 -2
- package/dist/logging/install.js +2 -2
- package/dist/todo/follow-through.d.ts +0 -4
- package/dist/todo/follow-through.js +0 -4
- package/dist/todo/limits.d.ts +0 -10
- package/dist/todo/limits.js +0 -10
- package/dist/todo/prompt.d.ts +0 -15
- package/dist/todo/store.d.ts +8 -13
- package/dist/todo/store.js +13 -14
- package/dist/todo/types.d.ts +2 -1
- package/dist/ui/ActivityLine.d.ts +26 -34
- package/dist/ui/ActivityLine.js +30 -19
- package/dist/ui/App.d.ts +3 -41
- package/dist/ui/App.js +223 -247
- package/dist/ui/BottomStatusRow.d.ts +1 -3
- package/dist/ui/BottomStatusRow.js +4 -8
- package/dist/ui/Composer.d.ts +38 -5
- package/dist/ui/Composer.js +45 -23
- package/dist/ui/Header.d.ts +2 -16
- package/dist/ui/Header.js +3 -18
- package/dist/ui/Logo.d.ts +1 -17
- package/dist/ui/Logo.js +0 -16
- package/dist/ui/ModeChip.d.ts +7 -18
- package/dist/ui/ModeChip.js +9 -0
- package/dist/ui/PromptCaret.d.ts +11 -0
- package/dist/ui/PromptCaret.js +31 -0
- package/dist/ui/PromptInput.d.ts +10 -33
- package/dist/ui/PromptInput.js +37 -20
- package/dist/ui/StatusBar.d.ts +1 -11
- package/dist/ui/StatusBar.js +14 -8
- package/dist/ui/ToastStack.d.ts +2 -18
- package/dist/ui/ToastStack.js +4 -11
- package/dist/ui/Transcript.d.ts +5 -71
- package/dist/ui/Transcript.js +10 -199
- package/dist/ui/composer-rows.js +50 -9
- package/dist/ui/entries/AssistantEntry.d.ts +0 -23
- package/dist/ui/entries/AssistantEntry.js +3 -4
- package/dist/ui/entries/DiffView.d.ts +1 -7
- package/dist/ui/entries/DiffView.js +2 -2
- package/dist/ui/entries/ToolCard.d.ts +0 -6
- package/dist/ui/entries/ToolCard.js +4 -13
- package/dist/ui/frame-differ.d.ts +13 -0
- package/dist/ui/frame-differ.js +37 -24
- package/dist/ui/frame-observer.d.ts +17 -0
- package/dist/ui/frame-observer.js +81 -0
- package/dist/ui/frame-parser.d.ts +7 -0
- package/dist/ui/frame-parser.js +18 -0
- package/dist/ui/ink-runtime.d.ts +1 -0
- package/dist/ui/ink-runtime.js +21 -0
- package/dist/ui/layout/AppShell.d.ts +4 -58
- package/dist/ui/layout/AppShell.js +3 -6
- package/dist/ui/layout/OverlayFrame.d.ts +1 -25
- package/dist/ui/layout/OverlayFrame.js +0 -24
- package/dist/ui/layout/ScrollIndicator.d.ts +4 -3
- package/dist/ui/layout/ScrollIndicator.js +4 -4
- package/dist/ui/layout/ScrollViewport.d.ts +16 -66
- package/dist/ui/layout/ScrollViewport.js +195 -185
- package/dist/ui/layout/budget.d.ts +3 -51
- package/dist/ui/layout/budget.js +4 -53
- package/dist/ui/layout/document-layout.d.ts +13 -0
- package/dist/ui/layout/document-layout.js +11 -0
- package/dist/ui/layout/follow-state.d.ts +1 -0
- package/dist/ui/layout/follow-state.js +1 -3
- package/dist/ui/layout/frame.d.ts +0 -22
- package/dist/ui/layout/frame.js +0 -36
- package/dist/ui/layout/scroll-indicator.d.ts +1 -1
- package/dist/ui/layout/scroll-indicator.js +3 -2
- package/dist/ui/layout/scrollbar-geometry.d.ts +17 -0
- package/dist/ui/layout/scrollbar-geometry.js +13 -0
- package/dist/ui/layout/todo-layout.d.ts +2 -3
- package/dist/ui/layout/todo-layout.js +9 -9
- package/dist/ui/layout/viewport-geometry.d.ts +2 -1
- package/dist/ui/layout/viewport-geometry.js +2 -1
- package/dist/ui/layout/virtual-window.d.ts +1 -0
- package/dist/ui/layout/virtual-window.js +1 -1
- package/dist/ui/live-clamp.d.ts +1 -29
- package/dist/ui/live-clamp.js +1 -29
- package/dist/ui/overlays/HelpOverlay.js +6 -2
- package/dist/ui/run-status-row.d.ts +61 -0
- package/dist/ui/run-status-row.js +70 -0
- package/dist/ui/screen.d.ts +1 -1
- package/dist/ui/screen.js +4 -6
- package/dist/ui/scrollbar-controller.d.ts +30 -0
- package/dist/ui/scrollbar-controller.js +124 -0
- package/dist/ui/transcript-text.d.ts +0 -8
- package/dist/ui/transcript-text.js +0 -8
- package/dist/ui/use-startup-notices.d.ts +1 -57
- package/dist/ui/use-startup-notices.js +3 -54
- package/dist/ui/use-wheel-routing.d.ts +3 -1
- package/dist/ui/use-wheel-routing.js +6 -0
- package/package.json +2 -2
- package/dist/ui/TodoStrip.d.ts +0 -36
- package/dist/ui/TodoStrip.js +0 -32
|
@@ -1,204 +1,214 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
/**
|
|
3
|
-
* ScrollViewport (spec §4.5) — a self-drawn scrolling window built from Ink's
|
|
4
|
-
* negative `marginTop` + `overflow: hidden` + `measureElement`.
|
|
5
|
-
*
|
|
6
|
-
* Once the frame occupies the whole screen, `<Static>` history is pushed above
|
|
7
|
-
* the frame where only one line is visible, so native scrollback stops being a
|
|
8
|
-
* usable mechanism and the app has to own scrolling. That is why this exists.
|
|
9
|
-
*
|
|
10
|
-
* SINGLE SOURCE OF TRUTH (review P1-7): both the metrics and the scroll offset
|
|
11
|
-
* live here. The parent only pushes an *intent* down and receives one derived
|
|
12
|
-
* *display* number back. Holding the offset in the parent as well would create a
|
|
13
|
-
* measure → setState → re-render → measure loop whose convergence is provable
|
|
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`.
|
|
26
|
-
*/
|
|
27
2
|
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
28
3
|
import { Box, measureElement, useStdout } from 'ink';
|
|
4
|
+
import { createScrollbarController } from '../scrollbar-controller.js';
|
|
29
5
|
import { ViewportGeometryContext } from './viewport-geometry.js';
|
|
30
|
-
import { applyScrollTimes
|
|
6
|
+
import { applyScrollTimes } from './scroll.js';
|
|
31
7
|
import { reduceFollow, shouldArmResume } from './follow-state.js';
|
|
32
8
|
import { thumbRange } from './scroll-indicator.js';
|
|
33
|
-
import {
|
|
34
|
-
|
|
9
|
+
import { ScrollIndicator } from './ScrollIndicator.js';
|
|
10
|
+
import { buildDocumentLayout } from './document-layout.js';
|
|
11
|
+
/**
|
|
12
|
+
* Whether `element` lies fully inside `clip`. A missing element (not mounted) or
|
|
13
|
+
* clip reports `true`, so a caller that consumes the answer must first know the
|
|
14
|
+
* element is actually mounted. Rows are one line tall, so "partially visible"
|
|
15
|
+
* cannot occur for the run status row.
|
|
16
|
+
*/
|
|
17
|
+
function elementVisible(element, clip) {
|
|
18
|
+
if (!element || !clip)
|
|
19
|
+
return true;
|
|
20
|
+
let top = 0;
|
|
21
|
+
let current = element;
|
|
22
|
+
while (current && current !== clip) {
|
|
23
|
+
top += current.yogaNode?.getComputedTop() ?? 0;
|
|
24
|
+
current = current.parentNode;
|
|
25
|
+
}
|
|
26
|
+
return current === clip && top >= 0 &&
|
|
27
|
+
top + measureElement(element).height <= measureElement(clip).height;
|
|
28
|
+
}
|
|
29
|
+
/** One offset for messages, padding and editor; hidden trees keep their local state. */
|
|
30
|
+
export function ScrollViewport(props) {
|
|
35
31
|
const { stdout } = useStdout();
|
|
36
|
-
const
|
|
32
|
+
const { active = true, hold = false, resumeMs = 0, scrollbar } = props;
|
|
33
|
+
const cols = props.cols ?? stdout?.columns ?? 80;
|
|
37
34
|
const clipRef = useRef(null);
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
const [
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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);
|
|
35
|
+
const bodyRef = useRef(null);
|
|
36
|
+
const footerRef = useRef(null);
|
|
37
|
+
const [{ metrics, offset }, setGeometry] = useState({
|
|
38
|
+
metrics: { viewport: 0, content: 0, padding: 0, footer: 0 }, offset: 0,
|
|
39
|
+
});
|
|
40
|
+
const pendingMetrics = useRef(metrics);
|
|
41
|
+
const geometryScheduled = useRef(false);
|
|
42
|
+
const mounted = useRef(true);
|
|
43
|
+
const [dragging, setDragging] = useState(false);
|
|
44
|
+
const offsetRef = useRef(0);
|
|
45
|
+
const baseline = useRef(null);
|
|
46
|
+
const latest = useRef({ active, hold, resumeMs, metrics, scrollbar });
|
|
47
|
+
latest.current = { active, hold, resumeMs, metrics, scrollbar };
|
|
48
|
+
const pausedLines = useRef(0);
|
|
66
49
|
const resumeTimer = useRef(null);
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
50
|
+
const lastIntent = useRef(props.intent?.nonce);
|
|
51
|
+
const lastPin = useRef(props.pinToBottomNonce);
|
|
52
|
+
const lastReset = useRef(props.resetKey);
|
|
53
|
+
const lastVisible = useRef();
|
|
54
|
+
const lastActivityVisible = useRef();
|
|
55
|
+
const previousActive = useRef(active);
|
|
56
|
+
const geometryRevision = useRef(0);
|
|
57
|
+
const geometryBounds = useRef('');
|
|
58
|
+
const clearResume = useCallback(() => {
|
|
59
|
+
if (resumeTimer.current)
|
|
60
|
+
clearTimeout(resumeTimer.current);
|
|
71
61
|
resumeTimer.current = null;
|
|
72
62
|
}, []);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
63
|
+
const publishGeometry = useCallback(() => {
|
|
64
|
+
if (geometryScheduled.current)
|
|
65
|
+
return;
|
|
66
|
+
geometryScheduled.current = true;
|
|
67
|
+
queueMicrotask(() => {
|
|
68
|
+
geometryScheduled.current = false;
|
|
69
|
+
if (!mounted.current)
|
|
70
|
+
return;
|
|
71
|
+
const nextMetrics = pendingMetrics.current;
|
|
72
|
+
const nextOffset = offsetRef.current;
|
|
73
|
+
setGeometry((previous) => previous.offset === nextOffset &&
|
|
74
|
+
Object.keys(nextMetrics).every((key) => previous.metrics[key] ===
|
|
75
|
+
nextMetrics[key])
|
|
76
|
+
? previous : { metrics: nextMetrics, offset: nextOffset });
|
|
77
|
+
});
|
|
78
|
+
}, []);
|
|
79
|
+
const publishOffset = useCallback((next) => {
|
|
80
|
+
offsetRef.current = next;
|
|
81
|
+
if (next === 0)
|
|
82
|
+
pausedLines.current = 0;
|
|
83
|
+
publishGeometry();
|
|
84
|
+
}, [publishGeometry]);
|
|
85
|
+
const isHeld = useCallback(() => latest.current.hold ||
|
|
86
|
+
!!latest.current.scrollbar?.controller?.isCaptured(), []);
|
|
83
87
|
const armResume = useCallback(() => {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
offset: offsetRef.current,
|
|
88
|
-
newLinesWhilePaused: newLinesRef.current,
|
|
89
|
-
hold: holdRef.current,
|
|
90
|
-
})) {
|
|
88
|
+
clearResume();
|
|
89
|
+
const current = latest.current;
|
|
90
|
+
if (!current.active || !shouldArmResume({ resumeMs: current.resumeMs,
|
|
91
|
+
offset: offsetRef.current, newLinesWhilePaused: pausedLines.current, hold: isHeld() }))
|
|
91
92
|
return;
|
|
92
|
-
|
|
93
|
-
const timer = setTimeout(() => {
|
|
93
|
+
resumeTimer.current = setTimeout(() => {
|
|
94
94
|
resumeTimer.current = null;
|
|
95
|
-
if (
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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]);
|
|
105
|
-
// Ink calculates yoga layout in `onRender`, which React runs BEFORE layout
|
|
106
|
-
// effects, so the measurement below reflects the commit that just happened.
|
|
107
|
-
// The children have already rendered, so `tailRowsRef` is current too.
|
|
95
|
+
if (latest.current.active && !isHeld())
|
|
96
|
+
publishOffset(0);
|
|
97
|
+
}, current.resumeMs);
|
|
98
|
+
resumeTimer.current.unref?.();
|
|
99
|
+
}, [clearResume, isHeld, publishOffset]);
|
|
108
100
|
useLayoutEffect(() => {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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,
|
|
101
|
+
if (!scrollbar)
|
|
102
|
+
return;
|
|
103
|
+
const controller = createScrollbarController({
|
|
104
|
+
getGeometry: () => scrollbar.geometry,
|
|
105
|
+
isEnabled: () => latest.current.active && scrollbar.frameReady && scrollbar.isEnabled(),
|
|
106
|
+
setOffset: (next) => { publishOffset(next); armResume(); },
|
|
107
|
+
page: (kind) => {
|
|
108
|
+
publishOffset(applyScrollTimes({ offset: offsetRef.current }, kind, latest.current.metrics, 1).offset);
|
|
109
|
+
},
|
|
110
|
+
onStart: () => { clearResume(); scrollbar.onStart?.(); },
|
|
111
|
+
onHold: (held) => { setDragging(held); armResume(); },
|
|
130
112
|
});
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
113
|
+
scrollbar.controller = controller;
|
|
114
|
+
return () => {
|
|
115
|
+
controller.dispose();
|
|
116
|
+
scrollbar.controller = null;
|
|
117
|
+
scrollbar.geometry = null;
|
|
118
|
+
scrollbar.invalidate();
|
|
119
|
+
};
|
|
120
|
+
}, [scrollbar, publishOffset, armResume, clearResume]);
|
|
121
|
+
useEffect(() => clearResume, [clearResume]);
|
|
122
|
+
useLayoutEffect(() => {
|
|
123
|
+
mounted.current = true;
|
|
124
|
+
return () => { mounted.current = false; };
|
|
125
|
+
}, []);
|
|
126
|
+
useEffect(() => { armResume(); }, [hold, resumeMs, armResume]);
|
|
127
|
+
useLayoutEffect(() => {
|
|
128
|
+
const reset = lastReset.current !== props.resetKey;
|
|
129
|
+
lastReset.current = props.resetKey;
|
|
130
|
+
const pin = lastPin.current !== props.pinToBottomNonce;
|
|
131
|
+
lastPin.current = props.pinToBottomNonce;
|
|
132
|
+
const intent = lastIntent.current !== props.intent?.nonce ? props.intent : undefined;
|
|
133
|
+
lastIntent.current = props.intent?.nonce;
|
|
134
|
+
if (reset) {
|
|
135
|
+
baseline.current = null;
|
|
136
|
+
pausedLines.current = 0;
|
|
137
|
+
publishOffset(0);
|
|
138
|
+
scrollbar?.controller?.cancel();
|
|
139
139
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
// without the guard, closing an overlay replays whatever wheel gesture was in
|
|
147
|
-
// flight when it opened, and the transcript jumps a page on its own. That is
|
|
148
|
-
// R-P1-7, which this codebase has already paid for once through the keyboard
|
|
149
|
-
// path; the overlay-swallowing rule in `App`'s key handler becomes a SECOND
|
|
150
|
-
// line of defence rather than the only one.
|
|
151
|
-
const lastAppliedNonce = useRef(intent?.nonce);
|
|
152
|
-
const intentNonce = intent?.nonce ?? 0;
|
|
153
|
-
useEffect(() => {
|
|
154
|
-
const current = intentRef.current;
|
|
155
|
-
if (!current)
|
|
140
|
+
if (!active) {
|
|
141
|
+
clearResume();
|
|
142
|
+
scrollbar?.invalidate();
|
|
143
|
+
if (scrollbar)
|
|
144
|
+
scrollbar.geometry = null;
|
|
145
|
+
previousActive.current = false;
|
|
156
146
|
return;
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
147
|
+
}
|
|
148
|
+
const viewport = clipRef.current ? measureElement(clipRef.current).height : 0;
|
|
149
|
+
const body = bodyRef.current ? measureElement(bodyRef.current).height : 0;
|
|
150
|
+
const footer = footerRef.current ? measureElement(footerRef.current).height : 0;
|
|
151
|
+
const layout = buildDocumentLayout({ rows: viewport + 4, bodyRows: body, footerRows: footer });
|
|
152
|
+
const padding = props.footer === undefined ? 0 : layout.paddingRows;
|
|
153
|
+
const content = body + footer + padding;
|
|
154
|
+
const tail = props.tailRowsRef?.current.rows ?? 0;
|
|
155
|
+
const old = baseline.current;
|
|
156
|
+
const reflow = !old || old.cols !== cols || old.viewport !== viewport;
|
|
157
|
+
const tailDelta = reflow ? 0 : tail - old.tail;
|
|
158
|
+
const footerDelta = reflow ? 0 : footer - old.footer;
|
|
159
|
+
const paddingDelta = !old || reflow || props.footer === undefined ? 0 :
|
|
160
|
+
Math.max(0, viewport - old.body - old.footer - tailDelta - footerDelta) - old.padding;
|
|
161
|
+
const nextMetrics = { viewport, content, padding, footer };
|
|
162
|
+
latest.current.metrics = nextMetrics;
|
|
163
|
+
// Markdown estimates may take more than 50 measurements to converge in a
|
|
164
|
+
// tall viewport. Publishing during the layout commit recursively mounts
|
|
165
|
+
// another window before the height store's microtask can finish. Coalesce
|
|
166
|
+
// geometry with that store outside the commit; keep measuring every frame.
|
|
167
|
+
// Offset clamps must join the same publication: at the top, each corrected
|
|
168
|
+
// estimate shrinks the overflow and would otherwise recurse via setOffset.
|
|
169
|
+
pendingMetrics.current = nextMetrics;
|
|
170
|
+
publishGeometry();
|
|
171
|
+
const followed = reduceFollow({ offset: offsetRef.current,
|
|
172
|
+
overflowLines: Math.max(0, content - viewport), tailDelta,
|
|
173
|
+
layoutTailDelta: footerDelta + paddingDelta, hold: isHeld(),
|
|
174
|
+
newLinesWhilePaused: pausedLines.current });
|
|
175
|
+
pausedLines.current = followed.newLinesWhilePaused;
|
|
176
|
+
let next = pin || reset ? 0 : followed.offset;
|
|
177
|
+
if (!pin && intent)
|
|
178
|
+
next = applyScrollTimes({ offset: next }, intent.kind, nextMetrics, intent.repeat ?? 1).offset;
|
|
179
|
+
publishOffset(next);
|
|
180
|
+
baseline.current = { viewport, content, body, footer, padding, cols, tail };
|
|
181
|
+
if (tailDelta !== 0 || pin || intent || !previousActive.current)
|
|
182
|
+
armResume();
|
|
183
|
+
previousActive.current = true;
|
|
184
|
+
const bounds = `${cols}:${stdout?.columns}:${stdout?.rows}:${viewport}`;
|
|
185
|
+
if (bounds !== geometryBounds.current) {
|
|
186
|
+
geometryBounds.current = bounds;
|
|
187
|
+
geometryRevision.current++;
|
|
188
|
+
scrollbar?.invalidate();
|
|
189
|
+
}
|
|
190
|
+
if (scrollbar) {
|
|
191
|
+
scrollbar.geometry = { trackTop: 2, trackCol: stdout?.columns ?? cols,
|
|
192
|
+
trackRows: viewport, contentRows: content, offset: next,
|
|
193
|
+
thumb: thumbRange(viewport, content, next, true), revision: geometryRevision.current };
|
|
194
|
+
scrollbar.onGeometry?.();
|
|
195
|
+
}
|
|
196
|
+
const visible = elementVisible(props.composerRef?.current ?? null, clipRef.current);
|
|
197
|
+
if (lastVisible.current !== visible) {
|
|
198
|
+
lastVisible.current = visible;
|
|
199
|
+
props.onComposerVisibilityChange?.(visible);
|
|
200
|
+
}
|
|
201
|
+
const activityVisible = elementVisible(props.activityRef?.current ?? null, clipRef.current);
|
|
202
|
+
if (lastActivityVisible.current !== activityVisible) {
|
|
203
|
+
lastActivityVisible.current = activityVisible;
|
|
204
|
+
props.onActivityVisibilityChange?.(activityVisible);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
const shiftUp = Math.max(0, metrics.content - metrics.viewport - offset);
|
|
208
|
+
useEffect(() => { props.onScrolledLinesChange?.(offset); }, [offset, props.onScrolledLinesChange]);
|
|
209
|
+
useEffect(() => { if (active)
|
|
210
|
+
props.onViewportShiftChange?.(shiftUp); }, [active, shiftUp, props.onViewportShiftChange]);
|
|
211
|
+
const geometry = useMemo(() => ({ viewportRows: metrics.viewport, offset,
|
|
212
|
+
contentRows: metrics.content, trailingContentRows: metrics.padding + metrics.footer }), [metrics, offset]);
|
|
213
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: [_jsxs(Box, { display: active ? 'flex' : 'none', flexDirection: "row", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: [_jsx(Box, { ref: clipRef, flexDirection: "column", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsxs(Box, { flexDirection: "column", flexShrink: 0, marginTop: -shiftUp, children: [_jsx(ViewportGeometryContext.Provider, { value: geometry, children: _jsx(Box, { ref: bodyRef, flexDirection: "column", flexShrink: 0, children: props.children }) }), _jsx(Box, { height: metrics.padding, flexShrink: 0 }), _jsx(Box, { ref: footerRef, flexDirection: "column", flexShrink: 0, children: props.footer })] }) }), props.rail, props.showScrollIndicator && _jsx(ScrollIndicator, { rows: metrics.viewport, thumb: thumbRange(metrics.viewport, metrics.content, offset, true), dragging: dragging, theme: props.theme, caps: props.caps })] }), _jsx(Box, { display: active ? 'none' : 'flex', flexDirection: "column", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: props.overlay ?? null })] }));
|
|
204
214
|
}
|
|
@@ -1,29 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Layout budget (spec §4.2a) — the ONE function that says how many rows the
|
|
3
|
-
* viewport gets.
|
|
4
|
-
*
|
|
5
|
-
* Before v0.4.0 this number was computed nowhere and assumed in three places,
|
|
6
|
-
* which is how the viewport ended up NON-MONOTONIC in `rows`: growing the
|
|
7
|
-
* terminal from 27 to 28 rows promoted the header to the 8-row wordmark tier and
|
|
8
|
-
* the visible content SHRANK from 18 rows to 13. Making the brand region a
|
|
9
|
-
* constant 1 row (§4.2) is what fixes that, and this module is where the fix is
|
|
10
|
-
* expressed as arithmetic so `budget.test.ts` can pin it.
|
|
11
|
-
*
|
|
12
|
-
* Deliberately NOT part of `frame.ts`: that file is the read-only foundation
|
|
13
|
-
* (invariant I-1) and this is content-layer policy.
|
|
14
|
-
*
|
|
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:
|
|
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).
|
|
26
|
-
*/
|
|
1
|
+
/** Fullscreen reserves three fixed rows; composer height is a footer layout input. */
|
|
27
2
|
/** Below this many rows the composer's hint line is dropped to buy back a row. */
|
|
28
3
|
export declare const HINT_MIN_ROWS = 20;
|
|
29
4
|
export interface ChromeBudget {
|
|
@@ -50,28 +25,5 @@ export interface ChromeBudget {
|
|
|
50
25
|
*/
|
|
51
26
|
export declare function clampDraftRows(rows: number, draftRows: number): number;
|
|
52
27
|
export declare function chromeBudget(rows: number, draftRows?: number): ChromeBudget;
|
|
53
|
-
/**
|
|
54
|
-
|
|
55
|
-
*
|
|
56
|
-
* Note what is NOT a parameter: whether the transcript is empty. The absence of
|
|
57
|
-
* that argument is the guarantee that the first submitted message cannot make
|
|
58
|
-
* the layout jump (A-4). Do not add it back.
|
|
59
|
-
*
|
|
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.
|
|
76
|
-
*/
|
|
77
|
-
export declare function viewportRows(rows: number, draftRows?: number): number;
|
|
28
|
+
/** Shared document height; the legacy draft argument remains source-compatible. */
|
|
29
|
+
export declare function viewportRows(rows: number, _draftRows?: number): number;
|
package/dist/ui/layout/budget.js
CHANGED
|
@@ -1,29 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Layout budget (spec §4.2a) — the ONE function that says how many rows the
|
|
3
|
-
* viewport gets.
|
|
4
|
-
*
|
|
5
|
-
* Before v0.4.0 this number was computed nowhere and assumed in three places,
|
|
6
|
-
* which is how the viewport ended up NON-MONOTONIC in `rows`: growing the
|
|
7
|
-
* terminal from 27 to 28 rows promoted the header to the 8-row wordmark tier and
|
|
8
|
-
* the visible content SHRANK from 18 rows to 13. Making the brand region a
|
|
9
|
-
* constant 1 row (§4.2) is what fixes that, and this module is where the fix is
|
|
10
|
-
* expressed as arithmetic so `budget.test.ts` can pin it.
|
|
11
|
-
*
|
|
12
|
-
* Deliberately NOT part of `frame.ts`: that file is the read-only foundation
|
|
13
|
-
* (invariant I-1) and this is content-layer policy.
|
|
14
|
-
*
|
|
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:
|
|
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).
|
|
26
|
-
*/
|
|
1
|
+
/** Fullscreen reserves three fixed rows; composer height is a footer layout input. */
|
|
27
2
|
import { draftMaxRows } from '../composer-limits.js';
|
|
28
3
|
import { frameHeight, MIN_FULLSCREEN_ROWS } from './frame.js';
|
|
29
4
|
/** Below this many rows the composer's hint line is dropped to buy back a row. */
|
|
@@ -47,33 +22,9 @@ export function chromeBudget(rows, draftRows = 1) {
|
|
|
47
22
|
status: 1,
|
|
48
23
|
};
|
|
49
24
|
}
|
|
50
|
-
/**
|
|
51
|
-
|
|
52
|
-
*
|
|
53
|
-
* Note what is NOT a parameter: whether the transcript is empty. The absence of
|
|
54
|
-
* that argument is the guarantee that the first submitted message cannot make
|
|
55
|
-
* the layout jump (A-4). Do not add it back.
|
|
56
|
-
*
|
|
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.
|
|
73
|
-
*/
|
|
74
|
-
export function viewportRows(rows, draftRows = 1) {
|
|
25
|
+
/** Shared document height; the legacy draft argument remains source-compatible. */
|
|
26
|
+
export function viewportRows(rows, _draftRows = 1) {
|
|
75
27
|
if (!Number.isFinite(rows) || rows < MIN_FULLSCREEN_ROWS)
|
|
76
28
|
return 0;
|
|
77
|
-
|
|
78
|
-
return Math.max(0, frameHeight(rows) - c.header - c.toast - c.composer - c.status);
|
|
29
|
+
return Math.max(0, frameHeight(rows) - 3);
|
|
79
30
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface DocumentLayoutInput {
|
|
2
|
+
rows: number;
|
|
3
|
+
bodyRows: number;
|
|
4
|
+
footerRows: number;
|
|
5
|
+
}
|
|
6
|
+
export interface DocumentLayout {
|
|
7
|
+
viewportRows: number;
|
|
8
|
+
paddingRows: number;
|
|
9
|
+
contentRows: number;
|
|
10
|
+
trailingContentRows: number;
|
|
11
|
+
}
|
|
12
|
+
/** Allocate the shared document, filling short sessions before the editor. */
|
|
13
|
+
export declare function buildDocumentLayout(input: DocumentLayoutInput): DocumentLayout;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { frameHeight } from './frame.js';
|
|
2
|
+
/** Allocate the shared document, filling short sessions before the editor. */
|
|
3
|
+
export function buildDocumentLayout(input) {
|
|
4
|
+
const count = (n) => Number.isFinite(n) ? Math.max(0, Math.floor(n)) : 0;
|
|
5
|
+
const viewportRows = count(input.rows) > 0 ? Math.max(0, frameHeight(count(input.rows)) - 3) : 0;
|
|
6
|
+
const body = count(input.bodyRows);
|
|
7
|
+
const footer = count(input.footerRows);
|
|
8
|
+
const paddingRows = Math.max(0, viewportRows - body - footer);
|
|
9
|
+
return { viewportRows, paddingRows, contentRows: body + paddingRows + footer,
|
|
10
|
+
trailingContentRows: paddingRows + footer };
|
|
11
|
+
}
|
|
@@ -58,6 +58,7 @@ export interface FollowInput {
|
|
|
58
58
|
overflowLines: number;
|
|
59
59
|
/** Signed: rows appended at (or removed from) the tail since the last frame. */
|
|
60
60
|
tailDelta: number;
|
|
61
|
+
layoutTailDelta?: number;
|
|
61
62
|
/** True while a selection drag holds the viewport (§4.4). */
|
|
62
63
|
hold: boolean;
|
|
63
64
|
newLinesWhilePaused: number;
|
|
@@ -74,9 +74,7 @@ export function reduceFollow(input) {
|
|
|
74
74
|
// of the drag (T-20).
|
|
75
75
|
if (offset === 0 && !hold)
|
|
76
76
|
return { offset, newLinesWhilePaused: 0 };
|
|
77
|
-
|
|
78
|
-
return { offset, newLinesWhilePaused: input.newLinesWhilePaused };
|
|
79
|
-
const next = clampScroll(offset + tailDelta, overflowLines);
|
|
77
|
+
const next = clampScroll(offset + tailDelta + (input.layoutTailDelta ?? 0), overflowLines);
|
|
80
78
|
return {
|
|
81
79
|
offset: next,
|
|
82
80
|
// Only real arrivals arm the resume timer, and reaching the bottom by ANY
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
* Two pure functions with zero React dependency so the load-bearing invariant
|
|
5
5
|
* I-1 (`frameHeight(r) < r`) can be pinned by a unit test rather than by hope.
|
|
6
6
|
*/
|
|
7
|
-
export type RenderMode = 'fullscreen' | 'inline';
|
|
8
7
|
/** Below this many rows a full-screen frame has no usable viewport left. */
|
|
9
8
|
export declare const MIN_FULLSCREEN_ROWS = 12;
|
|
10
9
|
/** Below this many columns the chrome (composer + status bar) cannot line-wrap sanely. */
|
|
@@ -35,24 +34,3 @@ export declare const FALLBACK_COLS = 80;
|
|
|
35
34
|
* terminal is strictly better than a per-frame full-screen repaint.
|
|
36
35
|
*/
|
|
37
36
|
export declare function frameHeight(rows: number | undefined): number;
|
|
38
|
-
interface StdoutLike {
|
|
39
|
-
isTTY?: boolean;
|
|
40
|
-
rows?: number;
|
|
41
|
-
columns?: number;
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Decide the render mode once, at startup (never re-decided at run time —
|
|
45
|
-
* entering/leaving the alt-screen mid-session is a state machine we do not want).
|
|
46
|
-
*
|
|
47
|
-
* Order is semantics, not style:
|
|
48
|
-
* 1. an explicit opt-out always wins (users can always get v0.2.0 back);
|
|
49
|
-
* 2. `isTTY` is the one gate `--fullscreen` cannot override — writing ANSI into
|
|
50
|
-
* a pipe or a redirected file is a contract violation, whereas a heuristic
|
|
51
|
-
* misfire only costs polish;
|
|
52
|
-
* 3. the explicit force comes next so it can beat the heuristics below;
|
|
53
|
-
* 4. heuristics (TERM/CI/size), all overridable by `--fullscreen`.
|
|
54
|
-
*/
|
|
55
|
-
export declare function decideRenderMode(flags: {
|
|
56
|
-
fullscreen?: boolean;
|
|
57
|
-
}, env: Record<string, string | undefined>, stdout: StdoutLike | undefined): RenderMode;
|
|
58
|
-
export {};
|