jeo-code 0.6.5 → 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 +10 -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/flags.ts +13 -53
- package/src/commands/launch.ts +21 -2
- package/src/tui/components/input-box.ts +56 -0
- package/src/tui/components/welcome.ts +11 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,16 @@ 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.6] - 2026-06-16
|
|
10
|
+
_Vertical caret movement between input-box rows, a centered welcome banner, and a leaner `parseFlags`._
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Vertical caret movement in the boxed prompt.** ↑/↓ inside a multi-line or wrapped draft now move the caret between the input box's visual rows (textarea feel) via `verticalCursorOffset`; an ↑/↓ at the top/bottom edge still falls through to readline history recall.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- **Welcome banner is centered.**
|
|
17
|
+
- **`parseFlags` simplified** — duplicate `--flag` / `--flag=` branches collapsed into one (`takeValue()` already resolves both spellings), −40 lines with zero behavior change.
|
|
18
|
+
|
|
9
19
|
## [0.6.5] - 2026-06-16
|
|
10
20
|
_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._
|
|
11
21
|
|
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.6]** (2026-06-16) — Vertical caret movement between input-box rows, a centered welcome banner, and a leaner `parseFlags`.
|
|
165
166
|
- **[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.
|
|
166
167
|
- **[0.6.4]** (2026-06-16) — Branding, a responsive-resize fix, `/provider` realignment, and engine repeat-spin recovery.
|
|
167
168
|
- **[0.6.3]** (2026-06-16) — OAuth loopback reliability fix.
|
|
168
169
|
- **[0.6.2]** (2026-06-16) — Interactive `/provider` picker, clearer animated status + labeled block/prose boundaries, and a transient empty-response retry.
|
|
169
|
-
- **[0.6.1]** (2026-06-16) — Live reasoning progress (no more frozen "calling model"), thinking-level fixes for Anthropic/Antigravity, and input-box/Ctrl+O TUI fixes.
|
|
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.6]** (2026-06-16) — Vertical caret movement between input-box rows, a centered welcome banner, and a leaner `parseFlags`.
|
|
165
166
|
- **[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.
|
|
166
167
|
- **[0.6.4]** (2026-06-16) — Branding, a responsive-resize fix, `/provider` realignment, and engine repeat-spin recovery.
|
|
167
168
|
- **[0.6.3]** (2026-06-16) — OAuth loopback reliability fix.
|
|
168
169
|
- **[0.6.2]** (2026-06-16) — Interactive `/provider` picker, clearer animated status + labeled block/prose boundaries, and a transient empty-response retry.
|
|
169
|
-
- **[0.6.1]** (2026-06-16) — Live reasoning progress (no more frozen "calling model"), thinking-level fixes for Anthropic/Antigravity, and input-box/Ctrl+O TUI fixes.
|
|
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.6]** (2026-06-16) — Vertical caret movement between input-box rows, a centered welcome banner, and a leaner `parseFlags`.
|
|
165
166
|
- **[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.
|
|
166
167
|
- **[0.6.4]** (2026-06-16) — Branding, a responsive-resize fix, `/provider` realignment, and engine repeat-spin recovery.
|
|
167
168
|
- **[0.6.3]** (2026-06-16) — OAuth loopback reliability fix.
|
|
168
169
|
- **[0.6.2]** (2026-06-16) — Interactive `/provider` picker, clearer animated status + labeled block/prose boundaries, and a transient empty-response retry.
|
|
169
|
-
- **[0.6.1]** (2026-06-16) — Live reasoning progress (no more frozen "calling model"), thinking-level fixes for Anthropic/Antigravity, and input-box/Ctrl+O TUI fixes.
|
|
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.6]** (2026-06-16) — Vertical caret movement between input-box rows, a centered welcome banner, and a leaner `parseFlags`.
|
|
165
166
|
- **[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.
|
|
166
167
|
- **[0.6.4]** (2026-06-16) — Branding, a responsive-resize fix, `/provider` realignment, and engine repeat-spin recovery.
|
|
167
168
|
- **[0.6.3]** (2026-06-16) — OAuth loopback reliability fix.
|
|
168
169
|
- **[0.6.2]** (2026-06-16) — Interactive `/provider` picker, clearer animated status + labeled block/prose boundaries, and a transient empty-response retry.
|
|
169
|
-
- **[0.6.1]** (2026-06-16) — Live reasoning progress (no more frozen "calling model"), thinking-level fixes for Anthropic/Antigravity, and input-box/Ctrl+O TUI fixes.
|
|
170
170
|
|
|
171
171
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
172
172
|
<!-- CHANGELOG:END -->
|
package/package.json
CHANGED
|
@@ -84,46 +84,28 @@ export function parseFlags(args: string[], cwd: string = process.cwd()): LaunchF
|
|
|
84
84
|
flags.noSession = true;
|
|
85
85
|
} else if (a === "--no-tui") {
|
|
86
86
|
flags.noTui = true;
|
|
87
|
-
} else if (a === "--max-steps") {
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
flags.maxSteps = n;
|
|
91
|
-
i++;
|
|
92
|
-
}
|
|
93
|
-
} else if (a.startsWith("--max-steps=")) {
|
|
94
|
-
const n = parseInt(a.slice(12), 10);
|
|
87
|
+
} else if (a === "--max-steps" || a.startsWith("--max-steps=")) {
|
|
88
|
+
const { value, nextIndex } = takeValue(args, i, "--max-steps=");
|
|
89
|
+
const n = parseInt(value ?? "", 10);
|
|
95
90
|
if (Number.isFinite(n) && n > 0) flags.maxSteps = n;
|
|
96
|
-
|
|
91
|
+
i = nextIndex;
|
|
92
|
+
} else if (a === "--model" || a.startsWith("--model=")) {
|
|
97
93
|
const { value, nextIndex } = takeValue(args, i, "--model=");
|
|
98
94
|
if (value) flags.model = value;
|
|
99
95
|
else flags.errors.push("--model requires a value");
|
|
100
96
|
i = nextIndex;
|
|
101
|
-
} else if (a.startsWith("--
|
|
102
|
-
const { value } = takeValue(args, i, "--model=");
|
|
103
|
-
if (value) flags.model = value;
|
|
104
|
-
else flags.errors.push("--model requires a value");
|
|
105
|
-
} else if (a === "--provider") {
|
|
97
|
+
} else if (a === "--provider" || a.startsWith("--provider=")) {
|
|
106
98
|
const { value, nextIndex } = takeValue(args, i, "--provider=");
|
|
107
99
|
const normalized = value?.toLowerCase();
|
|
108
100
|
if (isProviderName(normalized)) flags.provider = normalized;
|
|
109
101
|
else flags.errors.push("--provider must be one of: anthropic, openai, gemini, ollama");
|
|
110
102
|
i = nextIndex;
|
|
111
|
-
} else if (a.startsWith("--
|
|
112
|
-
const { value } = takeValue(args, i, "--provider=");
|
|
113
|
-
const normalized = value?.toLowerCase();
|
|
114
|
-
if (isProviderName(normalized)) flags.provider = normalized;
|
|
115
|
-
else flags.errors.push("--provider must be one of: anthropic, openai, gemini, ollama");
|
|
116
|
-
} else if (a === "--thinking") {
|
|
103
|
+
} else if (a === "--thinking" || a.startsWith("--thinking=")) {
|
|
117
104
|
const { value, nextIndex } = takeValue(args, i, "--thinking=");
|
|
118
105
|
const normalized = value?.toLowerCase();
|
|
119
106
|
if (isThinkingLevel(normalized)) flags.thinking = normalized;
|
|
120
107
|
else flags.errors.push("--thinking must be one of: minimal, low, medium, high, xhigh");
|
|
121
108
|
i = nextIndex;
|
|
122
|
-
} else if (a.startsWith("--thinking=")) {
|
|
123
|
-
const { value } = takeValue(args, i, "--thinking=");
|
|
124
|
-
const normalized = value?.toLowerCase();
|
|
125
|
-
if (isThinkingLevel(normalized)) flags.thinking = normalized;
|
|
126
|
-
else flags.errors.push("--thinking must be one of: minimal, low, medium, high, xhigh");
|
|
127
109
|
} else if (a === "--smol" || a === "--slow" || a === "--plan") {
|
|
128
110
|
flags.modelRole = a.slice(2) as ModelRole;
|
|
129
111
|
} else if (a === "--resume" || a === "--continue" || a === "-c") {
|
|
@@ -142,52 +124,30 @@ export function parseFlags(args: string[], cwd: string = process.cwd()): LaunchF
|
|
|
142
124
|
} else {
|
|
143
125
|
rest.push(val);
|
|
144
126
|
}
|
|
145
|
-
} else if (a === "--append-system-prompt") {
|
|
127
|
+
} else if (a === "--append-system-prompt" || a.startsWith("--append-system-prompt=")) {
|
|
146
128
|
const { value, nextIndex } = takeValue(args, i, "--append-system-prompt=");
|
|
147
|
-
if (value)
|
|
148
|
-
|
|
149
|
-
} else {
|
|
150
|
-
flags.errors.push("--append-system-prompt requires a value");
|
|
151
|
-
}
|
|
129
|
+
if (value) flags.appendSystemPromptRaw = value;
|
|
130
|
+
else flags.errors.push("--append-system-prompt requires a value");
|
|
152
131
|
i = nextIndex;
|
|
153
|
-
} else if (a.startsWith("--append-system-prompt=")) {
|
|
154
|
-
const { value } = takeValue(args, i, "--append-system-prompt=");
|
|
155
|
-
if (value) {
|
|
156
|
-
flags.appendSystemPromptRaw = value;
|
|
157
|
-
} else {
|
|
158
|
-
flags.errors.push("--append-system-prompt requires a value");
|
|
159
|
-
}
|
|
160
132
|
} else if (a === "--no-skills") {
|
|
161
133
|
flags.noSkills = true;
|
|
162
|
-
} else if (a === "--skills") {
|
|
134
|
+
} else if (a === "--skills" || a.startsWith("--skills=")) {
|
|
163
135
|
const { value, nextIndex } = takeValue(args, i, "--skills=");
|
|
164
136
|
if (value) flags.skills = value;
|
|
165
137
|
else flags.errors.push("--skills requires a value");
|
|
166
138
|
i = nextIndex;
|
|
167
|
-
} else if (a.startsWith("--skills=")) {
|
|
168
|
-
const { value } = takeValue(args, i, "--skills=");
|
|
169
|
-
if (value) flags.skills = value;
|
|
170
|
-
else flags.errors.push("--skills requires a value");
|
|
171
139
|
} else if (a === "--no-tools") {
|
|
172
140
|
flags.noTools = true;
|
|
173
|
-
} else if (a === "--tools") {
|
|
141
|
+
} else if (a === "--tools" || a.startsWith("--tools=")) {
|
|
174
142
|
const { value, nextIndex } = takeValue(args, i, "--tools=");
|
|
175
143
|
if (value) flags.tools = value;
|
|
176
144
|
else flags.errors.push("--tools requires a value");
|
|
177
145
|
i = nextIndex;
|
|
178
|
-
} else if (a.startsWith("--
|
|
179
|
-
const { value } = takeValue(args, i, "--tools=");
|
|
180
|
-
if (value) flags.tools = value;
|
|
181
|
-
else flags.errors.push("--tools requires a value");
|
|
182
|
-
} else if (a === "--system-prompt") {
|
|
146
|
+
} else if (a === "--system-prompt" || a.startsWith("--system-prompt=")) {
|
|
183
147
|
const { value, nextIndex } = takeValue(args, i, "--system-prompt=");
|
|
184
148
|
if (value) flags.systemPromptRaw = value;
|
|
185
149
|
else flags.errors.push("--system-prompt requires a value");
|
|
186
150
|
i = nextIndex;
|
|
187
|
-
} else if (a.startsWith("--system-prompt=")) {
|
|
188
|
-
const { value } = takeValue(args, i, "--system-prompt=");
|
|
189
|
-
if (value) flags.systemPromptRaw = value;
|
|
190
|
-
else flags.errors.push("--system-prompt requires a value");
|
|
191
151
|
} else {
|
|
192
152
|
rest.push(a);
|
|
193
153
|
}
|
package/src/commands/launch.ts
CHANGED
|
@@ -60,7 +60,8 @@ import { liveModelPicker, renderLiveModelPicker, type ModelAssignmentBadge } fro
|
|
|
60
60
|
import { providerPicker, renderProviderPicker } from "../tui/components/provider-picker";
|
|
61
61
|
import { detectLanguage, languageLabel, parseLineRange, sliceLines, formatCodeBlock, formatDiff, sanitizeForTerminal } from "../tui/components/code-view";
|
|
62
62
|
import { categoryBadge } from "../tui/components/category-index";
|
|
63
|
-
import { renderInputFrame } from "../tui/components/input-box";
|
|
63
|
+
import { renderInputFrame, verticalCursorOffset } from "../tui/components/input-box";
|
|
64
|
+
|
|
64
65
|
import { renderStatusBar } from "../tui/components/status";
|
|
65
66
|
import { detectColorLevel, ColorLevel, visibleWidth } from "../tui/components/color";
|
|
66
67
|
import { readClipboardImage } from "../util/clipboard-image";
|
|
@@ -1003,6 +1004,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1003
1004
|
cols: terminalSize().cols,
|
|
1004
1005
|
unicode: supportsUnicode(),
|
|
1005
1006
|
color: welcomeTheme.color,
|
|
1007
|
+
center: true,
|
|
1006
1008
|
accent: accentPaint(welcomeTheme),
|
|
1007
1009
|
accentShadow: accentShadowPaint(welcomeTheme),
|
|
1008
1010
|
};
|
|
@@ -1221,7 +1223,8 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1221
1223
|
let keyFilter: PassThrough | undefined;
|
|
1222
1224
|
// Holder for the active readline so the input filter can see the current line
|
|
1223
1225
|
// buffer (used by the empty-line backspace guard below). Set after rl is created.
|
|
1224
|
-
let activeRl: { line?: string } | undefined;
|
|
1226
|
+
let activeRl: { line?: string; cursor?: number } | undefined;
|
|
1227
|
+
|
|
1225
1228
|
if (multilineInput) {
|
|
1226
1229
|
const kf = new PassThrough();
|
|
1227
1230
|
(kf as unknown as { isTTY: boolean }).isTTY = true;
|
|
@@ -1267,6 +1270,22 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1267
1270
|
// Option/Cmd+Backspace) into the canonical control bytes it DOES act on.
|
|
1268
1271
|
const combo = matchCursorCombo(data, i);
|
|
1269
1272
|
if (combo) { out += combo[1]; i += combo[0].length; continue; }
|
|
1273
|
+
// Up/Down inside a multi-line / wrapped draft move the caret between the box's
|
|
1274
|
+
// visual rows (textarea feel). Only when no slash list or history panel owns ↑/↓,
|
|
1275
|
+
// and only away from the top/bottom edge — at the edge the keys fall through to
|
|
1276
|
+
// readline so ↑/↓ still recalls input history.
|
|
1277
|
+
if ((data.startsWith("\u001b[", i) || data.startsWith("\u001bO", i)) && (data[i + 2] === "A" || data[i + 2] === "B")) {
|
|
1278
|
+
const dir = data[i + 2] === "A" ? "up" : "down";
|
|
1279
|
+
const line = activeRl?.line ?? "";
|
|
1280
|
+
if (line.length > 0 && navMatches.length === 0 && promptHistoryLines == null && activeRl) {
|
|
1281
|
+
const winCols = Math.max(24, (process.stdout.columns ?? 80) - 1);
|
|
1282
|
+
const textWidth = Math.max(1, Math.max(24, Math.min(120, winCols)) - 6);
|
|
1283
|
+
const cur = typeof activeRl.cursor === "number" ? activeRl.cursor : line.length;
|
|
1284
|
+
const next = verticalCursorOffset(expandSentinel(line), cur, textWidth, dir);
|
|
1285
|
+
if (next != null) { activeRl.cursor = next; i += 3; continue; }
|
|
1286
|
+
}
|
|
1287
|
+
out += data.slice(i, i + 3); i += 3; continue;
|
|
1288
|
+
}
|
|
1270
1289
|
if (loneLfShiftEnter && data[i] === "\n") { out += SENTINEL; i += 1; continue; } // lone LF = Shift+Enter (opt-in)
|
|
1271
1290
|
out += data[i]; i += 1;
|
|
1272
1291
|
}
|
|
@@ -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 {
|
|
@@ -137,7 +139,15 @@ export function renderWelcome(d: WelcomeData): string[] {
|
|
|
137
139
|
return leftBorder + line + rightBorder;
|
|
138
140
|
});
|
|
139
141
|
|
|
140
|
-
|
|
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;
|
|
141
151
|
}
|
|
142
152
|
|
|
143
153
|
/**
|