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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jeo-code",
3
- "version": "0.6.5",
3
+ "version": "0.6.6",
4
4
  "description": "Clean, highly optimized AI coding agent using spec-first loop",
5
5
  "type": "module",
6
6
  "main": "src/cli.ts",
@@ -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 n = parseInt(args[i + 1] ?? "", 10);
89
- if (Number.isFinite(n) && n > 0) {
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
- } else if (a === "--model") {
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("--model=")) {
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("--provider=")) {
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
- flags.appendSystemPromptRaw = value;
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("--tools=")) {
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
  }
@@ -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
- return [topBorderLine, ...finalContentLines, bottomBorderLine];
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
  /**