jeo-code 0.7.23 → 0.7.25

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,30 @@ 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.7.25] - 2026-07-02
10
+ _Shift+Enter now reliably distinguishable from Enter on kitty/WezTerm/iTerm2/xterm via modifyOtherKeys + kitty keyboard protocol; row-aware Home/End and Cmd+Left/Right in multi-row drafts._
11
+
12
+ ### Added
13
+ - **modifyOtherKeys + kitty keyboard protocol on launch.** `enableModifyOtherKeys()` (`CSI >4;2m`) and `enableKittyKeyboard()` (`CSI >1u`) are sent to the terminal at REPL startup so Shift+Enter is distinguishable from plain Enter on every modern terminal that supports either protocol. Terminals that support neither silently ignore both sequences. Both modes are restored to defaults (`CSI >4;0m` / `CSI >0u`) on process exit via `process.once("exit", …)`. Four new exports in `src/tui/terminal.ts`: `enableModifyOtherKeys`, `disableModifyOtherKeys`, `enableKittyKeyboard`, `disableKittyKeyboard`.
14
+ - **Row-aware Home/End (and macOS Cmd+Left/Right) in multi-row input.** `ROW_HOME_SEQS` / `ROW_END_SEQS` sequences are now caught by `filterPromptInputChunk` _before_ the old whole-buffer `CURSOR_COMBO_REWRITES` path. `rowBoundaryOffset()` (new export in `input-box.ts`) walks the same `caretCells` grid as vertical navigation and jumps the caret to the start/end of the current **visual row**, degenerating to `0`/`length` on a single-row draft — matching macOS/editor convention of "start/end of THIS line".
15
+
16
+ ### Verified
17
+ - `bun run typecheck` clean; full suite green — 2073 pass / 0 fail across 239 files — including two new test files: `test/row-boundary.test.ts` (9 cases for `rowBoundaryOffset`) and `test/row-home-end-filter.test.ts` (5 cases for filter-level row-aware Home/End).
18
+
19
+ ## [0.7.24] - 2026-07-02
20
+ _OAuth callback page redesign with embedded jeo wordmark + Close button; memory budget O(n²)→O(n) incremental tracking fix._
21
+
22
+ ### Added
23
+ - **OAuth callback page: jeo wordmark + Close button.** The post-login browser page now embeds the jeo synthwave wordmark as an inline WebP data URI (no static-asset pipeline required in a compiled binary), and gains an explicit `id="jeo-close"` button that calls `window.close()` immediately — the countdown timer remains as a fallback (bumped 3→5 s). Both success and failure pages now auto-close so OAuth tabs never linger.
24
+ - **`HEADER_BY_TYPE` lookup for memory budget tracking.** Extracted from `TYPE_LAYOUT` alongside the existing `DIR_BY_TYPE`, enabling incremental section-header length computation in `selectWithinBudget` without a full re-render.
25
+
26
+ ### Fixed
27
+ - **`selectWithinBudget` O(n²)→O(n) budget tracking.** Extracted `renderConceptItem` and introduced per-type accumulated section-length tracking so each candidate's budget contribution is computed incrementally (header + item delta) rather than by re-rendering the entire accumulated selection. Produces identical truncation boundaries with O(n) work instead of O(n²); locked by a new regression test (`memory-search-okf.test.ts`) that verifies the exact 50-of-60 cutoff against a hand-computed derivation.
28
+ - **OAuth failure page now shows auto-close / Close button.** Previously the failure page intentionally suppressed `window.close()`; it now uses the same countdown + Close button as the success page so users are not stranded on an error screen.
29
+
30
+ ### Verified
31
+ - `bun run typecheck` clean; full suite green — 2064 pass / 0 fail across 237 files — including the new `selectWithinBudget` incremental-budget regression test and updated OAuth callback page assertions (`id="jeo-countdown"`, `id="jeo-close"`, `window.close()` present on both success and failure pages).
32
+
9
33
  ## [0.7.23] - 2026-07-01
10
34
  _Anthropic 5th-generation model catalog: adds Sonnet 5, Fable 5, and Mythos 5, promotes the 4.6 generation to 1M context / 128k output, and retires the superseded 4.5/3.5/4.1 ids. Bumps the default model to Sonnet 4.6, prices the new tiers, and fixes the thinking transport for Haiku 4.5 and every 5th-gen model._
11
35
 
package/README.ja.md CHANGED
@@ -205,11 +205,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
205
205
  ## 変更履歴 (Changelog)
206
206
 
