jeo-code 0.6.4 → 0.6.6
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/CHANGELOG.md +25 -0
- package/README.ja.md +2 -2
- package/README.ko.md +2 -2
- package/README.md +2 -2
- package/README.zh.md +2 -2
- package/package.json +1 -1
- package/src/commands/launch/flags.ts +242 -0
- package/src/commands/launch/input.ts +330 -0
- package/src/commands/launch/stream.ts +102 -0
- package/src/commands/launch/tmux.ts +227 -0
- package/src/commands/launch/workflow.ts +26 -0
- package/src/commands/launch.ts +176 -967
- package/src/tui/components/input-box.ts +56 -0
- package/src/tui/components/welcome.ts +28 -6
- package/src/tui/renderer.ts +6 -2
- package/src/tui/terminal.ts +7 -0
|
@@ -159,3 +159,59 @@ export function renderInputFrame(line: string, opts: InputBoxOptions = {}): Inpu
|
|
|
159
159
|
export function renderInputBox(line: string, opts: InputBoxOptions = {}): string[] {
|
|
160
160
|
return renderInputFrame(line, opts).lines;
|
|
161
161
|
}
|
|
162
|
+
/** Visual (row, display-col) of every caret position 0..N of `text` wrapped at `width`,
|
|
163
|
+
* using the SAME wrapping rule as the input box (`wrapWithCursor`). Index i is the caret
|
|
164
|
+
* sitting BEFORE char i (N = end of text); `cursor` offsets are code points, matching how
|
|
165
|
+
* `renderInputFrame` clamps `opts.cursor` against `Array.from(text)`. */
|
|
166
|
+
export interface CaretCell {
|
|
167
|
+
row: number;
|
|
168
|
+
col: number;
|
|
169
|
+
}
|
|
170
|
+
export function caretCells(text: string, width: number): CaretCell[] {
|
|
171
|
+
const cells: CaretCell[] = [];
|
|
172
|
+
let curW = 0;
|
|
173
|
+
let row = 0;
|
|
174
|
+
const chars = Array.from(text.replace(/\r/g, ""));
|
|
175
|
+
for (let i = 0; i <= chars.length; i++) {
|
|
176
|
+
const ch = i < chars.length ? chars[i]! : "";
|
|
177
|
+
const w = ch === "" || ch === "\n" ? 0 : ch === "\t" ? 2 : visibleWidth(ch);
|
|
178
|
+
// Wrap BEFORE recording the caret so a caret on a wrapping char follows it down — the
|
|
179
|
+
// exact order wrapWithCursor uses, so cell rows match the rendered box rows.
|
|
180
|
+
if (w > 0 && curW + w > width && curW > 0) { row += 1; curW = 0; }
|
|
181
|
+
cells.push({ row, col: curW });
|
|
182
|
+
if (ch === "\n") { row += 1; curW = 0; continue; }
|
|
183
|
+
if (ch !== "") curW += w;
|
|
184
|
+
}
|
|
185
|
+
return cells;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** New caret offset after an Up/Down move within the wrapped input box, keeping the
|
|
189
|
+
* display column (textarea convention: snap to the nearest column ≤ the current one on the
|
|
190
|
+
* target row). Returns null when already on the top row (Up) or bottom row (Down), so the
|
|
191
|
+
* caller can fall through to readline's input-history recall. */
|
|
192
|
+
export function verticalCursorOffset(
|
|
193
|
+
text: string,
|
|
194
|
+
cursor: number,
|
|
195
|
+
width: number,
|
|
196
|
+
dir: "up" | "down",
|
|
197
|
+
): number | null {
|
|
198
|
+
const cells = caretCells(text, Math.max(1, width));
|
|
199
|
+
if (cells.length === 0) return null;
|
|
200
|
+
const pos = Math.max(0, Math.min(cursor, cells.length - 1));
|
|
201
|
+
const curRow = cells[pos]!.row;
|
|
202
|
+
const targetRow = dir === "up" ? curRow - 1 : curRow + 1;
|
|
203
|
+
const maxRow = cells[cells.length - 1]!.row;
|
|
204
|
+
if (targetRow < 0 || targetRow > maxRow) return null;
|
|
205
|
+
const curCol = cells[pos]!.col;
|
|
206
|
+
let best = -1;
|
|
207
|
+
let bestCol = -1;
|
|
208
|
+
let firstOnRow = -1;
|
|
209
|
+
for (let p = 0; p < cells.length; p++) {
|
|
210
|
+
if (cells[p]!.row !== targetRow) continue;
|
|
211
|
+
if (firstOnRow === -1) firstOnRow = p;
|
|
212
|
+
const c = cells[p]!.col;
|
|
213
|
+
// Largest column not past the current one — the standard column-preserving snap.
|
|
214
|
+
if (c <= curCol && c > bestCol) { best = p; bestCol = c; }
|
|
215
|
+
}
|
|
216
|
+
return best !== -1 ? best : firstOnRow;
|
|
217
|
+
}
|
|
@@ -21,6 +21,8 @@ export interface WelcomeData {
|
|
|
21
21
|
accentShadow?: (s: string) => string;
|
|
22
22
|
unicode?: boolean; // default true
|
|
23
23
|
color?: boolean; // default true
|
|
24
|
+
/** Center the hero box horizontally within `cols` (gjc forge screen placement). */
|
|
25
|
+
center?: boolean;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
function getVisibleWidth(s: string): number {
|
|
@@ -55,7 +57,20 @@ export function renderWelcome(d: WelcomeData): string[] {
|
|
|
55
57
|
return [ `jeo v${d.version} · ${d.model}` ];
|
|
56
58
|
}
|
|
57
59
|
|
|
58
|
-
|
|
60
|
+
// The banner hugs a NATURAL hero width so it reads as a proportional box — the
|
|
61
|
+
// grand DNA-claw framed by even margins — instead of stretching into a mostly
|
|
62
|
+
// empty rectangle on wide terminals. When the terminal is SMALLER than that
|
|
63
|
+
// initial/native width it shrinks proportionally: the box narrows and the claw
|
|
64
|
+
// steps grand→compact (below) so the art keeps its shape and never clips.
|
|
65
|
+
const grandWidth = Math.max(...DNA_CLAW_ART_GRAND.map(l => l.length));
|
|
66
|
+
// Title rides ON the top border: `─── jeo v{version} · JEO forge ───`. Defined
|
|
67
|
+
// once here so the natural-width calc and the border render below can't drift.
|
|
68
|
+
const titleDashes = 3;
|
|
69
|
+
const titleLabel = ` jeo v${d.version} · JEO forge `;
|
|
70
|
+
const titleWidth = titleDashes + titleLabel.length;
|
|
71
|
+
// grand art (36) + a 6-col margin each side, never narrower than the title.
|
|
72
|
+
const naturalInner = Math.max(grandWidth + 12, titleWidth + 2);
|
|
73
|
+
const W = Math.min(cols - 2, naturalInner + 2);
|
|
59
74
|
const inner = W - 2;
|
|
60
75
|
|
|
61
76
|
const BOX_UNICODE = { tl: "╭", tr: "╮", bl: "╰", br: "╯", h: "─", v: "│" };
|
|
@@ -67,9 +82,9 @@ export function renderWelcome(d: WelcomeData): string[] {
|
|
|
67
82
|
const lit = useColor ? (d.accent ?? chalk.gray) : (s: string) => s;
|
|
68
83
|
const shadow = useColor ? (d.accentShadow ?? ((s: string) => chalk.dim(chalk.gray(s)))) : (s: string) => s;
|
|
69
84
|
|
|
70
|
-
// Title text: ─── jeo v{version} · JEO forge ─── (bold for contrast against the border)
|
|
71
|
-
|
|
72
|
-
const
|
|
85
|
+
// Title text: ─── jeo v{version} · JEO forge ─── (bold for contrast against the border).
|
|
86
|
+
// `titleDashes`/`titleLabel` were fixed above so width and render stay in sync.
|
|
87
|
+
const dashStr = g.h.repeat(titleDashes);
|
|
73
88
|
const titleHead = `${dashStr}${titleLabel}`;
|
|
74
89
|
let topBorderLine: string;
|
|
75
90
|
if (titleHead.length + 2 > inner) {
|
|
@@ -87,7 +102,6 @@ export function renderWelcome(d: WelcomeData): string[] {
|
|
|
87
102
|
|
|
88
103
|
// Grand symbol when the box is wide enough; compact DNA Claw otherwise.
|
|
89
104
|
const colorLevel = useColor ? detectColorLevel(process.env, isTTY()) : ColorLevel.None;
|
|
90
|
-
const grandWidth = Math.max(...DNA_CLAW_ART_GRAND.map(l => l.length));
|
|
91
105
|
const grand = inner >= grandWidth;
|
|
92
106
|
const artLines = renderDnaClaw({
|
|
93
107
|
color: useColor,
|
|
@@ -125,7 +139,15 @@ export function renderWelcome(d: WelcomeData): string[] {
|
|
|
125
139
|
return leftBorder + line + rightBorder;
|
|
126
140
|
});
|
|
127
141
|
|
|
128
|
-
|
|
142
|
+
const boxLines = [topBorderLine, ...finalContentLines, bottomBorderLine];
|
|
143
|
+
// Center the hero box on screen (gjc forge placement): pad every row by half the
|
|
144
|
+
// slack between the terminal width and the box width. Leading spaces only — the
|
|
145
|
+
// box borders/ANSI are untouched, so color and width math stay exact.
|
|
146
|
+
if (d.center && cols > W) {
|
|
147
|
+
const leftPad = " ".repeat(Math.floor((cols - W) / 2));
|
|
148
|
+
return boxLines.map(line => leftPad + line);
|
|
149
|
+
}
|
|
150
|
+
return boxLines;
|
|
129
151
|
}
|
|
130
152
|
|
|
131
153
|
/**
|
package/src/tui/renderer.ts
CHANGED
|
@@ -29,6 +29,7 @@ export class Renderer {
|
|
|
29
29
|
private cols: () => number;
|
|
30
30
|
private prev: string[] = [];
|
|
31
31
|
private prevCols?: number;
|
|
32
|
+
private prevRows?: number;
|
|
32
33
|
private readonly reserve: boolean;
|
|
33
34
|
// Stale rows left on screen by the previous frame after insertAbove() dropped the
|
|
34
35
|
// baseline; the next render() must EL-clear any of them beyond the new frame.
|
|
@@ -45,10 +46,13 @@ export class Renderer {
|
|
|
45
46
|
|
|
46
47
|
render(lines: string[]): void {
|
|
47
48
|
const currentCols = this.cols();
|
|
48
|
-
|
|
49
|
+
const currentRows = size().rows;
|
|
50
|
+
if ((this.prevCols !== undefined && this.prevCols !== currentCols) ||
|
|
51
|
+
(this.prevRows !== undefined && this.prevRows !== currentRows)) {
|
|
49
52
|
this.clear();
|
|
50
53
|
}
|
|
51
54
|
this.prevCols = currentCols;
|
|
55
|
+
this.prevRows = currentRows;
|
|
52
56
|
|
|
53
57
|
const next = lines.map(line => truncate(line, currentCols));
|
|
54
58
|
// Rows physically occupied by the prior frame — or recorded by reset() when the
|
|
@@ -61,7 +65,7 @@ export class Renderer {
|
|
|
61
65
|
let cursorRow = 0;
|
|
62
66
|
let out = "";
|
|
63
67
|
|
|
64
|
-
if (this.reserve && next.length > occupied && next.length <= Math.max(1,
|
|
68
|
+
if (this.reserve && next.length > occupied && next.length <= Math.max(1, currentRows)) {
|
|
65
69
|
// The cursor rests on the frame's first row (the anchor). Walk to the last
|
|
66
70
|
// currently-occupied row, emit one newline per missing row (scrolling the
|
|
67
71
|
// viewport when at the bottom margin), then hop back up to the — possibly
|
package/src/tui/terminal.ts
CHANGED
|
@@ -22,6 +22,13 @@ export function clearToEnd(): string {
|
|
|
22
22
|
return `${ESC}0J`;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/** Full reset of the visible screen: erase the screen (2J), erase the scrollback
|
|
26
|
+
* buffer (3J), and home the cursor (H). This is the gjc-style "fresh start" clear —
|
|
27
|
+
* use it at launch and for `/clear`, NEVER mid-turn (it would flood tmux scrollback). */
|
|
28
|
+
export function clearScreen(): string {
|
|
29
|
+
return `${ESC}2J${ESC}3J${ESC}H`;
|
|
30
|
+
}
|
|
31
|
+
|
|
25
32
|
export function hideCursor(): string {
|
|
26
33
|
return `${ESC}?25l`;
|
|
27
34
|
}
|