jeo-code 0.6.1 → 0.6.3
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 +17 -0
- package/README.ja.md +2 -2
- package/README.ko.md +2 -2
- package/README.md +2 -2
- package/README.zh.md +2 -2
- package/package.json +1 -1
- package/src/auth/callback-server.ts +1 -1
- package/src/commands/launch.ts +47 -17
- package/src/tui/app.ts +16 -7
- package/src/tui/components/config-panel.ts +29 -0
- package/src/tui/components/evolution.ts +82 -3
- package/src/tui/components/forge.ts +1 -1
- package/src/tui/components/markdown-text.ts +58 -6
- package/src/tui/components/provider-picker.ts +5 -3
- package/src/tui/components/section.ts +18 -4
- package/src/tui/components/spinner.ts +1 -0
- package/src/tui/components/status.ts +28 -6
- package/src/tui/components/transcript.ts +6 -0
- package/src/util/retry.ts +9 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
6
6
|
|
|
7
7
|
The README mirrors the latest 5 entries — regenerate with `bun run changelog:sync`.
|
|
8
8
|
|
|
9
|
+
## [0.6.3] - 2026-06-16
|
|
10
|
+
_OAuth loopback reliability fix._
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **OAuth loopback redirect uses `127.0.0.1` instead of `localhost`** (RFC 8252 §7.3). `localhost` can resolve to IPv6 `::1` or be hosts-file-overridden, intermittently breaking the auth callback; the IP literal is reliable. Only the dynamic-loopback path changes — providers with a fixed redirect URI are unaffected (#30).
|
|
14
|
+
|
|
15
|
+
## [0.6.2] - 2026-06-16
|
|
16
|
+
_Interactive `/provider` picker, clearer animated status + labeled block/prose boundaries, and a transient empty-response retry._
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- **Interactive `/provider` picker** (gjc-style) with a clean screen after login (#26).
|
|
20
|
+
- **Clearer visual structure** — unified labeled boundaries for thinking / reasoning / output blocks and a dimensional animated status line (#29, building on the labeled-boundary work).
|
|
21
|
+
- `docs/minimo/` — a plan to apply MiMo Code's memory & goal-management ideas to jeo (#28).
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- **Retry transient empty-200 responses** (a 200 with an empty body) for stability — gjc parity (#27).
|
|
25
|
+
|
|
9
26
|
## [0.6.1] - 2026-06-16
|
|
10
27
|
_Live reasoning progress (no more frozen "calling model"), thinking-level fixes for Anthropic/Antigravity, and input-box/Ctrl+O TUI fixes._
|
|
11
28
|
|
package/README.ja.md
CHANGED
|
@@ -158,11 +158,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
|
|
|
158
158
|
## 変更履歴 (Changelog)
|
|
159
159
|
|
|
160
160
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
161
|
+
- **[0.6.3]** (2026-06-16) — OAuth loopback reliability fix.
|
|
162
|
+
- **[0.6.2]** (2026-06-16) — Interactive `/provider` picker, clearer animated status + labeled block/prose boundaries, and a transient empty-response retry.
|
|
161
163
|
- **[0.6.1]** (2026-06-16) — Live reasoning progress (no more frozen "calling model"), thinking-level fixes for Anthropic/Antigravity, and input-box/Ctrl+O TUI fixes.
|
|
162
164
|
- **[0.6.0]** (2026-06-16) — TUI quality of life: durable input history (↑ recalls past queries across launches), clean `/resume` rendering, and a scrollable mid-turn Ctrl+O panel.
|
|
163
165
|
- **[0.5.16]** (2026-06-16) — `/resume` and Ctrl+O no longer corrupt the TUI — clean screen restore + scrollback expand.
|
|
164
|
-
- **[0.5.15]** (2026-06-16) — `jeo update` now actually upgrades — bare command installs the latest release instead of just printing a manual command.
|
|
165
|
-
- **[0.5.14]** (2026-06-16) — `jeo --tmux` live-verification harness — repeatable stability + behavior checks.
|
|
166
166
|
|
|
167
167
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
168
168
|
<!-- CHANGELOG:END -->
|
package/README.ko.md
CHANGED
|
@@ -158,11 +158,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
|
|
|
158
158
|
## 변경 이력 (Changelog)
|
|
159
159
|
|
|
160
160
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
161
|
+
- **[0.6.3]** (2026-06-16) — OAuth loopback reliability fix.
|
|
162
|
+
- **[0.6.2]** (2026-06-16) — Interactive `/provider` picker, clearer animated status + labeled block/prose boundaries, and a transient empty-response retry.
|
|
161
163
|
- **[0.6.1]** (2026-06-16) — Live reasoning progress (no more frozen "calling model"), thinking-level fixes for Anthropic/Antigravity, and input-box/Ctrl+O TUI fixes.
|
|
162
164
|
- **[0.6.0]** (2026-06-16) — TUI quality of life: durable input history (↑ recalls past queries across launches), clean `/resume` rendering, and a scrollable mid-turn Ctrl+O panel.
|
|
163
165
|
- **[0.5.16]** (2026-06-16) — `/resume` and Ctrl+O no longer corrupt the TUI — clean screen restore + scrollback expand.
|
|
164
|
-
- **[0.5.15]** (2026-06-16) — `jeo update` now actually upgrades — bare command installs the latest release instead of just printing a manual command.
|
|
165
|
-
- **[0.5.14]** (2026-06-16) — `jeo --tmux` live-verification harness — repeatable stability + behavior checks.
|
|
166
166
|
|
|
167
167
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
168
168
|
<!-- CHANGELOG:END -->
|
package/README.md
CHANGED
|
@@ -158,11 +158,11 @@ Required npm token permissions (repository secret `NPM_TOKEN`):
|
|
|
158
158
|
## Changelog
|
|
159
159
|
|
|
160
160
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
161
|
+
- **[0.6.3]** (2026-06-16) — OAuth loopback reliability fix.
|
|
162
|
+
- **[0.6.2]** (2026-06-16) — Interactive `/provider` picker, clearer animated status + labeled block/prose boundaries, and a transient empty-response retry.
|
|
161
163
|
- **[0.6.1]** (2026-06-16) — Live reasoning progress (no more frozen "calling model"), thinking-level fixes for Anthropic/Antigravity, and input-box/Ctrl+O TUI fixes.
|
|
162
164
|
- **[0.6.0]** (2026-06-16) — TUI quality of life: durable input history (↑ recalls past queries across launches), clean `/resume` rendering, and a scrollable mid-turn Ctrl+O panel.
|
|
163
165
|
- **[0.5.16]** (2026-06-16) — `/resume` and Ctrl+O no longer corrupt the TUI — clean screen restore + scrollback expand.
|
|
164
|
-
- **[0.5.15]** (2026-06-16) — `jeo update` now actually upgrades — bare command installs the latest release instead of just printing a manual command.
|
|
165
|
-
- **[0.5.14]** (2026-06-16) — `jeo --tmux` live-verification harness — repeatable stability + behavior checks.
|
|
166
166
|
|
|
167
167
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
168
168
|
<!-- CHANGELOG:END -->
|
package/README.zh.md
CHANGED
|
@@ -158,11 +158,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
|
|
|
158
158
|
## 更新日志 (Changelog)
|
|
159
159
|
|
|
160
160
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
161
|
+
- **[0.6.3]** (2026-06-16) — OAuth loopback reliability fix.
|
|
162
|
+
- **[0.6.2]** (2026-06-16) — Interactive `/provider` picker, clearer animated status + labeled block/prose boundaries, and a transient empty-response retry.
|
|
161
163
|
- **[0.6.1]** (2026-06-16) — Live reasoning progress (no more frozen "calling model"), thinking-level fixes for Anthropic/Antigravity, and input-box/Ctrl+O TUI fixes.
|
|
162
164
|
- **[0.6.0]** (2026-06-16) — TUI quality of life: durable input history (↑ recalls past queries across launches), clean `/resume` rendering, and a scrollable mid-turn Ctrl+O panel.
|
|
163
165
|
- **[0.5.16]** (2026-06-16) — `/resume` and Ctrl+O no longer corrupt the TUI — clean screen restore + scrollback expand.
|
|
164
|
-
- **[0.5.15]** (2026-06-16) — `jeo update` now actually upgrades — bare command installs the latest release instead of just printing a manual command.
|
|
165
|
-
- **[0.5.14]** (2026-06-16) — `jeo --tmux` live-verification harness — repeatable stability + behavior checks.
|
|
166
166
|
|
|
167
167
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
168
168
|
<!-- CHANGELOG:END -->
|
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@ import type { OAuthController, OAuthCredentials } from "./types";
|
|
|
11
11
|
import { generateState } from "./pkce";
|
|
12
12
|
|
|
13
13
|
const DEFAULT_TIMEOUT_MS = 300_000;
|
|
14
|
-
const DEFAULT_HOSTNAME = "
|
|
14
|
+
const DEFAULT_HOSTNAME = "127.0.0.1";
|
|
15
15
|
const DEFAULT_CALLBACK_PATH = "/callback";
|
|
16
16
|
|
|
17
17
|
export interface OAuthCallbackFlowOptions {
|
package/src/commands/launch.ts
CHANGED
|
@@ -45,6 +45,7 @@ import { SelectList, renderSelectList, type SelectItem } from "../tui/components
|
|
|
45
45
|
import {
|
|
46
46
|
formatModelLine,
|
|
47
47
|
formatProviderPanel,
|
|
48
|
+
emitLoginCleanup,
|
|
48
49
|
formatAgentsPanel,
|
|
49
50
|
formatAgentDetail,
|
|
50
51
|
formatConfigPanel,
|
|
@@ -55,7 +56,7 @@ import {
|
|
|
55
56
|
} from "../tui/components/config-panel";
|
|
56
57
|
import { liveModelPicker, renderLiveModelPicker, type ModelAssignmentBadge } from "../tui/components/live-model-picker";
|
|
57
58
|
|
|
58
|
-
import { providerPicker, renderProviderPicker } from "../tui/components/provider-picker";
|
|
59
|
+
import { providerPicker, renderProviderPicker, buildProviderChoices } from "../tui/components/provider-picker";
|
|
59
60
|
import { detectLanguage, languageLabel, parseLineRange, sliceLines, formatCodeBlock, formatDiff, sanitizeForTerminal } from "../tui/components/code-view";
|
|
60
61
|
import { categoryBadge } from "../tui/components/category-index";
|
|
61
62
|
import { renderInputFrame } from "../tui/components/input-box";
|
|
@@ -3631,7 +3632,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
3631
3632
|
}
|
|
3632
3633
|
if (input.startsWith("/provider") && (input === "/provider" || input[9] === " ")) {
|
|
3633
3634
|
const tokens = input.substring(9).trim().split(/\s+/).filter(Boolean);
|
|
3634
|
-
|
|
3635
|
+
let name = (tokens[0] ?? "").toLowerCase();
|
|
3635
3636
|
const explicitModel = tokens[1];
|
|
3636
3637
|
// `/provider login|auth [name]` → run OAuth login from the REPL.
|
|
3637
3638
|
if (name === "login" || name === "auth") {
|
|
@@ -3660,20 +3661,34 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
3660
3661
|
console.log(`Starting OAuth login for ${target}…`);
|
|
3661
3662
|
try {
|
|
3662
3663
|
const { email } = await interactiveOAuthLogin(target as AuthProvider, rl);
|
|
3663
|
-
|
|
3664
|
+
// Tidy back to the initial query-input screen: the OAuth flow printed
|
|
3665
|
+
// browser prompts / "waiting…" lines that clutter scrollback. Clear the
|
|
3666
|
+
// screen + scrollback and re-render the welcome (same path as /clear),
|
|
3667
|
+
// then a single concise confirmation. lastPickIndex is still seeded so
|
|
3668
|
+
// `/model #N` works; the verbose live-model dump is dropped (it cluttered).
|
|
3664
3669
|
const live = await refreshLiveModelsCache();
|
|
3665
3670
|
const after = (await describeAllProviders()).find(s => s.name === target);
|
|
3666
|
-
if (after) console.log(` status → ${after.name}: ${after.ready ? `✓ ${after.label}` : after.label}`);
|
|
3667
3671
|
const forProvider = live.filter(r => r.provider === target);
|
|
3668
|
-
if (forProvider.some(r => r.ok && r.models.length > 0))
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3672
|
+
if (forProvider.some(r => r.ok && r.models.length > 0)) lastPickIndex = flattenModels(forProvider);
|
|
3673
|
+
// Tidy back to the initial query-input screen: the OAuth flow printed
|
|
3674
|
+
// browser prompts / "waiting…" lines that clutter scrollback. emitLoginCleanup
|
|
3675
|
+
// clears + re-renders the welcome (same path as /clear) then prints one
|
|
3676
|
+
// confirmation; the verbose live-model dump is dropped (lastPickIndex is
|
|
3677
|
+
// still seeded so /model #N works). Orchestration is unit-tested.
|
|
3678
|
+
emitLoginCleanup(
|
|
3679
|
+
{
|
|
3680
|
+
clear: () => { disarmPreview(); process.stdout.write("\x1b[2J\x1b[3J\x1b[H"); },
|
|
3681
|
+
write: line => console.log(line),
|
|
3682
|
+
},
|
|
3683
|
+
{
|
|
3684
|
+
isTty: process.stdout.isTTY === true,
|
|
3685
|
+
provider: target,
|
|
3686
|
+
email,
|
|
3687
|
+
ready: after?.ready ?? false,
|
|
3688
|
+
label: after?.label,
|
|
3689
|
+
welcomeLines: renderWelcome(welcomeData),
|
|
3690
|
+
},
|
|
3691
|
+
);
|
|
3677
3692
|
} catch (err) {
|
|
3678
3693
|
console.log(`[FAILED] ${(err as Error).message} — or set ${target.toUpperCase()}_API_KEY.`);
|
|
3679
3694
|
}
|
|
@@ -3682,10 +3697,25 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
3682
3697
|
const cfgNow = await readGlobalConfig();
|
|
3683
3698
|
const statuses = await describeAllProviders(cfgNow);
|
|
3684
3699
|
if (!name) {
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3700
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
3701
|
+
// gjc-style interactive picker (grouped ready / needs-setup, current marked)
|
|
3702
|
+
// instead of a static text dump — arrows + Enter switch, Esc cancels.
|
|
3703
|
+
const current = (await describeModel(sessionModel || cfgNow.defaultModel, cfgNow)).provider;
|
|
3704
|
+
const items = buildProviderChoices(statuses, true, current).map(c => ({
|
|
3705
|
+
value: c.value as string,
|
|
3706
|
+
label: c.label,
|
|
3707
|
+
hint: c.hint,
|
|
3708
|
+
group: c.group,
|
|
3709
|
+
}));
|
|
3710
|
+
const picked = await pickFromOptions("Select a provider ↑↓ move · Enter switch · Esc cancel", items);
|
|
3711
|
+
if (!picked) { console.log("(switch cancelled)"); continue; }
|
|
3712
|
+
name = picked.toLowerCase(); // fall through to the switch logic below
|
|
3713
|
+
} else {
|
|
3714
|
+
console.log("Providers (credential · base URL):");
|
|
3715
|
+
logLines(formatProviderPanel(statuses));
|
|
3716
|
+
console.log("Switch with: /provider <name> [model] · choose models: /model");
|
|
3717
|
+
continue;
|
|
3718
|
+
}
|
|
3689
3719
|
}
|
|
3690
3720
|
if (!isProviderName(name)) {
|
|
3691
3721
|
console.log(`Unknown provider '${name}'. Known: ${statuses.map(s => s.name).join(", ")}.`);
|
package/src/tui/app.ts
CHANGED
|
@@ -21,7 +21,7 @@ import { evolutionTrack, createStageProgress, type StageProgress, transitionMess
|
|
|
21
21
|
import type { TaskSubEvent } from "../agent/task-tool";
|
|
22
22
|
import { supportsUnicode } from "./components/capability";
|
|
23
23
|
import { centerBlock, padLineTo, boxBlock, BOX_ASCII, BOX_UNICODE } from "./components/layout";
|
|
24
|
-
import { SECTION_GAP, stackSections } from "./components/section";
|
|
24
|
+
import { SECTION_GAP, sectionLabel, stackSections } from "./components/section";
|
|
25
25
|
import { resolveTheme, themeGradient, accentPaint, accentShadowPaint, diffPaint, mutedPaint, cardFillPaint } from "./components/themes";
|
|
26
26
|
import { detectColorLevel, animatedGradientText, ColorLevel } from "./components/color";
|
|
27
27
|
import { formatForgeBox, summarizeForgeInvocation, summarizeForgeResult, fitForgeBoxes, webSearchCardLines, type ForgeSummary } from "./components/forge";
|
|
@@ -414,12 +414,21 @@ export class LaunchTui {
|
|
|
414
414
|
this.thinking = false; // model replied; now dispatching the tool
|
|
415
415
|
this.retryNotice = null; // the call got through — clear any backoff notice
|
|
416
416
|
// Flush the streamed reasoning once into scrollback as a jeo-ref reasoning
|
|
417
|
-
// block —
|
|
418
|
-
//
|
|
417
|
+
// block — a muted "Reasoning" divider header, the prose below it (the durable
|
|
418
|
+
// record) — then stop showing the transient live reasoning row.
|
|
419
419
|
if (this.streamingReasoning && this.streamingReasoning !== this.flushedReasoning) {
|
|
420
420
|
this.flushedReasoning = this.streamingReasoning;
|
|
421
|
-
|
|
422
|
-
|
|
421
|
+
// A muted "Reasoning" card-header divider announces the reasoning block
|
|
422
|
+
// boundary (consistent with the section design tokens — Thinking/Reasoning/
|
|
423
|
+
// Output share one visual language); the prose below it is the durable record.
|
|
424
|
+
// A full-width divider ROW respects appendLedger's 1-line=1-row pre-wrap
|
|
425
|
+
// invariant (no per-line prefix), unlike a left-border enclosure which would
|
|
426
|
+
// push wrapped lines past `cols` and tear the frame.
|
|
427
|
+
const header = sectionLabel("Reasoning", Math.max(20, size().cols), {
|
|
428
|
+
color: this.theme.color,
|
|
429
|
+
unicode: this.unicode,
|
|
430
|
+
});
|
|
431
|
+
this.appendLedger(`${header}\n${this.streamingReasoning}\n`, "reasoning");
|
|
423
432
|
}
|
|
424
433
|
this.streamingReasoning = "";
|
|
425
434
|
this.streamingThought = "";
|
|
@@ -1236,7 +1245,7 @@ export class LaunchTui {
|
|
|
1236
1245
|
// (duplicate model bar) is gone; height now toggles only at lifecycle boundaries.
|
|
1237
1246
|
const ROWS = 6;
|
|
1238
1247
|
const shown = wrapped.slice(-ROWS);
|
|
1239
|
-
tail.push(
|
|
1248
|
+
tail.push(sectionLabel("Thinking", Math.max(8, Math.min(120, cols)), { color: this.theme.color, unicode: this.unicode }));
|
|
1240
1249
|
for (let k = 0; k < ROWS - shown.length; k++) tail.push("");
|
|
1241
1250
|
for (const l of shown) tail.push(dim(` ${l}`));
|
|
1242
1251
|
tail.push("");
|
|
@@ -1255,7 +1264,7 @@ export class LaunchTui {
|
|
|
1255
1264
|
// so cumulative stdout growth does not thrash the frame height.
|
|
1256
1265
|
const ROWS = 8;
|
|
1257
1266
|
const shown = wrapped.slice(-ROWS);
|
|
1258
|
-
tail.push(
|
|
1267
|
+
tail.push(sectionLabel("Output", Math.max(8, Math.min(120, cols)), { color: this.theme.color, unicode: this.unicode }));
|
|
1259
1268
|
for (let k = 0; k < ROWS - shown.length; k++) tail.push("");
|
|
1260
1269
|
for (const l of shown) tail.push(dim(` ${l}`));
|
|
1261
1270
|
tail.push("");
|
|
@@ -46,6 +46,35 @@ export function formatProviderPanel(statuses: ProviderStatus[]): string[] {
|
|
|
46
46
|
});
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/** Side-effect sinks for {@link emitLoginCleanup} — injected so the orchestration
|
|
50
|
+
* (ordering + conditionals + the confirmation string) is unit-testable without a
|
|
51
|
+
* real terminal. The caller wires `clear` to the actual screen+scrollback escape. */
|
|
52
|
+
export interface LoginCleanupIO {
|
|
53
|
+
clear: () => void;
|
|
54
|
+
write: (line: string) => void;
|
|
55
|
+
}
|
|
56
|
+
export interface LoginCleanupOpts {
|
|
57
|
+
isTty: boolean;
|
|
58
|
+
provider: string;
|
|
59
|
+
email?: string;
|
|
60
|
+
ready: boolean;
|
|
61
|
+
label?: string;
|
|
62
|
+
welcomeLines: string[];
|
|
63
|
+
}
|
|
64
|
+
/** Tidy the screen after a successful `/provider login`: on a TTY clear the screen
|
|
65
|
+
* and re-render the welcome (drops the OAuth flow's browser/"waiting…" noise — the
|
|
66
|
+
* same path `/clear` uses), then print ONE confirmation line; off a TTY just the
|
|
67
|
+
* confirmation. Pure orchestration over the injected IO. */
|
|
68
|
+
export function emitLoginCleanup(io: LoginCleanupIO, opts: LoginCleanupOpts): void {
|
|
69
|
+
if (opts.isTty) {
|
|
70
|
+
io.clear();
|
|
71
|
+
io.write(opts.welcomeLines.join("\n"));
|
|
72
|
+
}
|
|
73
|
+
const who = opts.email ? ` (${opts.email})` : "";
|
|
74
|
+
const status = !opts.ready && opts.label ? ` — ${opts.label}` : "";
|
|
75
|
+
io.write(`✓ Logged in to ${opts.provider}${who}${status}. Pick a model with /model.`);
|
|
76
|
+
}
|
|
77
|
+
|
|
49
78
|
/** Subagent roster: ` id title — model · thinking ≤N steps (read-only)`. */
|
|
50
79
|
export function formatAgentsPanel(
|
|
51
80
|
roles: readonly SubagentRole[],
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ColorLevel, hexToRgb, lerpColor, fgEscape, bgEscape, resetEscape, stripAnsi } from "./color";
|
|
1
2
|
import chalk from "chalk";
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -33,10 +34,10 @@ export const EVOLUTION_STAGE_COLORS: readonly ((s: string) => string)[] = [
|
|
|
33
34
|
/** Spinner frame sets, one per evolution stage. */
|
|
34
35
|
export const EVOLUTION_SPINNER_FRAMES: readonly string[][] = [
|
|
35
36
|
[". ", ".. ", "... ", "....", "... ", ".. "],
|
|
36
|
-
["\u2801", "\
|
|
37
|
-
["
|
|
37
|
+
["\u2801", "\u2803", "\u2807", "\u2827", "\u2837", "\u283f", "\u283e", "\u283d", "\u283b", "\u2819", "\u2818", "\u2810"],
|
|
38
|
+
["▲", "▶", "▼", "◀"],
|
|
38
39
|
["\u280b", "\u2819", "\u2839", "\u2838", "\u283c", "\u2834", "\u2826", "\u2827", "\u2807", "\u280f"],
|
|
39
|
-
["\
|
|
40
|
+
["\u25f0", "\u25f3", "\u25f2", "\u25f1"],
|
|
40
41
|
];
|
|
41
42
|
|
|
42
43
|
/**
|
|
@@ -303,3 +304,81 @@ export const EVOLUTION_TRANSITION_MESSAGES: readonly string[] = [
|
|
|
303
304
|
export function transitionMessage(index: number): string {
|
|
304
305
|
return EVOLUTION_TRANSITION_MESSAGES[clampStageIndex(index)]!;
|
|
305
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* Applies a dimensional, animated gradient (foreground or background) to a string of text.
|
|
309
|
+
* Driven by a phaseMs timestamp, it creates a slow, tasteful shimmer wave
|
|
310
|
+
* and adds depth cues (bright core, dimmer edges).
|
|
311
|
+
*/
|
|
312
|
+
export function applyDimensionalGradient(
|
|
313
|
+
text: string,
|
|
314
|
+
timeMs: number,
|
|
315
|
+
fromHex: string,
|
|
316
|
+
toHex: string,
|
|
317
|
+
level: ColorLevel,
|
|
318
|
+
isBg: boolean,
|
|
319
|
+
fgHex: string = "#ebebeb"
|
|
320
|
+
): string {
|
|
321
|
+
const plain = stripAnsi(text);
|
|
322
|
+
if (level === ColorLevel.None || plain.length === 0) return plain;
|
|
323
|
+
|
|
324
|
+
const from = hexToRgb(fromHex);
|
|
325
|
+
const to = hexToRgb(toHex);
|
|
326
|
+
const fg = hexToRgb(fgHex);
|
|
327
|
+
|
|
328
|
+
const L = plain.length;
|
|
329
|
+
let out = "";
|
|
330
|
+
if (isBg) {
|
|
331
|
+
out += fgEscape(fg, level);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Slow period: 4.5 seconds for a complete wave cycle
|
|
335
|
+
const p = (timeMs / 4500) % 1;
|
|
336
|
+
|
|
337
|
+
for (let i = 0; i < L; i++) {
|
|
338
|
+
const ch = plain[i]!;
|
|
339
|
+
if (!isBg && ch === " ") {
|
|
340
|
+
out += ch;
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const x = L > 1 ? i / (L - 1) : 0;
|
|
345
|
+
const base = lerpColor(from, to, x);
|
|
346
|
+
|
|
347
|
+
// 3D depth cue: bright center (1.0), dimmer edges (0.75)
|
|
348
|
+
// Using a parabolic/sin curve
|
|
349
|
+
const depth = 0.75 + 0.25 * Math.sin(x * Math.PI);
|
|
350
|
+
|
|
351
|
+
// Shimmer highlight wave
|
|
352
|
+
// Shortest circular distance between position x and wave phase p
|
|
353
|
+
let dist = Math.abs(x - p);
|
|
354
|
+
if (dist > 0.5) dist = 1 - dist;
|
|
355
|
+
|
|
356
|
+
const waveWidth = 0.22;
|
|
357
|
+
const highlight = dist < waveWidth ? Math.cos((dist / waveWidth) * (Math.PI / 2)) : 0;
|
|
358
|
+
|
|
359
|
+
// Apply depth
|
|
360
|
+
let r = base.r * depth;
|
|
361
|
+
let g = base.g * depth;
|
|
362
|
+
let b = base.b * depth;
|
|
363
|
+
|
|
364
|
+
// Apply shimmer highlight (blending towards white)
|
|
365
|
+
const shimmerIntensity = 0.45;
|
|
366
|
+
r += (255 - r) * highlight * shimmerIntensity;
|
|
367
|
+
g += (255 - g) * highlight * shimmerIntensity;
|
|
368
|
+
b += (255 - b) * highlight * shimmerIntensity;
|
|
369
|
+
|
|
370
|
+
const rgb = {
|
|
371
|
+
r: Math.max(0, Math.min(255, Math.round(r))),
|
|
372
|
+
g: Math.max(0, Math.min(255, Math.round(g))),
|
|
373
|
+
b: Math.max(0, Math.min(255, Math.round(b))),
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
if (isBg) {
|
|
377
|
+
out += bgEscape(rgb, level) + ch;
|
|
378
|
+
} else {
|
|
379
|
+
out += fgEscape(rgb, level) + ch;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return out + resetEscape(level);
|
|
384
|
+
}
|
|
@@ -419,8 +419,8 @@ export function summarizeForgeResult(tool: string, success: boolean, output: str
|
|
|
419
419
|
}
|
|
420
420
|
}
|
|
421
421
|
const lines = previewLines(body, success ? 5 : 10, success ? 600 : 1200);
|
|
422
|
+
lines.unshift(forgeDivider("Output"));
|
|
422
423
|
if (normalized === "bash") {
|
|
423
|
-
lines.unshift(forgeDivider("Output"));
|
|
424
424
|
if (exitNote) lines.push("", exitNote);
|
|
425
425
|
}
|
|
426
426
|
return {
|
|
@@ -74,33 +74,85 @@ export function renderMarkdownAnsi(text: string, opts: MarkdownAnsiOptions = {})
|
|
|
74
74
|
|
|
75
75
|
const out: string[] = [];
|
|
76
76
|
let inFence = false;
|
|
77
|
+
|
|
78
|
+
// Track the type of the last pushed content line to manage rhythm
|
|
79
|
+
// Types: "empty" | "heading" | "blockquote" | "list" | "code" | "prose"
|
|
80
|
+
let lastType: "empty" | "heading" | "blockquote" | "list" | "code" | "prose" = "empty";
|
|
81
|
+
|
|
82
|
+
const ensureBlankLine = () => {
|
|
83
|
+
if (out.length > 0 && out[out.length - 1] !== "") {
|
|
84
|
+
out.push("");
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
77
88
|
for (const line of text.split("\n")) {
|
|
78
|
-
|
|
89
|
+
const trimmed = line.trim();
|
|
90
|
+
|
|
91
|
+
// Check if we are toggling a code fence
|
|
92
|
+
if (/^```/.test(trimmed)) {
|
|
93
|
+
ensureBlankLine();
|
|
79
94
|
inFence = !inFence;
|
|
80
|
-
|
|
95
|
+
lastType = inFence ? "code" : "empty";
|
|
96
|
+
continue;
|
|
81
97
|
}
|
|
98
|
+
|
|
82
99
|
if (inFence) {
|
|
83
|
-
out.push(line); // code bodies verbatim
|
|
100
|
+
out.push(line); // code bodies verbatim
|
|
101
|
+
lastType = "code";
|
|
84
102
|
continue;
|
|
85
103
|
}
|
|
104
|
+
|
|
105
|
+
if (trimmed === "") {
|
|
106
|
+
ensureBlankLine();
|
|
107
|
+
lastType = "empty";
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
86
111
|
const heading = line.match(/^(#{1,6})\s+(.+)$/);
|
|
87
112
|
if (heading) {
|
|
88
|
-
|
|
89
|
-
// breathing room above it (final-report readability), never a leading blank.
|
|
90
|
-
if (out.length > 0 && out[out.length - 1]!.trim() !== "") out.push("");
|
|
113
|
+
ensureBlankLine();
|
|
91
114
|
out.push(accent(styleInline(heading[2]!)));
|
|
115
|
+
lastType = "heading";
|
|
92
116
|
continue;
|
|
93
117
|
}
|
|
118
|
+
|
|
94
119
|
const quote = line.match(/^>\s+(.+)$/);
|
|
95
120
|
if (quote) {
|
|
121
|
+
if (lastType !== "blockquote" && lastType !== "empty") {
|
|
122
|
+
ensureBlankLine();
|
|
123
|
+
}
|
|
96
124
|
out.push(chalk.dim(`▎ ${styleInline(quote[1]!)}`));
|
|
125
|
+
lastType = "blockquote";
|
|
97
126
|
continue;
|
|
98
127
|
}
|
|
128
|
+
|
|
99
129
|
if (/^[-\*_]{3,}\s*$/.test(line)) {
|
|
130
|
+
ensureBlankLine();
|
|
100
131
|
out.push(chalk.dim("─".repeat(24)));
|
|
132
|
+
ensureBlankLine();
|
|
133
|
+
lastType = "empty";
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Check if list item
|
|
138
|
+
const isList = /^\s*([*\-+]\s|\d+\.\s)/.test(line);
|
|
139
|
+
if (isList) {
|
|
140
|
+
if (lastType !== "list" && lastType !== "empty") {
|
|
141
|
+
ensureBlankLine();
|
|
142
|
+
}
|
|
143
|
+
out.push(styleInline(line));
|
|
144
|
+
lastType = "list";
|
|
101
145
|
continue;
|
|
102
146
|
}
|
|
147
|
+
|
|
148
|
+
// Default prose line
|
|
149
|
+
if (lastType !== "prose" && lastType !== "empty" && lastType !== "list" && lastType !== "heading") {
|
|
150
|
+
// Transitioning from list/quote/code to prose -> ensure blank line
|
|
151
|
+
ensureBlankLine();
|
|
152
|
+
}
|
|
103
153
|
out.push(styleInline(line));
|
|
154
|
+
lastType = "prose";
|
|
104
155
|
}
|
|
156
|
+
|
|
105
157
|
return out.join("\n").trim();
|
|
106
158
|
}
|
|
@@ -16,14 +16,16 @@ export function providerHint(s: ProviderStatus, unicode = true): string {
|
|
|
16
16
|
return parts.join(" \u00b7 ");
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
/** Build provider choices, ready providers first (stable within each group).
|
|
20
|
-
|
|
19
|
+
/** Build provider choices, ready providers first (stable within each group). When
|
|
20
|
+
* `current` is given, that provider's row is marked `· ● current` so the active
|
|
21
|
+
* provider is obvious in the picker (gjc-style). */
|
|
22
|
+
export function buildProviderChoices(statuses: ProviderStatus[], unicode = true, current?: ProviderName): SelectItem<ProviderName>[] {
|
|
21
23
|
const sorted = [...statuses].sort((a, b) => (a.ready === b.ready ? 0 : a.ready ? -1 : 1));
|
|
22
24
|
return sorted.map(s => ({
|
|
23
25
|
value: s.name,
|
|
24
26
|
label: `${s.name} (${companyLabel(s.name)})`,
|
|
25
27
|
group: s.ready ? "ready" : "needs setup",
|
|
26
|
-
hint: providerHint(s, unicode),
|
|
28
|
+
hint: s.name === current ? `${providerHint(s, unicode)} · ${unicode ? "●" : "*"} current` : providerHint(s, unicode),
|
|
27
29
|
}));
|
|
28
30
|
}
|
|
29
31
|
|
|
@@ -29,7 +29,10 @@ export interface SectionLabelOpts {
|
|
|
29
29
|
export function sectionLabel(title: string, width: number, opts: SectionLabelOpts = {}): string {
|
|
30
30
|
const unicode = opts.unicode !== false;
|
|
31
31
|
const dash = unicode ? "─" : "-";
|
|
32
|
-
|
|
32
|
+
// A 3-dash lead-in makes the card header read as a distinct boundary (stronger than a
|
|
33
|
+
// single rule) without becoming loud.
|
|
34
|
+
const startDash = unicode ? "───" : "---";
|
|
35
|
+
const head = `${startDash} ${title.trim()} `;
|
|
33
36
|
const headW = visibleWidth(head);
|
|
34
37
|
const fill = Math.max(0, Math.trunc(width) - headW);
|
|
35
38
|
const line = head + dash.repeat(fill);
|
|
@@ -59,12 +62,23 @@ export function stackSections(sections: Section[], opts: StackOptions): string[]
|
|
|
59
62
|
const gap = Math.max(0, opts.gap ?? SECTION_GAP);
|
|
60
63
|
const out: string[] = [];
|
|
61
64
|
for (const section of sections) {
|
|
62
|
-
|
|
63
|
-
|
|
65
|
+
const lines = [...section.lines];
|
|
66
|
+
while (lines.length && lines[0] === "") {
|
|
67
|
+
lines.shift();
|
|
68
|
+
}
|
|
69
|
+
while (lines.length && lines[lines.length - 1] === "") {
|
|
70
|
+
lines.pop();
|
|
71
|
+
}
|
|
72
|
+
if (!lines.length) continue;
|
|
73
|
+
if (out.length) {
|
|
74
|
+
for (let i = 0; i < gap; i++) out.push("");
|
|
75
|
+
}
|
|
64
76
|
if (section.title) {
|
|
65
77
|
out.push(sectionLabel(section.title, opts.width, { color: opts.color, unicode: opts.unicode }));
|
|
66
78
|
}
|
|
67
|
-
for (const line of
|
|
79
|
+
for (const line of lines) {
|
|
80
|
+
out.push(line);
|
|
81
|
+
}
|
|
68
82
|
}
|
|
69
83
|
return out;
|
|
70
84
|
}
|
|
@@ -4,6 +4,7 @@ import { spinnerFramesFor, stageIndexForStep, clampStageIndex } from "./evolutio
|
|
|
4
4
|
* Stage-aware spinner. Frames evolve with the agent's step against its budget,
|
|
5
5
|
* sourced from the canonical evolution model so the spinner stays in lockstep
|
|
6
6
|
* with the ASCII art, meter, and footer track.
|
|
7
|
+
* Enriched with 3D/dimensional quadrant and twisting braille helix elements.
|
|
7
8
|
*/
|
|
8
9
|
export class Spinner {
|
|
9
10
|
private defaultFrames: string[];
|
|
@@ -5,6 +5,7 @@ import { animatedGradientText, applyBgGradient, hexToRgb, visibleWidth, ColorLev
|
|
|
5
5
|
import * as os from "node:os";
|
|
6
6
|
import { formatUsage } from "./duration";
|
|
7
7
|
import { formatCost } from "../../ai/pricing";
|
|
8
|
+
import { applyDimensionalGradient, stageGradient, stageIndexForStep } from "./evolution";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* One-row status bar pinned directly above the boxed input (gjc-layout parity):
|
|
@@ -100,7 +101,7 @@ export function renderStatusBar(d: StatusBarData): string {
|
|
|
100
101
|
const level = d.colorLevel ?? (useColor ? ColorLevel.TrueColor : ColorLevel.None);
|
|
101
102
|
const grad = d.gradient ?? { from: "#0a3d62", to: "#48dbfb" };
|
|
102
103
|
const paintedLeft = useColor
|
|
103
|
-
?
|
|
104
|
+
? applyDimensionalGradient(left, Date.now(), grad.from, grad.to, level, true)
|
|
104
105
|
: left;
|
|
105
106
|
return `${paintedLeft}${" ".repeat(gap)}${right}`;
|
|
106
107
|
}
|
|
@@ -151,9 +152,19 @@ export function renderJeoStatus(data: JeoStatusData): string[] {
|
|
|
151
152
|
const elapsed = `${seconds(data.elapsedMs)}s`;
|
|
152
153
|
let msg = data.message ?? "thinking through the next tool call";
|
|
153
154
|
const level = data.colorLevel ?? (useColor ? ColorLevel.TrueColor : ColorLevel.None);
|
|
154
|
-
if (useColor &&
|
|
155
|
-
|
|
156
|
-
|
|
155
|
+
if (useColor && level !== ColorLevel.None && (data.isThinking !== false)) {
|
|
156
|
+
let fromHex = "#0a3d62";
|
|
157
|
+
let toHex = "#48dbfb";
|
|
158
|
+
if (data.palette && data.palette.length > 0) {
|
|
159
|
+
fromHex = data.palette[0]!;
|
|
160
|
+
toHex = data.palette[data.palette.length - 1]!;
|
|
161
|
+
} else {
|
|
162
|
+
const stageIdx = stageIndexForStep(data.step ?? 0, data.maxSteps ?? 25);
|
|
163
|
+
const grad = stageGradient(stageIdx);
|
|
164
|
+
fromHex = grad.from;
|
|
165
|
+
toHex = grad.to;
|
|
166
|
+
}
|
|
167
|
+
msg = applyDimensionalGradient(msg, Date.now(), fromHex, toHex, level, false);
|
|
157
168
|
}
|
|
158
169
|
const current = data.currentTool ? `forging ${data.currentTool}` : "forge idle";
|
|
159
170
|
const stage = data.stage ? `${data.stage} · ` : "";
|
|
@@ -260,8 +271,19 @@ export function renderStatusBox(data: StatusBoxData): string[] {
|
|
|
260
271
|
activity = cut + (unicode ? "…" : "...");
|
|
261
272
|
}
|
|
262
273
|
const plainActivityWidth = visibleWidth(activity);
|
|
263
|
-
if (useColor &&
|
|
264
|
-
|
|
274
|
+
if (useColor && level !== ColorLevel.None && (data.isThinking !== false)) {
|
|
275
|
+
let fromHex = "#0a3d62";
|
|
276
|
+
let toHex = "#48dbfb";
|
|
277
|
+
if (data.palette && data.palette.length > 0) {
|
|
278
|
+
fromHex = data.palette[0]!;
|
|
279
|
+
toHex = data.palette[data.palette.length - 1]!;
|
|
280
|
+
} else {
|
|
281
|
+
const stageIdx = stageIndexForStep(data.step ?? 0, data.maxSteps ?? 25);
|
|
282
|
+
const grad = stageGradient(stageIdx);
|
|
283
|
+
fromHex = grad.from;
|
|
284
|
+
toHex = grad.to;
|
|
285
|
+
}
|
|
286
|
+
activity = applyDimensionalGradient(activity, Date.now(), fromHex, toHex, level, false);
|
|
265
287
|
}
|
|
266
288
|
const escPad = esc
|
|
267
289
|
? " ".repeat(Math.max(1, cols - 1 - visibleWidth(headPlain) - plainActivityWidth - visibleWidth(esc))) + dim(esc)
|
|
@@ -131,6 +131,9 @@ export function formatTranscript(messages: readonly Message[], opts: TranscriptO
|
|
|
131
131
|
: [];
|
|
132
132
|
const toolCalls = calls.filter(c => c.tool !== "done");
|
|
133
133
|
if (toolCalls.length > 0) {
|
|
134
|
+
if (lines.length > 0 && lines[lines.length - 1] !== "") {
|
|
135
|
+
lines.push("");
|
|
136
|
+
}
|
|
134
137
|
// The matching `Tool [x] result (ok|fail)` user message follows; for a batch it
|
|
135
138
|
// is ONE message with several blocks. Parse verdicts in call order.
|
|
136
139
|
const next = messages[i + 1];
|
|
@@ -153,6 +156,9 @@ export function formatTranscript(messages: readonly Message[], opts: TranscriptO
|
|
|
153
156
|
? ""
|
|
154
157
|
: m.content;
|
|
155
158
|
if (!reason.trim()) continue;
|
|
159
|
+
if (lines.length > 0 && lines[lines.length - 1] !== "") {
|
|
160
|
+
lines.push("");
|
|
161
|
+
}
|
|
156
162
|
lines.push(`${magentaBold(`jeo ${jeoMark}`)}`);
|
|
157
163
|
lines.push(...clipBody(reason.trim(), bodyCap));
|
|
158
164
|
}
|
package/src/util/retry.ts
CHANGED
|
@@ -61,6 +61,15 @@ export function defaultRetryable(err: unknown): boolean {
|
|
|
61
61
|
return true;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
// Transient empty 200s — a provider returned a successful response with no content.
|
|
65
|
+
// This is a known intermittent failure (load/edge races on Anthropic/Gemini/OpenAI), so
|
|
66
|
+
// retry it like an overload instead of letting one empty reply drop the turn. EXCEPTION:
|
|
67
|
+
// deterministic budget exhaustion (max_tokens / length / "output budget exhausted") re-empties
|
|
68
|
+
// on every retry — fail fast so the caller sees the raise-maxTokens/lower-thinking hint.
|
|
69
|
+
if (lowerMessage.includes("returned no content")) {
|
|
70
|
+
return !/max_tokens|max_output_tokens|finish_reason=length|done_reason=length|output budget exhausted/.test(lowerMessage);
|
|
71
|
+
}
|
|
72
|
+
|
|
64
73
|
// Numeric `.status` field (structured provider errors, fetch responses).
|
|
65
74
|
if (typeof err === "object" && err !== null) {
|
|
66
75
|
const status = (err as any).status;
|