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.
Files changed (49) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.ja.md +6 -2
  3. package/README.ko.md +6 -2
  4. package/README.md +6 -2
  5. package/README.zh.md +6 -2
  6. package/package.json +1 -1
  7. package/src/agent/config-schema.ts +12 -0
  8. package/src/agent/session.ts +10 -3
  9. package/src/agent/state.ts +19 -14
  10. package/src/ai/index.ts +1 -0
  11. package/src/ai/model-catalog.ts +121 -1
  12. package/src/ai/model-discovery.ts +55 -3
  13. package/src/ai/model-manager.ts +43 -11
  14. package/src/ai/model-registry.ts +2 -0
  15. package/src/ai/provider-status.ts +45 -7
  16. package/src/ai/providers/anthropic-compatible.ts +27 -0
  17. package/src/ai/providers/anthropic.ts +3 -1
  18. package/src/ai/providers/antigravity.ts +31 -6
  19. package/src/ai/providers/gemini.ts +45 -4
  20. package/src/ai/providers/kimi.ts +18 -0
  21. package/src/ai/providers/lmstudio.ts +8 -0
  22. package/src/ai/providers/ollama.ts +17 -5
  23. package/src/ai/providers/openai-compatible-catalog.ts +83 -0
  24. package/src/ai/providers/openai-compatible.ts +34 -0
  25. package/src/ai/providers/openai-responses.ts +11 -0
  26. package/src/ai/providers/openai.ts +115 -7
  27. package/src/ai/providers/xai.ts +18 -0
  28. package/src/ai/register-providers.ts +18 -0
  29. package/src/ai/think-tags.ts +84 -0
  30. package/src/ai/types.ts +11 -1
  31. package/src/auth/flows/index.ts +3 -3
  32. package/src/auth/index.ts +4 -1
  33. package/src/auth/oauth.ts +3 -3
  34. package/src/auth/refresh.ts +5 -0
  35. package/src/auth/storage.ts +12 -1
  36. package/src/commands/auth.ts +21 -2
  37. package/src/commands/launch/flags.ts +5 -1
  38. package/src/commands/launch/input.ts +13 -0
  39. package/src/commands/launch.ts +307 -26
  40. package/src/commands/setup.ts +3 -2
  41. package/src/tui/app.ts +61 -41
  42. package/src/tui/components/ascii-art.ts +91 -124
  43. package/src/tui/components/autocomplete.ts +16 -0
  44. package/src/tui/components/forge.ts +1 -1
  45. package/src/tui/components/provider-picker.ts +162 -0
  46. package/src/tui/components/slash.ts +2 -2
  47. package/src/tui/components/transcript.ts +7 -0
  48. package/src/tui/components/welcome.ts +8 -8
  49. 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 { renderDnaClaw, dnaClawHeight, dnaClawFrameCount, dnaClawBeat, DNA_FLOW_PALETTE } from "./components/ascii-art";
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
- /** Uniform live-activity fallback for models that stream no `reasoning` field: derive
90
- * what the model is doing from the forming JSON (`"tool":"x"` → "calling x…") or, for
91
- * prose-replying models, show the reply head so the live status field behaves identically
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 t.replace(/\s+/g, " ").slice(0, 140);
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
- // Surface the model's LIVE activity uniformly for every model/provider:
397
- // the streamed `reasoning` field when the model emits one, else a derived
398
- // fallback (tool being formed / reply prose head) so no model leaves the
399
- // live status field silent while it streams.
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
- this.streamingActivity = r;
409
- } else {
410
- const fallback = extractStreamingActivity(textSoFar);
411
- if (fallback && fallback !== this.streamingActivity) {
412
- this.streamingActivity = fallback;
413
- changed = true;
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
- return renderInputBox(display, {
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
- // DNA-flow identity on LIVE cards only: the flowing helix gradient rides
1177
- // the card border and the claw beat marks the title. Flushed/final cards
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: DNA_FLOW_PALETTE, phase: anim.phase, colorLevel: anim.colorLevel }, titleMark: anim.beat }
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
- const wrapped = tailForWrap(text)
1201
- .split("\n")
1202
- .flatMap(l => wrapTextWithAnsi(l, wrapW))
1203
- .filter(l => l.length > 0);
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 claw beat advances every 3 ticks. Quantization keeps repaints
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 = dnaClawBeat(Math.trunc(this.tickCount / 3), this.unicode);
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 DNA Claw brand symbol
1427
- // a twist-frame helix rotation combined with the flowing gradient phase.
1428
- // Both are quantized (3 twist frames × 20 gradient phases), so the cache
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: twist frame advances every 3 ticks,
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) % dnaClawFrameCount() : 0;
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 renderDnaClaw ever
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 = renderDnaClaw({
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 ? dnaClawHeight() : 0;
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
- export const DNA_CLAW_ART: string[] = [
416
- " ╭╯ ◆ ◆ ╰╮ ",
417
- " ╭╯ ╱╲ ╱╲ ╰╮ ",
418
- " ║ ╲ ╱ ║ ",
419
- " ╰╮ ╳ ╳ ╭╯ ",
420
- " ╰╮ ╱ ╲ ╭╯ ",
421
- " ╚══○ ○══╝ ",
422
- " ║ ║ ",
423
- " [ DNA Claw ] "
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 DNA_CLAW_ART_ASCII: string[] = [
427
- " /{ * * }\\ ",
428
- " /{ / \\ / \\ }\\ ",
429
- " | \\ X / | ",
430
- " \\{ X X }/ ",
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
- /** Twist animation frames for the compact DNA Claw: the claw silhouette stays
438
- * fixed while the inner helix lattice rotates. Frame 0 === DNA_CLAW_ART, so a
439
- * frameless render is byte-identical to the static symbol. All lines are the
440
- * same width (18) and every glyph is display-width 1. */
441
- export const DNA_CLAW_FRAMES: string[][] = [
442
- DNA_CLAW_ART,
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 DNA_CLAW_FRAMES_ASCII: string[][] = [
466
- DNA_CLAW_ART_ASCII,
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
- " | X X | ",
481
- " \\{ \\ X / }/ ",
482
- " \\{ X X }/ ",
483
- " \\==o o==/ ",
484
- " | | ",
485
- " [ DNA Claw ] "
455
+ " \\ o | * / ",
456
+ " <=[ @@@ ]=> ",
457
+ " \\ | / ",
458
+ " <_> "
486
459
  ]
487
460
  ];
