jeo-code 0.6.22 → 0.6.24
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/CHANGELOG.md +26 -0
- package/README.ja.md +6 -2
- package/README.ko.md +6 -2
- package/README.md +6 -2
- package/README.zh.md +6 -2
- package/package.json +1 -1
- package/src/agent/config-schema.ts +12 -0
- package/src/agent/session.ts +10 -3
- package/src/agent/state.ts +19 -14
- package/src/ai/index.ts +1 -0
- package/src/ai/model-catalog.ts +121 -1
- package/src/ai/model-discovery.ts +55 -3
- package/src/ai/model-manager.ts +43 -11
- package/src/ai/model-registry.ts +2 -0
- package/src/ai/provider-status.ts +45 -7
- package/src/ai/providers/anthropic-compatible.ts +27 -0
- package/src/ai/providers/anthropic.ts +3 -1
- package/src/ai/providers/antigravity.ts +31 -6
- package/src/ai/providers/gemini.ts +45 -4
- package/src/ai/providers/kimi.ts +18 -0
- package/src/ai/providers/lmstudio.ts +8 -0
- package/src/ai/providers/ollama.ts +17 -5
- package/src/ai/providers/openai-compatible-catalog.ts +83 -0
- package/src/ai/providers/openai-compatible.ts +34 -0
- package/src/ai/providers/openai-responses.ts +11 -0
- package/src/ai/providers/openai.ts +115 -7
- package/src/ai/providers/xai.ts +18 -0
- package/src/ai/register-providers.ts +18 -0
- package/src/ai/think-tags.ts +84 -0
- package/src/ai/types.ts +11 -1
- package/src/auth/flows/index.ts +3 -3
- package/src/auth/index.ts +4 -1
- package/src/auth/oauth.ts +3 -3
- package/src/auth/refresh.ts +5 -0
- package/src/auth/storage.ts +12 -1
- package/src/commands/auth.ts +21 -2
- package/src/commands/launch/flags.ts +5 -1
- package/src/commands/launch/input.ts +13 -0
- package/src/commands/launch.ts +307 -26
- package/src/commands/setup.ts +3 -2
- package/src/tui/app.ts +61 -41
- package/src/tui/components/ascii-art.ts +91 -124
- package/src/tui/components/autocomplete.ts +16 -0
- package/src/tui/components/forge.ts +1 -1
- package/src/tui/components/provider-picker.ts +162 -0
- package/src/tui/components/slash.ts +2 -2
- package/src/tui/components/transcript.ts +7 -0
- package/src/tui/components/welcome.ts +8 -8
- package/src/tui/components/width.ts +21 -0
package/src/tui/app.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { Spinner } from "./components/spinner";
|
|
|
16
16
|
import { ToolList } from "./components/tool-list";
|
|
17
17
|
import { StreamRegion } from "./components/stream";
|
|
18
18
|
import { renderFooter, type FooterData } from "./components/footer";
|
|
19
|
-
import {
|
|
19
|
+
import { renderForgeMark, forgeMarkHeight, forgeMarkFrameCount, forgeBeat, FORGE_FLOW_PALETTE } from "./components/ascii-art";
|
|
20
20
|
import { evolutionTrack, createStageProgress, type StageProgress, transitionMessage } from "./components/evolution";
|
|
21
21
|
import type { TaskSubEvent } from "../agent/task-tool";
|
|
22
22
|
import { supportsUnicode } from "./components/capability";
|
|
@@ -30,7 +30,7 @@ import { costForUsage } from "../ai/pricing";
|
|
|
30
30
|
import { renderMarkdownTables } from "./components/markdown-table";
|
|
31
31
|
|
|
32
32
|
import { stripMarkdown, renderMarkdownAnsi } from "./components/markdown-text";
|
|
33
|
-
import { visibleWidth, wrapTextWithAnsi, truncateToWidth, sanitizeForFrame } from "./components/width";
|
|
33
|
+
import { visibleWidth, wrapTextWithAnsi, truncateToWidth, sanitizeForFrame, lastValueCache } from "./components/width";
|
|
34
34
|
import { categoryBadge } from "./components/category-index";
|
|
35
35
|
import { formatStepTimeline, stepsFromTools, formatStepHeader, formatStepTimelineCompact, formatDuration as formatToolMs, type StepState } from "./components/step-timeline";
|
|
36
36
|
import { formatHintBar } from "./components/hints";
|
|
@@ -86,10 +86,9 @@ function extractStreamingReasoning(buf: string): string {
|
|
|
86
86
|
catch { return m[1].replace(/\\\\/g, "\\").replace(/\\n/g, " ").replace(/\\"/g, '"'); }
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* across providers/models instead of staying silent for some of them. */
|
|
89
|
+
/** Derive a HUD STATUS (never content) from the forming stream: `"tool":"x"` → "calling
|
|
90
|
+
* x…", bare JSON/fence → "forming the next tool call…", prose reply → "writing the
|
|
91
|
+
* reply…". The reply/reasoning TEXT itself belongs in the Thinking block, not the HUD. */
|
|
93
92
|
function extractStreamingActivity(buf: string): string {
|
|
94
93
|
const head = buf.length > 512 ? buf.slice(0, 512) : buf;
|
|
95
94
|
const tool = head.match(/"tool"\s*:\s*"([^"]+)"/)?.[1];
|
|
@@ -97,7 +96,7 @@ function extractStreamingActivity(buf: string): string {
|
|
|
97
96
|
const t = head.trim();
|
|
98
97
|
if (!t) return "";
|
|
99
98
|
if (t.startsWith("{") || t.startsWith("```")) return "forming the next tool call…";
|
|
100
|
-
return
|
|
99
|
+
return "writing the reply…";
|
|
101
100
|
}
|
|
102
101
|
|
|
103
102
|
/** Bound the input to a per-frame wrap to a fixed trailing window. The live thinking
|
|
@@ -265,6 +264,10 @@ export class LaunchTui {
|
|
|
265
264
|
private cachedFrame = -1;
|
|
266
265
|
private cachedArt: string[] = [];
|
|
267
266
|
private cachedTrack = "";
|
|
267
|
+
// Per-label (Thinking / Output) single-slot wrap memo: the 120ms spinner tick
|
|
268
|
+
// re-renders the frame ~8×/s, but the streamed text changes only on a new delta —
|
|
269
|
+
// so the live block reuses its prior wrap instead of re-segmenting the 16KB tail.
|
|
270
|
+
private readonly liveBlockWrapCaches = new Map<string, (key: string, compute: () => string[]) => string[]>();
|
|
268
271
|
// Monotonic stage progress so evolution only ever moves forward this turn.
|
|
269
272
|
private readonly progress: StageProgress = createStageProgress();
|
|
270
273
|
// Terminal unicode capability, detected once (drives spinner/track glyph set).
|
|
@@ -393,25 +396,20 @@ export class LaunchTui {
|
|
|
393
396
|
this.draw();
|
|
394
397
|
},
|
|
395
398
|
onModelStream: textSoFar => {
|
|
396
|
-
//
|
|
397
|
-
//
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
// Draws are THROTTLED to one per 100ms: the old per-delta draw() rendered
|
|
401
|
-
// the full frame hundreds of times per response (a real chunk of jeo's
|
|
402
|
-
// per-step latency); the 120ms timer tick covers the gaps anyway.
|
|
399
|
+
// The model's JSON-protocol `reasoning` field → the live Thinking block (alongside
|
|
400
|
+
// native reasoning). The HUD status row gets ONLY a derived STATUS — never the raw
|
|
401
|
+
// reasoning/reply text, so a JSON-streaming model shows "forming the next tool
|
|
402
|
+
// call…", not its JSON content. Draws throttled to ≤1/100ms (timer covers gaps).
|
|
403
403
|
const r = extractStreamingReasoning(textSoFar);
|
|
404
404
|
let changed = false;
|
|
405
|
-
if (r) {
|
|
406
|
-
changed = r !== this.streamingReasoning;
|
|
405
|
+
if (r && r !== this.streamingReasoning) {
|
|
407
406
|
this.streamingReasoning = r;
|
|
408
|
-
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
}
|
|
407
|
+
changed = true;
|
|
408
|
+
}
|
|
409
|
+
const status = extractStreamingActivity(textSoFar);
|
|
410
|
+
if (status && status !== this.streamingActivity) {
|
|
411
|
+
this.streamingActivity = status;
|
|
412
|
+
changed = true;
|
|
415
413
|
}
|
|
416
414
|
if (changed && Date.now() - this.lastStreamDraw >= 100) {
|
|
417
415
|
this.lastStreamDraw = Date.now();
|
|
@@ -624,10 +622,21 @@ export class LaunchTui {
|
|
|
624
622
|
this.draw();
|
|
625
623
|
}
|
|
626
624
|
|
|
625
|
+
private livePromptHint: string[] = [];
|
|
626
|
+
/** Mid-turn command/skill preview lines shown above the live input box, so a
|
|
627
|
+
* /command or $skill typed WHILE a turn runs visibly reacts (idle-prompt parity). */
|
|
628
|
+
setLivePromptHint(lines: string[]): void {
|
|
629
|
+
if (this.finished) return;
|
|
630
|
+
const next = lines ?? [];
|
|
631
|
+
if (next.join("\n") === this.livePromptHint.join("\n")) return;
|
|
632
|
+
this.livePromptHint = next;
|
|
633
|
+
this.draw();
|
|
634
|
+
}
|
|
635
|
+
|
|
627
636
|
private renderLiveInputBox(cols: number): string[] {
|
|
628
637
|
const caret = this.unicode ? "▌" : "_";
|
|
629
638
|
const display = this.livePromptInput ? `${this.livePromptInput}${caret}` : "";
|
|
630
|
-
|
|
639
|
+
const box = renderInputBox(display, {
|
|
631
640
|
cols: Math.max(24, cols),
|
|
632
641
|
color: this.theme.color,
|
|
633
642
|
unicode: this.unicode,
|
|
@@ -636,6 +645,9 @@ export class LaunchTui {
|
|
|
636
645
|
placeholder: "Type your next message...",
|
|
637
646
|
maxBodyRows: 2,
|
|
638
647
|
});
|
|
648
|
+
if (this.livePromptHint.length === 0) return box;
|
|
649
|
+
const dim = this.theme.color ? chalk.dim : (s: string) => s;
|
|
650
|
+
return [...this.livePromptHint.map(l => dim(l)), ...box];
|
|
639
651
|
}
|
|
640
652
|
|
|
641
653
|
/** Render a `user`-labeled query card (orange "user" header over a filled box).
|
|
@@ -884,6 +896,7 @@ export class LaunchTui {
|
|
|
884
896
|
this.turnUsage = null;
|
|
885
897
|
this.lastLedgerKind = null; // fresh turn: no leading spacer before the first ledger line
|
|
886
898
|
this.livePromptInput = ""; // fresh turn: no next-prompt draft yet
|
|
899
|
+
this.livePromptHint = []; // fresh turn: no mid-turn command preview yet
|
|
887
900
|
this.subagentLive = null; // fresh turn: no nested subagent in flight
|
|
888
901
|
this.activityLog.length = 0; // per-turn ring: timestamps are turn-relative
|
|
889
902
|
this.spinner.updateStep(0, this.footer.maxSteps);
|
|
@@ -1173,11 +1186,11 @@ export class LaunchTui {
|
|
|
1173
1186
|
index: i + 1,
|
|
1174
1187
|
color: this.theme.color,
|
|
1175
1188
|
dim,
|
|
1176
|
-
//
|
|
1177
|
-
// the card border and the
|
|
1189
|
+
// Forge-flow identity on LIVE cards only: the flowing neon gradient rides
|
|
1190
|
+
// the card border and the prompt beat marks the title. Flushed/final cards
|
|
1178
1191
|
// stay static. Suppressed while `dim` (in-flight shading takes precedence).
|
|
1179
1192
|
...(anim && !dim
|
|
1180
|
-
? { flow: { palette:
|
|
1193
|
+
? { flow: { palette: FORGE_FLOW_PALETTE, phase: anim.phase, colorLevel: anim.colorLevel }, titleMark: anim.beat }
|
|
1181
1194
|
: {}),
|
|
1182
1195
|
}));
|
|
1183
1196
|
}
|
|
@@ -1197,10 +1210,17 @@ export class LaunchTui {
|
|
|
1197
1210
|
const dim = this.theme.color ? chalk.dim : (s: string) => s;
|
|
1198
1211
|
if (!text.trim()) return [];
|
|
1199
1212
|
const wrapW = Math.max(8, cols - 2);
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1213
|
+
// Memoize the wrap: only the spinner/clock change on most 120ms ticks, so re-wrapping
|
|
1214
|
+
// this (up to 16KB) tail every frame just re-segments graphemes for no visible change.
|
|
1215
|
+
// Per-label slot (Thinking / Output) keyed by wrap width + text — a real delta misses
|
|
1216
|
+
// once and recomputes; an idle tick hits the cache. `rows` only gates the post-slice.
|
|
1217
|
+
let cache = this.liveBlockWrapCaches.get(label);
|
|
1218
|
+
if (!cache) { cache = lastValueCache<string[]>(); this.liveBlockWrapCaches.set(label, cache); }
|
|
1219
|
+
const wrapped = cache(`${wrapW}\u0000${text}`, () =>
|
|
1220
|
+
tailForWrap(text)
|
|
1221
|
+
.split("\n")
|
|
1222
|
+
.flatMap(l => wrapTextWithAnsi(l, wrapW))
|
|
1223
|
+
.filter(l => l.length > 0));
|
|
1204
1224
|
if (wrapped.length === 0) return [];
|
|
1205
1225
|
const cap = Math.max(3, Math.min(ceiling, Math.floor(rows * 0.3)));
|
|
1206
1226
|
const out: string[] = [sectionLabel(label, Math.max(8, cols), { color: this.theme.color, unicode: this.unicode })];
|
|
@@ -1312,10 +1332,10 @@ export class LaunchTui {
|
|
|
1312
1332
|
const dim = this.theme.color ? chalk.dim : (s: string) => s;
|
|
1313
1333
|
const colorLevel = detectColorLevel(process.env, isTTY());
|
|
1314
1334
|
// One quantized animation clock for the whole frame: gradient phase cycles 20
|
|
1315
|
-
// steps, the
|
|
1335
|
+
// steps, the prompt beat advances every 3 ticks. Quantization keeps repaints
|
|
1316
1336
|
// coherent (status field + forge border move together) and bounds per-tick work.
|
|
1317
1337
|
const phase = (this.tickCount * 0.05) % 1;
|
|
1318
|
-
const beat =
|
|
1338
|
+
const beat = forgeBeat(Math.trunc(this.tickCount / 3), this.unicode);
|
|
1319
1339
|
|
|
1320
1340
|
// Assemble the bottom-pinned tail FIRST (status line → todos → hud → model bar):
|
|
1321
1341
|
// it is the live heartbeat and must always be visible; the in-flight card gets
|
|
@@ -1423,9 +1443,9 @@ export class LaunchTui {
|
|
|
1423
1443
|
const innerWidth = !fit ? cols : this.inline ? cols - 1 : cols - 4;
|
|
1424
1444
|
|
|
1425
1445
|
// Resolve the current (monotonic) stage for the track; announce a transition
|
|
1426
|
-
// once when it first advances. The header art is the
|
|
1427
|
-
//
|
|
1428
|
-
//
|
|
1446
|
+
// once when it first advances. The header art is the jeo forge mark — a
|
|
1447
|
+
// prompt-cursor blink combined with the flowing gradient phase. Both are
|
|
1448
|
+
// quantized (2 blink frames × 20 gradient phases), so the cache
|
|
1429
1449
|
// recomputes at most once per changed tick and stays a single slot (O(1)).
|
|
1430
1450
|
const stepNow = this.footer.step || 0;
|
|
1431
1451
|
const idx = this.progress.observe(stepNow, this.footer.maxSteps ?? DEFAULT_MAX_STEPS);
|
|
@@ -1435,16 +1455,16 @@ export class LaunchTui {
|
|
|
1435
1455
|
this.appendLedger(`${arrow} ${transitionMessage(idx)}\n`, "notice");
|
|
1436
1456
|
}
|
|
1437
1457
|
const showArt = fit && !this.inline && rows >= 18 && cols >= 40;
|
|
1438
|
-
// One int key folds both animation axes:
|
|
1458
|
+
// One int key folds both animation axes: blink frame advances every 3 ticks,
|
|
1439
1459
|
// gradient phase cycles 20 quantized steps (tickCount*0.05 % 1).
|
|
1440
|
-
const twist = isThinking ? Math.trunc(this.tickCount / 3) %
|
|
1460
|
+
const twist = isThinking ? Math.trunc(this.tickCount / 3) % forgeMarkFrameCount() : 0;
|
|
1441
1461
|
const qPhase = isThinking ? this.tickCount % 20 : 0;
|
|
1442
1462
|
const effFrame = twist * 100 + qPhase;
|
|
1443
1463
|
if (showArt && (idx !== this.cachedStageIndex || cols !== this.cachedCols || effFrame !== this.cachedFrame)) {
|
|
1444
|
-
// Commit the cache keys only AFTER the render succeeds: if
|
|
1464
|
+
// Commit the cache keys only AFTER the render succeeds: if renderForgeMark ever
|
|
1445
1465
|
// throws (resize race, bad gradient level), pre-committed keys would mark the
|
|
1446
1466
|
// STALE art as current and freeze the header at an old frame forever.
|
|
1447
|
-
const art =
|
|
1467
|
+
const art = renderForgeMark({
|
|
1448
1468
|
cols: innerWidth,
|
|
1449
1469
|
phase: qPhase * 0.05,
|
|
1450
1470
|
frame: twist,
|
|
@@ -1459,7 +1479,7 @@ export class LaunchTui {
|
|
|
1459
1479
|
this.cachedCols = cols;
|
|
1460
1480
|
this.cachedFrame = effFrame;
|
|
1461
1481
|
}
|
|
1462
|
-
const artLinesCount = showArt ?
|
|
1482
|
+
const artLinesCount = showArt ? forgeMarkHeight() : 0;
|
|
1463
1483
|
const trackCount = showArt ? 1 : 0;
|
|
1464
1484
|
const headerHeight = artLinesCount + trackCount + (showArt ? 1 : 0);
|
|
1465
1485
|
|
|
@@ -412,128 +412,90 @@ export async function animateFrames(stage: AsciiStage, opts: AnimateFramesOption
|
|
|
412
412
|
}
|
|
413
413
|
return total;
|
|
414
414
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
415
|
+
/** The compact jeo forge mark: a clean, wordless pictograph of the mascot neon
|
|
416
|
+
* crayfish. Read top→bottom — antennae arc outward (╲ ╱) flanking the asymmetric
|
|
417
|
+
* sunglasses face (◆ blue lens / ◇ pink lens on a ┃ nose-bridge); the front claws
|
|
418
|
+
* (❮ ❯) on short arms (━┫ ┣━) hugging the rounded carapace (◉◉◉); tucked legs
|
|
419
|
+
* (╲ ┃ ╱); the tail fan tipped by the blinking telson (◀▮▶). The mark is purely
|
|
420
|
+
* symbolic — NO embedded lettering (the brand wordmark lives in the welcome
|
|
421
|
+
* header, not under the emblem). Width-1 glyphs only (box drawing + geometrics)
|
|
422
|
+
* so padding/centering math stays exact. Frame 0 is the static symbol. */
|
|
423
|
+
export const FORGE_MARK_ART: string[] = [
|
|
424
|
+
" ╲ ◆ ┃ ◇ ╱ ",
|
|
425
|
+
" ❮━┫ ◉◉◉ ┣━❯ ",
|
|
426
|
+
" ╲ ┃ ╱ ",
|
|
427
|
+
" ◀▮▶ "
|
|
424
428
|
];
|
|
425
429
|
|
|
426
|
-
export const
|
|
427
|
-
"
|
|
428
|
-
"
|
|
429
|
-
" |
|
|
430
|
-
"
|
|
431
|
-
" \\{ / X \\ }/ ",
|
|
432
|
-
" \\==o o==/ ",
|
|
433
|
-
" | | ",
|
|
434
|
-
" [ DNA Claw ] "
|
|
430
|
+
export const FORGE_MARK_ART_ASCII: string[] = [
|
|
431
|
+
" \\ * | o / ",
|
|
432
|
+
" <=[ @@@ ]=> ",
|
|
433
|
+
" \\ | / ",
|
|
434
|
+
" <#> "
|
|
435
435
|
];
|
|
436
436
|
|
|
437
|
-
/**
|
|
438
|
-
* fixed while the
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
|
|
442
|
-
|
|
437
|
+
/** Blink animation frames for the compact crayfish forge mark: the antennae,
|
|
438
|
+
* carapace and legs stay fixed while the telson cursor blinks (▮ → ▯) and the
|
|
439
|
+
* asymmetric sunglass lenses swap accent (◆ ◇ → ◇ ◆), so the crayfish "winks".
|
|
440
|
+
* Frame 0 === FORGE_MARK_ART, so a frameless render is byte-identical to the
|
|
441
|
+
* static symbol. All lines share the same width (21) and width-1 glyphs. */
|
|
442
|
+
export const FORGE_MARK_FRAMES: string[][] = [
|
|
443
|
+
FORGE_MARK_ART,
|
|
443
444
|
[
|
|
444
|
-
"
|
|
445
|
-
"
|
|
446
|
-
"
|
|
447
|
-
"
|
|
448
|
-
" ╰╮ ╳ ╳ ╭╯ ",
|
|
449
|
-
" ╚══○ ○══╝ ",
|
|
450
|
-
" ║ ║ ",
|
|
451
|
-
" [ DNA Claw ] "
|
|
452
|
-
],
|
|
453
|
-
[
|
|
454
|
-
" ╭╯ ◆ ◆ ╰╮ ",
|
|
455
|
-
" ╭╯ ╱╲ ╱╲ ╰╮ ",
|
|
456
|
-
" ║ ╳ ╳ ║ ",
|
|
457
|
-
" ╰╮ ╲ ╳ ╱ ╭╯ ",
|
|
458
|
-
" ╰╮ ╳ ╳ ╭╯ ",
|
|
459
|
-
" ╚══○ ○══╝ ",
|
|
460
|
-
" ║ ║ ",
|
|
461
|
-
" [ DNA Claw ] "
|
|
445
|
+
" ╲ ◇ ┃ ◆ ╱ ",
|
|
446
|
+
" ❮━┫ ◉◉◉ ┣━❯ ",
|
|
447
|
+
" ╲ ┃ ╱ ",
|
|
448
|
+
" ◀▯▶ "
|
|
462
449
|
]
|
|
463
450
|
];
|
|
464
451
|
|
|
465
|
-
export const
|
|
466
|
-
|
|
467
|
-
[
|
|
468
|
-
" /{ * * }\\ ",
|
|
469
|
-
" /{ \\ / \\ / }\\ ",
|
|
470
|
-
" | X X | ",
|
|
471
|
-
" \\{ / X \\ }/ ",
|
|
472
|
-
" \\{ X X }/ ",
|
|
473
|
-
" \\==o o==/ ",
|
|
474
|
-
" | | ",
|
|
475
|
-
" [ DNA Claw ] "
|
|
476
|
-
],
|
|
452
|
+
export const FORGE_MARK_FRAMES_ASCII: string[][] = [
|
|
453
|
+
FORGE_MARK_ART_ASCII,
|
|
477
454
|
[
|
|
478
|
-
"
|
|
479
|
-
"
|
|
480
|
-
" |
|
|
481
|
-
"
|
|
482
|
-
" \\{ X X }/ ",
|
|
483
|
-
" \\==o o==/ ",
|
|
484
|
-
" | | ",
|
|
485
|
-
" [ DNA Claw ] "
|
|
455
|
+
" \\ o | * / ",
|
|
456
|
+
" <=[ @@@ ]=> ",
|
|
457
|
+
" \\ | / ",
|
|
458
|
+
" <_> "
|
|
486
459
|
]
|
|
487
460
|
];
|
|
488
461
|
|
|
489
|
-
/** Number of
|
|
490
|
-
export function
|
|
491
|
-
return
|
|
462
|
+
/** Number of blink frames in the compact forge-mark animation cycle. */
|
|
463
|
+
export function forgeMarkFrameCount(): number {
|
|
464
|
+
return FORGE_MARK_FRAMES.length;
|
|
492
465
|
}
|
|
493
466
|
|
|
494
|
-
/** Grand hero variant for the welcome forge box (gjc-style spacious banner):
|
|
495
|
-
*
|
|
496
|
-
*
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
"
|
|
503
|
-
"
|
|
504
|
-
"
|
|
505
|
-
"
|
|
506
|
-
" ╰──╮ ║ ╲╳╳╱ ║ ╭──╯ ",
|
|
507
|
-
" ╰════○ ╳╳ ○════╯ ",
|
|
508
|
-
" ║ ╱╳╳╲ ║ ",
|
|
509
|
-
" [ D N A · C L A W ] "
|
|
467
|
+
/** Grand hero variant for the welcome forge box (gjc-style spacious banner): the
|
|
468
|
+
* same mascot crayfish rendered large and wordless — antennae (╲ ╱) flanking the
|
|
469
|
+
* asymmetric ◆/◇ sunglasses face on a ┃ nose-bridge, the big front pincers
|
|
470
|
+
* (❮━━┫ ┣━━❯) hugging the segmented carapace (◉ ◉ ◉), tucked legs, and the broad
|
|
471
|
+
* tail fan tipped by the telson (◀──▮──▶). Purely symbolic — NO embedded
|
|
472
|
+
* lettering or caption. Width-1 glyphs only so padding/centering math stays
|
|
473
|
+
* exact. */
|
|
474
|
+
export const FORGE_MARK_ART_GRAND: string[] = [
|
|
475
|
+
" ╲ ◆ ┃ ◇ ╱ ",
|
|
476
|
+
" ❮━━┫ ◉ ◉ ◉ ┣━━❯ ",
|
|
477
|
+
" ╲ ╲ ┃ ╱ ╱ ",
|
|
478
|
+
" ◀──▮──▶ "
|
|
510
479
|
];
|
|
511
480
|
|
|
512
|
-
export const
|
|
513
|
-
"
|
|
514
|
-
"
|
|
515
|
-
"
|
|
516
|
-
"
|
|
517
|
-
" | | XX | | ",
|
|
518
|
-
" | | /XX\\ | | ",
|
|
519
|
-
" \\, | // \\\\ | ,/ ",
|
|
520
|
-
" \\, | \\\\ // | ,/ ",
|
|
521
|
-
" \\--, | \\XX/ | ,--/ ",
|
|
522
|
-
" \\====o XX o====/ ",
|
|
523
|
-
" | /XX\\ | ",
|
|
524
|
-
" [ D N A . C L A W ] "
|
|
481
|
+
export const FORGE_MARK_ART_GRAND_ASCII: string[] = [
|
|
482
|
+
" \\ * | o / ",
|
|
483
|
+
" <==[ O O O ]==> ",
|
|
484
|
+
" \\ \\ | / / ",
|
|
485
|
+
" <--#--> "
|
|
525
486
|
];
|
|
526
487
|
|
|
527
|
-
|
|
488
|
+
|
|
489
|
+
// Bounded memo of fully-rendered forge-mark frames keyed by every input that affects
|
|
528
490
|
// output (grand/unicode/cols/color/colorLevel/phase/frame). The live HUD cycles a
|
|
529
|
-
// FIXED
|
|
530
|
-
//
|
|
531
|
-
//
|
|
532
|
-
const
|
|
533
|
-
const
|
|
534
|
-
const
|
|
491
|
+
// FIXED frame set (blink × gradient phases) at ~120ms; without this each recurrence
|
|
492
|
+
// recomputed per-line animatedGradientText (ANSI gradient) from scratch. The memo
|
|
493
|
+
// makes the 2nd+ cycle O(1) lookups, cutting steady-state HUD CPU. LRU-capped.
|
|
494
|
+
const forgeMarkMemo = new Map<string, string[]>();
|
|
495
|
+
const FORGE_MARK_MEMO_CAP = 256;
|
|
496
|
+
const EMPTY_FORGE_FRAME: string[] = [];
|
|
535
497
|
|
|
536
|
-
export function
|
|
498
|
+
export function renderForgeMark(opts: {
|
|
537
499
|
cols?: number;
|
|
538
500
|
phase?: number;
|
|
539
501
|
unicode?: boolean;
|
|
@@ -541,34 +503,35 @@ export function renderDnaClaw(opts: {
|
|
|
541
503
|
colorLevel?: ColorLevel;
|
|
542
504
|
/** Grand hero variant (welcome forge box); default is the compact in-turn symbol. */
|
|
543
505
|
grand?: boolean;
|
|
544
|
-
/**
|
|
545
|
-
* while the
|
|
546
|
-
* `phase` this animates the forge identity without any
|
|
506
|
+
/** Blink-animation frame (compact symbol only; wraps). The cursor blinks and the
|
|
507
|
+
* status lamps swap while the window silhouette stays fixed — combined with the
|
|
508
|
+
* flowing gradient `phase` this animates the forge identity without any
|
|
509
|
+
* frame-count growth. */
|
|
547
510
|
frame?: number;
|
|
548
511
|
}): string[] {
|
|
549
512
|
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 =
|
|
513
|
+
const memoHit = forgeMarkMemo.get(memoKey);
|
|
551
514
|
if (memoHit) return memoHit;
|
|
552
515
|
const useUnicode = opts.unicode !== false;
|
|
553
516
|
let source: string[];
|
|
554
517
|
if (opts.grand) {
|
|
555
|
-
source = useUnicode ?
|
|
518
|
+
source = useUnicode ? FORGE_MARK_ART_GRAND : FORGE_MARK_ART_GRAND_ASCII;
|
|
556
519
|
} else {
|
|
557
|
-
const frames = useUnicode ?
|
|
520
|
+
const frames = useUnicode ? FORGE_MARK_FRAMES : FORGE_MARK_FRAMES_ASCII;
|
|
558
521
|
const f = Math.abs(Math.trunc(opts.frame ?? 0)) % frames.length;
|
|
559
522
|
source = frames[f]!;
|
|
560
523
|
}
|
|
561
524
|
const width = Math.max(0, ...source.map(l => l.length));
|
|
562
525
|
|
|
563
526
|
if (opts.cols !== undefined && opts.cols < width) {
|
|
564
|
-
|
|
565
|
-
return
|
|
527
|
+
forgeMarkMemo.set(memoKey, EMPTY_FORGE_FRAME);
|
|
528
|
+
return EMPTY_FORGE_FRAME;
|
|
566
529
|
}
|
|
567
530
|
|
|
568
531
|
const phase = opts.phase ?? 0;
|
|
569
532
|
const useColor = opts.color !== false;
|
|
570
533
|
const colorLevel = opts.colorLevel ?? ColorLevel.TrueColor;
|
|
571
|
-
const palette =
|
|
534
|
+
const palette = FORGE_FLOW_PALETTE;
|
|
572
535
|
|
|
573
536
|
const result = source.map((line, idx) => {
|
|
574
537
|
const padded = line.length < width ? line + " ".repeat(width - line.length) : line;
|
|
@@ -578,25 +541,29 @@ export function renderDnaClaw(opts: {
|
|
|
578
541
|
return animatedGradientText(padded, palette, phase + idx * 0.07, { colorLevel });
|
|
579
542
|
});
|
|
580
543
|
|
|
581
|
-
if (
|
|
582
|
-
const oldest =
|
|
583
|
-
if (oldest !== undefined)
|
|
544
|
+
if (forgeMarkMemo.size >= FORGE_MARK_MEMO_CAP) {
|
|
545
|
+
const oldest = forgeMarkMemo.keys().next().value;
|
|
546
|
+
if (oldest !== undefined) forgeMarkMemo.delete(oldest);
|
|
584
547
|
}
|
|
585
|
-
|
|
548
|
+
forgeMarkMemo.set(memoKey, result);
|
|
586
549
|
return result;
|
|
587
550
|
}
|
|
588
551
|
|
|
589
|
-
/** The
|
|
590
|
-
* the
|
|
591
|
-
|
|
552
|
+
/** The jeo identity palette — the mascot crayfish's synthwave neon read straight
|
|
553
|
+
* off the character: blue antennae glow → violet carapace → pink claw tips.
|
|
554
|
+
* Shared by the forge mark and the forge-card border flow so the whole brand
|
|
555
|
+
* glows in the crayfish-wizard's signature blue→violet→pink shell sheen. */
|
|
556
|
+
export const FORGE_FLOW_PALETTE: readonly string[] = ["#48dbfb", "#8e44ad", "#f368e0"];
|
|
592
557
|
|
|
593
|
-
/** Width-1
|
|
594
|
-
*
|
|
595
|
-
|
|
596
|
-
|
|
558
|
+
/** Width-1 forge title-mark glyph cycling the mascot's `jeo>` terminal prompt:
|
|
559
|
+
* a prompt caret then a blinking block cursor (filled → hollow), echoing the
|
|
560
|
+
* glowing terminal screen the character holds. Used as the live forge-card
|
|
561
|
+
* title mark, so an in-flight card reads as a live coding-agent prompt. */
|
|
562
|
+
export function forgeBeat(frame: number, unicode = true): string {
|
|
563
|
+
const beats = unicode ? ["❯", "▮", "▯"] : [">", "#", "_"];
|
|
597
564
|
return beats[Math.abs(Math.trunc(frame)) % beats.length]!;
|
|
598
565
|
}
|
|
599
566
|
|
|
600
|
-
export function
|
|
601
|
-
return
|
|
567
|
+
export function forgeMarkHeight(): number {
|
|
568
|
+
return FORGE_MARK_ART.length;
|
|
602
569
|
}
|
|
@@ -50,6 +50,7 @@ export interface CompletionResult {
|
|
|
50
50
|
|
|
51
51
|
const PREVIEW_LABEL: Record<string, string> = {
|
|
52
52
|
command: "Commands",
|
|
53
|
+
skill: "Skills",
|
|
53
54
|
model: "Models",
|
|
54
55
|
provider: "Providers",
|
|
55
56
|
role: "Subagent roles",
|
|
@@ -219,6 +220,21 @@ export function formatCompletionPreview(line: string, ctx: CompletionContext, ma
|
|
|
219
220
|
return lines;
|
|
220
221
|
}
|
|
221
222
|
|
|
223
|
+
/** Compact mid-turn command/skill preview. Like formatCompletionPreview but ALSO
|
|
224
|
+
* surfaces command-name and $skill-name matches (the kinds the argument-only preview
|
|
225
|
+
* skips), so a /command or $skill typed WHILE a turn runs visibly reacts. */
|
|
226
|
+
export function formatMidTurnHint(line: string, ctx: CompletionContext, max = 5): string[] {
|
|
227
|
+
if (max <= 0) return [];
|
|
228
|
+
const result = complete(line, ctx);
|
|
229
|
+
if (result.completions.length === 0) return [];
|
|
230
|
+
const label = PREVIEW_LABEL[result.kind] ?? "Matches";
|
|
231
|
+
const shown = result.completions.slice(0, max);
|
|
232
|
+
const hidden = result.completions.length - shown.length;
|
|
233
|
+
const lines = [`${label}:`, ...shown.map(c => ` ${c}`)];
|
|
234
|
+
if (hidden > 0) lines.push(` …(+${hidden} more)`);
|
|
235
|
+
return lines;
|
|
236
|
+
}
|
|
237
|
+
|
|
222
238
|
/** Longest common prefix of a list (for tab "fill to ambiguity"). */
|
|
223
239
|
export function commonPrefix(items: string[]): string {
|
|
224
240
|
if (items.length === 0) return "";
|
|
@@ -27,7 +27,7 @@ export interface ForgeBoxOptions {
|
|
|
27
27
|
* `phase` per tick, so nothing is retained between frames. Below TrueColor
|
|
28
28
|
* (`colorLevel < 3`) this degrades to the static `paint`/`paintShadow`. */
|
|
29
29
|
flow?: { palette: readonly string[]; phase: number; colorLevel: number };
|
|
30
|
-
/** Width-1 mark prepended to the border title (e.g. the
|
|
30
|
+
/** Width-1 mark prepended to the border title (e.g. the jeo prompt beat glyph). */
|
|
31
31
|
titleMark?: string;
|
|
32
32
|
/** Themed +/- painters for `language: "patch"` cards (edit diffs): applied to
|
|
33
33
|
* the FULL padded row so added/removed lines read as background-tinted
|