jeo-code 0.7.0 → 0.7.1

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,23 @@ 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.1] - 2026-06-22
10
+ _TUI polish + provider breadth: the live forge-card border and forge mark now flow in the **active theme's** own neon palette instead of a fixed brand gradient, the boxed prompt's Up/Down keys no longer wipe a multi-line draft at its top/bottom edge, the Tencent Cloud MaaS catalog gains the live-verified DeepSeek/MiniMax/GLM model families (with catalog backfill for providers that expose no models-list endpoint), and the `jeo --tmux` smoke check drops a false-positive launcher-log grep. Verified leak-free (`mem-probe`, 2000 turns, exit 0) with a fresh `jeo --tmux` boot battery (6/6)._
11
+
12
+ ### Added
13
+ - **Theme-derived forge flow palette.** `themeFlowPalette(theme)` builds a 3-stop neon palette (`accentShadow → accent → brightest stage-gradient end`) from the active TUI theme; the animated forge-card border and the forge mark now sweep the theme's own glow instead of the fixed brand `FORGE_FLOW_PALETTE`. Colorless/`mono` themes collapse to a single accent stop (the animation is already gated on color/TrueColor).
14
+ - **Expanded Tencent Cloud MaaS catalog.** Added live-verified model ids across DeepSeek (`deepseek-v4-pro`/`-202606`, `deepseek-v4-flash`/`-202605`, `deepseek-v3.2`), MiniMax (`minimax-m3`/`-m2.7`/`-m2.5`), and Zhipu GLM (`glm-5.2`/`5.1`/`5`). Ids were confirmed against live `/v1/messages` probes (the host exposes no `/v1/models` route).
15
+
16
+ ### Fixed
17
+ - **The boxed prompt's Up/Down keys no longer wipe a multi-line draft at its edges.** A new `boxVerticalNavAction` classifier returns `move` (a visual row exists → reposition the caret), `swallow` (no row AND a genuine multi-line draft → keep the keystroke inside the box, since falling through to readline would recall input history and erase the draft — the "↓ cuts the lower text" bug), or `history` (no row on a soft-wrapped single line → fall through so ↑/↓ recall history at the edges).
18
+ - **Model-picker no longer pins API-key providers to a bare default when the models-list endpoint is absent.** `catalogOr` now backfills the static capability catalog for OAuth sources *and* for API-key providers whose models endpoint returns HTTP 404 (e.g. Tencent MaaS), so the picker shows the full known model set instead of a single default.
19
+ - **`scripts/tmux-verify.sh smoke` no longer false-fails on the benign non-TTY attach.** The launcher-log grep was pure false-positive surface (it re-flagged the harmless `not a terminal` attach message); a real pre-handoff crash already fails via the session-start timeout and a post-handoff crash shows in the captured frame, so the rendered frame is the authoritative signal.
20
+
21
+ ### Verified
22
+ - `bun run typecheck` clean; full suite **1806 pass / 0 fail** (221 files).
23
+ - `scripts/mem-probe.ts` (2000 turns × 40 tools): no long-term leak (exit 0; settled heap floor flat at ~4.3 MB, exit-listeners flat at 1).
24
+ - `scripts/tmux-verify.sh battery`: **6/6 PASSED** on a fresh `jeo --tmux` boot (boot, `/help`, unknown `$skill`, `/agents`, `$ultragoal`, unresolved `/command`).
25
+
9
26
  ## [0.7.0] - 2026-06-22
10
27
  _Adds an opt-in, fail-closed **computer-use** capability (a `computer` tool + `jeo computer` CLI for screenshot/click/type/keypress/scroll/drag/wait/batch), hardens the spec-first workflow against silent post-consensus plan edits and PID-reuse lock starvation, makes OAuth refresh degrade cleanly when the refresh token is dead, and teaches tool-arg JSON repair to tolerate unescaped control characters. Verified leak-free (`mem-probe`, 2000 turns, −447 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6)._
11
28
 
package/README.ja.md CHANGED
@@ -193,11 +193,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
193
193
  ## 変更履歴 (Changelog)
194
194
 
195
195
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
196
+ - **[0.7.1]** (2026-06-22) — TUI polish + provider breadth: the live forge-card border and forge mark now flow in the **active theme's** own neon palette instead of a fixed brand gradient, the boxed prompt's Up/Down keys no longer wipe a multi-line draft at its top/bottom edge, the Tencent Cloud MaaS catalog gains the live-verified DeepSeek/MiniMax/GLM model families (with catalog backfill for providers that expose no models-list endpoint), and the `jeo --tmux` smoke check drops a false-positive launcher-log grep. Verified leak-free (`mem-probe`, 2000 turns, exit 0) with a fresh `jeo --tmux` boot battery (6/6).
196
197
  - **[0.7.0]** (2026-06-22) — Adds an opt-in, fail-closed **computer-use** capability (a `computer` tool + `jeo computer` CLI for screenshot/click/type/keypress/scroll/drag/wait/batch), hardens the spec-first workflow against silent post-consensus plan edits and PID-reuse lock starvation, makes OAuth refresh degrade cleanly when the refresh token is dead, and teaches tool-arg JSON repair to tolerate unescaped control characters. Verified leak-free (`mem-probe`, 2000 turns, −447 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6).
