jeo-code 0.6.26 → 0.6.28
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 +25 -0
- package/README.ja.md +2 -6
- package/README.ko.md +2 -6
- package/README.md +2 -6
- package/README.zh.md +2 -6
- package/package.json +1 -1
- package/src/agent/compaction.ts +10 -1
- package/src/agent/engine.ts +62 -16
- package/src/agent/loop.ts +3 -0
- package/src/ai/model-manager.ts +6 -8
- package/src/ai/providers/anthropic.ts +114 -21
- package/src/ai/providers/antigravity.ts +6 -0
- package/src/ai/providers/errors.ts +18 -0
- package/src/ai/providers/gemini.ts +84 -28
- package/src/ai/providers/openai-compatible-catalog.ts +10 -4
- package/src/ai/providers/openai-responses.ts +76 -19
- package/src/ai/types.ts +55 -2
- package/src/commands/launch/flags.ts +5 -2
- package/src/commands/launch.ts +119 -25
- package/src/tui/app.ts +38 -6
- package/src/tui/components/ascii-art.ts +38 -45
package/src/commands/launch.ts
CHANGED
|
@@ -36,7 +36,7 @@ import { callLlm, type Message } from "../agent/loop";
|
|
|
36
36
|
import { friendlyProviderError } from "../util/provider-error";
|
|
37
37
|
import { readGlobalConfig, saveConfigPatch } from "../agent/state";
|
|
38
38
|
import { rememberModelPatch, recentModelsForDisplay } from "../agent/model-recency";
|
|
39
|
-
import { describeModel, describeAllProviders, thinkingMaxTokens, thinkingToReasoningEffort, discoverModels, flattenModels, resolveSelection, catalogMetadata, resolveRoleModel, CODEX_MODELS, qualifyModelId } from "../ai";
|
|
39
|
+
import { describeModel, describeAllProviders, thinkingMaxTokens, thinkingToReasoningEffort, discoverModels, flattenModels, resolveSelection, catalogMetadata, catalogByProvider, resolveRoleModel, CODEX_MODELS, qualifyModelId } from "../ai";
|
|
40
40
|
import type { ProviderModelsResult, PickEntry, ProviderName, ModelRole, ThinkLevel } from "../ai";
|
|
41
41
|
import { readGoalState, writeGoalState, clearGoalState, verifyGoal } from "../agent/goal-verifier";
|
|
42
42
|
|
|
@@ -232,6 +232,42 @@ function providerDefaultModel(p: ProviderName): string {
|
|
|
232
232
|
return openaiCompatDef(p)?.defaultModel ?? STATIC_PROVIDER_DEFAULT[p] ?? "";
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
+
/**
|
|
236
|
+
* Pick-list entries for ONE provider, with static fallbacks so the list is never
|
|
237
|
+
* empty.
|
|
238
|
+
*
|
|
239
|
+
* Live discovery yields ids only for a logged-in, reachable provider, and
|
|
240
|
+
* `catalogOr` backfills the static catalog ONLY for OAuth sources — so an
|
|
241
|
+
* API-key/keyless provider that isn't configured yet had an empty list and was
|
|
242
|
+
* silently pinned to a bare default. Prefer live ids; else the provider's
|
|
243
|
+
* capability catalog; else its single known default model (all 24 OpenAI-compat
|
|
244
|
+
* providers carry one) so the user always sees at least one pickable id.
|
|
245
|
+
*/
|
|
246
|
+
export function providerPickEntries(live: ProviderModelsResult[], want: ProviderName): PickEntry[] {
|
|
247
|
+
const fromLive = flattenModels(live.filter(r => r.provider === want));
|
|
248
|
+
if (fromLive.length) return fromLive;
|
|
249
|
+
const catalog = catalogByProvider(want);
|
|
250
|
+
if (catalog.length) {
|
|
251
|
+
return catalog.map((m, i) => ({ index: i + 1, provider: want, model: qualifyModelId(m.providerModel, want) }));
|
|
252
|
+
}
|
|
253
|
+
// Offline fallback for catalog-less (OpenAI-compatible) providers: the def's
|
|
254
|
+
// defaultModel first, then its knownModels list, so the per-role provider picker
|
|
255
|
+
// shows several pickable ids instead of one. De-duped + provider-qualified.
|
|
256
|
+
const def = openaiCompatDef(want);
|
|
257
|
+
if (def) {
|
|
258
|
+
const ids = [def.defaultModel, ...(def.knownModels ?? [])].map(m => qualifyModelId(m, want));
|
|
259
|
+
const seen = new Set<string>();
|
|
260
|
+
const entries: PickEntry[] = [];
|
|
261
|
+
for (const model of ids) {
|
|
262
|
+
if (seen.has(model)) continue;
|
|
263
|
+
seen.add(model);
|
|
264
|
+
entries.push({ index: entries.length + 1, provider: want, model });
|
|
265
|
+
}
|
|
266
|
+
if (entries.length) return entries;
|
|
267
|
+
}
|
|
268
|
+
const fallback = providerDefaultModel(want);
|
|
269
|
+
return fallback ? [{ index: 1, provider: want, model: qualifyModelId(fallback, want) }] : [];
|
|
270
|
+
}
|
|
235
271
|
|
|
236
272
|
export function formatResumeHint(sessionId: string): string {
|
|
237
273
|
return `Resume with: jeo launch --resume ${sessionId}`;
|
|
@@ -487,6 +523,10 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
487
523
|
// answer). Captured from the reasoning stream and persisted on the assistant message so
|
|
488
524
|
// it survives /resume + export (gjc "think → answer" record). Reset at each turn start.
|
|
489
525
|
let lastTurnReasoning = "";
|
|
526
|
+
// Native reasoning artifacts for the FINAL (done) step — the engine attaches intermediate
|
|
527
|
+
// steps' artifacts to their own pushed messages, but the done turn is built here. Reset on
|
|
528
|
+
// each step boundary so only the last step's artifacts ride the final reply (no duplication).
|
|
529
|
+
let lastTurnArtifacts: import("../ai/types").ReasoningArtifact[] = [];
|
|
490
530
|
/** Wrap turn events so EVERY sink (TUI or plain stream) records the last full
|
|
491
531
|
* tool output for the Ctrl+O detail view. */
|
|
492
532
|
const withToolDetailCapture = (base: ReturnType<LaunchTui["events"]>): ReturnType<LaunchTui["events"]> => ({
|
|
@@ -495,12 +535,22 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
495
535
|
lastToolDetail = { tool, output };
|
|
496
536
|
base.onToolResult?.(tool, success, output);
|
|
497
537
|
},
|
|
538
|
+
onStep: (step: number) => {
|
|
539
|
+
// New step: drop the prior step's final-reply artifacts so only the LAST step's ride
|
|
540
|
+
// the done reply (intermediate steps are persisted by the engine on their own turns).
|
|
541
|
+
lastTurnArtifacts = [];
|
|
542
|
+
base.onStep?.(step);
|
|
543
|
+
},
|
|
498
544
|
onReasoningStream: (textSoFar: string) => {
|
|
499
545
|
// textSoFar is the cumulative thought for the current step; keep the latest
|
|
500
546
|
// non-empty value (the thought immediately preceding the turn's answer).
|
|
501
547
|
if (textSoFar.trim()) lastTurnReasoning = textSoFar;
|
|
502
548
|
base.onReasoningStream?.(textSoFar);
|
|
503
549
|
},
|
|
550
|
+
onReasoningArtifactStream: (artifact) => {
|
|
551
|
+
lastTurnArtifacts.push(artifact);
|
|
552
|
+
base.onReasoningArtifactStream?.(artifact);
|
|
553
|
+
},
|
|
504
554
|
});
|
|
505
555
|
/** Compose a session-persistence flush into onStep so each completed step is
|
|
506
556
|
* written as it lands (durability across mid-turn interruption) without
|
|
@@ -603,6 +653,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
603
653
|
// persistence block below.
|
|
604
654
|
let beforeLen = history.length;
|
|
605
655
|
lastTurnReasoning = ""; // fresh turn: capture this turn's thinking from scratch
|
|
656
|
+
lastTurnArtifacts = [];
|
|
606
657
|
// Incremental session persistence (durability across mid-turn interruption):
|
|
607
658
|
// persistTurnTail() flushes history messages added since the last flush — called
|
|
608
659
|
// right after the user prompt, on every onStep boundary, and once post-turn — so
|
|
@@ -906,9 +957,9 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
906
957
|
// this only covers the tail — net content is the full turn either way.
|
|
907
958
|
try {
|
|
908
959
|
await persistTurnTail();
|
|
909
|
-
const assistantMsg: Message =
|
|
910
|
-
|
|
911
|
-
|
|
960
|
+
const assistantMsg: Message = { role: "assistant", content: reply };
|
|
961
|
+
if (lastTurnReasoning.trim()) assistantMsg.reasoning = lastTurnReasoning;
|
|
962
|
+
if (lastTurnArtifacts.length) assistantMsg.reasoningArtifacts = lastTurnArtifacts;
|
|
912
963
|
history.push(assistantMsg);
|
|
913
964
|
if (sessionId) await appendMessage(sessionId, assistantMsg, cwd);
|
|
914
965
|
if (tui) tui.finish(reply);
|
|
@@ -1593,7 +1644,15 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1593
1644
|
jeoEnv("NO_SLASH_PREVIEW") !== "1";
|
|
1594
1645
|
// Footer height reserved by the CURRENTLY armed region; disarm/draw must use the
|
|
1595
1646
|
// same value the arm computed, even if the terminal was resized in between.
|
|
1647
|
+
// `footerRows` is the MAX reservation height (the budget previewLines/historyPreview
|
|
1648
|
+
// may fill). The PHYSICAL reservation (`footerRendered`) is now dynamic: compact at
|
|
1649
|
+
// idle (no dropdown) so a finished/idle prompt leaves NO reserved blank rows, and
|
|
1650
|
+
// grown on demand when a slash/arg preview needs more. `footerWantRows` is the height
|
|
1651
|
+
// the latest previewLines/historyPreview wants; drawFooter re-pins to it in place.
|
|
1596
1652
|
let footerRows = MAX_PREVIEW_ROWS;
|
|
1653
|
+
// Compact idle reservation: status bar (1) + spacer (1) + input box (3 rows).
|
|
1654
|
+
const COMPACT_FOOTER_ROWS = 5;
|
|
1655
|
+
let footerWantRows = COMPACT_FOOTER_ROWS;
|
|
1597
1656
|
const out = process.stdout;
|
|
1598
1657
|
// Arrow-key selection over the slash preview list.
|
|
1599
1658
|
let navMatches: string[] = []; // command names matching the typed keyword (display order)
|
|
@@ -1643,24 +1702,42 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1643
1702
|
// line painted at an older, wider geometry reflows onto extra rows after a width shrink.
|
|
1644
1703
|
let lastDrawnLines: string[] = [];
|
|
1645
1704
|
const padToFooter = (lines: string[]): string[] => {
|
|
1646
|
-
if (lines.length >=
|
|
1647
|
-
return [...lines, ...new Array(
|
|
1705
|
+
if (lines.length >= footerRendered) return lines.slice(0, footerRendered);
|
|
1706
|
+
return [...lines, ...new Array(footerRendered - lines.length).fill("")];
|
|
1648
1707
|
};
|
|
1649
1708
|
const armPreview = () => {
|
|
1650
1709
|
if (!previewEnabled || previewArmed) return;
|
|
1651
1710
|
footerRows = previewRowsFor(process.stdout.rows ?? 24);
|
|
1652
|
-
// Reserve
|
|
1653
|
-
//
|
|
1654
|
-
//
|
|
1655
|
-
|
|
1656
|
-
|
|
1711
|
+
// Reserve a COMPACT region (idle prompt height) right after the current output —
|
|
1712
|
+
// not the full `footerRows` budget — so a finished/idle prompt leaves no blank rows
|
|
1713
|
+
// below it. drawFooter grows the reservation in place when a dropdown needs more.
|
|
1714
|
+
const initial = Math.max(1, Math.min(footerRows, COMPACT_FOOTER_ROWS));
|
|
1715
|
+
if (initial > 1) {
|
|
1716
|
+
out.write("\n".repeat(initial - 1) + cursorUp(initial - 1));
|
|
1657
1717
|
}
|
|
1658
1718
|
out.write(toColumn(1));
|
|
1659
|
-
footerRendered =
|
|
1719
|
+
footerRendered = initial;
|
|
1720
|
+
footerWantRows = initial;
|
|
1660
1721
|
footerParkedRow = 0;
|
|
1661
1722
|
previewArmed = true;
|
|
1662
1723
|
lastFooterKey = "";
|
|
1663
1724
|
};
|
|
1725
|
+
// Re-pin the reservation to `n` rows IN PLACE (right after the existing output, never
|
|
1726
|
+
// bottom-pinned): clear the old region from its top, then reserve `n` rows there. Used
|
|
1727
|
+
// by drawFooter to grow for a dropdown and shrink back to the compact idle height, so
|
|
1728
|
+
// the prompt never carries a trailing/floating blank block.
|
|
1729
|
+
const setFooterRows = (n: number) => {
|
|
1730
|
+
n = Math.max(1, Math.min(n, footerRows));
|
|
1731
|
+
if (!previewArmed || n === footerRendered) return;
|
|
1732
|
+
let s = footerParkedRow > 0 ? cursorUp(footerParkedRow) : "";
|
|
1733
|
+
s += toColumn(1) + clearToEnd(); // wipe old region; cursor now at its top (after output)
|
|
1734
|
+
if (n > 1) s += "\n".repeat(n - 1) + cursorUp(n - 1);
|
|
1735
|
+
s += toColumn(1);
|
|
1736
|
+
out.write(s);
|
|
1737
|
+
footerRendered = n;
|
|
1738
|
+
footerParkedRow = 0;
|
|
1739
|
+
lastFooterKey = ""; // force a full repaint into the resized region
|
|
1740
|
+
};
|
|
1664
1741
|
// Clear the reserved region and park the cursor at its top row so subsequent
|
|
1665
1742
|
// command output starts where the box was (and inherits the existing scrollback).
|
|
1666
1743
|
const disarmPreview = () => {
|
|
@@ -1777,7 +1854,11 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1777
1854
|
const slash = budget > 0 ? formatSlashPreview(line, budget, selected, skillSlashDetails, resolvedSkills) : [];
|
|
1778
1855
|
const args = !slash.length && budget > 0 ? formatCompletionPreview(line, completionContext(), budget) : [];
|
|
1779
1856
|
const preview = (slash.length ? slash : args).map(l => chalk.gray(truncateAnsi(l, cols)));
|
|
1780
|
-
|
|
1857
|
+
const result = [statusBarLine(cols), "", ...input, ...preview].slice(0, footerRows);
|
|
1858
|
+
// Want only the input box + status bar at idle (no dropdown) → compact reservation;
|
|
1859
|
+
// grow to fit the dropdown when a preview is present.
|
|
1860
|
+
footerWantRows = preview.length > 0 ? result.length : Math.min(footerRows, 2 + input.length);
|
|
1861
|
+
return result;
|
|
1781
1862
|
};
|
|
1782
1863
|
// Render the reversible Ctrl+O detail panel into the footer reservation: a status
|
|
1783
1864
|
// bar, a title (with scroll hint when needed), then a windowed slice of the detail
|
|
@@ -1815,10 +1896,15 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1815
1896
|
if (below > 0) body.push(chalk.dim(`↓ ${below} more below`));
|
|
1816
1897
|
}
|
|
1817
1898
|
footerCursor = { row: Math.min(1, footerRows - 1), col: 1 };
|
|
1818
|
-
|
|
1899
|
+
const result = [statusBarLine(cols), title, ...body].slice(0, footerRows);
|
|
1900
|
+
footerWantRows = result.length; // the Ctrl+O panel sizes the reservation to its content
|
|
1901
|
+
return result;
|
|
1819
1902
|
};
|
|
1820
1903
|
const drawFooter = (lines: string[]) => {
|
|
1821
1904
|
if (!previewArmed || footerRendered === 0) return;
|
|
1905
|
+
// Re-pin the reservation to the height the latest preview/panel wants (compact at
|
|
1906
|
+
// idle, grown for a dropdown) BEFORE painting, so no reserved blank trails the prompt.
|
|
1907
|
+
setFooterRows(footerWantRows);
|
|
1822
1908
|
// ALWAYS paint exactly footerRendered rows so the reservation is fully covered
|
|
1823
1909
|
// and no row can spill past it — the bug fix that kept `@folder<more text>`
|
|
1824
1910
|
// typing from scrolling the input box (and prior output) off the top.
|
|
@@ -2535,9 +2621,11 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2535
2621
|
out.write(clearScreen());
|
|
2536
2622
|
out.write(renderWelcome({ ...welcomeData, cols }).join("\n") + "\n");
|
|
2537
2623
|
footerRows = previewRowsFor(rows);
|
|
2538
|
-
|
|
2624
|
+
const initial = Math.max(1, Math.min(footerRows, COMPACT_FOOTER_ROWS));
|
|
2625
|
+
if (initial > 1) out.write("\n".repeat(initial - 1) + cursorUp(initial - 1));
|
|
2539
2626
|
out.write(toColumn(1));
|
|
2540
|
-
footerRendered =
|
|
2627
|
+
footerRendered = initial;
|
|
2628
|
+
footerWantRows = initial;
|
|
2541
2629
|
drawFooter(promptHistoryLines ? historyPreviewLines(promptHistoryLines) : previewLines(typedLine, navIdx));
|
|
2542
2630
|
return;
|
|
2543
2631
|
}
|
|
@@ -2568,14 +2656,17 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2568
2656
|
const caretSubRow = Math.floor(Math.max(0, footerCursor.col - 1) / c);
|
|
2569
2657
|
const hopUp = abovePhysical + caretSubRow;
|
|
2570
2658
|
let s = (hopUp > 0 ? cursorUp(hopUp) : "") + toColumn(1) + clearToEnd();
|
|
2571
|
-
// Re-pin a
|
|
2572
|
-
//
|
|
2659
|
+
// Re-pin a COMPACT reservation right where the frame top was (just below the
|
|
2660
|
+
// static content) — NOT bottom-pinned. ED already blanked from the frame top
|
|
2661
|
+
// down, so reserve the idle height here; drawFooter grows it for a dropdown.
|
|
2662
|
+
// Bottom-pinning left a tall blank gap above the bar when the content was short.
|
|
2573
2663
|
footerRows = previewRowsFor(rows);
|
|
2574
|
-
|
|
2575
|
-
if (
|
|
2664
|
+
const initial = Math.max(1, Math.min(footerRows, COMPACT_FOOTER_ROWS));
|
|
2665
|
+
if (initial > 1) s += "\n".repeat(initial - 1) + cursorUp(initial - 1);
|
|
2576
2666
|
s += toColumn(1);
|
|
2577
2667
|
out.write(s);
|
|
2578
|
-
footerRendered =
|
|
2668
|
+
footerRendered = initial;
|
|
2669
|
+
footerWantRows = initial;
|
|
2579
2670
|
footerParkedRow = 0;
|
|
2580
2671
|
lastFooterKey = "";
|
|
2581
2672
|
drawFooter(promptHistoryLines ? historyPreviewLines(promptHistoryLines) : previewLines(typedLine, navIdx));
|
|
@@ -3462,7 +3553,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
3462
3553
|
if (modelArg?.toLowerCase() === "provider") {
|
|
3463
3554
|
const want = (tokens[2] ?? "").toLowerCase();
|
|
3464
3555
|
if (!isProviderName(want)) {
|
|
3465
|
-
console.log(`Usage: /agents ${role.id} provider <
|
|
3556
|
+
console.log(`Usage: /agents ${role.id} provider <name> [model|#N] — e.g. anthropic, openai, gemini, groq, deepseek, openrouter (any configured provider)`);
|
|
3466
3557
|
continue;
|
|
3467
3558
|
}
|
|
3468
3559
|
const st = (await describeAllProviders()).find(s => s.name === want);
|
|
@@ -3475,7 +3566,8 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
3475
3566
|
}
|
|
3476
3567
|
}
|
|
3477
3568
|
const live = await getLiveModels();
|
|
3478
|
-
const forProvider =
|
|
3569
|
+
const forProvider = providerPickEntries(live, want);
|
|
3570
|
+
const liveForProvider = live.some(r => r.ok && r.provider === want && r.models.length > 0);
|
|
3479
3571
|
const explicit = tokens[3];
|
|
3480
3572
|
let chosenModel: string;
|
|
3481
3573
|
if (explicit && forProvider.length) {
|
|
@@ -3494,7 +3586,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
3494
3586
|
} else if (explicit) {
|
|
3495
3587
|
chosenModel = qualifyModelId(explicit, want);
|
|
3496
3588
|
} else if (forProvider.length) {
|
|
3497
|
-
// No model given → the provider's first
|
|
3589
|
+
// No model given → the provider's first known model, provider-qualified.
|
|
3498
3590
|
chosenModel = qualifyModelId(forProvider[0]!.model, want);
|
|
3499
3591
|
} else {
|
|
3500
3592
|
chosenModel = providerDefaultModel(want);
|
|
@@ -3503,7 +3595,9 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
3503
3595
|
console.log(`${role.title} pinned to ${want} via model ${chosenModel} — saved to ~/.jeo/config.json`);
|
|
3504
3596
|
if (forProvider.length) {
|
|
3505
3597
|
lastPickIndex = forProvider;
|
|
3506
|
-
|
|
3598
|
+
const sourceNote = liveForProvider ? "Live" : "Catalog";
|
|
3599
|
+
const tail = liveForProvider ? "" : " (log in to list live models)";
|
|
3600
|
+
console.log(`${sourceNote} ${want} models — refine with /agents ${role.id} #N:${tail}`);
|
|
3507
3601
|
for (const line of formatPickListWithCapabilities(lastPickIndex, { current: chosenModel, cap: 12 })) console.log(line);
|
|
3508
3602
|
}
|
|
3509
3603
|
continue;
|
package/src/tui/app.ts
CHANGED
|
@@ -68,6 +68,9 @@ export interface AgentEventsLike {
|
|
|
68
68
|
onUsage?(usage: { inputTokens: number; outputTokens: number }): void;
|
|
69
69
|
onModelStream?(textSoFar: string): void;
|
|
70
70
|
onReasoningStream?(textSoFar: string): void;
|
|
71
|
+
/** Per-artifact native reasoning replay records (signature / thoughtSignature / reasoning
|
|
72
|
+
* item). The TUI ignores these; launch.ts uses them to persist the final reply's artifacts. */
|
|
73
|
+
onReasoningArtifactStream?(artifact: import("../ai/types").ReasoningArtifact): void;
|
|
71
74
|
onBudget?(limit: number, reason: string): void;
|
|
72
75
|
|
|
73
76
|
}
|
|
@@ -112,6 +115,27 @@ export function tailForWrap(text: string, maxChars = FRAME_WRAP_TAIL_CHARS): str
|
|
|
112
115
|
return text.length > maxChars ? text.slice(text.length - maxChars) : text;
|
|
113
116
|
}
|
|
114
117
|
|
|
118
|
+
/** Max lines of a committed reasoning block kept in scrollback (gjc-style collapse): a
|
|
119
|
+
* long chain-of-thought is clipped with a "+N more" hint so it never floods the ledger. */
|
|
120
|
+
export const THINKING_COMMIT_MAX_LINES = 12;
|
|
121
|
+
|
|
122
|
+
/** Collapse a committed reasoning block to a line cap, appending a "… (+N more lines)"
|
|
123
|
+
* hint when clipped (gjc collapsed-by-default parity). Returns the input verbatim when
|
|
124
|
+
* it already fits. */
|
|
125
|
+
export function clipReasoningLines(text: string, cap = THINKING_COMMIT_MAX_LINES): string {
|
|
126
|
+
const rows = text.replace(/\r/g, "").split("\n");
|
|
127
|
+
if (rows.length <= cap) return rows.join("\n");
|
|
128
|
+
return [...rows.slice(0, cap), `… (+${rows.length - cap} more lines)`].join("\n");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** gjc-style "thought for Ns" header for a committed/streaming Thinking block. Omits the
|
|
132
|
+
* duration when no step start is known (e.g. resumed/exported records). */
|
|
133
|
+
export function thinkingHeader(elapsedMs: number | undefined, unicode: boolean): string {
|
|
134
|
+
const diamond = unicode ? "◇" : "*";
|
|
135
|
+
const secs = elapsedMs !== undefined && elapsedMs >= 0 ? `${(elapsedMs / 1000).toFixed(1)}s` : null;
|
|
136
|
+
return `${diamond} thinking${secs ? ` · ${secs}` : ""}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
115
139
|
/** Status animation palette while a tool/process runs (background verification): an
|
|
116
140
|
* amber→yellow gradient, distinct from the cool thinking gradient, so "the agent is
|
|
117
141
|
* running a process / verifying" reads at a glance (gjc parity: `theme.fg("warning")`
|
|
@@ -444,13 +468,17 @@ export class LaunchTui {
|
|
|
444
468
|
: (s: string) => s;
|
|
445
469
|
const style = (prose: string) => prose.split("\n").map(styleThought).join("\n");
|
|
446
470
|
const parts: string[] = [this.agentLabel()];
|
|
471
|
+
// gjc "thought for Ns" header: step-start → commit ≈ the model's think+gen time.
|
|
472
|
+
const elapsedMs = this.currentStepStartedAt ? Date.now() - this.currentStepStartedAt : undefined;
|
|
473
|
+
const header = thinkingHeader(elapsedMs, this.unicode);
|
|
474
|
+
parts.push(this.theme.color ? chalk.dim(header) : header);
|
|
447
475
|
if (willFlushThought) {
|
|
448
476
|
this.flushedThought = this.streamingThought;
|
|
449
|
-
parts.push(style(this.streamingThought));
|
|
477
|
+
parts.push(style(clipReasoningLines(this.streamingThought)));
|
|
450
478
|
}
|
|
451
479
|
if (willFlushReasoning) {
|
|
452
480
|
this.flushedReasoning = this.streamingReasoning;
|
|
453
|
-
parts.push(style(this.streamingReasoning));
|
|
481
|
+
parts.push(style(clipReasoningLines(this.streamingReasoning)));
|
|
454
482
|
}
|
|
455
483
|
this.appendLedger(`${parts.join("\n")}\n`, "reasoning");
|
|
456
484
|
}
|
|
@@ -1206,7 +1234,7 @@ export class LaunchTui {
|
|
|
1206
1234
|
* block shows only the most-recent lines, capped at ~30% of the screen height (a
|
|
1207
1235
|
* ceiling guards a tall terminal), so it grows with the stream and shrinks with the
|
|
1208
1236
|
* viewport. Returns [] when there is nothing to show. */
|
|
1209
|
-
private renderLiveBlock(label: string, text: string, cols: number, rows: number, ceiling: number): string[] {
|
|
1237
|
+
private renderLiveBlock(label: string, text: string, cols: number, rows: number, ceiling: number, cacheKey = label): string[] {
|
|
1210
1238
|
const dim = this.theme.color ? chalk.dim : (s: string) => s;
|
|
1211
1239
|
if (!text.trim()) return [];
|
|
1212
1240
|
const wrapW = Math.max(8, cols - 2);
|
|
@@ -1214,8 +1242,8 @@ export class LaunchTui {
|
|
|
1214
1242
|
// this (up to 16KB) tail every frame just re-segments graphemes for no visible change.
|
|
1215
1243
|
// Per-label slot (Thinking / Output) keyed by wrap width + text — a real delta misses
|
|
1216
1244
|
// once and recomputes; an idle tick hits the cache. `rows` only gates the post-slice.
|
|
1217
|
-
let cache = this.liveBlockWrapCaches.get(
|
|
1218
|
-
if (!cache) { cache = lastValueCache<string[]>(); this.liveBlockWrapCaches.set(
|
|
1245
|
+
let cache = this.liveBlockWrapCaches.get(cacheKey);
|
|
1246
|
+
if (!cache) { cache = lastValueCache<string[]>(); this.liveBlockWrapCaches.set(cacheKey, cache); }
|
|
1219
1247
|
const wrapped = cache(`${wrapW}\u0000${text}`, () =>
|
|
1220
1248
|
tailForWrap(text)
|
|
1221
1249
|
.split("\n")
|
|
@@ -1349,7 +1377,11 @@ export class LaunchTui {
|
|
|
1349
1377
|
// rectangle, so a short trace leaves no padded "hole" and a short terminal is spared.
|
|
1350
1378
|
const liveThink = this.streamingThought.trim() || this.streamingReasoning.trim();
|
|
1351
1379
|
if (isThinking && liveThink) {
|
|
1352
|
-
|
|
1380
|
+
// gjc-parity: the Thinking block label carries a running timer ("Thinking · Ns").
|
|
1381
|
+
// Cache key stays the constant "Thinking" so the per-frame wrap memo is unaffected.
|
|
1382
|
+
const liveMs = this.currentStepStartedAt ? Date.now() - this.currentStepStartedAt : undefined;
|
|
1383
|
+
const liveLabel = liveMs !== undefined ? `Thinking · ${(liveMs / 1000).toFixed(1)}s` : "Thinking";
|
|
1384
|
+
tail.push(...this.renderLiveBlock(liveLabel, liveThink, cols, rows, 6, "Thinking"));
|
|
1353
1385
|
}
|
|
1354
1386
|
|
|
1355
1387
|
// Live tool output (gjc-style streaming bash stdout): while a tool runs, its
|
|
@@ -412,52 +412,46 @@ export async function animateFrames(stage: AsciiStage, opts: AnimateFramesOption
|
|
|
412
412
|
}
|
|
413
413
|
return total;
|
|
414
414
|
}
|
|
415
|
-
/** The compact jeo forge mark: a
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
*
|
|
421
|
-
*
|
|
422
|
-
*
|
|
423
|
-
* alone. Width-1 glyphs only (box drawing + geometrics) so padding/centering
|
|
424
|
-
* math stays exact. Frame 0 is the static symbol. */
|
|
415
|
+
/** The compact jeo forge mark: a symmetrical crayfish (가재) brand emblem composed
|
|
416
|
+
* of SIMPLE, DISCONNECTED shapes (no connecting strokes) that highlight the signature
|
|
417
|
+
* pincer CLAWS (집게) flanking the sides (◤◣ and ❮ on the left, ◥◢ and ❯ on the right),
|
|
418
|
+
* the body carrying the JEO wordmark (J E O), and a DNA double-helix woven above and
|
|
419
|
+
* below as a row of crossing nodes (╳ = base-pair crossings). gjc-forge aesthetic:
|
|
420
|
+
* clean negative space, geometric symmetry, the blue→violet→pink flow gradient applied
|
|
421
|
+
* by renderForgeMark doing the neon glow. Width-1 glyphs only (box drawing + geometrics)
|
|
422
|
+
* so padding/centering math stays exact. Frame 0 is the static symbol. */
|
|
425
423
|
export const FORGE_MARK_ART: string[] = [
|
|
426
|
-
"
|
|
427
|
-
"
|
|
428
|
-
"
|
|
429
|
-
" ╲▔▔╱ "
|
|
424
|
+
"◤ ╳ ╳ ╳ ╳ ◥",
|
|
425
|
+
"❮ J E O ❯",
|
|
426
|
+
"◣ ╳ ╳ ╳ ╳ ◢"
|
|
430
427
|
];
|
|
431
428
|
|
|
432
429
|
export const FORGE_MARK_ART_ASCII: string[] = [
|
|
433
|
-
"
|
|
434
|
-
"
|
|
435
|
-
"
|
|
436
|
-
" \\__/ "
|
|
430
|
+
"/ x x x x \\",
|
|
431
|
+
"< J E O >",
|
|
432
|
+
"\\ x x x x /"
|
|
437
433
|
];
|
|
438
434
|
|
|
439
|
-
/** Claw-snap blink frames for the compact
|
|
440
|
-
*
|
|
441
|
-
* closed), so the
|
|
442
|
-
* frameless render is byte-identical to the static
|
|
443
|
-
* same width and width-1 glyphs. */
|
|
435
|
+
/** Claw-snap blink frames for the compact lobster forge mark: the helix nodes, the
|
|
436
|
+
* JEO body and the inner claw/tail glyphs stay fixed while the four splayed pincer
|
|
437
|
+
* CORNERS snap (◤◣ / ◥◢ open → ◢◥ / ◣◤ closed), so the lobster "clicks" its claws.
|
|
438
|
+
* Frame 0 === FORGE_MARK_ART, so a frameless render is byte-identical to the static
|
|
439
|
+
* symbol. All lines share the same width and width-1 glyphs. */
|
|
444
440
|
export const FORGE_MARK_FRAMES: string[][] = [
|
|
445
441
|
FORGE_MARK_ART,
|
|
446
442
|
[
|
|
447
|
-
"
|
|
448
|
-
"
|
|
449
|
-
"
|
|
450
|
-
" ╲▔▔╱ "
|
|
443
|
+
"◢ ╳ ╳ ╳ ╳ ◣",
|
|
444
|
+
"❮ J E O ❯",
|
|
445
|
+
"◥ ╳ ╳ ╳ ╳ ◤"
|
|
451
446
|
]
|
|
452
447
|
];
|
|
453
448
|
|
|
454
449
|
export const FORGE_MARK_FRAMES_ASCII: string[][] = [
|
|
455
450
|
FORGE_MARK_ART_ASCII,
|
|
456
451
|
[
|
|
457
|
-
"
|
|
458
|
-
"
|
|
459
|
-
"
|
|
460
|
-
" \\__/ "
|
|
452
|
+
"\\ x x x x /",
|
|
453
|
+
"< J E O >",
|
|
454
|
+
"/ x x x x \\"
|
|
461
455
|
]
|
|
462
456
|
];
|
|
463
457
|
|
|
@@ -466,27 +460,26 @@ export function forgeMarkFrameCount(): number {
|
|
|
466
460
|
return FORGE_MARK_FRAMES.length;
|
|
467
461
|
}
|
|
468
462
|
|
|
469
|
-
/** Grand hero variant for the welcome forge box (gjc-style spacious banner): the
|
|
470
|
-
*
|
|
471
|
-
* (
|
|
472
|
-
*
|
|
473
|
-
*
|
|
463
|
+
/** Grand hero variant for the welcome forge box (gjc-style spacious banner): the same
|
|
464
|
+
* symmetrical crayfish emblem rendered large — the splayed pincer claws as corner wedges
|
|
465
|
+
* (◤◣ left, ◥◢ right) and heavy brackets (❮ left, ❯ right), the JEO wordmark spaced
|
|
466
|
+
* across the body (J E O), and the DNA double-helix woven above and below as a wider
|
|
467
|
+
* row of crossing nodes (╳). gjc-forge aesthetic: generous negative space + geometric
|
|
468
|
+
* symmetry, with renderForgeMark's blue→violet→pink flow gradient supplying the neon glow.
|
|
469
|
+
* Width 29 (matches the welcome compact↔grand threshold) and width-1 glyphs only so
|
|
474
470
|
* padding/centering math stays exact. */
|
|
475
471
|
export const FORGE_MARK_ART_GRAND: string[] = [
|
|
476
|
-
"
|
|
477
|
-
"
|
|
478
|
-
"
|
|
479
|
-
" ╲▔▔▔▔▔▔▔▔▔╱ "
|
|
472
|
+
"◤ ╳ ╳ ╳ ╳ ╳ ╳ ◥",
|
|
473
|
+
"❮ J E O ❯",
|
|
474
|
+
"◣ ╳ ╳ ╳ ╳ ╳ ╳ ◢"
|
|
480
475
|
];
|
|
481
476
|
|
|
482
477
|
export const FORGE_MARK_ART_GRAND_ASCII: string[] = [
|
|
483
|
-
"
|
|
484
|
-
"
|
|
485
|
-
"
|
|
486
|
-
" \\_________/ "
|
|
478
|
+
"/ x x x x x x \\",
|
|
479
|
+
"< J E O >",
|
|
480
|
+
"\\ x x x x x x /"
|
|
487
481
|
];
|
|
488
482
|
|
|
489
|
-
|
|
490
483
|
// Bounded memo of fully-rendered forge-mark frames keyed by every input that affects
|
|
491
484
|
// output (grand/unicode/cols/color/colorLevel/phase/frame). The live HUD cycles a
|
|
492
485
|
// FIXED frame set (blink × gradient phases) at ~120ms; without this each recurrence
|