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.
Files changed (133) hide show
  1. package/dist/entry.js +71148 -61
  2. package/package.json +2 -2
  3. package/dist/app/completion.js +0 -102
  4. package/dist/app/createGatewayEventHandler.js +0 -508
  5. package/dist/app/createSlashHandler.js +0 -101
  6. package/dist/app/delegationStore.js +0 -51
  7. package/dist/app/gatewayContext.js +0 -17
  8. package/dist/app/historyStore.js +0 -123
  9. package/dist/app/inputBuffer.js +0 -120
  10. package/dist/app/inputSelectionStore.js +0 -8
  11. package/dist/app/inputStore.js +0 -28
  12. package/dist/app/interfaces.js +0 -6
  13. package/dist/app/overlayStore.js +0 -40
  14. package/dist/app/promptStore.js +0 -44
  15. package/dist/app/queueStore.js +0 -25
  16. package/dist/app/scroll.js +0 -44
  17. package/dist/app/setupHandoff.js +0 -28
  18. package/dist/app/slash/commands/core.js +0 -479
  19. package/dist/app/slash/commands/debug.js +0 -44
  20. package/dist/app/slash/commands/ops.js +0 -498
  21. package/dist/app/slash/commands/session.js +0 -431
  22. package/dist/app/slash/commands/setup.js +0 -20
  23. package/dist/app/slash/commands/toggles.js +0 -40
  24. package/dist/app/slash/registry.js +0 -18
  25. package/dist/app/slash/types.js +0 -1
  26. package/dist/app/spawnHistoryStore.js +0 -105
  27. package/dist/app/turnController.js +0 -650
  28. package/dist/app/turnStore.js +0 -48
  29. package/dist/app/uiStore.js +0 -36
  30. package/dist/app/useComposerState.js +0 -265
  31. package/dist/app/useConfigSync.js +0 -144
  32. package/dist/app/useInputHandlers.js +0 -403
  33. package/dist/app/useLongRunToolCharms.js +0 -50
  34. package/dist/app/useMainApp.js +0 -629
  35. package/dist/app/useSessionLifecycle.js +0 -175
  36. package/dist/app/useSubmission.js +0 -287
  37. package/dist/app.js +0 -15
  38. package/dist/banner.js +0 -57
  39. package/dist/components/agentsOverlay.js +0 -474
  40. package/dist/components/appChrome.js +0 -252
  41. package/dist/components/appLayout.js +0 -121
  42. package/dist/components/appOverlays.js +0 -65
  43. package/dist/components/branding.js +0 -97
  44. package/dist/components/fpsOverlay.js +0 -22
  45. package/dist/components/helpHint.js +0 -21
  46. package/dist/components/markdown.js +0 -501
  47. package/dist/components/maskedPrompt.js +0 -12
  48. package/dist/components/messageLine.js +0 -82
  49. package/dist/components/modelPicker.js +0 -254
  50. package/dist/components/overlayControls.js +0 -30
  51. package/dist/components/overlays/confirmPrompt.js +0 -25
  52. package/dist/components/overlays/helpOverlay.js +0 -76
  53. package/dist/components/overlays/historySearch.js +0 -49
  54. package/dist/components/overlays/modelPicker.js +0 -60
  55. package/dist/components/overlays/overlayUtils.js +0 -19
  56. package/dist/components/overlays/secretPrompt.js +0 -36
  57. package/dist/components/overlays/sessionPicker.js +0 -93
  58. package/dist/components/overlays/skillsHub.js +0 -71
  59. package/dist/components/prompts.js +0 -95
  60. package/dist/components/queuedMessages.js +0 -24
  61. package/dist/components/sessionPicker.js +0 -130
  62. package/dist/components/skillsHub.js +0 -165
  63. package/dist/components/streamingAssistant.js +0 -35
  64. package/dist/components/streamingMarkdown.js +0 -144
  65. package/dist/components/textInput.js +0 -794
  66. package/dist/components/themed.js +0 -12
  67. package/dist/components/thinking.js +0 -496
  68. package/dist/components/todoPanel.js +0 -40
  69. package/dist/components/transcript.js +0 -22
  70. package/dist/config/env.js +0 -18
  71. package/dist/config/limits.js +0 -22
  72. package/dist/config/timing.js +0 -18
  73. package/dist/content/charms.js +0 -5
  74. package/dist/content/faces.js +0 -21
  75. package/dist/content/fortunes.js +0 -29
  76. package/dist/content/hotkeys.js +0 -38
  77. package/dist/content/placeholders.js +0 -15
  78. package/dist/content/setup.js +0 -14
  79. package/dist/content/verbs.js +0 -41
  80. package/dist/domain/details.js +0 -53
  81. package/dist/domain/messages.js +0 -63
  82. package/dist/domain/paths.js +0 -16
  83. package/dist/domain/providers.js +0 -11
  84. package/dist/domain/roles.js +0 -6
  85. package/dist/domain/slash.js +0 -11
  86. package/dist/domain/usage.js +0 -1
  87. package/dist/domain/viewport.js +0 -33
  88. package/dist/gateway/client.js +0 -312
  89. package/dist/gatewayClient.js +0 -574
  90. package/dist/gatewayTypes.js +0 -1
  91. package/dist/hooks/useCompletion.js +0 -86
  92. package/dist/hooks/useGitBranch.js +0 -58
  93. package/dist/hooks/useInputHistory.js +0 -12
  94. package/dist/hooks/useQueue.js +0 -57
  95. package/dist/hooks/useVirtualHistory.js +0 -401
  96. package/dist/lib/circularBuffer.js +0 -43
  97. package/dist/lib/clipboard.js +0 -126
  98. package/dist/lib/editor.js +0 -41
  99. package/dist/lib/editor.test.js +0 -58
  100. package/dist/lib/emoji.js +0 -49
  101. package/dist/lib/externalCli.js +0 -11
  102. package/dist/lib/forceTruecolor.js +0 -26
  103. package/dist/lib/fpsStore.js +0 -36
  104. package/dist/lib/gracefulExit.js +0 -29
  105. package/dist/lib/history.js +0 -69
  106. package/dist/lib/inputMetrics.js +0 -143
  107. package/dist/lib/liveProgress.js +0 -51
  108. package/dist/lib/liveProgress.test.js +0 -89
  109. package/dist/lib/mathUnicode.js +0 -685
  110. package/dist/lib/memory.js +0 -123
  111. package/dist/lib/memoryMonitor.js +0 -76
  112. package/dist/lib/messages.js +0 -3
  113. package/dist/lib/messages.test.js +0 -25
  114. package/dist/lib/osc52.js +0 -53
  115. package/dist/lib/perfPane.js +0 -94
  116. package/dist/lib/platform.js +0 -312
  117. package/dist/lib/precisionWheel.js +0 -25
  118. package/dist/lib/reasoning.js +0 -39
  119. package/dist/lib/rpc.js +0 -26
  120. package/dist/lib/subagentTree.js +0 -287
  121. package/dist/lib/syntax.js +0 -89
  122. package/dist/lib/terminalModes.js +0 -46
  123. package/dist/lib/terminalParity.js +0 -48
  124. package/dist/lib/terminalSetup.js +0 -321
  125. package/dist/lib/text.js +0 -203
  126. package/dist/lib/text.test.js +0 -18
  127. package/dist/lib/todo.js +0 -2
  128. package/dist/lib/todo.test.js +0 -22
  129. package/dist/lib/viewportStore.js +0 -82
  130. package/dist/lib/virtualHeights.js +0 -61
  131. package/dist/lib/wheelAccel.js +0 -143
  132. package/dist/theme.js +0 -398
  133. 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
- }
@@ -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
- }