jeo-code 0.6.23 → 0.6.26
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 +24 -0
- package/README.ja.md +3 -3
- package/README.ko.md +3 -3
- package/README.md +3 -3
- package/README.zh.md +3 -3
- package/package.json +1 -1
- package/src/ai/model-manager.ts +7 -4
- package/src/ai/provider-status.ts +19 -0
- package/src/ai/providers/anthropic.ts +4 -2
- package/src/ai/providers/antigravity.ts +5 -1
- package/src/ai/providers/gemini.ts +4 -3
- package/src/ai/providers/openai-compatible-catalog.ts +19 -8
- package/src/ai/providers/openai-compatible.ts +4 -1
- package/src/ai/providers/openai-responses.ts +11 -0
- package/src/ai/providers/openai.ts +99 -7
- package/src/ai/register-providers.ts +1 -1
- package/src/ai/types.ts +5 -0
- package/src/commands/auth.ts +4 -2
- package/src/commands/launch.ts +230 -15
- package/src/tui/app.ts +11 -11
- package/src/tui/components/ascii-art.ts +86 -122
- package/src/tui/components/provider-picker.ts +162 -0
- package/src/tui/components/slash.ts +2 -2
- package/src/tui/components/welcome.ts +8 -8
|
@@ -412,128 +412,91 @@ 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 — the
|
|
416
|
+
* neon crayfish (가재) from assets/character.png, read for its signature feature:
|
|
417
|
+
* the two raised pincer CLAWS (집게). Read top→bottom: the open pincer jaws
|
|
418
|
+
* (◣◣ / ◢◢) reaching up, the claw arms (◆══╲ ╱══◆) angling into the body where
|
|
419
|
+
* the blue ◆ knuckles meet, the head with its glowing eye/terminal cluster
|
|
420
|
+
* (◉◉◉), then the rounded carapace/tail (╲▔▔╱). The mark is purely symbolic —
|
|
421
|
+
* NO embedded lettering (the brand wordmark lives in the welcome header, not
|
|
422
|
+
* under the emblem); the JEO identity is carried by the crayfish-claw silhouette
|
|
423
|
+
* alone. Width-1 glyphs only (box drawing + geometrics) so padding/centering
|
|
424
|
+
* math stays exact. Frame 0 is the static symbol. */
|
|
425
|
+
export const FORGE_MARK_ART: string[] = [
|
|
426
|
+
" ◣◣ ◢◢ ",
|
|
427
|
+
" ◆══╲ ╱══◆ ",
|
|
428
|
+
" ╲◉◉◉╱ ",
|
|
429
|
+
" ╲▔▔╱ "
|
|
424
430
|
];
|
|
425
431
|
|
|
426
|
-
export const
|
|
427
|
-
"
|
|
428
|
-
"
|
|
429
|
-
"
|
|
430
|
-
"
|
|
431
|
-
" \\{ / X \\ }/ ",
|
|
432
|
-
" \\==o o==/ ",
|
|
433
|
-
" | | ",
|
|
434
|
-
" [ DNA Claw ] "
|
|
432
|
+
export const FORGE_MARK_ART_ASCII: string[] = [
|
|
433
|
+
" // \\\\ ",
|
|
434
|
+
" o==\\ /==o ",
|
|
435
|
+
" \\ooo/ ",
|
|
436
|
+
" \\__/ "
|
|
435
437
|
];
|
|
436
438
|
|
|
437
|
-
/**
|
|
438
|
-
* fixed while the
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
|
|
442
|
-
|
|
439
|
+
/** Claw-snap blink frames for the compact crayfish forge mark: the arms, head and
|
|
440
|
+
* carapace stay fixed while the two pincer jaws SNAP shut (◣◣/◢◢ open → ◢◣/◢◣
|
|
441
|
+
* closed), so the crayfish "clicks" its claws. Frame 0 === FORGE_MARK_ART, so a
|
|
442
|
+
* frameless render is byte-identical to the static symbol. All lines share the
|
|
443
|
+
* same width and width-1 glyphs. */
|
|
444
|
+
export const FORGE_MARK_FRAMES: string[][] = [
|
|
445
|
+
FORGE_MARK_ART,
|
|
443
446
|
[
|
|
444
|
-
"
|
|
445
|
-
"
|
|
446
|
-
"
|
|
447
|
-
"
|
|
448
|
-
" ╰╮ ╳ ╳ ╭╯ ",
|
|
449
|
-
" ╚══○ ○══╝ ",
|
|
450
|
-
" ║ ║ ",
|
|
451
|
-
" [ DNA Claw ] "
|
|
452
|
-
],
|
|
453
|
-
[
|
|
454
|
-
" ╭╯ ◆ ◆ ╰╮ ",
|
|
455
|
-
" ╭╯ ╱╲ ╱╲ ╰╮ ",
|
|
456
|
-
" ║ ╳ ╳ ║ ",
|
|
457
|
-
" ╰╮ ╲ ╳ ╱ ╭╯ ",
|
|
458
|
-
" ╰╮ ╳ ╳ ╭╯ ",
|
|
459
|
-
" ╚══○ ○══╝ ",
|
|
460
|
-
" ║ ║ ",
|
|
461
|
-
" [ DNA Claw ] "
|
|
447
|
+
" ◢◣ ◢◣ ",
|
|
448
|
+
" ◆══╲ ╱══◆ ",
|
|
449
|
+
" ╲◉◉◉╱ ",
|
|
450
|
+
" ╲▔▔╱ "
|
|
462
451
|
]
|
|
463
452
|
];
|
|
464
453
|
|
|
465
|
-
export const
|
|
466
|
-
|
|
467
|
-
[
|
|
468
|
-
" /{ * * }\\ ",
|
|
469
|
-
" /{ \\ / \\ / }\\ ",
|
|
470
|
-
" | X X | ",
|
|
471
|
-
" \\{ / X \\ }/ ",
|
|
472
|
-
" \\{ X X }/ ",
|
|
473
|
-
" \\==o o==/ ",
|
|
474
|
-
" | | ",
|
|
475
|
-
" [ DNA Claw ] "
|
|
476
|
-
],
|
|
454
|
+
export const FORGE_MARK_FRAMES_ASCII: string[][] = [
|
|
455
|
+
FORGE_MARK_ART_ASCII,
|
|
477
456
|
[
|
|
478
|
-
"
|
|
479
|
-
"
|
|
480
|
-
"
|
|
481
|
-
"
|
|
482
|
-
" \\{ X X }/ ",
|
|
483
|
-
" \\==o o==/ ",
|
|
484
|
-
" | | ",
|
|
485
|
-
" [ DNA Claw ] "
|
|
457
|
+
" >< >< ",
|
|
458
|
+
" o==\\ /==o ",
|
|
459
|
+
" \\ooo/ ",
|
|
460
|
+
" \\__/ "
|
|
486
461
|
]
|
|
487
462
|
];
|
|
488
463
|
|
|
489
|
-
/** Number of
|
|
490
|
-
export function
|
|
491
|
-
return
|
|
464
|
+
/** Number of blink frames in the compact forge-mark animation cycle. */
|
|
465
|
+
export function forgeMarkFrameCount(): number {
|
|
466
|
+
return FORGE_MARK_FRAMES.length;
|
|
492
467
|
}
|
|
493
468
|
|
|
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 ] "
|
|
469
|
+
/** Grand hero variant for the welcome forge box (gjc-style spacious banner): the
|
|
470
|
+
* same mascot crayfish rendered large and wordless — the two raised pincer claws
|
|
471
|
+
* (◣◣ / ◢◢, the 집게 feature) on wide arms (◆══╲ ╱══◆), the head with its glowing
|
|
472
|
+
* eye/terminal cluster (◉ ◉ ◉), and the broad rounded carapace/tail (╲▔▔▔▔▔╱).
|
|
473
|
+
* Purely symbolic — NO embedded lettering or caption. Width-1 glyphs only so
|
|
474
|
+
* padding/centering math stays exact. */
|
|
475
|
+
export const FORGE_MARK_ART_GRAND: string[] = [
|
|
476
|
+
" ◣◣ ◢◢ ",
|
|
477
|
+
" ◆══╲ ╱══◆ ",
|
|
478
|
+
" ╲ ◉ ◉ ◉ ╱ ",
|
|
479
|
+
" ╲▔▔▔▔▔▔▔▔▔╱ "
|
|
510
480
|
];
|
|
511
481
|
|
|
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 ] "
|
|
482
|
+
export const FORGE_MARK_ART_GRAND_ASCII: string[] = [
|
|
483
|
+
" // \\\\ ",
|
|
484
|
+
" o==\\ /==o ",
|
|
485
|
+
" \\ o o o / ",
|
|
486
|
+
" \\_________/ "
|
|
525
487
|
];
|
|
526
488
|
|
|
527
|
-
|
|
489
|
+
|
|
490
|
+
// Bounded memo of fully-rendered forge-mark frames keyed by every input that affects
|
|
528
491
|
// output (grand/unicode/cols/color/colorLevel/phase/frame). The live HUD cycles a
|
|
529
|
-
// FIXED
|
|
530
|
-
//
|
|
531
|
-
//
|
|
532
|
-
const
|
|
533
|
-
const
|
|
534
|
-
const
|
|
492
|
+
// FIXED frame set (blink × gradient phases) at ~120ms; without this each recurrence
|
|
493
|
+
// recomputed per-line animatedGradientText (ANSI gradient) from scratch. The memo
|
|
494
|
+
// makes the 2nd+ cycle O(1) lookups, cutting steady-state HUD CPU. LRU-capped.
|
|
495
|
+
const forgeMarkMemo = new Map<string, string[]>();
|
|
496
|
+
const FORGE_MARK_MEMO_CAP = 256;
|
|
497
|
+
const EMPTY_FORGE_FRAME: string[] = [];
|
|
535
498
|
|
|
536
|
-
export function
|
|
499
|
+
export function renderForgeMark(opts: {
|
|
537
500
|
cols?: number;
|
|
538
501
|
phase?: number;
|
|
539
502
|
unicode?: boolean;
|
|
@@ -541,34 +504,35 @@ export function renderDnaClaw(opts: {
|
|
|
541
504
|
colorLevel?: ColorLevel;
|
|
542
505
|
/** Grand hero variant (welcome forge box); default is the compact in-turn symbol. */
|
|
543
506
|
grand?: boolean;
|
|
544
|
-
/**
|
|
545
|
-
* while the
|
|
546
|
-
* `phase` this animates the forge identity without any
|
|
507
|
+
/** Blink-animation frame (compact symbol only; wraps). The cursor blinks and the
|
|
508
|
+
* status lamps swap while the window silhouette stays fixed — combined with the
|
|
509
|
+
* flowing gradient `phase` this animates the forge identity without any
|
|
510
|
+
* frame-count growth. */
|
|
547
511
|
frame?: number;
|
|
548
512
|
}): string[] {
|
|
549
513
|
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 =
|
|
514
|
+
const memoHit = forgeMarkMemo.get(memoKey);
|
|
551
515
|
if (memoHit) return memoHit;
|
|
552
516
|
const useUnicode = opts.unicode !== false;
|
|
553
517
|
let source: string[];
|
|
554
518
|
if (opts.grand) {
|
|
555
|
-
source = useUnicode ?
|
|
519
|
+
source = useUnicode ? FORGE_MARK_ART_GRAND : FORGE_MARK_ART_GRAND_ASCII;
|
|
556
520
|
} else {
|
|
557
|
-
const frames = useUnicode ?
|
|
521
|
+
const frames = useUnicode ? FORGE_MARK_FRAMES : FORGE_MARK_FRAMES_ASCII;
|
|
558
522
|
const f = Math.abs(Math.trunc(opts.frame ?? 0)) % frames.length;
|
|
559
523
|
source = frames[f]!;
|
|
560
524
|
}
|
|
561
525
|
const width = Math.max(0, ...source.map(l => l.length));
|
|
562
526
|
|
|
563
527
|
if (opts.cols !== undefined && opts.cols < width) {
|
|
564
|
-
|
|
565
|
-
return
|
|
528
|
+
forgeMarkMemo.set(memoKey, EMPTY_FORGE_FRAME);
|
|
529
|
+
return EMPTY_FORGE_FRAME;
|
|
566
530
|
}
|
|
567
531
|
|
|
568
532
|
const phase = opts.phase ?? 0;
|
|
569
533
|
const useColor = opts.color !== false;
|
|
570
534
|
const colorLevel = opts.colorLevel ?? ColorLevel.TrueColor;
|
|
571
|
-
const palette =
|
|
535
|
+
const palette = FORGE_FLOW_PALETTE;
|
|
572
536
|
|
|
573
537
|
const result = source.map((line, idx) => {
|
|
574
538
|
const padded = line.length < width ? line + " ".repeat(width - line.length) : line;
|
|
@@ -578,19 +542,19 @@ export function renderDnaClaw(opts: {
|
|
|
578
542
|
return animatedGradientText(padded, palette, phase + idx * 0.07, { colorLevel });
|
|
579
543
|
});
|
|
580
544
|
|
|
581
|
-
if (
|
|
582
|
-
const oldest =
|
|
583
|
-
if (oldest !== undefined)
|
|
545
|
+
if (forgeMarkMemo.size >= FORGE_MARK_MEMO_CAP) {
|
|
546
|
+
const oldest = forgeMarkMemo.keys().next().value;
|
|
547
|
+
if (oldest !== undefined) forgeMarkMemo.delete(oldest);
|
|
584
548
|
}
|
|
585
|
-
|
|
549
|
+
forgeMarkMemo.set(memoKey, result);
|
|
586
550
|
return result;
|
|
587
551
|
}
|
|
588
552
|
|
|
589
|
-
/** The jeo identity palette — the mascot's synthwave neon read straight
|
|
590
|
-
* character: blue
|
|
591
|
-
* forge-card border flow so the whole brand
|
|
592
|
-
* blue→violet→pink
|
|
593
|
-
export const
|
|
553
|
+
/** The jeo identity palette — the mascot crayfish's synthwave neon read straight
|
|
554
|
+
* off the character: blue antennae glow → violet carapace → pink claw tips.
|
|
555
|
+
* Shared by the forge mark and the forge-card border flow so the whole brand
|
|
556
|
+
* glows in the crayfish-wizard's signature blue→violet→pink shell sheen. */
|
|
557
|
+
export const FORGE_FLOW_PALETTE: readonly string[] = ["#48dbfb", "#8e44ad", "#f368e0"];
|
|
594
558
|
|
|
595
559
|
/** Width-1 forge title-mark glyph cycling the mascot's `jeo>` terminal prompt:
|
|
596
560
|
* a prompt caret then a blinking block cursor (filled → hollow), echoing the
|
|
@@ -601,6 +565,6 @@ export function forgeBeat(frame: number, unicode = true): string {
|
|
|
601
565
|
return beats[Math.abs(Math.trunc(frame)) % beats.length]!;
|
|
602
566
|
}
|
|
603
567
|
|
|
604
|
-
export function
|
|
605
|
-
return
|
|
568
|
+
export function forgeMarkHeight(): number {
|
|
569
|
+
return FORGE_MARK_ART.length;
|
|
606
570
|
}
|
|
@@ -7,6 +7,12 @@ import { SelectList, renderSelectList, type SelectItem, type RenderSelectOptions
|
|
|
7
7
|
import type { ProviderStatus } from "../../ai/provider-status";
|
|
8
8
|
import type { ProviderName } from "../../ai/types";
|
|
9
9
|
import { companyLabel } from "../../ai/model-catalog";
|
|
10
|
+
import { SUBSCRIPTION_PROVIDER_NAMES } from "../../ai/providers/openai-compatible-catalog";
|
|
11
|
+
|
|
12
|
+
/** True for subscription/plan-tier providers (coding-plan, portal, token-plan, code). */
|
|
13
|
+
export function isSubscriptionProvider(name: ProviderName): boolean {
|
|
14
|
+
return (SUBSCRIPTION_PROVIDER_NAMES as readonly string[]).includes(name);
|
|
15
|
+
}
|
|
10
16
|
|
|
11
17
|
/** Right-aligned hint for a provider row: credential kind + base URL + readiness. */
|
|
12
18
|
export function providerHint(s: ProviderStatus, unicode = true): string {
|
|
@@ -43,3 +49,159 @@ export function providerPicker(statuses: ProviderStatus[], unicode = true): Sele
|
|
|
43
49
|
export function renderProviderPicker(list: SelectList<ProviderName>, opts: RenderSelectOptions = {}): string[] {
|
|
44
50
|
return renderSelectList(list, { title: "Select a provider", rows: 8, ...opts });
|
|
45
51
|
}
|
|
52
|
+
/** Relative expiry label for a stored OAuth token, e.g. "expires in 42m" / "expired". */
|
|
53
|
+
export function loginExpiryLabel(expires: number | undefined, now: number = Date.now()): string | undefined {
|
|
54
|
+
if (!expires) return undefined;
|
|
55
|
+
const ms = expires - now;
|
|
56
|
+
if (ms <= 0) return "expired";
|
|
57
|
+
const mins = Math.round(ms / 60000);
|
|
58
|
+
if (mins < 60) return `expires in ${mins}m`;
|
|
59
|
+
return `expires in ${Math.round(mins / 60)}h`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Right-aligned hint for a `/login` row: live OAuth login status (account + expiry)
|
|
63
|
+
* rather than generic readiness. Logged-in providers show a check + account/expiry;
|
|
64
|
+
* others show a muted "not logged in". gjc-parity for the login selector. */
|
|
65
|
+
export function loginHint(s: ProviderStatus, unicode = true): string {
|
|
66
|
+
if (!s.loggedIn) return unicode ? "\u00b7 not logged in" : "not logged in";
|
|
67
|
+
const parts: string[] = [unicode ? "\u2713 logged in" : "logged in"];
|
|
68
|
+
if (s.oauthEmail) parts.push(s.oauthEmail);
|
|
69
|
+
const expiry = loginExpiryLabel(s.oauthExpires);
|
|
70
|
+
if (expiry) parts.push(expiry);
|
|
71
|
+
return parts.join(" \u00b7 ");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Build `/login` choices: logged-in providers first, each row badged with its live
|
|
75
|
+
* OAuth login status (account/expiry). Pure builder mirroring gjc's OAuth selector. */
|
|
76
|
+
export function buildLoginChoices(statuses: ProviderStatus[], unicode = true): SelectItem<ProviderName>[] {
|
|
77
|
+
const sorted = [...statuses].sort((a, b) => (!!a.loggedIn === !!b.loggedIn ? 0 : a.loggedIn ? -1 : 1));
|
|
78
|
+
return sorted.map(s => ({
|
|
79
|
+
value: s.name,
|
|
80
|
+
label: `${s.name} (${companyLabel(s.name)})`,
|
|
81
|
+
group: s.loggedIn ? "logged in" : "not logged in",
|
|
82
|
+
hint: loginHint(s, unicode),
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Construct a ready-to-drive `SelectList` for the `/login` flow. */
|
|
87
|
+
export function loginPicker(statuses: ProviderStatus[], unicode = true): SelectList<ProviderName> {
|
|
88
|
+
return new SelectList(buildLoginChoices(statuses, unicode));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Right-aligned hint for a subscription-provider row: whether its key/token is stored,
|
|
92
|
+
* plus the env var that seeds it. Subscriptions authenticate by token, not OAuth. */
|
|
93
|
+
export function subscriptionHint(s: ProviderStatus, unicode = true): string {
|
|
94
|
+
const set = s.kind === "api_key";
|
|
95
|
+
const badge = set ? (unicode ? "\u2713 active" : "active") : (unicode ? "\u00b7 no token" : "no token");
|
|
96
|
+
return s.envVar ? `${badge} \u00b7 ${s.envVar}` : badge;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Build the combined "OAuth / subscription" login choices: OAuth providers (logged-in
|
|
100
|
+
* first, badged with account/expiry) followed by subscription-tier providers (active
|
|
101
|
+
* first, badged with token status). Pure builder mirroring gjc's onboarding selector. */
|
|
102
|
+
export function buildSubscriptionLoginChoices(
|
|
103
|
+
oauthStatuses: ProviderStatus[],
|
|
104
|
+
subscriptionStatuses: ProviderStatus[],
|
|
105
|
+
unicode = true,
|
|
106
|
+
): SelectItem<ProviderName>[] {
|
|
107
|
+
const oauth = [...oauthStatuses]
|
|
108
|
+
.sort((a, b) => (!!a.loggedIn === !!b.loggedIn ? 0 : a.loggedIn ? -1 : 1))
|
|
109
|
+
.map(s => ({
|
|
110
|
+
value: s.name,
|
|
111
|
+
label: `${s.name} (${companyLabel(s.name)})`,
|
|
112
|
+
group: "OAuth login",
|
|
113
|
+
hint: loginHint(s, unicode),
|
|
114
|
+
}));
|
|
115
|
+
const subs = [...subscriptionStatuses]
|
|
116
|
+
.sort((a, b) => ((a.kind === "api_key") === (b.kind === "api_key") ? 0 : a.kind === "api_key" ? -1 : 1))
|
|
117
|
+
.map(s => ({
|
|
118
|
+
value: s.name,
|
|
119
|
+
label: `${s.name} (${companyLabel(s.name)})`,
|
|
120
|
+
group: "subscription / plan",
|
|
121
|
+
hint: subscriptionHint(s, unicode),
|
|
122
|
+
}));
|
|
123
|
+
return [...oauth, ...subs];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Construct a ready-to-drive `SelectList` for the combined OAuth / subscription login flow. */
|
|
127
|
+
export function subscriptionLoginPicker(
|
|
128
|
+
oauthStatuses: ProviderStatus[],
|
|
129
|
+
subscriptionStatuses: ProviderStatus[],
|
|
130
|
+
unicode = true,
|
|
131
|
+
): SelectList<ProviderName> {
|
|
132
|
+
return new SelectList(buildSubscriptionLoginChoices(oauthStatuses, subscriptionStatuses, unicode));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Render a login picker `SelectList` with a sensible default title. */
|
|
136
|
+
export function renderLoginPicker(list: SelectList<ProviderName>, opts: RenderSelectOptions = {}): string[] {
|
|
137
|
+
return renderSelectList(list, { title: "Select provider to login", rows: 8, ...opts });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** The ways to onboard a provider, mirroring gjc's `/provider` onboarding selector:
|
|
141
|
+
* log in to an OAuth/subscription provider, register an API-compatible endpoint, or
|
|
142
|
+
* store an API key for one of the bundled API-key-only providers (groq, deepseek, …). */
|
|
143
|
+
export type OnboardingAction = "oauth-login" | "api-key" | "api-add";
|
|
144
|
+
|
|
145
|
+
/** Build the bare-`/provider` onboarding choices (gjc-parity interactive selector).
|
|
146
|
+
* Pure builder: OAuth-login first (the common path), then API-key providers, then a
|
|
147
|
+
* custom API-compatible endpoint. */
|
|
148
|
+
export function buildOnboardingChoices(unicode = true): SelectItem<OnboardingAction>[] {
|
|
149
|
+
const arrow = unicode ? "\u2192 " : "";
|
|
150
|
+
return [
|
|
151
|
+
{
|
|
152
|
+
value: "oauth-login",
|
|
153
|
+
label: "Login with OAuth / subscription",
|
|
154
|
+
hint: `${arrow}OAuth providers + subscription / plan tokens`,
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
value: "api-key",
|
|
158
|
+
label: "Set an API key for a provider",
|
|
159
|
+
hint: `${arrow}groq, deepseek, mistral, openrouter, …`,
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
value: "api-add",
|
|
163
|
+
label: "Add an API-compatible endpoint",
|
|
164
|
+
hint: `${arrow}/provider add --base-url <url>`,
|
|
165
|
+
},
|
|
166
|
+
];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Construct a ready-to-drive `SelectList` for the bare-`/provider` onboarding flow. */
|
|
170
|
+
export function onboardingPicker(unicode = true): SelectList<OnboardingAction> {
|
|
171
|
+
return new SelectList(buildOnboardingChoices(unicode));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Render the onboarding picker `SelectList` with a sensible default title. */
|
|
175
|
+
export function renderOnboardingPicker(list: SelectList<OnboardingAction>, opts: RenderSelectOptions = {}): string[] {
|
|
176
|
+
return renderSelectList(list, { title: "Provider onboarding \u2191\u2193 move \u00b7 Enter select \u00b7 Esc cancel", rows: 4, ...opts });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Right-aligned hint for an API-key provider row: whether a key is stored, plus the
|
|
180
|
+
* env var that seeds it. Mirrors `loginHint` but for keyed (no-OAuth) providers. */
|
|
181
|
+
export function apiKeyHint(s: ProviderStatus, unicode = true): string {
|
|
182
|
+
const set = s.kind === "api_key";
|
|
183
|
+
const badge = set ? (unicode ? "\u2713 key set" : "key set") : (unicode ? "\u00b7 no key" : "no key");
|
|
184
|
+
return s.envVar ? `${badge} \u00b7 ${s.envVar}` : badge;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Build `/provider` API-key choices: providers with a stored key first, each badged with
|
|
188
|
+
* its key status + env var. Pure builder mirroring the OAuth login selector. */
|
|
189
|
+
export function buildApiKeyChoices(statuses: ProviderStatus[], unicode = true): SelectItem<ProviderName>[] {
|
|
190
|
+
const sorted = [...statuses].sort((a, b) => (a.kind === "api_key") === (b.kind === "api_key") ? 0 : a.kind === "api_key" ? -1 : 1);
|
|
191
|
+
return sorted.map(s => ({
|
|
192
|
+
value: s.name,
|
|
193
|
+
label: `${s.name} (${companyLabel(s.name)})`,
|
|
194
|
+
group: s.kind === "api_key" ? "key set" : "needs key",
|
|
195
|
+
hint: apiKeyHint(s, unicode),
|
|
196
|
+
}));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Construct a ready-to-drive `SelectList` for the API-key onboarding flow. */
|
|
200
|
+
export function apiKeyPicker(statuses: ProviderStatus[], unicode = true): SelectList<ProviderName> {
|
|
201
|
+
return new SelectList(buildApiKeyChoices(statuses, unicode));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Render the API-key provider picker `SelectList` with a sensible default title. */
|
|
205
|
+
export function renderApiKeyPicker(list: SelectList<ProviderName>, opts: RenderSelectOptions = {}): string[] {
|
|
206
|
+
return renderSelectList(list, { title: "Select a provider to key", rows: 8, ...opts });
|
|
207
|
+
}
|
|
@@ -37,8 +37,8 @@ export const SLASH_COMMAND_DETAILS: readonly SlashCommandInfo[] = [
|
|
|
37
37
|
{ command: "/goal", usage: "/goal <condition>", description: "Set a natural language stop condition for the session", group: "session" },
|
|
38
38
|
{ command: "/model", usage: "/model [id|#N|save|thinking <level>|subagent <role> <model|#N|thinking L>]", description: "Show/switch model; picker can apply to default or any subagent role and set thinking", group: "models" },
|
|
39
39
|
{ command: "/fast", usage: "/fast [on|off|status]", description: "Toggle fast thinking mode when the active model supports it", group: "models" },
|
|
40
|
-
{ command: "/provider", usage: "/provider [login [name] | add --base-url <url> [--model <m>]]", description: "Provider onboarding: `login [name]` starts OAuth; `add --base-url <url>` registers an OpenAI-compatible endpoint. Switch the active model/provider with /model", group: "models" },
|
|
41
|
-
{ command: "/login", usage: "/login [provider]", description: "OAuth login (alias of /provider login)", group: "models" },
|
|
40
|
+
{ command: "/provider", usage: "/provider [login [name] | key [name] [key] | add --base-url <url> [--model <m>]]", description: "Provider onboarding: `login [name]` starts OAuth; `key [name]` stores an API key (groq, deepseek, …); `add --base-url <url>` registers an OpenAI-compatible endpoint. Switch the active model/provider with /model", group: "models" },
|
|
41
|
+
{ command: "/login", usage: "/login [provider]", description: "OAuth login — opens a provider picker showing live login status (account · expiry) for each provider (alias of /provider login)", group: "models" },
|
|
42
42
|
{ command: "/logout", usage: "/logout <anthropic|openai|gemini|antigravity>", description: "Remove the stored OAuth token for a provider", group: "models" },
|
|
43
43
|
{ command: "/roles", usage: "/roles [tier model]", description: "Show or set model role tiers (smol/slow/plan)", group: "models" },
|
|
44
44
|
{ command: "/thinking", usage: "/thinking [level]", description: "Show or set thinking budget (minimal/low/medium/high/xhigh)", group: "models" },
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
-
import {
|
|
2
|
+
import { renderForgeMark, FORGE_MARK_ART_GRAND } from "./ascii-art";
|
|
3
3
|
import { truncate, isTTY } from "../terminal";
|
|
4
4
|
import { detectColorLevel, ColorLevel } from "./color";
|
|
5
5
|
|
|
@@ -13,7 +13,7 @@ export interface WelcomeData {
|
|
|
13
13
|
contextFiles?: string[]; // project context file paths (render basenames)
|
|
14
14
|
recentSessions?: { name: string; timeAgo: string }[];
|
|
15
15
|
cols?: number; // default 80
|
|
16
|
-
/** Gradient phase [0..1) for the
|
|
16
|
+
/** Gradient phase [0..1) for the forge mark — drives the launch sweep animation. */
|
|
17
17
|
phase?: number;
|
|
18
18
|
/** Lit-edge painter (top border + left edge); theme accent. Default gray. */
|
|
19
19
|
accent?: (s: string) => string;
|
|
@@ -42,7 +42,7 @@ function padLine(line: string, width: number, align: "left" | "center" | "right"
|
|
|
42
42
|
/**
|
|
43
43
|
* The gjc-style hero welcome box ("JEO forge"): one outer box with the version
|
|
44
44
|
* embedded in the top border and a SINGLE CENTERED column inside — brand line,
|
|
45
|
-
* tagline, the grand
|
|
45
|
+
* tagline, the grand jeo forge mark (flowing gradient on capable terminals),
|
|
46
46
|
* and the model/provider pills. Workspace details and key hints intentionally
|
|
47
47
|
* live elsewhere (footer/status bar), matching the gjc forge banner.
|
|
48
48
|
*/
|
|
@@ -57,8 +57,8 @@ export function renderWelcome(d: WelcomeData): string[] {
|
|
|
57
57
|
|
|
58
58
|
// The banner fills the full terminal width (gjc forge: flush with the input box and
|
|
59
59
|
// status bar below it). `cols - 1` leaves the last column free so a full-width row
|
|
60
|
-
// never wraps; the
|
|
61
|
-
const grandWidth = Math.max(...
|
|
60
|
+
// never wraps; the forge mark + pills stay centered inside the box.
|
|
61
|
+
const grandWidth = Math.max(...FORGE_MARK_ART_GRAND.map(l => l.length));
|
|
62
62
|
// Title rides ON the top border: `─── jeo v{version} · JEO forge ───`. Defined
|
|
63
63
|
// once here so the width calc and the border render below can't drift.
|
|
64
64
|
const titleDashes = 3;
|
|
@@ -93,10 +93,10 @@ export function renderWelcome(d: WelcomeData): string[] {
|
|
|
93
93
|
const bottomBorderPlain = g.bl + g.h.repeat(inner) + g.br;
|
|
94
94
|
const bottomBorderLine = shadow(bottomBorderPlain);
|
|
95
95
|
|
|
96
|
-
// Grand symbol when the box is wide enough; compact
|
|
96
|
+
// Grand symbol when the box is wide enough; compact forge mark otherwise.
|
|
97
97
|
const colorLevel = useColor ? detectColorLevel(process.env, isTTY()) : ColorLevel.None;
|
|
98
98
|
const grand = inner >= grandWidth;
|
|
99
|
-
const artLines =
|
|
99
|
+
const artLines = renderForgeMark({
|
|
100
100
|
color: useColor,
|
|
101
101
|
phase: d.phase ?? 0,
|
|
102
102
|
unicode,
|
|
@@ -136,7 +136,7 @@ export function renderWelcome(d: WelcomeData): string[] {
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
/**
|
|
139
|
-
* Launch animation: sweep the
|
|
139
|
+
* Launch animation: sweep the forge mark's gradient through `cycles` FULL palette
|
|
140
140
|
* cycles by re-printing the welcome box in place (cursor-up rewrites, same row
|
|
141
141
|
* count every frame). The loop is SEAMLESS — the phase wraps exactly at each
|
|
142
142
|
* cycle boundary with a constant frame delay, so consecutive cycles join with
|