207
207
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
208
+ - **[0.7.25]** (2026-07-02) — Shift+Enter now reliably distinguishable from Enter on kitty/WezTerm/iTerm2/xterm via modifyOtherKeys + kitty keyboard protocol; row-aware Home/End and Cmd+Left/Right in multi-row drafts.
209
+ - **[0.7.24]** (2026-07-02) — OAuth callback page redesign with embedded jeo wordmark + Close button; memory budget O(n²)→O(n) incremental tracking fix.
208
210
  - **[0.7.23]** (2026-07-01) — Anthropic 5th-generation model catalog: adds Sonnet 5, Fable 5, and Mythos 5, promotes the 4.6 generation to 1M context / 128k output, and retires the superseded 4.5/3.5/4.1 ids. Bumps the default model to Sonnet 4.6, prices the new tiers, and fixes the thinking transport for Haiku 4.5 and every 5th-gen model.
209
211
  - **[0.7.22]** (2026-06-30) — gajae-code 0.7.3–0.7.8 parity sweep: terminal-bell notifications, a gruvbox-dark TUI theme, a resilient `jeo update` runtime check, and an ultragoal artifact gate. Ports the user-facing improvements from upstream gajae-code that jeo did not yet have, re-implemented to fit jeo's config, command, theming, and skill surfaces.
210
212
  - **[0.7.21]** (2026-06-26) — Global llm-wiki vault integration, Gemini/Antigravity thinking indicators, generous file-reading windows, and autopilot flag validation. Adds a shared global wiki root configuration with a /wiki slash command, fires reasoning start signals up front for Gemini/Antigravity models, adjusts the large-file reading discipline to use generous windows, and validates autopilot goal and integer flags.
211
- - **[0.7.20]** (2026-06-26) — OKF concept-memory retrieval gains a hybrid reranker ported from memsearch. Injection priority no longer rides one raw keyword score — it fuses two complementary ranked channels by Reciprocal Rank Fusion (RRF): IDF-weighted lexical relevance (the sparse/BM25 channel, so rare discriminating terms steer recall) and concept-graph proximity (the local dense/semantic-neighbour channel, so a hub linked from multiple query hits surfaces even with no keyword of its own). All embedding-free and deterministic, layered atop the existing failure-first tier and pinned-invariant reserved budget.
212
- - **[0.7.19]** (2026-06-26) — The live model picker gains gajae-code's `/model` provider tabs, and skill invocation is consolidated onto a single `$` entrypoint. The picker now shows an `ALL` tab plus one tab per provider that `tab`/`shift+tab` cycles, and skills (including their declared aliases) are invoked only via `$` — the slash palette stays builtins-only.
213
213
 
214
214
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
215
215
  <!-- CHANGELOG:END -->
package/README.ko.md CHANGED
@@ -205,11 +205,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
205
205
  ## 변경 이력 (Changelog)
206
206
 
207
207
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
208
+ - **[0.7.25]** (2026-07-02) — Shift+Enter now reliably distinguishable from Enter on kitty/WezTerm/iTerm2/xterm via modifyOtherKeys + kitty keyboard protocol; row-aware Home/End and Cmd+Left/Right in multi-row drafts.
209
+ - **[0.7.24]** (2026-07-02) — OAuth callback page redesign with embedded jeo wordmark + Close button; memory budget O(n²)→O(n) incremental tracking fix.
208
210
  - **[0.7.23]** (2026-07-01) — Anthropic 5th-generation model catalog: adds Sonnet 5, Fable 5, and Mythos 5, promotes the 4.6 generation to 1M context / 128k output, and retires the superseded 4.5/3.5/4.1 ids. Bumps the default model to Sonnet 4.6, prices the new tiers, and fixes the thinking transport for Haiku 4.5 and every 5th-gen model.
209
211
  - **[0.7.22]** (2026-06-30) — gajae-code 0.7.3–0.7.8 parity sweep: terminal-bell notifications, a gruvbox-dark TUI theme, a resilient `jeo update` runtime check, and an ultragoal artifact gate. Ports the user-facing improvements from upstream gajae-code that jeo did not yet have, re-implemented to fit jeo's config, command, theming, and skill surfaces.
210
212
  - **[0.7.21]** (2026-06-26) — Global llm-wiki vault integration, Gemini/Antigravity thinking indicators, generous file-reading windows, and autopilot flag validation. Adds a shared global wiki root configuration with a /wiki slash command, fires reasoning start signals up front for Gemini/Antigravity models, adjusts the large-file reading discipline to use generous windows, and validates autopilot goal and integer flags.
211
- - **[0.7.20]** (2026-06-26) — OKF concept-memory retrieval gains a hybrid reranker ported from memsearch. Injection priority no longer rides one raw keyword score — it fuses two complementary ranked channels by Reciprocal Rank Fusion (RRF): IDF-weighted lexical relevance (the sparse/BM25 channel, so rare discriminating terms steer recall) and concept-graph proximity (the local dense/semantic-neighbour channel, so a hub linked from multiple query hits surfaces even with no keyword of its own). All embedding-free and deterministic, layered atop the existing failure-first tier and pinned-invariant reserved budget.
212
- - **[0.7.19]** (2026-06-26) — The live model picker gains gajae-code's `/model` provider tabs, and skill invocation is consolidated onto a single `$` entrypoint. The picker now shows an `ALL` tab plus one tab per provider that `tab`/`shift+tab` cycles, and skills (including their declared aliases) are invoked only via `$` — the slash palette stays builtins-only.
213
213
 
