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
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { renderDnaClaw, DNA_CLAW_ART_GRAND } from "./ascii-art";
|
|
3
|
+
import { truncate, isTTY } from "../terminal";
|
|
4
|
+
import { detectColorLevel, ColorLevel } from "./color";
|
|
5
|
+
|
|
6
|
+
export interface WelcomeData {
|
|
7
|
+
version: string;
|
|
8
|
+
model: string;
|
|
9
|
+
provider?: string;
|
|
10
|
+
cwd?: string; // absolute; render ~-shortened
|
|
11
|
+
thinking?: string; // e.g. "medium"
|
|
12
|
+
sessionId?: string; // render first 8 chars
|
|
13
|
+
contextFiles?: string[]; // project context file paths (render basenames)
|
|
14
|
+
recentSessions?: { name: string; timeAgo: string }[];
|
|
15
|
+
cols?: number; // default 80
|
|
16
|
+
/** Gradient phase [0..1) for the DNA Claw symbol — drives the launch sweep animation. */
|
|
17
|
+
phase?: number;
|
|
18
|
+
/** Lit-edge painter (top border + left edge); theme accent. Default gray. */
|
|
19
|
+
accent?: (s: string) => string;
|
|
20
|
+
/** Shaded-edge painter (bottom border + right edge); dimmed accent. Default dim gray. */
|
|
21
|
+
accentShadow?: (s: string) => string;
|
|
22
|
+
unicode?: boolean; // default true
|
|
23
|
+
color?: boolean; // default true
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getVisibleWidth(s: string): number {
|
|
27
|
+
return s.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function padLine(line: string, width: number, align: "left" | "center" | "right" = "left"): string {
|
|
31
|
+
const vis = getVisibleWidth(line);
|
|
32
|
+
if (width <= 0 || vis >= width) return line;
|
|
33
|
+
const total = width - vis;
|
|
34
|
+
if (align === "right") return " ".repeat(total) + line;
|
|
35
|
+
if (align === "center") {
|
|
36
|
+
const left = Math.floor(total / 2);
|
|
37
|
+
return " ".repeat(left) + line + " ".repeat(total - left);
|
|
38
|
+
}
|
|
39
|
+
return line + " ".repeat(total);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The gjc-style hero welcome box ("JEO forge"): one outer box with the version
|
|
44
|
+
* embedded in the top border and a SINGLE CENTERED column inside — brand line,
|
|
45
|
+
* tagline, the grand DNA Claw symbol (flowing gradient on capable terminals),
|
|
46
|
+
* and the model/provider pills. Workspace details and key hints intentionally
|
|
47
|
+
* live elsewhere (footer/status bar), matching the gjc forge banner.
|
|
48
|
+
*/
|
|
49
|
+
export function renderWelcome(d: WelcomeData): string[] {
|
|
50
|
+
const cols = d.cols ?? 80;
|
|
51
|
+
const unicode = d.unicode !== false;
|
|
52
|
+
const useColor = d.color !== false;
|
|
53
|
+
|
|
54
|
+
if (cols < 30) {
|
|
55
|
+
return [ `jeo v${d.version} · ${d.model}` ];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const W = Math.min(100, cols - 2);
|
|
59
|
+
const inner = W - 2;
|
|
60
|
+
|
|
61
|
+
const BOX_UNICODE = { tl: "╭", tr: "╮", bl: "╰", br: "╯", h: "─", v: "│" };
|
|
62
|
+
const BOX_ASCII = { tl: "+", tr: "+", bl: "+", br: "+", h: "-", v: "|" };
|
|
63
|
+
const g = unicode ? BOX_UNICODE : BOX_ASCII;
|
|
64
|
+
|
|
65
|
+
// Depth cue (two-tone borders): top border + left edge are "lit" with the
|
|
66
|
+
// accent; bottom border + right edge are "shaded" with the dimmed accent.
|
|
67
|
+
const lit = useColor ? (d.accent ?? chalk.gray) : (s: string) => s;
|
|
68
|
+
const shadow = useColor ? (d.accentShadow ?? ((s: string) => chalk.dim(chalk.gray(s)))) : (s: string) => s;
|
|
69
|
+
|
|
70
|
+
// Title text: ─── jeo v{version} · JEO forge ─── (bold for contrast against the border)
|
|
71
|
+
const dashStr = g.h.repeat(3);
|
|
72
|
+
const titleLabel = ` jeo v${d.version} · JEO forge `;
|
|
73
|
+
const titleHead = `${dashStr}${titleLabel}`;
|
|
74
|
+
let topBorderLine: string;
|
|
75
|
+
if (titleHead.length + 2 > inner) {
|
|
76
|
+
const clipped = titleHead.slice(0, inner);
|
|
77
|
+
topBorderLine = lit(g.tl + clipped + g.h.repeat(Math.max(0, inner - clipped.length)) + g.tr);
|
|
78
|
+
} else {
|
|
79
|
+
const fill = g.h.repeat(inner - titleHead.length);
|
|
80
|
+
topBorderLine = useColor
|
|
81
|
+
? lit(g.tl + dashStr) + chalk.bold(lit(titleLabel)) + lit(fill) + lit(g.tr)
|
|
82
|
+
: g.tl + titleHead + fill + g.tr;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const bottomBorderPlain = g.bl + g.h.repeat(inner) + g.br;
|
|
86
|
+
const bottomBorderLine = shadow(bottomBorderPlain);
|
|
87
|
+
|
|
88
|
+
// Grand symbol when the box is wide enough; compact DNA Claw otherwise.
|
|
89
|
+
const colorLevel = useColor ? detectColorLevel(process.env, isTTY()) : ColorLevel.None;
|
|
90
|
+
const grandWidth = Math.max(...DNA_CLAW_ART_GRAND.map(l => l.length));
|
|
91
|
+
const grand = inner >= grandWidth;
|
|
92
|
+
const artLines = renderDnaClaw({
|
|
93
|
+
color: useColor,
|
|
94
|
+
phase: d.phase ?? 0,
|
|
95
|
+
unicode,
|
|
96
|
+
colorLevel,
|
|
97
|
+
grand,
|
|
98
|
+
cols: inner,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Single centered hero column (gjc forge layout): breathing room, brand,
|
|
102
|
+
// tagline, the symbol, then the model/provider pills.
|
|
103
|
+
const content: string[] = [];
|
|
104
|
+
content.push("");
|
|
105
|
+
content.push(useColor ? chalk.bold.cyan("Jeo forge") : "Jeo forge");
|
|
106
|
+
content.push(useColor ? chalk.dim("evolve · act · prove") : "evolve · act · prove");
|
|
107
|
+
content.push("");
|
|
108
|
+
for (const line of artLines) content.push(line);
|
|
109
|
+
content.push("");
|
|
110
|
+
|
|
111
|
+
const modelIcon = unicode ? "◆" : "*";
|
|
112
|
+
const modelPill = truncate(`[ ${modelIcon} ${d.model} ]`, inner);
|
|
113
|
+
content.push(useColor ? chalk.cyan(modelPill) : modelPill);
|
|
114
|
+
if (d.provider) {
|
|
115
|
+
const providerIcon = unicode ? "◇" : "o";
|
|
116
|
+
const providerPill = truncate(`[ ${providerIcon} ${d.provider} ]`, inner);
|
|
117
|
+
content.push(useColor ? chalk.blue(providerPill) : providerPill);
|
|
118
|
+
}
|
|
119
|
+
content.push("");
|
|
120
|
+
|
|
121
|
+
const leftBorder = lit(g.v);
|
|
122
|
+
const rightBorder = shadow(g.v);
|
|
123
|
+
const finalContentLines = content.map(raw => {
|
|
124
|
+
const line = padLine(truncate(raw, inner), inner, "center");
|
|
125
|
+
return leftBorder + line + rightBorder;
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
return [topBorderLine, ...finalContentLines, bottomBorderLine];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Launch animation: sweep the DNA Claw's gradient through `cycles` FULL palette
|
|
133
|
+
* cycles by re-printing the welcome box in place (cursor-up rewrites, same row
|
|
134
|
+
* count every frame). The loop is SEAMLESS — the phase wraps exactly at each
|
|
135
|
+
* cycle boundary with a constant frame delay, so consecutive cycles join with
|
|
136
|
+
* no pause or color jump — and every repaint is wrapped in a DECSET 2026
|
|
137
|
+
* synchronized update so frames land atomically (no tearing/flicker on slow
|
|
138
|
+
* terminals). The FINAL frame is phase 0 — byte-identical to the static
|
|
139
|
+
* `renderWelcome` — so the resting banner matches non-animated output exactly.
|
|
140
|
+
* `write`/`sleep` are injectable for tests; callers gate on TTY + truecolor.
|
|
141
|
+
*/
|
|
142
|
+
export async function playWelcomeSweep(
|
|
143
|
+
d: WelcomeData,
|
|
144
|
+
opts: { write?: (s: string) => void; sleep?: (ms: number) => Promise<unknown>; frames?: number; delayMs?: number; cycles?: number } = {},
|
|
145
|
+
): Promise<void> {
|
|
146
|
+
const write = opts.write ?? ((s: string) => process.stdout.write(s));
|
|
147
|
+
const sleep = opts.sleep ?? ((ms: number) => new Promise(r => setTimeout(r, ms)));
|
|
148
|
+
const frames = Math.max(1, Math.trunc(opts.frames ?? 10));
|
|
149
|
+
const cycles = Math.max(1, Math.trunc(opts.cycles ?? 2));
|
|
150
|
+
const delay = opts.delayMs ?? 50;
|
|
151
|
+
const total = frames * cycles;
|
|
152
|
+
let lineCount = 0;
|
|
153
|
+
for (let f = 0; f <= total; f++) {
|
|
154
|
+
const phase = (f % frames) / frames; // wraps each cycle; f === total → 0 (the static banner)
|
|
155
|
+
const lines = renderWelcome({ ...d, phase });
|
|
156
|
+
const rewind = f > 0 ? `\x1b[${lineCount}A` : "";
|
|
157
|
+
// BSU/ESU: the whole repaint (rewind + every row) applies atomically.
|
|
158
|
+
write(`\x1b[?2026h${rewind}${lines.map(l => `${l}\x1b[K`).join("\n")}\n\x1b[?2026l`);
|
|
159
|
+
lineCount = lines.length;
|
|
160
|
+
if (f < total && delay > 0) await sleep(delay);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ANSI + Unicode display-width helpers (consensus-seed P2.B9).
|
|
3
|
+
*
|
|
4
|
+
* Terminals render some code points two columns wide (CJK ideographs, Hangul,
|
|
5
|
+
* fullwidth forms, most emoji) and some zero (combining marks, ZWJ, variation
|
|
6
|
+
* selectors). Counting `string.length` — as the old ad-hoc `truncate` did —
|
|
7
|
+
* overflows or under-fills any line containing them. These helpers count by
|
|
8
|
+
* DISPLAY width, treat tabs as advancing to the next 8-col stop, and copy SGR
|
|
9
|
+
* color escapes verbatim (never counting them) so colored/CJK lines truncate and
|
|
10
|
+
* wrap without tearing an escape or miscounting a wide glyph.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const TAB_STOP = 8;
|
|
14
|
+
// Sticky SGR matcher: scan a heavily color-escaped line in O(n) without slicing.
|
|
15
|
+
const SGR = /\x1b\[[0-9;]*m/y;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Display columns for a single code point. 0 for combining/zero-width, 2 for
|
|
19
|
+
* East-Asian Wide/Fullwidth and emoji, 1 otherwise. Ranges follow the common
|
|
20
|
+
* wcwidth/East_Asian_Width tables (not exhaustive, but covers the cases a coding
|
|
21
|
+
* TUI actually shows: Hangul, CJK, kana, fullwidth ASCII, emoji blocks).
|
|
22
|
+
*/
|
|
23
|
+
export function charWidth(cp: number): number {
|
|
24
|
+
if (cp === 0) return 0;
|
|
25
|
+
// C0/C1 control characters have no width here (callers strip/By handle them).
|
|
26
|
+
if (cp < 32 || (cp >= 0x7f && cp < 0xa0)) return 0;
|
|
27
|
+
// Zero-width: combining marks, ZWSP/ZWNJ/ZWJ, variation selectors, BOM.
|
|
28
|
+
if (
|
|
29
|
+
(cp >= 0x0300 && cp <= 0x036f) || // combining diacritical marks
|
|
30
|
+
(cp >= 0x200b && cp <= 0x200f) || // zero-width space..RLM
|
|
31
|
+
(cp >= 0xfe00 && cp <= 0xfe0f) || // variation selectors
|
|
32
|
+
(cp >= 0x1ab0 && cp <= 0x1aff) || // combining diacritical marks extended
|
|
33
|
+
(cp >= 0x1dc0 && cp <= 0x1dff) || // combining diacritical marks supplement
|
|
34
|
+
(cp >= 0x20d0 && cp <= 0x20ff) || // combining marks for symbols
|
|
35
|
+
cp === 0xfeff
|
|
36
|
+
) {
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
// Wide (2 columns).
|
|
40
|
+
if (
|
|
41
|
+
(cp >= 0x1100 && cp <= 0x115f) || // Hangul Jamo
|
|
42
|
+
(cp >= 0x2e80 && cp <= 0x303e) || // CJK radicals .. Kangxi
|
|
43
|
+
(cp >= 0x3041 && cp <= 0x33ff) || // Hiragana, Katakana, CJK symbols
|
|
44
|
+
(cp >= 0x3400 && cp <= 0x4dbf) || // CJK Ext A
|
|
45
|
+
(cp >= 0x4e00 && cp <= 0x9fff) || // CJK Unified
|
|
46
|
+
(cp >= 0xa000 && cp <= 0xa4cf) || // Yi
|
|
47
|
+
(cp >= 0xac00 && cp <= 0xd7a3) || // Hangul syllables
|
|
48
|
+
(cp >= 0xf900 && cp <= 0xfaff) || // CJK compatibility ideographs
|
|
49
|
+
(cp >= 0xfe30 && cp <= 0xfe4f) || // CJK compatibility forms
|
|
50
|
+
(cp >= 0xff00 && cp <= 0xff60) || // fullwidth forms
|
|
51
|
+
(cp >= 0xffe0 && cp <= 0xffe6) || // fullwidth signs
|
|
52
|
+
(cp >= 0x1f300 && cp <= 0x1faff) || // emoji & pictographs (incl. supplemental/symbols-extended)
|
|
53
|
+
(cp >= 0x1f000 && cp <= 0x1f0ff) || // mahjong/dominoes/playing cards
|
|
54
|
+
(cp >= 0x20000 && cp <= 0x3fffd) // CJK Ext B+ (supplementary ideographic planes)
|
|
55
|
+
) {
|
|
56
|
+
return 2;
|
|
57
|
+
}
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Visible display width of a string: SGR escapes count 0, tabs advance to the
|
|
63
|
+
* next 8-col stop, wide glyphs count 2. Iterates by code point (surrogate-safe).
|
|
64
|
+
*/
|
|
65
|
+
export function visibleWidth(s: string): number {
|
|
66
|
+
if (!s) return 0;
|
|
67
|
+
let w = 0;
|
|
68
|
+
let i = 0;
|
|
69
|
+
while (i < s.length) {
|
|
70
|
+
if (s[i] === "\x1b") {
|
|
71
|
+
SGR.lastIndex = i;
|
|
72
|
+
const m = SGR.exec(s);
|
|
73
|
+
if (m) {
|
|
74
|
+
i += m[0].length;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (s[i] === "\t") {
|
|
79
|
+
w += TAB_STOP - (w % TAB_STOP);
|
|
80
|
+
i += 1;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const cp = s.codePointAt(i)!;
|
|
84
|
+
w += charWidth(cp);
|
|
85
|
+
i += cp > 0xffff ? 2 : 1;
|
|
86
|
+
}
|
|
87
|
+
return w;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Truncate a string to at most `cols` DISPLAY columns. SGR escapes are copied
|
|
92
|
+
* verbatim (free); a wide glyph that would straddle the boundary is dropped
|
|
93
|
+
* whole (never half-rendered). If a color was active at the cut, a reset is
|
|
94
|
+
* appended so trailing frame content is not tinted.
|
|
95
|
+
*/
|
|
96
|
+
export function truncateToWidth(s: string, cols: number): string {
|
|
97
|
+
const limit = Math.max(0, cols);
|
|
98
|
+
if (limit === 0) return "";
|
|
99
|
+
// Fast path: no escapes, no wide chars, no tabs → plain slice by length.
|
|
100
|
+
if (!s.includes("\x1b") && !/[\t\u0300-\uffff]/.test(s) && !/[\u{10000}-\u{10ffff}]/u.test(s)) {
|
|
101
|
+
return s.length <= limit ? s : s.slice(0, limit);
|
|
102
|
+
}
|
|
103
|
+
let out = "";
|
|
104
|
+
let w = 0;
|
|
105
|
+
let sawEscape = false;
|
|
106
|
+
let i = 0;
|
|
107
|
+
while (i < s.length) {
|
|
108
|
+
if (s[i] === "\x1b") {
|
|
109
|
+
SGR.lastIndex = i;
|
|
110
|
+
const m = SGR.exec(s);
|
|
111
|
+
if (m) {
|
|
112
|
+
out += m[0];
|
|
113
|
+
sawEscape = true;
|
|
114
|
+
i += m[0].length;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
let cw: number;
|
|
119
|
+
let chunk: string;
|
|
120
|
+
if (s[i] === "\t") {
|
|
121
|
+
cw = TAB_STOP - (w % TAB_STOP);
|
|
122
|
+
chunk = "\t";
|
|
123
|
+
} else {
|
|
124
|
+
const cp = s.codePointAt(i)!;
|
|
125
|
+
cw = charWidth(cp);
|
|
126
|
+
chunk = cp > 0xffff ? s.slice(i, i + 2) : s[i]!;
|
|
127
|
+
}
|
|
128
|
+
if (w + cw > limit) break;
|
|
129
|
+
out += chunk;
|
|
130
|
+
w += cw;
|
|
131
|
+
i += chunk.length;
|
|
132
|
+
}
|
|
133
|
+
if (i < s.length && sawEscape && !out.endsWith("\x1b[0m")) out += "\x1b[0m";
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Hard-wrap text to `cols` display columns, breaking long words and preserving
|
|
139
|
+
* existing newlines. SGR-aware (escapes don't consume width). Returns the wrapped
|
|
140
|
+
* lines. Used by markdown/table rendering where alignment must be column-correct.
|
|
141
|
+
*/
|
|
142
|
+
export function wrapTextWithAnsi(text: string, cols: number): string[] {
|
|
143
|
+
const width = Math.max(1, cols);
|
|
144
|
+
const out: string[] = [];
|
|
145
|
+
for (const rawLine of text.split("\n")) {
|
|
146
|
+
if (visibleWidth(rawLine) <= width) {
|
|
147
|
+
out.push(rawLine);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
let rest = rawLine;
|
|
151
|
+
while (visibleWidth(rest) > width) {
|
|
152
|
+
const head = truncateToWidth(rest, width);
|
|
153
|
+
// Advance past exactly the consumed substring (head may carry a trailing reset).
|
|
154
|
+
const consumed = head.endsWith("\x1b[0m") && !rest.endsWith("\x1b[0m")
|
|
155
|
+
? head.slice(0, -"\x1b[0m".length)
|
|
156
|
+
: head;
|
|
157
|
+
out.push(head);
|
|
158
|
+
rest = rest.slice(consumed.length);
|
|
159
|
+
}
|
|
160
|
+
if (rest.length > 0) out.push(rest);
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<!-- Parent: ../../AGENTS.md -->
|
|
2
|
+
<!-- Generated: 2026-06-11 | Updated: 2026-06-11 -->
|
|
3
|
+
|
|
4
|
+
# monitoring
|
|
5
|
+
|
|
6
|
+
## Purpose
|
|
7
|
+
Specialized HUD views and sovereign monitoring interfaces.
|
|
8
|
+
|
|
9
|
+
## Key Files
|
|
10
|
+
| File | Description |
|
|
11
|
+
|------|-------------|
|
|
12
|
+
| `hud-view.ts` | Real-time evolution tracking and self-analysis visibility |
|
|
13
|
+
|
|
14
|
+
## Subdirectories
|
|
15
|
+
*(None)*
|
|
16
|
+
|
|
17
|
+
## For AI Agents
|
|
18
|
+
|
|
19
|
+
### Working In This Directory
|
|
20
|
+
- Ensure compatibility with the differential renderer.
|
|
21
|
+
|
|
22
|
+
### Testing Requirements
|
|
23
|
+
- Unit tests with mock state.
|
|
24
|
+
|
|
25
|
+
### Common Patterns
|
|
26
|
+
*(None)*
|
|
27
|
+
|
|
28
|
+
## Dependencies
|
|
29
|
+
*(None)*
|
|
30
|
+
|
|
31
|
+
<!-- MANUAL: -->
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { renderHud, type JeoPhase } from "../components/hud";
|
|
3
|
+
import {
|
|
4
|
+
evolutionTrack,
|
|
5
|
+
stageIndexForStep,
|
|
6
|
+
getEvolutionStatusMessage,
|
|
7
|
+
stageProgressRatio,
|
|
8
|
+
meterGlyphsFor,
|
|
9
|
+
EVOLUTION_STAGE_COLORS
|
|
10
|
+
} from "../components/evolution";
|
|
11
|
+
|
|
12
|
+
export interface MonitorState {
|
|
13
|
+
phase: JeoPhase;
|
|
14
|
+
step: number;
|
|
15
|
+
maxSteps: number;
|
|
16
|
+
tickCount: number;
|
|
17
|
+
analysisReport?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function renderMonitorView(state: MonitorState): string {
|
|
21
|
+
const unicode = true;
|
|
22
|
+
const stage = stageIndexForStep(state.step, state.maxSteps);
|
|
23
|
+
const hud = renderHud(state.phase, { unicode, color: true });
|
|
24
|
+
const evo = evolutionTrack(stage, { color: true, unicode, ratio: state.step / state.maxSteps });
|
|
25
|
+
const statusMsg = getEvolutionStatusMessage(state.step, state.maxSteps, state.tickCount);
|
|
26
|
+
|
|
27
|
+
// Progress Bar / Meter
|
|
28
|
+
const ratio = Math.max(0, Math.min(1, state.step / state.maxSteps));
|
|
29
|
+
const barWidth = 30;
|
|
30
|
+
const filledWidth = Math.round(ratio * barWidth);
|
|
31
|
+
const glyphs = meterGlyphsFor(stage, unicode);
|
|
32
|
+
const bar = glyphs.color(glyphs.fill.repeat(filledWidth)) + chalk.dim(glyphs.empty.repeat(barWidth - filledWidth));
|
|
33
|
+
const percentage = (ratio * 100).toFixed(1) + "%";
|
|
34
|
+
|
|
35
|
+
let output = "";
|
|
36
|
+
output += chalk.bold.cyan("┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓") + "\n";
|
|
37
|
+
output += chalk.bold.cyan("┃") + " " + chalk.bold.yellow("ooo ralph") + chalk.bold(" Sovereign Monitoring HUD") + " ".repeat(25) + chalk.bold.cyan("┃") + "\n";
|
|
38
|
+
output += chalk.bold.cyan("┠──────────────────────────────────────────────────────────────┨") + "\n";
|
|
39
|
+
output += chalk.bold.cyan("┃") + " " + chalk.bold("PHASE:") + " " + hud.padEnd(50) + " ".repeat(4) + chalk.bold.cyan("┃") + "\n";
|
|
40
|
+
output += chalk.bold.cyan("┃") + " " + chalk.bold("EVO :") + " " + evo.padEnd(50) + " ".repeat(4) + chalk.bold.cyan("┃") + "\n";
|
|
41
|
+
output += chalk.bold.cyan("┃") + " " + chalk.bold("PROG :") + " " + bar.padEnd(50) + " " + chalk.bold(percentage).padStart(6) + chalk.bold.cyan("┃") + "\n";
|
|
42
|
+
output += chalk.bold.cyan("┠──────────────────────────────────────────────────────────────┨") + "\n";
|
|
43
|
+
output += chalk.bold.cyan("┃") + " " + chalk.italic.dim("> " + statusMsg).padEnd(60) + " " + chalk.bold.cyan("┃") + "\n";
|
|
44
|
+
|
|
45
|
+
if (state.analysisReport) {
|
|
46
|
+
output += chalk.bold.cyan("┠──────────────────────────────────────────────────────────────┨") + "\n";
|
|
47
|
+
const lines = state.analysisReport.split("\n").slice(0, 5);
|
|
48
|
+
for (const line of lines) {
|
|
49
|
+
output += chalk.bold.cyan("┃") + " " + chalk.yellow(line.substring(0, 58).padEnd(58)) + " " + chalk.bold.cyan("┃") + "\n";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
output += chalk.bold.cyan("┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛") + "\n";
|
|
53
|
+
|
|
54
|
+
return output;
|
|
55
|
+
}
|
package/src/tui/renderer.ts
CHANGED
|
@@ -2,15 +2,45 @@ import { cursorDown, cursorUp, toColumn, clearLine, clearToEnd, size, truncate }
|
|
|
2
2
|
|
|
3
3
|
export type Writer = (s: string) => void;
|
|
4
4
|
|
|
5
|
+
export interface RendererOptions {
|
|
6
|
+
/** Inline (main-buffer) mode: before painting a frame TALLER than the previous one,
|
|
7
|
+
* reserve the missing rows with real newlines. Cursor-down can't scroll past the
|
|
8
|
+
* bottom margin, so without reservation a frame anchored near the bottom of the
|
|
9
|
+
* viewport would collapse onto its last rows. The newlines DO scroll, pushing prior
|
|
10
|
+
* content (the progress ledger) up into normal scrollback — which is exactly what
|
|
11
|
+
* keeps tmux / terminal mouse-wheel history working mid-turn.
|
|
12
|
+
* Caller invariant: frames must be sliced to the viewport height. A frame taller
|
|
13
|
+
* than the viewport is NOT reserved (cursor-up would clamp at the top margin and
|
|
14
|
+
* mis-anchor the repaint), so the diff degrades to in-place painting instead. */
|
|
15
|
+
reserve?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// DECSET 2026 "synchronized update": the terminal buffers everything between BSU and
|
|
19
|
+
// ESU and presents it atomically, so the insertAbove flow (EL-overwrite of the first
|
|
20
|
+
// row(s) + full repaint below) never flashes an intermediate half-painted frame.
|
|
21
|
+
// Unsupported terminals ignore the
|
|
22
|
+
// sequences; supporting ones (incl. tmux ≥3.4) also time the update out (~150ms) if
|
|
23
|
+
// ESU never arrives, so a crash mid-update cannot freeze the screen.
|
|
24
|
+
const BEGIN_SYNC = "\x1b[?2026h";
|
|
25
|
+
const END_SYNC = "\x1b[?2026l";
|
|
26
|
+
|
|
5
27
|
export class Renderer {
|
|
6
28
|
private write: Writer;
|
|
7
29
|
private cols: () => number;
|
|
8
30
|
private prev: string[] = [];
|
|
9
31
|
private prevCols?: number;
|
|
32
|
+
private readonly reserve: boolean;
|
|
33
|
+
// Stale rows left on screen by the previous frame after insertAbove() dropped the
|
|
34
|
+
// baseline; the next render() must EL-clear any of them beyond the new frame.
|
|
35
|
+
private coverRows = 0;
|
|
36
|
+
// True between an insertAbove() (which opens a synchronized update) and the next
|
|
37
|
+
// render()/clear() (which closes it after the repaint is fully written).
|
|
38
|
+
private syncOpen = false;
|
|
10
39
|
|
|
11
|
-
constructor(write?: Writer, cols?: () => number) {
|
|
40
|
+
constructor(write?: Writer, cols?: () => number, opts?: RendererOptions) {
|
|
12
41
|
this.write = write || ((s: string) => process.stdout.write(s));
|
|
13
42
|
this.cols = cols || (() => size().cols);
|
|
43
|
+
this.reserve = opts?.reserve ?? false;
|
|
14
44
|
}
|
|
15
45
|
|
|
16
46
|
render(lines: string[]): void {
|
|
@@ -21,10 +51,20 @@ export class Renderer {
|
|
|
21
51
|
this.prevCols = currentCols;
|
|
22
52
|
|
|
23
53
|
const next = lines.map(line => truncate(line, currentCols));
|
|
24
|
-
const maxLen = Math.max(this.prev.length, next.length);
|
|
54
|
+
const maxLen = Math.max(this.prev.length, next.length, this.coverRows);
|
|
55
|
+
this.coverRows = 0;
|
|
25
56
|
let cursorRow = 0;
|
|
26
57
|
let out = "";
|
|
27
58
|
|
|
59
|
+
if (this.reserve && next.length > this.prev.length && next.length <= Math.max(1, size().rows)) {
|
|
60
|
+
// The cursor rests on the frame's first row (the anchor). Walk to the last
|
|
61
|
+
// currently-occupied row, emit one newline per missing row (scrolling the
|
|
62
|
+
// viewport when at the bottom margin), then hop back up to the — possibly
|
|
63
|
+
// shifted — anchor so the diff below paints at stable relative positions.
|
|
64
|
+
const have = Math.max(this.prev.length, 1);
|
|
65
|
+
out += cursorDown(have - 1) + "\n".repeat(next.length - have) + cursorUp(next.length - 1) + toColumn(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
28
68
|
for (let i = 0; i < maxLen; i++) {
|
|
29
69
|
if (i < next.length) {
|
|
30
70
|
if (next[i] !== this.prev[i]) {
|
|
@@ -52,6 +92,14 @@ export class Renderer {
|
|
|
52
92
|
}
|
|
53
93
|
out += toColumn(1);
|
|
54
94
|
|
|
95
|
+
// Close the synchronized update opened by insertAbove() now that the full
|
|
96
|
+
// repaint is in the same buffered stream — the terminal presents the overwritten
|
|
97
|
+
// first row(s), the flushed ledger line, and the repainted frame as ONE atomic update.
|
|
98
|
+
if (this.syncOpen) {
|
|
99
|
+
out += END_SYNC;
|
|
100
|
+
this.syncOpen = false;
|
|
101
|
+
}
|
|
102
|
+
|
|
55
103
|
if (out.length > 0) {
|
|
56
104
|
this.write(out);
|
|
57
105
|
}
|
|
@@ -59,8 +107,69 @@ export class Renderer {
|
|
|
59
107
|
this.prev = next;
|
|
60
108
|
}
|
|
61
109
|
|
|
110
|
+
/** Flush static text into normal scrollback ABOVE the live frame: overwrite the
|
|
111
|
+
* frame's first row(s) with the text (caller terminates it with "\n") and drop the
|
|
112
|
+
* differential baseline so the next render() repaints the full frame below the
|
|
113
|
+
* newly emitted line(s). The follow-up render's row reservation scrolls the text
|
|
114
|
+
* up into history, where tmux / terminal mouse-wheel can reach it mid-turn.
|
|
115
|
+
* Erases with per-line EL (\x1b[2K), NEVER clear-to-end: tmux pushes ED-erased
|
|
116
|
+
* rows into scrollback, so an ED here would flood history with one full frame
|
|
117
|
+
* copy per flush (the bug this replaced). Rows the new frame doesn't cover are
|
|
118
|
+
* EL-cleared by the next render() via coverRows.
|
|
119
|
+
* Opens a DECSET 2026 synchronized update that the next render()/clear() closes,
|
|
120
|
+
* so the overwrite → flush → repaint triplet never flashes intermediate states.
|
|
121
|
+
* Inline-mode only by convention — the alt screen has no scrollback to flush into. */
|
|
122
|
+
insertAbove(text: string): void {
|
|
123
|
+
this.syncOpen = true;
|
|
124
|
+
const rows = text.split("\n");
|
|
125
|
+
// Rows the body actually writes (the trailing "" from the final "\n" emits nothing).
|
|
126
|
+
const written = rows.length - (rows[rows.length - 1] === "" ? 1 : 0);
|
|
127
|
+
const body = rows
|
|
128
|
+
.map((line, i, arr) => (i === arr.length - 1 && line === "" ? "" : toColumn(1) + clearLine() + line))
|
|
129
|
+
.join("\n");
|
|
130
|
+
let out = BEGIN_SYNC + body;
|
|
131
|
+
// Eagerly EL-clear the old frame rows the inserted block did NOT cover, then hop
|
|
132
|
+
// back to the row right below the insert (where the next render() anchors).
|
|
133
|
+
// The geometry is provably safe HERE: when stale > 0 the body write never hit
|
|
134
|
+
// the bottom margin (the old frame fit on screen and the insert is shorter), so
|
|
135
|
+
// every stale row exists and cursor-down cannot clamp. Deferring this clear to
|
|
136
|
+
// the next render() via coverRows walked PAST the bottom margin, where the
|
|
137
|
+
// clamped cursor-down desynced the row bookkeeping — each subsequent frame then
|
|
138
|
+
// painted one row higher, devouring the flushed scrollback content above (the
|
|
139
|
+
// "truncated card" corruption).
|
|
140
|
+
const stale = this.prev.length - written;
|
|
141
|
+
if (stale > 0) {
|
|
142
|
+
for (let i = 0; i < stale; i++) {
|
|
143
|
+
out += toColumn(1) + clearLine() + (i < stale - 1 ? cursorDown(1) : "");
|
|
144
|
+
}
|
|
145
|
+
out += (stale > 1 ? cursorUp(stale - 1) : "") + toColumn(1);
|
|
146
|
+
}
|
|
147
|
+
this.write(out);
|
|
148
|
+
this.prev = [];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Clear the live frame. Inline (reserve) mode walks the known frame rows with
|
|
152
|
+
* per-line EL — clear-to-end would make tmux push the erased frame into
|
|
153
|
+
* scrollback (see insertAbove). Alt-screen/non-TTY renderers keep the cheaper
|
|
154
|
+
* ED clear: the alt screen has no history and pipes have no screen. */
|
|
62
155
|
clear(): void {
|
|
63
|
-
|
|
156
|
+
let out: string;
|
|
157
|
+
if (this.reserve) {
|
|
158
|
+
const rows = Math.max(this.prev.length, this.coverRows);
|
|
159
|
+
out = toColumn(1);
|
|
160
|
+
for (let i = 0; i < rows; i++) {
|
|
161
|
+
out += (i > 0 ? cursorDown(1) : "") + toColumn(1) + clearLine();
|
|
162
|
+
}
|
|
163
|
+
if (rows > 1) out += cursorUp(rows - 1) + toColumn(1);
|
|
164
|
+
} else {
|
|
165
|
+
out = toColumn(1) + clearToEnd();
|
|
166
|
+
}
|
|
167
|
+
if (this.syncOpen) {
|
|
168
|
+
out += END_SYNC;
|
|
169
|
+
this.syncOpen = false;
|
|
170
|
+
}
|
|
171
|
+
this.coverRows = 0;
|
|
172
|
+
this.write(out);
|
|
64
173
|
this.prev = [];
|
|
65
174
|
}
|
|
66
175
|
|
package/src/tui/terminal.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { truncateToWidth } from "./components/width";
|
|
2
|
+
|
|
1
3
|
export const ESC = "\x1b[";
|
|
2
4
|
|
|
3
5
|
export function cursorUp(n: number): string {
|
|
@@ -28,6 +30,37 @@ export function showCursor(): string {
|
|
|
28
30
|
return `${ESC}?25h`;
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Defensively DISABLE every xterm mouse-tracking mode + coordinate encoding.
|
|
35
|
+
* jeo never enables these itself, but a previous program that crashed (or a
|
|
36
|
+
* stale tmux pane) can leave them ON — the terminal then reports clicks/motion
|
|
37
|
+
* as escape sequences from the very first prompt, which reads as "the mouse
|
|
38
|
+
* starts out clicked/held" and sprays `[<0;…M`-style garbage into input.
|
|
39
|
+
* Emitting the `l` (reset) forms is harmless when the modes are already off.
|
|
40
|
+
* ?9 X10 · ?1000 normal · ?1002 button-motion · ?1003 any-motion
|
|
41
|
+
* ?1005 UTF-8 · ?1006 SGR · ?1015 urxvt · ?1016 SGR-pixel
|
|
42
|
+
*/
|
|
43
|
+
export function resetMouseTracking(): string {
|
|
44
|
+
return `${ESC}?9l${ESC}?1000l${ESC}?1002l${ESC}?1003l${ESC}?1005l${ESC}?1006l${ESC}?1015l${ESC}?1016l`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Enter the alternate screen buffer (xterm `?1049h`): a separate, scrollback-free
|
|
48
|
+
* screen. Used for the transient live-turn UI so terminal scroll (mouse wheel) can't
|
|
49
|
+
* fight the in-place repaint — and the main buffer / scrollback is left untouched.
|
|
50
|
+
* Also disables "alternate scroll" (`?1007l`): with it on, terminals (and tmux)
|
|
51
|
+
* translate mouse-wheel motion in the alt screen into Up/Down arrow key sequences,
|
|
52
|
+
* which would otherwise leak into readline's buffer and corrupt the next prompt. */
|
|
53
|
+
export function enterAltScreen(): string {
|
|
54
|
+
return `${ESC}?1049h${ESC}?1007l`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Leave the alternate screen buffer (`?1049l`), restoring the main buffer + scrollback.
|
|
58
|
+
* Re-enables alternate scroll (`?1007h`, the common terminal default) so other
|
|
59
|
+
* full-screen apps (vim/less) keep their wheel behavior after jeo exits the turn. */
|
|
60
|
+
export function leaveAltScreen(): string {
|
|
61
|
+
return `${ESC}?1007h${ESC}?1049l`;
|
|
62
|
+
}
|
|
63
|
+
|
|
31
64
|
export function size(): { cols: number; rows: number } {
|
|
32
65
|
return {
|
|
33
66
|
cols: process.stdout.columns || 80,
|
|
@@ -40,39 +73,13 @@ export function isTTY(): boolean {
|
|
|
40
73
|
}
|
|
41
74
|
|
|
42
75
|
/**
|
|
43
|
-
* Truncate a line to `cols` *visible* columns.
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
76
|
+
* Truncate a line to `cols` *visible* DISPLAY columns. Delegates to the
|
|
77
|
+
* width-aware `truncateToWidth` (consensus-seed P2.B9): SGR escapes are copied
|
|
78
|
+
* verbatim (counted 0), CJK/emoji glyphs count 2 so a wide-char line no longer
|
|
79
|
+
* overflows the terminal width, tabs advance to the next stop, and a reset is
|
|
80
|
+
* appended if the cut lands mid-color. The plain-ASCII fast path is preserved
|
|
81
|
+
* inside `truncateToWidth`, so hot-path render cost is unchanged for ASCII frames.
|
|
48
82
|
*/
|
|
49
83
|
export function truncate(line: string, cols: number): string {
|
|
50
|
-
|
|
51
|
-
// Fast path: no escapes → plain slice by length.
|
|
52
|
-
if (!line.includes("\x1b")) {
|
|
53
|
-
return line.length <= limit ? line : line.slice(0, limit);
|
|
54
|
-
}
|
|
55
|
-
let out = "";
|
|
56
|
-
let visible = 0;
|
|
57
|
-
let sawEscape = false;
|
|
58
|
-
let i = 0;
|
|
59
|
-
while (i < line.length) {
|
|
60
|
-
if (line[i] === "\x1b") {
|
|
61
|
-
const m = /^\x1b\[[0-9;]*m/.exec(line.slice(i));
|
|
62
|
-
if (m) {
|
|
63
|
-
out += m[0];
|
|
64
|
-
sawEscape = true;
|
|
65
|
-
i += m[0].length;
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
if (visible >= limit) break;
|
|
70
|
-
out += line[i];
|
|
71
|
-
visible++;
|
|
72
|
-
i++;
|
|
73
|
-
}
|
|
74
|
-
if (i < line.length && sawEscape && !out.endsWith("\x1b[0m")) {
|
|
75
|
-
out += "\x1b[0m";
|
|
76
|
-
}
|
|
77
|
-
return out;
|
|
84
|
+
return truncateToWidth(line, cols);
|
|
78
85
|
}
|