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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jeo-code",
3
- "version": "0.6.6",
3
+ "version": "0.6.7",
4
4
  "description": "Clean, highly optimized AI coding agent using spec-first loop",
5
5
  "type": "module",
6
6
  "main": "src/cli.ts",
@@ -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;
@@ -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, Math.min(120, winCols)) - 6);
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
- // Cap the input box at 120 cols to match the live-turn box (renderLiveInputBox)
1683
- // and the user-card width, so the box doesn't visibly jump width on the
1684
- // idle→live transition on wide terminals. The status bar below stays full-width.
1685
- cols: Math.min(120, cols),
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, Math.min(100, size().cols)),
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, Math.min(120, cols)),
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, Math.min(120, cols));
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, Math.min(120, size().cols));
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 (cap at formatForgeBox's own 120 ceiling) so an
1152
- // in-frame box does not leave a dead right-margin column inside the outer panel.
1153
- const boxWidth = Math.max(floor, Math.min(120, width));
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, Math.min(120, width));
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, Math.min(120, cols) - 2);
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, Math.min(120, cols)), { color: this.theme.color, unicode: this.unicode }));
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, Math.min(120, cols) - 2);
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, Math.min(120, cols)), { color: this.theme.color, unicode: this.unicode }));
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, Math.min(120, cols)), elapsedMs, stepNow, phase, colorLevel, idx })));
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
- const innerWidth = fit && !this.inline ? cols - 4 : cols;
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
- const inlineFrame = this.composeInlineFrame({ cols, rows, stepNow, elapsedMs, idx, isThinking, planLines });
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.min(120, Math.trunc(innerWidth)));
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 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.
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 natural-width calc and the border render below can't drift.
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 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);
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
- 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;
135
+ return [topBorderLine, ...finalContentLines, bottomBorderLine];
151
136
  }
152
137
 
153
138
  /**