cvc-tui 0.4.0 → 0.4.2
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/dist/entry.js +71148 -61
- package/package.json +2 -2
- package/dist/app/completion.js +0 -102
- package/dist/app/createGatewayEventHandler.js +0 -508
- package/dist/app/createSlashHandler.js +0 -101
- package/dist/app/delegationStore.js +0 -51
- package/dist/app/gatewayContext.js +0 -17
- package/dist/app/historyStore.js +0 -123
- package/dist/app/inputBuffer.js +0 -120
- package/dist/app/inputSelectionStore.js +0 -8
- package/dist/app/inputStore.js +0 -28
- package/dist/app/interfaces.js +0 -6
- package/dist/app/overlayStore.js +0 -40
- package/dist/app/promptStore.js +0 -44
- package/dist/app/queueStore.js +0 -25
- package/dist/app/scroll.js +0 -44
- package/dist/app/setupHandoff.js +0 -28
- package/dist/app/slash/commands/core.js +0 -479
- package/dist/app/slash/commands/debug.js +0 -44
- package/dist/app/slash/commands/ops.js +0 -498
- package/dist/app/slash/commands/session.js +0 -431
- package/dist/app/slash/commands/setup.js +0 -20
- package/dist/app/slash/commands/toggles.js +0 -40
- package/dist/app/slash/registry.js +0 -18
- package/dist/app/slash/types.js +0 -1
- package/dist/app/spawnHistoryStore.js +0 -105
- package/dist/app/turnController.js +0 -650
- package/dist/app/turnStore.js +0 -48
- package/dist/app/uiStore.js +0 -36
- package/dist/app/useComposerState.js +0 -265
- package/dist/app/useConfigSync.js +0 -144
- package/dist/app/useInputHandlers.js +0 -403
- package/dist/app/useLongRunToolCharms.js +0 -50
- package/dist/app/useMainApp.js +0 -629
- package/dist/app/useSessionLifecycle.js +0 -175
- package/dist/app/useSubmission.js +0 -287
- package/dist/app.js +0 -15
- package/dist/banner.js +0 -57
- package/dist/components/agentsOverlay.js +0 -474
- package/dist/components/appChrome.js +0 -252
- package/dist/components/appLayout.js +0 -121
- package/dist/components/appOverlays.js +0 -65
- package/dist/components/branding.js +0 -97
- package/dist/components/fpsOverlay.js +0 -22
- package/dist/components/helpHint.js +0 -21
- package/dist/components/markdown.js +0 -501
- package/dist/components/maskedPrompt.js +0 -12
- package/dist/components/messageLine.js +0 -82
- package/dist/components/modelPicker.js +0 -254
- package/dist/components/overlayControls.js +0 -30
- package/dist/components/overlays/confirmPrompt.js +0 -25
- package/dist/components/overlays/helpOverlay.js +0 -76
- package/dist/components/overlays/historySearch.js +0 -49
- package/dist/components/overlays/modelPicker.js +0 -60
- package/dist/components/overlays/overlayUtils.js +0 -19
- package/dist/components/overlays/secretPrompt.js +0 -36
- package/dist/components/overlays/sessionPicker.js +0 -93
- package/dist/components/overlays/skillsHub.js +0 -71
- package/dist/components/prompts.js +0 -95
- package/dist/components/queuedMessages.js +0 -24
- package/dist/components/sessionPicker.js +0 -130
- package/dist/components/skillsHub.js +0 -165
- package/dist/components/streamingAssistant.js +0 -35
- package/dist/components/streamingMarkdown.js +0 -144
- package/dist/components/textInput.js +0 -794
- package/dist/components/themed.js +0 -12
- package/dist/components/thinking.js +0 -496
- package/dist/components/todoPanel.js +0 -40
- package/dist/components/transcript.js +0 -22
- package/dist/config/env.js +0 -18
- package/dist/config/limits.js +0 -22
- package/dist/config/timing.js +0 -18
- package/dist/content/charms.js +0 -5
- package/dist/content/faces.js +0 -21
- package/dist/content/fortunes.js +0 -29
- package/dist/content/hotkeys.js +0 -38
- package/dist/content/placeholders.js +0 -15
- package/dist/content/setup.js +0 -14
- package/dist/content/verbs.js +0 -41
- package/dist/domain/details.js +0 -53
- package/dist/domain/messages.js +0 -63
- package/dist/domain/paths.js +0 -16
- package/dist/domain/providers.js +0 -11
- package/dist/domain/roles.js +0 -6
- package/dist/domain/slash.js +0 -11
- package/dist/domain/usage.js +0 -1
- package/dist/domain/viewport.js +0 -33
- package/dist/gateway/client.js +0 -312
- package/dist/gatewayClient.js +0 -574
- package/dist/gatewayTypes.js +0 -1
- package/dist/hooks/useCompletion.js +0 -86
- package/dist/hooks/useGitBranch.js +0 -58
- package/dist/hooks/useInputHistory.js +0 -12
- package/dist/hooks/useQueue.js +0 -57
- package/dist/hooks/useVirtualHistory.js +0 -401
- package/dist/lib/circularBuffer.js +0 -43
- package/dist/lib/clipboard.js +0 -126
- package/dist/lib/editor.js +0 -41
- package/dist/lib/editor.test.js +0 -58
- package/dist/lib/emoji.js +0 -49
- package/dist/lib/externalCli.js +0 -11
- package/dist/lib/forceTruecolor.js +0 -26
- package/dist/lib/fpsStore.js +0 -36
- package/dist/lib/gracefulExit.js +0 -29
- package/dist/lib/history.js +0 -69
- package/dist/lib/inputMetrics.js +0 -143
- package/dist/lib/liveProgress.js +0 -51
- package/dist/lib/liveProgress.test.js +0 -89
- package/dist/lib/mathUnicode.js +0 -685
- package/dist/lib/memory.js +0 -123
- package/dist/lib/memoryMonitor.js +0 -76
- package/dist/lib/messages.js +0 -3
- package/dist/lib/messages.test.js +0 -25
- package/dist/lib/osc52.js +0 -53
- package/dist/lib/perfPane.js +0 -94
- package/dist/lib/platform.js +0 -312
- package/dist/lib/precisionWheel.js +0 -25
- package/dist/lib/reasoning.js +0 -39
- package/dist/lib/rpc.js +0 -26
- package/dist/lib/subagentTree.js +0 -287
- package/dist/lib/syntax.js +0 -89
- package/dist/lib/terminalModes.js +0 -46
- package/dist/lib/terminalParity.js +0 -48
- package/dist/lib/terminalSetup.js +0 -321
- package/dist/lib/text.js +0 -203
- package/dist/lib/text.test.js +0 -18
- package/dist/lib/todo.js +0 -2
- package/dist/lib/todo.test.js +0 -22
- package/dist/lib/viewportStore.js +0 -82
- package/dist/lib/virtualHeights.js +0 -61
- package/dist/lib/wheelAccel.js +0 -143
- package/dist/theme.js +0 -398
- package/dist/types.js +0 -1
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
// SPDX-License-Identifier: MIT
|
|
3
|
-
// Ported from CVC Agent (https://github.com/NousResearch/cvc)
|
|
4
|
-
// Original Copyright (c) 2025 Nous Research. CVC adaptations (c) 2026 Jai Kumar Meena.
|
|
5
|
-
import { execFile } from 'node:child_process';
|
|
6
|
-
import { promisify } from 'node:util';
|
|
7
|
-
import { useEffect, useState } from 'react';
|
|
8
|
-
const TTL_MS = 15_000;
|
|
9
|
-
const TIMEOUT_MS = 500;
|
|
10
|
-
const pexec = promisify(execFile);
|
|
11
|
-
const cache = new Map();
|
|
12
|
-
const inflight = new Map();
|
|
13
|
-
const resolveBranch = async (cwd) => {
|
|
14
|
-
try {
|
|
15
|
-
const { stdout } = await pexec('git', ['-C', cwd, 'rev-parse', '--abbrev-ref', 'HEAD'], { timeout: TIMEOUT_MS });
|
|
16
|
-
const b = stdout.trim();
|
|
17
|
-
return !b || b === 'HEAD' ? null : b;
|
|
18
|
-
}
|
|
19
|
-
catch {
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
22
|
-
};
|
|
23
|
-
const fetchBranch = (cwd) => {
|
|
24
|
-
const pending = inflight.get(cwd);
|
|
25
|
-
if (pending) {
|
|
26
|
-
return pending;
|
|
27
|
-
}
|
|
28
|
-
const p = resolveBranch(cwd).finally(() => inflight.delete(cwd));
|
|
29
|
-
inflight.set(cwd, p);
|
|
30
|
-
return p;
|
|
31
|
-
};
|
|
32
|
-
export function useGitBranch(cwd) {
|
|
33
|
-
const [branch, setBranch] = useState(() => cache.get(cwd)?.branch ?? null);
|
|
34
|
-
useEffect(() => {
|
|
35
|
-
let cancelled = false;
|
|
36
|
-
const tick = async () => {
|
|
37
|
-
const hit = cache.get(cwd);
|
|
38
|
-
if (hit && Date.now() - hit.at < TTL_MS) {
|
|
39
|
-
if (!cancelled) {
|
|
40
|
-
setBranch(hit.branch);
|
|
41
|
-
}
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
const b = await fetchBranch(cwd);
|
|
45
|
-
cache.set(cwd, { at: Date.now(), branch: b });
|
|
46
|
-
if (!cancelled) {
|
|
47
|
-
setBranch(b);
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
void tick();
|
|
51
|
-
const id = setInterval(() => void tick(), TTL_MS);
|
|
52
|
-
return () => {
|
|
53
|
-
cancelled = true;
|
|
54
|
-
clearInterval(id);
|
|
55
|
-
};
|
|
56
|
-
}, [cwd]);
|
|
57
|
-
return branch;
|
|
58
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
// SPDX-License-Identifier: MIT
|
|
3
|
-
// Ported from CVC Agent (https://github.com/NousResearch/cvc)
|
|
4
|
-
// Original Copyright (c) 2025 Nous Research. CVC adaptations (c) 2026 Jai Kumar Meena.
|
|
5
|
-
import { useRef, useState } from 'react';
|
|
6
|
-
import * as inputHistory from '../lib/history.js';
|
|
7
|
-
export function useInputHistory() {
|
|
8
|
-
const historyRef = useRef(inputHistory.load());
|
|
9
|
-
const [historyIdx, setHistoryIdx] = useState(null);
|
|
10
|
-
const historyDraftRef = useRef('');
|
|
11
|
-
return { historyRef, historyIdx, setHistoryIdx, historyDraftRef, pushHistory: inputHistory.append };
|
|
12
|
-
}
|
package/dist/hooks/useQueue.js
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
// SPDX-License-Identifier: MIT
|
|
3
|
-
// Ported from CVC Agent (https://github.com/NousResearch/cvc)
|
|
4
|
-
// Original Copyright (c) 2025 Nous Research. CVC adaptations (c) 2026 Jai Kumar Meena.
|
|
5
|
-
import { useCallback, useRef, useState } from 'react';
|
|
6
|
-
// Mutates `arr` in place; returned reference is the same input array, kept
|
|
7
|
-
// so callers can chain. Use `Array.prototype.toSpliced` if you need a copy.
|
|
8
|
-
export function removeAtInPlace(arr, i) {
|
|
9
|
-
if (i < 0 || i >= arr.length) {
|
|
10
|
-
return arr;
|
|
11
|
-
}
|
|
12
|
-
arr.splice(i, 1);
|
|
13
|
-
return arr;
|
|
14
|
-
}
|
|
15
|
-
export function useQueue() {
|
|
16
|
-
const queueRef = useRef([]);
|
|
17
|
-
const [queuedDisplay, setQueuedDisplay] = useState([]);
|
|
18
|
-
const queueEditRef = useRef(null);
|
|
19
|
-
const [queueEditIdx, setQueueEditIdx] = useState(null);
|
|
20
|
-
const syncQueue = useCallback(() => setQueuedDisplay([...queueRef.current]), []);
|
|
21
|
-
const setQueueEdit = useCallback((idx) => {
|
|
22
|
-
queueEditRef.current = idx;
|
|
23
|
-
setQueueEditIdx(idx);
|
|
24
|
-
}, []);
|
|
25
|
-
const enqueue = useCallback((text) => {
|
|
26
|
-
queueRef.current.push(text);
|
|
27
|
-
syncQueue();
|
|
28
|
-
}, [syncQueue]);
|
|
29
|
-
const dequeue = useCallback(() => {
|
|
30
|
-
const head = queueRef.current.shift();
|
|
31
|
-
syncQueue();
|
|
32
|
-
return head;
|
|
33
|
-
}, [syncQueue]);
|
|
34
|
-
const replaceQ = useCallback((i, text) => {
|
|
35
|
-
queueRef.current[i] = text;
|
|
36
|
-
syncQueue();
|
|
37
|
-
}, [syncQueue]);
|
|
38
|
-
const removeQ = useCallback((i) => {
|
|
39
|
-
const before = queueRef.current.length;
|
|
40
|
-
removeAtInPlace(queueRef.current, i);
|
|
41
|
-
if (queueRef.current.length !== before) {
|
|
42
|
-
syncQueue();
|
|
43
|
-
}
|
|
44
|
-
}, [syncQueue]);
|
|
45
|
-
return {
|
|
46
|
-
dequeue,
|
|
47
|
-
enqueue,
|
|
48
|
-
queueEditIdx,
|
|
49
|
-
queueEditRef,
|
|
50
|
-
queueRef,
|
|
51
|
-
queuedDisplay,
|
|
52
|
-
removeQ,
|
|
53
|
-
replaceQ,
|
|
54
|
-
setQueueEdit,
|
|
55
|
-
syncQueue
|
|
56
|
-
};
|
|
57
|
-
}
|
|
@@ -1,401 +0,0 @@
|
|
|
1
|
-
import { useCallback, useDeferredValue, useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from 'react';
|
|
2
|
-
const ESTIMATE = 4;
|
|
3
|
-
// Overscan was 40 (= viewport) which is way more than needed when heights
|
|
4
|
-
// are well-estimated. Cutting in half saves ~20 mounted items per scroll
|
|
5
|
-
// edge → smaller fiber tree → less buffer-compose work per frame. HN/CC
|
|
6
|
-
// dev (https://news.ycombinator.com/item?id=46699072) confirmed GC pressure
|
|
7
|
-
// from large JSX trees was their main perf issue post-rewrite.
|
|
8
|
-
const OVERSCAN = 20;
|
|
9
|
-
// Hard cap on mounted items. Was 260; profiling showed ~23k live Yoga
|
|
10
|
-
// nodes during sustained PageUp catch-up (renderer p99=106ms). The
|
|
11
|
-
// viewport+2*overscan = 80 rows of needed coverage = ~25 items at avg 3
|
|
12
|
-
// rows/item, so 120 leaves >4× headroom and never blanks the viewport
|
|
13
|
-
// even when items are tiny.
|
|
14
|
-
const MAX_MOUNTED = 120;
|
|
15
|
-
const COLD_START = 30;
|
|
16
|
-
// Floor on unmeasured row height used when computing coverage — guarantees
|
|
17
|
-
// the mounted span physically reaches the viewport bottom regardless of how
|
|
18
|
-
// small items actually are (at the cost of over-mounting when items are
|
|
19
|
-
// larger; overscan absorbs that).
|
|
20
|
-
const PESSIMISTIC = 1;
|
|
21
|
-
// Tightest safe scrollTop bin for the useSyncExternalStore snapshot. Small
|
|
22
|
-
// wheel ticks that don't cross a bin short-circuit React's commit entirely;
|
|
23
|
-
// Ink keeps painting via ScrollBox.forceRender + direct scrollTop reads.
|
|
24
|
-
// Half of OVERSCAN keeps ≥20 rows of cushion before the mounted range
|
|
25
|
-
// would actually need to shift.
|
|
26
|
-
const QUANTUM = OVERSCAN >> 1;
|
|
27
|
-
// Renders to keep the mount range frozen after width change (heights scaled
|
|
28
|
-
// but not yet re-measured). Render #1 skips measurement so pre-resize Yoga
|
|
29
|
-
// doesn't poison the scaled cache; render #2's useLayoutEffect captures
|
|
30
|
-
// post-resize heights; render #3 recomputes range with accurate data.
|
|
31
|
-
const FREEZE_RENDERS = 2;
|
|
32
|
-
// Cap on NEW items mounted per commit when scrolling fast. Without this,
|
|
33
|
-
// a single PageUp into unmeasured territory mounts ~190 rows with
|
|
34
|
-
// PESSIMISTIC=1 coverage — each row running marked lexer + syntax
|
|
35
|
-
// highlighting for ~3ms = ~600ms sync block. Sliding toward the target
|
|
36
|
-
// over several commits keeps per-commit mount cost bounded. Tightened
|
|
37
|
-
// from 25 → 12: each new item adds ~100 fibers / Yoga nodes, and a
|
|
38
|
-
// 25-item commit was the dominant contributor to the 100ms+ p99 frames.
|
|
39
|
-
const SLIDE_STEP = 12;
|
|
40
|
-
const NOOP = () => { };
|
|
41
|
-
const upperBound = (arr, target, length = arr.length) => {
|
|
42
|
-
let lo = 0;
|
|
43
|
-
let hi = length;
|
|
44
|
-
while (lo < hi) {
|
|
45
|
-
const mid = (lo + hi) >> 1;
|
|
46
|
-
arr[mid] <= target ? (lo = mid + 1) : (hi = mid);
|
|
47
|
-
}
|
|
48
|
-
return lo;
|
|
49
|
-
};
|
|
50
|
-
export const shouldSetVirtualClamp = ({ itemCount, liveTailActive = false, sticky, viewportHeight }) => itemCount > 0 && viewportHeight > 0 && !sticky && !liveTailActive;
|
|
51
|
-
export const ensureVirtualItemHeight = (heights, key, index, estimate, estimateHeight) => {
|
|
52
|
-
const cached = heights.get(key);
|
|
53
|
-
if (cached !== undefined) {
|
|
54
|
-
return Math.max(1, Math.floor(cached));
|
|
55
|
-
}
|
|
56
|
-
const seeded = Math.max(1, Math.floor(estimateHeight?.(index, key) ?? estimate));
|
|
57
|
-
heights.set(key, seeded);
|
|
58
|
-
return seeded;
|
|
59
|
-
};
|
|
60
|
-
export function useVirtualHistory(scrollRef, items, columns, { estimate = ESTIMATE, estimateHeight, initialHeights, liveTailActive = false, onHeightsChange, overscan = OVERSCAN, maxMounted = MAX_MOUNTED, coldStartCount = COLD_START } = {}) {
|
|
61
|
-
const nodes = useRef(new Map());
|
|
62
|
-
const heights = useRef(new Map(initialHeights));
|
|
63
|
-
const initialHeightsRef = useRef(initialHeights);
|
|
64
|
-
const refs = useRef(new Map());
|
|
65
|
-
const onHeightsChangeRef = useRef(onHeightsChange);
|
|
66
|
-
// Bump whenever heightCache mutates so offsets rebuild on next read.
|
|
67
|
-
// Ref (not state) — checked during render phase, zero extra commits.
|
|
68
|
-
const offsetVersion = useRef(0);
|
|
69
|
-
// Cached offsets: reused Float64Array keyed on (itemCount, version) so we
|
|
70
|
-
// only rebuild when something actually changed. Previous approach allocated
|
|
71
|
-
// a fresh Array(n+1) every render — at n=10k that's ~80KB/render of GC
|
|
72
|
-
// pressure during streaming.
|
|
73
|
-
const offsetsCache = useRef({
|
|
74
|
-
arr: new Float64Array(0),
|
|
75
|
-
n: -1,
|
|
76
|
-
version: -1
|
|
77
|
-
});
|
|
78
|
-
const [hasScrollRef, setHasScrollRef] = useState(false);
|
|
79
|
-
// Height cache writes happen in layout effects; bump once so offsets and
|
|
80
|
-
// clamp bounds rebuild without waiting for the next scroll/input event.
|
|
81
|
-
const [measuredHeightVersion, bumpMeasuredHeightVersion] = useState(0);
|
|
82
|
-
const metrics = useRef({ sticky: true, top: 0, vp: 0 });
|
|
83
|
-
const lastScrollTopRef = useRef(0);
|
|
84
|
-
// Width change: scale cached heights by oldCols/newCols instead of clearing
|
|
85
|
-
// (clearing forces a pessimistic back-walk mounting ~190 rows at once, each
|
|
86
|
-
// a fresh marked.lexer + syntax highlight ≈ 3ms). Freeze the mount range
|
|
87
|
-
// for 2 renders so warm memos survive; skip one measurement pass so
|
|
88
|
-
// useLayoutEffect doesn't poison the scaled cache with pre-resize Yoga
|
|
89
|
-
// heights.
|
|
90
|
-
const prevColumns = useRef(columns);
|
|
91
|
-
const skipMeasurement = useRef(false);
|
|
92
|
-
const prevRange = useRef(null);
|
|
93
|
-
const freezeRenders = useRef(0);
|
|
94
|
-
onHeightsChangeRef.current = onHeightsChange;
|
|
95
|
-
if (initialHeightsRef.current !== initialHeights) {
|
|
96
|
-
initialHeightsRef.current = initialHeights;
|
|
97
|
-
heights.current = new Map(initialHeights);
|
|
98
|
-
offsetVersion.current++;
|
|
99
|
-
}
|
|
100
|
-
if (prevColumns.current !== columns && prevColumns.current > 0 && columns > 0) {
|
|
101
|
-
const ratio = prevColumns.current / columns;
|
|
102
|
-
prevColumns.current = columns;
|
|
103
|
-
for (const [k, h] of heights.current) {
|
|
104
|
-
heights.current.set(k, Math.max(1, Math.round(h * ratio)));
|
|
105
|
-
}
|
|
106
|
-
offsetVersion.current++;
|
|
107
|
-
skipMeasurement.current = true;
|
|
108
|
-
freezeRenders.current = FREEZE_RENDERS;
|
|
109
|
-
}
|
|
110
|
-
useLayoutEffect(() => {
|
|
111
|
-
setHasScrollRef(Boolean(scrollRef.current));
|
|
112
|
-
}, [scrollRef]);
|
|
113
|
-
// Quantized snapshot: same-bin scrolls (most wheel ticks) produce the same
|
|
114
|
-
// number → React.Object.is short-circuits the commit entirely. sticky state
|
|
115
|
-
// is folded in via the sign bit so sticky→broken transitions also trigger.
|
|
116
|
-
// Uses the TARGET (committed + pendingDelta), not committed scrollTop, so
|
|
117
|
-
// scrollBy notifications immediately remount for the destination before
|
|
118
|
-
// Ink's drain frames need the children.
|
|
119
|
-
const subscribe = useCallback((cb) => (hasScrollRef ? scrollRef.current?.subscribe(cb) : null) ?? NOOP, [hasScrollRef, scrollRef]);
|
|
120
|
-
useSyncExternalStore(subscribe, () => {
|
|
121
|
-
const s = scrollRef.current;
|
|
122
|
-
if (!s) {
|
|
123
|
-
return NaN;
|
|
124
|
-
}
|
|
125
|
-
const target = s.getScrollTop() + s.getPendingDelta();
|
|
126
|
-
const bin = Math.floor(target / QUANTUM);
|
|
127
|
-
return s.isSticky() ? ~bin : bin;
|
|
128
|
-
}, () => NaN);
|
|
129
|
-
useEffect(() => {
|
|
130
|
-
const keep = new Set(items.map(i => i.key));
|
|
131
|
-
let dirty = false;
|
|
132
|
-
for (const k of heights.current.keys()) {
|
|
133
|
-
if (!keep.has(k)) {
|
|
134
|
-
heights.current.delete(k);
|
|
135
|
-
nodes.current.delete(k);
|
|
136
|
-
refs.current.delete(k);
|
|
137
|
-
dirty = true;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
if (dirty) {
|
|
141
|
-
offsetVersion.current++;
|
|
142
|
-
}
|
|
143
|
-
}, [items]);
|
|
144
|
-
// Offsets: Float64Array reused across renders, invalidated by offsetVersion
|
|
145
|
-
// bumps from heightCache writers (measureRef, resize-scale, GC). Binary
|
|
146
|
-
// search tolerates either monotone source, so no need to rebuild unless
|
|
147
|
-
// something changed.
|
|
148
|
-
const n = items.length;
|
|
149
|
-
if (offsetsCache.current.version !== offsetVersion.current || offsetsCache.current.n !== n) {
|
|
150
|
-
const arr = offsetsCache.current.arr.length >= n + 1 ? offsetsCache.current.arr : new Float64Array(n + 1);
|
|
151
|
-
arr[0] = 0;
|
|
152
|
-
for (let i = 0; i < n; i++) {
|
|
153
|
-
arr[i + 1] = arr[i] + ensureVirtualItemHeight(heights.current, items[i].key, i, estimate, estimateHeight);
|
|
154
|
-
}
|
|
155
|
-
offsetsCache.current = { arr, n, version: offsetVersion.current };
|
|
156
|
-
}
|
|
157
|
-
const offsets = offsetsCache.current.arr;
|
|
158
|
-
const total = offsets[n] ?? 0;
|
|
159
|
-
const top = Math.max(0, scrollRef.current?.getScrollTop() ?? 0);
|
|
160
|
-
const pendingDelta = scrollRef.current?.getPendingDelta() ?? 0;
|
|
161
|
-
const target = Math.max(0, top + pendingDelta);
|
|
162
|
-
const vp = Math.max(0, scrollRef.current?.getViewportHeight() ?? 0);
|
|
163
|
-
const sticky = scrollRef.current?.isSticky() ?? true;
|
|
164
|
-
const recentManual = Date.now() - (scrollRef.current?.getLastManualScrollAt() ?? 0) < 1200;
|
|
165
|
-
// During a freeze, drop the frozen range if items shrank past its start
|
|
166
|
-
// (/clear, compaction) — clamping would collapse to an empty mount and
|
|
167
|
-
// flash blank. Fall through to the normal path in that case.
|
|
168
|
-
const frozenRange = freezeRenders.current > 0 && prevRange.current && prevRange.current[0] < n ? prevRange.current : null;
|
|
169
|
-
let start = 0;
|
|
170
|
-
let end = n;
|
|
171
|
-
if (frozenRange) {
|
|
172
|
-
start = frozenRange[0];
|
|
173
|
-
end = Math.min(frozenRange[1], n);
|
|
174
|
-
}
|
|
175
|
-
else if (n > 0) {
|
|
176
|
-
if (vp <= 0) {
|
|
177
|
-
start = Math.max(0, n - coldStartCount);
|
|
178
|
-
}
|
|
179
|
-
else if (sticky && !recentManual) {
|
|
180
|
-
const budget = vp + overscan;
|
|
181
|
-
start = n;
|
|
182
|
-
while (start > 0 && total - offsets[start - 1] < budget) {
|
|
183
|
-
start--;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
else {
|
|
187
|
-
// User scrolled up. Span [committed..target] so every drain frame is
|
|
188
|
-
// covered. Claude-code caps the span at 3×viewport so pendingDelta
|
|
189
|
-
// growing unbounded (MX Master free-spin) doesn't blow the mount
|
|
190
|
-
// budget; the clamp (setClampBounds) shows edge-of-mounted content
|
|
191
|
-
// during catch-up.
|
|
192
|
-
const MAX_SPAN = vp * 3;
|
|
193
|
-
const rawLo = Math.min(top, target);
|
|
194
|
-
const rawHi = Math.max(top, target);
|
|
195
|
-
const span = rawHi - rawLo;
|
|
196
|
-
const clampedLo = span > MAX_SPAN ? (pendingDelta < 0 ? rawHi - MAX_SPAN : rawLo) : rawLo;
|
|
197
|
-
const clampedHi = clampedLo + Math.min(span, MAX_SPAN);
|
|
198
|
-
const lo = Math.max(0, clampedLo - overscan);
|
|
199
|
-
const hi = clampedHi + vp + overscan;
|
|
200
|
-
// Binary search — offsets is monotone. Linear walk was O(n) at n=10k+,
|
|
201
|
-
// ~2ms per render during scroll.
|
|
202
|
-
start = Math.max(0, Math.min(n - 1, upperBound(offsets, lo, n + 1) - 1));
|
|
203
|
-
end = Math.max(start + 1, Math.min(n, upperBound(offsets, hi, n + 1)));
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
if (end - start > maxMounted) {
|
|
207
|
-
sticky ? (start = Math.max(0, end - maxMounted)) : (end = Math.min(n, start + maxMounted));
|
|
208
|
-
}
|
|
209
|
-
// Coverage guarantee: ensure sum(real or pessimistic heights) ≥
|
|
210
|
-
// viewportH + 2*overscan so the viewport is physically covered even when
|
|
211
|
-
// items are tiny. Pessimistic because uncached items use a floor of 1 —
|
|
212
|
-
// over-mounts when items are large, never leaves blank spacer showing.
|
|
213
|
-
if (n > 0 && vp > 0 && !frozenRange) {
|
|
214
|
-
const needed = vp + 2 * overscan;
|
|
215
|
-
let coverage = 0;
|
|
216
|
-
for (let i = start; i < end; i++) {
|
|
217
|
-
coverage += ensureVirtualItemHeight(heights.current, items[i].key, i, PESSIMISTIC, estimateHeight);
|
|
218
|
-
}
|
|
219
|
-
if (sticky) {
|
|
220
|
-
const minStart = Math.max(0, end - maxMounted);
|
|
221
|
-
while (start > minStart && coverage < needed) {
|
|
222
|
-
start--;
|
|
223
|
-
coverage += ensureVirtualItemHeight(heights.current, items[start].key, start, PESSIMISTIC, estimateHeight);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
else {
|
|
227
|
-
const maxEnd = Math.min(n, start + maxMounted);
|
|
228
|
-
while (end < maxEnd && coverage < needed) {
|
|
229
|
-
coverage += ensureVirtualItemHeight(heights.current, items[end].key, end, PESSIMISTIC, estimateHeight);
|
|
230
|
-
end++;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
// Slide cap: limit how many NEW items mount this commit. Gates on scroll
|
|
235
|
-
// VELOCITY (|scrollTop delta since last commit| + |pendingDelta| >
|
|
236
|
-
// 2×viewport — key-repeat PageUp moves ~viewport/2 per press). Covers
|
|
237
|
-
// both scrollBy (pendingDelta) and scrollTo (direct write). Normal single
|
|
238
|
-
// PageUp skips this; the clamp holds the viewport at the mounted edge
|
|
239
|
-
// during catch-up so there's no blank screen. Only caps range GROWTH;
|
|
240
|
-
// shrinking is unbounded.
|
|
241
|
-
if (!frozenRange && prevRange.current && vp > 0) {
|
|
242
|
-
const velocity = Math.abs(top - lastScrollTopRef.current) + Math.abs(pendingDelta);
|
|
243
|
-
if (velocity > vp * 2) {
|
|
244
|
-
const [pS, pE] = prevRange.current;
|
|
245
|
-
start = Math.max(start, pS - SLIDE_STEP);
|
|
246
|
-
end = Math.min(end, pE + SLIDE_STEP);
|
|
247
|
-
// A large jump past the capped end can invert (start > end); mount
|
|
248
|
-
// SLIDE_STEP items from the new start so the viewport isn't blank
|
|
249
|
-
// during catch-up.
|
|
250
|
-
if (start > end) {
|
|
251
|
-
end = Math.min(start + SLIDE_STEP, n);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
lastScrollTopRef.current = top;
|
|
256
|
-
if (freezeRenders.current > 0) {
|
|
257
|
-
freezeRenders.current--;
|
|
258
|
-
}
|
|
259
|
-
else {
|
|
260
|
-
prevRange.current = [start, end];
|
|
261
|
-
}
|
|
262
|
-
// Time-slice range growth via useDeferredValue. Urgent render keeps Ink
|
|
263
|
-
// painting with the OLD range (all memo hits, fast); deferred render
|
|
264
|
-
// transitions to the NEW range (fresh mounts: Md, syntax highlight) in a
|
|
265
|
-
// non-blocking background commit. The clamp (setClampBounds) pins the
|
|
266
|
-
// viewport to the mounted edge so there's no visual artifact from the
|
|
267
|
-
// deferred range lagging briefly. Only deferral range GROWTH — shrinking
|
|
268
|
-
// is cheap (unmount = remove fiber, no parse).
|
|
269
|
-
const dStart = useDeferredValue(start);
|
|
270
|
-
const dEnd = useDeferredValue(end);
|
|
271
|
-
let effStart = start < dStart ? dStart : start;
|
|
272
|
-
let effEnd = end > dEnd ? dEnd : end;
|
|
273
|
-
// Inverted range (large jump with deferred value lagging) or sticky snap
|
|
274
|
-
// (scrollToBottom needs the tail mounted NOW so maxScroll lands on content,
|
|
275
|
-
// not bottomSpacer) — skip deferral.
|
|
276
|
-
if (effStart > effEnd || sticky) {
|
|
277
|
-
effStart = start;
|
|
278
|
-
effEnd = end;
|
|
279
|
-
}
|
|
280
|
-
// Scrolling DOWN — bypass effEnd deferral so the tail mounts immediately.
|
|
281
|
-
// Without this, the clamp holds scrollTop short of the real bottom and
|
|
282
|
-
// the user feels "stuck before bottom". effStart stays deferred so scroll-
|
|
283
|
-
// UP keeps time-slicing (older messages parse on mount).
|
|
284
|
-
if (pendingDelta > 0) {
|
|
285
|
-
effEnd = end;
|
|
286
|
-
}
|
|
287
|
-
// Final O(viewport) enforcement. Deferred+bypass combinations above can
|
|
288
|
-
// leak: during sustained PageUp, concurrent mode interleaves dStart updates
|
|
289
|
-
// with effEnd=end bypasses across commits and the effective window drifts
|
|
290
|
-
// wider than either bound alone. Trim the far edge by viewport position
|
|
291
|
-
// (not pendingDelta direction — that flips mid-settle under concurrent
|
|
292
|
-
// scheduling and yanks scrollTop).
|
|
293
|
-
if (effEnd - effStart > maxMounted && vp > 0) {
|
|
294
|
-
const mid = (offsets[effStart] + offsets[effEnd]) / 2;
|
|
295
|
-
if (top < mid) {
|
|
296
|
-
effEnd = effStart + maxMounted;
|
|
297
|
-
}
|
|
298
|
-
else {
|
|
299
|
-
effStart = effEnd - maxMounted;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
const measureRef = useCallback((key) => {
|
|
303
|
-
let fn = refs.current.get(key);
|
|
304
|
-
if (!fn) {
|
|
305
|
-
fn = (el) => {
|
|
306
|
-
if (el) {
|
|
307
|
-
nodes.current.set(key, el);
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
// Measure-at-unmount: the yogaNode is still valid here (reconciler
|
|
311
|
-
// calls ref(null) before removeChild → freeRecursive), so we grab
|
|
312
|
-
// the final height before WASM release. Without this, items
|
|
313
|
-
// scrolled out during fast pan keep a stale estimate in heightCache
|
|
314
|
-
// and offset math drifts until the next mount/remount cycle.
|
|
315
|
-
const existing = nodes.current.get(key);
|
|
316
|
-
const h = Math.ceil(existing?.yogaNode?.getComputedHeight?.() ?? 0);
|
|
317
|
-
if (h > 0 && heights.current.get(key) !== h) {
|
|
318
|
-
heights.current.set(key, h);
|
|
319
|
-
offsetVersion.current++;
|
|
320
|
-
onHeightsChangeRef.current?.(heights.current);
|
|
321
|
-
}
|
|
322
|
-
nodes.current.delete(key);
|
|
323
|
-
};
|
|
324
|
-
refs.current.set(key, fn);
|
|
325
|
-
}
|
|
326
|
-
return fn;
|
|
327
|
-
}, []);
|
|
328
|
-
useLayoutEffect(() => {
|
|
329
|
-
const s = scrollRef.current;
|
|
330
|
-
let dirty = false;
|
|
331
|
-
let heightDirty = false;
|
|
332
|
-
// Give the renderer the mounted-row coverage for passive scroll clamping.
|
|
333
|
-
// Clamp MUST use the EFFECTIVE (deferred) range, not the immediate one.
|
|
334
|
-
// During fast scroll, immediate [start,end] may already cover the new
|
|
335
|
-
// scrollTop position, but children still render at the deferred range.
|
|
336
|
-
// If clamp used immediate bounds, render-node-to-output's drain-gate
|
|
337
|
-
// would drain past the deferred children's span → viewport lands in
|
|
338
|
-
// spacer → white flash.
|
|
339
|
-
if (s && shouldSetVirtualClamp({ itemCount: n, liveTailActive, sticky, viewportHeight: vp })) {
|
|
340
|
-
const effTopSpacer = offsets[effStart] ?? 0;
|
|
341
|
-
const effBottom = offsets[effEnd] ?? total;
|
|
342
|
-
// At effEnd=n there's no bottomSpacer — use Infinity so render-node-
|
|
343
|
-
// to-output's own Math.min(cur, maxScroll) governs. Using offsets[n]
|
|
344
|
-
// here would bake in heightCache (one render behind Yoga), and during
|
|
345
|
-
// streaming the tail item's cached height lags its real height —
|
|
346
|
-
// sticky-break would then clamp below the real max and push
|
|
347
|
-
// streaming text off-viewport.
|
|
348
|
-
const clampMin = effStart === 0 ? 0 : effTopSpacer;
|
|
349
|
-
const clampMax = effEnd === n ? Infinity : Math.max(effTopSpacer, effBottom - vp);
|
|
350
|
-
s.setClampBounds(clampMin, clampMax);
|
|
351
|
-
}
|
|
352
|
-
else {
|
|
353
|
-
s?.setClampBounds(undefined, undefined);
|
|
354
|
-
}
|
|
355
|
-
if (skipMeasurement.current) {
|
|
356
|
-
skipMeasurement.current = false;
|
|
357
|
-
}
|
|
358
|
-
else {
|
|
359
|
-
for (let i = effStart; i < effEnd; i++) {
|
|
360
|
-
const k = items[i]?.key;
|
|
361
|
-
if (!k) {
|
|
362
|
-
continue;
|
|
363
|
-
}
|
|
364
|
-
const h = Math.ceil(nodes.current.get(k)?.yogaNode?.getComputedHeight?.() ?? 0);
|
|
365
|
-
if (h > 0 && heights.current.get(k) !== h) {
|
|
366
|
-
heights.current.set(k, h);
|
|
367
|
-
dirty = true;
|
|
368
|
-
heightDirty = true;
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
if (s) {
|
|
373
|
-
const next = {
|
|
374
|
-
sticky: s.isSticky(),
|
|
375
|
-
top: Math.max(0, s.getScrollTop() + s.getPendingDelta()),
|
|
376
|
-
vp: Math.max(0, s.getViewportHeight())
|
|
377
|
-
};
|
|
378
|
-
if (next.sticky !== metrics.current.sticky ||
|
|
379
|
-
next.top !== metrics.current.top ||
|
|
380
|
-
next.vp !== metrics.current.vp) {
|
|
381
|
-
metrics.current = next;
|
|
382
|
-
dirty = true;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
if (dirty) {
|
|
386
|
-
offsetVersion.current++;
|
|
387
|
-
onHeightsChangeRef.current?.(heights.current);
|
|
388
|
-
}
|
|
389
|
-
if (heightDirty) {
|
|
390
|
-
bumpMeasuredHeightVersion(n => n + 1);
|
|
391
|
-
}
|
|
392
|
-
}, [effEnd, effStart, items, liveTailActive, measuredHeightVersion, n, offsets, scrollRef, sticky, total, vp]);
|
|
393
|
-
return {
|
|
394
|
-
bottomSpacer: Math.max(0, total - (offsets[effEnd] ?? total)),
|
|
395
|
-
end: effEnd,
|
|
396
|
-
measureRef,
|
|
397
|
-
offsets,
|
|
398
|
-
start: effStart,
|
|
399
|
-
topSpacer: offsets[effStart] ?? 0
|
|
400
|
-
};
|
|
401
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
// SPDX-License-Identifier: MIT
|
|
3
|
-
// Ported from CVC Agent (https://github.com/NousResearch/cvc)
|
|
4
|
-
// Original Copyright (c) 2025 Nous Research. CVC adaptations (c) 2026 Jai Kumar Meena.
|
|
5
|
-
export class CircularBuffer {
|
|
6
|
-
capacity;
|
|
7
|
-
buf;
|
|
8
|
-
head = 0;
|
|
9
|
-
len = 0;
|
|
10
|
-
constructor(capacity) {
|
|
11
|
-
this.capacity = capacity;
|
|
12
|
-
if (!Number.isInteger(capacity) || capacity <= 0) {
|
|
13
|
-
throw new RangeError(`CircularBuffer capacity must be a positive integer, got ${capacity}`);
|
|
14
|
-
}
|
|
15
|
-
this.buf = new Array(capacity);
|
|
16
|
-
}
|
|
17
|
-
push(item) {
|
|
18
|
-
this.buf[this.head] = item;
|
|
19
|
-
this.head = (this.head + 1) % this.capacity;
|
|
20
|
-
if (this.len < this.capacity) {
|
|
21
|
-
this.len++;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
tail(n = this.len) {
|
|
25
|
-
const take = Math.min(Math.max(0, n), this.len);
|
|
26
|
-
const start = this.len < this.capacity ? 0 : this.head;
|
|
27
|
-
const out = new Array(take);
|
|
28
|
-
for (let i = 0; i < take; i++) {
|
|
29
|
-
out[i] = this.buf[(start + this.len - take + i) % this.capacity];
|
|
30
|
-
}
|
|
31
|
-
return out;
|
|
32
|
-
}
|
|
33
|
-
drain() {
|
|
34
|
-
const out = this.tail();
|
|
35
|
-
this.clear();
|
|
36
|
-
return out;
|
|
37
|
-
}
|
|
38
|
-
clear() {
|
|
39
|
-
this.buf = new Array(this.capacity);
|
|
40
|
-
this.head = 0;
|
|
41
|
-
this.len = 0;
|
|
42
|
-
}
|
|
43
|
-
}
|