214
214
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
215
215
  <!-- CHANGELOG:END -->
package/README.md CHANGED
@@ -232,11 +232,11 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
232
232
  ## Changelog
233
233
 
234
234
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
235
+ - **[0.7.25]** (2026-07-02) — Shift+Enter now reliably distinguishable from Enter on kitty/WezTerm/iTerm2/xterm via modifyOtherKeys + kitty keyboard protocol; row-aware Home/End and Cmd+Left/Right in multi-row drafts.
236
+ - **[0.7.24]** (2026-07-02) — OAuth callback page redesign with embedded jeo wordmark + Close button; memory budget O(n²)→O(n) incremental tracking fix.
235
237
  - **[0.7.23]** (2026-07-01) — Anthropic 5th-generation model catalog: adds Sonnet 5, Fable 5, and Mythos 5, promotes the 4.6 generation to 1M context / 128k output, and retires the superseded 4.5/3.5/4.1 ids. Bumps the default model to Sonnet 4.6, prices the new tiers, and fixes the thinking transport for Haiku 4.5 and every 5th-gen model.
236
238
  - **[0.7.22]** (2026-06-30) — gajae-code 0.7.3–0.7.8 parity sweep: terminal-bell notifications, a gruvbox-dark TUI theme, a resilient `jeo update` runtime check, and an ultragoal artifact gate. Ports the user-facing improvements from upstream gajae-code that jeo did not yet have, re-implemented to fit jeo's config, command, theming, and skill surfaces.
237
239
  - **[0.7.21]** (2026-06-26) — Global llm-wiki vault integration, Gemini/Antigravity thinking indicators, generous file-reading windows, and autopilot flag validation. Adds a shared global wiki root configuration with a /wiki slash command, fires reasoning start signals up front for Gemini/Antigravity models, adjusts the large-file reading discipline to use generous windows, and validates autopilot goal and integer flags.
238
- - **[0.7.20]** (2026-06-26) — OKF concept-memory retrieval gains a hybrid reranker ported from memsearch. Injection priority no longer rides one raw keyword score — it fuses two complementary ranked channels by Reciprocal Rank Fusion (RRF): IDF-weighted lexical relevance (the sparse/BM25 channel, so rare discriminating terms steer recall) and concept-graph proximity (the local dense/semantic-neighbour channel, so a hub linked from multiple query hits surfaces even with no keyword of its own). All embedding-free and deterministic, layered atop the existing failure-first tier and pinned-invariant reserved budget.
239
- - **[0.7.19]** (2026-06-26) — The live model picker gains gajae-code's `/model` provider tabs, and skill invocation is consolidated onto a single `$` entrypoint. The picker now shows an `ALL` tab plus one tab per provider that `tab`/`shift+tab` cycles, and skills (including their declared aliases) are invoked only via `$` — the slash palette stays builtins-only.
240
240
 
241
241
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
242
242
  <!-- CHANGELOG:END -->
package/README.zh.md CHANGED
@@ -205,11 +205,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
205
205
  ## 更新日志 (Changelog)
206
206
 
207
207
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
208
+ - **[0.7.25]** (2026-07-02) — Shift+Enter now reliably distinguishable from Enter on kitty/WezTerm/iTerm2/xterm via modifyOtherKeys + kitty keyboard protocol; row-aware Home/End and Cmd+Left/Right in multi-row drafts.
209
+ - **[0.7.24]** (2026-07-02) — OAuth callback page redesign with embedded jeo wordmark + Close button; memory budget O(n²)→O(n) incremental tracking fix.
208
210
  - **[0.7.23]** (2026-07-01) — Anthropic 5th-generation model catalog: adds Sonnet 5, Fable 5, and Mythos 5, promotes the 4.6 generation to 1M context / 128k output, and retires the superseded 4.5/3.5/4.1 ids. Bumps the default model to Sonnet 4.6, prices the new tiers, and fixes the thinking transport for Haiku 4.5 and every 5th-gen model.
209
211
  - **[0.7.22]** (2026-06-30) — gajae-code 0.7.3–0.7.8 parity sweep: terminal-bell notifications, a gruvbox-dark TUI theme, a resilient `jeo update` runtime check, and an ultragoal artifact gate. Ports the user-facing improvements from upstream gajae-code that jeo did not yet have, re-implemented to fit jeo's config, command, theming, and skill surfaces.
210
212
  - **[0.7.21]** (2026-06-26) — Global llm-wiki vault integration, Gemini/Antigravity thinking indicators, generous file-reading windows, and autopilot flag validation. Adds a shared global wiki root configuration with a /wiki slash command, fires reasoning start signals up front for Gemini/Antigravity models, adjusts the large-file reading discipline to use generous windows, and validates autopilot goal and integer flags.
