gnhf 0.1.9 → 0.1.10
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/README.md +1 -0
- package/dist/cli.mjs +185 -38
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -47,6 +47,7 @@ You wake up to a branch full of clean work and a log of everything that happened
|
|
|
47
47
|
- **Dead simple** — one command starts an autonomous loop that runs until you Ctrl+C or a configured runtime cap is reached
|
|
48
48
|
- **Long running** — each iteration is committed on success, rolled back on failure, with sensible retries and exponential backoff
|
|
49
49
|
- **Agent-agnostic** — works with Claude Code, Codex, Rovo Dev, or OpenCode out of the box
|
|
50
|
+
- **Terminal-safe rendering** — the live UI keeps wide Unicode text such as emoji and CJK glyphs aligned instead of clipping or shifting the frame
|
|
50
51
|
|
|
51
52
|
## Quick Start
|
|
52
53
|
|
package/dist/cli.mjs
CHANGED
|
@@ -2356,7 +2356,68 @@ function formatTokens(count) {
|
|
|
2356
2356
|
return String(count);
|
|
2357
2357
|
}
|
|
2358
2358
|
//#endregion
|
|
2359
|
+
//#region src/utils/terminal-width.ts
|
|
2360
|
+
const graphemeSegmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
2361
|
+
const MARK_REGEX = /\p{Mark}/u;
|
|
2362
|
+
const REGIONAL_INDICATOR_REGEX = /\p{Regional_Indicator}/u;
|
|
2363
|
+
const EXTENDED_PICTOGRAPHIC_REGEX = /\p{Extended_Pictographic}/u;
|
|
2364
|
+
function isFullWidthCodePoint(codePoint) {
|
|
2365
|
+
return codePoint >= 4352 && (codePoint <= 4447 || codePoint === 9001 || codePoint === 9002 || codePoint >= 11904 && codePoint <= 12871 && codePoint !== 12351 || codePoint >= 12880 && codePoint <= 19903 || codePoint >= 19968 && codePoint <= 42182 || codePoint >= 43360 && codePoint <= 43388 || codePoint >= 44032 && codePoint <= 55203 || codePoint >= 63744 && codePoint <= 64255 || codePoint >= 65040 && codePoint <= 65049 || codePoint >= 65072 && codePoint <= 65131 || codePoint >= 65281 && codePoint <= 65376 || codePoint >= 65504 && codePoint <= 65510 || codePoint >= 110592 && codePoint <= 110593 || codePoint >= 127488 && codePoint <= 127569 || codePoint >= 131072 && codePoint <= 262141);
|
|
2366
|
+
}
|
|
2367
|
+
function codePointWidth(codePoint) {
|
|
2368
|
+
if (codePoint === 0 || codePoint === 8204 || codePoint === 8205 || codePoint === 65038 || codePoint === 65039) return 0;
|
|
2369
|
+
if (MARK_REGEX.test(String.fromCodePoint(codePoint))) return 0;
|
|
2370
|
+
return isFullWidthCodePoint(codePoint) ? 2 : 1;
|
|
2371
|
+
}
|
|
2372
|
+
function isWideEmojiGrapheme(grapheme) {
|
|
2373
|
+
return grapheme.includes("") || grapheme.includes("️") || grapheme.includes("⃣") || REGIONAL_INDICATOR_REGEX.test(grapheme) || Array.from(grapheme).some((char) => EXTENDED_PICTOGRAPHIC_REGEX.test(char));
|
|
2374
|
+
}
|
|
2375
|
+
function splitGraphemes(text) {
|
|
2376
|
+
return Array.from(graphemeSegmenter.segment(text), ({ segment }) => segment);
|
|
2377
|
+
}
|
|
2378
|
+
function graphemeWidth(grapheme) {
|
|
2379
|
+
if (!grapheme) return 0;
|
|
2380
|
+
if (isWideEmojiGrapheme(grapheme)) return 2;
|
|
2381
|
+
let width = 0;
|
|
2382
|
+
for (const char of grapheme) width += codePointWidth(char.codePointAt(0) ?? 0);
|
|
2383
|
+
return width;
|
|
2384
|
+
}
|
|
2385
|
+
function stringWidth(text) {
|
|
2386
|
+
let width = 0;
|
|
2387
|
+
for (const grapheme of splitGraphemes(text)) width += graphemeWidth(grapheme);
|
|
2388
|
+
return width;
|
|
2389
|
+
}
|
|
2390
|
+
//#endregion
|
|
2359
2391
|
//#region src/utils/wordwrap.ts
|
|
2392
|
+
function sliceToWidth(text, width) {
|
|
2393
|
+
let result = "";
|
|
2394
|
+
let currentWidth = 0;
|
|
2395
|
+
for (const grapheme of splitGraphemes(text)) {
|
|
2396
|
+
const nextWidth = currentWidth + graphemeWidth(grapheme);
|
|
2397
|
+
if (nextWidth > width) break;
|
|
2398
|
+
result += grapheme;
|
|
2399
|
+
currentWidth = nextWidth;
|
|
2400
|
+
}
|
|
2401
|
+
return result;
|
|
2402
|
+
}
|
|
2403
|
+
function splitByWidth(text, width) {
|
|
2404
|
+
const lines = [];
|
|
2405
|
+
let current = "";
|
|
2406
|
+
let currentWidth = 0;
|
|
2407
|
+
for (const grapheme of splitGraphemes(text)) {
|
|
2408
|
+
const glyphWidth = graphemeWidth(grapheme);
|
|
2409
|
+
if (current && currentWidth + glyphWidth > width) {
|
|
2410
|
+
lines.push(current);
|
|
2411
|
+
current = grapheme;
|
|
2412
|
+
currentWidth = glyphWidth;
|
|
2413
|
+
continue;
|
|
2414
|
+
}
|
|
2415
|
+
current += grapheme;
|
|
2416
|
+
currentWidth += glyphWidth;
|
|
2417
|
+
}
|
|
2418
|
+
if (current) lines.push(current);
|
|
2419
|
+
return lines;
|
|
2420
|
+
}
|
|
2360
2421
|
function wordWrap(text, width, maxLines) {
|
|
2361
2422
|
if (!text) return [];
|
|
2362
2423
|
const lines = [];
|
|
@@ -2367,26 +2428,34 @@ function wordWrap(text, width, maxLines) {
|
|
|
2367
2428
|
continue;
|
|
2368
2429
|
}
|
|
2369
2430
|
let current = "";
|
|
2431
|
+
let currentWidth = 0;
|
|
2370
2432
|
for (const word of words) {
|
|
2371
|
-
|
|
2433
|
+
const wordWidth = stringWidth(word);
|
|
2434
|
+
if (wordWidth > width) {
|
|
2372
2435
|
if (current) {
|
|
2373
2436
|
lines.push(current);
|
|
2374
2437
|
current = "";
|
|
2438
|
+
currentWidth = 0;
|
|
2375
2439
|
}
|
|
2376
|
-
for (
|
|
2440
|
+
for (const slice of splitByWidth(word, width)) lines.push(slice);
|
|
2377
2441
|
continue;
|
|
2378
2442
|
}
|
|
2379
|
-
|
|
2443
|
+
const nextWidth = current ? currentWidth + 1 + wordWidth : wordWidth;
|
|
2444
|
+
if (current && nextWidth > width) {
|
|
2380
2445
|
lines.push(current);
|
|
2381
2446
|
current = word;
|
|
2382
|
-
|
|
2447
|
+
currentWidth = wordWidth;
|
|
2448
|
+
} else {
|
|
2449
|
+
current = current ? current + " " + word : word;
|
|
2450
|
+
currentWidth = nextWidth;
|
|
2451
|
+
}
|
|
2383
2452
|
}
|
|
2384
2453
|
if (current) lines.push(current);
|
|
2385
2454
|
}
|
|
2386
2455
|
if (maxLines && lines.length > maxLines) {
|
|
2387
2456
|
const capped = lines.slice(0, maxLines);
|
|
2388
2457
|
const last = capped[maxLines - 1];
|
|
2389
|
-
capped[maxLines - 1] = last
|
|
2458
|
+
capped[maxLines - 1] = stringWidth(last) >= width ? sliceToWidth(last, width - 1) + "…" : last + "…";
|
|
2390
2459
|
return capped;
|
|
2391
2460
|
}
|
|
2392
2461
|
return lines;
|
|
@@ -2402,13 +2471,13 @@ function makeCell(char, style) {
|
|
|
2402
2471
|
return {
|
|
2403
2472
|
char,
|
|
2404
2473
|
style,
|
|
2405
|
-
width: (char
|
|
2474
|
+
width: graphemeWidth(char)
|
|
2406
2475
|
};
|
|
2407
2476
|
}
|
|
2408
2477
|
function textToCells(text, style) {
|
|
2409
2478
|
const cells = [];
|
|
2410
|
-
for (const
|
|
2411
|
-
const cell = makeCell(
|
|
2479
|
+
for (const grapheme of splitGraphemes(text)) {
|
|
2480
|
+
const cell = makeCell(grapheme, style);
|
|
2412
2481
|
cells.push(cell);
|
|
2413
2482
|
if (cell.width === 2) cells.push({
|
|
2414
2483
|
char: "",
|
|
@@ -2490,7 +2559,7 @@ const TICK_MS = 200;
|
|
|
2490
2559
|
const MOONS_PER_ROW = 30;
|
|
2491
2560
|
const MOON_PHASE_PERIOD = 1600;
|
|
2492
2561
|
const MAX_MSG_LINES = 3;
|
|
2493
|
-
const MAX_MSG_LINE_LEN =
|
|
2562
|
+
const MAX_MSG_LINE_LEN = CONTENT_WIDTH;
|
|
2494
2563
|
const RESUME_HINT = "[ctrl+c to stop, gnhf again to resume]";
|
|
2495
2564
|
function spacedLabel(text) {
|
|
2496
2565
|
return text.split("").join(" ");
|
|
@@ -2584,52 +2653,124 @@ function renderSideStarsCells(stars, rowIndex, xOffset, sideWidth, now) {
|
|
|
2584
2653
|
placeStarsInCells(cells, stars, rowIndex, xOffset, xOffset + sideWidth, xOffset, now);
|
|
2585
2654
|
return cells;
|
|
2586
2655
|
}
|
|
2656
|
+
function clampCellsToWidth(content, width) {
|
|
2657
|
+
if (content.length <= width) return content;
|
|
2658
|
+
const clamped = [];
|
|
2659
|
+
let remaining = width;
|
|
2660
|
+
for (let i = 0; i < content.length && remaining > 0; i++) {
|
|
2661
|
+
const cell = content[i];
|
|
2662
|
+
if (cell.width === 0) continue;
|
|
2663
|
+
if (cell.width > remaining) break;
|
|
2664
|
+
clamped.push(cell);
|
|
2665
|
+
remaining -= cell.width;
|
|
2666
|
+
if (cell.width === 2 && content[i + 1]?.width === 0) {
|
|
2667
|
+
clamped.push(content[i + 1]);
|
|
2668
|
+
i += 1;
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
return clamped;
|
|
2672
|
+
}
|
|
2587
2673
|
function centerLineCells(content, width) {
|
|
2588
|
-
const
|
|
2674
|
+
const clamped = clampCellsToWidth(content, width);
|
|
2675
|
+
const w = clamped.length;
|
|
2589
2676
|
const pad = Math.max(0, Math.floor((width - w) / 2));
|
|
2590
2677
|
const rightPad = Math.max(0, width - w - pad);
|
|
2591
2678
|
return [
|
|
2592
2679
|
...emptyCells(pad),
|
|
2593
|
-
...
|
|
2680
|
+
...clamped,
|
|
2594
2681
|
...emptyCells(rightPad)
|
|
2595
2682
|
];
|
|
2596
2683
|
}
|
|
2597
2684
|
function renderResumeHintCells(width) {
|
|
2598
2685
|
return centerLineCells(textToCells(RESUME_HINT, "dim"), width);
|
|
2599
2686
|
}
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
return fitted.length > maxRows ? fitted.slice(fitted.length - maxRows) : fitted;
|
|
2609
|
-
}
|
|
2610
|
-
function buildContentCells(prompt, agentName, state, elapsed, now) {
|
|
2611
|
-
const rows = [];
|
|
2687
|
+
/**
|
|
2688
|
+
* Builds the centered content viewport for the renderer.
|
|
2689
|
+
*
|
|
2690
|
+
* When `availableHeight` is constrained, the layout drops optional sections in
|
|
2691
|
+
* priority order (ASCII art, eyebrow, agent message, then prompt) so the stats
|
|
2692
|
+
* row remains visible and any remaining space is used for the newest moon rows.
|
|
2693
|
+
*/
|
|
2694
|
+
function buildContentCells(prompt, agentName, state, elapsed, now, availableHeight) {
|
|
2612
2695
|
const isRunning = state.status === "running" || state.status === "waiting";
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2696
|
+
const moonRows = renderMoonStripCells(state.iterations, isRunning, now);
|
|
2697
|
+
const maxRows = availableHeight ?? Infinity;
|
|
2698
|
+
if (maxRows <= 0) return [];
|
|
2699
|
+
const titleCells = renderTitleCells(agentName);
|
|
2700
|
+
const titleSpacer = titleCells[1] ?? [];
|
|
2616
2701
|
const promptLines = wordWrap(prompt, CONTENT_WIDTH, MAX_PROMPT_LINES);
|
|
2702
|
+
const promptRows = [];
|
|
2617
2703
|
for (let i = 0; i < MAX_PROMPT_LINES; i++) {
|
|
2618
2704
|
const pl = promptLines[i] ?? "";
|
|
2619
|
-
|
|
2620
|
-
}
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2705
|
+
promptRows.push(pl ? textToCells(pl, "dim") : []);
|
|
2706
|
+
}
|
|
2707
|
+
const sections = {
|
|
2708
|
+
top: [[]],
|
|
2709
|
+
eyebrow: [
|
|
2710
|
+
titleCells[0],
|
|
2711
|
+
[],
|
|
2712
|
+
[]
|
|
2713
|
+
],
|
|
2714
|
+
art: titleCells.slice(2),
|
|
2715
|
+
prompt: [
|
|
2716
|
+
titleSpacer,
|
|
2717
|
+
...promptRows,
|
|
2718
|
+
[],
|
|
2719
|
+
[]
|
|
2720
|
+
],
|
|
2721
|
+
stats: [renderStatsCells(elapsed, state.totalInputTokens, state.totalOutputTokens, state.commitCount)],
|
|
2722
|
+
agent: [
|
|
2723
|
+
[],
|
|
2724
|
+
[],
|
|
2725
|
+
...renderAgentMessageCells(state.lastMessage, state.status)
|
|
2726
|
+
],
|
|
2727
|
+
moon: [
|
|
2728
|
+
[],
|
|
2729
|
+
[],
|
|
2730
|
+
...moonRows
|
|
2731
|
+
]
|
|
2732
|
+
};
|
|
2733
|
+
const flattenSections = () => [
|
|
2734
|
+
...sections.top,
|
|
2735
|
+
...sections.eyebrow,
|
|
2736
|
+
...sections.art,
|
|
2737
|
+
...sections.prompt,
|
|
2738
|
+
...sections.stats,
|
|
2739
|
+
...sections.agent,
|
|
2740
|
+
...sections.moon
|
|
2741
|
+
];
|
|
2742
|
+
const optionalSections = [
|
|
2743
|
+
"art",
|
|
2744
|
+
"eyebrow",
|
|
2745
|
+
"agent",
|
|
2746
|
+
"prompt"
|
|
2747
|
+
];
|
|
2748
|
+
let rows = flattenSections();
|
|
2749
|
+
for (const section of optionalSections) {
|
|
2750
|
+
if (rows.length <= maxRows) break;
|
|
2751
|
+
sections[section] = [];
|
|
2752
|
+
rows = flattenSections();
|
|
2753
|
+
}
|
|
2754
|
+
if (rows.length > maxRows) rows = rows.filter((row) => row.length > 0);
|
|
2755
|
+
if (rows.length > maxRows) {
|
|
2756
|
+
const nonMoonRows = [
|
|
2757
|
+
...sections.top,
|
|
2758
|
+
...sections.eyebrow,
|
|
2759
|
+
...sections.art,
|
|
2760
|
+
...sections.prompt,
|
|
2761
|
+
...sections.stats,
|
|
2762
|
+
...sections.agent
|
|
2763
|
+
].filter((row) => row.length > 0);
|
|
2764
|
+
const allowedMoonRows = Math.max(0, maxRows - nonMoonRows.length);
|
|
2765
|
+
const visibleMoonRows = allowedMoonRows === 0 ? [] : moonRows.filter((row) => row.length > 0).slice(-allowedMoonRows);
|
|
2766
|
+
rows = [...nonMoonRows, ...visibleMoonRows];
|
|
2767
|
+
}
|
|
2627
2768
|
return rows;
|
|
2628
2769
|
}
|
|
2629
2770
|
function buildFrameCells(prompt, agentName, state, topStars, bottomStars, sideStars, now, terminalWidth, terminalHeight) {
|
|
2630
2771
|
const elapsed = formatElapsed(now - state.startTime.getTime());
|
|
2631
2772
|
const availableHeight = Math.max(0, terminalHeight - 2);
|
|
2632
|
-
const contentRows =
|
|
2773
|
+
const contentRows = buildContentCells(prompt, agentName, state, elapsed, now, availableHeight);
|
|
2633
2774
|
while (contentRows.length < Math.min(BASE_CONTENT_ROWS, availableHeight)) contentRows.push([]);
|
|
2634
2775
|
const contentCount = contentRows.length;
|
|
2635
2776
|
const remaining = Math.max(0, availableHeight - contentCount);
|
|
@@ -2668,11 +2809,17 @@ var Renderer = class {
|
|
|
2668
2809
|
cachedHeight = 0;
|
|
2669
2810
|
prevCells = [];
|
|
2670
2811
|
isFirstFrame = true;
|
|
2812
|
+
seedTop;
|
|
2813
|
+
seedBottom;
|
|
2814
|
+
seedSide;
|
|
2671
2815
|
constructor(orchestrator, prompt, agentName) {
|
|
2672
2816
|
this.orchestrator = orchestrator;
|
|
2673
2817
|
this.prompt = prompt;
|
|
2674
2818
|
this.agentName = agentName;
|
|
2675
2819
|
this.state = orchestrator.getState();
|
|
2820
|
+
this.seedTop = Math.floor(Math.random() * 2147483646) + 1;
|
|
2821
|
+
this.seedBottom = Math.floor(Math.random() * 2147483646) + 1;
|
|
2822
|
+
this.seedSide = Math.floor(Math.random() * 2147483646) + 1;
|
|
2676
2823
|
this.exitPromise = new Promise((resolve) => {
|
|
2677
2824
|
this.exitResolve = resolve;
|
|
2678
2825
|
});
|
|
@@ -2736,9 +2883,9 @@ var Renderer = class {
|
|
|
2736
2883
|
rest: "dim"
|
|
2737
2884
|
} : star;
|
|
2738
2885
|
};
|
|
2739
|
-
this.topStars = generateStarField(w, h, STAR_DENSITY,
|
|
2740
|
-
this.bottomStars = generateStarField(w, h, STAR_DENSITY,
|
|
2741
|
-
this.sideStars = generateStarField(w, Math.max(BASE_CONTENT_ROWS, availableHeight), STAR_DENSITY,
|
|
2886
|
+
this.topStars = generateStarField(w, h, STAR_DENSITY, this.seedTop).map((s) => shrinkBig(s, s.y >= topHeight - proximityRows));
|
|
2887
|
+
this.bottomStars = generateStarField(w, h, STAR_DENSITY, this.seedBottom).map((s) => shrinkBig(s, s.y < proximityRows));
|
|
2888
|
+
this.sideStars = generateStarField(w, Math.max(BASE_CONTENT_ROWS, availableHeight), STAR_DENSITY, this.seedSide);
|
|
2742
2889
|
return true;
|
|
2743
2890
|
}
|
|
2744
2891
|
return false;
|