jeo-code 0.9.7 → 0.9.9
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 +26 -0
- package/README.ja.md +2 -2
- package/README.ko.md +2 -2
- package/README.md +2 -2
- package/README.zh.md +2 -2
- package/package.json +1 -1
- package/src/ai/index.ts +1 -0
- package/src/ai/model-cache.ts +163 -0
- package/src/commands/auth.ts +5 -0
- package/src/commands/launch/tmux.ts +11 -0
- package/src/commands/launch.ts +29 -9
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,32 @@ The README mirrors the latest 5 entries — regenerate with `bun run changelog:s
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.9.9] - 2026-07-27
|
|
12
|
+
_Root-causes the frozen `jeo --tmux` TUI: the window was pinned to its launch size, so resizing the terminal only cut the view._
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- **`jeo --tmux` no longer freezes the window at its launch geometry** (`src/commands/launch.ts`, `src/commands/launch/tmux.ts`) — the pre-attach `tmux resize-window` flipped the window's `window-size` option to `manual` as a documented side effect, permanently detaching it from the attached client. Resizing the terminal then left the tmux window frozen: the pane was shown cut/letterboxed and jeo never received SIGWINCH, so no amount of in-app reflow could help. The redundant `resize-window` is gone (`new-session -x/-y` already sizes the detached window), and every jeo-owned session now explicitly asserts `window-size latest`, which also protects users whose `tmux.conf` sets `manual` globally.
|
|
16
|
+
|
|
17
|
+
### Verified
|
|
18
|
+
- Live tmux 3.6a: attached client 100x30 → window 100x29, 140x40 → 140x39, 72x20 → 72x19, with the TUI reflowing at each step.
|
|
19
|
+
- Control run reproducing the old behaviour: with `window-size manual` the window stayed 80x24 under a 120x36 client (the reported truncation); restoring `latest` recovered instantly.
|
|
20
|
+
- tmux suite: 18 pass / 0 fail, including new assertions that the launch flow never issues `resize-window` and always sets `window-size latest`.
|
|
21
|
+
|
|
22
|
+
## [0.9.8] - 2026-07-27
|
|
23
|
+
_The provider model list now persists and rehydrates, so an account's live models survive across launches (gjc parity)._
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
- **Persistent live model catalog** (`src/ai/model-cache.ts`) — every successful discovery is written to `~/.jeo/model-catalog-cache.json` and rehydrated at the next launch, before any network call. `jeo auth login openai` persists the account's Codex list immediately.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- **Newly released OpenAI models were rejected at cold start** (`src/ai/model-cache.ts`, `src/commands/launch.ts`) — the OAuth Codex gate only knew the maintained static snapshot until this launch's own discovery landed, so an account that already serves `gpt-5.6-luna`/`-sol`/`-terra` could not select them for the first seconds of a session. Rehydration seeds the gate, routing, and the pickers from the account's own last-reported list.
|
|
30
|
+
- **Cold-start model completion fell back to the static catalog** (`src/commands/launch.ts`) — tab completion and `modelsForProvider` now use the persisted per-provider lists until live discovery replaces them.
|
|
31
|
+
|
|
32
|
+
### Verified
|
|
33
|
+
- Model cache suite: 11 pass / 0 fail (round-trip, gate rehydration, api_key vs oauth scoping, offline/merge safety, corrupt-file degradation, test-write hermeticity).
|
|
34
|
+
- Model/provider suite: 169 pass / 0 fail across discovery, catalog, routing, pickers, `/model`, OpenAI OAuth, and autocomplete.
|
|
35
|
+
- Live account check: discovery persisted 4 providers; a fresh process turned `gpt-5.6-luna` from rejected into servable purely from disk.
|
|
36
|
+
|
|
11
37
|
## [0.9.7] - 2026-07-27
|
|
12
38
|
_Fixes the broken 0.9.3-0.9.6 npm releases: `jeo` and `jeo --tmux` crashed at startup right after `jeo update`._
|
|
13
39
|
|
package/README.ja.md
CHANGED
|
@@ -285,10 +285,10 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
|
|
|
285
285
|
|
|
286
286
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
287
287
|
- **[Unreleased]**
|
|
288
|
+
- **[0.9.9]** (2026-07-27) — Root-causes the frozen `jeo --tmux` TUI: the window was pinned to its launch size, so resizing the terminal only cut the view.
|
|
289
|
+
- **[0.9.8]** (2026-07-27) — The provider model list now persists and rehydrates, so an account's live models survive across launches (gjc parity).
|
|
288
290
|
- **[0.9.7]** (2026-07-27) — Fixes the broken 0.9.3-0.9.6 npm releases: `jeo` and `jeo --tmux` crashed at startup right after `jeo update`.
|
|
289
291
|
- **[0.9.6]** (2026-07-27) — Resize-safe ledger flush and terminal clear handling after viewport shrink.
|
|
290
|
-
- **[0.9.5]** (2026-07-27) — Resize-safe live TUI rendering for terminal viewport shrink/grow transitions.
|
|
291
|
-
- **[0.9.4]** (2026-07-27) — Real OpenAI/Codex model discovery after login, npm package-content hardening, and pinned self-update support.
|
|
292
292
|
|
|
293
293
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
294
294
|
<!-- CHANGELOG:END -->
|
package/README.ko.md
CHANGED
|
@@ -286,10 +286,10 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
|
|
|
286
286
|
|
|
287
287
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
288
288
|
- **[Unreleased]**
|
|
289
|
+
- **[0.9.9]** (2026-07-27) — Root-causes the frozen `jeo --tmux` TUI: the window was pinned to its launch size, so resizing the terminal only cut the view.
|
|
290
|
+
- **[0.9.8]** (2026-07-27) — The provider model list now persists and rehydrates, so an account's live models survive across launches (gjc parity).
|
|
289
291
|
- **[0.9.7]** (2026-07-27) — Fixes the broken 0.9.3-0.9.6 npm releases: `jeo` and `jeo --tmux` crashed at startup right after `jeo update`.
|
|
290
292
|
- **[0.9.6]** (2026-07-27) — Resize-safe ledger flush and terminal clear handling after viewport shrink.
|
|
291
|
-
- **[0.9.5]** (2026-07-27) — Resize-safe live TUI rendering for terminal viewport shrink/grow transitions.
|
|
292
|
-
- **[0.9.4]** (2026-07-27) — Real OpenAI/Codex model discovery after login, npm package-content hardening, and pinned self-update support.
|
|
293
293
|
|
|
294
294
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
295
295
|
<!-- CHANGELOG:END -->
|
package/README.md
CHANGED
|
@@ -293,10 +293,10 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
|
|
|
293
293
|
|
|
294
294
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
295
295
|
- **[Unreleased]**
|
|
296
|
+
- **[0.9.9]** (2026-07-27) — Root-causes the frozen `jeo --tmux` TUI: the window was pinned to its launch size, so resizing the terminal only cut the view.
|
|
297
|
+
- **[0.9.8]** (2026-07-27) — The provider model list now persists and rehydrates, so an account's live models survive across launches (gjc parity).
|
|
296
298
|
- **[0.9.7]** (2026-07-27) — Fixes the broken 0.9.3-0.9.6 npm releases: `jeo` and `jeo --tmux` crashed at startup right after `jeo update`.
|
|
297
299
|
- **[0.9.6]** (2026-07-27) — Resize-safe ledger flush and terminal clear handling after viewport shrink.
|
|
298
|
-
- **[0.9.5]** (2026-07-27) — Resize-safe live TUI rendering for terminal viewport shrink/grow transitions.
|
|
299
|
-
- **[0.9.4]** (2026-07-27) — Real OpenAI/Codex model discovery after login, npm package-content hardening, and pinned self-update support.
|
|
300
300
|
|
|
301
301
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
302
302
|
<!-- CHANGELOG:END -->
|
package/README.zh.md
CHANGED
|
@@ -285,10 +285,10 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
|
|
|
285
285
|
|
|
286
286
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
287
287
|
- **[Unreleased]**
|
|
288
|
+
- **[0.9.9]** (2026-07-27) — Root-causes the frozen `jeo --tmux` TUI: the window was pinned to its launch size, so resizing the terminal only cut the view.
|
|
289
|
+
- **[0.9.8]** (2026-07-27) — The provider model list now persists and rehydrates, so an account's live models survive across launches (gjc parity).
|
|
288
290
|
- **[0.9.7]** (2026-07-27) — Fixes the broken 0.9.3-0.9.6 npm releases: `jeo` and `jeo --tmux` crashed at startup right after `jeo update`.
|
|
289
291
|
- **[0.9.6]** (2026-07-27) — Resize-safe ledger flush and terminal clear handling after viewport shrink.
|
|
290
|
-
- **[0.9.5]** (2026-07-27) — Resize-safe live TUI rendering for terminal viewport shrink/grow transitions.
|
|
291
|
-
- **[0.9.4]** (2026-07-27) — Real OpenAI/Codex model discovery after login, npm package-content hardening, and pinned self-update support.
|
|
292
292
|
|
|
293
293
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
294
294
|
<!-- CHANGELOG:END -->
|
package/package.json
CHANGED
package/src/ai/index.ts
CHANGED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent cache of the model lists providers reported from their OWN endpoints.
|
|
3
|
+
*
|
|
4
|
+
* Live discovery (`listProviderModels`) already asks each authenticated provider
|
|
5
|
+
* what it serves, but the answer only ever lived in this process: every launch
|
|
6
|
+
* started blind, showed the maintained static snapshot until the network call
|
|
7
|
+
* returned, and — worse — the OAuth Codex gate (`isCodexModel`) rejected any model
|
|
8
|
+
* newer than that snapshot, so an account that had `gpt-5.6-*` could not select it
|
|
9
|
+
* until discovery happened to finish first.
|
|
10
|
+
*
|
|
11
|
+
* Persisting the discovered lists (gjc parity) closes both gaps: the next launch
|
|
12
|
+
* rehydrates the account's REAL model set from disk before any network call, and a
|
|
13
|
+
* background refresh keeps it current. The cache is an optimization — a missing,
|
|
14
|
+
* corrupt, or stale file only means "fall back to live discovery + static catalog",
|
|
15
|
+
* never an error.
|
|
16
|
+
*/
|
|
17
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
18
|
+
import * as os from "node:os";
|
|
19
|
+
import * as path from "node:path";
|
|
20
|
+
import { jeoEnv } from "../util/env";
|
|
21
|
+
import { recordLiveCodexModels, recordLiveProviderModels } from "./model-catalog";
|
|
22
|
+
import type { ProviderName } from "./types";
|
|
23
|
+
|
|
24
|
+
/** How a provider's list was obtained; mirrors `ProviderModelsResult["source"]`. */
|
|
25
|
+
export type ModelCacheSource = "oauth" | "api_key" | "keyless" | "none";
|
|
26
|
+
|
|
27
|
+
export interface CachedProviderModels {
|
|
28
|
+
provider: ProviderName;
|
|
29
|
+
models: string[];
|
|
30
|
+
source: ModelCacheSource;
|
|
31
|
+
/** Base URL the list came from, so an OpenAI-compatible endpoint's ids stay scoped to it. */
|
|
32
|
+
baseUrl?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ModelCacheFile {
|
|
36
|
+
version: 1;
|
|
37
|
+
updatedAt: number;
|
|
38
|
+
providers: CachedProviderModels[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const CACHE_VERSION = 1;
|
|
42
|
+
/** Refresh in the background once the cache is older than this (6h). */
|
|
43
|
+
export const MODEL_CACHE_TTL_MS = 6 * 60 * 60 * 1000;
|
|
44
|
+
/** Bound a single provider's persisted list so a pathological aggregator cannot bloat the file. */
|
|
45
|
+
const MAX_MODELS_PER_PROVIDER = 500;
|
|
46
|
+
|
|
47
|
+
function cacheDir(): string {
|
|
48
|
+
return jeoEnv("CONFIG_DIR") || path.join(os.homedir(), ".jeo");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function cachePath(): string {
|
|
52
|
+
return path.join(cacheDir(), "model-catalog-cache.json");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Keep only well-formed entries; a hand-edited or partially-written file must not throw. */
|
|
56
|
+
export function normalizeCacheEntries(raw: unknown): CachedProviderModels[] {
|
|
57
|
+
if (!Array.isArray(raw)) return [];
|
|
58
|
+
const out: CachedProviderModels[] = [];
|
|
59
|
+
for (const entry of raw) {
|
|
60
|
+
const row = entry as Partial<CachedProviderModels>;
|
|
61
|
+
if (!row || typeof row.provider !== "string" || !Array.isArray(row.models)) continue;
|
|
62
|
+
const models = row.models.filter((m): m is string => typeof m === "string" && m.trim().length > 0).slice(0, MAX_MODELS_PER_PROVIDER);
|
|
63
|
+
if (models.length === 0) continue;
|
|
64
|
+
out.push({
|
|
65
|
+
provider: row.provider as ProviderName,
|
|
66
|
+
models,
|
|
67
|
+
source: row.source === "oauth" || row.source === "api_key" || row.source === "keyless" ? row.source : "none",
|
|
68
|
+
...(typeof row.baseUrl === "string" && row.baseUrl ? { baseUrl: row.baseUrl } : {}),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Read the persisted lists, or null when absent/unreadable/incompatible. */
|
|
75
|
+
export async function readModelCache(): Promise<ModelCacheFile | null> {
|
|
76
|
+
try {
|
|
77
|
+
const parsed = JSON.parse(await readFile(cachePath(), "utf-8")) as Partial<ModelCacheFile>;
|
|
78
|
+
if (!parsed || parsed.version !== CACHE_VERSION) return null;
|
|
79
|
+
const providers = normalizeCacheEntries(parsed.providers);
|
|
80
|
+
if (providers.length === 0) return null;
|
|
81
|
+
return {
|
|
82
|
+
version: CACHE_VERSION,
|
|
83
|
+
updatedAt: typeof parsed.updatedAt === "number" ? parsed.updatedAt : 0,
|
|
84
|
+
providers,
|
|
85
|
+
};
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** True when the cache is missing or older than the TTL, i.e. a refresh is worth doing. */
|
|
92
|
+
export function isModelCacheStale(cache: ModelCacheFile | null, now = Date.now(), ttlMs = MODEL_CACHE_TTL_MS): boolean {
|
|
93
|
+
if (!cache) return true;
|
|
94
|
+
return now - cache.updatedAt >= ttlMs;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Merge fresh discovery results over the existing cache. Providers absent from
|
|
99
|
+
* `results` (not logged in this run, or a transient failure) keep their previous
|
|
100
|
+
* entry, so one offline launch never erases a known-good list.
|
|
101
|
+
*/
|
|
102
|
+
export function mergeCacheEntries(
|
|
103
|
+
previous: readonly CachedProviderModels[],
|
|
104
|
+
results: readonly { provider: ProviderName; models: readonly string[]; ok: boolean; source?: ModelCacheSource; baseUrl?: string }[],
|
|
105
|
+
): CachedProviderModels[] {
|
|
106
|
+
const byKey = new Map<string, CachedProviderModels>();
|
|
107
|
+
const keyOf = (provider: string, baseUrl?: string) => `${provider}\u0000${baseUrl ?? ""}`;
|
|
108
|
+
for (const entry of previous) byKey.set(keyOf(entry.provider, entry.baseUrl), entry);
|
|
109
|
+
for (const result of results) {
|
|
110
|
+
if (!result.ok || result.models.length === 0) continue;
|
|
111
|
+
byKey.set(keyOf(result.provider, result.baseUrl), {
|
|
112
|
+
provider: result.provider,
|
|
113
|
+
models: [...result.models].slice(0, MAX_MODELS_PER_PROVIDER),
|
|
114
|
+
source: result.source ?? "none",
|
|
115
|
+
...(result.baseUrl ? { baseUrl: result.baseUrl } : {}),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return [...byKey.values()];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Persist discovery results (best-effort; never throws, never blocks a turn). */
|
|
122
|
+
export async function writeModelCache(
|
|
123
|
+
results: readonly { provider: ProviderName; models: readonly string[]; ok: boolean; source?: ModelCacheSource; baseUrl?: string }[],
|
|
124
|
+
): Promise<void> {
|
|
125
|
+
// Same hermeticity rule as saveGlobalConfig: a test run may only write into an
|
|
126
|
+
// explicitly sandboxed JEO_CONFIG_DIR, never the developer's real ~/.jeo.
|
|
127
|
+
if (process.env.NODE_ENV === "test" && !jeoEnv("CONFIG_DIR")) return;
|
|
128
|
+
try {
|
|
129
|
+
const existing = await readModelCache();
|
|
130
|
+
const providers = mergeCacheEntries(existing?.providers ?? [], results);
|
|
131
|
+
if (providers.length === 0) return;
|
|
132
|
+
const payload: ModelCacheFile = { version: CACHE_VERSION, updatedAt: Date.now(), providers };
|
|
133
|
+
await mkdir(cacheDir(), { recursive: true, mode: 0o700 });
|
|
134
|
+
await writeFile(cachePath(), JSON.stringify(payload, null, 2), { encoding: "utf-8", mode: 0o600 });
|
|
135
|
+
} catch {
|
|
136
|
+
// A cache write failure must never surface to the user.
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Feed cached lists into the in-process registries so pickers, autocomplete,
|
|
142
|
+
* routing, and — critically — the OAuth Codex gate know the account's real models
|
|
143
|
+
* BEFORE the first network call of this launch. Returns the number of ids applied.
|
|
144
|
+
*/
|
|
145
|
+
export function applyCachedModels(cache: ModelCacheFile | null): number {
|
|
146
|
+
if (!cache) return 0;
|
|
147
|
+
let applied = 0;
|
|
148
|
+
for (const entry of cache.providers) {
|
|
149
|
+
recordLiveProviderModels(entry.provider, entry.models, { source: entry.source, baseUrl: entry.baseUrl });
|
|
150
|
+
// An OAuth-sourced OpenAI list IS the Codex allow-list; without this the gate
|
|
151
|
+
// still rejects any model newer than the maintained static snapshot.
|
|
152
|
+
if (entry.provider === "openai" && entry.source === "oauth") recordLiveCodexModels(entry.models);
|
|
153
|
+
applied += entry.models.length;
|
|
154
|
+
}
|
|
155
|
+
return applied;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Read + apply in one step. Returns the cache so callers can decide about refreshing. */
|
|
159
|
+
export async function rehydrateLiveModels(): Promise<ModelCacheFile | null> {
|
|
160
|
+
const cache = await readModelCache();
|
|
161
|
+
applyCachedModels(cache);
|
|
162
|
+
return cache;
|
|
163
|
+
}
|
package/src/commands/auth.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { createInterface } from "node:readline/promises";
|
|
|
6
6
|
import { readGlobalConfig } from "../agent/state";
|
|
7
7
|
import { jeoEnv } from "../util/env";
|
|
8
8
|
import { listProviderModels, CODEX_MODELS_URL } from "../ai/model-discovery";
|
|
9
|
+
import { writeModelCache } from "../ai/model-cache";
|
|
9
10
|
import {
|
|
10
11
|
OAUTH_FLOWS,
|
|
11
12
|
OAUTH_FLOW_REGISTRY,
|
|
@@ -152,6 +153,10 @@ async function reportOpenAiCodexModels(): Promise<void> {
|
|
|
152
153
|
try {
|
|
153
154
|
const result = await listProviderModels("openai", { preferOAuth: true });
|
|
154
155
|
if (result.ok) {
|
|
156
|
+
// Persist immediately: the very next `jeo` launch then knows this account's
|
|
157
|
+
// real model set (including ids newer than the maintained static snapshot)
|
|
158
|
+
// before it makes any network call.
|
|
159
|
+
await writeModelCache([result]);
|
|
155
160
|
console.log(`Live Codex models (${result.models.length}): ${result.models.join(", ")}`);
|
|
156
161
|
} else {
|
|
157
162
|
console.log(`[WARN] Could not verify live Codex models from ${CODEX_MODELS_URL}: ${result.error ?? "unknown error"} (non-fatal — login succeeded).`);
|
|
@@ -193,6 +193,17 @@ export function tmuxProfileCommands(
|
|
|
193
193
|
): TmuxProfileCommand[] {
|
|
194
194
|
const t = `=${target}:`;
|
|
195
195
|
const commands: TmuxProfileCommand[] = [];
|
|
196
|
+
// Follow the attached client's geometry, always. tmux's default is already
|
|
197
|
+
// `latest`, but ANY `resize-window` (jeo's own, a user keybinding, a wrapper
|
|
198
|
+
// script) silently flips the window to `manual`, and a user's tmux.conf may set
|
|
199
|
+
// `manual` globally. A manual window never resizes with the terminal: jeo's pane
|
|
200
|
+
// stays frozen at its launch size, gets shown letterboxed/cut, and jeo never sees
|
|
201
|
+
// SIGWINCH so it cannot reflow either. Asserting `latest` per jeo session makes
|
|
202
|
+
// the live TUI track every terminal resize.
|
|
203
|
+
commands.push({
|
|
204
|
+
description: "track the attached client's size (never freeze the window)",
|
|
205
|
+
args: ["set-window-option", "-t", t, "window-size", "latest"],
|
|
206
|
+
});
|
|
196
207
|
if (env.JEO_TMUX_MOUSE !== "0") {
|
|
197
208
|
commands.push({
|
|
198
209
|
description: "enable tmux mouse scrolling (wheel-up → copy-mode over real history)",
|
package/src/commands/launch.ts
CHANGED
|
@@ -55,6 +55,7 @@ import { friendlyProviderError } from "../util/provider-error";
|
|
|
55
55
|
import { readGlobalConfig, saveConfigPatch, resolveWikiRoot } from "../agent/state";
|
|
56
56
|
import { rememberModelPatch, recentModelsForDisplay } from "../agent/model-recency";
|
|
57
57
|
import { describeModel, describeAllProviders, describeProvider, resolveProvider, thinkingMaxTokens, resolveMaxOutputTokens, thinkingToReasoningEffort, discoverModels, flattenModels, resolveSelection, catalogMetadata, catalogByProvider, resolveRoleModel, CODEX_MODELS, qualifyModelId, modelServableWithConfig, isLocalProviderReachable } from "../ai";
|
|
58
|
+
import { rehydrateLiveModels, writeModelCache } from "../ai/model-cache";
|
|
58
59
|
import type { ProviderModelsResult, PickEntry, ProviderName, ModelRole, ThinkLevel } from "../ai";
|
|
59
60
|
import { readGoalState, writeGoalState, clearGoalState, verifyGoal, applyEvidenceGate } from "../agent/goal-verifier";
|
|
60
61
|
import { routePrompt, deriveCacheSessionKey, warnOnce, tierModelPool, selectFromPool, PROMPT_TIERS, withRoutingTierSetting, inferTierForModel, credentialScopeFor, resolveVerifierModel, type PromptTier, type RouteDecision, type RoutingConfig } from "../agent/prompt-router";
|
|
@@ -490,13 +491,15 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
490
491
|
: `Starting new independent tmux session: ${sessionName} (another live jeo session already owns ${sessionBase}; reattach later with: tmux attach -t ${sessionName})`,
|
|
491
492
|
);
|
|
492
493
|
|
|
493
|
-
//
|
|
494
|
-
//
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
494
|
+
// NOTE: never `tmux resize-window` here. tmux flips the window's `window-size`
|
|
495
|
+
// option to `manual` as a side effect of that command, which permanently
|
|
496
|
+
// detaches the window from the attached client's geometry — the terminal is
|
|
497
|
+
// resized, tmux keeps the window frozen at its old size, and the pane is shown
|
|
498
|
+
// letterboxed/cut with jeo never receiving SIGWINCH (the "TUI is stuck at the
|
|
499
|
+
// launch size and the screen is cut" report). `new-session -x/-y` already
|
|
500
|
+
// creates the detached window at the caller's size, and `window-size latest`
|
|
501
|
+
// (asserted in tmuxProfileCommands) makes the attach and every later terminal
|
|
502
|
+
// resize track the client automatically.
|
|
500
503
|
|
|
501
504
|
const attach = Bun.spawn([tmuxBin, "attach-session", "-t", `=${sessionName}`], {
|
|
502
505
|
stdin: "inherit",
|
|
@@ -692,10 +695,13 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
692
695
|
let sessionComputerOverride: boolean | undefined;
|
|
693
696
|
|
|
694
697
|
// Cache of live, credential-validated models per provider (refreshed by live pickers).
|
|
698
|
+
// Every successful discovery is also persisted (`writeModelCache`), so the NEXT launch
|
|
699
|
+
// starts from the account's real model set instead of the maintained static snapshot.
|
|
695
700
|
let liveModelsCache: ProviderModelsResult[] | null = null;
|
|
696
701
|
const getLiveModels = async (force = false): Promise<ProviderModelsResult[]> => {
|
|
697
702
|
if (force || !liveModelsCache) {
|
|
698
703
|
liveModelsCache = await discoverModels({ timeoutMs: 4000 });
|
|
704
|
+
void writeModelCache(liveModelsCache);
|
|
699
705
|
}
|
|
700
706
|
return liveModelsCache;
|
|
701
707
|
};
|
|
@@ -2352,7 +2358,14 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2352
2358
|
// Tab autocomplete: alias names snapshotted once; live models come from the
|
|
2353
2359
|
// background-warmed cache (logged-in/OAuth accounts). The completer is sync, so
|
|
2354
2360
|
// it never blocks on the network — it reads whatever the cache currently holds.
|
|
2361
|
+
//
|
|
2362
|
+
// Disk rehydration runs FIRST and synchronously-awaited: it costs one small file
|
|
2363
|
+
// read but makes the account's real model set (including ids newer than the
|
|
2364
|
+
// maintained static snapshot) available to the picker, autocomplete, routing, and
|
|
2365
|
+
// the OAuth Codex gate from the first keystroke, instead of only after the network
|
|
2366
|
+
// discovery below happens to land.
|
|
2355
2367
|
const aliasNames = Object.keys(await listAliases());
|
|
2368
|
+
const cachedModels = await rehydrateLiveModels().catch(() => null);
|
|
2356
2369
|
void getLiveModels()
|
|
2357
2370
|
.then(r => {
|
|
2358
2371
|
liveModelsCache ??= r;
|
|
@@ -2361,13 +2374,20 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2361
2374
|
const mentionPaths = (prefix: string): string[] => mentionPathsIn(cwd, prefix);
|
|
2362
2375
|
const completionContext = (): CompletionContext => {
|
|
2363
2376
|
const base = staticCompletionContext();
|
|
2377
|
+
// Until this launch's own discovery lands, completion falls back to the models
|
|
2378
|
+
// the account reported on a PREVIOUS run. Without the fallback a cold start
|
|
2379
|
+
// offered only the static catalog, so freshly released ids the user already has
|
|
2380
|
+
// access to were un-completable for the first seconds of every session.
|
|
2381
|
+
const liveFromDiscovery = liveModelsCache ? flattenModels(liveModelsCache).map(e => e.model) : null;
|
|
2382
|
+
const cachedFor = (p: string): string[] =>
|
|
2383
|
+
cachedModels?.providers.filter(entry => entry.provider === p).flatMap(entry => entry.models) ?? [];
|
|
2364
2384
|
return {
|
|
2365
2385
|
...base,
|
|
2366
2386
|
slashCommands: [...base.slashCommands, ...skillSlashDetails.map(d => d.command)],
|
|
2367
|
-
liveModels:
|
|
2387
|
+
liveModels: liveFromDiscovery ?? (cachedModels?.providers.flatMap(entry => entry.models) ?? []),
|
|
2368
2388
|
aliases: aliasNames,
|
|
2369
2389
|
skillNames: resolvedSkillTokens,
|
|
2370
|
-
modelsForProvider: p => liveModelsCache?.find(r => r.provider === p)?.models ??
|
|
2390
|
+
modelsForProvider: p => liveModelsCache?.find(r => r.provider === p)?.models ?? cachedFor(p),
|
|
2371
2391
|
mentionPaths,
|
|
2372
2392
|
};
|
|
2373
2393
|
};
|