jeo-code 0.6.23 → 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.
@@ -17,7 +17,7 @@ import { runUltragoalEngine, type UltragoalEngineOptions } from "./ultragoal";
17
17
  import { skillsPromptSection, loadSkills, buildSkillTask, workflowSkillsForPrompt, parseSkillInvocation, parseSkillChain, looksLikeSkillEcho, skillInvocationCard, type SkillDoc, type SkillInvocation } from "../skills/catalog";
18
18
  import { formatForgeBox } from "../tui/components/forge";
19
19
  import { interactiveOAuthLogin } from "./auth";
20
- import { logoutOAuth } from "../auth";
20
+ import { logoutOAuth, OAUTH_PROVIDERS, API_KEY_ONLY_PROVIDERS, setApiKey } from "../auth";
21
21
  import type { AuthProvider } from "../auth";
22
22
  import { matchSlash, isSlashAttempt, suggestSlashCommands, formatSlashCommandList, formatSlashPreview, slashPreviewMatches, activeTriggerToken, tabCompleteSelection, type SlashCommandInfo } from "../tui/components/slash";
23
23
  import { staticCompletionContext, readlineCompleter, formatCompletionPreview, formatMidTurnHint, tokenize, type CompletionContext } from "../tui/components/autocomplete";
@@ -41,7 +41,7 @@ import type { ProviderModelsResult, PickEntry, ProviderName, ModelRole, ThinkLev
41
41
  import { readGoalState, writeGoalState, clearGoalState, verifyGoal } from "../agent/goal-verifier";
42
42
 
43
43
  import { listAliases } from "../ai/model-registry";
44
- import { openaiCompatDef } from "../ai/providers/openai-compatible-catalog";
44
+ import { openaiCompatDef, SUBSCRIPTION_PROVIDER_NAMES } from "../ai/providers/openai-compatible-catalog";
45
45
 
46
46
  import { allSubagentRoles, getSubagentRole, resolveSubagentModel, resolveSubagentMaxSteps, resolveSubagentThinking, parseMaxSteps, withSubagentSetting, clearSubagentSetting } from "../agent/subagents";
47
47
  import { SelectList, renderSelectList, type SelectItem } from "../tui/components/select-list";
@@ -58,7 +58,7 @@ import {
58
58
  } from "../tui/components/config-panel";
59
59
  import { liveModelPicker, renderLiveModelPicker, type ModelAssignmentBadge } from "../tui/components/live-model-picker";
60
60
 
61
- import { providerPicker, renderProviderPicker } from "../tui/components/provider-picker";
61
+ import { loginPicker, renderLoginPicker, onboardingPicker, renderOnboardingPicker, apiKeyPicker, renderApiKeyPicker, subscriptionLoginPicker, type OnboardingAction } from "../tui/components/provider-picker";
62
62
  import { detectLanguage, languageLabel, parseLineRange, sliceLines, formatCodeBlock, formatDiff, sanitizeForTerminal } from "../tui/components/code-view";
63
63
  import { categoryBadge } from "../tui/components/category-index";
64
64
  import { renderInputFrame, verticalCursorOffset } from "../tui/components/input-box";
@@ -214,7 +214,10 @@ export {
214
214
  currentAtLabelFn as currentAtLabel,
215
215
  };
216
216
  export function normalizeSlashAlias(input: string): string {
217
- if (input === "/login" || input.startsWith("/login ")) return `/provider login${input.slice("/login".length)}`;
217
+ // gjc-parity: bare `/login` opens the provider onboarding selector (same as bare
218
+ // `/provider`); `/login <provider|args>` is the direct OAuth-login alias.
219
+ if (input === "/login") return "/provider";
220
+ if (input.startsWith("/login ")) return `/provider login${input.slice("/login".length)}`;
218
221
  if (input === "/settings") return "/config";
219
222
  if (input === "/subagent" || input.startsWith("/subagent ")) return `/agents${input.slice("/subagent".length)}`;
220
223
  if (input === "/subagents" || input.startsWith("/subagents ")) return `/agents${input.slice("/subagents".length)}`;
@@ -1093,7 +1096,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1093
1096
  // gjc-style fresh-start clear so the banner opens atop a clean screen. TTY only,
1094
1097
  // never mid-turn (scrollback flood). ponytail: add an opt-out env if anyone misses their scrollback.
1095
1098
  if (process.stdout.isTTY) process.stdout.write(clearScreen());
1096
- // Launch sweep: the DNA Claw's gradient loops seamlessly (default 2 full
1099
+ // Launch sweep: the forge mark's gradient loops seamlessly (default 2 full
1097
1100
  // cycles, JEO_WELCOME_ANIM_CYCLES overrides), ending on the static banner.
1098
1101
  // Truecolor TTYs only; JEO_NO_WELCOME_ANIM=1 opts out.
1099
1102
  const sweepable =
@@ -2206,13 +2209,112 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
2206
2209
 
2207
2210
 
2208
2211
  const pickCloudProvider = async (statuses: Awaited<ReturnType<typeof describeAllProviders>>): Promise<AuthProvider | undefined> => {
2209
- const cloud = new Set(["anthropic", "openai", "gemini", "antigravity"]);
2210
- const list = providerPicker(statuses.filter(s => cloud.has(s.name)), true);
2212
+ const cloud = new Set<string>(OAUTH_PROVIDERS); // OAuth-login providers (anthropic/openai/gemini/antigravity)
2213
+ const subs = new Set<string>(SUBSCRIPTION_PROVIDER_NAMES); // subscription/plan products (token-keyed)
2214
+ const list = subscriptionLoginPicker(
2215
+ statuses.filter(s => cloud.has(s.name)),
2216
+ statuses.filter(s => subs.has(s.name)),
2217
+ true,
2218
+ );
2219
+ let chosen: ProviderName | undefined;
2220
+ await runSelectPicker(
2221
+ (cols, rows) =>
2222
+ renderLoginPicker(list, {
2223
+ title: "Login with OAuth / subscription ↑↓ move · Enter select · Esc cancel",
2224
+ cols,
2225
+ rows: Math.max(4, Math.min(rows, 8)),
2226
+ unicode: true,
2227
+ color: true,
2228
+ }),
2229
+ (ch, key) => {
2230
+ if (key?.name === "up") {
2231
+ list.up();
2232
+ return false;
2233
+ }
2234
+ if (key?.name === "down") {
2235
+ list.down();
2236
+ return false;
2237
+ }
2238
+ if (key?.name === "pageup") {
2239
+ list.page(-1, 4);
2240
+ return false;
2241
+ }
2242
+ if (key?.name === "pagedown") {
2243
+ list.page(1, 4);
2244
+ return false;
2245
+ }
2246
+ if (key?.name === "backspace") {
2247
+ list.backspace();
2248
+ return false;
2249
+ }
2250
+ if (key?.name === "escape" || (key?.ctrl && key.name === "c")) {
2251
+ return true;
2252
+ }
2253
+ if (key?.name === "return" || key?.name === "enter") {
2254
+ chosen = list.selected()?.value;
2255
+ return true;
2256
+ }
2257
+ if (ch && ch >= " " && !key?.ctrl && !key?.meta) {
2258
+ list.typeChar(ch);
2259
+ }
2260
+ return false;
2261
+ },
2262
+ );
2263
+ return chosen && (cloud.has(chosen) || subs.has(chosen)) ? chosen as AuthProvider : undefined;
2264
+ };
2265
+
2266
+ // Bare `/provider` opens gjc's interactive onboarding selector: choose between
2267
+ // OAuth/subscription login and registering an API-compatible endpoint. Returns the
2268
+ // picked action, or undefined when cancelled (Esc/Ctrl+C). TTY only — callers fall
2269
+ // back to the printed usage in non-interactive mode.
2270
+ const pickOnboardingAction = async (): Promise<OnboardingAction | undefined> => {
2271
+ const list = onboardingPicker(true);
2272
+ let chosen: OnboardingAction | undefined;
2273
+ await runSelectPicker(
2274
+ (cols, rows) =>
2275
+ renderOnboardingPicker(list, {
2276
+ cols,
2277
+ rows: Math.max(4, Math.min(rows, 6)),
2278
+ unicode: true,
2279
+ color: true,
2280
+ }),
2281
+ (ch, key) => {
2282
+ if (key?.name === "up") {
2283
+ list.up();
2284
+ return false;
2285
+ }
2286
+ if (key?.name === "down") {
2287
+ list.down();
2288
+ return false;
2289
+ }
2290
+ if (key?.name === "escape" || (key?.ctrl && key.name === "c")) {
2291
+ return true;
2292
+ }
2293
+ if (key?.name === "return" || key?.name === "enter") {
2294
+ chosen = list.selected()?.value;
2295
+ return true;
2296
+ }
2297
+ if (ch && ch >= " " && !key?.ctrl && !key?.meta) {
2298
+ list.typeChar(ch);
2299
+ }
2300
+ return false;
2301
+ },
2302
+ );
2303
+ return chosen;
2304
+ };
2305
+
2306
+ // API-key onboarding: pick one of the bundled API-key-only providers (groq, deepseek,
2307
+ // mistral, …) to store a key for. Returns the picked provider, or undefined on cancel.
2308
+ // TTY only — the caller prints scriptable guidance otherwise.
2309
+ const pickApiKeyProvider = async (statuses: Awaited<ReturnType<typeof describeAllProviders>>): Promise<AuthProvider | undefined> => {
2310
+ const subs = new Set<string>(SUBSCRIPTION_PROVIDER_NAMES); // surfaced under OAuth/subscription login instead
2311
+ const keyed = new Set<string>(API_KEY_ONLY_PROVIDERS);
2312
+ const list = apiKeyPicker(statuses.filter(s => keyed.has(s.name) && !subs.has(s.name)), true);
2211
2313
  let chosen: ProviderName | undefined;
2212
2314
  await runSelectPicker(
2213
2315
  (cols, rows) =>
2214
- renderProviderPicker(list, {
2215
- title: "Select OAuth provider",
2316
+ renderApiKeyPicker(list, {
2317
+ title: "Select a provider to key \u2191\u2193 move \u00b7 Enter select \u00b7 Esc cancel",
2216
2318
  cols,
2217
2319
  rows: Math.max(4, Math.min(rows, 8)),
2218
2320
  unicode: true,
@@ -2252,9 +2354,10 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
2252
2354
  return false;
2253
2355
  },
2254
2356
  );
2255
- return chosen && cloud.has(chosen) ? chosen as AuthProvider : undefined;
2357
+ return chosen && keyed.has(chosen) ? chosen as AuthProvider : undefined;
2256
2358
  };
2257
2359
 
2360
+
2258
2361
  if (previewEnabled) {
2259
2362
  process.once("exit", () => out.write("\x1b[?25h")); // safety net: never leave the cursor hidden
2260
2363
  const footerKeypressHandler = (_ch: string, key: { name?: string; ctrl?: boolean; meta?: boolean } | undefined) => {
@@ -3002,40 +3105,152 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
3002
3105
  }
3003
3106
  if (input.startsWith("/provider") && (input === "/provider" || input[9] === " ")) {
3004
3107
  const tokens = input.substring(9).trim().split(/\s+/).filter(Boolean);
3005
- const name = (tokens[0] ?? "").toLowerCase();
3108
+ let name = (tokens[0] ?? "").toLowerCase();
3006
3109
  // gjc-parity (semantic): /provider is ONBOARDING ONLY — set up OAuth credentials
3007
3110
  // or an API-compatible endpoint. Switching the active provider/model lives in /model.
3008
3111
  const providerOnboardingUsage = (): string[] => [
3009
3112
  "Provider onboarding — set up credentials or an API-compatible endpoint:",
3010
- " OAuth / subscription : /provider login [anthropic|openai|gemini|antigravity] (alias: /login)",
3113
+ " OAuth / subscription : /provider login [anthropic|openai|gemini|antigravity|<subscription>] (alias: /login)",
3114
+ " subscriptions (token): alibaba-coding-plan, qwen-portal, xiaomi-token-plan-*, minimax-code*",
3115
+ " API key (cloud) : /provider key [provider] [key] (groq, deepseek, mistral, openrouter, …)",
3011
3116
  " API-compatible : /provider add --base-url <url> [--model <model>] [--compat openai] (reads OPENAI_API_KEY)",
3012
3117
  " show current / clear: /provider add · /provider add clear",
3013
3118
  " Logout : /logout <provider>",
3119
+ " Headless OAuth : paste the redirect URL or code when the login prompt asks.",
3014
3120
  "Switch the active model or provider with /model.",
3015
3121
  ];
3122
+ // Bare `/provider` in an interactive TTY → gjc's interactive onboarding selector
3123
+ // (OAuth login vs API-compatible endpoint). The choice routes into the same
3124
+ // `login`/`add` branches below; cancel falls through to the printed readiness +
3125
+ // usage. Non-TTY / `help` keep the static panel (scriptable, unchanged).
3126
+ if (!name && process.stdin.isTTY && process.stdout.isTTY) {
3127
+ const action = await pickOnboardingAction();
3128
+ if (action === "oauth-login") name = "login";
3129
+ else if (action === "api-key") name = "key";
3130
+ else if (action === "api-add") {
3131
+ console.log("Add an API-compatible endpoint:");
3132
+ console.log(" /provider add --base-url <url> [--model <model>] [--compat openai]");
3133
+ console.log(" reads OPENAI_API_KEY · show current / clear: /provider add · /provider add clear");
3134
+ continue;
3135
+ }
3136
+ // action === undefined (cancelled) → fall through to the readiness panel.
3137
+ }
3138
+ // `/provider key [name] [key]` → store an API key for an API-key-only provider
3139
+ // (groq/deepseek/mistral/…). Interactive: pick the provider, then paste the key.
3140
+ if (name === "key") {
3141
+ const keyed = new Set<string>(API_KEY_ONLY_PROVIDERS);
3142
+ let target = tokens.slice(1).map(t => t.toLowerCase()).find(t => keyed.has(t)) as AuthProvider | undefined;
3143
+ // A trailing token after the provider name is treated as the key itself.
3144
+ const inlineKey = target ? tokens.slice(1).filter(t => t.toLowerCase() !== target).pop() : undefined;
3145
+ if (!target) {
3146
+ const statuses = await describeAllProviders();
3147
+ if (process.stdin.isTTY && process.stdout.isTTY) {
3148
+ target = await pickApiKeyProvider(statuses);
3149
+ } else {
3150
+ console.log("Set an API key for which provider?");
3151
+ console.log(` ${API_KEY_ONLY_PROVIDERS.filter(p => !(SUBSCRIPTION_PROVIDER_NAMES as readonly string[]).includes(p)).join(", ")}`);
3152
+ console.log(" Subscription / plan products use /provider login (token).");
3153
+ console.log(" Usage: /provider key <provider> <api-key> (or set <PROVIDER>_API_KEY)");
3154
+ }
3155
+ if (!target) {
3156
+ console.log("(cancelled)");
3157
+ continue;
3158
+ }
3159
+ }
3160
+ const envVar = `${target.toUpperCase().replace(/-/g, "_")}_API_KEY`;
3161
+ let apiKey = inlineKey;
3162
+ if (!apiKey) {
3163
+ apiKey = (await promptInput(`Paste ${target} API key (blank to cancel): `)).trim();
3164
+ }
3165
+ if (!apiKey) {
3166
+ console.log("(cancelled — no key entered)");
3167
+ continue;
3168
+ }
3169
+ await setApiKey(target, apiKey);
3170
+ console.log(`[SUCCESS] Stored ${target} API key in ~/.jeo/config.json (also reads ${envVar}).`);
3171
+ const live = await refreshLiveModelsCache();
3172
+ const after = (await describeAllProviders()).find(s => s.name === target);
3173
+ if (after) console.log(` status → ${after.name}: ${after.ready ? `✓ ${after.label}` : after.label}`);
3174
+ const forProvider = live.filter(r => r.provider === target);
3175
+ if (forProvider.some(r => r.ok && r.models.length > 0)) {
3176
+ lastPickIndex = flattenModels(forProvider);
3177
+ const viaCatalog = forProvider.some(r => r.fallback);
3178
+ console.log(` ${viaCatalog ? "catalog" : "live"} ${target} models → /model #N${viaCatalog ? " (live list endpoint unavailable; showing known models)" : ""}`);
3179
+ logLines(formatPickListWithCapabilities(lastPickIndex, { cap: 12 }));
3180
+ } else {
3181
+ const failed = forProvider.find(r => !r.ok);
3182
+ if (failed?.error) console.log(` live ${target} models unavailable: ${failed.error}`);
3183
+ }
3184
+ continue;
3185
+ }
3016
3186
  // `/provider login|auth [name]` → run OAuth login from the REPL.
3017
3187
  if (name === "login" || name === "auth") {
3018
3188
  const cloud = ["anthropic", "openai", "gemini", "antigravity"] as const;
3019
- let target = tokens.slice(1).map(t => t.toLowerCase()).find(t => (cloud as readonly string[]).includes(t));
3189
+ const subs = new Set<string>(SUBSCRIPTION_PROVIDER_NAMES); // token-keyed subscription/plan products
3190
+ let target = tokens.slice(1).map(t => t.toLowerCase()).find(t => (cloud as readonly string[]).includes(t) || subs.has(t));
3020
3191
  if (!target) {
3021
3192
  const statuses = await describeAllProviders();
3022
3193
  if (process.stdin.isTTY && process.stdout.isTTY) {
3023
3194
  target = await pickCloudProvider(statuses);
3024
3195
  } else {
3025
3196
  console.log("Log in to which provider?");
3197
+ console.log(" OAuth:");
3026
3198
  cloud.forEach((p, i) => {
3027
3199
  const st = statuses.find(s => s.name === p);
3028
- console.log(` ${i + 1}) ${p.padEnd(10)} ${st?.ready ? `✓ ${st.label}` : "· not ready"}`);
3200
+ const badge = st?.loggedIn
3201
+ ? `\u2713 logged in${st.oauthEmail ? ` (${st.oauthEmail})` : ""}`
3202
+ : "\u00b7 not logged in";
3203
+ console.log(` ${i + 1}) ${p.padEnd(10)} ${badge}`);
3029
3204
  });
3205
+ console.log(" Subscription / plan (token):");
3206
+ for (const p of SUBSCRIPTION_PROVIDER_NAMES) {
3207
+ const st = statuses.find(s => s.name === p);
3208
+ const badge = st?.kind === "api_key" ? "\u2713 active" : "\u00b7 no token";
3209
+ console.log(` ${p.padEnd(22)} ${badge} (set ${st?.envVar ?? `${p.toUpperCase().replace(/-/g, "_")}_API_KEY`})`);
3210
+ }
3030
3211
  const ans = (await promptInput(`Choose [1-${cloud.length}] or name (blank to cancel): `)).trim().toLowerCase();
3031
3212
  const byNum: Record<string, string> = Object.fromEntries(cloud.map((p, i) => [String(i + 1), p]));
3032
- target = byNum[ans] ?? ((cloud as readonly string[]).includes(ans) ? ans : undefined);
3213
+ target = byNum[ans] ?? ((cloud as readonly string[]).includes(ans) || subs.has(ans) ? ans : undefined);
3033
3214
  }
3034
3215
  if (!target) {
3035
3216
  console.log("(cancelled)");
3036
3217
  continue;
3037
3218
  }
3038
3219
  }
3220
+ // Subscription/plan providers authenticate by token (not OAuth): prompt for the key
3221
+ // and store it like `/provider key`, then refresh + list models.
3222
+ if (subs.has(target)) {
3223
+ const sub = target as AuthProvider;
3224
+ const envVar = `${sub.toUpperCase().replace(/-/g, "_")}_API_KEY`;
3225
+ let token = tokens.slice(1).filter(t => t.toLowerCase() !== sub).pop();
3226
+ if (!token) {
3227
+ if (process.stdin.isTTY && process.stdout.isTTY) {
3228
+ token = (await promptInput(`Paste ${sub} subscription token (blank to cancel): `)).trim();
3229
+ } else {
3230
+ console.log(`Set the ${sub} subscription token with: /provider login ${sub} <token> (or set ${envVar}).`);
3231
+ }
3232
+ }
3233
+ if (!token) {
3234
+ console.log("(cancelled — no token entered)");
3235
+ continue;
3236
+ }
3237
+ await setApiKey(sub, token);
3238
+ console.log(`[SUCCESS] Stored ${sub} subscription token in ~/.jeo/config.json (also reads ${envVar}).`);
3239
+ const live = await refreshLiveModelsCache();
3240
+ const after = (await describeAllProviders()).find(s => s.name === sub);
3241
+ if (after) console.log(` status → ${after.name}: ${after.ready ? `✓ ${after.label}` : after.label}`);
3242
+ const forProvider = live.filter(r => r.provider === sub);
3243
+ if (forProvider.some(r => r.ok && r.models.length > 0)) {
3244
+ lastPickIndex = flattenModels(forProvider);
3245
+ const viaCatalog = forProvider.some(r => r.fallback);
3246
+ console.log(` ${viaCatalog ? "catalog" : "live"} ${sub} models → /model #N${viaCatalog ? " (live list endpoint unavailable; showing known models)" : ""}`);
3247
+ logLines(formatPickListWithCapabilities(lastPickIndex, { cap: 12 }));
3248
+ } else {
3249
+ const failed = forProvider.find(r => !r.ok);
3250
+ if (failed?.error) console.log(` live ${sub} models unavailable: ${failed.error}`);
3251
+ }
3252
+ continue;
3253
+ }
3039
3254
  console.log(`Starting OAuth login for ${target}…`);
3040
3255
  try {
3041
3256
  const { email } = await interactiveOAuthLogin(target as AuthProvider, rl);
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, forgeBeat, 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";
@@ -1186,11 +1186,11 @@ export class LaunchTui {
1186
1186
  index: i + 1,
1187
1187
  color: this.theme.color,
1188
1188
  dim,
1189
- // DNA-flow identity on LIVE cards only: the flowing helix gradient rides
1189
+ // Forge-flow identity on LIVE cards only: the flowing neon gradient rides
1190
1190
  // the card border and the prompt beat marks the title. Flushed/final cards
1191
1191
  // stay static. Suppressed while `dim` (in-flight shading takes precedence).
1192
1192
  ...(anim && !dim
1193
- ? { 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 }
1194
1194
  : {}),
1195
1195
  }));
1196
1196
  }
@@ -1443,9 +1443,9 @@ export class LaunchTui {
1443
1443
  const innerWidth = !fit ? cols : this.inline ? cols - 1 : cols - 4;
1444
1444
 
1445
1445
  // Resolve the current (monotonic) stage for the track; announce a transition
1446
- // once when it first advances. The header art is the DNA Claw brand symbol
1447
- // a twist-frame helix rotation combined with the flowing gradient phase.
1448
- // 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
1449
1449
  // recomputes at most once per changed tick and stays a single slot (O(1)).
1450
1450
  const stepNow = this.footer.step || 0;
1451
1451
  const idx = this.progress.observe(stepNow, this.footer.maxSteps ?? DEFAULT_MAX_STEPS);
@@ -1455,16 +1455,16 @@ export class LaunchTui {
1455
1455
  this.appendLedger(`${arrow} ${transitionMessage(idx)}\n`, "notice");
1456
1456
  }
1457
1457
  const showArt = fit && !this.inline && rows >= 18 && cols >= 40;
1458
- // 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,
1459
1459
  // gradient phase cycles 20 quantized steps (tickCount*0.05 % 1).
1460
- const twist = isThinking ? Math.trunc(this.tickCount / 3) % dnaClawFrameCount() : 0;
1460
+ const twist = isThinking ? Math.trunc(this.tickCount / 3) % forgeMarkFrameCount() : 0;
1461
1461
  const qPhase = isThinking ? this.tickCount % 20 : 0;
1462
1462
  const effFrame = twist * 100 + qPhase;
1463
1463
  if (showArt && (idx !== this.cachedStageIndex || cols !== this.cachedCols || effFrame !== this.cachedFrame)) {
1464
- // 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
1465
1465
  // throws (resize race, bad gradient level), pre-committed keys would mark the
1466
1466
  // STALE art as current and freeze the header at an old frame forever.
1467
- const art = renderDnaClaw({
1467
+ const art = renderForgeMark({
1468
1468
  cols: innerWidth,
1469
1469
  phase: qPhase * 0.05,
1470
1470
  frame: twist,
@@ -1479,7 +1479,7 @@ export class LaunchTui {
1479
1479
  this.cachedCols = cols;
1480
1480
  this.cachedFrame = effFrame;
1481
1481
  }
1482
- const artLinesCount = showArt ? dnaClawHeight() : 0;
1482
+ const artLinesCount = showArt ? forgeMarkHeight() : 0;
1483
1483
  const trackCount = showArt ? 1 : 0;
1484
1484
  const headerHeight = artLinesCount + trackCount + (showArt ? 1 : 0);
1485
1485