jeo-code 0.1.0 → 0.4.4

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 (177) hide show
  1. package/README.ja.md +160 -0
  2. package/README.ko.md +160 -0
  3. package/README.md +115 -297
  4. package/README.zh.md +160 -0
  5. package/package.json +11 -6
  6. package/scripts/install.sh +28 -28
  7. package/scripts/uninstall.sh +17 -15
  8. package/src/AGENTS.md +50 -0
  9. package/src/agent/AGENTS.md +49 -0
  10. package/src/agent/bash-fixups.ts +103 -0
  11. package/src/agent/compaction.ts +410 -19
  12. package/src/agent/config-schema.ts +119 -5
  13. package/src/agent/context-files.ts +314 -17
  14. package/src/agent/dev/AGENTS.md +36 -0
  15. package/src/agent/dev/advanced-analyzer.ts +12 -0
  16. package/src/agent/dev/evolution-bridge.ts +82 -0
  17. package/src/agent/dev/evolution-logger.ts +41 -0
  18. package/src/agent/dev/self-analysis.ts +64 -0
  19. package/src/agent/dev/self-improve.ts +24 -0
  20. package/src/agent/dev/spec-automation.ts +49 -0
  21. package/src/agent/engine.ts +804 -54
  22. package/src/agent/hooks.ts +273 -0
  23. package/src/agent/loop.ts +21 -1
  24. package/src/agent/memory.ts +201 -0
  25. package/src/agent/model-recency.ts +32 -0
  26. package/src/agent/output-minimizer.ts +108 -0
  27. package/src/agent/output-util.ts +64 -0
  28. package/src/agent/plan.ts +187 -0
  29. package/src/agent/seed.ts +52 -0
  30. package/src/agent/session.ts +235 -21
  31. package/src/agent/state.ts +286 -39
  32. package/src/agent/step-budget.ts +232 -0
  33. package/src/agent/subagents.ts +223 -26
  34. package/src/agent/task-tool.ts +272 -0
  35. package/src/agent/todo-tool.ts +87 -0
  36. package/src/agent/tokenizer.ts +117 -0
  37. package/src/agent/tool-registry.ts +54 -0
  38. package/src/agent/tools.ts +562 -103
  39. package/src/agent/web-search.ts +538 -0
  40. package/src/ai/AGENTS.md +44 -0
  41. package/src/ai/index.ts +1 -0
  42. package/src/ai/model-catalog-compat.ts +3 -1
  43. package/src/ai/model-catalog.ts +74 -9
  44. package/src/ai/model-discovery.ts +215 -17
  45. package/src/ai/model-manager.ts +346 -32
  46. package/src/ai/model-picker.ts +1 -1
  47. package/src/ai/model-registry.ts +4 -2
  48. package/src/ai/pricing.ts +84 -0
  49. package/src/ai/provider-registry.ts +23 -0
  50. package/src/ai/provider-status.ts +60 -16
  51. package/src/ai/providers/AGENTS.md +42 -0
  52. package/src/ai/providers/anthropic.ts +250 -31
  53. package/src/ai/providers/antigravity.ts +219 -0
  54. package/src/ai/providers/errors.ts +15 -1
  55. package/src/ai/providers/gemini.ts +196 -13
  56. package/src/ai/providers/ollama.ts +37 -7
  57. package/src/ai/providers/openai-responses.ts +173 -0
  58. package/src/ai/providers/openai.ts +64 -12
  59. package/src/ai/sse.ts +4 -1
  60. package/src/ai/types.ts +18 -1
  61. package/src/auth/AGENTS.md +41 -0
  62. package/src/auth/callback-server.ts +6 -1
  63. package/src/auth/flows/AGENTS.md +32 -0
  64. package/src/auth/flows/antigravity.ts +151 -0
  65. package/src/auth/flows/google-project.ts +190 -0
  66. package/src/auth/flows/google.ts +39 -18
  67. package/src/auth/flows/index.ts +15 -5
  68. package/src/auth/flows/openai.ts +2 -2
  69. package/src/auth/oauth.ts +8 -0
  70. package/src/auth/refresh.ts +44 -27
  71. package/src/auth/storage.ts +149 -26
  72. package/src/auth/types.ts +1 -1
  73. package/src/autopilot.ts +362 -0
  74. package/src/bun-imports.d.ts +4 -0
  75. package/src/cli/AGENTS.md +39 -0
  76. package/src/cli/runner.ts +148 -14
  77. package/src/cli.ts +13 -4
  78. package/src/commands/AGENTS.md +40 -0
  79. package/src/commands/approve.ts +62 -3
  80. package/src/commands/auth.ts +167 -25
  81. package/src/commands/chat.ts +37 -8
  82. package/src/commands/deep-interview.ts +633 -175
  83. package/src/commands/doctor.ts +84 -37
  84. package/src/commands/evolve-core.ts +18 -0
  85. package/src/commands/evolve.ts +2 -1
  86. package/src/commands/export.ts +176 -0
  87. package/src/commands/gjc.ts +52 -0
  88. package/src/commands/launch.ts +3549 -240
  89. package/src/commands/mcp.ts +3 -3
  90. package/src/commands/ooo-seed.ts +19 -0
  91. package/src/commands/ralplan.ts +253 -35
  92. package/src/commands/resume.ts +1 -1
  93. package/src/commands/session.ts +183 -0
  94. package/src/commands/setup-helpers.ts +10 -3
  95. package/src/commands/setup.ts +57 -16
  96. package/src/commands/skills.ts +78 -18
  97. package/src/commands/state.ts +198 -0
  98. package/src/commands/status.ts +84 -0
  99. package/src/commands/team.ts +340 -212
  100. package/src/commands/ultragoal.ts +122 -61
  101. package/src/commands/update.ts +244 -0
  102. package/src/ledger.ts +270 -0
  103. package/src/mcp/AGENTS.md +38 -0
  104. package/src/mcp/server.ts +115 -14
  105. package/src/mcp/tools.ts +42 -22
  106. package/src/md-modules.d.ts +4 -0
  107. package/src/prompts/AGENTS.md +41 -0
  108. package/src/prompts/agents/AGENTS.md +35 -0
  109. package/src/prompts/agents/architect.md +35 -0
  110. package/src/prompts/agents/critic.md +37 -0
  111. package/src/prompts/agents/executor.md +36 -0
  112. package/src/prompts/agents/planner.md +37 -0
  113. package/src/prompts/skills/AGENTS.md +36 -0
  114. package/src/prompts/skills/deep-dive/AGENTS.md +31 -0
  115. package/src/prompts/skills/deep-dive/SKILL.md +13 -0
  116. package/src/prompts/skills/deep-interview/AGENTS.md +31 -0
  117. package/src/prompts/skills/deep-interview/SKILL.md +12 -0
  118. package/src/prompts/skills/gjc/AGENTS.md +31 -0
  119. package/src/prompts/skills/gjc/SKILL.md +15 -0
  120. package/src/prompts/skills/ralplan/AGENTS.md +31 -0
  121. package/src/prompts/skills/ralplan/SKILL.md +11 -0
  122. package/src/prompts/skills/team/AGENTS.md +31 -0
  123. package/src/prompts/skills/team/SKILL.md +11 -0
  124. package/src/prompts/skills/ultragoal/AGENTS.md +31 -0
  125. package/src/prompts/skills/ultragoal/SKILL.md +11 -0
  126. package/src/skills/AGENTS.md +38 -0
  127. package/src/skills/catalog.ts +565 -31
  128. package/src/tui/AGENTS.md +43 -0
  129. package/src/tui/app.ts +1181 -92
  130. package/src/tui/components/AGENTS.md +42 -0
  131. package/src/tui/components/ascii-art.ts +257 -15
  132. package/src/tui/components/autocomplete.ts +98 -16
  133. package/src/tui/components/autopilot-status.ts +65 -0
  134. package/src/tui/components/category-index.ts +49 -0
  135. package/src/tui/components/code-view.ts +54 -11
  136. package/src/tui/components/color.ts +171 -2
  137. package/src/tui/components/config-panel.ts +82 -15
  138. package/src/tui/components/duration.ts +38 -0
  139. package/src/tui/components/evolution.ts +3 -3
  140. package/src/tui/components/footer.ts +91 -42
  141. package/src/tui/components/forge.ts +426 -31
  142. package/src/tui/components/hints.ts +54 -0
  143. package/src/tui/components/hud.ts +73 -0
  144. package/src/tui/components/index.ts +4 -0
  145. package/src/tui/components/input-box.ts +150 -0
  146. package/src/tui/components/layout.ts +11 -3
  147. package/src/tui/components/live-model-picker.ts +108 -0
  148. package/src/tui/components/markdown-table.ts +140 -0
  149. package/src/tui/components/markdown-text.ts +97 -0
  150. package/src/tui/components/meter.ts +4 -1
  151. package/src/tui/components/model-picker.ts +3 -2
  152. package/src/tui/components/provider-picker.ts +3 -2
  153. package/src/tui/components/section.ts +70 -0
  154. package/src/tui/components/select-list.ts +40 -10
  155. package/src/tui/components/skill-picker.ts +25 -0
  156. package/src/tui/components/slash.ts +244 -21
  157. package/src/tui/components/status.ts +272 -11
  158. package/src/tui/components/step-timeline.ts +218 -0
  159. package/src/tui/components/stream.ts +26 -9
  160. package/src/tui/components/themes.ts +212 -6
  161. package/src/tui/components/todo-card.ts +47 -0
  162. package/src/tui/components/tool-list.ts +58 -12
  163. package/src/tui/components/transcript.ts +120 -0
  164. package/src/tui/components/update-box.ts +31 -0
  165. package/src/tui/components/welcome.ts +162 -0
  166. package/src/tui/components/width.ts +163 -0
  167. package/src/tui/monitoring/AGENTS.md +31 -0
  168. package/src/tui/monitoring/hud-view.ts +55 -0
  169. package/src/tui/renderer.ts +112 -3
  170. package/src/tui/terminal.ts +40 -33
  171. package/src/util/AGENTS.md +39 -0
  172. package/src/util/clipboard-image.ts +118 -0
  173. package/src/util/env.ts +12 -0
  174. package/src/util/provider-error.ts +78 -0
  175. package/src/util/retry.ts +91 -6
  176. package/src/util/update-check.ts +64 -0
  177. package/src/commands/models.ts +0 -104
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import chalk from "chalk";
9
9
  import { truncate } from "../terminal";
