jeo-code 0.4.9 → 0.5.0
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.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/package.json +1 -1
- package/src/agent/context-files.ts +14 -1
- package/src/agent/state.ts +93 -23
- package/src/tui/components/ascii-art.ts +22 -2
package/README.ja.md
CHANGED
|
@@ -150,11 +150,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
|
|
|
150
150
|
## 変更履歴 (Changelog)
|
|
151
151
|
|
|
152
152
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
153
|
+
- **[0.5.0]** (2026-06-14) — Performance: workspace-scan, workflow-state, and DNA-Claw HUD caches; plus a credential-safety fix that never wipes OAuth over an invalid config.
|
|
153
154
|
- **[0.4.9]** (2026-06-14) — Live-frame width-clamp (content-sized height) replaces the constant-height approach, typed text shows during a running turn, and a docs/AGENTS refresh.
|
|
154
155
|
- **[0.4.8]** (2026-06-14) — Live-frame stability: constant-height live turn, renderer self-heal off-by-one fix, and frame-safe child-stdout sanitizing — no more duplicate model bar or torn escapes.
|
|
155
156
|
- **[0.4.7]** (2026-06-14) — Detached subagents + `subagent` control tool, live shaded in-flight output, registry-driven providers, fuller `read` budget, styled italics in the final report, and `gjc` retired.
|
|
156
157
|
- **[0.4.6]** (2026-06-14) — Width-correct forge cards for CJK/emoji, red borders on failed tool cards, aligned `ooo ralph` monitor HUD, and a per-theme user-card palette.
|
|
157
|
-
- **[0.4.5]** (2026-06-14) — First-class filesystem make/remove tools.
|
|
158
158
|
|
|
159
159
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
160
160
|
<!-- CHANGELOG:END -->
|
package/README.ko.md
CHANGED
|
@@ -150,11 +150,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
|
|
|
150
150
|
## 변경 이력 (Changelog)
|
|
151
151
|
|
|
152
152
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
153
|
+
- **[0.5.0]** (2026-06-14) — Performance: workspace-scan, workflow-state, and DNA-Claw HUD caches; plus a credential-safety fix that never wipes OAuth over an invalid config.
|
|
153
154
|
- **[0.4.9]** (2026-06-14) — Live-frame width-clamp (content-sized height) replaces the constant-height approach, typed text shows during a running turn, and a docs/AGENTS refresh.
|
|
154
155
|
- **[0.4.8]** (2026-06-14) — Live-frame stability: constant-height live turn, renderer self-heal off-by-one fix, and frame-safe child-stdout sanitizing — no more duplicate model bar or torn escapes.
|
|
155
156
|
- **[0.4.7]** (2026-06-14) — Detached subagents + `subagent` control tool, live shaded in-flight output, registry-driven providers, fuller `read` budget, styled italics in the final report, and `gjc` retired.
|
|
156
157
|
- **[0.4.6]** (2026-06-14) — Width-correct forge cards for CJK/emoji, red borders on failed tool cards, aligned `ooo ralph` monitor HUD, and a per-theme user-card palette.
|
|
157
|
-
- **[0.4.5]** (2026-06-14) — First-class filesystem make/remove tools.
|
|
158
158
|
|
|
159
159
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
160
160
|
<!-- CHANGELOG:END -->
|
package/README.md
CHANGED
|
@@ -150,11 +150,11 @@ Required npm token permissions (repository secret `NPM_TOKEN`):
|
|
|
150
150
|
## Changelog
|
|
151
151
|
|
|
152
152
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
153
|
+
- **[0.5.0]** (2026-06-14) — Performance: workspace-scan, workflow-state, and DNA-Claw HUD caches; plus a credential-safety fix that never wipes OAuth over an invalid config.
|
|
153
154
|
- **[0.4.9]** (2026-06-14) — Live-frame width-clamp (content-sized height) replaces the constant-height approach, typed text shows during a running turn, and a docs/AGENTS refresh.
|
|
154
155
|
- **[0.4.8]** (2026-06-14) — Live-frame stability: constant-height live turn, renderer self-heal off-by-one fix, and frame-safe child-stdout sanitizing — no more duplicate model bar or torn escapes.
|
|
155
156
|
- **[0.4.7]** (2026-06-14) — Detached subagents + `subagent` control tool, live shaded in-flight output, registry-driven providers, fuller `read` budget, styled italics in the final report, and `gjc` retired.
|
|
156
157
|
- **[0.4.6]** (2026-06-14) — Width-correct forge cards for CJK/emoji, red borders on failed tool cards, aligned `ooo ralph` monitor HUD, and a per-theme user-card palette.
|
|
157
|
-
- **[0.4.5]** (2026-06-14) — First-class filesystem make/remove tools.
|
|
158
158
|
|
|
159
159
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
160
160
|
<!-- CHANGELOG:END -->
|
package/README.zh.md
CHANGED
|
@@ -150,11 +150,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
|
|
|
150
150
|
## 更新日志 (Changelog)
|
|
151
151
|
|
|
152
152
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
153
|
+
- **[0.5.0]** (2026-06-14) — Performance: workspace-scan, workflow-state, and DNA-Claw HUD caches; plus a credential-safety fix that never wipes OAuth over an invalid config.
|
|
153
154
|
- **[0.4.9]** (2026-06-14) — Live-frame width-clamp (content-sized height) replaces the constant-height approach, typed text shows during a running turn, and a docs/AGENTS refresh.
|
|
154
155
|
- **[0.4.8]** (2026-06-14) — Live-frame stability: constant-height live turn, renderer self-heal off-by-one fix, and frame-safe child-stdout sanitizing — no more duplicate model bar or torn escapes.
|
|
155
156
|
- **[0.4.7]** (2026-06-14) — Detached subagents + `subagent` control tool, live shaded in-flight output, registry-driven providers, fuller `read` budget, styled italics in the final report, and `gjc` retired.
|
|
156
157
|
- **[0.4.6]** (2026-06-14) — Width-correct forge cards for CJK/emoji, red borders on failed tool cards, aligned `ooo ralph` monitor HUD, and a per-theme user-card palette.
|
|
157
|
-
- **[0.4.5]** (2026-06-14) — First-class filesystem make/remove tools.
|
|
158
158
|
|
|
159
159
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
160
160
|
<!-- CHANGELOG:END -->
|
package/package.json
CHANGED
|
@@ -78,7 +78,10 @@ const EXPLICIT_GUIDANCE_FILES = [".agents/oma-config.yaml", ".agents/oma-config.
|
|
|
78
78
|
|
|
79
79
|
// Module-level cache of the downward scan, keyed by resolved cwd. The scan is
|
|
80
80
|
// independent of $HOME (it only walks the cwd subtree), so caching by cwd is safe.
|
|
81
|
+
// Bounded LRU: a long-running session that scans many distinct cwds (subagents,
|
|
82
|
+
// worktrees, /view of other trees) must not grow this Map without bound.
|
|
81
83
|
const workspaceScanCache = new Map<string, WorkspaceScan>();
|
|
84
|
+
const WORKSPACE_SCAN_CACHE_CAP = 32;
|
|
82
85
|
|
|
83
86
|
function segsEqual(a: readonly string[], b: readonly string[]): boolean {
|
|
84
87
|
if (a.length !== b.length) return false;
|
|
@@ -161,8 +164,18 @@ async function scanWorkspaceDownwards(resolvedCwd: string): Promise<WorkspaceSca
|
|
|
161
164
|
async function getWorkspaceScan(cwd: string): Promise<WorkspaceScan> {
|
|
162
165
|
const resolvedCwd = path.resolve(cwd);
|
|
163
166
|
const cached = workspaceScanCache.get(resolvedCwd);
|
|
164
|
-
if (cached)
|
|
167
|
+
if (cached) {
|
|
168
|
+
// LRU refresh: re-insert so the most-recently-used entry is evicted last.
|
|
169
|
+
workspaceScanCache.delete(resolvedCwd);
|
|
170
|
+
workspaceScanCache.set(resolvedCwd, cached);
|
|
171
|
+
return cached;
|
|
172
|
+
}
|
|
165
173
|
const scan = await scanWorkspaceDownwards(resolvedCwd);
|
|
174
|
+
// Evict the oldest entry (Map preserves insertion order) once at capacity.
|
|
175
|
+
if (workspaceScanCache.size >= WORKSPACE_SCAN_CACHE_CAP) {
|
|
176
|
+
const oldest = workspaceScanCache.keys().next().value;
|
|
177
|
+
if (oldest !== undefined) workspaceScanCache.delete(oldest);
|
|
178
|
+
}
|
|
166
179
|
workspaceScanCache.set(resolvedCwd, scan);
|
|
167
180
|
return scan;
|
|
168
181
|
}
|
package/src/agent/state.ts
CHANGED
|
@@ -233,7 +233,7 @@ type ParsedFile =
|
|
|
233
233
|
| { kind: "ok"; config: Config }
|
|
234
234
|
| { kind: "missing" }
|
|
235
235
|
| { kind: "bad-json"; warned: boolean }
|
|
236
|
-
| { kind: "invalid"; message: string; warned: boolean };
|
|
236
|
+
| { kind: "invalid"; message: string; warned: boolean; rawObject?: unknown };
|
|
237
237
|
const configReadCache = new Map<string, { mtimeMs: number; size: number; parsed: ParsedFile }>();
|
|
238
238
|
const CONFIG_CACHE_CAP = 8;
|
|
239
239
|
|
|
@@ -259,7 +259,9 @@ async function readParsedConfigFile(): Promise<ParsedFile> {
|
|
|
259
259
|
const result = parseConfig(raw);
|
|
260
260
|
parsed = result.ok
|
|
261
261
|
? { kind: "ok", config: result.config as Config }
|
|
262
|
-
|
|
262
|
+
// Keep the raw JSON object on a schema-invalid (but JSON-valid) config so
|
|
263
|
+
// readRawGlobalConfig can salvage the credential blocks instead of dropping them.
|
|
264
|
+
: { kind: "invalid", message: result.message, warned: false, rawObject: raw };
|
|
263
265
|
} catch {
|
|
264
266
|
parsed = { kind: "bad-json", warned: false };
|
|
265
267
|
}
|
|
@@ -271,6 +273,20 @@ async function readParsedConfigFile(): Promise<ParsedFile> {
|
|
|
271
273
|
return parsed;
|
|
272
274
|
}
|
|
273
275
|
|
|
276
|
+
/** Overlay the `oauth` + `providers` credential blocks from a schema-invalid (but
|
|
277
|
+
* JSON-valid) config's raw object onto `base`. A single bad scalar field (e.g. a
|
|
278
|
+
* non-string defaultModel) must never cost the user their stored credentials — neither
|
|
279
|
+
* when resolving them at runtime (readGlobalConfig) nor when persisting (readRawGlobalConfig). */
|
|
280
|
+
function salvageCredentials(base: Config, parsed: ParsedFile): Config {
|
|
281
|
+
if (parsed.kind !== "invalid" || !parsed.rawObject || typeof parsed.rawObject !== "object") return base;
|
|
282
|
+
const ro = parsed.rawObject as Record<string, unknown>;
|
|
283
|
+
if (ro.oauth && typeof ro.oauth === "object") base.oauth = structuredClone(ro.oauth) as Config["oauth"];
|
|
284
|
+
if (ro.providers && typeof ro.providers === "object") {
|
|
285
|
+
base.providers = { ...base.providers, ...(structuredClone(ro.providers) as Config["providers"]) };
|
|
286
|
+
}
|
|
287
|
+
return base;
|
|
288
|
+
}
|
|
289
|
+
|
|
274
290
|
export async function readGlobalConfig(): Promise<Config> {
|
|
275
291
|
const parsed = await readParsedConfigFile();
|
|
276
292
|
if (parsed.kind === "bad-json" || parsed.kind === "invalid") {
|
|
@@ -280,7 +296,9 @@ export async function readGlobalConfig(): Promise<Config> {
|
|
|
280
296
|
const detail = parsed.kind === "invalid" ? ` is invalid (${parsed.message})` : " is not valid JSON";
|
|
281
297
|
process.stderr.write(`[jeo] ${globalConfigPath()}${detail}; using environment defaults.\n`);
|
|
282
298
|
}
|
|
283
|
-
|
|
299
|
+
// Credential salvage: keep oauth/providers from a JSON-valid-but-schema-invalid
|
|
300
|
+
// config so the runtime stays authenticated (and a later write repairs the file).
|
|
301
|
+
return withEnvOverlay(salvageCredentials(envDefaultConfig(), parsed));
|
|
284
302
|
}
|
|
285
303
|
if (parsed.kind === "missing") return withEnvOverlay(envDefaultConfig());
|
|
286
304
|
return withEnvOverlay(structuredClone(parsed.config));
|
|
@@ -307,9 +325,15 @@ export async function saveGlobalConfig(config: Config): Promise<void> {
|
|
|
307
325
|
* JEO_*_MODEL role tiers, OLLAMA_HOST/OPENAI_BASE_URL) are never baked into
|
|
308
326
|
* ~/.jeo/config.json by an unrelated `/agents`/`/roles`/`/model save`. */
|
|
309
327
|
export async function readRawGlobalConfig(): Promise<Config> {
|
|
310
|
-
const clean: Config = { providers: {}, defaultModel: DEFAULT_MODEL, thinkingLevel: "medium" };
|
|
311
328
|
const parsed = await readParsedConfigFile();
|
|
312
|
-
|
|
329
|
+
if (parsed.kind === "ok") return structuredClone(parsed.config);
|
|
330
|
+
// CREDENTIAL SAFETY: when the on-disk config is JSON-valid but fails schema validation
|
|
331
|
+
// on some unrelated field, salvage the `oauth` + `providers` blocks. Without this, the
|
|
332
|
+
// next saveConfigPatch (e.g. an auto token refresh that patches ONE provider) bases on
|
|
333
|
+
// a clean config and silently wipes every OTHER stored credential — the "OAuth de-authed
|
|
334
|
+
// after a session" bug. The invalid scalar field is dropped (reset to default).
|
|
335
|
+
const clean: Config = { providers: {}, defaultModel: DEFAULT_MODEL, thinkingLevel: "medium" };
|
|
336
|
+
return salvageCredentials(clean, parsed);
|
|
313
337
|
}
|
|
314
338
|
|
|
315
339
|
/** Merge a patch onto the RAW on-disk config and persist. The `build` callback
|
|
@@ -326,17 +350,65 @@ export function getLocalJeoDir(cwd: string = process.cwd()): string {
|
|
|
326
350
|
return path.join(cwd, ".jeo");
|
|
327
351
|
}
|
|
328
352
|
|
|
353
|
+
// mtime+size-validated cache for the small per-skill workflow-state JSON. readWorkflowState
|
|
354
|
+
// runs repeatedly (the mutation guard reads before every mutating tool), so re-reading +
|
|
355
|
+
// re-parsing each call is wasteful. CROSS-PROCESS-SAFE: a write by another process bumps
|
|
356
|
+
// mtime/size → forces a fresh read, so the security-sensitive guard never serves a stale
|
|
357
|
+
// lock. Same-process write/clear update or drop the entry directly. LRU-capped.
|
|
358
|
+
const workflowStateCache = new Map<string, { mtimeMs: number; size: number; value: WorkflowState | null }>();
|
|
359
|
+
const WORKFLOW_STATE_CACHE_CAP = 16;
|
|
360
|
+
|
|
361
|
+
function cacheWorkflowState(statePath: string, mtimeMs: number, size: number, value: WorkflowState | null): void {
|
|
362
|
+
workflowStateCache.delete(statePath);
|
|
363
|
+
if (workflowStateCache.size >= WORKFLOW_STATE_CACHE_CAP) {
|
|
364
|
+
const oldest = workflowStateCache.keys().next().value;
|
|
365
|
+
if (oldest !== undefined) workflowStateCache.delete(oldest);
|
|
366
|
+
}
|
|
367
|
+
workflowStateCache.set(statePath, { mtimeMs, size, value });
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async function loadWorkflowStateFile(statePath: string, strict: boolean): Promise<WorkflowState | null> {
|
|
371
|
+
let st: { mtimeMs: number; size: number };
|
|
372
|
+
try {
|
|
373
|
+
st = await fs.stat(statePath);
|
|
374
|
+
} catch (err) {
|
|
375
|
+
workflowStateCache.delete(statePath);
|
|
376
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
377
|
+
if (strict) throw err;
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
const hit = workflowStateCache.get(statePath);
|
|
381
|
+
if (hit && hit.mtimeMs === st.mtimeMs && hit.size === st.size) {
|
|
382
|
+
workflowStateCache.delete(statePath); // LRU refresh
|
|
383
|
+
workflowStateCache.set(statePath, hit);
|
|
384
|
+
return hit.value;
|
|
385
|
+
}
|
|
386
|
+
let data: string;
|
|
387
|
+
try {
|
|
388
|
+
data = await fs.readFile(statePath, "utf-8");
|
|
389
|
+
} catch (err) {
|
|
390
|
+
workflowStateCache.delete(statePath);
|
|
391
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
392
|
+
if (strict) throw err;
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
try {
|
|
396
|
+
const parsed = JSON.parse(data) as WorkflowState;
|
|
397
|
+
cacheWorkflowState(statePath, st.mtimeMs, st.size, parsed);
|
|
398
|
+
return parsed;
|
|
399
|
+
} catch {
|
|
400
|
+
workflowStateCache.delete(statePath);
|
|
401
|
+
if (strict) throw new Error(`workflow state ${statePath} is corrupt (invalid JSON)`);
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
329
406
|
export async function readWorkflowState(
|
|
330
407
|
skill: "deep-interview" | "ralplan" | "team" | "ultragoal",
|
|
331
408
|
cwd: string = process.cwd()
|
|
332
409
|
): Promise<WorkflowState | null> {
|
|
333
410
|
const statePath = path.join(getLocalJeoDir(cwd), "state", `${skill}-state.json`);
|
|
334
|
-
|
|
335
|
-
const data = await fs.readFile(statePath, "utf-8");
|
|
336
|
-
return JSON.parse(data) as WorkflowState;
|
|
337
|
-
} catch {
|
|
338
|
-
return null;
|
|
339
|
-
}
|
|
411
|
+
return loadWorkflowStateFile(statePath, false);
|
|
340
412
|
}
|
|
341
413
|
|
|
342
414
|
/**
|
|
@@ -349,18 +421,7 @@ export async function readWorkflowStateStrict(
|
|
|
349
421
|
cwd: string = process.cwd()
|
|
350
422
|
): Promise<WorkflowState | null> {
|
|
351
423
|
const statePath = path.join(getLocalJeoDir(cwd), "state", `${skill}-state.json`);
|
|
352
|
-
|
|
353
|
-
try {
|
|
354
|
-
data = await fs.readFile(statePath, "utf-8");
|
|
355
|
-
} catch (err) {
|
|
356
|
-
if ((err as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
357
|
-
throw err;
|
|
358
|
-
}
|
|
359
|
-
try {
|
|
360
|
-
return JSON.parse(data) as WorkflowState;
|
|
361
|
-
} catch {
|
|
362
|
-
throw new Error(`workflow state ${statePath} is corrupt (invalid JSON)`);
|
|
363
|
-
}
|
|
424
|
+
return loadWorkflowStateFile(statePath, true);
|
|
364
425
|
}
|
|
365
426
|
|
|
366
427
|
export async function writeWorkflowState(
|
|
@@ -382,6 +443,14 @@ export async function writeWorkflowState(
|
|
|
382
443
|
await fs.unlink(tmpPath).catch(() => {});
|
|
383
444
|
throw err;
|
|
384
445
|
}
|
|
446
|
+
// Cache the just-written state keyed on the new file fingerprint so the next read
|
|
447
|
+
// (often the mutation guard milliseconds later) is served from memory.
|
|
448
|
+
try {
|
|
449
|
+
const st = await fs.stat(statePath);
|
|
450
|
+
cacheWorkflowState(statePath, st.mtimeMs, st.size, state);
|
|
451
|
+
} catch {
|
|
452
|
+
workflowStateCache.delete(statePath);
|
|
453
|
+
}
|
|
385
454
|
return statePath;
|
|
386
455
|
}
|
|
387
456
|
|
|
@@ -393,6 +462,7 @@ export async function clearWorkflowState(
|
|
|
393
462
|
try {
|
|
394
463
|
await fs.unlink(statePath);
|
|
395
464
|
} catch {}
|
|
465
|
+
workflowStateCache.delete(statePath);
|
|
396
466
|
}
|
|
397
467
|
|
|
398
468
|
/**
|
|
@@ -524,6 +524,15 @@ export const DNA_CLAW_ART_GRAND_ASCII: string[] = [
|
|
|
524
524
|
" [ D N A . C L A W ] "
|
|
525
525
|
];
|
|
526
526
|
|
|
527
|
+
// Bounded memo of fully-rendered DNA Claw frames keyed by every input that affects
|
|
528
|
+
// output (grand/unicode/cols/color/colorLevel/phase/frame). The live HUD cycles a
|
|
529
|
+
// FIXED ~60-frame set (3 twists × 20 gradient phases) at ~120ms; without this each
|
|
530
|
+
// recurrence recomputed per-line animatedGradientText (ANSI gradient) from scratch.
|
|
531
|
+
// The memo makes the 2nd+ cycle O(1) lookups, cutting steady-state HUD CPU. LRU-capped.
|
|
532
|
+
const dnaClawMemo = new Map<string, string[]>();
|
|
533
|
+
const DNA_CLAW_MEMO_CAP = 256;
|
|
534
|
+
const EMPTY_DNA_FRAME: string[] = [];
|
|
535
|
+
|
|
527
536
|
export function renderDnaClaw(opts: {
|
|
528
537
|
cols?: number;
|
|
529
538
|
phase?: number;
|
|
@@ -537,6 +546,9 @@ export function renderDnaClaw(opts: {
|
|
|
537
546
|
* `phase` this animates the forge identity without any frame-count growth. */
|
|
538
547
|
frame?: number;
|
|
539
548
|
}): string[] {
|
|
549
|
+
const memoKey = `${opts.grand ? "g" : "c"}|${opts.unicode !== false ? 1 : 0}|${opts.cols ?? -1}|${opts.color !== false ? 1 : 0}|${opts.colorLevel ?? ColorLevel.TrueColor}|${opts.phase ?? 0}|${opts.frame ?? 0}`;
|
|
550
|
+
const memoHit = dnaClawMemo.get(memoKey);
|
|
551
|
+
if (memoHit) return memoHit;
|
|
540
552
|
const useUnicode = opts.unicode !== false;
|
|
541
553
|
let source: string[];
|
|
542
554
|
if (opts.grand) {
|
|
@@ -549,7 +561,8 @@ export function renderDnaClaw(opts: {
|
|
|
549
561
|
const width = Math.max(0, ...source.map(l => l.length));
|
|
550
562
|
|
|
551
563
|
if (opts.cols !== undefined && opts.cols < width) {
|
|
552
|
-
|
|
564
|
+
dnaClawMemo.set(memoKey, EMPTY_DNA_FRAME);
|
|
565
|
+
return EMPTY_DNA_FRAME;
|
|
553
566
|
}
|
|
554
567
|
|
|
555
568
|
const phase = opts.phase ?? 0;
|
|
@@ -557,13 +570,20 @@ export function renderDnaClaw(opts: {
|
|
|
557
570
|
const colorLevel = opts.colorLevel ?? ColorLevel.TrueColor;
|
|
558
571
|
const palette = DNA_FLOW_PALETTE;
|
|
559
572
|
|
|
560
|
-
|
|
573
|
+
const result = source.map((line, idx) => {
|
|
561
574
|
const padded = line.length < width ? line + " ".repeat(width - line.length) : line;
|
|
562
575
|
if (!useColor || colorLevel < ColorLevel.TrueColor) {
|
|
563
576
|
return padded;
|
|
564
577
|
}
|
|
565
578
|
return animatedGradientText(padded, palette, phase + idx * 0.07, { colorLevel });
|
|
566
579
|
});
|
|
580
|
+
|
|
581
|
+
if (dnaClawMemo.size >= DNA_CLAW_MEMO_CAP) {
|
|
582
|
+
const oldest = dnaClawMemo.keys().next().value;
|
|
583
|
+
if (oldest !== undefined) dnaClawMemo.delete(oldest);
|
|
584
|
+
}
|
|
585
|
+
dnaClawMemo.set(memoKey, result);
|
|
586
|
+
return result;
|
|
567
587
|
}
|
|
568
588
|
|
|
569
589
|
/** The DNA Claw identity palette (emerald → cyan → violet helix flow). Shared by
|