197
198
  - **[0.6.41]** (2026-06-21) — The non-streaming model path now inherits the v0.6.40 300s window: an unattended long-reasoning turn (compaction, ralplan, deep-interview, memory distill, goal-verify, subagent/autopilot steps) is no longer false-aborted at the old 120s wall cap. Re-verified leak-free (`mem-probe`, 2000 turns, −610 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6).
198
199
  - **[0.6.40]** (2026-06-21) — Wire-level stream heartbeat: ANY bytes from the provider (SSE keepalive/ping, filtered events) now re-arm the idle watchdog — a connected-but-quiet stream can no longer trip a false `stream idle` retry. Default idle window raised to 300s so Ollama/llama.cpp model-load silence no longer exhausts retries and stops the turn.
199
200
  - **[0.6.39]** (2026-06-21) — A long "thinking" phase no longer trips a false stream-idle retry: reasoning/thinking deltas now act as a stream heartbeat, so a model that streams thought tokens past the idle window before any visible text is no longer mistaken for a stalled stream and retried (which discarded the in-progress reasoning). Re-verified leak-free (`mem-probe`, 2000 turns, −525 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6).
200
- - **[0.6.38]** (2026-06-21) — The OKF concept-bundle memory no longer silently drops what it learns: a legacy single-doc `MEMORY.md` (or a text-only distill fallback) can no longer shadow the concept bundle, break OKF conformance, or lose a turn's learnings — its content is folded into the concept merge and the stale blob is archived. Re-verified leak-free (`mem-probe`, 2000 turns, −570 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6).
201
201
 
202
202
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
203
203
  <!-- CHANGELOG:END -->
package/README.ko.md CHANGED
@@ -193,11 +193,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
193
193
  ## 변경 이력 (Changelog)
194
194
 
195
195
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
196
+ - **[0.7.1]** (2026-06-22) — TUI polish + provider breadth: the live forge-card border and forge mark now flow in the **active theme's** own neon palette instead of a fixed brand gradient, the boxed prompt's Up/Down keys no longer wipe a multi-line draft at its top/bottom edge, the Tencent Cloud MaaS catalog gains the live-verified DeepSeek/MiniMax/GLM model families (with catalog backfill for providers that expose no models-list endpoint), and the `jeo --tmux` smoke check drops a false-positive launcher-log grep. Verified leak-free (`mem-probe`, 2000 turns, exit 0) with a fresh `jeo --tmux` boot battery (6/6).
196
197
  - **[0.7.0]** (2026-06-22) — Adds an opt-in, fail-closed **computer-use** capability (a `computer` tool + `jeo computer` CLI for screenshot/click/type/keypress/scroll/drag/wait/batch), hardens the spec-first workflow against silent post-consensus plan edits and PID-reuse lock starvation, makes OAuth refresh degrade cleanly when the refresh token is dead, and teaches tool-arg JSON repair to tolerate unescaped control characters. Verified leak-free (`mem-probe`, 2000 turns, −447 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6).
197
198
  - **[0.6.41]** (2026-06-21) — The non-streaming model path now inherits the v0.6.40 300s window: an unattended long-reasoning turn (compaction, ralplan, deep-interview, memory distill, goal-verify, subagent/autopilot steps) is no longer false-aborted at the old 120s wall cap. Re-verified leak-free (`mem-probe`, 2000 turns, −610 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6).
198
199
  - **[0.6.40]** (2026-06-21) — Wire-level stream heartbeat: ANY bytes from the provider (SSE keepalive/ping, filtered events) now re-arm the idle watchdog — a connected-but-quiet stream can no longer trip a false `stream idle` retry. Default idle window raised to 300s so Ollama/llama.cpp model-load silence no longer exhausts retries and stops the turn.
199
200
  - **[0.6.39]** (2026-06-21) — A long "thinking" phase no longer trips a false stream-idle retry: reasoning/thinking deltas now act as a stream heartbeat, so a model that streams thought tokens past the idle window before any visible text is no longer mistaken for a stalled stream and retried (which discarded the in-progress reasoning). Re-verified leak-free (`mem-probe`, 2000 turns, −525 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6).
200
- - **[0.6.38]** (2026-06-21) — The OKF concept-bundle memory no longer silently drops what it learns: a legacy single-doc `MEMORY.md` (or a text-only distill fallback) can no longer shadow the concept bundle, break OKF conformance, or lose a turn's learnings — its content is folded into the concept merge and the stale blob is archived. Re-verified leak-free (`mem-probe`, 2000 turns, −570 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6).
201
201
 
202
202
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
203
203
  <!-- CHANGELOG:END -->
package/README.md CHANGED
@@ -195,11 +195,11 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
195
195
  ## Changelog
196
196
 
197
197
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
198
+ - **[0.7.1]** (2026-06-22) — TUI polish + provider breadth: the live forge-card border and forge mark now flow in the **active theme's** own neon palette instead of a fixed brand gradient, the boxed prompt's Up/Down keys no longer wipe a multi-line draft at its top/bottom edge, the Tencent Cloud MaaS catalog gains the live-verified DeepSeek/MiniMax/GLM model families (with catalog backfill for providers that expose no models-list endpoint), and the `jeo --tmux` smoke check drops a false-positive launcher-log grep. Verified leak-free (`mem-probe`, 2000 turns, exit 0) with a fresh `jeo --tmux` boot battery (6/6).
198
199
  - **[0.7.0]** (2026-06-22) — Adds an opt-in, fail-closed **computer-use** capability (a `computer` tool + `jeo computer` CLI for screenshot/click/type/keypress/scroll/drag/wait/batch), hardens the spec-first workflow against silent post-consensus plan edits and PID-reuse lock starvation, makes OAuth refresh degrade cleanly when the refresh token is dead, and teaches tool-arg JSON repair to tolerate unescaped control characters. Verified leak-free (`mem-probe`, 2000 turns, −447 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6).
