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
|
@@ -1,7 +1,115 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
-
import {
|
|
2
|
+
import { categoryBadge } from "./category-index";
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
import { animatedGradientText, applyBgGradient, hexToRgb, visibleWidth, ColorLevel } from "./color";
|
|
5
|
+
import * as os from "node:os";
|
|
6
|
+
import { formatUsage } from "./duration";
|
|
7
|
+
import { formatCost } from "../../ai/pricing";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* One-row status bar pinned directly above the boxed input (gjc-layout parity):
|
|
11
|
+
* <bg-gradient block: ⬢ model · ◔ thinking / ⑂ branch *D ?N / ▸ cwd> … ⤴ R/s · ctx P%/MaxM
|
|
12
|
+
* The left identity segment rides the theme's gradient as a BACKGROUND block;
|
|
13
|
+
* right-side live stats stay plain so they read at a glance.
|
|
14
|
+
*/
|
|
15
|
+
export interface StatusBarData {
|
|
16
|
+
model: string;
|
|
17
|
+
/** Thinking level label ("high", …); omitted when unset. */
|
|
18
|
+
thinking?: string;
|
|
19
|
+
branch?: string;
|
|
20
|
+
/** Uncommitted-change count for the `?N` dirty flag; omit/0 = clean. */
|
|
21
|
+
dirtyCount?: number;
|
|
22
|
+
cwd?: string;
|
|
23
|
+
/** Live output-token rate (tokens/s); omitted when not in a turn. */
|
|
24
|
+
rate?: number;
|
|
25
|
+
/** Estimated context usage 0-100 (%), when known. */
|
|
26
|
+
ctxPct?: number;
|
|
27
|
+
/** Context window in tokens, when known. */
|
|
28
|
+
ctxMaxTokens?: number;
|
|
29
|
+
cols: number;
|
|
30
|
+
unicode?: boolean;
|
|
31
|
+
color?: boolean;
|
|
32
|
+
colorLevel?: ColorLevel;
|
|
33
|
+
/** Theme gradient (from→to hex) for the left segment background. */
|
|
34
|
+
gradient?: { from: string; to: string };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function shortTokens(n: number): string {
|
|
38
|
+
if (n >= 1_000_000) {
|
|
39
|
+
const v = (n / 1_000_000).toFixed(1);
|
|
40
|
+
return (v.endsWith(".0") ? v.slice(0, -2) : v) + "M";
|
|
41
|
+
}
|
|
42
|
+
if (n >= 1000) return Math.round(n / 1000) + "k";
|
|
43
|
+
return Math.round(n).toString();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function shortenCwd(cwd: string, maxLen: number, unicode: boolean): string {
|
|
47
|
+
let s = cwd;
|
|
48
|
+
const home = os.homedir();
|
|
49
|
+
if (home && (s === home || s.startsWith(home + "/") || s.startsWith(home + "\\"))) {
|
|
50
|
+
s = "~" + s.slice(home.length);
|
|
51
|
+
}
|
|
52
|
+
if (s.length <= maxLen) return s;
|
|
53
|
+
const ell = unicode ? "…" : "...";
|
|
54
|
+
return ell + s.slice(-(Math.max(1, maxLen - ell.length)));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function renderStatusBar(d: StatusBarData): string {
|
|
58
|
+
const unicode = d.unicode !== false;
|
|
59
|
+
const useColor = d.color !== false;
|
|
60
|
+
const cols = Math.max(24, Math.trunc(d.cols));
|
|
61
|
+
const sep = " / ";
|
|
62
|
+
|
|
63
|
+
// Right side first — it has priority and fixed width.
|
|
64
|
+
const rightParts: string[] = [];
|
|
65
|
+
if (typeof d.rate === "number" && Number.isFinite(d.rate) && d.rate > 0) {
|
|
66
|
+
rightParts.push(`${unicode ? "⤴" : "^"} ${d.rate >= 100 ? d.rate.toFixed(0) : d.rate.toFixed(1)}/s`);
|
|
67
|
+
}
|
|
68
|
+
if (typeof d.ctxPct === "number" && Number.isFinite(d.ctxPct)) {
|
|
69
|
+
const pct = Math.max(0, Math.min(999, Math.round(d.ctxPct)));
|
|
70
|
+
const cap = d.ctxMaxTokens && d.ctxMaxTokens > 0 ? `/${shortTokens(d.ctxMaxTokens)}` : "";
|
|
71
|
+
rightParts.push(`${unicode ? "▦" : "#"} ${pct}%${cap}`);
|
|
72
|
+
}
|
|
73
|
+
let right = rightParts.join(" · ");
|
|
74
|
+
if (useColor && right) {
|
|
75
|
+
const pct = d.ctxPct ?? 0;
|
|
76
|
+
right = pct >= 85 ? chalk.red(right) : pct >= 60 ? chalk.yellow(right) : chalk.gray(right);
|
|
77
|
+
}
|
|
78
|
+
const rightWidth = rightParts.length ? visibleWidth(rightParts.join(" · ")) : 0;
|
|
79
|
+
|
|
80
|
+
// Left identity segment (plain text; painted as one bg block at the end).
|
|
81
|
+
const bits: string[] = [];
|
|
82
|
+
let modelBit = `${unicode ? "⬢" : "*"} ${d.model}`;
|
|
83
|
+
if (d.thinking) modelBit += ` · ${unicode ? "◔" : "@"} ${d.thinking}`;
|
|
84
|
+
bits.push(modelBit);
|
|
85
|
+
if (d.branch) {
|
|
86
|
+
const dirty = d.dirtyCount && d.dirtyCount > 0 ? ` ?${d.dirtyCount}` : "";
|
|
87
|
+
bits.push(`${unicode ? "⑂" : "y"} ${d.branch}${dirty}`);
|
|
88
|
+
}
|
|
89
|
+
// Budget the cwd into whatever width remains (right stats + 2-col gap reserved).
|
|
90
|
+
const leftBudget = Math.max(8, cols - rightWidth - (rightWidth > 0 ? 2 : 0));
|
|
91
|
+
if (d.cwd) {
|
|
92
|
+
const used = visibleWidth(` ${bits.join(sep)}${sep}${unicode ? "▸" : ">"} `);
|
|
93
|
+
const room = leftBudget - used - 1;
|
|
94
|
+
if (room >= 4) bits.push(`${unicode ? "▸" : ">"} ${shortenCwd(d.cwd, room, unicode)}`);
|
|
95
|
+
}
|
|
96
|
+
let left = ` ${bits.join(sep)} `;
|
|
97
|
+
if (visibleWidth(left) > leftBudget) left = left.slice(0, leftBudget);
|
|
98
|
+
|
|
99
|
+
const gap = Math.max(0, cols - visibleWidth(left) - rightWidth);
|
|
100
|
+
const level = d.colorLevel ?? (useColor ? ColorLevel.TrueColor : ColorLevel.None);
|
|
101
|
+
const grad = d.gradient ?? { from: "#0a3d62", to: "#48dbfb" };
|
|
102
|
+
const paintedLeft = useColor
|
|
103
|
+
? applyBgGradient(left, hexToRgb(grad.from), hexToRgb(grad.to), level)
|
|
104
|
+
: left;
|
|
105
|
+
return `${paintedLeft}${" ".repeat(gap)}${right}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface JeoStatusData {
|
|
109
|
+
colorLevel?: number;
|
|
110
|
+
phase?: number;
|
|
111
|
+
palette?: readonly string[];
|
|
112
|
+
isThinking?: boolean;
|
|
5
113
|
step?: number;
|
|
6
114
|
maxSteps?: number;
|
|
7
115
|
elapsedMs?: number;
|
|
@@ -13,6 +121,20 @@ export interface JocStatusData {
|
|
|
13
121
|
totalCount?: number;
|
|
14
122
|
mutationGuarded?: boolean;
|
|
15
123
|
unicode?: boolean;
|
|
124
|
+
color?: boolean;
|
|
125
|
+
/** Compact evolution-stage identity (e.g. "●●○○○ Double Helix (DNA) [2/5]") shown in the
|
|
126
|
+
* forge line so the current stage — the double helix — is always exposed, even when the
|
|
127
|
+
* large ASCII art is dropped on short terminals. */
|
|
128
|
+
stage?: string;
|
|
129
|
+
stepElapsedMs?: number;
|
|
130
|
+
avgStepMs?: number;
|
|
131
|
+
/** Cumulative turn token usage (engine onUsage) shown live on the [STEP] row. */
|
|
132
|
+
usage?: { inputTokens: number; outputTokens: number } | null;
|
|
133
|
+
/** Live USD cost for the turn (price table × usage); omit when the model has no known price. */
|
|
134
|
+
costUsd?: number;
|
|
135
|
+
/** True while a delegated subagent turn is in flight — renders gjc's `(sub)` marker. */
|
|
136
|
+
subagentActive?: boolean;
|
|
137
|
+
|
|
16
138
|
}
|
|
17
139
|
|
|
18
140
|
export function progressPercent(step: number | undefined, maxSteps: number | undefined): number {
|
|
@@ -24,22 +146,161 @@ function seconds(ms: number | undefined): number {
|
|
|
24
146
|
return Number.isFinite(ms) && (ms ?? 0) > 0 ? Math.round((ms ?? 0) / 1000) : 0;
|
|
25
147
|
}
|
|
26
148
|
|
|
27
|
-
export function
|
|
28
|
-
const
|
|
29
|
-
const max = Number.isFinite(data.maxSteps) && (data.maxSteps ?? 0) > 0 ? Math.trunc(data.maxSteps ?? 0) : 0;
|
|
30
|
-
const pct = progressPercent(step, max);
|
|
31
|
-
const bar = meter(step, max || 1, 10, { unicode: data.unicode !== false });
|
|
149
|
+
export function renderJeoStatus(data: JeoStatusData): string[] {
|
|
150
|
+
const useColor = data.color !== false;
|
|
32
151
|
const elapsed = `${seconds(data.elapsedMs)}s`;
|
|
33
|
-
|
|
152
|
+
let msg = data.message ?? "thinking through the next tool call";
|
|
153
|
+
const level = data.colorLevel ?? (useColor ? ColorLevel.TrueColor : ColorLevel.None);
|
|
154
|
+
if (useColor && data.isThinking && level === ColorLevel.TrueColor && data.palette && data.palette.length > 0) {
|
|
155
|
+
const phase = data.phase ?? 0;
|
|
156
|
+
msg = animatedGradientText(msg, data.palette, phase, { colorLevel: level });
|
|
157
|
+
}
|
|
34
158
|
const current = data.currentTool ? `forging ${data.currentTool}` : "forge idle";
|
|
159
|
+
const stage = data.stage ? `${data.stage} · ` : "";
|
|
35
160
|
const ok = data.okCount ?? 0;
|
|
36
161
|
const fail = data.failCount ?? 0;
|
|
37
162
|
const running = data.runningCount ?? 0;
|
|
38
163
|
const total = data.totalCount ?? ok + fail + running;
|
|
39
|
-
const guard = data.mutationGuarded ? ` · ${chalk.red.bold("mutation locked")}` : "";
|
|
40
164
|
|
|
165
|
+
const cyanBold = useColor ? chalk.cyan.bold : (s: string) => s;
|
|
166
|
+
const magentaBold = useColor ? chalk.magenta.bold : (s: string) => s;
|
|
167
|
+
const redBold = useColor ? chalk.red.bold : (s: string) => s;
|
|
168
|
+
const green = useColor ? chalk.green : (s: string) => s;
|
|
169
|
+
const yellow = useColor ? chalk.yellow : (s: string) => s;
|
|
170
|
+
const red = useColor ? chalk.red : (s: string) => s;
|
|
171
|
+
const toolCounts = `${green(`${ok} ok`)} / ${red(`${fail} fail`)} / ${yellow(`${running} running`)}`;
|
|
172
|
+
|
|
173
|
+
const guard = data.mutationGuarded ? ` · ${redBold("mutation locked")}` : "";
|
|
174
|
+
let extraStats = "";
|
|
175
|
+
if (Number.isFinite(data.stepElapsedMs)) {
|
|
176
|
+
extraStats += ` · now ${(data.stepElapsedMs! / 1000).toFixed(1)}s`;
|
|
177
|
+
}
|
|
178
|
+
if (Number.isFinite(data.avgStepMs)) {
|
|
179
|
+
extraStats += ` · avg ${(data.avgStepMs! / 1000).toFixed(1)}s`;
|
|
180
|
+
}
|
|
181
|
+
// Live token spend for the turn — visible per step, not only in the final summary.
|
|
182
|
+
if (data.usage && (data.usage.inputTokens || data.usage.outputTokens)) {
|
|
183
|
+
extraStats += ` · ${formatUsage(data.usage)}`;
|
|
184
|
+
// gjc-parity live output-token rate (logs/gjc-tui-study analysis Gap B):
|
|
185
|
+
// `⤴ N.N/s` like gjc's HUD. Derived purely from existing usage + elapsed —
|
|
186
|
+
// no new data sources; gated past the first second so a fresh turn doesn't
|
|
187
|
+
// flash a meaningless spike.
|
|
188
|
+
const elapsedSec = (data.elapsedMs ?? 0) / 1000;
|
|
189
|
+
if (elapsedSec >= 1 && (data.usage.outputTokens ?? 0) > 0) {
|
|
190
|
+
const rate = data.usage.outputTokens / elapsedSec;
|
|
191
|
+
const glyph = data.unicode !== false ? "⤴" : "^";
|
|
192
|
+
extraStats += ` · ${glyph} ${rate >= 100 ? rate.toFixed(0) : rate.toFixed(1)}/s`;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// Live USD cost (gjc parity `$0.42 (sub)`): only when a known price produced a figure.
|
|
196
|
+
if (typeof data.costUsd === "number" && Number.isFinite(data.costUsd) && data.costUsd > 0) {
|
|
197
|
+
extraStats += ` · ${formatCost(data.costUsd)}`;
|
|
198
|
+
}
|
|
199
|
+
if (data.subagentActive) {
|
|
200
|
+
extraStats += " (sub)";
|
|
201
|
+
}
|
|
202
|
+
// No step counter / step-driven meter here: with the dynamic step budget the
|
|
203
|
+
// denominator keeps extending, so `step n/m` carried no information (user feedback).
|
|
41
204
|
return [
|
|
42
|
-
` ${
|
|
43
|
-
` ${
|
|
205
|
+
` ${categoryBadge("progress", { color: useColor })} elapsed ${elapsed}${extraStats}`,
|
|
206
|
+
` ${categoryBadge("status", { color: useColor })} ${cyanBold("jeo status")} · ${msg}`,
|
|
207
|
+
` ${categoryBadge("tool", { color: useColor })} ${magentaBold("jeo forge")} · ${stage}${current} · tools ${total} (${toolCounts})${guard}`,
|
|
44
208
|
];
|
|
209
|
+
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface StatusBoxData extends JeoStatusData {
|
|
213
|
+
/** Frame width the status lines should fit. */
|
|
214
|
+
cols: number;
|
|
215
|
+
/** Phase label leading the activity row (thinking/planning/executing/reporting). */
|
|
216
|
+
phaseLabel?: string;
|
|
217
|
+
/** Current spinner frame glyph. */
|
|
218
|
+
spinner?: string;
|
|
219
|
+
/** Live streamed model activity (reasoning/uniform fallback); overrides `message`. */
|
|
220
|
+
activity?: string;
|
|
221
|
+
/** Append gjc's ⟦esc⟧ cancel hint to the activity row. */
|
|
222
|
+
escHint?: boolean;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Live status lines — UNBOXED (user feedback: the message/status must not be
|
|
227
|
+
* trapped inside a border). A fixed two-row layout:
|
|
228
|
+
*
|
|
229
|
+
* ⠙ thinking · <live reasoning / current activity> ⟦esc⟧
|
|
230
|
+
* 12s · now 2.5s · avg 2.5s · 8.2k in / 30 out · ⤴ 6/s · 2 ok
|
|
231
|
+
*
|
|
232
|
+
* The spinner + dim phase label lead the live thinking text, the cancel hint is
|
|
233
|
+
* right-aligned, and one compact dim metrics row folds timing/usage/rate/cost/
|
|
234
|
+
* tool-counts together. Step counters and the step-driven meter are deliberately
|
|
235
|
+
* absent: the dynamic step budget keeps extending the denominator, so `step n/m`
|
|
236
|
+
* carried no information.
|
|
237
|
+
*/
|
|
238
|
+
export function renderStatusBox(data: StatusBoxData): string[] {
|
|
239
|
+
const unicode = data.unicode !== false;
|
|
240
|
+
const useColor = data.color !== false;
|
|
241
|
+
const cols = Math.max(24, Math.trunc(data.cols));
|
|
242
|
+
const dim = useColor ? chalk.dim : (s: string) => s;
|
|
243
|
+
const gray = useColor ? chalk.gray : (s: string) => s;
|
|
244
|
+
|
|
245
|
+
// Activity row: spinner + phase + live thinking text (+ right-aligned ⟦esc⟧).
|
|
246
|
+
let activity = (data.activity?.trim() || data.message || "thinking through the next tool call").replace(/\s+/g, " ");
|
|
247
|
+
const level = data.colorLevel ?? (useColor ? ColorLevel.TrueColor : ColorLevel.None);
|
|
248
|
+
const esc = data.escHint ? (unicode ? "⟦esc⟧" : "[esc]") : "";
|
|
249
|
+
const spin = data.spinner ?? "";
|
|
250
|
+
const phaseLabel = data.phaseLabel ?? "thinking";
|
|
251
|
+
const headPlain = `${spin ? `${spin} ` : ""}${phaseLabel} ${unicode ? "·" : "-"} `;
|
|
252
|
+
const room = Math.max(8, cols - 1 - visibleWidth(headPlain) - (esc ? visibleWidth(esc) + 1 : 0));
|
|
253
|
+
if (visibleWidth(activity) > room) {
|
|
254
|
+
let w = 0; let cut = "";
|
|
255
|
+
for (const ch of activity) {
|
|
256
|
+
const cw = visibleWidth(ch);
|
|
257
|
+
if (w + cw > room - 1) break;
|
|
258
|
+
cut += ch; w += cw;
|
|
259
|
+
}
|
|
260
|
+
activity = cut + (unicode ? "…" : "...");
|
|
261
|
+
}
|
|
262
|
+
const plainActivityWidth = visibleWidth(activity);
|
|
263
|
+
if (useColor && data.isThinking && level === ColorLevel.TrueColor && data.palette && data.palette.length > 0) {
|
|
264
|
+
activity = animatedGradientText(activity, data.palette, data.phase ?? 0, { colorLevel: level });
|
|
265
|
+
}
|
|
266
|
+
const escPad = esc
|
|
267
|
+
? " ".repeat(Math.max(1, cols - 1 - visibleWidth(headPlain) - plainActivityWidth - visibleWidth(esc))) + dim(esc)
|
|
268
|
+
: "";
|
|
269
|
+
const head = useColor
|
|
270
|
+
? `${spin ? `${spin} ` : ""}${chalk.cyan.bold(phaseLabel)} ${dim(unicode ? "·" : "-")} `
|
|
271
|
+
: headPlain;
|
|
272
|
+
const activityRow = ` ${head}${activity}${escPad}`;
|
|
273
|
+
|
|
274
|
+
// Compact metrics row: elapsed · now/avg timing · usage · rate · cost · tools.
|
|
275
|
+
const bits: string[] = [`${seconds(data.elapsedMs)}s`];
|
|
276
|
+
if (Number.isFinite(data.stepElapsedMs)) bits.push(`now ${(data.stepElapsedMs! / 1000).toFixed(1)}s`);
|
|
277
|
+
if (Number.isFinite(data.avgStepMs)) bits.push(`avg ${(data.avgStepMs! / 1000).toFixed(1)}s`);
|
|
278
|
+
if (data.usage && (data.usage.inputTokens || data.usage.outputTokens)) {
|
|
279
|
+
bits.push(formatUsage(data.usage));
|
|
280
|
+
const elapsedSec = (data.elapsedMs ?? 0) / 1000;
|
|
281
|
+
if (elapsedSec >= 1 && (data.usage.outputTokens ?? 0) > 0) {
|
|
282
|
+
const rate = data.usage.outputTokens / elapsedSec;
|
|
283
|
+
bits.push(`${unicode ? "⤴" : "^"} ${rate >= 100 ? rate.toFixed(0) : rate.toFixed(1)}/s`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (typeof data.costUsd === "number" && Number.isFinite(data.costUsd) && data.costUsd > 0) bits.push(formatCost(data.costUsd));
|
|
287
|
+
const ok = data.okCount ?? 0;
|
|
288
|
+
const fail = data.failCount ?? 0;
|
|
289
|
+
const running = data.runningCount ?? 0;
|
|
290
|
+
if (ok + fail + running > 0) {
|
|
291
|
+
const counts: string[] = [];
|
|
292
|
+
if (ok) counts.push(useColor ? chalk.green(`${ok} ok`) : `${ok} ok`);
|
|
293
|
+
if (fail) counts.push(useColor ? chalk.red(`${fail} fail`) : `${fail} fail`);
|
|
294
|
+
if (running) counts.push(useColor ? chalk.yellow(`${running} run`) : `${running} run`);
|
|
295
|
+
bits.push(counts.join(" / "));
|
|
296
|
+
}
|
|
297
|
+
if (data.subagentActive) bits.push("(sub)");
|
|
298
|
+
if (data.mutationGuarded) bits.push(useColor ? chalk.red.bold("mutation locked") : "mutation locked");
|
|
299
|
+
// Width-fit by dropping trailing WHOLE segments (never mid-ANSI cuts): the
|
|
300
|
+
// leading elapsed/timing bits carry the most signal and always survive.
|
|
301
|
+
const metricsIndent = " ";
|
|
302
|
+
const maxMetricsWidth = Math.max(8, cols - metricsIndent.length);
|
|
303
|
+
while (bits.length > 1 && visibleWidth(bits.join(" · ")) > maxMetricsWidth) bits.pop();
|
|
304
|
+
const metricsRow = `${metricsIndent}${gray(bits.join(" · "))}`;
|
|
305
|
+
return [activityRow, metricsRow];
|
|
45
306
|
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Step-process timeline — a vertical, status-colored render of the agent's
|
|
3
|
+
* step sequence (one row per tool step) with a state glyph, an animated active
|
|
4
|
+
* marker, a connector gutter, and a summary line. Used by `LaunchTui.finish()`
|
|
5
|
+
* to collapse a turn into a readable process trace. Pure functions over a step
|
|
6
|
+
* list (color via chalk), so they unit-test with an ANSI-stripping helper.
|
|
7
|
+
*/
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
import { categoryBadge, type UiCategory } from "./category-index";
|
|
10
|
+
import type { ToolStatus } from "./tool-list";
|
|
11
|
+
|
|
12
|
+
export type StepState = "pending" | "active" | "done" | "failed";
|
|
13
|
+
|
|
14
|
+
export interface TimelineStep {
|
|
15
|
+
label: string;
|
|
16
|
+
state: StepState;
|
|
17
|
+
/** Optional trailing detail (e.g. a file path or short result note). */
|
|
18
|
+
detail?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const GLYPH_UNICODE: Record<StepState, string> = { pending: "○", active: "◐", done: "●", failed: "✗" };
|
|
22
|
+
const GLYPH_ASCII: Record<StepState, string> = { pending: "o", active: "*", done: "x", failed: "!" };
|
|
23
|
+
const SPIN_UNICODE = ["◐", "◓", "◑", "◒"];
|
|
24
|
+
const SPIN_ASCII = ["|", "/", "-", "\\"];
|
|
25
|
+
|
|
26
|
+
/** Glyph for a step state; an `active` step with a frame index animates a spinner. */
|
|
27
|
+
export function stepGlyph(state: StepState, opts: { unicode?: boolean; frame?: number } = {}): string {
|
|
28
|
+
const unicode = opts.unicode !== false;
|
|
29
|
+
if (state === "active" && typeof opts.frame === "number") {
|
|
30
|
+
const set = unicode ? SPIN_UNICODE : SPIN_ASCII;
|
|
31
|
+
return set[((opts.frame % set.length) + set.length) % set.length];
|
|
32
|
+
}
|
|
33
|
+
return (unicode ? GLYPH_UNICODE : GLYPH_ASCII)[state];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Map a state to its chalk colorizer (identity when color is off). */
|
|
37
|
+
export function colorForState(state: StepState, color = true): (s: string) => string {
|
|
38
|
+
if (!color) return (s: string) => s;
|
|
39
|
+
switch (state) {
|
|
40
|
+
case "pending": return chalk.gray;
|
|
41
|
+
case "active": return chalk.yellow;
|
|
42
|
+
case "done": return chalk.green;
|
|
43
|
+
case "failed": return chalk.red;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Map a live ToolStatus to a timeline state. */
|
|
48
|
+
export function stateFromToolStatus(status: ToolStatus): StepState {
|
|
49
|
+
return status === "running" ? "active" : status === "ok" ? "done" : "failed";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Build timeline steps from a ToolList snapshot. */
|
|
53
|
+
export function stepsFromTools(rows: { tool: string; status: ToolStatus }[]): TimelineStep[] {
|
|
54
|
+
return rows.map(r => ({ label: r.tool, state: stateFromToolStatus(r.status) }));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface StepSummary {
|
|
58
|
+
pending: number;
|
|
59
|
+
active: number;
|
|
60
|
+
done: number;
|
|
61
|
+
failed: number;
|
|
62
|
+
total: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function summarizeSteps(steps: TimelineStep[]): StepSummary {
|
|
66
|
+
const s: StepSummary = { pending: 0, active: 0, done: 0, failed: 0, total: steps.length };
|
|
67
|
+
for (const step of steps) s[step.state]++;
|
|
68
|
+
return s;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** One-line summary, e.g. "✓3 ✗1 ·1 / 5" (ASCII: "ok3 x1 .1 / 5"). */
|
|
72
|
+
export function formatStepSummary(steps: TimelineStep[], opts: { unicode?: boolean; color?: boolean } = {}): string {
|
|
73
|
+
const unicode = opts.unicode !== false;
|
|
74
|
+
const color = opts.color !== false;
|
|
75
|
+
const s = summarizeSteps(steps);
|
|
76
|
+
const okMark = unicode ? "✓" : "ok";
|
|
77
|
+
const failMark = unicode ? "✗" : "x";
|
|
78
|
+
const pendMark = unicode ? "·" : ".";
|
|
79
|
+
const wrap = (fn: (x: string) => string, t: string) => (color ? fn(t) : t);
|
|
80
|
+
const parts = [
|
|
81
|
+
wrap(chalk.green, `${okMark}${s.done}`),
|
|
82
|
+
s.failed ? wrap(chalk.red, `${failMark}${s.failed}`) : "",
|
|
83
|
+
s.active ? wrap(chalk.yellow, `${unicode ? "◐" : "*"}${s.active}`) : "",
|
|
84
|
+
s.pending ? wrap(chalk.gray, `${pendMark}${s.pending}`) : "",
|
|
85
|
+
].filter(Boolean);
|
|
86
|
+
return `${parts.join(" ")} / ${s.total}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface TimelineOptions {
|
|
90
|
+
unicode?: boolean;
|
|
91
|
+
color?: boolean;
|
|
92
|
+
/** Animation frame for the active step's spinner. */
|
|
93
|
+
frame?: number;
|
|
94
|
+
/** Draw a connector gutter (│ / └) between steps. */
|
|
95
|
+
connectors?: boolean;
|
|
96
|
+
/** Truncate labels+detail to this many visible chars. */
|
|
97
|
+
maxWidth?: number;
|
|
98
|
+
/** Title line above the timeline. */
|
|
99
|
+
title?: string;
|
|
100
|
+
/** Bold the active step row. */
|
|
101
|
+
highlightActive?: boolean;
|
|
102
|
+
/** Keep only the most recent N rows (older collapse to a "(+M earlier)" line). */
|
|
103
|
+
maxRows?: number;
|
|
104
|
+
/** Render the [NN:CAT] badge per row (default true); false → gjc-style bare rows. */
|
|
105
|
+
badges?: boolean;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Render a numbered, status-colored step timeline:
|
|
110
|
+
* 1 ● read ok
|
|
111
|
+
* 2 ◐ bash running…
|
|
112
|
+
* 3 ✗ edit FAILED
|
|
113
|
+
*/
|
|
114
|
+
export function formatStepTimeline(steps: TimelineStep[], opts: TimelineOptions = {}): string[] {
|
|
115
|
+
const unicode = opts.unicode !== false;
|
|
116
|
+
const color = opts.color !== false;
|
|
117
|
+
const connectors = opts.connectors ?? true;
|
|
118
|
+
if (steps.length === 0) return [opts.title ?? (color ? chalk.gray(" (no steps)") : " (no steps)")];
|
|
119
|
+
|
|
120
|
+
const out: string[] = [];
|
|
121
|
+
if (opts.title) out.push(opts.title);
|
|
122
|
+
const idxW = String(steps.length).length;
|
|
123
|
+
const vbar = unicode ? "│" : "|";
|
|
124
|
+
const corner = unicode ? "└" : "`";
|
|
125
|
+
|
|
126
|
+
// Keep only the most recent maxRows; collapse the rest to a count line.
|
|
127
|
+
let startIdx = 0;
|
|
128
|
+
if (opts.maxRows && opts.maxRows > 0 && steps.length > opts.maxRows) {
|
|
129
|
+
startIdx = steps.length - opts.maxRows;
|
|
130
|
+
out.push(color ? chalk.gray(` · (+${startIdx} earlier)`) : ` · (+${startIdx} earlier)`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
for (let i = startIdx; i < steps.length; i++) {
|
|
134
|
+
const step = steps[i];
|
|
135
|
+
const last = i === steps.length - 1;
|
|
136
|
+
const conn = connectors ? (last ? corner : vbar) : " ";
|
|
137
|
+
const glyph = stepGlyph(step.state, { unicode, frame: opts.frame });
|
|
138
|
+
const paint = colorForState(step.state, color);
|
|
139
|
+
let label = step.label;
|
|
140
|
+
if (step.detail) label += ` ${step.detail}`;
|
|
141
|
+
if (opts.maxWidth && label.length > opts.maxWidth) label = label.slice(0, opts.maxWidth - 1) + "…";
|
|
142
|
+
if (color) {
|
|
143
|
+
if (step.state === "done") {
|
|
144
|
+
label = `\x1b[9m${chalk.gray(chalk.dim(label))}\x1b[29m`;
|
|
145
|
+
} else if (step.state === "failed") {
|
|
146
|
+
label = chalk.red(label);
|
|
147
|
+
} else if (step.state === "active") {
|
|
148
|
+
if (opts.highlightActive) {
|
|
149
|
+
label = chalk.cyan.bold(label);
|
|
150
|
+
}
|
|
151
|
+
} else if (step.state === "pending") {
|
|
152
|
+
label = chalk.dim(label);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const cat: UiCategory = step.state === "active" ? "progress" : step.state === "done" ? "done" : step.state === "failed" ? "error" : "tool";
|
|
156
|
+
const badge = opts.badges === false ? "" : ` ${categoryBadge(cat, { index: i + 1, color })}`;
|
|
157
|
+
out.push(` ${conn} ${paint(glyph)}${badge} ${label}`);
|
|
158
|
+
}
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Compact duration: 0.9s under a minute, else Mm Ss; sub-second as Nms. */
|
|
163
|
+
export function formatDuration(ms: number): string {
|
|
164
|
+
if (ms < 1000) return `${Math.max(0, Math.round(ms))}ms`;
|
|
165
|
+
const s = ms / 1000;
|
|
166
|
+
if (s < 60) return `${s.toFixed(1)}s`;
|
|
167
|
+
const m = Math.floor(s / 60);
|
|
168
|
+
return `${m}m ${Math.round(s % 60)}s`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Header line: "Steps ✓2 ✗1 ◐1 / 4 · 3.2s". */
|
|
172
|
+
export function formatStepHeader(
|
|
173
|
+
steps: TimelineStep[],
|
|
174
|
+
opts: { elapsedMs?: number; unicode?: boolean; color?: boolean; label?: string } = {},
|
|
175
|
+
): string {
|
|
176
|
+
const color = opts.color !== false;
|
|
177
|
+
const title = opts.label ?? "Steps";
|
|
178
|
+
const head = color ? chalk.bold(title) : title;
|
|
179
|
+
const summary = formatStepSummary(steps, { unicode: opts.unicode, color });
|
|
180
|
+
const tail = typeof opts.elapsedMs === "number" ? ` · ${formatDuration(opts.elapsedMs)}` : "";
|
|
181
|
+
return `${head} ${summary}${tail}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Horizontal glyph strip, e.g. "● ● ✗ ◐" — a glanceable status bar. */
|
|
185
|
+
export function formatStepTimelineCompact(
|
|
186
|
+
steps: TimelineStep[],
|
|
187
|
+
opts: { unicode?: boolean; color?: boolean; frame?: number; cap?: number } = {},
|
|
188
|
+
): string {
|
|
189
|
+
if (steps.length === 0) return "";
|
|
190
|
+
const unicode = opts.unicode !== false;
|
|
191
|
+
const color = opts.color !== false;
|
|
192
|
+
const cap = opts.cap ?? 40;
|
|
193
|
+
const shown = steps.slice(-cap);
|
|
194
|
+
const glyphs = shown.map(s => {
|
|
195
|
+
const g = stepGlyph(s.state, { unicode, frame: opts.frame });
|
|
196
|
+
return color ? colorForState(s.state, true)(g) : g;
|
|
197
|
+
});
|
|
198
|
+
const overflow = steps.length > cap ? (color ? chalk.gray(` +${steps.length - cap}`) : ` +${steps.length - cap}`) : "";
|
|
199
|
+
return glyphs.join(" ") + overflow;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** A done/total progress bar: "▓▓▓░░ 3/5" (ascii: "###.. 3/5"). */
|
|
203
|
+
export function formatProgressBar(
|
|
204
|
+
done: number,
|
|
205
|
+
total: number,
|
|
206
|
+
opts: { width?: number; unicode?: boolean; color?: boolean } = {},
|
|
207
|
+
): string {
|
|
208
|
+
const width = Math.max(1, opts.width ?? 10);
|
|
209
|
+
const unicode = opts.unicode !== false;
|
|
210
|
+
const color = opts.color !== false;
|
|
211
|
+
const ratio = total > 0 ? Math.min(1, Math.max(0, done / total)) : 0;
|
|
212
|
+
const filled = Math.round(ratio * width);
|
|
213
|
+
const fillCh = unicode ? "▓" : "#";
|
|
214
|
+
const emptyCh = unicode ? "░" : ".";
|
|
215
|
+
const bar = fillCh.repeat(filled) + emptyCh.repeat(width - filled);
|
|
216
|
+
const painted = color ? chalk.green(fillCh.repeat(filled)) + chalk.gray(emptyCh.repeat(width - filled)) : bar;
|
|
217
|
+
return `${painted} ${done}/${total}`;
|
|
218
|
+
}
|
|
@@ -1,20 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streamed-output region. Keeps a BOUNDED ring of completed lines (+ a trailing
|
|
3
|
+
* partial line not yet terminated by "\n") so memory and per-frame render cost
|
|
4
|
+
* stay flat over a long session instead of growing with total output (the old
|
|
5
|
+
* implementation accumulated one ever-growing string and re-split/re-wrapped it
|
|
6
|
+
* every 120ms frame).
|
|
7
|
+
*/
|
|
1
8
|
export class StreamRegion {
|
|
2
|
-
private
|
|
9
|
+
private lines: string[] = [];
|
|
10
|
+
private partial = "";
|
|
11
|
+
private readonly cap: number;
|
|
12
|
+
|
|
13
|
+
constructor(cap = 500) {
|
|
14
|
+
this.cap = Math.max(1, cap);
|
|
15
|
+
}
|
|
3
16
|
|
|
4
17
|
append(text: string): void {
|
|
5
|
-
this.
|
|
18
|
+
const parts = (this.partial + text).split("\n");
|
|
19
|
+
this.partial = parts.pop() ?? "";
|
|
20
|
+
if (parts.length > 0) {
|
|
21
|
+
for (const p of parts) this.lines.push(p);
|
|
22
|
+
// Trim from the front so the ring never exceeds `cap` completed lines.
|
|
23
|
+
if (this.lines.length > this.cap) this.lines.splice(0, this.lines.length - this.cap);
|
|
24
|
+
}
|
|
6
25
|
}
|
|
7
26
|
|
|
8
27
|
render(width: number, maxLines?: number): string[] {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
28
|
+
const all = this.partial ? [...this.lines, this.partial] : this.lines;
|
|
29
|
+
if (all.length === 0) return [];
|
|
12
30
|
|
|
13
31
|
const cols = Math.max(1, width);
|
|
14
|
-
const segments = this.buffer.split("\n");
|
|
15
32
|
const result: string[] = [];
|
|
16
|
-
|
|
17
|
-
for (const segment of segments) {
|
|
33
|
+
for (const segment of all) {
|
|
18
34
|
if (segment === "") {
|
|
19
35
|
result.push("");
|
|
20
36
|
} else {
|
|
@@ -31,6 +47,7 @@ export class StreamRegion {
|
|
|
31
47
|
}
|
|
32
48
|
|
|
33
49
|
clear(): void {
|
|
34
|
-
this.
|
|
50
|
+
this.lines = [];
|
|
51
|
+
this.partial = "";
|
|
35
52
|
}
|
|
36
53
|
}
|