10
+ import { diffPaint, getTheme, type EvolutionTheme } from "./themes";
10
11
 
11
12
  const LANG_BY_EXT: Record<string, string> = {
12
13
  ts: "ts", tsx: "ts", mts: "ts", cts: "ts",
@@ -80,6 +81,30 @@ export function lightHighlightLine(line: string, lang: string): string {
80
81
  return out;
81
82
  }
82
83
 
84
+ /**
85
+ * Make a line from arbitrary file/diff content safe to print in the REPL/TUI region.
86
+ * File bytes are untrusted display data: a raw `\x1b[2J`, OSC title set, lone `\r`
87
+ * progress-overwrite, or a stray C0 byte can clear the screen, move the cursor, or
88
+ * corrupt the gutter. Strip CR, expand tabs, and remove ANSI/C0 control sequences.
89
+ * jeo's own coloring is applied AFTER this, so no intended color is lost.
90
+ */
91
+ export function sanitizeForTerminal(line: string): string {
92
+ return line
93
+ .replace(/\r/g, "")
94
+ .replace(/\t/g, " ")
95
+ // OSC: ESC ] ... (BEL | ST)
96
+ .replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "")
97
+ // CSI: ESC [ ... final-byte (SGR, cursor moves, screen/line clears, etc.)
98
+ .replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
99
+ // other ESC-led two-byte sequences
100
+ .replace(/\x1b[@-Z\\-_]/g, "")
101
+ // 8-bit C1 CSI/OSC (U+009B / U+009D …) — neutralize the payload like the ESC forms
102
+ .replace(/\x9b[0-?]*[ -/]*[@-~]/g, "")
103
+ .replace(/\x9d[^\x07\x9c]*(?:\x07|\x9c)/g, "")
104
+ // any remaining C0 controls + DEL + C1 (U+0080–U+009F)
105
+ .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/g, "");
106
+ }
107
+
83
108
  export interface CodeViewOptions {
84
109
  startLine?: number;
85
110
  lang?: string;
@@ -111,14 +136,23 @@ export function formatCodeBlock(content: string, opts: CodeViewOptions = {}): st
111
136
  const out: string[] = [];
112
137
  for (let i = 0; i < shown.length; i++) {
113
138
  const no = startLine + i;
139
+ const num = String(no).padStart(gutterW, " ");
140
+ const sepGlyph = color ? chalk.gray(sep) : "|";
141
+ // File-origin content can carry hostile escapes — neutralize, then expand tabs
142
+ // so the gutter alignment is column-stable.
143
+ const body = sanitizeForTerminal(allLines[i]!).replace(/\t/g, " ");
144
+ const colored = color ? lightHighlightLine(body, lang) : body;
114
145
  const marked = highlight.has(no);
115
- const numText = String(no).padStart(gutterW);
116
- const num = color ? (marked ? chalk.yellow.bold(numText) : chalk.gray(numText)) : numText;
117
- const body = color ? lightHighlightLine(shown[i], lang) : shown[i];
118
146
  const marker = marked ? (color ? chalk.yellow("▶") : ">") : " ";
119
- const line = `${marker}${num} ${sep} ${body}`;
147
+
148
+ let prefix = " ";
149
+ if (body.startsWith("+")) prefix = color ? chalk.green("+") : "+";
150
+ else if (body.startsWith("-")) prefix = color ? chalk.red("-") : "-";
151
+
152
+ const line = `${marker}${prefix}${num} ${sepGlyph} ${colored}`;
120
153
  out.push(truncate(line, cols));
121
154
  }
155
+
122
156
  if (allLines.length > maxLines) {
123
157
  const more = allLines.length - maxLines;
124
158
  out.push(color ? chalk.gray(` …(+${more} more line${more === 1 ? "" : "s"})`) : ` …(+${more} more lines)`);
@@ -126,19 +160,28 @@ export function formatCodeBlock(content: string, opts: CodeViewOptions = {}): st
126
160
  return out;
127
161
  }
128
162
 
129
- /** Render a unified diff with +/-/@@ coloring, clamped to `cols`. */
130
- export function formatDiff(diffText: string, opts: { cols?: number; maxLines?: number; color?: boolean } = {}): string[] {
163
+ /** Render a unified diff with themed contrast: added/removed lines carry a
164
+ * foreground + full-row background tint (block-level separation, not just a
165
+ * colored sign), file heads are bold, and hunk headers get a distinct accent.
166
+ * `theme` selects the palette; the default palette applies when omitted. */
167
+ export function formatDiff(
168
+ diffText: string,
169
+ opts: { cols?: number; maxLines?: number; color?: boolean; theme?: EvolutionTheme } = {},
170
+ ): string[] {
131
171
  const cols = opts.cols ?? 100;
132
172
  const maxLines = opts.maxLines ?? 400;
133
173
  const color = opts.color !== false;
174
+ const dp = diffPaint(opts.theme ?? getTheme(undefined));
134
175
  const lines = diffText.split("\n");
135
176
  const shown = lines.slice(0, maxLines);
136
- const out = shown.map(l => {
177
+ const out = shown.map(raw => {
178
+ const l = sanitizeForTerminal(raw);
137
179
  if (!color) return truncate(l, cols);
138
- if (l.startsWith("+++") || l.startsWith("---")) return truncate(chalk.bold(l), cols);
139
- if (l.startsWith("@@")) return truncate(chalk.cyan(l), cols);
140
- if (l.startsWith("+")) return truncate(chalk.green(l), cols);
141
- if (l.startsWith("-")) return truncate(chalk.red(l), cols);
180
+ if (l.startsWith("+++")) return truncate(dp.addHead(l), cols);
181
+ if (l.startsWith("---")) return truncate(dp.delHead(l), cols);
182
+ if (l.startsWith("@@")) return truncate(dp.hunk(l), cols);
183
+ if (l.startsWith("+")) return truncate(dp.add(l), cols);
184
+ if (l.startsWith("-")) return truncate(dp.del(l), cols);
142
185
  return truncate(l, cols);
143
186
  });
144
187
  if (lines.length > maxLines) out.push(color ? chalk.gray(` …(+${lines.length - maxLines} more)`) : ` …(+${lines.length - maxLines} more)`);
@@ -11,6 +11,12 @@
11
11
  * and testable regardless of chalk's own TTY auto-detection.
12
12
  */
13
13
 
14
+ import { execSync } from "node:child_process";
15
+ import * as fs from "node:fs";
16
+ import * as path from "node:path";
17
+ import * as os from "node:os";
18
+ import { visibleWidth as widthOf } from "./width";
19
+
14
20
  /** Color capability tiers. */
15
21
  export enum ColorLevel {
16
22
  None = 0,
@@ -36,9 +42,13 @@ export function stripAnsi(s: string): string {
36
42
  return s.replace(ANSI_RE, "");
37
43
  }
38
44
 
39
- /** Visible (printable) width of a string, ignoring SGR escapes. */
45
+ /** Visible DISPLAY width of a string, ignoring SGR escapes. Delegates to the
46
+ * width-aware implementation (consensus-seed P2.B9) so CJK/emoji glyphs count 2
47
+ * columns — every box/pad that calls this (input box, forge cards, welcome,
48
+ * tables) now aligns correctly for wide-character content instead of overflowing
49
+ * the right border (the "입력창 깨짐" corruption). */
40
50
  export function visibleWidth(s: string): number {
41
- return stripAnsi(s).length;
51
+ return widthOf(s);
42
52
  }
43
53
 
44
54
  /**
@@ -144,6 +154,20 @@ export function fgEscape(c: RGB, level: ColorLevel): string {
144
154
  }
145
155
  }
146
156
 
157
+ /** Open-background escape for one color at a given level (empty at None). */
158
+ export function bgEscape(c: RGB, level: ColorLevel): string {
159
+ switch (level) {
160
+ case ColorLevel.TrueColor:
161
+ return `${ESC}48;2;${c.r};${c.g};${c.b}m`;
162
+ case ColorLevel.Ansi256:
163
+ return `${ESC}48;5;${rgbToAnsi256(c)}m`;
164
+ case ColorLevel.Basic:
165
+ return `${ESC}${rgbToAnsi16(c) + 10}m`;
166
+ default:
167
+ return "";
168
+ }
169
+ }
170
+
147
171
  /** Reset escape (empty at None). */
148
172
  export function resetEscape(level: ColorLevel): string {
149
173
  return level === ColorLevel.None ? "" : `${ESC}0m`;
@@ -170,3 +194,148 @@ export function applyGradient(text: string, from: RGB, to: RGB, level: ColorLeve
170
194
  }
171
195
  return out + resetEscape(level);
172
196
  }
197
+
198
+ /**
199
+ * Paint `text` on a left→right BACKGROUND gradient (`from`→`to`) with a fixed
200
+ * high-contrast foreground — the status-bar "highlight block" look. Unlike
201
+ * `applyGradient`, spaces ARE painted (the block must be continuous).
202
+ * At `ColorLevel.None` the plain text is returned unchanged.
203
+ */
204
+ export function applyBgGradient(
205
+ text: string,
206
+ from: RGB,
207
+ to: RGB,
208
+ level: ColorLevel = ColorLevel.TrueColor,
209
+ fg: RGB = { r: 235, g: 235, b: 235 },
210
+ ): string {
211
+ const plain = stripAnsi(text);
212
+ if (level === ColorLevel.None || plain.length === 0) return plain;
213
+ const stops = gradientStops(from, to, plain.length);
214
+ let out = fgEscape(fg, level);
215
+ for (let i = 0; i < plain.length; i++) {
216
+ out += bgEscape(stops[i]!, level) + plain[i]!;
217
+ }
218
+ return out + resetEscape(level);
219
+ }
220
+
221
+ /**
222
+ * Apply a flowing color gradient across the visible characters of `text`,
223
+ * shifted by `phase` (0..1 wraps) around the palette.
224
+ * If colorLevel < 3 (or opts.colorLevel < 3), return the text unchanged.
225
+ */
226
+ export function animatedGradientText(
227
+ text: string,
228
+ palette: readonly string[],
229
+ phase: number,
230
+ opts: { colorLevel: number }
231
+ ): string {
232
+ if (opts.colorLevel < 3) {
233
+ return text;
234
+ }
235
+ const plain = stripAnsi(text);
236
+ if (plain.length === 0) {
237
+ return plain;
238
+ }
239
+
240
+ const rgbPalette = palette.map(hex => hexToRgb(hex));
241
+ const M = rgbPalette.length;
242
+ if (M === 0) {
243
+ return plain;
244
+ }
245
+
246
+ let out = "";
247
+ const N = plain.length;
248
+
249
+ for (let i = 0; i < N; i++) {
250
+ const ch = plain[i]!;
251
+ if (ch === " ") {
252
+ out += ch;
253
+ continue;
254
+ }
255
+
256
+ const x = N > 1 ? i / (N - 1) : 0;
257
+ let t = (x + phase) % 1;
258
+ if (t < 0) t += 1;
259
+
260
+ let color: RGB;
261
+ if (M === 1) {
262
+ color = rgbPalette[0]!;
263
+ } else {
264
+ const rawSegment = t * M;
265
+ const index = Math.floor(rawSegment);
266
+ const fraction = rawSegment - index;
267
+ const colorA = rgbPalette[index % M]!;
268
+ const colorB = rgbPalette[(index + 1) % M]!;
269
+ color = lerpColor(colorA, colorB, fraction);
270
+ }
271
+
272
+ out += fgEscape(color, ColorLevel.TrueColor) + ch;
273
+ }
274
+
275
+ return out + resetEscape(ColorLevel.TrueColor);
276
+ }
277
+ /** Process-lifetime memo for `detectAppearance`: the darwin fallback shells out to
278
+ * `defaults read` (execSync ≈ 12ms — measured), and theme resolution runs on the
279
+ * keystroke-hot path. OS appearance changing mid-session is cosmetic; a fresh run
280
+ * picks it up. Keyed by COLORFGBG so a terminal that DOES advertise its palette
281
+ * is still honored per-env. */
282
+ const appearanceCache = new Map<string, "light" | "dark" | undefined>();
283
+
284
+ /** Test-only: clear the appearance memo. */
285
+ export function resetAppearanceCache(): void {
286
+ appearanceCache.clear();
287
+ }
288
+
289
+ export function detectAppearance(env: EnvLike = process.env): "light" | "dark" | undefined {
290
+ const key = env.COLORFGBG ?? "";
291
+ if (appearanceCache.has(key)) return appearanceCache.get(key);
292
+ const result = detectAppearanceUncached(env);
293
+ appearanceCache.set(key, result);
294
+ return result;
295
+ }
296
+
297
+ function detectAppearanceUncached(env: EnvLike = process.env): "light" | "dark" | undefined {
298
+ const colorfgbg = env.COLORFGBG;
299
+ if (colorfgbg) {
300
+ const parts = colorfgbg.split(";");
301
+ if (parts.length > 1) {
302
+ const bgStr = parts[parts.length - 1]!.trim();
303
+ const bg = parseInt(bgStr, 10);
304
+ if (!isNaN(bg)) {
305
+ if (bg >= 0 && bg <= 6) return "dark";
306
+ if (bg === 8) return "dark";
307
+ if (bg === 7) return "light";
308
+ if (bg >= 9 && bg <= 15) return "light";
309
+ if (bg >= 232 && bg <= 243) return "dark";
310
+ if (bg >= 244 && bg <= 255) return "light";
311
+ if (bg >= 16 && bg <= 231) {
312
+ const code = bg - 16;
313
+ const b = code % 6;
314
+ const g = Math.floor((code % 36) / 6);
315
+ const r = Math.floor(code / 36);
316
+ const R = r * 51;
317
+ const G = g * 51;
318
+ const B = b * 51;
319
+ const Y = 0.299 * R + 0.587 * G + 0.114 * B;
320
+ return Y < 128 ? "dark" : "light";
321
+ }
322
+ }
323
+ }
324
+ }
325
+
326
+ if (process.platform === "darwin") {
327
+ try {
328
+ const style = execSync("defaults read -g AppleInterfaceStyle", { stdio: ["ignore", "pipe", "ignore"] })
329
+ .toString()
330
+ .trim();
331
+ if (style === "Dark") {
332
+ return "dark";
333
+ }
334
+ return "light";
335
+ } catch (e) {
336
+ return "light";
337
+ }
338
+ }
339
+
340
+ return undefined;
341
+ }
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Pure formatters for the TUI configuration panels (`/model`, `/models`,
3
- * `/provider`, `/agents`, `/config`). Each returns plain string lines (color via
2
+ * Pure formatters for the TUI configuration panels (`/model`, `/provider`,
3
+ * `/agents`, `/config`). Each returns plain string lines (color via
4
4
  * chalk) so it can be unit-tested with an ANSI-stripping helper and reused by
5
5
  * both the interactive REPL and one-shot commands.
6
6
  */
@@ -11,7 +11,7 @@ import type { ProviderModelsResult } from "../../ai/model-discovery";
11
11
  import type { PickEntry } from "../../ai/model-picker";
12
12
  import type { CatalogModel } from "../../ai/model-catalog";
13
13
  import type { EnrichedModel } from "../../ai/model-enrich";
14
- import { formatTokens } from "../../ai/model-catalog";
14
+ import { catalogMetadata, formatTokens, companyLabel } from "../../ai/model-catalog";
15
15
 
16
16
  /** A single "Model: alias → resolved (provider)" status line. */
17
17
  export function formatModelLine(d: {
@@ -22,7 +22,7 @@ export function formatModelLine(d: {
22
22
  }): string {
23
23
  const expansion = d.resolved !== d.label ? ` → ${d.resolved}` : "";
24
24
  const readyMark = d.ready === undefined ? "" : d.ready ? ` ${chalk.green("✓")}` : ` ${chalk.yellow("· no credential")}`;
25
- return `${chalk.bold(d.label)}${expansion} ${chalk.gray(`(${d.provider})`)}${readyMark}`;
25
+ return `${chalk.bold(d.label)}${expansion} ${chalk.gray(`(${d.provider} · ${companyLabel(d.provider)})`)}${readyMark}`;
26
26
  }
27
27
 
28
28
  /** Aliases section: ` alias → target` lines, sorted by alias. */
@@ -36,38 +36,41 @@ export function formatAliasLines(aliases: Record<string, string>): string[] {
36
36
  /** Provider credential table: ` name ✓ label [baseUrl]`. */
37
37
  export function formatProviderPanel(statuses: ProviderStatus[]): string[] {
38
38
  if (statuses.length === 0) return [" (no providers)"];
39
- const width = Math.max(...statuses.map(s => s.name.length), 6);
39
+ const nameWithCompany = (name: string) => `${name} (${companyLabel(name)})`;
40
+ const width = Math.max(...statuses.map(s => nameWithCompany(s.name).length), 6);
40
41
  return statuses.map(s => {
41
42
  const mark = s.ready ? chalk.green("✓") : chalk.gray("·");
42
43
  const base = s.baseUrl ? chalk.gray(` [${s.baseUrl}]`) : "";
43
44
  const label = s.ready ? s.label : chalk.yellow(s.label);
44
- return ` ${s.name.padEnd(width)} ${mark} ${label}${base}`;
45
+ return ` ${nameWithCompany(s.name).padEnd(width)} ${mark} ${label}${base}`;
45
46
  });
46
47
  }
47
48
 
48
- /** Subagent roster: ` id title — model ≤N steps (read-only)`. */
49
+ /** Subagent roster: ` id title — model · thinking ≤N steps (read-only)`. */
49
50
  export function formatAgentsPanel(
50
51
  roles: readonly SubagentRole[],
51
- resolve: (role: SubagentRole) => { model: string; maxSteps: number },
52
+ resolve: (role: SubagentRole) => { model: string; maxSteps: number; thinking?: string },
52
53
  ): string[] {
53
54
  if (roles.length === 0) return [" (no subagent roles)"];
54
55
  const width = Math.max(...roles.map(r => r.id.length), 8);
55
56
  return roles.map(r => {
56
- const { model, maxSteps } = resolve(r);
57
+ const { model, maxSteps, thinking } = resolve(r);
57
58
  const ro = r.readOnly ? chalk.gray(" (read-only)") : "";
58
- return ` ${chalk.cyan(r.id.padEnd(width))} ${r.title} ${model} ≤${maxSteps} steps${ro}`;
59
+ const think = chalk.dim(`(${thinking ?? "inherit"})`);
60
+ return ` ${chalk.cyan(r.id.padEnd(width))} ${r.title} — ${model} ${think} ≤${maxSteps} steps${ro}`;
59
61
  });
60
62
  }
61
63
 
62
64
  /** Detail block for a single subagent role. */
63
65
  export function formatAgentDetail(
64
66
  role: SubagentRole,
65
- resolved: { model: string; maxSteps: number },
67
+ resolved: { model: string; maxSteps: number; thinking?: string },
66
68
  ): string[] {
67
69
  return [
68
70
  `${chalk.cyan(role.id)} — ${role.title}`,
69
71
  ` ${role.description}`,
70
72
  ` model: ${resolved.model}`,
73
+ ` thinking: ${resolved.thinking ?? "inherit (follows the default thinking level)"}`,
71
74
  ` maxSteps: ${resolved.maxSteps}`,
72
75
  ` mutates: ${role.readOnly ? "no (read-only: read/find/search only)" : "yes (full toolset)"}`,
73
76
  ];
@@ -111,14 +114,15 @@ export function formatLiveModels(
111
114
  lines.push(`${chalk.bold(r.provider)} ${chalk.gray(`(${r.source})`)}: ${chalk.yellow(r.error ?? "unavailable")}`);
112
115
  continue;
113
116
  }
114
- lines.push(`${chalk.bold(r.provider)} ${chalk.gray(`(${r.source})`)}: ${r.models.length} model${r.models.length === 1 ? "" : "s"}`);
117
+ const tag = r.fallback ? chalk.gray(" · catalog (live list endpoint unavailable)") : "";
118
+ lines.push(`${chalk.bold(r.provider)} ${chalk.gray(`(${r.source})`)}: ${r.models.length} model${r.models.length === 1 ? "" : "s"}${tag}`);
115
119
  for (const m of r.models.slice(0, cap)) {
116
120
  const mark = opts.current && m === opts.current ? chalk.green(" ◀ current") : "";
117
121
  lines.push(` ${m}${mark}`);
118
122
  }
119
123
  if (r.models.length > cap) lines.push(chalk.gray(` …(+${r.models.length - cap} more)`));
120
124
  }
121
- if (lines.length === 0) lines.push(" (no live models — log in with 'joc auth login' or start Ollama)");
125
+ if (lines.length === 0) lines.push(" (no live models — log in with 'jeo auth login' or start Ollama)");
122
126
  return lines;
123
127
  }
124
128
 
@@ -132,7 +136,7 @@ export function liveModelKnown(results: ProviderModelsResult[], model: string):
132
136
  * The active model (if any) is marked.
133
137
  */
134
138
  export function formatPickList(entries: PickEntry[], opts: { current?: string; cap?: number } = {}): string[] {
135
- if (entries.length === 0) return [" (no models — log in with 'joc auth login' or start Ollama)"];
139
+ if (entries.length === 0) return [" (no models — log in with 'jeo auth login' or start Ollama)"];
136
140
  const cap = opts.cap ?? 60;
137
141
  const width = String(Math.min(entries.length, cap)).length + 1; // "#" + digits
138
142
  const lines = entries.slice(0, cap).map(e => {
@@ -144,6 +148,35 @@ export function formatPickList(entries: PickEntry[], opts: { current?: string; c
144
148
  return lines;
145
149
  }
146
150
 
151
+ /**
152
+ * Numbered pick list with GJC-style capability columns. This is the setting-flow
153
+ * view: every row has a stable `#N` token and the live/OAuth model id is
154
+ * annotated with catalog metadata when known.
155
+ */
156
+ export function formatPickListWithCapabilities(entries: PickEntry[], opts: { current?: string; cap?: number } = {}): string[] {
157
+ if (entries.length === 0) return [" (no models — log in with 'jeo auth login' or start Ollama)"];
158
+ const cap = opts.cap ?? 50;
159
+ const shown = entries.slice(0, cap);
160
+ const iw = String(Math.min(entries.length, cap)).length + 1;
161
+ const pw = Math.max(...shown.map(e => e.provider.length), 8);
162
+ const mw = Math.min(Math.max(...shown.map(e => e.model.length), 6), 36);
163
+ const lines = [` ${"#".padStart(iw)} ${"provider".padEnd(pw)} ${"model".padEnd(mw)} ${"ctx".padStart(5)} ${"out".padStart(5)} thinking img`];
164
+ for (const e of shown) {
165
+ const meta = catalogMetadata(e.model);
166
+ const ctx = meta ? formatTokens(meta.contextTokens) : "-";
167
+ const out = meta ? formatTokens(meta.maxOutputTokens) : "-";
168
+ const think = meta ? thinkCell(meta.thinking) : "?";
169
+ const img = meta ? (meta.images ? "yes" : "no") : "?";
170
+ const id = e.model.length > mw ? e.model.slice(0, mw - 1) + "…" : e.model.padEnd(mw);
171
+ const mark = opts.current && e.model === opts.current ? chalk.green(" ◀ current") : "";
172
+ lines.push(
173
+ ` ${chalk.yellow(`#${e.index}`.padStart(iw))} ${chalk.gray(e.provider.padEnd(pw))} ${id} ${ctx.padStart(5)} ${out.padStart(5)} ${chalk.cyan(think)} ${img}${mark}`,
174
+ );
175
+ }
176
+ if (entries.length > cap) lines.push(chalk.gray(` …(+${entries.length - cap} more — narrow with /provider <name>)`));
177
+ return lines;
178
+ }
179
+
147
180
  function thinkCell(levels: string[]): string {
148
181
  return levels.length ? levels.join(",") : "-";
149
182
  }
@@ -163,6 +196,40 @@ export function formatCatalogTable(models: CatalogModel[], opts: { current?: str
163
196
  return lines;
164
197
  }
165
198
 
199
+ /**
200
+ * Canonical catalog table used by the `/model` and `/provider` pickers:
201
+ * canonical id, selected provider model, variant count, context, and max output.
202
+ */
203
+ export function formatCanonicalCatalogTable(models: CatalogModel[], opts: { current?: string; cap?: number } = {}): string[] {
204
+ if (models.length === 0) return [" (no catalog matches)"];
205
+ const grouped = new Map<string, CatalogModel[]>();
206
+ for (const m of models) grouped.set(m.canonical, [...(grouped.get(m.canonical) ?? []), m]);
207
+ const rows = [...grouped.entries()].map(([canonical, variants]) => {
208
+ const selected =
209
+ variants.find(m => opts.current && (m.canonical === opts.current || m.providerModel === opts.current || `${m.provider}/${m.providerModel}` === opts.current)) ??
210
+ variants[0]!;
211
+ const selectedId = selected.providerModel.startsWith(`${selected.provider}/`)
212
+ ? selected.providerModel
213
+ : `${selected.provider}/${selected.providerModel}`;
214
+ return { canonical, selected, selectedId, variants: variants.length };
215
+ });
216
+ const cap = opts.cap ?? 50;
217
+ const shown = rows.slice(0, cap);
218
+ const cw = Math.min(Math.max(...shown.map(r => r.canonical.length), 9), 36);
219
+ const sw = Math.min(Math.max(...shown.map(r => r.selectedId.length), 8), 42);
220
+ const lines = [` ${"canonical".padEnd(cw)} ${"selected".padEnd(sw)} variants ${"context".padStart(7)} ${"max-out".padStart(7)}`];
221
+ for (const r of shown) {
222
+ const canonical = r.canonical.length > cw ? r.canonical.slice(0, cw - 1) + "…" : r.canonical.padEnd(cw);
223
+ const selectedId = r.selectedId.length > sw ? r.selectedId.slice(0, sw - 1) + "…" : r.selectedId.padEnd(sw);
224
+ const mark = opts.current && (r.selected.canonical === opts.current || r.selected.providerModel === opts.current || r.selectedId === opts.current) ? chalk.green(" ◀") : "";
225
+ lines.push(
226
+ ` ${canonical} ${selectedId} ${String(r.variants).padStart(8)} ${formatTokens(r.selected.contextTokens).padStart(7)} ${formatTokens(r.selected.maxOutputTokens).padStart(7)}${mark}`,
227
+ );
228
+ }
229
+ if (rows.length > cap) lines.push(chalk.gray(` …(+${rows.length - cap} more)`));
230
+ return lines;
231
+ }
232
+
166
233
  /** One-line capability summary for a single model, e.g. for `/model` output. */
167
234
  export function formatCapabilityLine(m: CatalogModel): string {
168
235
  return `${chalk.gray("caps:")} ctx ${formatTokens(m.contextTokens)} · out ${formatTokens(m.maxOutputTokens)} · thinking ${thinkCell(m.thinking)} · images ${m.images ? "yes" : "no"}`;
@@ -173,7 +240,7 @@ export function formatCapabilityLine(m: CatalogModel): string {
173
240
  * context/out/thinking/img when the catalog knows them, "-" otherwise.
174
241
  */
175
242
  export function formatEnrichedModels(models: EnrichedModel[], opts: { current?: string; cap?: number } = {}): string[] {
176
- if (models.length === 0) return [" (no live models — log in with 'joc auth login' or start Ollama)"];
243
+ if (models.length === 0) return [" (no live models — log in with 'jeo auth login' or start Ollama)"];
177
244
  const cap = opts.cap ?? 50;
178
245
  const shown = models.slice(0, cap);
179
246
  const pw = Math.max(...shown.map(m => m.provider.length), 8);
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Human-scale duration + token-usage formatting for status lines.
3
+ *
4
+ * gjc renders elapsed time at minute granularity once a turn crosses 60s
5
+ * ("took 3 steps in 1m 45s"), and surfaces live token usage per turn. These
6
+ * helpers are pure so both the TUI footer/summary and the plain stream sink
7
+ * share one formatting contract.
8
+ */
9
+
10
+ /** Format milliseconds as "42s", "1m 45s", "12m", or "1h 2m" (minute-first past 60s). */
11
+ export function formatDuration(ms: number): string {
12
+ const totalSecs = Math.max(0, Math.round(ms / 1000));
13
+ if (totalSecs < 60) return `${totalSecs}s`;
14
+ const totalMins = Math.floor(totalSecs / 60);
15
+ const secs = totalSecs % 60;
16
+ if (totalMins < 60) return secs ? `${totalMins}m ${secs}s` : `${totalMins}m`;
17
+ const hours = Math.floor(totalMins / 60);
18
+ const mins = totalMins % 60;
19
+ return mins ? `${hours}h ${mins}m` : `${hours}h`;
20
+ }
21
+
22
+ /** Compact token count: 950 → "950", 12_345 → "12.3k", 1_234_567 → "1.2M". */
23
+ export function formatTokenCount(n: number): string {
24
+ if (!Number.isFinite(n) || n < 0) return "0";
25
+ if (n < 1000) return String(Math.round(n));
26
+ if (n < 1_000_000) {
27
+ const k = n / 1000;
28
+ return `${k >= 100 ? Math.round(k) : Math.round(k * 10) / 10}k`;
29
+ }
30
+ const m = n / 1_000_000;
31
+ return `${m >= 100 ? Math.round(m) : Math.round(m * 10) / 10}M`;
32
+ }
33
+
34
+ /** "12.3k in / 1.2k out tokens" — empty string when usage was never reported. */
35
+ export function formatUsage(usage?: { inputTokens?: number; outputTokens?: number }): string {
36
+ if (!usage || (usage.inputTokens == null && usage.outputTokens == null)) return "";
37
+ return `${formatTokenCount(usage.inputTokens ?? 0)} in / ${formatTokenCount(usage.outputTokens ?? 0)} out tokens`;
38
+ }
@@ -1,7 +1,7 @@
1
1
  import chalk from "chalk";
2
2
 
3
3
  /**
4
- * Canonical "evolution" model for the joc TUI (single source of truth).
4
+ * Canonical "evolution" model for the jeo TUI (single source of truth).
5
5
  *
6
6
  * Every evolving surface — the ASCII art, the spinner, the progress meter, and
7
7
  * the footer track — derives its stage from the functions and tables here, so a
@@ -32,7 +32,7 @@ export const EVOLUTION_STAGE_COLORS: readonly ((s: string) => string)[] = [
32
32
 
33
33
  /** Spinner frame sets, one per evolution stage. */
34
34
  export const EVOLUTION_SPINNER_FRAMES: readonly string[][] = [
35
- [".", "..", "...", "....", "...", ".."],
35
+ [". ", ".. ", "... ", "....", "... ", ".. "],
36
36
  ["\u2801", "\u2802", "\u2804", "\u2808", "\u2810", "\u2820"],
37
37
  ["|", "/", "-", "\\"],
38
38
  ["\u280b", "\u2819", "\u2839", "\u2838", "\u283c", "\u2834", "\u2826", "\u2827", "\u2807", "\u280f"],
@@ -45,7 +45,7 @@ export const EVOLUTION_SPINNER_FRAMES: readonly string[][] = [
45
45
  * `EVOLUTION_SPINNER_FRAMES`.
46
46
  */
47
47
  export const EVOLUTION_SPINNER_FRAMES_ASCII: readonly string[][] = [
48
- [".", "..", "...", "....", "...", ".."],
48
+ [". ", ".. ", "... ", "....", "... ", ".. "],
49
49
  ["-", "=", "~", "="],
50
50
  ["|", "/", "-", "\\"],
51
51
  ["[. ]", "[.. ]", "[...]", "[ ..]", "[ .]", "[ ]"],