488
461
 
489
- /** Number of twist frames in the compact DNA Claw animation cycle. */
490
- export function dnaClawFrameCount(): number {
491
- return DNA_CLAW_FRAMES.length;
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
- * a wide claw whose pincers frame a twisting DNA helix. Width-1 glyphs only
496
- * (box drawing + diagonals + geometrics) so padding/centering math stays exact. */
497
- export const DNA_CLAW_ART_GRAND: string[] = [
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 DNA_CLAW_ART_GRAND_ASCII: string[] = [
513
- " ** ** ",
514
- " /--'`--\\ /--'`--\\ ",
515
- " /' `\\ \\\\ // /' `\\ ",
516
- " /' | \\XX/ | `\\ ",
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
- // Bounded memo of fully-rendered DNA Claw frames keyed by every input that affects
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 ~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[] = [];
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 renderDnaClaw(opts: {
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
- /** Twist-animation frame (compact symbol only; wraps). The helix lattice rotates
545
- * while the claw silhouette stays fixed — combined with the flowing gradient
546
- * `phase` this animates the forge identity without any frame-count growth. */
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 = dnaClawMemo.get(memoKey);
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 ? DNA_CLAW_ART_GRAND : DNA_CLAW_ART_GRAND_ASCII;
518
+ source = useUnicode ? FORGE_MARK_ART_GRAND : FORGE_MARK_ART_GRAND_ASCII;
556
519
  } else {
557
- const frames = useUnicode ? DNA_CLAW_FRAMES : DNA_CLAW_FRAMES_ASCII;
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
- dnaClawMemo.set(memoKey, EMPTY_DNA_FRAME);
565
- return EMPTY_DNA_FRAME;
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 = DNA_FLOW_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 (dnaClawMemo.size >= DNA_CLAW_MEMO_CAP) {
582
- const oldest = dnaClawMemo.keys().next().value;
583
- if (oldest !== undefined) dnaClawMemo.delete(oldest);
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
- dnaClawMemo.set(memoKey, result);
548
+ forgeMarkMemo.set(memoKey, result);
586
549
  return result;
587
550
  }
588
551
 
589
- /** The DNA Claw identity palette (emerald cyan violet helix flow). Shared by
590
- * the claw art and the forge-card border flow so the brand gradient is uniform. */
591
- export const DNA_FLOW_PALETTE: readonly string[] = ["#10ac84", "#48dbfb", "#8e44ad"];
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 "claw beat" glyph for an animation tick — the ◆/╳/○ motifs of the
594
- * claw art cycling in place. Used as the live forge-card title mark. */
595
- export function dnaClawBeat(frame: number, unicode = true): string {
596
- const beats = unicode ? ["◆", "╳", "○"] : ["*", "X", "o"];
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 dnaClawHeight(): number {
601
- return DNA_CLAW_ART.length;
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 DNA claw beat glyph). */
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