jeo-code 0.6.22 → 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.
Files changed (45) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.ja.md +5 -1
  3. package/README.ko.md +5 -1
  4. package/README.md +5 -1
  5. package/README.zh.md +5 -1
  6. package/package.json +1 -1
  7. package/src/agent/config-schema.ts +12 -0
  8. package/src/agent/session.ts +10 -3
  9. package/src/agent/state.ts +19 -14
  10. package/src/ai/index.ts +1 -0
  11. package/src/ai/model-catalog.ts +121 -1
  12. package/src/ai/model-discovery.ts +55 -3
  13. package/src/ai/model-manager.ts +43 -11
  14. package/src/ai/model-registry.ts +2 -0
  15. package/src/ai/provider-status.ts +26 -7
  16. package/src/ai/providers/anthropic-compatible.ts +27 -0
  17. package/src/ai/providers/anthropic.ts +3 -1
  18. package/src/ai/providers/antigravity.ts +31 -6
  19. package/src/ai/providers/gemini.ts +45 -4
  20. package/src/ai/providers/kimi.ts +18 -0
  21. package/src/ai/providers/lmstudio.ts +8 -0
  22. package/src/ai/providers/ollama.ts +17 -5
  23. package/src/ai/providers/openai-compatible-catalog.ts +72 -0
  24. package/src/ai/providers/openai-compatible.ts +31 -0
  25. package/src/ai/providers/openai.ts +23 -7
  26. package/src/ai/providers/xai.ts +18 -0
  27. package/src/ai/register-providers.ts +18 -0
  28. package/src/ai/think-tags.ts +84 -0
  29. package/src/ai/types.ts +6 -1
  30. package/src/auth/flows/index.ts +3 -3
  31. package/src/auth/index.ts +4 -1
  32. package/src/auth/oauth.ts +3 -3
  33. package/src/auth/refresh.ts +5 -0
  34. package/src/auth/storage.ts +12 -1
  35. package/src/commands/auth.ts +19 -2
  36. package/src/commands/launch/flags.ts +5 -1
  37. package/src/commands/launch/input.ts +13 -0
  38. package/src/commands/launch.ts +78 -12
  39. package/src/commands/setup.ts +3 -2
  40. package/src/tui/app.ts +51 -31
  41. package/src/tui/components/ascii-art.ts +11 -7
  42. package/src/tui/components/autocomplete.ts +16 -0
  43. package/src/tui/components/forge.ts +1 -1
  44. package/src/tui/components/transcript.ts +7 -0
  45. 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, dnaClawBeat, DNA_FLOW_PALETTE } from "./components/ascii-art";
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
- /** Uniform live-activity fallback for models that stream no `reasoning` field: derive
90
- * what the model is doing from the forming JSON (`"tool":"x"` → "calling x…") or, for
91
- * prose-replying models, show the reply head so the live status field behaves identically
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 t.replace(/\s+/g, " ").slice(0, 140);
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
- // Surface the model's LIVE activity uniformly for every model/provider:
397
- // the streamed `reasoning` field when the model emits one, else a derived
398
- // fallback (tool being formed / reply prose head) so no model leaves the
399
- // live status field silent while it streams.
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
- this.streamingActivity = r;
409
- } else {
410
- const fallback = extractStreamingActivity(textSoFar);
411
- if (fallback && fallback !== this.streamingActivity) {
412
- this.streamingActivity = fallback;
413
- changed = true;
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
- return renderInputBox(display, {
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 claw beat marks the title. Flushed/final cards
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
- const wrapped = tailForWrap(text)
1201
- .split("\n")
1202
- .flatMap(l => wrapTextWithAnsi(l, wrapW))
1203
- .filter(l => l.length > 0);
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 claw beat advances every 3 ticks. Quantization keeps repaints
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 = dnaClawBeat(Math.trunc(this.tickCount / 3), this.unicode);
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 DNA Claw identity palette (emerald cyan violet helix flow). Shared by
590
- * the claw art and the forge-card border flow so the brand gradient is uniform. */
591
- export const DNA_FLOW_PALETTE: readonly string[] = ["#10ac84", "#48dbfb", "#8e44ad"];
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 "claw beat" glyph for an animation tick — the ◆/╳/○ motifs of the
594
- * claw art cycling in place. Used as the live forge-card title mark. */
595
- export function dnaClawBeat(frame: number, unicode = true): string {
596
- const beats = unicode ? ["◆", "╳", "○"] : ["*", "X", "o"];
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 DNA claw beat glyph). */
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
+ }