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.
- package/README.ja.md +160 -0
- package/README.ko.md +160 -0
- package/README.md +115 -297
- package/README.zh.md +160 -0
- package/package.json +11 -6
- package/scripts/install.sh +28 -28
- package/scripts/uninstall.sh +17 -15
- package/src/AGENTS.md +50 -0
- package/src/agent/AGENTS.md +49 -0
- package/src/agent/bash-fixups.ts +103 -0
- package/src/agent/compaction.ts +410 -19
- package/src/agent/config-schema.ts +119 -5
- package/src/agent/context-files.ts +314 -17
- package/src/agent/dev/AGENTS.md +36 -0
- package/src/agent/dev/advanced-analyzer.ts +12 -0
- package/src/agent/dev/evolution-bridge.ts +82 -0
- package/src/agent/dev/evolution-logger.ts +41 -0
- package/src/agent/dev/self-analysis.ts +64 -0
- package/src/agent/dev/self-improve.ts +24 -0
- package/src/agent/dev/spec-automation.ts +49 -0
- package/src/agent/engine.ts +804 -54
- package/src/agent/hooks.ts +273 -0
- package/src/agent/loop.ts +21 -1
- package/src/agent/memory.ts +201 -0
- package/src/agent/model-recency.ts +32 -0
- package/src/agent/output-minimizer.ts +108 -0
- package/src/agent/output-util.ts +64 -0
- package/src/agent/plan.ts +187 -0
- package/src/agent/seed.ts +52 -0
- package/src/agent/session.ts +235 -21
- package/src/agent/state.ts +286 -39
- package/src/agent/step-budget.ts +232 -0
- package/src/agent/subagents.ts +223 -26
- package/src/agent/task-tool.ts +272 -0
- package/src/agent/todo-tool.ts +87 -0
- package/src/agent/tokenizer.ts +117 -0
- package/src/agent/tool-registry.ts +54 -0
- package/src/agent/tools.ts +562 -103
- package/src/agent/web-search.ts +538 -0
- package/src/ai/AGENTS.md +44 -0
- package/src/ai/index.ts +1 -0
- package/src/ai/model-catalog-compat.ts +3 -1
- package/src/ai/model-catalog.ts +74 -9
- package/src/ai/model-discovery.ts +215 -17
- package/src/ai/model-manager.ts +346 -32
- package/src/ai/model-picker.ts +1 -1
- package/src/ai/model-registry.ts +4 -2
- package/src/ai/pricing.ts +84 -0
- package/src/ai/provider-registry.ts +23 -0
- package/src/ai/provider-status.ts +60 -16
- package/src/ai/providers/AGENTS.md +42 -0
- package/src/ai/providers/anthropic.ts +250 -31
- package/src/ai/providers/antigravity.ts +219 -0
- package/src/ai/providers/errors.ts +15 -1
- package/src/ai/providers/gemini.ts +196 -13
- package/src/ai/providers/ollama.ts +37 -7
- package/src/ai/providers/openai-responses.ts +173 -0
- package/src/ai/providers/openai.ts +64 -12
- package/src/ai/sse.ts +4 -1
- package/src/ai/types.ts +18 -1
- package/src/auth/AGENTS.md +41 -0
- package/src/auth/callback-server.ts +6 -1
- package/src/auth/flows/AGENTS.md +32 -0
- package/src/auth/flows/antigravity.ts +151 -0
- package/src/auth/flows/google-project.ts +190 -0
- package/src/auth/flows/google.ts +39 -18
- package/src/auth/flows/index.ts +15 -5
- package/src/auth/flows/openai.ts +2 -2
- package/src/auth/oauth.ts +8 -0
- package/src/auth/refresh.ts +44 -27
- package/src/auth/storage.ts +149 -26
- package/src/auth/types.ts +1 -1
- package/src/autopilot.ts +362 -0
- package/src/bun-imports.d.ts +4 -0
- package/src/cli/AGENTS.md +39 -0
- package/src/cli/runner.ts +148 -14
- package/src/cli.ts +13 -4
- package/src/commands/AGENTS.md +40 -0
- package/src/commands/approve.ts +62 -3
- package/src/commands/auth.ts +167 -25
- package/src/commands/chat.ts +37 -8
- package/src/commands/deep-interview.ts +633 -175
- package/src/commands/doctor.ts +84 -37
- package/src/commands/evolve-core.ts +18 -0
- package/src/commands/evolve.ts +2 -1
- package/src/commands/export.ts +176 -0
- package/src/commands/gjc.ts +52 -0
- package/src/commands/launch.ts +3549 -240
- package/src/commands/mcp.ts +3 -3
- package/src/commands/ooo-seed.ts +19 -0
- package/src/commands/ralplan.ts +253 -35
- package/src/commands/resume.ts +1 -1
- package/src/commands/session.ts +183 -0
- package/src/commands/setup-helpers.ts +10 -3
- package/src/commands/setup.ts +57 -16
- package/src/commands/skills.ts +78 -18
- package/src/commands/state.ts +198 -0
- package/src/commands/status.ts +84 -0
- package/src/commands/team.ts +340 -212
- package/src/commands/ultragoal.ts +122 -61
- package/src/commands/update.ts +244 -0
- package/src/ledger.ts +270 -0
- package/src/mcp/AGENTS.md +38 -0
- package/src/mcp/server.ts +115 -14
- package/src/mcp/tools.ts +42 -22
- package/src/md-modules.d.ts +4 -0
- package/src/prompts/AGENTS.md +41 -0
- package/src/prompts/agents/AGENTS.md +35 -0
- package/src/prompts/agents/architect.md +35 -0
- package/src/prompts/agents/critic.md +37 -0
- package/src/prompts/agents/executor.md +36 -0
- package/src/prompts/agents/planner.md +37 -0
- package/src/prompts/skills/AGENTS.md +36 -0
- package/src/prompts/skills/deep-dive/AGENTS.md +31 -0
- package/src/prompts/skills/deep-dive/SKILL.md +13 -0
- package/src/prompts/skills/deep-interview/AGENTS.md +31 -0
- package/src/prompts/skills/deep-interview/SKILL.md +12 -0
- package/src/prompts/skills/gjc/AGENTS.md +31 -0
- package/src/prompts/skills/gjc/SKILL.md +15 -0
- package/src/prompts/skills/ralplan/AGENTS.md +31 -0
- package/src/prompts/skills/ralplan/SKILL.md +11 -0
- package/src/prompts/skills/team/AGENTS.md +31 -0
- package/src/prompts/skills/team/SKILL.md +11 -0
- package/src/prompts/skills/ultragoal/AGENTS.md +31 -0
- package/src/prompts/skills/ultragoal/SKILL.md +11 -0
- package/src/skills/AGENTS.md +38 -0
- package/src/skills/catalog.ts +565 -31
- package/src/tui/AGENTS.md +43 -0
- package/src/tui/app.ts +1181 -92
- package/src/tui/components/AGENTS.md +42 -0
- package/src/tui/components/ascii-art.ts +257 -15
- package/src/tui/components/autocomplete.ts +98 -16
- package/src/tui/components/autopilot-status.ts +65 -0
- package/src/tui/components/category-index.ts +49 -0
- package/src/tui/components/code-view.ts +54 -11
- package/src/tui/components/color.ts +171 -2
- package/src/tui/components/config-panel.ts +82 -15
- package/src/tui/components/duration.ts +38 -0
- package/src/tui/components/evolution.ts +3 -3
- package/src/tui/components/footer.ts +91 -42
- package/src/tui/components/forge.ts +426 -31
- package/src/tui/components/hints.ts +54 -0
- package/src/tui/components/hud.ts +73 -0
- package/src/tui/components/index.ts +4 -0
- package/src/tui/components/input-box.ts +150 -0
- package/src/tui/components/layout.ts +11 -3
- package/src/tui/components/live-model-picker.ts +108 -0
- package/src/tui/components/markdown-table.ts +140 -0
- package/src/tui/components/markdown-text.ts +97 -0
- package/src/tui/components/meter.ts +4 -1
- package/src/tui/components/model-picker.ts +3 -2
- package/src/tui/components/provider-picker.ts +3 -2
- package/src/tui/components/section.ts +70 -0
- package/src/tui/components/select-list.ts +40 -10
- package/src/tui/components/skill-picker.ts +25 -0
- package/src/tui/components/slash.ts +244 -21
- package/src/tui/components/status.ts +272 -11
- package/src/tui/components/step-timeline.ts +218 -0
- package/src/tui/components/stream.ts +26 -9
- package/src/tui/components/themes.ts +212 -6
- package/src/tui/components/todo-card.ts +47 -0
- package/src/tui/components/tool-list.ts +58 -12
- package/src/tui/components/transcript.ts +120 -0
- package/src/tui/components/update-box.ts +31 -0
- package/src/tui/components/welcome.ts +162 -0
- package/src/tui/components/width.ts +163 -0
- package/src/tui/monitoring/AGENTS.md +31 -0
- package/src/tui/monitoring/hud-view.ts +55 -0
- package/src/tui/renderer.ts +112 -3
- package/src/tui/terminal.ts +40 -33
- package/src/util/AGENTS.md +39 -0
- package/src/util/clipboard-image.ts +118 -0
- package/src/util/env.ts +12 -0
- package/src/util/provider-error.ts +78 -0
- package/src/util/retry.ts +91 -6
- package/src/util/update-check.ts +64 -0
- 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
|
-
|
|
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
|
|
130
|
-
|
|
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(
|
|
177
|
+
const out = shown.map(raw => {
|
|
178
|
+
const l = sanitizeForTerminal(raw);
|
|
137
179
|
if (!color) return truncate(l, cols);
|
|
138
|
-
if (l.startsWith("+++")
|
|
139
|
-
if (l.startsWith("
|
|
140
|
-
if (l.startsWith("
|
|
141
|
-
if (l.startsWith("
|
|
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
|
|
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
|
|
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`, `/
|
|
3
|
-
* `/
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 '
|
|
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 '
|
|
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 '
|
|
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
|
|
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
|
["[. ]", "[.. ]", "[...]", "[ ..]", "[ .]", "[ ]"],
|