jeo-code 0.6.6 → 0.6.7
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 +9 -0
- package/README.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/package.json +1 -1
- package/src/commands/launch/input.ts +39 -1
- package/src/commands/launch.ts +34 -6
- package/src/tui/app.ts +23 -17
- package/src/tui/components/forge.ts +1 -1
- package/src/tui/components/welcome.ts +6 -21
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
6
6
|
|
|
7
7
|
The README mirrors the latest 5 entries — regenerate with `bun run changelog:sync`.
|
|
8
8
|
|
|
9
|
+
## [0.6.7] - 2026-06-16
|
|
10
|
+
_Mouse-report input corruption fixed under `jeo --tmux`, and a full-width TUI at one consistent width._
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Mouse reports no longer corrupt the prompt.** `jeo --tmux` enables tmux `mouse on` (so wheel-scroll reaches copy-mode), but the mouse-report bytes it delivers — X10 `ESC[M…` and SGR `ESC[<…M/m` — were landing in the input box as typed text (the "값 입력" digit spray when you click or scroll). A filter now swallows whole mouse-report sequences on both the idle keypress path and the live-turn raw-stdin drain, so they never reach readline.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- **TUI fills the full terminal width.** The welcome banner, input box, user/forge cards, history panel, and status box now share one wrap-safe `cols - 1` width instead of capping at 100/120 columns — every box lines up, and a full-width row never trips the terminal's last-column autowrap. The welcome banner's separate proportional/centered modes are dropped in favor of this single width.
|
|
17
|
+
|
|
9
18
|
## [0.6.6] - 2026-06-16
|
|
10
19
|
_Vertical caret movement between input-box rows, a centered welcome banner, and a leaner `parseFlags`._
|
|
11
20
|
|
package/README.ja.md
CHANGED
|
@@ -162,11 +162,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
|
|
|
162
162
|
## 変更履歴 (Changelog)
|
|
163
163
|
|
|
164
164
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
165
|
+
- **[0.6.7]** (2026-06-16) — Mouse-report input corruption fixed under `jeo --tmux`, and a full-width TUI at one consistent width.
|
|
165
166
|
- **[0.6.6]** (2026-06-16) — Vertical caret movement between input-box rows, a centered welcome banner, and a leaner `parseFlags`.
|
|
166
167
|
- **[0.6.5]** (2026-06-16) — macOS combo-key editing in the boxed prompt, a fresh-start screen clear at launch, a proportional welcome banner, height-aware relayout — and `launch.ts` split into focused submodules.
|
|
167
168
|
- **[0.6.4]** (2026-06-16) — Branding, a responsive-resize fix, `/provider` realignment, and engine repeat-spin recovery.
|
|
168
169
|
- **[0.6.3]** (2026-06-16) — OAuth loopback reliability fix.
|
|
169
|
-
- **[0.6.2]** (2026-06-16) — Interactive `/provider` picker, clearer animated status + labeled block/prose boundaries, and a transient empty-response retry.
|
|
170
170
|
|
|
171
171
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
172
172
|
<!-- CHANGELOG:END -->
|
package/README.ko.md
CHANGED
|
@@ -162,11 +162,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
|
|
|
162
162
|
## 변경 이력 (Changelog)
|
|
163
163
|
|
|
164
164
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
165
|
+
- **[0.6.7]** (2026-06-16) — Mouse-report input corruption fixed under `jeo --tmux`, and a full-width TUI at one consistent width.
|
|
165
166
|
- **[0.6.6]** (2026-06-16) — Vertical caret movement between input-box rows, a centered welcome banner, and a leaner `parseFlags`.
|
|
166
167
|
- **[0.6.5]** (2026-06-16) — macOS combo-key editing in the boxed prompt, a fresh-start screen clear at launch, a proportional welcome banner, height-aware relayout — and `launch.ts` split into focused submodules.
|
|
167
168
|
- **[0.6.4]** (2026-06-16) — Branding, a responsive-resize fix, `/provider` realignment, and engine repeat-spin recovery.
|
|
168
169
|
- **[0.6.3]** (2026-06-16) — OAuth loopback reliability fix.
|
|
169
|
-
- **[0.6.2]** (2026-06-16) — Interactive `/provider` picker, clearer animated status + labeled block/prose boundaries, and a transient empty-response retry.
|
|
170
170
|
|
|
171
171
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
172
172
|
<!-- CHANGELOG:END -->
|
package/README.md
CHANGED
|
@@ -162,11 +162,11 @@ Required npm token permissions (repository secret `NPM_TOKEN`):
|
|
|
162
162
|
## Changelog
|
|
163
163
|
|
|
164
164
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
165
|
+
- **[0.6.7]** (2026-06-16) — Mouse-report input corruption fixed under `jeo --tmux`, and a full-width TUI at one consistent width.
|
|
165
166
|
- **[0.6.6]** (2026-06-16) — Vertical caret movement between input-box rows, a centered welcome banner, and a leaner `parseFlags`.
|
|
166
167
|
- **[0.6.5]** (2026-06-16) — macOS combo-key editing in the boxed prompt, a fresh-start screen clear at launch, a proportional welcome banner, height-aware relayout — and `launch.ts` split into focused submodules.
|
|
167
168
|
- **[0.6.4]** (2026-06-16) — Branding, a responsive-resize fix, `/provider` realignment, and engine repeat-spin recovery.
|
|
168
169
|
- **[0.6.3]** (2026-06-16) — OAuth loopback reliability fix.
|
|
169
|
-
- **[0.6.2]** (2026-06-16) — Interactive `/provider` picker, clearer animated status + labeled block/prose boundaries, and a transient empty-response retry.
|
|
170
170
|
|
|
171
171
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
172
172
|
<!-- CHANGELOG:END -->
|
package/README.zh.md
CHANGED
|
@@ -162,11 +162,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
|
|
|
162
162
|
## 更新日志 (Changelog)
|
|
163
163
|
|
|
164
164
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
165
|
+
- **[0.6.7]** (2026-06-16) — Mouse-report input corruption fixed under `jeo --tmux`, and a full-width TUI at one consistent width.
|
|
165
166
|
- **[0.6.6]** (2026-06-16) — Vertical caret movement between input-box rows, a centered welcome banner, and a leaner `parseFlags`.
|
|
166
167
|
- **[0.6.5]** (2026-06-16) — macOS combo-key editing in the boxed prompt, a fresh-start screen clear at launch, a proportional welcome banner, height-aware relayout — and `launch.ts` split into focused submodules.
|
|
167
168
|
- **[0.6.4]** (2026-06-16) — Branding, a responsive-resize fix, `/provider` realignment, and engine repeat-spin recovery.
|
|
168
169
|
- **[0.6.3]** (2026-06-16) — OAuth loopback reliability fix.
|
|
169
|
-
- **[0.6.2]** (2026-06-16) — Interactive `/provider` picker, clearer animated status + labeled block/prose boundaries, and a transient empty-response retry.
|
|
170
170
|
|
|
171
171
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
172
172
|
<!-- CHANGELOG:END -->
|
package/package.json
CHANGED
|
@@ -83,6 +83,44 @@ export function matchCursorCombo(data: string, i: number): readonly [string, str
|
|
|
83
83
|
return undefined;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/** Byte length of a terminal MOUSE-REPORT sequence beginning at `data[i]`, else 0.
|
|
87
|
+
* jeo never requests mouse reporting (resetMouseTracking disables it), but tmux
|
|
88
|
+
* `mouse on` — which `jeo --tmux` sets so wheel-scroll reaches copy-mode — or a stale
|
|
89
|
+
* pane can still deliver reports. Their payload bytes (X10 `ESC[M` + 3 raw bytes, or
|
|
90
|
+
* SGR `ESC[<b;x;y` + `M`/`m`) would otherwise land in the prompt as typed text — the
|
|
91
|
+
* "값 입력" corruption where clicking/scrolling sprays digits into the input box. The
|
|
92
|
+
* filter swallows the whole sequence so it never reaches readline. `ESC[<` and `ESC[M`
|
|
93
|
+
* are input-unambiguous (mouse-only), so an unterminated tail (split across chunks) is
|
|
94
|
+
* consumed too rather than leaked. */
|
|
95
|
+
export function matchMouseReport(data: string, i: number): number {
|
|
96
|
+
if (data.startsWith("\u001b[<", i)) {
|
|
97
|
+
let j = i + 3;
|
|
98
|
+
while (j < data.length && data[j] !== "M" && data[j] !== "m") j++;
|
|
99
|
+
return (j < data.length ? j + 1 : data.length) - i;
|
|
100
|
+
}
|
|
101
|
+
if (data.startsWith("\u001b[M", i)) {
|
|
102
|
+
return Math.min(6, data.length - i);
|
|
103
|
+
}
|
|
104
|
+
return 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Remove every terminal MOUSE-REPORT sequence from a plain (non-paste) input segment.
|
|
108
|
+
* The live-turn drain (`queuePromptInputChunk`) reads RAW stdin, so a wheel/click report
|
|
109
|
+
* buffered during a running turn would otherwise have its printable remnant (`[M`, SGR
|
|
110
|
+
* digits) fed into the next prompt — the same "값 입력" corruption the keyFilter blocks
|
|
111
|
+
* on the idle path. */
|
|
112
|
+
export function stripMouseReports(s: string): string {
|
|
113
|
+
let out = "";
|
|
114
|
+
let i = 0;
|
|
115
|
+
while (i < s.length) {
|
|
116
|
+
const m = matchMouseReport(s, i);
|
|
117
|
+
if (m > 0) { i += m; continue; }
|
|
118
|
+
out += s[i];
|
|
119
|
+
i += 1;
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
|
|
86
124
|
/** Apply combo-key rewrites across a plain (non-paste) input segment. Shares
|
|
87
125
|
* `matchCursorCombo` with the live input filter, so the filter and this exported
|
|
88
126
|
* helper can never diverge. */
|
|
@@ -169,7 +207,7 @@ export function queuePromptInputChunk(state: PromptInputQueue, chunk: string): b
|
|
|
169
207
|
const plain = start === -1 ? rest : rest.slice(0, start);
|
|
170
208
|
if (start !== -1) state.inPaste = true;
|
|
171
209
|
rest = start === -1 ? "" : rest.slice(start + PASTE_START.length);
|
|
172
|
-
if (feedTypedSegment(state, plain)) accepted = true;
|
|
210
|
+
if (feedTypedSegment(state, stripMouseReports(plain))) accepted = true;
|
|
173
211
|
}
|
|
174
212
|
}
|
|
175
213
|
return accepted;
|
package/src/commands/launch.ts
CHANGED
|
@@ -126,6 +126,8 @@ import {
|
|
|
126
126
|
isStandaloneBackspace,
|
|
127
127
|
CURSOR_COMBO_REWRITES,
|
|
128
128
|
matchCursorCombo,
|
|
129
|
+
matchMouseReport,
|
|
130
|
+
stripMouseReports,
|
|
129
131
|
rewriteCursorCombos,
|
|
130
132
|
queuePromptInputChunk,
|
|
131
133
|
captureLivePromptInputChunk,
|
|
@@ -172,6 +174,8 @@ export {
|
|
|
172
174
|
isStandaloneBackspace,
|
|
173
175
|
CURSOR_COMBO_REWRITES,
|
|
174
176
|
matchCursorCombo,
|
|
177
|
+
matchMouseReport,
|
|
178
|
+
stripMouseReports,
|
|
175
179
|
rewriteCursorCombos,
|
|
176
180
|
queuePromptInputChunk,
|
|
177
181
|
captureLivePromptInputChunk,
|
|
@@ -1004,7 +1008,6 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1004
1008
|
cols: terminalSize().cols,
|
|
1005
1009
|
unicode: supportsUnicode(),
|
|
1006
1010
|
color: welcomeTheme.color,
|
|
1007
|
-
center: true,
|
|
1008
1011
|
accent: accentPaint(welcomeTheme),
|
|
1009
1012
|
accentShadow: accentShadowPaint(welcomeTheme),
|
|
1010
1013
|
};
|
|
@@ -1261,6 +1264,10 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1261
1264
|
if (data[i] === "\n" || data[i] === "\r") { out += SENTINEL; i += 1; continue; }
|
|
1262
1265
|
out += data[i]; i += 1; continue;
|
|
1263
1266
|
}
|
|
1267
|
+
// Swallow MOUSE-REPORT sequences (tmux `mouse on` from --tmux, or a stale pane):
|
|
1268
|
+
// their payload bytes would otherwise be typed into the prompt. Never in a paste.
|
|
1269
|
+
const mouse = matchMouseReport(data, i);
|
|
1270
|
+
if (mouse > 0) { i += mouse; continue; }
|
|
1264
1271
|
let matched = false;
|
|
1265
1272
|
for (const seq of SHIFT_ENTER_SEQS) {
|
|
1266
1273
|
if (data.startsWith(seq, i)) { out += SENTINEL; i += seq.length; matched = true; break; }
|
|
@@ -1279,7 +1286,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1279
1286
|
const line = activeRl?.line ?? "";
|
|
1280
1287
|
if (line.length > 0 && navMatches.length === 0 && promptHistoryLines == null && activeRl) {
|
|
1281
1288
|
const winCols = Math.max(24, (process.stdout.columns ?? 80) - 1);
|
|
1282
|
-
const textWidth = Math.max(1, Math.max(24,
|
|
1289
|
+
const textWidth = Math.max(1, Math.max(24, winCols) - 6);
|
|
1283
1290
|
const cur = typeof activeRl.cursor === "number" ? activeRl.cursor : line.length;
|
|
1284
1291
|
const next = verticalCursorOffset(expandSentinel(line), cur, textWidth, dir);
|
|
1285
1292
|
if (next != null) { activeRl.cursor = next; i += 3; continue; }
|
|
@@ -1679,10 +1686,11 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1679
1686
|
const caret = rli.line === line && typeof rli.cursor === "number" ? rli.cursor : line.length;
|
|
1680
1687
|
const { accent: boxAccent, shadow: boxShadow } = boxAccents(line);
|
|
1681
1688
|
const frame = renderInputFrame(expandSentinel(line), {
|
|
1682
|
-
//
|
|
1683
|
-
//
|
|
1684
|
-
//
|
|
1685
|
-
|
|
1689
|
+
// Full terminal width (cols is already columns - 1, leaving the last column free
|
|
1690
|
+
// so a full-width row never wraps). Matches the live-turn box, user/forge cards,
|
|
1691
|
+
// and the welcome banner — all share this cols-1 width so nothing jumps on the
|
|
1692
|
+
// idle↔live transition. The status bar below stays full-width too.
|
|
1693
|
+
cols: cols,
|
|
1686
1694
|
color: true,
|
|
1687
1695
|
unicode: true,
|
|
1688
1696
|
accent: boxAccent,
|
|
@@ -2350,6 +2358,26 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2350
2358
|
lastIdleCols = cols;
|
|
2351
2359
|
lastIdleRows = rows;
|
|
2352
2360
|
try {
|
|
2361
|
+
// Fresh launch / post-`/clear`: the only thing on screen above the footer is the
|
|
2362
|
+
// welcome banner, which the terminal reflows into a fragmented mess on a width
|
|
2363
|
+
// change (every full-width box row wraps, floating its right border onto its own
|
|
2364
|
+
// line). Redraw it cleanly at the NEW width instead — full clear + reprint banner
|
|
2365
|
+
// + fresh footer reservation. Scoped to history.length <= 1, so once real work has
|
|
2366
|
+
// scrolled the banner into deep scrollback the caret-anchored relayout below runs.
|
|
2367
|
+
if (history.length <= 1 && process.stdout.isTTY) {
|
|
2368
|
+
footerRendered = 0;
|
|
2369
|
+
footerParkedRow = 0;
|
|
2370
|
+
lastFooterKey = "";
|
|
2371
|
+
lastDrawnLines = [];
|
|
2372
|
+
out.write(clearScreen());
|
|
2373
|
+
out.write(renderWelcome({ ...welcomeData, cols }).join("\n") + "\n");
|
|
2374
|
+
footerRows = previewRowsFor(rows);
|
|
2375
|
+
if (footerRows > 1) out.write("\n".repeat(footerRows - 1) + cursorUp(footerRows - 1));
|
|
2376
|
+
out.write(toColumn(1));
|
|
2377
|
+
footerRendered = footerRows;
|
|
2378
|
+
drawFooter(promptHistoryLines ? historyPreviewLines(promptHistoryLines) : previewLines(typedLine, navIdx));
|
|
2379
|
+
return;
|
|
2380
|
+
}
|
|
2353
2381
|
// Resolution-safe relayout, anchored to the CURSOR (not the screen bottom). The
|
|
2354
2382
|
// previous frame was painted at the OLD width; on resize the terminal reflows its
|
|
2355
2383
|
// full-width rows AND repositions the frame — a width shrink wraps lines and floats
|
package/src/tui/app.ts
CHANGED
|
@@ -325,7 +325,7 @@ export class LaunchTui {
|
|
|
325
325
|
muted: mutedPaint(this.theme),
|
|
326
326
|
accent: this.theme.color ? accentPaint(this.theme) : undefined,
|
|
327
327
|
fill: cardFillPaint(this.theme),
|
|
328
|
-
width: Math.max(24,
|
|
328
|
+
width: Math.max(24, size().cols - 1),
|
|
329
329
|
});
|
|
330
330
|
this.appendLedger(card.join("\n") + "\n", "card");
|
|
331
331
|
}
|
|
@@ -624,7 +624,7 @@ export class LaunchTui {
|
|
|
624
624
|
const caret = this.unicode ? "▌" : "_";
|
|
625
625
|
const display = this.livePromptInput ? `${this.livePromptInput}${caret}` : "";
|
|
626
626
|
return renderInputBox(display, {
|
|
627
|
-
cols: Math.max(24,
|
|
627
|
+
cols: Math.max(24, cols),
|
|
628
628
|
color: this.theme.color,
|
|
629
629
|
unicode: this.unicode,
|
|
630
630
|
accent: this.theme.color ? accentPaint(this.theme) : undefined,
|
|
@@ -639,7 +639,7 @@ export class LaunchTui {
|
|
|
639
639
|
private renderUserCard(rawText: string, cols: number): string[] {
|
|
640
640
|
const text = (rawText ?? "").trim();
|
|
641
641
|
if (!text) return [];
|
|
642
|
-
const boxWidth = Math.max(24,
|
|
642
|
+
const boxWidth = Math.max(24, cols);
|
|
643
643
|
const inner = Math.max(10, boxWidth - 2);
|
|
644
644
|
const g = this.unicode ? BOX_UNICODE : BOX_ASCII;
|
|
645
645
|
const uc = this.theme.userCard;
|
|
@@ -668,7 +668,7 @@ export class LaunchTui {
|
|
|
668
668
|
flushUserCard(text: string): void {
|
|
669
669
|
const t = (text ?? "").trim();
|
|
670
670
|
if (!t || this.finished) return;
|
|
671
|
-
const cols = Math.max(20, size().cols);
|
|
671
|
+
const cols = Math.max(20, size().cols - 1);
|
|
672
672
|
const lines = this.renderUserCard(t, cols);
|
|
673
673
|
if (lines.length) this.appendLedger(lines.join("\n"), "card");
|
|
674
674
|
}
|
|
@@ -1037,7 +1037,7 @@ export class LaunchTui {
|
|
|
1037
1037
|
// Inline turns flushed every completed card into scrollback live; re-printing
|
|
1038
1038
|
// the cards here would duplicate them right below themselves. A spacer row
|
|
1039
1039
|
// keeps the card block from gluing to the stream above (jeo-ref rhythm).
|
|
1040
|
-
const forge = this.renderForge(size().cols, 3);
|
|
1040
|
+
const forge = this.renderForge(size().cols - 1, 3);
|
|
1041
1041
|
if (forge.length && finalLines.length) finalLines.push("");
|
|
1042
1042
|
finalLines.push(...forge);
|
|
1043
1043
|
}
|
|
@@ -1118,7 +1118,7 @@ export class LaunchTui {
|
|
|
1118
1118
|
* Non-inline modes keep the card in `forgeSummaries` for the final static summary. */
|
|
1119
1119
|
private flushForgeCard(summary: ForgeSummary, success?: boolean): void {
|
|
1120
1120
|
if (!this.inline || this.finished) return;
|
|
1121
|
-
const width = Math.max(24,
|
|
1121
|
+
const width = Math.max(24, size().cols - 1);
|
|
1122
1122
|
// gjc D2 (state-encoded border): a FAILED card gets a red border so it pops
|
|
1123
1123
|
// out of scrollback at a glance; OK/neutral cards keep the theme accent
|
|
1124
1124
|
// identity. The ✓/✗ title mark already encodes state, but the border tone
|
|
@@ -1148,9 +1148,9 @@ export class LaunchTui {
|
|
|
1148
1148
|
dim = false,
|
|
1149
1149
|
): string[] {
|
|
1150
1150
|
const floor = Math.min(24, width);
|
|
1151
|
-
// Fill the available width
|
|
1152
|
-
//
|
|
1153
|
-
const boxWidth = Math.max(floor,
|
|
1151
|
+
// Fill the available width so an in-frame box does not leave a dead right-margin
|
|
1152
|
+
// column inside the outer panel.
|
|
1153
|
+
const boxWidth = Math.max(floor, width);
|
|
1154
1154
|
const paint = this.theme.color ? accentPaint(this.theme) : (s: string) => s;
|
|
1155
1155
|
const lines: string[] = [];
|
|
1156
1156
|
for (const [i, summary] of this.forgeSummaries.slice(-maxEntries).entries()) {
|
|
@@ -1181,7 +1181,7 @@ export class LaunchTui {
|
|
|
1181
1181
|
/** Render the Ctrl+O panel inside the live frame. `maxRows` includes borders. */
|
|
1182
1182
|
private renderHistoryPanel(width: number, maxRows: number): string[] {
|
|
1183
1183
|
if (!this.historyLines || maxRows < 4) return [];
|
|
1184
|
-
const boxWidth = Math.max(24,
|
|
1184
|
+
const boxWidth = Math.max(24, width);
|
|
1185
1185
|
const inner = Math.max(10, boxWidth - 2);
|
|
1186
1186
|
const accent = this.theme.color ? accentPaint(this.theme) : (s: string) => s;
|
|
1187
1187
|
const dim = this.theme.color ? chalk.dim : (s: string) => s;
|
|
@@ -1296,7 +1296,7 @@ export class LaunchTui {
|
|
|
1296
1296
|
// so the in-progress trace stays shaded while the final record reads in normal text.
|
|
1297
1297
|
const liveThink = this.streamingThought.trim() || this.streamingReasoning.trim();
|
|
1298
1298
|
if (isThinking && liveThink) {
|
|
1299
|
-
const wrapW = Math.max(8,
|
|
1299
|
+
const wrapW = Math.max(8, cols - 2);
|
|
1300
1300
|
const wrapped = tailForWrap(liveThink)
|
|
1301
1301
|
.split("\n")
|
|
1302
1302
|
.flatMap(l => wrapTextWithAnsi(l, wrapW))
|
|
@@ -1307,7 +1307,7 @@ export class LaunchTui {
|
|
|
1307
1307
|
// (duplicate model bar) is gone; height now toggles only at lifecycle boundaries.
|
|
1308
1308
|
const ROWS = 6;
|
|
1309
1309
|
const shown = wrapped.slice(-ROWS);
|
|
1310
|
-
tail.push(sectionLabel("Thinking", Math.max(8,
|
|
1310
|
+
tail.push(sectionLabel("Thinking", Math.max(8, cols), { color: this.theme.color, unicode: this.unicode }));
|
|
1311
1311
|
for (let k = 0; k < ROWS - shown.length; k++) tail.push("");
|
|
1312
1312
|
for (const l of shown) tail.push(dim(` ${l}`));
|
|
1313
1313
|
tail.push("");
|
|
@@ -1317,7 +1317,7 @@ export class LaunchTui {
|
|
|
1317
1317
|
// output arrives via onToolProgress and is shown as a DIMMED, bounded tail block.
|
|
1318
1318
|
// It is transient — cleared on result, when the formatted forge card takes over.
|
|
1319
1319
|
if (this.runningTool && this.liveToolOutput.trim()) {
|
|
1320
|
-
const wrapW = Math.max(8,
|
|
1320
|
+
const wrapW = Math.max(8, cols - 2);
|
|
1321
1321
|
const wrapped = tailForWrap(this.liveToolOutput)
|
|
1322
1322
|
.split("\n")
|
|
1323
1323
|
.flatMap(l => wrapTextWithAnsi(l, wrapW))
|
|
@@ -1326,7 +1326,7 @@ export class LaunchTui {
|
|
|
1326
1326
|
// so cumulative stdout growth does not thrash the frame height.
|
|
1327
1327
|
const ROWS = 8;
|
|
1328
1328
|
const shown = wrapped.slice(-ROWS);
|
|
1329
|
-
tail.push(sectionLabel("Output", Math.max(8,
|
|
1329
|
+
tail.push(sectionLabel("Output", Math.max(8, cols), { color: this.theme.color, unicode: this.unicode }));
|
|
1330
1330
|
for (let k = 0; k < ROWS - shown.length; k++) tail.push("");
|
|
1331
1331
|
for (const l of shown) tail.push(dim(` ${l}`));
|
|
1332
1332
|
tail.push("");
|
|
@@ -1336,7 +1336,7 @@ export class LaunchTui {
|
|
|
1336
1336
|
// streamed activity is uniform across providers via streamingActivity and keeps
|
|
1337
1337
|
// the ⟦esc⟧ cancel hint visible without trapping the message inside a border.
|
|
1338
1338
|
if (isThinking) {
|
|
1339
|
-
tail.push(...renderStatusBox(this.statusBoxData({ cols: Math.max(24,
|
|
1339
|
+
tail.push(...renderStatusBox(this.statusBoxData({ cols: Math.max(24, cols), elapsedMs, stepNow, phase, colorLevel, idx })));
|
|
1340
1340
|
}
|
|
1341
1341
|
|
|
1342
1342
|
|
|
@@ -1410,7 +1410,10 @@ export class LaunchTui {
|
|
|
1410
1410
|
const { cols, rows } = size();
|
|
1411
1411
|
const fit = this.tty; // boxed full-screen layout only on a TTY (defaults to isTTY())
|
|
1412
1412
|
const elapsedMs = this.startedAt ? Date.now() - this.startedAt : 0;
|
|
1413
|
-
|
|
1413
|
+
// Inline frame fills the width but leaves the LAST column free (cols - 1) — the same
|
|
1414
|
+
// wrap-safe convention as the welcome banner and idle input box, so every box lines up
|
|
1415
|
+
// at one width and a full-width row never trips the terminal's last-column autowrap.
|
|
1416
|
+
const innerWidth = !fit ? cols : this.inline ? cols - 1 : cols - 4;
|
|
1414
1417
|
|
|
1415
1418
|
// Resolve the current (monotonic) stage for the track; announce a transition
|
|
1416
1419
|
// once when it first advances. The header art is the DNA Claw brand symbol —
|
|
@@ -1462,7 +1465,10 @@ export class LaunchTui {
|
|
|
1462
1465
|
// gjc-style inline frame: a flat stack (live card → status line → todos → hud →
|
|
1463
1466
|
// model bar), no outer border, no mascot art — completed work lives in scrollback.
|
|
1464
1467
|
if (fit && this.inline) {
|
|
1465
|
-
|
|
1468
|
+
// Pass cols - 1 so every in-frame box (input, model bar, forge, status) lines up
|
|
1469
|
+
// with the welcome banner, scrollback cards, and idle input box — and a full-width
|
|
1470
|
+
// row never trips the terminal's last-column autowrap (the 1-line=1-row guard).
|
|
1471
|
+
const inlineFrame = this.composeInlineFrame({ cols: Math.max(20, cols - 1), rows, stepNow, elapsedMs, idx, isThinking, planLines });
|
|
1466
1472
|
// Screen-safety: every rendered line is width-clamped to `cols` so a long line
|
|
1467
1473
|
// (e.g. the model bar with a deep cwd) cannot soft-wrap into a second physical row
|
|
1468
1474
|
// and desync the differential renderer's 1-line=1-row accounting. Frame height stays
|
|
@@ -482,7 +482,7 @@ export function fitForgeBoxes(lines: string[], budget: number): string[] {
|
|
|
482
482
|
export function formatForgeBox(summary: ForgeSummary, opts: ForgeBoxOptions = {}): string[] {
|
|
483
483
|
const innerWidth = opts.width ?? 80;
|
|
484
484
|
const floor = Math.min(24, innerWidth);
|
|
485
|
-
const width = Math.max(floor, Math.
|
|
485
|
+
const width = Math.max(floor, Math.trunc(innerWidth));
|
|
486
486
|
const maxLines = Math.max(1, Math.trunc(opts.maxLines ?? 10));
|
|
487
487
|
const glyphs = borderGlyphs(opts.unicode);
|
|
488
488
|
const paint = opts.paint ?? chalk.gray;
|
|
@@ -21,8 +21,6 @@ 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;
|
|
26
24
|
}
|
|
27
25
|
|
|
28
26
|
function getVisibleWidth(s: string): number {
|
|
@@ -57,20 +55,15 @@ export function renderWelcome(d: WelcomeData): string[] {
|
|
|
57
55
|
return [ `jeo v${d.version} · ${d.model}` ];
|
|
58
56
|
}
|
|
59
57
|
|
|
60
|
-
// The banner
|
|
61
|
-
//
|
|
62
|
-
//
|
|
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.
|
|
58
|
+
// The banner fills the full terminal width (gjc forge: flush with the input box and
|
|
59
|
+
// status bar below it). `cols - 1` leaves the last column free so a full-width row
|
|
60
|
+
// never wraps; the DNA-claw + pills stay centered inside the box.
|
|
65
61
|
const grandWidth = Math.max(...DNA_CLAW_ART_GRAND.map(l => l.length));
|
|
66
62
|
// Title rides ON the top border: `─── jeo v{version} · JEO forge ───`. Defined
|
|
67
|
-
// once here so the
|
|
63
|
+
// once here so the width calc and the border render below can't drift.
|
|
68
64
|
const titleDashes = 3;
|
|
69
65
|
const titleLabel = ` jeo v${d.version} · JEO forge `;
|
|
70
|
-
const
|
|
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);
|
|
66
|
+
const W = cols - 1;
|
|
74
67
|
const inner = W - 2;
|
|
75
68
|
|
|
76
69
|
const BOX_UNICODE = { tl: "╭", tr: "╮", bl: "╰", br: "╯", h: "─", v: "│" };
|
|
@@ -139,15 +132,7 @@ export function renderWelcome(d: WelcomeData): string[] {
|
|
|
139
132
|
return leftBorder + line + rightBorder;
|
|
140
133
|
});
|
|
141
134
|
|
|
142
|
-
|
|
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;
|
|
135
|
+
return [topBorderLine, ...finalContentLines, bottomBorderLine];
|
|
151
136
|
}
|
|
152
137
|
|
|
153
138
|
/**
|