klyro 0.1.45 → 0.1.47

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.
@@ -0,0 +1,99 @@
1
+ /**
2
+ * scroll.md §5 — Anchor-based scroll model (pure, no React).
3
+ *
4
+ * Position is an Anchor ({itemId, lineInItem}), never a raw row index (I4),
5
+ * so resizes, group expand/collapse, and compaction never make the view jump.
6
+ * While anchored to 'bottom', new output follows; the instant the user scrolls
7
+ * up the anchor pins to an item and freezes, accumulating newSinceUnstick
8
+ * display lines for the `↓ N new` badge (§7).
9
+ */
10
+ export const FOLLOW_EPSILON = 1; // within 1 line of bottom counts as "at bottom" (§13)
11
+ export const initialScroll = {
12
+ anchor: { mode: 'bottom' },
13
+ userScrolled: false,
14
+ newSinceUnstick: 0,
15
+ };
16
+ function clampN(n, lo, hi) {
17
+ return Math.max(lo, Math.min(hi, n));
18
+ }
19
+ export function maxTopFor(ctx) {
20
+ return Math.max(0, ctx.total - ctx.viewportH);
21
+ }
22
+ function stickBottom() {
23
+ return { anchor: { mode: 'bottom' }, userScrolled: false, newSinceUnstick: 0 };
24
+ }
25
+ // FOLLOW_EPSILON is directional: scrolling DOWN into the last line re-sticks
26
+ // to bottom, but scrolling UP must always escape — otherwise single-line /
27
+ // wheel scrolling from the bottom could never leave it (dead scroll trap).
28
+ function pinAt(s, ctx, row, from) {
29
+ const maxTop = maxTopFor(ctx);
30
+ const top = clampN(row, 0, maxTop);
31
+ if (top >= maxTop - FOLLOW_EPSILON && top >= from)
32
+ return stickBottom();
33
+ if (ctx.count === 0)
34
+ return stickBottom();
35
+ const i = clampN(ctx.indexAt(top), 0, ctx.count - 1);
36
+ return {
37
+ anchor: { mode: 'pinned', itemId: ctx.keyOf(i), lineInItem: top - ctx.offsetOf(i) },
38
+ userScrolled: true,
39
+ newSinceUnstick: s.newSinceUnstick,
40
+ };
41
+ }
42
+ export function scrollReducer(s, a, ctx) {
43
+ const maxTop = maxTopFor(ctx);
44
+ // current resolved top (anchor may predate this frame's measurements)
45
+ const cur = resolveTopRow(s, ctx).topRow;
46
+ switch (a.type) {
47
+ case 'BY_LINES':
48
+ return pinAt(s, ctx, cur + a.delta, cur);
49
+ case 'BY_PAGE':
50
+ return pinAt(s, ctx, cur + a.dir * (ctx.viewportH - 1), cur); // 1-line overlap
51
+ case 'BY_HALF_PAGE':
52
+ return pinAt(s, ctx, cur + a.dir * Math.floor(ctx.viewportH / 2), cur);
53
+ case 'TO_TOP':
54
+ return pinAt(s, ctx, 0, cur);
55
+ case 'TO_BOTTOM':
56
+ return stickBottom();
57
+ case 'CONTENT_GREW':
58
+ if (a.lines <= 0) {
59
+ // tail shrank/rewrote (§12): keep anchor, recompute, don't count
60
+ return { ...s };
61
+ }
62
+ if (s.anchor.mode === 'bottom') {
63
+ // follow — keep the counter clean; topRow derives from maxTop
64
+ return { ...s, newSinceUnstick: 0 };
65
+ }
66
+ void maxTop;
67
+ return { ...s, newSinceUnstick: s.newSinceUnstick + a.lines };
68
+ case 'REFLOW':
69
+ return { ...s };
70
+ }
71
+ }
72
+ /** Anchor → topRow, run once per frame after the line index rebuild (§5.3). */
73
+ export function resolveTopRow(s, ctx) {
74
+ const maxTop = maxTopFor(ctx);
75
+ if (s.anchor.mode === 'bottom')
76
+ return { topRow: maxTop, atBottom: true };
77
+ let i = -1;
78
+ for (let k = 0; k < ctx.count; k++) {
79
+ if (ctx.keyOf(k) === s.anchor.itemId) {
80
+ i = k;
81
+ break;
82
+ }
83
+ }
84
+ if (i === -1)
85
+ return { topRow: maxTop, atBottom: true }; // pruned → re-stick (§12)
86
+ const raw = ctx.offsetOf(i) + Math.max(0, s.anchor.lineInItem);
87
+ const topRow = clampN(raw, 0, maxTop);
88
+ return { topRow, atBottom: topRow >= maxTop - FOLLOW_EPSILON };
89
+ }
90
+ /** Badge label per §7.2. Empty string = hidden. */
91
+ export function badgeLabel(atBottom, newSinceUnstick, idle) {
92
+ if (atBottom || newSinceUnstick <= 0)
93
+ return '';
94
+ if (idle)
95
+ return '↓ jump to end';
96
+ if (newSinceUnstick >= 1000)
97
+ return '↓ 999+ new';
98
+ return `↓ ${newSinceUnstick} new`;
99
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klyro",
3
- "version": "0.1.45",
3
+ "version": "0.1.47",
4
4
  "description": "Klyro \u2014 autonomous coding harness CLI that streams from any OpenAI-compatible or Anthropic LLM endpoint.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",