211
- - **[0.7.20]** (2026-06-26) — OKF concept-memory retrieval gains a hybrid reranker ported from memsearch. Injection priority no longer rides one raw keyword score — it fuses two complementary ranked channels by Reciprocal Rank Fusion (RRF): IDF-weighted lexical relevance (the sparse/BM25 channel, so rare discriminating terms steer recall) and concept-graph proximity (the local dense/semantic-neighbour channel, so a hub linked from multiple query hits surfaces even with no keyword of its own). All embedding-free and deterministic, layered atop the existing failure-first tier and pinned-invariant reserved budget.
212
- - **[0.7.19]** (2026-06-26) — The live model picker gains gajae-code's `/model` provider tabs, and skill invocation is consolidated onto a single `$` entrypoint. The picker now shows an `ALL` tab plus one tab per provider that `tab`/`shift+tab` cycles, and skills (including their declared aliases) are invoked only via `$` — the slash palette stays builtins-only.
213
213
 
214
214
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
215
215
  <!-- CHANGELOG:END -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jeo-code",
3
- "version": "0.7.23",
3
+ "version": "0.7.25",
4
4
  "description": "Clean, highly optimized AI coding agent using spec-first loop",
5
5
  "type": "module",
6
6
  "main": "src/cli.ts",
@@ -39,6 +39,7 @@ const TYPE_LAYOUT = [
39
39
  ] as const;
40
40
 
41
41
  const DIR_BY_TYPE: Record<string, string> = Object.fromEntries(TYPE_LAYOUT.map(t => [t.type, t.dir]));
42
+ const HEADER_BY_TYPE: Record<string, string> = Object.fromEntries(TYPE_LAYOUT.map(t => [t.type, t.header]));
42
43
 
43
44
  export function memoryFilePath(cwd: string): string {
44
45
  return path.join(cwd, ".jeo", "memory", "MEMORY.md");
@@ -180,17 +181,23 @@ export async function migrateLegacyMemory(cwd: string): Promise<MigrationResult>
180
181
  return { migrated: true, conceptCount: written.length, backupPath };
181
182
  }
182
183
 
184
+ /** Render one concept as its bullet + indented body lines (no header) — the atomic
185
+ * unit `renderConceptSection` stacks under a `## header`. Extracted so budget
186
+ * selection (`selectWithinBudget`) can compute a candidate's exact contribution
187
+ * without re-rendering the whole accumulated section (see there for why that
188
+ * matters). */
189
+ function renderConceptItem(c: { title: string; description: string; body: string }): string {
190
+ const lines = [`- **${c.title}**${c.description ? `: ${c.description}` : ""}`];
191
+ if (c.body) {
192
+ for (const bodyLine of c.body.split("\n")) lines.push(` ${bodyLine}`);
193
+ }
194
+ return lines.join("\n");
195
+ }
196
+
183
197
  /** Render a single index.md-style section: a `## header` followed by one bullet
184
198
  * per concept (`**title**: description`), with the concept body indented beneath. */
185
199
  function renderConceptSection(header: string, list: { title: string; description: string; body: string }[]): string {
186
- const lines = [`## ${header}`];
187
- for (const c of list) {
188
- lines.push(`- **${c.title}**${c.description ? `: ${c.description}` : ""}`);
189
- if (c.body) {
190
- for (const bodyLine of c.body.split("\n")) lines.push(` ${bodyLine}`);
191
- }
192
- }
193
- return lines.join("\n");
200
+ return [`## ${header}`, ...list.map(renderConceptItem)].join("\n");
194
201
  }
195
202
 
196
203
  /** A loaded OKF concept: frontmatter fields + body + bundle-relative path. */