199
200
  - **[0.6.41]** (2026-06-21) — The non-streaming model path now inherits the v0.6.40 300s window: an unattended long-reasoning turn (compaction, ralplan, deep-interview, memory distill, goal-verify, subagent/autopilot steps) is no longer false-aborted at the old 120s wall cap. Re-verified leak-free (`mem-probe`, 2000 turns, −610 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6).
200
201
  - **[0.6.40]** (2026-06-21) — Wire-level stream heartbeat: ANY bytes from the provider (SSE keepalive/ping, filtered events) now re-arm the idle watchdog — a connected-but-quiet stream can no longer trip a false `stream idle` retry. Default idle window raised to 300s so Ollama/llama.cpp model-load silence no longer exhausts retries and stops the turn.
201
202
  - **[0.6.39]** (2026-06-21) — A long "thinking" phase no longer trips a false stream-idle retry: reasoning/thinking deltas now act as a stream heartbeat, so a model that streams thought tokens past the idle window before any visible text is no longer mistaken for a stalled stream and retried (which discarded the in-progress reasoning). Re-verified leak-free (`mem-probe`, 2000 turns, −525 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6).
202
- - **[0.6.38]** (2026-06-21) — The OKF concept-bundle memory no longer silently drops what it learns: a legacy single-doc `MEMORY.md` (or a text-only distill fallback) can no longer shadow the concept bundle, break OKF conformance, or lose a turn's learnings — its content is folded into the concept merge and the stale blob is archived. Re-verified leak-free (`mem-probe`, 2000 turns, −570 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6).
203
203
 
204
204
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
205
205
  <!-- CHANGELOG:END -->
package/README.zh.md CHANGED
@@ -193,11 +193,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
193
193
  ## 更新日志 (Changelog)
194
194
 
195
195
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
196
+ - **[0.7.1]** (2026-06-22) — TUI polish + provider breadth: the live forge-card border and forge mark now flow in the **active theme's** own neon palette instead of a fixed brand gradient, the boxed prompt's Up/Down keys no longer wipe a multi-line draft at its top/bottom edge, the Tencent Cloud MaaS catalog gains the live-verified DeepSeek/MiniMax/GLM model families (with catalog backfill for providers that expose no models-list endpoint), and the `jeo --tmux` smoke check drops a false-positive launcher-log grep. Verified leak-free (`mem-probe`, 2000 turns, exit 0) with a fresh `jeo --tmux` boot battery (6/6).
196
197
  - **[0.7.0]** (2026-06-22) — Adds an opt-in, fail-closed **computer-use** capability (a `computer` tool + `jeo computer` CLI for screenshot/click/type/keypress/scroll/drag/wait/batch), hardens the spec-first workflow against silent post-consensus plan edits and PID-reuse lock starvation, makes OAuth refresh degrade cleanly when the refresh token is dead, and teaches tool-arg JSON repair to tolerate unescaped control characters. Verified leak-free (`mem-probe`, 2000 turns, −447 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6).
197
198
  - **[0.6.41]** (2026-06-21) — The non-streaming model path now inherits the v0.6.40 300s window: an unattended long-reasoning turn (compaction, ralplan, deep-interview, memory distill, goal-verify, subagent/autopilot steps) is no longer false-aborted at the old 120s wall cap. Re-verified leak-free (`mem-probe`, 2000 turns, −610 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6).
198
199
  - **[0.6.40]** (2026-06-21) — Wire-level stream heartbeat: ANY bytes from the provider (SSE keepalive/ping, filtered events) now re-arm the idle watchdog — a connected-but-quiet stream can no longer trip a false `stream idle` retry. Default idle window raised to 300s so Ollama/llama.cpp model-load silence no longer exhausts retries and stops the turn.
199
200
  - **[0.6.39]** (2026-06-21) — A long "thinking" phase no longer trips a false stream-idle retry: reasoning/thinking deltas now act as a stream heartbeat, so a model that streams thought tokens past the idle window before any visible text is no longer mistaken for a stalled stream and retried (which discarded the in-progress reasoning). Re-verified leak-free (`mem-probe`, 2000 turns, −525 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6).
200
- - **[0.6.38]** (2026-06-21) — The OKF concept-bundle memory no longer silently drops what it learns: a legacy single-doc `MEMORY.md` (or a text-only distill fallback) can no longer shadow the concept bundle, break OKF conformance, or lose a turn's learnings — its content is folded into the concept merge and the stale blob is archived. Re-verified leak-free (`mem-probe`, 2000 turns, −570 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6).
201
201
 
202
202
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
203
203
  <!-- CHANGELOG:END -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jeo-code",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Clean, highly optimized AI coding agent using spec-first loop",
5
5
  "type": "module",
6
6
  "main": "src/cli.ts",
