jeo-code 0.6.21 → 0.6.23
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 -1
- package/README.ja.md +6 -2
- package/README.ko.md +6 -2
- package/README.md +6 -2
- package/README.zh.md +6 -2
- package/package.json +1 -1
- package/src/agent/config-schema.ts +12 -0
- package/src/agent/session.ts +10 -3
- package/src/agent/state.ts +19 -14
- package/src/ai/index.ts +1 -0
- package/src/ai/model-catalog.ts +121 -1
- package/src/ai/model-discovery.ts +55 -3
- package/src/ai/model-manager.ts +43 -11
- package/src/ai/model-registry.ts +2 -0
- package/src/ai/provider-status.ts +26 -7
- package/src/ai/providers/anthropic-compatible.ts +27 -0
- package/src/ai/providers/anthropic.ts +7 -3
- package/src/ai/providers/antigravity.ts +31 -6
- package/src/ai/providers/gemini.ts +45 -4
- package/src/ai/providers/kimi.ts +18 -0
- package/src/ai/providers/lmstudio.ts +8 -0
- package/src/ai/providers/ollama.ts +17 -5
- package/src/ai/providers/openai-compatible-catalog.ts +72 -0
- package/src/ai/providers/openai-compatible.ts +31 -0
- package/src/ai/providers/openai.ts +23 -7
- package/src/ai/providers/xai.ts +18 -0
- package/src/ai/register-providers.ts +18 -0
- package/src/ai/think-tags.ts +84 -0
- package/src/ai/types.ts +6 -1
- package/src/auth/flows/index.ts +3 -3
- package/src/auth/index.ts +4 -1
- package/src/auth/oauth.ts +3 -3
- package/src/auth/refresh.ts +5 -0
- package/src/auth/storage.ts +12 -1
- package/src/commands/auth.ts +19 -2
- package/src/commands/launch/flags.ts +5 -1
- package/src/commands/launch/input.ts +13 -0
- package/src/commands/launch.ts +78 -12
- package/src/commands/setup.ts +3 -2
- package/src/tui/app.ts +51 -31
- package/src/tui/components/ascii-art.ts +11 -7
- package/src/tui/components/autocomplete.ts +16 -0
- package/src/tui/components/forge.ts +1 -1
- package/src/tui/components/transcript.ts +7 -0
- package/src/tui/components/width.ts +21 -0
package/src/tui/app.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { Spinner } from "./components/spinner";
|
|
|
16
16
|
import { ToolList } from "./components/tool-list";
|
|
17
17
|
import { StreamRegion } from "./components/stream";
|
|
18
18
|
import { renderFooter, type FooterData } from "./components/footer";
|
|
19
|
-
import { renderDnaClaw, dnaClawHeight, dnaClawFrameCount,
|
|
19
|
+
import { renderDnaClaw, dnaClawHeight, dnaClawFrameCount, forgeBeat, DNA_FLOW_PALETTE } from "./components/ascii-art";
|
|
20
20
|
import { evolutionTrack, createStageProgress, type StageProgress, transitionMessage } from "./components/evolution";
|
|
21
21
|
import type { TaskSubEvent } from "../agent/task-tool";
|
|
22
22
|
import { supportsUnicode } from "./components/capability";
|
|
@@ -30,7 +30,7 @@ import { costForUsage } from "../ai/pricing";
|
|
|
30
30
|
import { renderMarkdownTables } from "./components/markdown-table";
|
|
31
31
|
|
|
32
32
|
import { stripMarkdown, renderMarkdownAnsi } from "./components/markdown-text";
|
|
33
|
-
import { visibleWidth, wrapTextWithAnsi, truncateToWidth, sanitizeForFrame } from "./components/width";
|
|
33
|
+
import { visibleWidth, wrapTextWithAnsi, truncateToWidth, sanitizeForFrame, lastValueCache } from "./components/width";
|
|
34
34
|
import { categoryBadge } from "./components/category-index";
|
|
35
35
|
import { formatStepTimeline, stepsFromTools, formatStepHeader, formatStepTimelineCompact, formatDuration as formatToolMs, type StepState } from "./components/step-timeline";
|
|
36
36
|
import { formatHintBar } from "./components/hints";
|
|
@@ -86,10 +86,9 @@ function extractStreamingReasoning(buf: string): string {
|
|
|
86
86
|
catch { return m[1].replace(/\\\\/g, "\\").replace(/\\n/g, " ").replace(/\\"/g, '"'); }
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* across providers/models instead of staying silent for some of them. */
|
|
89
|
+
/** Derive a HUD STATUS (never content) from the forming stream: `"tool":"x"` → "calling
|
|
90
|
+
* x…", bare JSON/fence → "forming the next tool call…", prose reply → "writing the
|
|
91
|
+
* reply…". The reply/reasoning TEXT itself belongs in the Thinking block, not the HUD. */
|
|
93
92
|
function extractStreamingActivity(buf: string): string {
|
|
94
93
|
const head = buf.length > 512 ? buf.slice(0, 512) : buf;
|
|
95
94
|
const tool = head.match(/"tool"\s*:\s*"([^"]+)"/)?.[1];
|
|
@@ -97,7 +96,7 @@ function extractStreamingActivity(buf: string): string {
|
|
|
97
96
|
const t = head.trim();
|
|
98
97
|
if (!t) return "";
|
|
99
98
|
if (t.startsWith("{") || t.startsWith("```")) return "forming the next tool call…";
|
|
100
|
-
return
|
|
99
|
+
return "writing the reply…";
|
|
101
100
|
}
|
|
102
101
|
|
|
103
102
|
/** Bound the input to a per-frame wrap to a fixed trailing window. The live thinking
|
|
@@ -265,6 +264,10 @@ export class LaunchTui {
|
|
|
265
264
|
private cachedFrame = -1;
|
|
266
265
|
private cachedArt: string[] = [];
|
|
267
266
|
private cachedTrack = "";
|
|
267
|
+
// Per-label (Thinking / Output) single-slot wrap memo: the 120ms spinner tick
|
|
268
|
+
// re-renders the frame ~8×/s, but the streamed text changes only on a new delta —
|
|
269
|
+
// so the live block reuses its prior wrap instead of re-segmenting the 16KB tail.
|
|
270
|
+
private readonly liveBlockWrapCaches = new Map<string, (key: string, compute: () => string[]) => string[]>();
|
|
268
271
|
// Monotonic stage progress so evolution only ever moves forward this turn.
|
|
269
272
|
private readonly progress: StageProgress = createStageProgress();
|
|
270
273
|
// Terminal unicode capability, detected once (drives spinner/track glyph set).
|
|
@@ -393,25 +396,20 @@ export class LaunchTui {
|
|
|
393
396
|
this.draw();
|
|
394
397
|
},
|
|
395
398
|
onModelStream: textSoFar => {
|
|
396
|
-
//
|
|
397
|
-
//
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
// Draws are THROTTLED to one per 100ms: the old per-delta draw() rendered
|
|
401
|
-
// the full frame hundreds of times per response (a real chunk of jeo's
|
|
402
|
-
// per-step latency); the 120ms timer tick covers the gaps anyway.
|
|
399
|
+
// The model's JSON-protocol `reasoning` field → the live Thinking block (alongside
|
|
400
|
+
// native reasoning). The HUD status row gets ONLY a derived STATUS — never the raw
|
|
401
|
+
// reasoning/reply text, so a JSON-streaming model shows "forming the next tool
|
|
402
|
+
// call…", not its JSON content. Draws throttled to ≤1/100ms (timer covers gaps).
|
|
403
403
|
const r = extractStreamingReasoning(textSoFar);
|
|
404
404
|
let changed = false;
|
|
405
|
-
if (r) {
|
|
406
|
-
changed = r !== this.streamingReasoning;
|
|
405
|
+
if (r && r !== this.streamingReasoning) {
|
|
407
406
|
this.streamingReasoning = r;
|
|
408
|
-
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
}
|
|
407
|
+
changed = true;
|
|
408
|
+
}
|
|
409
|
+
const status = extractStreamingActivity(textSoFar);
|
|
410
|
+
if (status && status !== this.streamingActivity) {
|
|
411
|
+
this.streamingActivity = status;
|
|
412
|
+
changed = true;
|
|
415
413
|
}
|
|
416
414
|
if (changed && Date.now() - this.lastStreamDraw >= 100) {
|
|
417
415
|
this.lastStreamDraw = Date.now();
|
|
@@ -624,10 +622,21 @@ export class LaunchTui {
|
|
|
624
622
|
this.draw();
|
|
625
623
|
}
|
|
626
624
|
|
|
625
|
+
private livePromptHint: string[] = [];
|
|
626
|
+
/** Mid-turn command/skill preview lines shown above the live input box, so a
|
|
627
|
+
* /command or $skill typed WHILE a turn runs visibly reacts (idle-prompt parity). */
|
|
628
|
+
setLivePromptHint(lines: string[]): void {
|
|
629
|
+
if (this.finished) return;
|
|
630
|
+
const next = lines ?? [];
|
|
631
|
+
if (next.join("\n") === this.livePromptHint.join("\n")) return;
|
|
632
|
+
this.livePromptHint = next;
|
|
633
|
+
this.draw();
|
|
634
|
+
}
|
|
635
|
+
|
|
627
636
|
private renderLiveInputBox(cols: number): string[] {
|
|
628
637
|
const caret = this.unicode ? "▌" : "_";
|
|
629
638
|
const display = this.livePromptInput ? `${this.livePromptInput}${caret}` : "";
|
|
630
|
-
|
|
639
|
+
const box = renderInputBox(display, {
|
|
631
640
|
cols: Math.max(24, cols),
|
|
632
641
|
color: this.theme.color,
|
|
633
642
|
unicode: this.unicode,
|
|
@@ -636,6 +645,9 @@ export class LaunchTui {
|
|
|
636
645
|
placeholder: "Type your next message...",
|
|
637
646
|
maxBodyRows: 2,
|
|
638
647
|
});
|
|
648
|
+
if (this.livePromptHint.length === 0) return box;
|
|
649
|
+
const dim = this.theme.color ? chalk.dim : (s: string) => s;
|
|
650
|
+
return [...this.livePromptHint.map(l => dim(l)), ...box];
|
|
639
651
|
}
|
|
640
652
|
|
|
641
653
|
/** Render a `user`-labeled query card (orange "user" header over a filled box).
|
|
@@ -884,6 +896,7 @@ export class LaunchTui {
|
|
|
884
896
|
this.turnUsage = null;
|
|
885
897
|
this.lastLedgerKind = null; // fresh turn: no leading spacer before the first ledger line
|
|
886
898
|
this.livePromptInput = ""; // fresh turn: no next-prompt draft yet
|
|
899
|
+
this.livePromptHint = []; // fresh turn: no mid-turn command preview yet
|
|
887
900
|
this.subagentLive = null; // fresh turn: no nested subagent in flight
|
|
888
901
|
this.activityLog.length = 0; // per-turn ring: timestamps are turn-relative
|
|
889
902
|
this.spinner.updateStep(0, this.footer.maxSteps);
|
|
@@ -1174,7 +1187,7 @@ export class LaunchTui {
|
|
|
1174
1187
|
color: this.theme.color,
|
|
1175
1188
|
dim,
|
|
1176
1189
|
// DNA-flow identity on LIVE cards only: the flowing helix gradient rides
|
|
1177
|
-
// the card border and the
|
|
1190
|
+
// the card border and the prompt beat marks the title. Flushed/final cards
|
|
1178
1191
|
// stay static. Suppressed while `dim` (in-flight shading takes precedence).
|
|
1179
1192
|
...(anim && !dim
|
|
1180
1193
|
? { flow: { palette: DNA_FLOW_PALETTE, phase: anim.phase, colorLevel: anim.colorLevel }, titleMark: anim.beat }
|
|
@@ -1197,10 +1210,17 @@ export class LaunchTui {
|
|
|
1197
1210
|
const dim = this.theme.color ? chalk.dim : (s: string) => s;
|
|
1198
1211
|
if (!text.trim()) return [];
|
|
1199
1212
|
const wrapW = Math.max(8, cols - 2);
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1213
|
+
// Memoize the wrap: only the spinner/clock change on most 120ms ticks, so re-wrapping
|
|
1214
|
+
// this (up to 16KB) tail every frame just re-segments graphemes for no visible change.
|
|
1215
|
+
// Per-label slot (Thinking / Output) keyed by wrap width + text — a real delta misses
|
|
1216
|
+
// once and recomputes; an idle tick hits the cache. `rows` only gates the post-slice.
|
|
1217
|
+
let cache = this.liveBlockWrapCaches.get(label);
|
|
1218
|
+
if (!cache) { cache = lastValueCache<string[]>(); this.liveBlockWrapCaches.set(label, cache); }
|
|
1219
|
+
const wrapped = cache(`${wrapW}\u0000${text}`, () =>
|
|
1220
|
+
tailForWrap(text)
|
|
1221
|
+
.split("\n")
|
|
1222
|
+
.flatMap(l => wrapTextWithAnsi(l, wrapW))
|
|
1223
|
+
.filter(l => l.length > 0));
|
|
1204
1224
|
if (wrapped.length === 0) return [];
|
|
1205
1225
|
const cap = Math.max(3, Math.min(ceiling, Math.floor(rows * 0.3)));
|
|
1206
1226
|
const out: string[] = [sectionLabel(label, Math.max(8, cols), { color: this.theme.color, unicode: this.unicode })];
|
|
@@ -1312,10 +1332,10 @@ export class LaunchTui {
|
|
|
1312
1332
|
const dim = this.theme.color ? chalk.dim : (s: string) => s;
|
|
1313
1333
|
const colorLevel = detectColorLevel(process.env, isTTY());
|
|
1314
1334
|
// One quantized animation clock for the whole frame: gradient phase cycles 20
|
|
1315
|
-
// steps, the
|
|
1335
|
+
// steps, the prompt beat advances every 3 ticks. Quantization keeps repaints
|
|
1316
1336
|
// coherent (status field + forge border move together) and bounds per-tick work.
|
|
1317
1337
|
const phase = (this.tickCount * 0.05) % 1;
|
|
1318
|
-
const beat =
|
|
1338
|
+
const beat = forgeBeat(Math.trunc(this.tickCount / 3), this.unicode);
|
|
1319
1339
|
|
|
1320
1340
|
// Assemble the bottom-pinned tail FIRST (status line → todos → hud → model bar):
|
|
1321
1341
|
// it is the live heartbeat and must always be visible; the in-flight card gets
|
|
@@ -586,14 +586,18 @@ export function renderDnaClaw(opts: {
|
|
|
586
586
|
return result;
|
|
587
587
|
}
|
|
588
588
|
|
|
589
|
-
/** The
|
|
590
|
-
*
|
|
591
|
-
|
|
589
|
+
/** The jeo identity palette — the mascot's synthwave neon read straight off the
|
|
590
|
+
* character: blue lens → violet gown → pink lens. Shared by the claw art and the
|
|
591
|
+
* forge-card border flow so the whole brand glows in the wizard's signature
|
|
592
|
+
* blue→violet→pink (the dual neon lenses bracketing the gown). */
|
|
593
|
+
export const DNA_FLOW_PALETTE: readonly string[] = ["#48dbfb", "#8e44ad", "#f368e0"];
|
|
592
594
|
|
|
593
|
-
/** Width-1
|
|
594
|
-
*
|
|
595
|
-
|
|
596
|
-
|
|
595
|
+
/** Width-1 forge title-mark glyph cycling the mascot's `jeo>` terminal prompt:
|
|
596
|
+
* a prompt caret then a blinking block cursor (filled → hollow), echoing the
|
|
597
|
+
* glowing terminal screen the character holds. Used as the live forge-card
|
|
598
|
+
* title mark, so an in-flight card reads as a live coding-agent prompt. */
|
|
599
|
+
export function forgeBeat(frame: number, unicode = true): string {
|
|
600
|
+
const beats = unicode ? ["❯", "▮", "▯"] : [">", "#", "_"];
|
|
597
601
|
return beats[Math.abs(Math.trunc(frame)) % beats.length]!;
|
|
598
602
|
}
|
|
599
603
|
|
|
@@ -50,6 +50,7 @@ export interface CompletionResult {
|
|
|
50
50
|
|
|
51
51
|
const PREVIEW_LABEL: Record<string, string> = {
|
|
52
52
|
command: "Commands",
|
|
53
|
+
skill: "Skills",
|
|
53
54
|
model: "Models",
|
|
54
55
|
provider: "Providers",
|
|
55
56
|
role: "Subagent roles",
|
|
@@ -219,6 +220,21 @@ export function formatCompletionPreview(line: string, ctx: CompletionContext, ma
|
|
|
219
220
|
return lines;
|
|
220
221
|
}
|
|
221
222
|
|
|
223
|
+
/** Compact mid-turn command/skill preview. Like formatCompletionPreview but ALSO
|
|
224
|
+
* surfaces command-name and $skill-name matches (the kinds the argument-only preview
|
|
225
|
+
* skips), so a /command or $skill typed WHILE a turn runs visibly reacts. */
|
|
226
|
+
export function formatMidTurnHint(line: string, ctx: CompletionContext, max = 5): string[] {
|
|
227
|
+
if (max <= 0) return [];
|
|
228
|
+
const result = complete(line, ctx);
|
|
229
|
+
if (result.completions.length === 0) return [];
|
|
230
|
+
const label = PREVIEW_LABEL[result.kind] ?? "Matches";
|
|
231
|
+
const shown = result.completions.slice(0, max);
|
|
232
|
+
const hidden = result.completions.length - shown.length;
|
|
233
|
+
const lines = [`${label}:`, ...shown.map(c => ` ${c}`)];
|
|
234
|
+
if (hidden > 0) lines.push(` …(+${hidden} more)`);
|
|
235
|
+
return lines;
|
|
236
|
+
}
|
|
237
|
+
|
|
222
238
|
/** Longest common prefix of a list (for tab "fill to ambiguity"). */
|
|
223
239
|
export function commonPrefix(items: string[]): string {
|
|
224
240
|
if (items.length === 0) return "";
|
|
@@ -27,7 +27,7 @@ export interface ForgeBoxOptions {
|
|
|
27
27
|
* `phase` per tick, so nothing is retained between frames. Below TrueColor
|
|
28
28
|
* (`colorLevel < 3`) this degrades to the static `paint`/`paintShadow`. */
|
|
29
29
|
flow?: { palette: readonly string[]; phase: number; colorLevel: number };
|
|
30
|
-
/** Width-1 mark prepended to the border title (e.g. the
|
|
30
|
+
/** Width-1 mark prepended to the border title (e.g. the jeo prompt beat glyph). */
|
|
31
31
|
titleMark?: string;
|
|
32
32
|
/** Themed +/- painters for `language: "patch"` cards (edit diffs): applied to
|
|
33
33
|
* the FULL padded row so added/removed lines read as background-tinted
|
|
@@ -156,6 +156,13 @@ export function formatTranscript(messages: readonly Message[], opts: TranscriptO
|
|
|
156
156
|
? ""
|
|
157
157
|
: m.content;
|
|
158
158
|
if (!reason.trim()) continue;
|
|
159
|
+
// Persisted thinking (gjc "think → answer" order): show the turn's reasoning,
|
|
160
|
+
// dimmed, above the reply so the durable record carries it across /resume + export.
|
|
161
|
+
if (m.reasoning?.trim()) {
|
|
162
|
+
if (lines.length > 0 && lines[lines.length - 1] !== "") lines.push("");
|
|
163
|
+
lines.push(dim(`${unicode ? "◇" : "*"} thinking`));
|
|
164
|
+
for (const l of clipBody(m.reasoning.trim(), bodyCap)) lines.push(dim(l));
|
|
165
|
+
}
|
|
159
166
|
if (lines.length > 0 && lines[lines.length - 1] !== "") {
|
|
160
167
|
lines.push("");
|
|
161
168
|
}
|
|
@@ -246,3 +246,24 @@ export function wrapTextWithAnsi(text: string, cols: number): string[] {
|
|
|
246
246
|
}
|
|
247
247
|
return out;
|
|
248
248
|
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Single-slot memo for the live-frame wrap. The TUI's 120ms spinner tick re-renders
|
|
252
|
+
* the whole frame ~8×/s, but the reasoning / tool-output stream text only changes when
|
|
253
|
+
* a new delta arrives — so re-wrapping (grapheme-segmenting the up-to-16KB tail through
|
|
254
|
+
* `wrapTextWithAnsi`) on every idle tick is the hottest avoidable per-tick cost. This
|
|
255
|
+
* caches the most recent `key → value`: an unchanged frame reuses the prior wrap instead
|
|
256
|
+
* of recomputing it. One slot suffices — between two consecutive ticks the key (wrap
|
|
257
|
+
* width + text) is identical on the common path; a real change recomputes once.
|
|
258
|
+
*/
|
|
259
|
+
export function lastValueCache<T>(): (key: string, compute: () => T) => T {
|
|
260
|
+
let lastKey: string | undefined;
|
|
261
|
+
let lastValue: T;
|
|
262
|
+
return (key, compute) => {
|
|
263
|
+
if (key !== lastKey) {
|
|
264
|
+
lastKey = key;
|
|
265
|
+
lastValue = compute();
|
|
266
|
+
}
|
|
267
|
+
return lastValue;
|
|
268
|
+
};
|
|
269
|
+
}
|