@@ -532,12 +539,42 @@ function renderConcepts(concepts: Concept[]): string {
532
539
  * Pinned (load-bearing) invariants get a RESERVED budget: they are selected FIRST,
533
540
  * before any query/recency fill, so a tight cap can never evict an invariant the
534
541
  * gates depend on (philosophy-synthesis consensus #3). The remaining budget then
535
- * holds the failure-first / core / query-relevant fill. */
542
+ * holds the failure-first / core / query-relevant fill.
543
+ *
544
+ * Perf: computes each candidate's exact incremental contribution to the final
545
+ * `renderConcepts` output (header + item lines, "\n" within a section, "\n\n"
546
+ * between sections) instead of re-rendering the whole accumulated set per
547
+ * candidate — O(n) instead of O(n²) for a bundle of n concepts. This mirrors
548
+ * `renderConceptSection`/`renderConcepts`'s exact join rules; a mismatch here
549
+ * would silently mis-size the budget, so `memory-search-okf.test.ts` locks an
550
+ * exact hand-computed truncation boundary as a regression check. */
551
+
536
552
  function selectWithinBudget(concepts: Concept[], query: string | undefined, budget: number): Concept[] {
537
553
  const selected: Concept[] = [];
554
+ const sectionLenByType = new Map<string, number>(); // type -> rendered length of that type's section so far
555
+ let total = 0; // rendered length of `selected` under renderConcepts
556
+
538
557
  const take = (pool: Concept[]) => {
539
558
  for (const c of priorityOrder(pool, query)) {
540
- if (renderConcepts([...selected, c]).length <= budget) selected.push(c);
559
+ const itemLen = renderConceptItem(c).length;
560
+ const priorSectionLen = sectionLenByType.get(c.type);
561
+ let newSectionLen: number;
562
+ let newTotal: number;
563
+ if (priorSectionLen === undefined) {
564
+ // New section: "## header" + "\n" + item — plus a "\n\n" join if a section already exists.
565
+ const header = HEADER_BY_TYPE[c.type] ?? c.type;
566
+ newSectionLen = `## ${header}`.length + 1 + itemLen;
567
+ newTotal = total === 0 ? newSectionLen : total + 2 + newSectionLen;
568
+ } else {
569
+ // Existing section: one more "\n" + item line.
570
+ newSectionLen = priorSectionLen + 1 + itemLen;
571
+ newTotal = total - priorSectionLen + newSectionLen;
572
+ }
573
+ if (newTotal <= budget) {
574
+ selected.push(c);
575
+ sectionLenByType.set(c.type, newSectionLen);
576
+ total = newTotal;
577
+ }
541
578
  }
542
579
  };
543
580
  take(concepts.filter(c => c.pinned)); // reserved budget: invariants survive the cap
@@ -27,32 +27,58 @@ export interface OAuthCallbackFlowOptions {
27
27
 
28
28
  export type CallbackResult = { code: string; state: string };
29
29
 
30
- const PAGE_HTML = `<!doctype html><html><head><meta charset="utf-8"><title>jeo — login complete</title>
31
- <style>body{font-family:system-ui,sans-serif;background:#0d1117;color:#e6edf3;display:flex;align-items:center;justify-content:center;height:100vh;margin:0}
32
- .card{text-align:center;padding:2rem 3rem;border:1px solid #30363d;border-radius:12px;background:#161b22;max-width:30rem}
33
- h1{margin:0 0 .5rem;font-size:1.4rem}p{margin:.25rem 0;color:#8b949e}.hint{margin-top:1rem;font-size:.85rem;color:#6e7681}</style></head>
34
- <body><div class="card"><h1>__TITLE__</h1><p>__MSG__</p>__EXTRA__</div>__SCRIPT__</body></html>`;
35
-
36
- // Seconds before the success tab attempts to auto-close. `window.close()` only
30
+ // "jeo" wordmark — bold forged monospace lettering on a neon blue→violet→pink
31
+ // gradient (the mascot's synthwave palette), generated via god-tibo-imagen and
32
+ // matching jeo-pi's bold-wordmark typographic treatment. Embedded as a data URI
33
+ // (not a served static file) so the callback page renders standalone even from
34
+ // a bundled/compiled `jeo` binary with no static-asset pipeline.
35
+ const JEO_WORDMARK_DATA_URI =
36
+ "data:image/webp;base64,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";
37
+
38
+ const PAGE_HTML = `<!doctype html><html><head><meta charset="utf-8"><title>jeo — status</title>
39
+ <style>:root{color-scheme:dark}*{box-sizing:border-box}
40
+ body{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;background:#0b0a10;color:#e7e7ec;display:flex;align-items:center;justify-content:center;height:100vh;margin:0}
41
+ .card{text-align:center;padding:2.5rem 3rem;border:1px solid #2a2a35;border-radius:20px;background:#12131c;max-width:26rem}
42
+ .wordmark{width:88px;height:88px;display:block;margin:0 auto 1.25rem;border-radius:14px}
43
+ h1{margin:0 0 .5rem;font-size:1.1rem;font-weight:600}
44
+ h1.ok{color:#22d3ee}h1.fail{color:#f472b6}
45
+ p{margin:.25rem 0;color:#8b8b96;font-size:.92rem}
46
+ button{margin-top:1.5rem;padding:.55rem 1.6rem;border-radius:999px;border:1px solid #2a2a35;background:transparent;color:#e7e7ec;font:inherit;font-size:.88rem;cursor:pointer}
47
+ button:hover{background:#1b1c27}
48
+ .hint{margin-top:.9rem;font-size:.78rem;color:#5f5f6b}</style></head>
49
+ <body><div class="card">
50
+ <img class="wordmark" alt="jeo" src="${JEO_WORDMARK_DATA_URI}">
51
+ <h1 class="__STATUS__">__TITLE__</h1>
52
+ <p>__MSG__</p>
53
+ <button id="jeo-close" type="button">Close</button>
54
+ <p class="hint">Closing automatically in <span id="jeo-countdown">__SECONDS__</span>s…</p>
55
+ </div>
56
+ <script>__SCRIPT__</script>
57
+ </body></html>`;
58
+
59
+ // Seconds before the tab auto-closes — applies to BOTH the success and the
60
+ // failure page now (previously only success auto-closed). Pressing "Close"
61
+ // closes immediately without waiting out the countdown. `window.close()` only
37
62
  // succeeds on tabs the script itself opened; browsers may ignore it for
38
- // OS-launched OAuth tabs, so the page also shows a clear "you can close" hint.
39
- // ponytail: upgrade path — none needed; countdown is inline, no timer lib.
40
- const AUTO_CLOSE_SECONDS = 3;
63
+ // OS-launched OAuth tabs, so the button remains as the reliable fallback.
64
+ const AUTO_CLOSE_SECONDS = 5;
41
65
 
42
66
  function renderHtml(ok: boolean, msg: string): string {
43
- const extra = ok
44
- ? `<p class="hint">This window will close in <span id="jeo-countdown">${AUTO_CLOSE_SECONDS}</span>s — you can also close it manually.</p>`
45
- : "";
46
- const script = ok
47
- ? `<script>(function(){var n=${AUTO_CLOSE_SECONDS},el=document.getElementById("jeo-countdown");var t=setInterval(function(){n-=1;if(el)el.textContent=String(Math.max(n,0));if(n<=0){clearInterval(t);window.close();}},1000);})();</script>`
48
- : "";
67
+ const script = `(function(){
68
+ var n=${AUTO_CLOSE_SECONDS},el=document.getElementById("jeo-countdown"),btn=document.getElementById("jeo-close"),t;
69
+ function closeNow(){clearInterval(t);window.close();}
70
+ t=setInterval(function(){n-=1;if(el)el.textContent=String(Math.max(n,0));if(n<=0)closeNow();},1000);
71
+ if(btn)btn.addEventListener("click",closeNow);
72
+ })();`;
49
73
  return PAGE_HTML
74
+ .replace("__STATUS__", ok ? "ok" : "fail")
50
75
  .replace("__TITLE__", ok ? "Login complete \u2713" : "Login failed")
51
76
  .replace("__MSG__", msg)
52
- .replace("__EXTRA__", extra)
77
+ .replace("__SECONDS__", String(AUTO_CLOSE_SECONDS))
53
78
  .replace("__SCRIPT__", script);
54
79
  }
55
80
 
81
+
56
82
  export abstract class OAuthCallbackFlow {
57
83
  protected ctrl: OAuthController;
58
84
  protected preferredPort: number;
@@ -1,4 +1,5 @@
1
- import { verticalCursorOffset } from "../../tui/components/input-box";
1
+ import { verticalCursorOffset, rowBoundaryOffset } from "../../tui/components/input-box";
2
+
2
3
 
3
4
  export interface InFlightAbortHarness {
4
5
  controller: AbortController;
@@ -556,6 +557,17 @@ export function decideCtrlC(
556
557
  * `modifyOtherKeys` form (`CSI 27;2;13~`) and the kitty keyboard-protocol form
557
558
  * (`CSI 13;2u`). Shared by the live input filter and its tests so they can't drift. */
558
559
  export const SHIFT_ENTER_SEQS: readonly string[] = ["\u001b[27;2;13~", "\u001b[13;2u"];
560
+ /** Sequences that should jump the caret to the START of the current VISUAL ROW: macOS
561
+ * Cmd+Left (`CSI 1;9D`) and the platform-neutral Home key in both its xterm (`CSI H`)
562
+ * and vt220 (`CSI 1~`) forms, plus the SS3 variant (`ESC O H`) some terminals send in
563
+ * application-cursor-keys mode. Windows terminals have no Cmd-combo equivalent — bare
564
+ * Home is the "적절한 윈도우 단축키" for this action, so it must get the SAME row-aware
565
+ * treatment as Cmd+Left, not just readline's native (whole-buffer) binding. */
566
+ export const ROW_HOME_SEQS: readonly string[] = ["\u001b[1;9D", "\u001b[H", "\u001b[1~", "\u001bOH"];
567
+ /** Mirror of {@link ROW_HOME_SEQS} for the END of the current visual row: macOS
568
+ * Cmd+Right (`CSI 1;9C`) and the xterm/vt220/SS3 End key forms. */
569
+ export const ROW_END_SEQS: readonly string[] = ["\u001b[1;9C", "\u001b[F", "\u001b[4~", "\u001bOF"];
570
+
559
571
 
560
572
  /** Minimal readline view the prompt key-filter reads (and mutates `cursor` on a
561
573
  * vertical-nav "move"). The live filter passes the real readline interface; tests
@@ -697,6 +709,30 @@ export function filterPromptInputChunk(
697
709
  if (data.startsWith(seq, i)) { out += MULTILINE_SENTINEL; i += seq.length; matched = true; break; }
698
710
  }
699
711
  if (matched) continue;
712
+ // Row-aware Home/End: macOS Cmd+Left/Right and the bare Home/End keys (the Windows/
713
+ // Linux equivalent — there is no Cmd-combo there) jump to the START/END of the
714
+ // CURRENT VISUAL ROW, not the whole buffer. Native readline binds these to
715
+ // beginning-of-line/end-of-line, which is only correct on a single-row draft; on a
716
+ // multi-row draft (Shift+Enter breaks or box soft-wrap) it overshoots past the
717
+ // current row. Checked BEFORE `matchCursorCombo` (which would otherwise rewrite
718
+ // Cmd+Left/Right to the whole-buffer Ctrl+A/E) so this always wins when `rl` can
719
+ // supply a cursor to compute from; without `rl` it falls through to that rewrite
720
+ // (or, for bare Home/End, straight to readline's native binding) as a safe degrade.
721
+ const rowEdge = ROW_HOME_SEQS.find(seq => data.startsWith(seq, i)) !== undefined ? "start"
722
+ : ROW_END_SEQS.find(seq => data.startsWith(seq, i)) !== undefined ? "end"
723
+ : undefined;
724
+ if (rowEdge && rl) {
725
+ const seq = (rowEdge === "start" ? ROW_HOME_SEQS : ROW_END_SEQS).find(s => data.startsWith(s, i))!;
726
+ const line = rl.line ?? "";
727
+ const winCols = Math.max(24, env.columns - 1);
728
+ const textWidth = Math.max(1, Math.max(24, winCols) - 6);
729
+ const cur = typeof rl.cursor === "number" ? rl.cursor : line.length;
730
+ const expanded = line.split(MULTILINE_SENTINEL).join("\n");
731
+ rl.cursor = rowBoundaryOffset(expanded, cur, textWidth, rowEdge);
732
+ i += seq.length;
733
+ continue;
734
+ }
735
+
700
736
  const combo = matchCursorCombo(data, i);
701
737
  if (combo) { out += combo[1]; i += combo[0].length; continue; }
702
738
  // Up/Down between the box's visual rows (textarea feel) for any MULTI-ROW draft;
@@ -101,7 +101,8 @@ import {
101
101
  sessionPath,
102
102
  appendCompaction,
103
103
  } from "../agent/session";
104
- import { clearLine, cursorUp, toColumn, truncate as truncateAnsi, size as terminalSize, resetMouseTracking, clearScreen, clearVisible, clearToEnd } from "../tui/terminal";
104
+ import { clearLine, cursorUp, toColumn, truncate as truncateAnsi, size as terminalSize, resetMouseTracking, clearScreen, clearVisible, clearToEnd, enableModifyOtherKeys, disableModifyOtherKeys, enableKittyKeyboard, disableKittyKeyboard } from "../tui/terminal";
105
+
105
106
 
106
107
  import {
107
108
  type LaunchFlags,
@@ -1563,7 +1564,19 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1563
1564
  // 'keypress' too so the footer-redraw / paste-marker / picker listeners (registered
1564
1565
  // on process.stdin below) still fire.
1565
1566
  emitKeypressEvents(process.stdin);
1567
+ // Ask the terminal to make Shift+Enter DISTINGUISHABLE from plain Enter — without
1568
+ // this, `SHIFT_ENTER_SEQS`/the lone-LF opt-in never see anything to match, because
1569
+ // most terminals send NOTHING (or a byte identical to Enter) for Shift+Enter unless
1570
+ // one of these protocols is explicitly requested. Both are safe to send unconditionally:
1571
+ // a terminal that doesn't implement either silently ignores the unrecognized private-mode
1572
+ // sequence. Restored on exit so a shell/editor started afterward isn't left with an
1573
+ // unexpected keyboard-encoding mode.
1574
+ process.stdout.write(enableModifyOtherKeys() + enableKittyKeyboard());
1575
+ process.once("exit", () => {
1576
+ try { process.stdout.write(disableKittyKeyboard() + disableModifyOtherKeys()); } catch { /* terminal gone */ }
1577
+ });
1566
1578
  }
1579
+
1567
1580
  const rl = createInterface({
1568
1581
  input: keyFilter ?? process.stdin,
1569
1582
  // Single-box input: gate readline's output while the boxed footer is armed so its own
@@ -2825,17 +2838,23 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
2825
2838
  })();
2826
2839
  return;
2827
2840
  }
2828
- if (previewPending) return;
2829
2841
  // ESC (or a meta-mapped Cmd+C) at the prompt: wipe the typed text. A bare
2830
2842
  // ESC decodes as `escape` (meta is set for a lone ESC byte — accept both)
2831
2843
  // only after readline's escape-sequence timeout, so arrow/wheel sequences
2832
- // never trigger this.
2844
+ // never trigger this. Checked BEFORE the `previewPending` reentrancy gate
2845
+ // (like Ctrl+C above) — a fast typist/paste can deliver several keypress
2846
+ // events in ONE synchronous stdin read, all before the first keystroke's
2847
+ // setImmediate fires; gating this after `previewPending` silently dropped
2848
+ // an ESC that lands in the same burst as preceding keystrokes (the
2849
+ // "esc 입력 후 안 먹힘" bug — Esc right after typing intermittently did nothing).
2833
2850
  if (key && ((key.name === "escape" && !key.ctrl) || (key.meta && key.name === "c"))) {
2834
2851
  clearTypedInput();
2835
2852
  return;
2836
2853
  }
2854
+ if (previewPending) return;
2837
2855
  // Ctrl+C is handled above (clear-or-exit); keep this guard for defensive ordering only.
2838
2856
  if (key?.ctrl && key.name === "c") return;
2857
+
2839
2858
  previewPending = true;
2840
2859
  setImmediate(() => {
2841
2860
  previewPending = false;
@@ -242,3 +242,30 @@ export function verticalCursorOffset(
242
242
  }
243
243
  return best !== -1 ? best : firstOnRow;
244
244
  }
245
+ /** Caret offset at the START or END of the VISUAL ROW containing `cursor`, using the
246
+ * SAME wrapping rule as the input box (`caretCells`/`wrapWithCursor`). This is the
247
+ * row-aware counterpart to Home/End (and macOS Cmd+Left/Right): on a single-row draft
248
+ * it degenerates to the whole buffer's start/end (0 / length), but on a multi-row draft
249
+ * (Shift+Enter hard breaks OR a long line the box soft-wraps) it stops at the CURRENT
250
+ * row's boundary instead of jumping past other rows — matching the platform convention
251
+ * (macOS Cmd+←/→, editors' Home/End) of "start/end of THIS line", not "start/end of the
252
+ * whole document". */
253
+ export function rowBoundaryOffset(
254
+ text: string,
255
+ cursor: number,
256
+ width: number,
257
+ edge: "start" | "end",
258
+ ): number {
259
+ const cells = caretCells(text, Math.max(1, width));
260
+ if (cells.length === 0) return 0;
261
+ const pos = Math.max(0, Math.min(cursor, cells.length - 1));
262
+ const row = cells[pos]!.row;
263
+ let start = pos;
264
+ let end = pos;
265
+ for (let p = 0; p < cells.length; p++) {
266
+ if (cells[p]!.row !== row) continue;
267
+ if (p < start) start = p;
268
+ if (p > end) end = p;
269
+ }
270
+ return edge === "start" ? start : end;
271
+ }
@@ -100,3 +100,38 @@ export function isTTY(): boolean {
100
100
  export function truncate(line: string, cols: number): string {
101
101
  return truncateToWidth(line, cols);
102
102
  }
103
+ /** Request xterm's `modifyOtherKeys` mode 2 (`CSI > 4 ; 2 m`): the terminal starts
104
+ * encoding "inert" combos — Shift+Enter among them — as a full `CSI 27;<mod>;<code>~`
105
+ * sequence instead of sending a byte indistinguishable from plain Enter (or nothing at
106
+ * all). Without this, jeo's Shift+Enter → newline handling (`SHIFT_ENTER_SEQS` in
107
+ * `commands/launch/input.ts`) never actually fires on most terminals — the terminal
108
+ * never emits a sequence to match because nothing asked it to. Terminals that don't
109
+ * implement the mode (Windows Terminal's legacy conpty path, some minimal emulators)
110
+ * silently ignore the unrecognized private-mode sequence — safe to send unconditionally. */
111
+ export function enableModifyOtherKeys(): string {
112
+ return `${ESC}>4;2m`;
113
+ }
114
+
115
+ /** Restore `modifyOtherKeys` to its default (mode 0 — "no special encoding") on exit,
116
+ * the counterpart to {@link enableModifyOtherKeys}. */
117
+ export function disableModifyOtherKeys(): string {
118
+ return `${ESC}>4;0m`;
119
+ }
120
+
121
+ /** Enable the kitty keyboard protocol's "disambiguate escape codes" flag (bit 1 of
122
+ * `CSI > flags u`): the modern, more widely supported (kitty, wezterm, foot, contour,
123
+ * ghostty, iTerm2, and increasingly Windows Terminal) alternative delivery mechanism
124
+ * for the same Shift+Enter sequence jeo already recognizes (`CSI 13;2u`,
125
+ * `SHIFT_ENTER_SEQS`). Terminals without kitty-protocol support ignore the private-mode
126
+ * `u`-suffixed CSI sequence — safe to send unconditionally alongside
127
+ * {@link enableModifyOtherKeys} so whichever protocol the terminal supports (if either)
128
+ * makes Shift+Enter distinguishable from plain Enter. */
129
+ export function enableKittyKeyboard(): string {
130
+ return `${ESC}>1u`;
131
+ }
132
+
133
+ /** Reset the kitty keyboard protocol flags to 0 (disabled) on exit, the counterpart to
134
+ * {@link enableKittyKeyboard}. */
135
+ export function disableKittyKeyboard(): string {
136
+ return `${ESC}>0u`;
137
+ }