@jacobbd/relay-ai 0.7.6 → 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 +26 -10
- package/dist/{chunk-HXGZ4CTV.js → chunk-Q2FTCICO.js} +19 -3
- package/dist/chunk-Q2FTCICO.js.map +1 -0
- package/dist/{chunk-GHSURQOK.js → chunk-R4AWEK7T.js} +922 -216
- package/dist/chunk-R4AWEK7T.js.map +1 -0
- package/dist/cli.js +1818 -434
- package/dist/cli.js.map +1 -1
- package/dist/core/index.js +718 -83
- package/dist/core/index.js.map +1 -1
- package/dist/{provider-templates-4H3C4DRL.js → provider-templates-XKNRKAQU.js} +2 -2
- package/dist/ui/public/app.js +270 -41
- package/dist/ui/public/index.html +32 -0
- package/dist/ui/public/provider-model-browser.js +18 -2
- package/dist/{ui-command-27X4WJKC.js → ui-command-FARF2BF4.js} +96 -15
- package/dist/ui-command-FARF2BF4.js.map +1 -0
- package/package.json +4 -3
- package/dist/chunk-GHSURQOK.js.map +0 -1
- package/dist/chunk-HXGZ4CTV.js.map +0 -1
- package/dist/ui-command-27X4WJKC.js.map +0 -1
- /package/dist/{provider-templates-4H3C4DRL.js.map → provider-templates-XKNRKAQU.js.map} +0 -0
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
listAddableTemplates,
|
|
8
8
|
listSupportedTemplates,
|
|
9
9
|
listVisibleOAuthTemplates
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-Q2FTCICO.js";
|
|
11
11
|
init_provider_templates();
|
|
12
12
|
export {
|
|
13
13
|
PROVIDER_TEMPLATES,
|
|
@@ -17,4 +17,4 @@ export {
|
|
|
17
17
|
listSupportedTemplates,
|
|
18
18
|
listVisibleOAuthTemplates
|
|
19
19
|
};
|
|
20
|
-
//# sourceMappingURL=provider-templates-
|
|
20
|
+
//# sourceMappingURL=provider-templates-XKNRKAQU.js.map
|
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
|
});
|
|
@@ -1059,9 +1087,112 @@ function buildOAuthTemplateBodyContent(template) {
|
|
|
1059
1087
|
return content;
|
|
1060
1088
|
}
|
|
1061
1089
|
|
|
1090
|
+
function buildClinePassBodyContent(template, card, provider) {
|
|
1091
|
+
const content = document.createElement('div');
|
|
1092
|
+
content.className = 'provider-body-content';
|
|
1093
|
+
|
|
1094
|
+
const note = document.createElement('div');
|
|
1095
|
+
note.className = 'oauth-device-note';
|
|
1096
|
+
note.textContent = provider
|
|
1097
|
+
? 'Choose API key or ClinePass account sign-in. Switching methods replaces the stored credential safely.'
|
|
1098
|
+
: 'Choose an API key or sign in with a one-time device code.';
|
|
1099
|
+
content.appendChild(note);
|
|
1100
|
+
|
|
1101
|
+
const apiLabel = document.createElement('div');
|
|
1102
|
+
apiLabel.className = 'oauth-device-note';
|
|
1103
|
+
apiLabel.textContent = 'API key';
|
|
1104
|
+
apiLabel.style.marginTop = '10px';
|
|
1105
|
+
content.appendChild(apiLabel);
|
|
1106
|
+
|
|
1107
|
+
const keyRow = document.createElement('div');
|
|
1108
|
+
keyRow.className = 'key-row';
|
|
1109
|
+
const input = document.createElement('input');
|
|
1110
|
+
input.type = 'password';
|
|
1111
|
+
input.className = 'key-input';
|
|
1112
|
+
input.placeholder = 'Paste ClinePass API key…';
|
|
1113
|
+
input.autocomplete = 'off';
|
|
1114
|
+
const addBtn = document.createElement('button');
|
|
1115
|
+
addBtn.className = 'btn btn-primary';
|
|
1116
|
+
addBtn.textContent = provider ? 'Switch to API key' : 'Add with API key';
|
|
1117
|
+
keyRow.append(input, addBtn);
|
|
1118
|
+
content.appendChild(keyRow);
|
|
1119
|
+
|
|
1120
|
+
const apiFeedback = document.createElement('div');
|
|
1121
|
+
apiFeedback.className = 'key-feedback';
|
|
1122
|
+
content.appendChild(apiFeedback);
|
|
1123
|
+
|
|
1124
|
+
addBtn.addEventListener('click', async () => {
|
|
1125
|
+
const key = input.value.trim();
|
|
1126
|
+
if (!key) {
|
|
1127
|
+
apiFeedback.textContent = 'Enter an API key first.';
|
|
1128
|
+
apiFeedback.className = 'key-feedback error';
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
addBtn.disabled = true;
|
|
1132
|
+
apiFeedback.textContent = 'Validating key and fetching models…';
|
|
1133
|
+
apiFeedback.className = 'key-feedback muted';
|
|
1134
|
+
const result = await api('POST', '/api/providers/add', {
|
|
1135
|
+
templateId: 'cline-pass',
|
|
1136
|
+
key,
|
|
1137
|
+
replaceExisting: Boolean(provider),
|
|
1138
|
+
});
|
|
1139
|
+
addBtn.disabled = false;
|
|
1140
|
+
if (result.ok) {
|
|
1141
|
+
apiFeedback.textContent = `✓ ClinePass API key saved · ${result.count} models available`;
|
|
1142
|
+
apiFeedback.className = 'key-feedback success';
|
|
1143
|
+
state.modelsLoaded = false;
|
|
1144
|
+
await loadTemplates();
|
|
1145
|
+
await initModels();
|
|
1146
|
+
renderProviders();
|
|
1147
|
+
showToast('ClinePass API key saved');
|
|
1148
|
+
} else {
|
|
1149
|
+
apiFeedback.textContent = result.error ?? 'Failed to save ClinePass API key';
|
|
1150
|
+
if (result.hint) apiFeedback.textContent += ` — ${result.hint}`;
|
|
1151
|
+
apiFeedback.className = 'key-feedback error';
|
|
1152
|
+
}
|
|
1153
|
+
});
|
|
1154
|
+
|
|
1155
|
+
const oauthLabel = document.createElement('div');
|
|
1156
|
+
oauthLabel.className = 'oauth-device-note';
|
|
1157
|
+
oauthLabel.textContent = 'ClinePass account';
|
|
1158
|
+
oauthLabel.style.marginTop = '12px';
|
|
1159
|
+
content.appendChild(oauthLabel);
|
|
1160
|
+
|
|
1161
|
+
const oauthRow = document.createElement('div');
|
|
1162
|
+
oauthRow.className = 'key-row';
|
|
1163
|
+
const signInBtn = document.createElement('button');
|
|
1164
|
+
signInBtn.className = 'btn btn-ghost';
|
|
1165
|
+
signInBtn.textContent = provider?.authType === 'oauth' ? 'Re-authenticate with ClinePass' : 'Sign in with ClinePass';
|
|
1166
|
+
oauthRow.appendChild(signInBtn);
|
|
1167
|
+
content.appendChild(oauthRow);
|
|
1168
|
+
|
|
1169
|
+
const oauthFeedback = document.createElement('div');
|
|
1170
|
+
oauthFeedback.className = 'key-feedback';
|
|
1171
|
+
content.appendChild(oauthFeedback);
|
|
1172
|
+
signInBtn.addEventListener('click', () => beginDeviceOAuthFlow({
|
|
1173
|
+
providerId: 'cline-pass',
|
|
1174
|
+
signInButton: signInBtn,
|
|
1175
|
+
feedback: oauthFeedback,
|
|
1176
|
+
onDone: async () => {
|
|
1177
|
+
showToast('ClinePass account connected');
|
|
1178
|
+
state.modelsLoaded = false;
|
|
1179
|
+
await loadTemplates();
|
|
1180
|
+
await initModels();
|
|
1181
|
+
renderProviders();
|
|
1182
|
+
},
|
|
1183
|
+
}));
|
|
1184
|
+
|
|
1185
|
+
if (provider) content.appendChild(buildDeleteProviderRow(provider));
|
|
1186
|
+
return content;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1062
1189
|
function buildTemplateBodyContent(template, card) {
|
|
1063
1190
|
const isCustom = template.id === '__custom_openai__' || template.id === '__custom_anthropic__';
|
|
1064
1191
|
if (isCustom) return buildCustomEndpointBodyContent(template, card);
|
|
1192
|
+
if (template.id === 'cline-pass'
|
|
1193
|
+
|| (template.authMethods?.includes('api') && template.authMethods?.includes('oauth'))) {
|
|
1194
|
+
return buildClinePassBodyContent(template, card);
|
|
1195
|
+
}
|
|
1065
1196
|
if (template.authType === 'oauth') return buildOAuthTemplateBodyContent(template, card);
|
|
1066
1197
|
|
|
1067
1198
|
const content = document.createElement('div');
|
|
@@ -1162,6 +1293,9 @@ function buildTemplateBodyContent(template, card) {
|
|
|
1162
1293
|
}
|
|
1163
1294
|
|
|
1164
1295
|
function buildProviderBodyContent(provider) {
|
|
1296
|
+
if (provider.id === 'cline-pass') {
|
|
1297
|
+
return buildClinePassBodyContent({ id: 'cline-pass', name: 'ClinePass' }, null, provider);
|
|
1298
|
+
}
|
|
1165
1299
|
if (provider.authType === 'oauth') return buildOAuthProviderBodyContent(provider);
|
|
1166
1300
|
|
|
1167
1301
|
const content = document.createElement('div');
|
|
@@ -1192,33 +1326,34 @@ function buildProviderBodyContent(provider) {
|
|
|
1192
1326
|
content.appendChild(keyRow);
|
|
1193
1327
|
content.appendChild(feedback);
|
|
1194
1328
|
|
|
1195
|
-
async function doSave(key) {
|
|
1196
|
-
if (!key.trim()) return;
|
|
1197
|
-
const result = await saveKey(provider.id, key);
|
|
1198
|
-
if (result.ok) {
|
|
1199
|
-
feedback.textContent = '✓ Key saved to keychain';
|
|
1200
|
-
feedback.className = 'key-feedback success';
|
|
1201
|
-
provider.hasKey = true;
|
|
1202
|
-
const chip = document.querySelector(`[data-id="${CSS.escape(provider.id)}"] .status-chip`);
|
|
1203
|
-
if (chip) { chip.className = 'status-chip has-key'; chip.textContent = 'Key stored'; }
|
|
1204
|
-
} else {
|
|
1205
|
-
feedback.textContent = result.error ?? 'Failed to save key';
|
|
1206
|
-
feedback.className = 'key-feedback error';
|
|
1207
|
-
}
|
|
1208
|
-
setTimeout(() => { feedback.textContent = ''; feedback.className = 'key-feedback'; }, 3500);
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
input.addEventListener('blur', () => { if (input.value) doSave(input.value); });
|
|
1212
|
-
|
|
1213
1329
|
testBtn.addEventListener('click', async () => {
|
|
1214
|
-
|
|
1330
|
+
const enteredKey = input.value.trim();
|
|
1215
1331
|
feedback.textContent = 'Refreshing…';
|
|
1216
1332
|
feedback.className = 'key-feedback muted';
|
|
1217
1333
|
testBtn.disabled = true;
|
|
1218
|
-
const result = await refreshProvider(provider.id);
|
|
1334
|
+
const result = await refreshProvider(provider.id, enteredKey || undefined);
|
|
1219
1335
|
testBtn.disabled = false;
|
|
1220
1336
|
if (result.ok) {
|
|
1221
|
-
|
|
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}`;
|
|
1222
1357
|
feedback.className = 'key-feedback success';
|
|
1223
1358
|
const countEl = document.querySelector(`[data-id="${CSS.escape(provider.id)}"] .provider-models-count`);
|
|
1224
1359
|
if (countEl) countEl.textContent = `${result.count} models`;
|
|
@@ -1374,11 +1509,14 @@ function buildOAuthProviderBodyContent(provider) {
|
|
|
1374
1509
|
// ─── Model search results ─────────────────────────────────────────────────────
|
|
1375
1510
|
|
|
1376
1511
|
function buildModelResults(filter, listType) {
|
|
1377
|
-
const containerId = listType === 'agy' ? 'agy-results' : 'model-results';
|
|
1512
|
+
const containerId = listType === 'agy' ? 'agy-results' : listType === 'codex-subagents' ? 'codex-subagent-results' : 'model-results';
|
|
1378
1513
|
const container = document.getElementById(containerId);
|
|
1379
|
-
const currentFavs = listType === 'agy'
|
|
1380
|
-
|
|
1381
|
-
|
|
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;
|
|
1382
1520
|
|
|
1383
1521
|
if (!filter) { container.hidden = true; return; }
|
|
1384
1522
|
container.hidden = false;
|
|
@@ -1397,13 +1535,22 @@ function buildModelResults(filter, listType) {
|
|
|
1397
1535
|
return;
|
|
1398
1536
|
}
|
|
1399
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
|
+
|
|
1400
1544
|
const q = filter.trim().toLowerCase();
|
|
1401
|
-
const
|
|
1545
|
+
const sourceModels = listType === 'codex-subagents'
|
|
1546
|
+
? state.appModelsByTarget['codex-subagents']
|
|
1547
|
+
: state.allModels;
|
|
1548
|
+
const base = sourceModels.filter(m => !freeOnly || isFreeModel(m));
|
|
1402
1549
|
const matched = base.filter(m =>
|
|
1403
1550
|
!q ||
|
|
1404
|
-
m.id
|
|
1405
|
-
(m.name && m.name
|
|
1406
|
-
m.providerName
|
|
1551
|
+
matchesModelSearch(m.id, q) ||
|
|
1552
|
+
(m.name && matchesModelSearch(m.name, q)) ||
|
|
1553
|
+
matchesModelSearch(m.providerName, q)
|
|
1407
1554
|
).slice(0, freeOnly && !q ? 80 : 40);
|
|
1408
1555
|
|
|
1409
1556
|
if (matched.length === 0) {
|
|
@@ -1448,7 +1595,7 @@ function buildModelResults(filter, listType) {
|
|
|
1448
1595
|
addBtn.className = 'btn-add' + (isFav ? ' already-added' : '');
|
|
1449
1596
|
addBtn.textContent = isFav ? '✓' : '+';
|
|
1450
1597
|
addBtn.disabled = isFav || (!isFav && atCapacity);
|
|
1451
|
-
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');
|
|
1452
1599
|
if (!isFav && !atCapacity) {
|
|
1453
1600
|
addBtn.addEventListener('click', () => {
|
|
1454
1601
|
addToFavorites({ providerId: m.providerId, modelId: m.id }, listType);
|
|
@@ -1470,11 +1617,20 @@ function buildModelResults(filter, listType) {
|
|
|
1470
1617
|
function addToFavorites(fav, listType) {
|
|
1471
1618
|
if (listType === 'agy') {
|
|
1472
1619
|
if (state.agyFavorites.length >= AGY_MAX) return;
|
|
1620
|
+
if (state.agyFavorites.some(item => item.providerId === fav.providerId && item.modelId === fav.modelId)) return;
|
|
1473
1621
|
state.agyFavorites = [...state.agyFavorites, fav];
|
|
1474
1622
|
saveFavorites({ antigravityCliFavoriteModels: state.agyFavorites });
|
|
1475
1623
|
renderAgyList();
|
|
1476
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();
|
|
1477
1632
|
} else {
|
|
1633
|
+
if (state.generalFavorites.some(item => item.providerId === fav.providerId && item.modelId === fav.modelId)) return;
|
|
1478
1634
|
state.generalFavorites = [...state.generalFavorites, fav];
|
|
1479
1635
|
saveFavorites({ favoriteModels: state.generalFavorites });
|
|
1480
1636
|
renderFavList();
|
|
@@ -1492,6 +1648,16 @@ function removeFromFavorites(index, listType) {
|
|
|
1492
1648
|
saveFavorites({ antigravityCliFavoriteModels: state.agyFavorites });
|
|
1493
1649
|
renderAgyList(); updateAgyCounter();
|
|
1494
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
|
+
});
|
|
1495
1661
|
} else {
|
|
1496
1662
|
const prev = [...state.generalFavorites];
|
|
1497
1663
|
state.generalFavorites = state.generalFavorites.filter((_, i) => i !== index);
|
|
@@ -1506,7 +1672,7 @@ function removeFromFavorites(index, listType) {
|
|
|
1506
1672
|
}
|
|
1507
1673
|
|
|
1508
1674
|
function reorderFavorites(from, to, listType) {
|
|
1509
|
-
const arr = listType === 'agy' ? [...state.agyFavorites] : [...state.generalFavorites];
|
|
1675
|
+
const arr = listType === 'agy' ? [...state.agyFavorites] : listType === 'codex-subagents' ? [...state.codexSubagentModels] : [...state.generalFavorites];
|
|
1510
1676
|
const prev = [...arr];
|
|
1511
1677
|
const [item] = arr.splice(from, 1);
|
|
1512
1678
|
arr.splice(to, 0, item);
|
|
@@ -1514,6 +1680,10 @@ function reorderFavorites(from, to, listType) {
|
|
|
1514
1680
|
state.agyFavorites = arr;
|
|
1515
1681
|
saveFavorites({ antigravityCliFavoriteModels: state.agyFavorites });
|
|
1516
1682
|
renderAgyList();
|
|
1683
|
+
} else if (listType === 'codex-subagents') {
|
|
1684
|
+
state.codexSubagentModels = arr;
|
|
1685
|
+
saveFavorites({ codexSubagentModels: state.codexSubagentModels });
|
|
1686
|
+
renderCodexSubagentList();
|
|
1517
1687
|
} else {
|
|
1518
1688
|
state.generalFavorites = arr;
|
|
1519
1689
|
saveFavorites({ favoriteModels: state.generalFavorites });
|
|
@@ -1524,6 +1694,10 @@ function reorderFavorites(from, to, listType) {
|
|
|
1524
1694
|
state.agyFavorites = prev;
|
|
1525
1695
|
saveFavorites({ antigravityCliFavoriteModels: state.agyFavorites });
|
|
1526
1696
|
renderAgyList();
|
|
1697
|
+
} else if (listType === 'codex-subagents') {
|
|
1698
|
+
state.codexSubagentModels = prev;
|
|
1699
|
+
saveFavorites({ codexSubagentModels: state.codexSubagentModels });
|
|
1700
|
+
renderCodexSubagentList();
|
|
1527
1701
|
} else {
|
|
1528
1702
|
state.generalFavorites = prev;
|
|
1529
1703
|
saveFavorites({ favoriteModels: state.generalFavorites });
|
|
@@ -1613,7 +1787,7 @@ function buildFavItem(fav, index, listType) {
|
|
|
1613
1787
|
|
|
1614
1788
|
// Keyboard reorder
|
|
1615
1789
|
item.addEventListener('keydown', e => {
|
|
1616
|
-
const arr = listType === 'agy' ? state.agyFavorites : state.generalFavorites;
|
|
1790
|
+
const arr = listType === 'agy' ? state.agyFavorites : listType === 'codex-subagents' ? state.codexSubagentModels : state.generalFavorites;
|
|
1617
1791
|
if (e.altKey && e.key === 'ArrowUp' && index > 0) { e.preventDefault(); reorderFavorites(index, index - 1, listType); }
|
|
1618
1792
|
else if (e.altKey && e.key === 'ArrowDown' && index < arr.length - 1) { e.preventDefault(); reorderFavorites(index, index + 1, listType); }
|
|
1619
1793
|
});
|
|
@@ -1659,6 +1833,33 @@ function renderAgyList() {
|
|
|
1659
1833
|
state.agyFavorites.forEach((f, i) => list.appendChild(buildFavItem(f, i, 'agy')));
|
|
1660
1834
|
}
|
|
1661
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
|
+
|
|
1662
1863
|
function updateAgyCounter() {
|
|
1663
1864
|
const count = state.agyFavorites.length;
|
|
1664
1865
|
|
|
@@ -1695,7 +1896,7 @@ function initNav() {
|
|
|
1695
1896
|
// Host-only sections stay in the DOM but are CSS-hidden in server admin mode.
|
|
1696
1897
|
const sectionIds = isServerAdminUi()
|
|
1697
1898
|
? ['providers', 'favorites', 'server']
|
|
1698
|
-
: ['providers', 'favorites', 'antigravity', 'apps', 'server'];
|
|
1899
|
+
: ['providers', 'favorites', 'section-codex-subagents', 'antigravity', 'apps', 'server'];
|
|
1699
1900
|
const navItems = document.querySelectorAll('.nav-item');
|
|
1700
1901
|
const content = document.getElementById('content');
|
|
1701
1902
|
|
|
@@ -1803,6 +2004,7 @@ async function init() {
|
|
|
1803
2004
|
|
|
1804
2005
|
await loadConfig();
|
|
1805
2006
|
renderFavList();
|
|
2007
|
+
renderCodexSubagentList();
|
|
1806
2008
|
updateGeneralCounter();
|
|
1807
2009
|
if (!isServerAdminUi()) {
|
|
1808
2010
|
renderAgyList();
|
|
@@ -1823,9 +2025,11 @@ async function init() {
|
|
|
1823
2025
|
if (!isServerAdminUi()) renderApps();
|
|
1824
2026
|
// Re-render favorites now that we have full provider names
|
|
1825
2027
|
renderFavList();
|
|
2028
|
+
renderCodexSubagentList();
|
|
1826
2029
|
if (!isServerAdminUi()) renderAgyList();
|
|
1827
2030
|
if (state.modelFilter) buildModelResults(state.modelFilter, 'general');
|
|
1828
2031
|
if (!isServerAdminUi() && state.agyFilter) buildModelResults(state.agyFilter, 'agy');
|
|
2032
|
+
if (!isServerAdminUi() && state.codexSubagentFilter) buildModelResults(state.codexSubagentFilter, 'codex-subagents');
|
|
1829
2033
|
syncProviderModelBrowserFromHash();
|
|
1830
2034
|
});
|
|
1831
2035
|
|
|
@@ -1929,6 +2133,15 @@ async function init() {
|
|
|
1929
2133
|
buildModelResults(state.agyFilter, 'agy');
|
|
1930
2134
|
});
|
|
1931
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
|
+
|
|
1932
2145
|
document.getElementById('app-model-search')?.addEventListener('input', e => {
|
|
1933
2146
|
state.appModelFilter = e.target.value;
|
|
1934
2147
|
renderApps();
|
|
@@ -1989,10 +2202,10 @@ function matchedAppModels(appId) {
|
|
|
1989
2202
|
const providerName = getProviderName(m.providerId);
|
|
1990
2203
|
if (state.appFreeOnly && !isFreeModel(m)) return false;
|
|
1991
2204
|
if (!q) return true;
|
|
1992
|
-
return m.id
|
|
1993
|
-
|| (m.name && m.name
|
|
1994
|
-
|| providerName
|
|
1995
|
-
|| (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));
|
|
1996
2209
|
});
|
|
1997
2210
|
return matched.slice(0, 80);
|
|
1998
2211
|
}
|
|
@@ -2132,6 +2345,15 @@ function renderApps() {
|
|
|
2132
2345
|
</span>
|
|
2133
2346
|
</label>
|
|
2134
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
|
+
` : ''}
|
|
2135
2357
|
${app.type !== 'app' ? `
|
|
2136
2358
|
<div class="launch-folder-control">
|
|
2137
2359
|
<label style="font-size: 12px; font-weight: 500; color: var(--color-muted);">Launch folder 📁</label>
|
|
@@ -2175,6 +2397,10 @@ function renderApps() {
|
|
|
2175
2397
|
renderAppPathSettings();
|
|
2176
2398
|
}
|
|
2177
2399
|
|
|
2400
|
+
function setCodexNativeMode(appId, enabled) {
|
|
2401
|
+
state.appWithNative[appId] = Boolean(enabled);
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2178
2404
|
function renderAppPathSettings() {
|
|
2179
2405
|
const container = document.getElementById('app-paths-list');
|
|
2180
2406
|
if (!container) return;
|
|
@@ -2217,6 +2443,9 @@ async function launchApp(appId) {
|
|
|
2217
2443
|
if (appId === 'claude' && state.appHttpProxy[appId] && claudeHttpProxyAvailable(appId)) {
|
|
2218
2444
|
body.httpProxy = true;
|
|
2219
2445
|
}
|
|
2446
|
+
if ((appId === 'codex' || appId === 'codex-app') && state.appWithNative[appId]) {
|
|
2447
|
+
body.withNative = true;
|
|
2448
|
+
}
|
|
2220
2449
|
|
|
2221
2450
|
const folder = (state.appLaunchFolders[appId] ?? '').trim();
|
|
2222
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) {
|