@jacobbd/relay-ai 0.8.0 → 0.9.0
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/README.md +18 -6
- package/dist/{chunk-5DDQJSTU.js → chunk-R4AWEK7T.js} +392 -117
- package/dist/chunk-R4AWEK7T.js.map +1 -0
- package/dist/cli.js +1618 -341
- package/dist/cli.js.map +1 -1
- package/dist/core/index.js +93 -79
- package/dist/core/index.js.map +1 -1
- package/dist/ui/public/app.js +164 -41
- package/dist/ui/public/index.html +32 -0
- package/dist/ui/public/provider-model-browser.js +18 -2
- package/dist/{ui-command-RYWL36VR.js → ui-command-FARF2BF4.js} +54 -10
- package/dist/ui-command-FARF2BF4.js.map +1 -0
- package/package.json +4 -3
- package/dist/chunk-5DDQJSTU.js.map +0 -1
- package/dist/ui-command-RYWL36VR.js.map +0 -1
package/dist/ui/public/app.js
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
formatModelPrice,
|
|
3
3
|
getProviderModelPage,
|
|
4
4
|
isFreeModel,
|
|
5
|
+
matchesModelSearch,
|
|
5
6
|
PROVIDER_MODEL_PAGE_SIZE,
|
|
6
7
|
} from './provider-model-browser.js';
|
|
7
8
|
import { copyDeviceCode, copyTextToClipboard, oauthConnectionLabel } from './oauth-device.js';
|
|
@@ -11,6 +12,7 @@ import { providerInitial, providerLogoHtml } from './provider-logo.js';
|
|
|
11
12
|
|
|
12
13
|
const AGY_MAX = 6;
|
|
13
14
|
const GENERAL_MAX = 20;
|
|
15
|
+
const CODEX_SUBAGENT_MAX = 1;
|
|
14
16
|
const UPDATE_COMMAND = 'npm install -g @jacobbd/relay-ai@latest';
|
|
15
17
|
|
|
16
18
|
const state = {
|
|
@@ -19,6 +21,7 @@ const state = {
|
|
|
19
21
|
allModels: [],
|
|
20
22
|
appModelsByTarget: {}, // launch target → flattened, compatibility-filtered model list
|
|
21
23
|
generalFavorites: [],
|
|
24
|
+
codexSubagentModels: [],
|
|
22
25
|
agyFavorites: [],
|
|
23
26
|
modelsLoaded: false,
|
|
24
27
|
modelsError: null,
|
|
@@ -32,6 +35,8 @@ const state = {
|
|
|
32
35
|
modelFreeOnly: false,
|
|
33
36
|
agyFilter: '',
|
|
34
37
|
agyFreeOnly: false,
|
|
38
|
+
codexSubagentFilter: '',
|
|
39
|
+
codexSubagentFreeOnly: false,
|
|
35
40
|
appModelFilter: '',
|
|
36
41
|
appFreeOnly: false,
|
|
37
42
|
providerNameMap: {}, // providerId → full display name
|
|
@@ -41,6 +46,7 @@ const state = {
|
|
|
41
46
|
appModelOpen: null,
|
|
42
47
|
appSelections: {},
|
|
43
48
|
appHttpProxy: {},
|
|
49
|
+
appWithNative: {},
|
|
44
50
|
server: {
|
|
45
51
|
status: null,
|
|
46
52
|
error: null,
|
|
@@ -185,6 +191,7 @@ async function api(method, path, body) {
|
|
|
185
191
|
async function loadConfig() {
|
|
186
192
|
const data = await api('GET', '/api/config');
|
|
187
193
|
state.generalFavorites = data.favoriteModels ?? [];
|
|
194
|
+
state.codexSubagentModels = (data.codexSubagentModels ?? []).slice(0, CODEX_SUBAGENT_MAX);
|
|
188
195
|
state.agyFavorites = data.antigravityCliFavoriteModels ?? [];
|
|
189
196
|
}
|
|
190
197
|
|
|
@@ -264,7 +271,7 @@ function flattenModelProviders(providers) {
|
|
|
264
271
|
// Mirrors src/ui/api.ts's APP_ID_TO_LAUNCH_TARGET — which models are compatible with
|
|
265
272
|
// which app depends on the launch target, not just the raw catalog.
|
|
266
273
|
const APP_ID_NEEDS_TARGET_FILTER = new Set([
|
|
267
|
-
'claude', 'claude-app', 'codex', 'codex-app', 'gemini', 'agy', 'antigravity', 'antigravity-ide',
|
|
274
|
+
'claude', 'claude-app', 'codex', 'codex-app', 'codex-subagents', 'gemini', 'agy', 'antigravity', 'antigravity-ide',
|
|
268
275
|
]);
|
|
269
276
|
|
|
270
277
|
async function loadAppModels(appId) {
|
|
@@ -306,8 +313,12 @@ async function loadModels() {
|
|
|
306
313
|
}
|
|
307
314
|
|
|
308
315
|
async function saveFavorites(payload) { return api('POST', '/api/config', payload); }
|
|
309
|
-
async function saveKey(providerId, key
|
|
310
|
-
|
|
316
|
+
async function saveKey(providerId, key, confirmOverwrite = false) {
|
|
317
|
+
return api('POST', '/api/keys', { providerId, key, confirmOverwrite });
|
|
318
|
+
}
|
|
319
|
+
async function refreshProvider(providerId, key) {
|
|
320
|
+
return api('POST', '/api/providers/refresh', key ? { providerId, key } : { providerId });
|
|
321
|
+
}
|
|
311
322
|
|
|
312
323
|
// ─── Toast ────────────────────────────────────────────────────────────────────
|
|
313
324
|
|
|
@@ -521,8 +532,10 @@ function toggleModelFavoriteMenu(button, providerId, modelId) {
|
|
|
521
532
|
|
|
522
533
|
const isGenFav = isGeneralFavorite(providerId, modelId);
|
|
523
534
|
const isAgyFav = state.agyFavorites.some(f => f.providerId === providerId && f.modelId === modelId);
|
|
535
|
+
const isSubagent = state.codexSubagentModels.some(f => f.providerId === providerId && f.modelId === modelId);
|
|
524
536
|
const genAtCapacity = state.generalFavorites.length >= GENERAL_MAX;
|
|
525
537
|
const agyAtCapacity = state.agyFavorites.length >= AGY_MAX;
|
|
538
|
+
const subagentAtCapacity = state.codexSubagentModels.length >= CODEX_SUBAGENT_MAX;
|
|
526
539
|
|
|
527
540
|
const popover = document.createElement('div');
|
|
528
541
|
popover.className = 'model-fav-popover';
|
|
@@ -538,6 +551,11 @@ function toggleModelFavoriteMenu(button, providerId, modelId) {
|
|
|
538
551
|
if (isAgyFav) agyLabel = '✓ In Antigravity Favorites';
|
|
539
552
|
else if (agyAtCapacity) agyLabel = `✦ Antigravity full (${AGY_MAX}/${AGY_MAX})`;
|
|
540
553
|
|
|
554
|
+
const subagentDisabled = isSubagent || subagentAtCapacity;
|
|
555
|
+
let subagentLabel = '✦ Add to Codex SubAgent';
|
|
556
|
+
if (isSubagent) subagentLabel = '✓ In Codex SubAgent';
|
|
557
|
+
else if (subagentAtCapacity) subagentLabel = `✦ Codex SubAgent full (${CODEX_SUBAGENT_MAX}/${CODEX_SUBAGENT_MAX})`;
|
|
558
|
+
|
|
541
559
|
popover.innerHTML = `
|
|
542
560
|
<button class="model-fav-popover-item ${genDisabled ? 'disabled' : ''}" type="button" data-type="general" ${genDisabled ? 'disabled' : ''}>
|
|
543
561
|
<span>${genLabel}</span>
|
|
@@ -547,6 +565,10 @@ function toggleModelFavoriteMenu(button, providerId, modelId) {
|
|
|
547
565
|
<span>${agyLabel}</span>
|
|
548
566
|
<span class="popover-slot-count">${state.agyFavorites.length}/${AGY_MAX}</span>
|
|
549
567
|
</button>
|
|
568
|
+
<button class="model-fav-popover-item ${subagentDisabled ? 'disabled' : ''}" type="button" data-type="codex-subagents" ${subagentDisabled ? 'disabled' : ''}>
|
|
569
|
+
<span>${subagentLabel}</span>
|
|
570
|
+
<span class="popover-slot-count">${state.codexSubagentModels.length}/${CODEX_SUBAGENT_MAX}</span>
|
|
571
|
+
</button>
|
|
550
572
|
`;
|
|
551
573
|
|
|
552
574
|
popover.querySelectorAll('.model-fav-popover-item').forEach(item => {
|
|
@@ -555,7 +577,13 @@ function toggleModelFavoriteMenu(button, providerId, modelId) {
|
|
|
555
577
|
const listType = item.dataset.type;
|
|
556
578
|
addToFavorites({ providerId, modelId }, listType);
|
|
557
579
|
closeModelFavPopover();
|
|
558
|
-
showToast(
|
|
580
|
+
showToast(
|
|
581
|
+
listType === 'agy'
|
|
582
|
+
? 'Added to Antigravity Favorites'
|
|
583
|
+
: listType === 'codex-subagents'
|
|
584
|
+
? 'Added to Codex SubAgent'
|
|
585
|
+
: 'Added to Global Favorites',
|
|
586
|
+
);
|
|
559
587
|
renderProviderModelBrowser();
|
|
560
588
|
});
|
|
561
589
|
});
|
|
@@ -1298,33 +1326,34 @@ function buildProviderBodyContent(provider) {
|
|
|
1298
1326
|
content.appendChild(keyRow);
|
|
1299
1327
|
content.appendChild(feedback);
|
|
1300
1328
|
|
|
1301
|
-
async function doSave(key) {
|
|
1302
|
-
if (!key.trim()) return;
|
|
1303
|
-
const result = await saveKey(provider.id, key);
|
|
1304
|
-
if (result.ok) {
|
|
1305
|
-
feedback.textContent = '✓ Key saved to keychain';
|
|
1306
|
-
feedback.className = 'key-feedback success';
|
|
1307
|
-
provider.hasKey = true;
|
|
1308
|
-
const chip = document.querySelector(`[data-id="${CSS.escape(provider.id)}"] .status-chip`);
|
|
1309
|
-
if (chip) { chip.className = 'status-chip has-key'; chip.textContent = 'Key stored'; }
|
|
1310
|
-
} else {
|
|
1311
|
-
feedback.textContent = result.error ?? 'Failed to save key';
|
|
1312
|
-
feedback.className = 'key-feedback error';
|
|
1313
|
-
}
|
|
1314
|
-
setTimeout(() => { feedback.textContent = ''; feedback.className = 'key-feedback'; }, 3500);
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
input.addEventListener('blur', () => { if (input.value) doSave(input.value); });
|
|
1318
|
-
|
|
1319
1329
|
testBtn.addEventListener('click', async () => {
|
|
1320
|
-
|
|
1330
|
+
const enteredKey = input.value.trim();
|
|
1321
1331
|
feedback.textContent = 'Refreshing…';
|
|
1322
1332
|
feedback.className = 'key-feedback muted';
|
|
1323
1333
|
testBtn.disabled = true;
|
|
1324
|
-
const result = await refreshProvider(provider.id);
|
|
1334
|
+
const result = await refreshProvider(provider.id, enteredKey || undefined);
|
|
1325
1335
|
testBtn.disabled = false;
|
|
1326
1336
|
if (result.ok) {
|
|
1327
|
-
|
|
1337
|
+
let savedMessage = '';
|
|
1338
|
+
if (enteredKey && window.confirm('Save this API key for future Relay launches?')) {
|
|
1339
|
+
let saved = await saveKey(provider.id, enteredKey);
|
|
1340
|
+
if (saved.needsConfirmation && window.confirm('Replace the existing stored API key?')) {
|
|
1341
|
+
saved = await saveKey(provider.id, enteredKey, true);
|
|
1342
|
+
}
|
|
1343
|
+
if (saved.ok) {
|
|
1344
|
+
savedMessage = ' · key saved';
|
|
1345
|
+
provider.hasKey = true;
|
|
1346
|
+
const chip = document.querySelector(`[data-id="${CSS.escape(provider.id)}"] .status-chip`);
|
|
1347
|
+
if (chip) { chip.className = 'status-chip has-key'; chip.textContent = 'Key stored'; }
|
|
1348
|
+
} else if (saved.needsConfirmation) {
|
|
1349
|
+
savedMessage = ' · key used for this refresh only';
|
|
1350
|
+
} else {
|
|
1351
|
+
feedback.textContent = saved.error ?? 'Failed to save key';
|
|
1352
|
+
feedback.className = 'key-feedback error';
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
feedback.textContent = `✓ ${result.count} models available${savedMessage}`;
|
|
1328
1357
|
feedback.className = 'key-feedback success';
|
|
1329
1358
|
const countEl = document.querySelector(`[data-id="${CSS.escape(provider.id)}"] .provider-models-count`);
|
|
1330
1359
|
if (countEl) countEl.textContent = `${result.count} models`;
|
|
@@ -1480,11 +1509,14 @@ function buildOAuthProviderBodyContent(provider) {
|
|
|
1480
1509
|
// ─── Model search results ─────────────────────────────────────────────────────
|
|
1481
1510
|
|
|
1482
1511
|
function buildModelResults(filter, listType) {
|
|
1483
|
-
const containerId = listType === 'agy' ? 'agy-results' : 'model-results';
|
|
1512
|
+
const containerId = listType === 'agy' ? 'agy-results' : listType === 'codex-subagents' ? 'codex-subagent-results' : 'model-results';
|
|
1484
1513
|
const container = document.getElementById(containerId);
|
|
1485
|
-
const currentFavs = listType === 'agy'
|
|
1486
|
-
|
|
1487
|
-
|
|
1514
|
+
const currentFavs = listType === 'agy'
|
|
1515
|
+
? state.agyFavorites
|
|
1516
|
+
: listType === 'codex-subagents' ? state.codexSubagentModels : state.generalFavorites;
|
|
1517
|
+
const max = listType === 'agy' ? AGY_MAX : listType === 'codex-subagents' ? CODEX_SUBAGENT_MAX : GENERAL_MAX;
|
|
1518
|
+
const atCapacity = currentFavs.length >= max;
|
|
1519
|
+
const freeOnly = listType === 'agy' ? state.agyFreeOnly : listType === 'codex-subagents' ? state.codexSubagentFreeOnly : state.modelFreeOnly;
|
|
1488
1520
|
|
|
1489
1521
|
if (!filter) { container.hidden = true; return; }
|
|
1490
1522
|
container.hidden = false;
|
|
@@ -1503,13 +1535,22 @@ function buildModelResults(filter, listType) {
|
|
|
1503
1535
|
return;
|
|
1504
1536
|
}
|
|
1505
1537
|
|
|
1538
|
+
if (listType === 'codex-subagents' && !Object.prototype.hasOwnProperty.call(state.appModelsByTarget, 'codex-subagents')) {
|
|
1539
|
+
container.innerHTML = Array(3).fill('<div class="skeleton" style="height:36px;margin:4px 12px;border-radius:6px"></div>').join('');
|
|
1540
|
+
loadAppModels('codex-subagents').then(() => buildModelResults(filter, listType));
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1506
1544
|
const q = filter.trim().toLowerCase();
|
|
1507
|
-
const
|
|
1545
|
+
const sourceModels = listType === 'codex-subagents'
|
|
1546
|
+
? state.appModelsByTarget['codex-subagents']
|
|
1547
|
+
: state.allModels;
|
|
1548
|
+
const base = sourceModels.filter(m => !freeOnly || isFreeModel(m));
|
|
1508
1549
|
const matched = base.filter(m =>
|
|
1509
1550
|
!q ||
|
|
1510
|
-
m.id
|
|
1511
|
-
(m.name && m.name
|
|
1512
|
-
m.providerName
|
|
1551
|
+
matchesModelSearch(m.id, q) ||
|
|
1552
|
+
(m.name && matchesModelSearch(m.name, q)) ||
|
|
1553
|
+
matchesModelSearch(m.providerName, q)
|
|
1513
1554
|
).slice(0, freeOnly && !q ? 80 : 40);
|
|
1514
1555
|
|
|
1515
1556
|
if (matched.length === 0) {
|
|
@@ -1554,7 +1595,7 @@ function buildModelResults(filter, listType) {
|
|
|
1554
1595
|
addBtn.className = 'btn-add' + (isFav ? ' already-added' : '');
|
|
1555
1596
|
addBtn.textContent = isFav ? '✓' : '+';
|
|
1556
1597
|
addBtn.disabled = isFav || (!isFav && atCapacity);
|
|
1557
|
-
addBtn.title = atCapacity && !isFav ?
|
|
1598
|
+
addBtn.title = atCapacity && !isFav ? `${listType === 'codex-subagents' ? 'Codex SubAgent' : listType === 'agy' ? 'Antigravity' : 'Favorites'} is full (${max}/${max})` : (isFav ? 'Already added' : 'Add to favorites');
|
|
1558
1599
|
if (!isFav && !atCapacity) {
|
|
1559
1600
|
addBtn.addEventListener('click', () => {
|
|
1560
1601
|
addToFavorites({ providerId: m.providerId, modelId: m.id }, listType);
|
|
@@ -1576,11 +1617,20 @@ function buildModelResults(filter, listType) {
|
|
|
1576
1617
|
function addToFavorites(fav, listType) {
|
|
1577
1618
|
if (listType === 'agy') {
|
|
1578
1619
|
if (state.agyFavorites.length >= AGY_MAX) return;
|
|
1620
|
+
if (state.agyFavorites.some(item => item.providerId === fav.providerId && item.modelId === fav.modelId)) return;
|
|
1579
1621
|
state.agyFavorites = [...state.agyFavorites, fav];
|
|
1580
1622
|
saveFavorites({ antigravityCliFavoriteModels: state.agyFavorites });
|
|
1581
1623
|
renderAgyList();
|
|
1582
1624
|
updateAgyCounter();
|
|
1625
|
+
} else if (listType === 'codex-subagents') {
|
|
1626
|
+
if (state.codexSubagentModels.length >= CODEX_SUBAGENT_MAX) return;
|
|
1627
|
+
if (state.codexSubagentModels.some(item => item.providerId === fav.providerId && item.modelId === fav.modelId)) return;
|
|
1628
|
+
state.codexSubagentModels = [...state.codexSubagentModels, fav];
|
|
1629
|
+
saveFavorites({ codexSubagentModels: state.codexSubagentModels });
|
|
1630
|
+
renderCodexSubagentList();
|
|
1631
|
+
updateCodexSubagentCounter();
|
|
1583
1632
|
} else {
|
|
1633
|
+
if (state.generalFavorites.some(item => item.providerId === fav.providerId && item.modelId === fav.modelId)) return;
|
|
1584
1634
|
state.generalFavorites = [...state.generalFavorites, fav];
|
|
1585
1635
|
saveFavorites({ favoriteModels: state.generalFavorites });
|
|
1586
1636
|
renderFavList();
|
|
@@ -1598,6 +1648,16 @@ function removeFromFavorites(index, listType) {
|
|
|
1598
1648
|
saveFavorites({ antigravityCliFavoriteModels: state.agyFavorites });
|
|
1599
1649
|
renderAgyList(); updateAgyCounter();
|
|
1600
1650
|
});
|
|
1651
|
+
} else if (listType === 'codex-subagents') {
|
|
1652
|
+
const prev = [...state.codexSubagentModels];
|
|
1653
|
+
state.codexSubagentModels = state.codexSubagentModels.filter((_, i) => i !== index);
|
|
1654
|
+
saveFavorites({ codexSubagentModels: state.codexSubagentModels });
|
|
1655
|
+
renderCodexSubagentList(); updateCodexSubagentCounter();
|
|
1656
|
+
showToast('Removed from Codex SubAgent', () => {
|
|
1657
|
+
state.codexSubagentModels = prev;
|
|
1658
|
+
saveFavorites({ codexSubagentModels: state.codexSubagentModels });
|
|
1659
|
+
renderCodexSubagentList(); updateCodexSubagentCounter();
|
|
1660
|
+
});
|
|
1601
1661
|
} else {
|
|
1602
1662
|
const prev = [...state.generalFavorites];
|
|
1603
1663
|
state.generalFavorites = state.generalFavorites.filter((_, i) => i !== index);
|
|
@@ -1612,7 +1672,7 @@ function removeFromFavorites(index, listType) {
|
|
|
1612
1672
|
}
|
|
1613
1673
|
|
|
1614
1674
|
function reorderFavorites(from, to, listType) {
|
|
1615
|
-
const arr = listType === 'agy' ? [...state.agyFavorites] : [...state.generalFavorites];
|
|
1675
|
+
const arr = listType === 'agy' ? [...state.agyFavorites] : listType === 'codex-subagents' ? [...state.codexSubagentModels] : [...state.generalFavorites];
|
|
1616
1676
|
const prev = [...arr];
|
|
1617
1677
|
const [item] = arr.splice(from, 1);
|
|
1618
1678
|
arr.splice(to, 0, item);
|
|
@@ -1620,6 +1680,10 @@ function reorderFavorites(from, to, listType) {
|
|
|
1620
1680
|
state.agyFavorites = arr;
|
|
1621
1681
|
saveFavorites({ antigravityCliFavoriteModels: state.agyFavorites });
|
|
1622
1682
|
renderAgyList();
|
|
1683
|
+
} else if (listType === 'codex-subagents') {
|
|
1684
|
+
state.codexSubagentModels = arr;
|
|
1685
|
+
saveFavorites({ codexSubagentModels: state.codexSubagentModels });
|
|
1686
|
+
renderCodexSubagentList();
|
|
1623
1687
|
} else {
|
|
1624
1688
|
state.generalFavorites = arr;
|
|
1625
1689
|
saveFavorites({ favoriteModels: state.generalFavorites });
|
|
@@ -1630,6 +1694,10 @@ function reorderFavorites(from, to, listType) {
|
|
|
1630
1694
|
state.agyFavorites = prev;
|
|
1631
1695
|
saveFavorites({ antigravityCliFavoriteModels: state.agyFavorites });
|
|
1632
1696
|
renderAgyList();
|
|
1697
|
+
} else if (listType === 'codex-subagents') {
|
|
1698
|
+
state.codexSubagentModels = prev;
|
|
1699
|
+
saveFavorites({ codexSubagentModels: state.codexSubagentModels });
|
|
1700
|
+
renderCodexSubagentList();
|
|
1633
1701
|
} else {
|
|
1634
1702
|
state.generalFavorites = prev;
|
|
1635
1703
|
saveFavorites({ favoriteModels: state.generalFavorites });
|
|
@@ -1719,7 +1787,7 @@ function buildFavItem(fav, index, listType) {
|
|
|
1719
1787
|
|
|
1720
1788
|
// Keyboard reorder
|
|
1721
1789
|
item.addEventListener('keydown', e => {
|
|
1722
|
-
const arr = listType === 'agy' ? state.agyFavorites : state.generalFavorites;
|
|
1790
|
+
const arr = listType === 'agy' ? state.agyFavorites : listType === 'codex-subagents' ? state.codexSubagentModels : state.generalFavorites;
|
|
1723
1791
|
if (e.altKey && e.key === 'ArrowUp' && index > 0) { e.preventDefault(); reorderFavorites(index, index - 1, listType); }
|
|
1724
1792
|
else if (e.altKey && e.key === 'ArrowDown' && index < arr.length - 1) { e.preventDefault(); reorderFavorites(index, index + 1, listType); }
|
|
1725
1793
|
});
|
|
@@ -1765,6 +1833,33 @@ function renderAgyList() {
|
|
|
1765
1833
|
state.agyFavorites.forEach((f, i) => list.appendChild(buildFavItem(f, i, 'agy')));
|
|
1766
1834
|
}
|
|
1767
1835
|
|
|
1836
|
+
function renderCodexSubagentList() {
|
|
1837
|
+
const list = document.getElementById('codex-subagent-list');
|
|
1838
|
+
if (!list) return;
|
|
1839
|
+
list.innerHTML = '';
|
|
1840
|
+
if (state.codexSubagentModels.length === 0) {
|
|
1841
|
+
list.innerHTML = '<div class="fav-empty">No Codex SubAgent configured yet. Search above to add one.</div>';
|
|
1842
|
+
} else {
|
|
1843
|
+
state.codexSubagentModels.forEach((f, i) => list.appendChild(buildFavItem(f, i, 'codex-subagents')));
|
|
1844
|
+
}
|
|
1845
|
+
updateCodexSubagentCounter();
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
function updateCodexSubagentCounter() {
|
|
1849
|
+
const count = state.codexSubagentModels.length;
|
|
1850
|
+
const counter = document.getElementById('codex-subagent-slot-count');
|
|
1851
|
+
if (counter) counter.innerHTML = `${count}<span class="agy-slot-max">/${CODEX_SUBAGENT_MAX}</span>`;
|
|
1852
|
+
const pips = document.getElementById('codex-subagent-pips');
|
|
1853
|
+
if (pips) {
|
|
1854
|
+
pips.innerHTML = '';
|
|
1855
|
+
for (let i = 0; i < CODEX_SUBAGENT_MAX; i++) {
|
|
1856
|
+
const pip = document.createElement('div');
|
|
1857
|
+
pip.className = 'agy-pip' + (i < count ? ' filled' : '');
|
|
1858
|
+
pips.appendChild(pip);
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1768
1863
|
function updateAgyCounter() {
|
|
1769
1864
|
const count = state.agyFavorites.length;
|
|
1770
1865
|
|
|
@@ -1801,7 +1896,7 @@ function initNav() {
|
|
|
1801
1896
|
// Host-only sections stay in the DOM but are CSS-hidden in server admin mode.
|
|
1802
1897
|
const sectionIds = isServerAdminUi()
|
|
1803
1898
|
? ['providers', 'favorites', 'server']
|
|
1804
|
-
: ['providers', 'favorites', 'antigravity', 'apps', 'server'];
|
|
1899
|
+
: ['providers', 'favorites', 'section-codex-subagents', 'antigravity', 'apps', 'server'];
|
|
1805
1900
|
const navItems = document.querySelectorAll('.nav-item');
|
|
1806
1901
|
const content = document.getElementById('content');
|
|
1807
1902
|
|
|
@@ -1909,6 +2004,7 @@ async function init() {
|
|
|
1909
2004
|
|
|
1910
2005
|
await loadConfig();
|
|
1911
2006
|
renderFavList();
|
|
2007
|
+
renderCodexSubagentList();
|
|
1912
2008
|
updateGeneralCounter();
|
|
1913
2009
|
if (!isServerAdminUi()) {
|
|
1914
2010
|
renderAgyList();
|
|
@@ -1929,9 +2025,11 @@ async function init() {
|
|
|
1929
2025
|
if (!isServerAdminUi()) renderApps();
|
|
1930
2026
|
// Re-render favorites now that we have full provider names
|
|
1931
2027
|
renderFavList();
|
|
2028
|
+
renderCodexSubagentList();
|
|
1932
2029
|
if (!isServerAdminUi()) renderAgyList();
|
|
1933
2030
|
if (state.modelFilter) buildModelResults(state.modelFilter, 'general');
|
|
1934
2031
|
if (!isServerAdminUi() && state.agyFilter) buildModelResults(state.agyFilter, 'agy');
|
|
2032
|
+
if (!isServerAdminUi() && state.codexSubagentFilter) buildModelResults(state.codexSubagentFilter, 'codex-subagents');
|
|
1935
2033
|
syncProviderModelBrowserFromHash();
|
|
1936
2034
|
});
|
|
1937
2035
|
|
|
@@ -2035,6 +2133,15 @@ async function init() {
|
|
|
2035
2133
|
buildModelResults(state.agyFilter, 'agy');
|
|
2036
2134
|
});
|
|
2037
2135
|
|
|
2136
|
+
document.getElementById('codex-subagent-search')?.addEventListener('input', e => {
|
|
2137
|
+
state.codexSubagentFilter = e.target.value;
|
|
2138
|
+
buildModelResults(state.codexSubagentFilter, 'codex-subagents');
|
|
2139
|
+
});
|
|
2140
|
+
document.getElementById('codex-subagent-free-only')?.addEventListener('change', e => {
|
|
2141
|
+
state.codexSubagentFreeOnly = e.target.checked;
|
|
2142
|
+
buildModelResults(state.codexSubagentFilter, 'codex-subagents');
|
|
2143
|
+
});
|
|
2144
|
+
|
|
2038
2145
|
document.getElementById('app-model-search')?.addEventListener('input', e => {
|
|
2039
2146
|
state.appModelFilter = e.target.value;
|
|
2040
2147
|
renderApps();
|
|
@@ -2095,10 +2202,10 @@ function matchedAppModels(appId) {
|
|
|
2095
2202
|
const providerName = getProviderName(m.providerId);
|
|
2096
2203
|
if (state.appFreeOnly && !isFreeModel(m)) return false;
|
|
2097
2204
|
if (!q) return true;
|
|
2098
|
-
return m.id
|
|
2099
|
-
|| (m.name && m.name
|
|
2100
|
-
|| providerName
|
|
2101
|
-
|| (m.providerName && m.providerName
|
|
2205
|
+
return matchesModelSearch(m.id, q)
|
|
2206
|
+
|| (m.name && matchesModelSearch(m.name, q))
|
|
2207
|
+
|| matchesModelSearch(providerName, q)
|
|
2208
|
+
|| (m.providerName && matchesModelSearch(m.providerName, q));
|
|
2102
2209
|
});
|
|
2103
2210
|
return matched.slice(0, 80);
|
|
2104
2211
|
}
|
|
@@ -2238,6 +2345,15 @@ function renderApps() {
|
|
|
2238
2345
|
</span>
|
|
2239
2346
|
</label>
|
|
2240
2347
|
` : ''}
|
|
2348
|
+
${app.id === 'codex' || app.id === 'codex-app' ? `
|
|
2349
|
+
<label class="claude-proxy-option">
|
|
2350
|
+
<input type="checkbox" ${state.appWithNative[app.id] ? 'checked' : ''} onchange="setCodexNativeMode('${app.id}', this.checked)">
|
|
2351
|
+
<span class="claude-proxy-label">
|
|
2352
|
+
Load native Codex models alongside Relay models
|
|
2353
|
+
<span class="claude-proxy-tooltip" tabindex="0" role="img" aria-label="Keeps native Codex models available while exposing your Relay models and Codex SubAgent for this launch only." data-tooltip="Keeps native Codex models available while exposing your Relay models and Codex SubAgent for this launch only.">?</span>
|
|
2354
|
+
</span>
|
|
2355
|
+
</label>
|
|
2356
|
+
` : ''}
|
|
2241
2357
|
${app.type !== 'app' ? `
|
|
2242
2358
|
<div class="launch-folder-control">
|
|
2243
2359
|
<label style="font-size: 12px; font-weight: 500; color: var(--color-muted);">Launch folder 📁</label>
|
|
@@ -2281,6 +2397,10 @@ function renderApps() {
|
|
|
2281
2397
|
renderAppPathSettings();
|
|
2282
2398
|
}
|
|
2283
2399
|
|
|
2400
|
+
function setCodexNativeMode(appId, enabled) {
|
|
2401
|
+
state.appWithNative[appId] = Boolean(enabled);
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2284
2404
|
function renderAppPathSettings() {
|
|
2285
2405
|
const container = document.getElementById('app-paths-list');
|
|
2286
2406
|
if (!container) return;
|
|
@@ -2323,6 +2443,9 @@ async function launchApp(appId) {
|
|
|
2323
2443
|
if (appId === 'claude' && state.appHttpProxy[appId] && claudeHttpProxyAvailable(appId)) {
|
|
2324
2444
|
body.httpProxy = true;
|
|
2325
2445
|
}
|
|
2446
|
+
if ((appId === 'codex' || appId === 'codex-app') && state.appWithNative[appId]) {
|
|
2447
|
+
body.withNative = true;
|
|
2448
|
+
}
|
|
2326
2449
|
|
|
2327
2450
|
const folder = (state.appLaunchFolders[appId] ?? '').trim();
|
|
2328
2451
|
if (folder) {
|
|
@@ -82,6 +82,12 @@
|
|
|
82
82
|
</span>
|
|
83
83
|
Favorites
|
|
84
84
|
</a>
|
|
85
|
+
<a href="#section-codex-subagents" class="nav-item" data-section="section-codex-subagents" id="nav-codex-subagents">
|
|
86
|
+
<span class="nav-icon">
|
|
87
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M8 12h8M12 8v8"/></svg>
|
|
88
|
+
</span>
|
|
89
|
+
Codex SubAgent
|
|
90
|
+
</a>
|
|
85
91
|
<a href="#antigravity" class="nav-item" data-section="antigravity">
|
|
86
92
|
<span class="nav-icon">
|
|
87
93
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 22 8.5 22 15.5 12 22 2 15.5 2 8.5"/></svg>
|
|
@@ -172,6 +178,32 @@
|
|
|
172
178
|
<div id="favorites-list" class="fav-list" role="list" aria-label="General favorites" data-list="general"></div>
|
|
173
179
|
</section>
|
|
174
180
|
|
|
181
|
+
<!-- Codex SubAgent section -->
|
|
182
|
+
<section id="section-codex-subagents" class="section">
|
|
183
|
+
<div class="section-hero">
|
|
184
|
+
<div class="section-eyebrow">Codex Runtime</div>
|
|
185
|
+
<h1 class="section-heading">Codex <span class="heading-accent">SubAgent</span></h1>
|
|
186
|
+
<p class="section-sub">Choose one Relay model for the Codex SubAgent. This selection starts empty and does not sync with General Favorites.</p>
|
|
187
|
+
</div>
|
|
188
|
+
<div class="agy-slot-bar">
|
|
189
|
+
<div class="agy-slot-label">Slots used</div>
|
|
190
|
+
<div class="agy-slot-pips" id="codex-subagent-pips"></div>
|
|
191
|
+
<div class="agy-slot-count" id="codex-subagent-slot-count">0<span class="agy-slot-max">/1</span></div>
|
|
192
|
+
</div>
|
|
193
|
+
<div class="search-row">
|
|
194
|
+
<div class="search-field">
|
|
195
|
+
<svg class="search-icon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
|
|
196
|
+
<input type="search" id="codex-subagent-search" class="search-input" placeholder="Search models to add…" aria-label="Search Codex SubAgent models">
|
|
197
|
+
</div>
|
|
198
|
+
<label class="free-only-toggle">
|
|
199
|
+
<input type="checkbox" id="codex-subagent-free-only">
|
|
200
|
+
<span>Free models only</span>
|
|
201
|
+
</label>
|
|
202
|
+
</div>
|
|
203
|
+
<div id="codex-subagent-results" class="model-results" hidden></div>
|
|
204
|
+
<div id="codex-subagent-list" class="fav-list" role="list" aria-label="Codex SubAgent" data-list="codex-subagents"></div>
|
|
205
|
+
</section>
|
|
206
|
+
|
|
175
207
|
<!-- Antigravity section -->
|
|
176
208
|
<section id="antigravity" class="section">
|
|
177
209
|
<div class="section-hero">
|
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
export const PROVIDER_MODEL_PAGE_SIZE = 25;
|
|
2
2
|
|
|
3
|
+
function normalizeSearchText(value) {
|
|
4
|
+
return String(value ?? '')
|
|
5
|
+
.toLowerCase()
|
|
6
|
+
.replace(/([a-z])([0-9])/g, '$1 $2')
|
|
7
|
+
.replace(/([0-9])([a-z])/g, '$1 $2')
|
|
8
|
+
.replace(/[\s\-._/:]+/g, ' ')
|
|
9
|
+
.trim();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function matchesModelSearch(value, query) {
|
|
13
|
+
const tokens = normalizeSearchText(query).split(' ').filter(Boolean);
|
|
14
|
+
if (tokens.length === 0) return true;
|
|
15
|
+
const normalized = normalizeSearchText(value);
|
|
16
|
+
return tokens.every(token => normalized.includes(token));
|
|
17
|
+
}
|
|
18
|
+
|
|
3
19
|
export function isFreeModel(model) {
|
|
4
20
|
return Boolean(
|
|
5
21
|
model?.isFree
|
|
@@ -10,12 +26,12 @@ export function isFreeModel(model) {
|
|
|
10
26
|
}
|
|
11
27
|
|
|
12
28
|
export function filterProviderModels(models, query, opts = {}) {
|
|
13
|
-
const needle = query.trim()
|
|
29
|
+
const needle = query.trim();
|
|
14
30
|
const minCtx = opts.minContextWindow ?? 0;
|
|
15
31
|
const freeOnly = Boolean(opts.freeOnly);
|
|
16
32
|
|
|
17
33
|
return models.filter(model => {
|
|
18
|
-
if (needle && !model.id
|
|
34
|
+
if (needle && !matchesModelSearch(model.id, needle) && !matchesModelSearch(model.name ?? '', needle)) {
|
|
19
35
|
return false;
|
|
20
36
|
}
|
|
21
37
|
if (minCtx > 0 && (model.contextWindow ?? 0) < minCtx) {
|