Something went wrong. Try again.
small gleam coding and persistent agent daemon with a detachable cli and a canvas ui forked from okami.mom/albedo
Something went wrong. Try again.
20 kB · 489 lines
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490diff --git a/node_modules/ink/build/frame-throttle.js b/node_modules/ink/build/frame-throttle.jsnew file mode 100644index 0000000..347e2ca--- /dev/null+++ b/node_modules/ink/build/frame-throttle.js@@ -0,0 +1,29 @@+// A render deadline must not move when another input event arrives. The compat+// debounce throttle only checks maxWait on the next call, losing frame cadence.+export function frameThrottle(render, interval) {+ let last = -Infinity;+ let timer;+ let pending = false;+ const invoke = () => {+ if (timer !== undefined) clearTimeout(timer);+ timer = undefined;+ if (!pending) return;+ pending = false;+ last = performance.now();+ render();+ };+ const schedule = () => {+ pending = true;+ const remaining = interval - (performance.now() - last);+ if (remaining <= 0) invoke();+ else if (timer === undefined) timer = setTimeout(invoke, Math.ceil(remaining));+ };+ schedule.flush = invoke;+ schedule.cancel = () => {+ if (timer !== undefined) clearTimeout(timer);+ timer = undefined;+ pending = false;+ last = -Infinity;+ };+ return schedule;+}diff --git a/node_modules/ink/build/hooks/use-window-size.js b/node_modules/ink/build/hooks/use-window-size.jsindex 579a30d..50c3100 100644--- a/node_modules/ink/build/hooks/use-window-size.js+++ b/node_modules/ink/build/hooks/use-window-size.js@@ -1,22 +1,24 @@-import { useState, useEffect } from 'react';+import { useMemo, useSyncExternalStore } from 'react'; import { getWindowSize } from '../utils.js'; import useStdout from './use-stdout.js';-/**-A React hook that returns the current terminal window dimensions and re-renders the component whenever the terminal is resized.-*/+// Terminal geometry is an external store, not a deferred application update.+// Resize painting must read the new dimensions even before a passive state update. const useWindowSize = () => { const { stdout } = useStdout();- const [size, setSize] = useState(() => getWindowSize(stdout));- useEffect(() => {- const onResize = () => {- setSize(getWindowSize(stdout));- };- stdout.on('resize', onResize);- return () => {- stdout.off('resize', onResize);+ const store = useMemo(() => {+ let size = getWindowSize(stdout);+ return {+ subscribe(notify) {+ stdout.on('resize', notify);+ return () => stdout.off('resize', notify);+ },+ getSnapshot() {+ const next = getWindowSize(stdout);+ if (next.columns !== size.columns || next.rows !== size.rows) size = next;+ return size;+ }, }; }, [stdout]);- return size;+ return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot); }; export default useWindowSize;-//# sourceMappingURL=use-window-size.js.map\ No newline at end of filediff --git a/node_modules/ink/build/ink.js b/node_modules/ink/build/ink.jsindex 0082666..2df8847 100644--- a/node_modules/ink/build/ink.js+++ b/node_modules/ink/build/ink.js@@ -1,6 +1,7 @@ import process from 'node:process'; import React from 'react';-import { throttle } from 'es-toolkit/compat';+import throttle from 'es-toolkit/compat/throttle';+import { frameThrottle } from './frame-throttle.js'; import ansiEscapes from 'ansi-escapes'; import isInCi from 'is-in-ci'; import autoBind from 'auto-bind';@@ -155,6 +156,9 @@ export default class Ink { lastOutputToRender; lastOutputHeight; lastTerminalWidth;+ lastTerminalHeight;+ forceFullRender = false;+ resizeImmediate; container; rootNode; // This variable is used only in debug mode to store full static output@@ -202,10 +206,7 @@ export default class Ink { this.throttledOnRender = undefined; } else {- const throttled = throttle(this.onRender, renderThrottleMs, {- leading: true,- trailing: true,- });+ const throttled = frameThrottle(this.onRender, renderThrottleMs); this.rootNode.onRender = () => { this.hasPendingThrottledRender = true; throttled();@@ -244,6 +245,7 @@ export default class Ink { this.lastOutputToRender = ''; this.lastOutputHeight = 0; this.lastTerminalWidth = getWindowSize(this.options.stdout).columns;+ this.lastTerminalHeight = getWindowSize(this.options.stdout).rows; // This variable is used only in debug mode to store full static output // so that it's rerendered every time, not just new static parts, like in non-debug mode this.fullStaticOutput = '';@@ -277,18 +279,34 @@ export default class Ink { void this.exitPromise.catch(noop); } resized = () => {- const currentWidth = getWindowSize(this.options.stdout).columns;- if (currentWidth < this.lastTerminalWidth) {- // We clear the screen when decreasing terminal width to prevent duplicate overlapping re-renders.+ const size = getWindowSize(this.options.stdout);+ // A terminal may reflow old rows before delivering resize. Their previous+ // cursor positions are no longer usable for an incremental fullscreen diff.+ this.throttledOnRender?.cancel();+ this.throttledLog?.cancel();+ if (this.lastOutputHeight >= this.lastTerminalHeight) {+ this.forceFullRender = true;+ this.log.reset();+ }+ else if (size.columns < this.lastTerminalWidth) { this.log.clear(); this.lastOutput = ''; this.lastOutputToRender = ''; }- this.calculateLayout();- dom.emitLayoutListeners(this.rootNode);- this.onRender();- this.lastTerminalWidth = currentWidth;+ this.lastTerminalWidth = size.columns;+ this.lastTerminalHeight = size.rows;+ if (this.resizeImmediate !== undefined) clearImmediate(this.resizeImmediate);+ // Let useWindowSize subscribers commit the new dimensions first. Painting+ // the old React geometry at the new terminal width corrupts row positions.+ this.resizeImmediate = setImmediate(() => {+ this.resizeImmediate = undefined;+ if (this.isUnmounted) return;+ this.calculateLayout();+ dom.emitLayoutListeners(this.rootNode);+ this.onRender();+ }); };+ resolveExitPromise = () => { }; rejectExitPromise = () => { }; unsubscribeExit = () => { };@@ -493,6 +511,8 @@ export default class Ink { return; } this.isUnmounting = true;+ if (this.resizeImmediate !== undefined) clearImmediate(this.resizeImmediate);+ this.resizeImmediate = undefined; if (this.beforeExitHandler) { process.off('beforeExit', this.beforeExitHandler); this.beforeExitHandler = undefined;@@ -753,13 +773,14 @@ export default class Ink { const viewportRows = isTty ? getWindowSize(this.options.stdout).rows : 24; const isFullscreen = isTty && outputHeight >= viewportRows; const outputToRender = isFullscreen ? output : output + '\n';- const shouldClearTerminal = shouldClearTerminalForFrame({+ const shouldClearTerminal = this.forceFullRender || shouldClearTerminalForFrame({ isTty, viewportRows, previousOutputHeight: this.lastOutputHeight, nextOutputHeight: outputHeight, isUnmounting: this.isUnmounting, });+ this.forceFullRender = false; if (shouldClearTerminal) { const sync = this.shouldSync(); if (sync) {diff --git a/node_modules/ink/build/measure-text.js b/node_modules/ink/build/measure-text.jsindex 6934dc5..0d78e3d 100644--- a/node_modules/ink/build/measure-text.js+++ b/node_modules/ink/build/measure-text.js@@ -1,5 +1,6 @@ import widestLine from 'widest-line';-const cache = new Map();+import TextCache from './text-cache.js';+const cache = new TextCache(); const measureText = (text) => { if (text.length === 0) { return {diff --git a/node_modules/ink/build/output.js b/node_modules/ink/build/output.jsindex c763b77..8c87a13 100644--- a/node_modules/ink/build/output.js+++ b/node_modules/ink/build/output.js@@ -1,48 +1,67 @@ import sliceAnsi from 'slice-ansi'; import stringWidth from 'string-width'; import { styledCharsFromTokens, styledCharsToString, tokenize, } from '@alcalzone/ansi-tokenize';-class OutputCaches {- widths = new Map();- blockWidths = new Map();- styledChars = new Map();- getStyledChars(line) {- let cached = this.styledChars.get(line);- if (cached === undefined) {- cached = styledCharsFromTokens(tokenize(line));- this.styledChars.set(line, cached);+import { styledRuns } from './styled-runs.js';+// Two bounded generations: scrolling reuses nearby rows without retaining history.+class FrameCache {+ current = new Map();+ previous = new Map();+ units = 0;+ nextFrame() {+ this.previous = this.current;+ this.current = new Map();+ this.units = 0;+ }+ get(key, compute) {+ const current = this.current.get(key);+ if (current !== undefined) return current;+ const value = this.previous.get(key) ?? compute(key);+ if (this.current.size < 1024 && this.units + key.length + 1 <= 65536) {+ this.current.set(key, value);+ this.units += key.length + 1; }- return cached;+ return value;+ }+}+const parseStyledLine = line => styledCharsFromTokens(tokenize(line));+export class OutputCaches {+ widths = new FrameCache();+ blockWidths = new FrameCache();+ styledChars = new FrameCache();+ rows = new FrameCache();+ nextFrame() {+ this.widths.nextFrame();+ this.blockWidths.nextFrame();+ this.styledChars.nextFrame();+ this.rows.nextFrame();+ }+ getStyledChars(line) {+ return this.styledChars.get(line, parseStyledLine); } getStringWidth(text) {- let cached = this.widths.get(text);- if (cached === undefined) {- cached = stringWidth(text);- this.widths.set(text, cached);- }- return cached;+ return this.widths.get(text, stringWidth); }- getWidestLine(text) {- let cached = this.blockWidths.get(text);- if (cached === undefined) {- let lineWidth = 0;- for (const line of text.split('\n')) {- lineWidth = Math.max(lineWidth, this.getStringWidth(line));- }- cached = lineWidth;- this.blockWidths.set(text, cached);+ measureBlock = text => {+ let lineWidth = 0;+ for (const line of text.split('\n')) {+ lineWidth = Math.max(lineWidth, this.getStringWidth(line)); }- return cached;+ return lineWidth;+ };+ getWidestLine(text) {+ return this.blockWidths.get(text, this.measureBlock); } } export default class Output { width; height; operations = [];- caches = new OutputCaches();+ caches; constructor(options) { const { width, height } = options; this.width = width; this.height = height;+ this.caches = options.caches ?? new OutputCaches(); } write(x, y, text, options) { const { transformers } = options;@@ -71,17 +90,9 @@ export default class Output { get() { // Initialize output array with a specific set of rows, so that margin/padding at the bottom is preserved const output = [];+ const spaceCell = { type: 'char', value: ' ', fullWidth: false, styles: [] }; for (let y = 0; y < this.height; y++) {- const row = [];- for (let x = 0; x < this.width; x++) {- row.push({- type: 'char',- value: ' ',- fullWidth: false,- styles: [],- });- }- output.push(row);+ output.push([]); } const clips = []; for (const operation of this.operations) {@@ -144,19 +155,24 @@ export default class Output { for (const transformer of transformers) { line = transformer(line, index); }+ currentLine.push([x, line]);+ offsetY++;+ }+ }+ }+ const generatedOutput = output+ .map(writes => this.caches.rows.get(JSON.stringify([this.width, writes]), () => {+ // Cache composed rows, not the screen's character matrix. Scrolling+ // reuses the same row at another y; a spinner changes only its row.+ const currentLine = [];+ for (let x = 0; x < this.width; x++) currentLine.push(spaceCell);+ for (const [x, line] of writes) { const characters = this.caches.getStyledChars(line); let offsetX = x; // Nothing to write (e.g. line was clipped away). if (characters.length === 0) {- offsetY++; continue; }- const spaceCell = {- type: 'char',- value: ' ',- fullWidth: false,- styles: [],- }; // Wide characters (e.g. CJK) occupy two cells: a leading // cell with the character and a trailing placeholder with // value ''. When an overlapping write lands in the middle@@ -188,16 +204,9 @@ export default class Output { if (currentLine[offsetX]?.value === '') { currentLine[offsetX] = spaceCell; }- offsetY++; }- }- }- const generatedOutput = output- .map(line => {- // See https://github.com/vadimdemedes/ink/pull/564#issuecomment-1637022742- const lineWithoutEmptyItems = line.filter(item => item !== undefined);- return styledCharsToString(lineWithoutEmptyItems).trimEnd();- })+ return styledCharsToString(styledRuns(currentLine)).trimEnd();+ })) .join('\n'); return { output: generatedOutput,diff --git a/node_modules/ink/build/renderer.js b/node_modules/ink/build/renderer.jsindex bf23246..5247e84 100644--- a/node_modules/ink/build/renderer.js+++ b/node_modules/ink/build/renderer.js@@ -1,5 +1,6 @@ import renderNodeToOutput, { renderNodeToScreenReaderOutput, } from './render-node-to-output.js';-import Output from './output.js';+import Output, { OutputCaches } from './output.js';+const outputCaches = new WeakMap(); const renderer = (node, isScreenReaderEnabled) => { if (node.yogaNode) { if (isScreenReaderEnabled) {@@ -19,9 +20,13 @@ const renderer = (node, isScreenReaderEnabled) => { staticOutput: staticOutput ? `${staticOutput}\n` : '', }; }+ const caches = outputCaches.get(node) ?? new OutputCaches();+ caches.nextFrame();+ outputCaches.set(node, caches); const output = new Output({ width: node.yogaNode.getComputedWidth(), height: node.yogaNode.getComputedHeight(),+ caches, }); renderNodeToOutput(node, output, { skipStaticElements: true,diff --git a/node_modules/ink/build/styled-runs.js b/node_modules/ink/build/styled-runs.jsnew file mode 100644index 0000000..87a4580--- /dev/null+++ b/node_modules/ink/build/styled-runs.js@@ -0,0 +1,21 @@+// Style transitions belong to runs, not individual terminal cells. Keep distinct+// style objects conservative: identity equality proves the transition is a no-op.+const sameStyles = (left, right) => left.length === right.length &&+ left.every((code, index) => code === right[index]);+export function styledRuns(characters) {+ const runs = [];+ let run;+ const values = [];+ for (const character of characters) {+ if (character === undefined) continue;+ if (!run || !sameStyles(run.styles, character.styles)) {+ if (run) run.value = values.join('');+ run = { ...character };+ runs.push(run);+ values.length = 0;+ }+ values.push(character.value);+ }+ if (run) run.value = values.join('');+ return runs;+}diff --git a/node_modules/ink/build/text-cache.js b/node_modules/ink/build/text-cache.jsnew file mode 100644index 0000000..96e6c90--- /dev/null+++ b/node_modules/ink/build/text-cache.js@@ -0,0 +1,30 @@+// Wrapping and measurement see a new string on every streamed frame. Retain+// nearby frames only, bounded by both entry count and UTF-16 storage units.+export default class TextCache {+ entries = new Map();+ units = 0;+ constructor(maxUnits = 65536, maxEntries = 128) {+ this.maxUnits = maxUnits;+ this.maxEntries = maxEntries;+ }+ get(key) {+ const entry = this.entries.get(key);+ if (entry === undefined) return undefined;+ this.entries.delete(key);+ this.entries.set(key, entry);+ return entry.value;+ }+ set(key, value) {+ const old = this.entries.get(key);+ if (old !== undefined) { this.units -= old.units; this.entries.delete(key); }+ const units = key.length + (typeof value === 'string' ? value.length : 0) + 1;+ if (units > this.maxUnits) return;+ this.entries.set(key, { value, units });+ this.units += units;+ while (this.units > this.maxUnits || this.entries.size > this.maxEntries) {+ const first = this.entries.keys().next().value;+ this.units -= this.entries.get(first).units;+ this.entries.delete(first);+ }+ }+}diff --git a/node_modules/ink/build/wrap-text.js b/node_modules/ink/build/wrap-text.jsindex 51f696c..46b0c90 100644--- a/node_modules/ink/build/wrap-text.js+++ b/node_modules/ink/build/wrap-text.js@@ -1,10 +1,11 @@ import wrapAnsi from 'wrap-ansi'; import cliTruncate from 'cli-truncate';-const cache = {};+import TextCache from './text-cache.js';+const cache = new TextCache(); const wrapText = (text, maxWidth, wrapType) => {- const cacheKey = text + String(maxWidth) + String(wrapType);- const cachedText = cache[cacheKey];- if (cachedText) {+ const cacheKey = `${maxWidth}:${wrapType}:${text}`;+ const cachedText = cache.get(cacheKey);+ if (cachedText !== undefined) { return cachedText; } let wrappedText = text;@@ -31,7 +32,7 @@ const wrapText = (text, maxWidth, wrapType) => { } wrappedText = cliTruncate(text, maxWidth, { position }); }- cache[cacheKey] = wrappedText;+ cache.set(cacheKey, wrappedText); return wrappedText; }; export default wrapText;