@@ -109,9 +109,27 @@ export const MODEL_CATALOG: readonly CatalogModel[] = [
109
109
  company: id.includes("claude") ? "Anthropic via Antigravity" : id.includes("gpt") ? "OpenAI via Antigravity" : "Google Antigravity",
110
110
  })),
111
111
  // Tencent
112
+ // Tencent — DeepSeek
112
113
  { canonical: "deepseek-v4-pro", provider: "tencent", providerModel: "deepseek-v4-pro", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent" },
114
+ { canonical: "deepseek-v4-pro-202606", provider: "tencent", providerModel: "deepseek-v4-pro-202606", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent" },
113
115
  { canonical: "deepseek-v4-flash", provider: "tencent", providerModel: "deepseek-v4-flash", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent" },
116
+ { canonical: "deepseek-v4-flash-202605", provider: "tencent", providerModel: "deepseek-v4-flash-202605", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent" },
117
+ { canonical: "deepseek-v3.2", provider: "tencent", providerModel: "deepseek-v3.2", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent" },
118
+ // Tencent — MiniMax
114
119
  { canonical: "minimax-m3", provider: "tencent", providerModel: "minimax-m3", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent" },
120
+ { canonical: "minimax-m2.7", provider: "tencent", providerModel: "minimax-m2.7", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent" },
121
+ { canonical: "minimax-m2.5", provider: "tencent", providerModel: "minimax-m2.5", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent" },
122
+ // Tencent — Zhipu GLM
123
+ { canonical: "glm-5.2", provider: "tencent", providerModel: "glm-5.2", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent" },
124
+ { canonical: "glm-5.1", provider: "tencent", providerModel: "glm-5.1", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent" },
125
+ { canonical: "glm-5", provider: "tencent", providerModel: "glm-5", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent" },
126
+ { canonical: "glm-5-turbo", provider: "tencent", providerModel: "glm-5-turbo", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent" },
127
+ { canonical: "glm-5v-turbo", provider: "tencent", providerModel: "glm-5v-turbo", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: true, company: "Tencent" },
128
+ // Tencent — Moonshot Kimi
129
+ { canonical: "kimi-k2.6", provider: "tencent", providerModel: "kimi-k2.6", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent" },
130
+ { canonical: "kimi-k2.5", provider: "tencent", providerModel: "kimi-k2.5", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent" },
131
+ // Tencent — Hunyuan MT
132
+ { canonical: "hy-mt2-plus", provider: "tencent", providerModel: "hy-mt2-plus", contextTokens: 128_000, maxOutputTokens: 8192, thinking: STD, images: false, company: "Tencent" },
115
133
  // Ollama (local)
116
134
  { canonical: "qwen2.5", provider: "ollama", providerModel: "ollama/qwen2.5:0.5b", contextTokens: 32_768, maxOutputTokens: 8_192, thinking: [], images: false },
117
135
  ];
@@ -270,23 +270,41 @@ export function parseModelsBody(provider: ProviderName, body: unknown): string[]
270
270
 
271
271
  /**
272
272
  * When a provider is authenticated (oauth/api_key) but the live `models` endpoint
273
- * is unusable — e.g. ChatGPT/Codex OAuth tokens are rejected by `api.openai.com/v1/models`
274
- * — surface the static catalog ids so the provider's models still appear in pickers.
275
- * Keyless/not-logged-in results are returned unchanged.
273
+ * is unusable — e.g. ChatGPT/Codex OAuth tokens are rejected by `api.openai.com/v1/models`,
274
+ * or a provider (Tencent MaaS) simply has no `/v1/models` route — surface the static
275
+ * catalog ids so the provider's models still appear in pickers.
276
+ * Keyless/not-logged-in results, and api_key *auth rejections* (a bad key), are
277
+ * returned unchanged so they stay honest failures.
276
278
  */
277
279
  export function catalogOr(result: ProviderModelsResult): ProviderModelsResult {
278
280
  if (result.ok && result.models.length > 0) return result;
279
- // OpenAI/Codex OAuth legitimately rejects the standard /models endpoint while
280
- // the fixed Codex ids still work for calls. Other OAuth providers may fall
281
- // back to their static catalog too — EXCEPT Antigravity, whose available
282
- // models depend on the Cloud Code Assist agent backend and must not be faked.
283
- if (result.source !== "oauth") return result;
281
+ // Antigravity's available models depend on the Cloud Code Assist agent backend
282
+ // and must never be faked from a hard-coded catalog.
284
283
  if (result.provider === "antigravity") return result;
285
- const ids = result.provider === "openai" ? [...CODEX_MODELS] : catalogByProvider(result.provider).map(m => m.providerModel);
284
+ // OAuth: OpenAI/Codex legitimately rejects the standard /models endpoint while
285
+ // the fixed Codex ids still work; other OAuth providers fall back to their full
286
+ // static catalog too. API-key providers only fall back when the provider has NO
287
+ // models-list endpoint at all (HTTP 404 / not found) — never for an auth
288
+ // rejection, so a wrong key stays a failure instead of fabricating catalog rows.
289
+ const eligible = result.source === "oauth"
290
+ || (result.source === "api_key" && isMissingModelsEndpoint(result.error));
291
+ if (!eligible) return result;
292
+ const ids = result.provider === "openai" && result.source === "oauth"
293
+ ? [...CODEX_MODELS]
294
+ : catalogByProvider(result.provider).map(m => m.providerModel);
286
295
  if (ids.length === 0) return result;
287
296
  return { ...result, models: ids, ok: true, fallback: true };
288
297
  }
289
298
 
299
+ /**
300
+ * True when a discovery error means the models-list endpoint itself is absent
301
+ * (HTTP 404 / "not found") rather than an auth/key or transient network problem.
302
+ */
303
+ function isMissingModelsEndpoint(error: string | undefined): boolean {
304
+ if (!error) return false;
305
+ return /(^|\D)404(\D|$)/.test(error) || /not found/i.test(error);
306
+ }
307
+
290
308
  /** Discover the live model list for one provider. Never throws. */
291
309
  export async function listProviderModels(
292
310
  provider: ProviderName,
@@ -67,8 +67,8 @@ export const OPENAI_COMPAT_PROVIDERS: readonly OpenAICompatProviderDef[] = [
67
67
  // Anthropic-Messages-protocol providers (served via makeAnthropicCompatibleAdapter).
68
68
  { name: "zai", label: "z.ai", baseUrl: "https://api.z.ai/api/anthropic", apiKeyEnv: "ZAI_API_KEY", defaultModel: "zai/glm-5.2", protocol: "anthropic" },
69
69
  { name: "minimax", label: "MiniMax", baseUrl: "https://api.minimax.io/anthropic", apiKeyEnv: "MINIMAX_API_KEY", defaultModel: "minimax/minimax-m3", protocol: "anthropic" },
70
- // Tencent Cloud MaaS (international) serves deepseek-v4 / minimax-m3 over the Anthropic Messages wire format at tokenhub-intl; `${base}/v1/messages` is pinned by the adapter.
71
- { name: "tencent", label: "Tencent", baseUrl: "https://tokenhub-intl.tencentcloudmaas.com", apiKeyEnv: "TENCENT_API_KEY", defaultModel: "tencent/deepseek-v4-pro", knownModels: ["deepseek-v4-pro", "deepseek-v4-flash", "minimax-m3"], protocol: "anthropic" },
70
+ // Tencent Cloud MaaS (international) serves deepseek-v4 / deepseek-v3.2 / minimax-m3 over the Anthropic Messages wire format at tokenhub-intl; `${base}/v1/messages` is pinned by the adapter. Model ids verified live (2026-06) via /v1/messages probes — the host has no /v1/models route.
71
+ { name: "tencent", label: "Tencent", baseUrl: "https://tokenhub-intl.tencentcloudmaas.com", apiKeyEnv: "TENCENT_API_KEY", defaultModel: "tencent/deepseek-v4-pro", knownModels: ["deepseek-v4-pro", "deepseek-v4-flash", "minimax-m3", "deepseek-v3.2"], protocol: "anthropic" },
72
72
  ];
73
73
 
74
74
  const BY_NAME = new Map<string, OpenAICompatProviderDef>(OPENAI_COMPAT_PROVIDERS.map(p => [p.name, p]));
@@ -1,3 +1,5 @@
1
+ import { verticalCursorOffset } from "../../tui/components/input-box";
2
+
1
3
  export interface InFlightAbortHarness {
2
4
  controller: AbortController;
3
5
  handleSigint(): void;
@@ -86,6 +88,34 @@ export function shouldBoxVerticalNav(
86
88
  return line.length > 0 && opts.slashMatchCount === 0 && !opts.historyPanelOpen;
87
89
  }
88
90
 
91
+ /** The action an Up/Down keystroke should take on the boxed prompt, given the draft and
92
+ * caret. `expandedLine` has explicit breaks expanded to "\n" (the wrapped row model the
93
+ * box renders); `rawLine` keeps the sentinels so a genuine multi-line draft is detectable.
94
+ *
95
+ * - "move": a visual row exists in `dir` → reposition the caret to `cursor` (textarea feel).
96
+ * - "swallow": no row to move to AND the draft is a GENUINE multi-line message → keep the
97
+ * keystroke INSIDE the box. Falling through to readline at the top/bottom edge
98
+ * would recall input history and WIPE the multi-line draft the user is composing
99
+ * (the "↓ cuts the lower text" bug) — so the boundary key is a deliberate no-op.
100
+ * - "history": no row to move to on a SOFT-WRAPPED single line → fall through to readline so
101
+ * ↑/↓ recall input history at the edges (the dominant one-liner REPL expectation). */
102
+ export type BoxVerticalNavAction =
103
+ | { kind: "move"; cursor: number }
104
+ | { kind: "swallow" }
105
+ | { kind: "history" };
106
+
107
+ export function boxVerticalNavAction(
108
+ expandedLine: string,
109
+ rawLine: string,
110
+ cursor: number,
111
+ width: number,
112
+ dir: "up" | "down",
113
+ ): BoxVerticalNavAction {
114
+ const next = verticalCursorOffset(expandedLine, cursor, width, dir);
115
+ if (next != null) return { kind: "move", cursor: next };
116
+ return isGenuineMultilineDraft(rawLine) ? { kind: "swallow" } : { kind: "history" };
117
+ }
118
+
89
119
 
90
120
  /**
91
121
  * macOS / fixterms combo-key normalization for the boxed prompt's line editor.
@@ -62,7 +62,7 @@ import { liveModelPicker, renderLiveModelPicker, type ModelAssignmentBadge } fro
62
62
  import { loginPicker, renderLoginPicker, onboardingPicker, renderOnboardingPicker, apiKeyPicker, renderApiKeyPicker, subscriptionLoginPicker, type OnboardingAction } from "../tui/components/provider-picker";
63
63
  import { detectLanguage, languageLabel, parseLineRange, sliceLines, formatCodeBlock, formatDiff, sanitizeForTerminal } from "../tui/components/code-view";
64
64
  import { categoryBadge } from "../tui/components/category-index";
65
- import { renderInputFrame, verticalCursorOffset, type HighlightRange } from "../tui/components/input-box";
65
+ import { renderInputFrame, type HighlightRange } from "../tui/components/input-box";
66
66
 
67
67
  import { renderStatusBar } from "../tui/components/status";
68
68
  import { detectColorLevel, ColorLevel, visibleWidth } from "../tui/components/color";
@@ -139,6 +139,7 @@ import {
139
139
  MULTILINE_SENTINEL,
140
140
  isGenuineMultilineDraft,
141
141
  shouldBoxVerticalNav,
142
+ boxVerticalNavAction,
142
143
  queuePromptInputChunk,
143
144
  captureLivePromptInputChunk,
144
145
  restoreQueuedLinesToPrefill,
@@ -209,6 +210,7 @@ export {
209
210
  MULTILINE_SENTINEL,
210
211
  isGenuineMultilineDraft,
211
212
  shouldBoxVerticalNav,
213
+ boxVerticalNavAction,
212
214
  queuePromptInputChunk,
213
215
  captureLivePromptInputChunk,
214
216
  restoreQueuedLinesToPrefill,
@@ -250,11 +252,11 @@ function providerDefaultModel(p: ProviderName): string {
250
252
  * empty.
251
253
  *
252
254
  * Live discovery yields ids only for a logged-in, reachable provider, and
253
- * `catalogOr` backfills the static catalog ONLY for OAuth sources — so an
254
- * API-key/keyless provider that isn't configured yet had an empty list and was
255
- * silently pinned to a bare default. Prefer live ids; else the provider's
256
- * capability catalog; else its single known default model (all 24 OpenAI-compat
257
- * providers carry one) so the user always sees at least one pickable id.
255
+ * `catalogOr` backfills the static catalog for OAuth sources and for API-key
256
+ * providers whose models-list endpoint is absent (HTTP 404, e.g. Tencent MaaS).
257
+ * A not-yet-configured provider still has an empty live list, so prefer live ids;
258
+ * else the provider's capability catalog; else its single known default model (all
259
+ * 24 OpenAI-compat providers carry one) so the user always sees at least one id.
258
260
  */
259
261
  export function providerPickEntries(live: ProviderModelsResult[], want: ProviderName): PickEntry[] {
260
262
  const fromLive = flattenModels(live.filter(r => r.provider === want));
@@ -1471,11 +1473,13 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1471
1473
  if (combo) { out += combo[1]; i += combo[0].length; continue; }
1472
1474
  // Up/Down move the caret BETWEEN the box's visual rows (textarea feel) for any
1473
1475
  // MULTI-ROW draft — explicit Shift+Enter breaks (→ SENTINEL) OR a long line the
1474
- // box soft-wraps. The decision is boundary-aware: verticalCursorOffset returns a
1475
- // target only when a visual row exists to move to, so ↑ on the TOP row, ↓ on the
1476
- // BOTTOM row, and any single-visual-row draft fall through to readline and recall
1477
- // input history — the dominant REPL expectation once the caret can climb no
1478
- // higher. Skipped when a slash list or Ctrl+O history panel owns ↑/↓.
1476
+ // box soft-wraps. The decision is boundary-aware: an in-box visual row exists →
1477
+ // move the caret there. With NO row to move to (↑ on the TOP row, ↓ on the BOTTOM
1478
+ // row), a SOFT-WRAPPED one-liner falls through to readline to recall input history
1479
+ // (the dominant REPL expectation), but a GENUINE multi-line draft SWALLOWS the key
1480
+ // instead — falling through there would let readline's history nav WIPE the
1481
+ // multi-line message being composed (the "↓ cuts the lower text" bug). Skipped
1482
+ // entirely when a slash list or Ctrl+O history panel owns ↑/↓.
1479
1483
 
1480
1484
  if ((data.startsWith("\u001b[", i) || data.startsWith("\u001bO", i)) && (data[i + 2] === "A" || data[i + 2] === "B")) {
1481
1485
  const dir = data[i + 2] === "A" ? "up" : "down";
@@ -1484,8 +1488,9 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1484
1488
  const winCols = Math.max(24, (process.stdout.columns ?? 80) - 1);
1485
1489
  const textWidth = Math.max(1, Math.max(24, winCols) - 6);
1486
1490
  const cur = typeof activeRl.cursor === "number" ? activeRl.cursor : line.length;
1487
- const next = verticalCursorOffset(expandSentinel(line), cur, textWidth, dir);
1488
- if (next != null) { activeRl.cursor = next; i += 3; continue; }
1491
+ const action = boxVerticalNavAction(expandSentinel(line), line, cur, textWidth, dir);
1492
+ if (action.kind === "move") { activeRl.cursor = action.cursor; i += 3; continue; }
1493
+ if (action.kind === "swallow") { i += 3; continue; } // keep the multi-line draft intact
1489
1494
  }
1490
1495
  out += data.slice(i, i + 3); i += 3; continue;
1491
1496
  }
@@ -2167,10 +2172,13 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
2167
2172
  process.stdin.setRawMode(true);
2168
2173
  }
2169
2174
  process.stdin.resume();
2170
- const cols = Math.max(40, terminalSize().cols - 2);
2171
- const rows = Math.max(6, terminalSize().rows - 6);
2172
2175
  let rendered = 0;
2173
2176
  const repaint = () => {
2177
+ // Re-measure on EVERY paint so the picker tracks live terminal resizes. The old code
2178
+ // captured cols/rows ONCE at open, so a picker left open across a resize never reflowed
2179
+ // and stayed pinned to its opening geometry — it never recovered after a shrink→grow.
2180
+ const cols = Math.max(40, terminalSize().cols - 2);
2181
+ const rows = Math.max(6, terminalSize().rows - 6);
2174
2182
  const lines = render(cols, rows).map(line => truncateAnsi(line, cols));
2175
2183
  const total = Math.max(rendered, lines.length);
2176
2184
  // First paint: drop down ONCE to start on a fresh row; subsequent paints:
@@ -2205,6 +2213,19 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
2205
2213
  rendered = 0;
2206
2214
  };
2207
2215
  repaint();
2216
+ // Reflow the picker on terminal resize. The terminal has already reflowed the old
2217
+ // block's rows, so `rendered` no longer maps 1:1 to physical rows: drop the stale
2218
+ // accounting, wipe the visible screen (scrollback kept), and repaint at the freshly
2219
+ // measured geometry. Debounced so a drag-resize coalesces into a single clean repaint.
2220
+ let resizeTimer: ReturnType<typeof setTimeout> | undefined;
2221
+ const onPickerResize = () => {
2222
+ if (resizeTimer) clearTimeout(resizeTimer);
2223
+ resizeTimer = setTimeout(() => {
2224
+ resizeTimer = undefined;
2225
+ try { rendered = 0; out.write(clearVisible()); repaint(); } catch { /* resize render race */ }
2226
+ }, 16);
2227
+ };
2228
+ process.stdout.on("resize", onPickerResize);
2208
2229
  try {
2209
2230
  await new Promise<void>(resolve => {
2210
2231
  const handler = (ch: string, key: { name?: string; ctrl?: boolean; meta?: boolean } | undefined) => {
@@ -2220,6 +2241,8 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
2220
2241
  process.stdin.on("keypress", handler);
2221
2242
  });
2222
2243
  } finally {
2244
+ process.stdout.off("resize", onPickerResize);
2245
+ if (resizeTimer) clearTimeout(resizeTimer);
2223
2246
  if (process.stdin.setRawMode && !wasRaw) {
2224
2247
  process.stdin.setRawMode(false);
2225
2248
  }
package/src/tui/app.ts CHANGED
@@ -16,13 +16,13 @@ import { Spinner } from "./components/spinner";
16
16
  import { ToolList } from "./components/tool-list";
17
17
  import { StreamRegion } from "./components/stream";
18
18
  import { renderFooter, type FooterData } from "./components/footer";
19
- import { renderForgeMark, forgeMarkHeight, forgeMarkFrameCount, forgeBeat, FORGE_FLOW_PALETTE } from "./components/ascii-art";
19
+ import { renderForgeMark, forgeMarkHeight, forgeMarkFrameCount, forgeBeat } from "./components/ascii-art";
20
20
  import { evolutionTrack, createStageProgress, type StageProgress, transitionMessage } from "./components/evolution";
21
21
  import type { TaskSubEvent } from "../agent/task-tool";
22
22
  import { supportsUnicode } from "./components/capability";
23
23
  import { centerBlock, padLineTo, boxBlock, BOX_ASCII, BOX_UNICODE } from "./components/layout";
24
24
  import { SECTION_GAP, sectionLabel, stackSections } from "./components/section";
25
- import { resolveTheme, themeGradient, accentPaint, accentShadowPaint, diffPaint, mutedPaint, cardFillPaint } from "./components/themes";
25
+ import { resolveTheme, themeGradient, accentPaint, accentShadowPaint, diffPaint, mutedPaint, cardFillPaint, themeFlowPalette } from "./components/themes";
26
26
  import { detectColorLevel, animatedGradientText, ColorLevel } from "./components/color";
27
27
  import { formatForgeBox, summarizeForgeInvocation, summarizeForgeResult, fitForgeBoxes, webSearchCardLines, scaleForgeWidth, type ForgeSummary } from "./components/forge";
28
28
  import { renderStatusBar, renderStatusBox, type StatusBoxData } from "./components/status";
@@ -314,6 +314,9 @@ export class LaunchTui {
314
314
  private readonly unicode: boolean = supportsUnicode();
315
315
  // Active color theme (JEO_TUI_THEME), default cosmic; `mono` disables color.
316
316
  private readonly theme = resolveTheme(process.env);
317
+ // Neon flow palette derived from the active theme (forge-card border + forge mark
318
+ // animation), so the live glow tracks the theme instead of a fixed brand palette.
319
+ private readonly themeFlow = themeFlowPalette(this.theme);
317
320
  // Whether the live turn may use the alternate screen buffer (real TTY only).
318
321
  private readonly tty: boolean;
319
322
  // gjc-style inline rendering (default on a TTY): the live frame repaints in place in
@@ -968,7 +971,6 @@ export class LaunchTui {
968
971
  this.subagentLive = null; // fresh turn: no nested subagent in flight
969
972
  this.activityLog.length = 0; // per-turn ring: timestamps are turn-relative
970
973
  this.spinner.updateStep(0, this.footer.maxSteps);
971
- // On a real TTY the live turn renders gjc-style in the MAIN buffer by default:
972
974
  // completed ledger lines are flushed into normal scrollback as they happen, so a
973
975
  // tmux / terminal mouse-wheel scroll can review earlier progress mid-turn. The
974
976
  // differential renderer reserves frame rows with real newlines, keeping the
@@ -1278,7 +1280,7 @@ export class LaunchTui {
1278
1280
  // the card border and the prompt beat marks the title. Flushed/final cards
1279
1281
  // stay static. Suppressed while `dim` (in-flight shading takes precedence).
1280
1282
  ...(anim && !dim
1281
- ? { flow: { palette: FORGE_FLOW_PALETTE, phase: anim.phase, colorLevel: anim.colorLevel }, titleMark: anim.beat }
1283
+ ? { flow: { palette: this.themeFlow, phase: anim.phase, colorLevel: anim.colorLevel }, titleMark: anim.beat }
1282
1284
  : {}),
1283
1285
  }));
1284
1286
  }
@@ -1570,6 +1572,7 @@ export class LaunchTui {
1570
1572
  unicode: this.unicode,
1571
1573
  color: this.theme.color,
1572
1574
  colorLevel: detectColorLevel(process.env, isTTY()),
1575
+ palette: this.themeFlow,
1573
1576
  });
1574
1577
  this.cachedArt = fit ? centerBlock(art, innerWidth) : art;
1575
1578
  const track = evolutionTrack(idx, { unicode: this.unicode, color: this.theme.color });
@@ -528,8 +528,12 @@ export function renderForgeMark(opts: {
528
528
  * flowing gradient `phase` this animates the forge identity without any
529
529
  * frame-count growth. */
530
530
  frame?: number;
531
+ /** Gradient palette for the flowing glow. Defaults to the brand `FORGE_FLOW_PALETTE`;
532
+ * pass a theme-derived palette (see `themeFlowPalette`) so the mark tracks the theme. */
533
+ palette?: readonly string[];
531
534
  }): string[] {
532
- const memoKey = `${opts.grand ? "g" : "c"}|${opts.unicode !== false ? 1 : 0}|${opts.cols ?? -1}|${opts.color !== false ? 1 : 0}|${opts.colorLevel ?? ColorLevel.TrueColor}|${opts.phase ?? 0}|${opts.frame ?? 0}`;
535
+ const palette = opts.palette ?? FORGE_FLOW_PALETTE;
536
+ const memoKey = `${opts.grand ? "g" : "c"}|${opts.unicode !== false ? 1 : 0}|${opts.cols ?? -1}|${opts.color !== false ? 1 : 0}|${opts.colorLevel ?? ColorLevel.TrueColor}|${opts.phase ?? 0}|${opts.frame ?? 0}|${palette.join(",")}`;
533
537
  const memoHit = forgeMarkMemo.get(memoKey);
534
538
  if (memoHit) return memoHit;
535
539
  const useUnicode = opts.unicode !== false;
@@ -551,7 +555,7 @@ export function renderForgeMark(opts: {
551
555
  const phase = opts.phase ?? 0;
552
556
  const useColor = opts.color !== false;
553
557
  const colorLevel = opts.colorLevel ?? ColorLevel.TrueColor;
554
- const palette = FORGE_FLOW_PALETTE;
558
+
555
559
 
556
560
  const result = source.map((line, idx) => {
557
561
  const padded = line.length < width ? line + " ".repeat(width - line.length) : line;
@@ -285,6 +285,20 @@ export function accentShadowPaint(theme: EvolutionTheme): (s: string) => string
285
285
  return (s: string) => chalk.dim(chalk.hex(hex)(s));
286
286
  }
287
287
 
288
+
289
+ /** A 3-stop neon "flow" palette derived from the active theme, for the animated
290
+ * forge-card border and the forge mark so the live flow matches the THEME instead
291
+ * of a fixed brand palette. Sweeps `accentShadow` → `accent` → the brightest stage
292
+ * gradient end (a dark → accent → light shimmer that reads as the theme's own glow).
293
+ * Colorless themes collapse to a single accent stop — callers gate the animation on
294
+ * color/TrueColor, so the value is only a harmless fallback there. */
295
+ export function themeFlowPalette(theme: EvolutionTheme): readonly string[] {
296
+ if (!theme.color) return [theme.accent];
297
+ const bright = theme.gradients[theme.gradients.length - 1]?.to ?? theme.accent;
298
+ const dark = theme.accentShadow ?? theme.accent;
299
+ return [dark, theme.accent, bright];
300
+ }
301
+
288
302
  /** Default muted foreground when a theme defines no `muted` — a readable mid-gray
289
303
  * that holds up on dark terminals (unlike ANSI `dim`, which collapses toward the
290
304
  * background). */