@jacobbd/relay-ai 0.7.2 → 0.7.4

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.
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  formatModelPrice,
3
3
  getProviderModelPage,
4
+ isFreeModel,
4
5
  PROVIDER_MODEL_PAGE_SIZE,
5
6
  } from './provider-model-browser.js';
6
7
  import { copyDeviceCode, copyTextToClipboard, oauthConnectionLabel } from './oauth-device.js';
@@ -24,6 +25,8 @@ const state = {
24
25
  providerFilter: '',
25
26
  activeProviderId: null,
26
27
  providerModelFilter: '',
28
+ providerModelMinCtx: 0,
29
+ providerModelFreeOnly: false,
27
30
  providerModelPage: 1,
28
31
  modelFilter: '',
29
32
  modelFreeOnly: false,
@@ -351,10 +354,6 @@ function fmtCtx(n) {
351
354
  return String(n);
352
355
  }
353
356
 
354
- function isFreeModel(model) {
355
- return Boolean(model?.isFree || model?.freeStatus === 'verified_free' || model?.freeStatus === 'free_provider');
356
- }
357
-
358
357
  function freeBadgeLabel(model) {
359
358
  if (model?.freeStatus === 'free_provider') {
360
359
  const isCloudflare = model?.providerId === 'cloudflare' || model?.id?.startsWith('@cf/') || model?.id?.startsWith('@hf/');
@@ -503,7 +502,87 @@ function syncProviderModelBrowserFromHash() {
503
502
  renderProviderModelBrowser();
504
503
  }
505
504
 
505
+ let activeModelFavPopover = null;
506
+
507
+ function closeModelFavPopover() {
508
+ if (activeModelFavPopover) {
509
+ activeModelFavPopover.remove();
510
+ activeModelFavPopover = null;
511
+ }
512
+ }
513
+
514
+ function toggleModelFavoriteMenu(button, providerId, modelId) {
515
+ const targetKey = `${providerId}::${modelId}`;
516
+ if (activeModelFavPopover && activeModelFavPopover.dataset.targetId === targetKey) {
517
+ closeModelFavPopover();
518
+ return;
519
+ }
520
+ closeModelFavPopover();
521
+
522
+ const isGenFav = isGeneralFavorite(providerId, modelId);
523
+ const isAgyFav = state.agyFavorites.some(f => f.providerId === providerId && f.modelId === modelId);
524
+ const genAtCapacity = state.generalFavorites.length >= GENERAL_MAX;
525
+ const agyAtCapacity = state.agyFavorites.length >= AGY_MAX;
526
+
527
+ const popover = document.createElement('div');
528
+ popover.className = 'model-fav-popover';
529
+ popover.dataset.targetId = targetKey;
530
+
531
+ const genDisabled = isGenFav || genAtCapacity;
532
+ let genLabel = '★ Add to Favorites';
533
+ if (isGenFav) genLabel = '✓ In Global Favorites';
534
+ else if (genAtCapacity) genLabel = `★ Favorites full (${GENERAL_MAX}/${GENERAL_MAX})`;
535
+
536
+ const agyDisabled = isAgyFav || agyAtCapacity;
537
+ let agyLabel = '✦ Add to Antigravity Favorites';
538
+ if (isAgyFav) agyLabel = '✓ In Antigravity Favorites';
539
+ else if (agyAtCapacity) agyLabel = `✦ Antigravity full (${AGY_MAX}/${AGY_MAX})`;
540
+
541
+ popover.innerHTML = `
542
+ <button class="model-fav-popover-item ${genDisabled ? 'disabled' : ''}" type="button" data-type="general" ${genDisabled ? 'disabled' : ''}>
543
+ <span>${genLabel}</span>
544
+ <span class="popover-slot-count">${state.generalFavorites.length}/${GENERAL_MAX}</span>
545
+ </button>
546
+ <button class="model-fav-popover-item ${agyDisabled ? 'disabled' : ''}" type="button" data-type="agy" ${agyDisabled ? 'disabled' : ''}>
547
+ <span>${agyLabel}</span>
548
+ <span class="popover-slot-count">${state.agyFavorites.length}/${AGY_MAX}</span>
549
+ </button>
550
+ `;
551
+
552
+ popover.querySelectorAll('.model-fav-popover-item').forEach(item => {
553
+ item.addEventListener('click', event => {
554
+ event.stopPropagation();
555
+ const listType = item.dataset.type;
556
+ addToFavorites({ providerId, modelId }, listType);
557
+ closeModelFavPopover();
558
+ showToast(listType === 'agy' ? 'Added to Antigravity Favorites' : 'Added to Global Favorites');
559
+ renderProviderModelBrowser();
560
+ });
561
+ });
562
+
563
+ const rect = button.getBoundingClientRect();
564
+ popover.style.position = 'fixed';
565
+ popover.style.top = `${rect.bottom + 4}px`;
566
+ popover.style.left = `${Math.max(10, rect.right - 220)}px`;
567
+ popover.style.zIndex = '9999';
568
+
569
+ document.body.appendChild(popover);
570
+ activeModelFavPopover = popover;
571
+ }
572
+
573
+ document.addEventListener('click', event => {
574
+ if (activeModelFavPopover && !activeModelFavPopover.contains(event.target) && !event.target.closest('.btn-model-add')) {
575
+ closeModelFavPopover();
576
+ }
577
+ });
578
+ document.addEventListener('keydown', event => {
579
+ if (event.key === 'Escape' && activeModelFavPopover) {
580
+ closeModelFavPopover();
581
+ }
582
+ });
583
+
506
584
  function renderProviderModelBrowser() {
585
+ closeModelFavPopover();
507
586
  const container = document.getElementById('provider-model-browser');
508
587
  if (!state.activeProviderId) return;
509
588
 
@@ -515,7 +594,11 @@ function renderProviderModelBrowser() {
515
594
  return;
516
595
  }
517
596
 
518
- const result = getProviderModelPage(provider.models ?? [], state.providerModelFilter, state.providerModelPage);
597
+ const showActions = !isServerAdminUi();
598
+ const result = getProviderModelPage(provider.models ?? [], state.providerModelFilter, state.providerModelPage, {
599
+ minContextWindow: state.providerModelMinCtx,
600
+ freeOnly: state.providerModelFreeOnly,
601
+ });
519
602
  state.providerModelPage = result.page;
520
603
  const first = result.total === 0 ? 0 : (result.page - 1) * PROVIDER_MODEL_PAGE_SIZE + 1;
521
604
  const last = Math.min(result.page * PROVIDER_MODEL_PAGE_SIZE, result.total);
@@ -537,19 +620,37 @@ function renderProviderModelBrowser() {
537
620
  <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>
538
621
  <input class="search-input" id="provider-model-search" type="search" value="${escapeHtml(state.providerModelFilter)}" placeholder="Search ${escapeHtml(displayName)} models…" aria-label="Search ${escapeHtml(displayName)} models">
539
622
  </div>
623
+ <div class="provider-model-filters">
624
+ <select class="filter-select" id="provider-model-ctx-select" aria-label="Filter by context window size">
625
+ <option value="0" ${state.providerModelMinCtx === 0 ? 'selected' : ''}>All context sizes</option>
626
+ <option value="128000" ${state.providerModelMinCtx === 128000 ? 'selected' : ''}>≥ 128k</option>
627
+ <option value="200000" ${state.providerModelMinCtx === 200000 ? 'selected' : ''}>≥ 200k</option>
628
+ <option value="500000" ${state.providerModelMinCtx === 500000 ? 'selected' : ''}>≥ 500k</option>
629
+ <option value="1000000" ${state.providerModelMinCtx === 1000000 ? 'selected' : ''}>≥ 1M</option>
630
+ </select>
631
+ <label class="filter-checkbox-label">
632
+ <input type="checkbox" id="provider-model-free-checkbox" ${state.providerModelFreeOnly ? 'checked' : ''}>
633
+ <span>Free models only</span>
634
+ </label>
635
+ </div>
540
636
  <button class="btn btn-ghost" id="provider-model-refresh" type="button">Refresh</button>
541
637
  </div>
542
638
  <div class="provider-model-table-wrap">
543
639
  <table class="provider-model-table">
544
- <thead><tr><th>Model</th><th>Context</th><th>Price in / out</th></tr></thead>
640
+ <thead><tr><th>Model</th><th>Context</th><th>Price in / out</th>${showActions ? '<th class="th-actions">Actions</th>' : ''}</tr></thead>
545
641
  <tbody>
546
642
  ${result.items.map(model => `
547
643
  <tr>
548
644
  <td><strong>${escapeHtml(model.name || model.id)}</strong><code>${escapeHtml(model.id)}</code></td>
549
645
  <td>${fmtCtx(model.contextWindow) || '—'}</td>
550
646
  <td>${formatModelPrice(model.cost, isFreeModel(model), freeBadgeLabel(model))}</td>
647
+ ${showActions ? `
648
+ <td class="td-actions">
649
+ <button class="btn-model-add" type="button" data-model-id="${escapeHtml(model.id)}" title="Add to favorites">+</button>
650
+ </td>
651
+ ` : ''}
551
652
  </tr>
552
- `).join('') || '<tr><td colspan="3" class="provider-model-empty">No models match your search.</td></tr>'}
653
+ `).join('') || `<tr><td colspan="${showActions ? 4 : 3}" class="provider-model-empty">No models match your search and filters.</td></tr>`}
553
654
  </tbody>
554
655
  </table>
555
656
  </div>
@@ -571,6 +672,25 @@ function renderProviderModelBrowser() {
571
672
  search?.focus();
572
673
  search?.setSelectionRange(search.value.length, search.value.length);
573
674
  });
675
+ document.getElementById('provider-model-ctx-select')?.addEventListener('change', event => {
676
+ state.providerModelMinCtx = parseInt(event.target.value, 10) || 0;
677
+ state.providerModelPage = 1;
678
+ renderProviderModelBrowser();
679
+ });
680
+ document.getElementById('provider-model-free-checkbox')?.addEventListener('change', event => {
681
+ state.providerModelFreeOnly = event.target.checked;
682
+ state.providerModelPage = 1;
683
+ renderProviderModelBrowser();
684
+ });
685
+ if (showActions) {
686
+ container.querySelectorAll('.btn-model-add').forEach(btn => {
687
+ btn.addEventListener('click', event => {
688
+ event.stopPropagation();
689
+ const modelId = btn.dataset.modelId;
690
+ toggleModelFavoriteMenu(btn, provider.id, modelId);
691
+ });
692
+ });
693
+ }
574
694
  document.getElementById('provider-model-prev').addEventListener('click', () => {
575
695
  state.providerModelPage -= 1;
576
696
  renderProviderModelBrowser();
@@ -2336,6 +2456,7 @@ function seedServerFormFromSaved(saved) {
2336
2456
  f.freeModelsOnly = Boolean(saved.freeModelsOnly);
2337
2457
  f.exposedProviders = saved.exposedProviders ? [...saved.exposedProviders] : [];
2338
2458
  f.maskGatewayIds = saved.maskGatewayIds;
2459
+ f.autostart = Boolean(saved.autostart);
2339
2460
  // Published Docker ports only reach 0.0.0.0 inside the container.
2340
2461
  f.listenMode = isServerAdminUi() || saved.listenMode === 'network' ? 'network' : 'local';
2341
2462
  // Env password (Docker): prefill the field so Start works and clients can copy it.
@@ -2530,6 +2651,10 @@ function renderServerSetup(s) {
2530
2651
  <input type="checkbox" ${f.freeModelsOnly ? 'checked' : ''} onchange="setServerFreeModelsOnly(this.checked)">
2531
2652
  <span>Free models only <span class="server-field-hint">(includes verified $0 models and free developer access)</span></span>
2532
2653
  </label>
2654
+ <label class="server-toggle-row">
2655
+ <input type="checkbox" ${f.autostart ? 'checked' : ''} onchange="setServerAutostart(this.checked)">
2656
+ <span>Auto-start API Gateway when relay-ai starts <span class="server-field-hint">(port 17645 auto-starts on service boot)</span></span>
2657
+ </label>
2533
2658
  <div class="server-field-hint">${escapeHtml(freeModelsHint)}</div>
2534
2659
  </div>
2535
2660
 
@@ -2695,6 +2820,12 @@ function setServerFreeModelsOnly(checked) {
2695
2820
  renderServerPanel();
2696
2821
  }
2697
2822
 
2823
+ function setServerAutostart(checked) {
2824
+ state.server.form.autostart = checked;
2825
+ api('POST', '/api/server/config', { autostart: checked }).catch(() => {});
2826
+ renderServerPanel();
2827
+ }
2828
+
2698
2829
  function setServerListenMode(mode) {
2699
2830
  state.server.form.listenMode = mode;
2700
2831
  state.server.error = null;
@@ -2735,6 +2866,7 @@ async function submitServerStart() {
2735
2866
  : null,
2736
2867
  maskGatewayIds: f.maskGatewayIds,
2737
2868
  listenMode: f.listenMode,
2869
+ autostart: f.autostart,
2738
2870
  passwordMode: f.passwordMode,
2739
2871
  password: f.password,
2740
2872
  savePassword: f.savePassword,
@@ -1,16 +1,35 @@
1
1
  export const PROVIDER_MODEL_PAGE_SIZE = 25;
2
2
 
3
- export function filterProviderModels(models, query) {
4
- const needle = query.trim().toLowerCase();
5
- if (!needle) return models;
6
- return models.filter(model =>
7
- model.id.toLowerCase().includes(needle)
8
- || (model.name ?? '').toLowerCase().includes(needle),
3
+ export function isFreeModel(model) {
4
+ return Boolean(
5
+ model?.isFree
6
+ || model?.freeStatus === 'verified_free'
7
+ || model?.freeStatus === 'free_provider'
8
+ || (model?.cost && model?.cost.input === 0 && model?.cost.output === 0),
9
9
  );
10
10
  }
11
11
 
12
- export function getProviderModelPage(models, query, requestedPage) {
13
- const filtered = filterProviderModels(models, query);
12
+ export function filterProviderModels(models, query, opts = {}) {
13
+ const needle = query.trim().toLowerCase();
14
+ const minCtx = opts.minContextWindow ?? 0;
15
+ const freeOnly = Boolean(opts.freeOnly);
16
+
17
+ return models.filter(model => {
18
+ if (needle && !model.id.toLowerCase().includes(needle) && !(model.name ?? '').toLowerCase().includes(needle)) {
19
+ return false;
20
+ }
21
+ if (minCtx > 0 && (model.contextWindow ?? 0) < minCtx) {
22
+ return false;
23
+ }
24
+ if (freeOnly && !isFreeModel(model)) {
25
+ return false;
26
+ }
27
+ return true;
28
+ });
29
+ }
30
+
31
+ export function getProviderModelPage(models, query, requestedPage, opts = {}) {
32
+ const filtered = filterProviderModels(models, query, opts);
14
33
  const totalPages = Math.max(1, Math.ceil(filtered.length / PROVIDER_MODEL_PAGE_SIZE));
15
34
  const page = Math.min(Math.max(1, requestedPage), totalPages);
16
35
  const start = (page - 1) * PROVIDER_MODEL_PAGE_SIZE;
@@ -1194,12 +1194,55 @@ input { font-family: inherit; }
1194
1194
  .provider-browser-hero .section-heading { margin-bottom: 4px; }
1195
1195
 
1196
1196
  .provider-browser-tools {
1197
- display: grid;
1198
- grid-template-columns: minmax(0, 1fr) auto;
1199
- gap: 10px;
1197
+ display: flex;
1198
+ flex-wrap: wrap;
1199
+ align-items: center;
1200
+ gap: 12px;
1200
1201
  margin-bottom: 14px;
1201
1202
  }
1202
1203
 
1204
+ .provider-browser-tools .search-field {
1205
+ flex: 1;
1206
+ min-width: 200px;
1207
+ }
1208
+
1209
+ .provider-model-filters {
1210
+ display: flex;
1211
+ align-items: center;
1212
+ gap: 12px;
1213
+ }
1214
+
1215
+ .filter-select {
1216
+ padding: 7px 12px;
1217
+ font-size: 13px;
1218
+ color: var(--text-1);
1219
+ background: var(--surface);
1220
+ border: 1px solid var(--border);
1221
+ border-radius: var(--radius);
1222
+ outline: none;
1223
+ cursor: pointer;
1224
+ }
1225
+
1226
+ .filter-select:focus {
1227
+ border-color: var(--accent, #00d2b8);
1228
+ }
1229
+
1230
+ .filter-checkbox-label {
1231
+ display: flex;
1232
+ align-items: center;
1233
+ gap: 6px;
1234
+ font-size: 13px;
1235
+ color: var(--text-2);
1236
+ cursor: pointer;
1237
+ user-select: none;
1238
+ white-space: nowrap;
1239
+ }
1240
+
1241
+ .filter-checkbox-label input {
1242
+ accent-color: var(--accent, #00d2b8);
1243
+ cursor: pointer;
1244
+ }
1245
+
1203
1246
  .provider-model-table-wrap {
1204
1247
  overflow-x: auto;
1205
1248
  border: 1px solid var(--border);
@@ -1225,8 +1268,82 @@ input { font-family: inherit; }
1225
1268
  text-transform: uppercase;
1226
1269
  }
1227
1270
 
1228
- .provider-model-table th:nth-child(2) { width: 120px; }
1229
- .provider-model-table th:nth-child(3) { width: 170px; }
1271
+ .provider-model-table th:nth-child(2) { width: 110px; }
1272
+ .provider-model-table th:nth-child(3) { width: 160px; }
1273
+ .provider-model-table th.th-actions { width: 80px; text-align: center; }
1274
+ .provider-model-table td.td-actions { text-align: center; }
1275
+
1276
+ .btn-model-add {
1277
+ display: inline-flex;
1278
+ align-items: center;
1279
+ justify-content: center;
1280
+ width: 28px;
1281
+ height: 28px;
1282
+ border-radius: 6px;
1283
+ border: 1px solid var(--border);
1284
+ background: var(--surface-2);
1285
+ color: var(--text-1);
1286
+ font-size: 16px;
1287
+ font-weight: 600;
1288
+ cursor: pointer;
1289
+ transition: all 0.15s ease;
1290
+ }
1291
+
1292
+ .btn-model-add:hover {
1293
+ background: var(--surface-hover);
1294
+ border-color: var(--accent, #00d2b8);
1295
+ color: var(--accent, #00d2b8);
1296
+ }
1297
+
1298
+ .model-fav-popover {
1299
+ background: var(--surface);
1300
+ border: 1px solid var(--border);
1301
+ border-radius: 8px;
1302
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.35);
1303
+ padding: 5px;
1304
+ width: 230px;
1305
+ display: flex;
1306
+ flex-direction: column;
1307
+ gap: 2px;
1308
+ animation: popoverFadeIn 0.12s ease-out;
1309
+ }
1310
+
1311
+ @keyframes popoverFadeIn {
1312
+ from { opacity: 0; transform: translateY(-4px); }
1313
+ to { opacity: 1; transform: translateY(0); }
1314
+ }
1315
+
1316
+ .model-fav-popover-item {
1317
+ display: flex;
1318
+ align-items: center;
1319
+ justify-content: space-between;
1320
+ padding: 8px 10px;
1321
+ border-radius: 5px;
1322
+ border: 0;
1323
+ background: transparent;
1324
+ color: var(--text-1);
1325
+ font-size: 12px;
1326
+ font-weight: 500;
1327
+ cursor: pointer;
1328
+ text-align: left;
1329
+ transition: background 0.12s ease, color 0.12s ease;
1330
+ }
1331
+
1332
+ .model-fav-popover-item:hover:not(.disabled) {
1333
+ background: var(--surface-hover);
1334
+ color: var(--accent, #00d2b8);
1335
+ }
1336
+
1337
+ .model-fav-popover-item.disabled {
1338
+ opacity: 0.5;
1339
+ cursor: not-allowed;
1340
+ }
1341
+
1342
+ .popover-slot-count {
1343
+ font-size: 11px;
1344
+ opacity: 0.7;
1345
+ margin-left: 6px;
1346
+ }
1230
1347
 
1231
1348
  .provider-model-table td {
1232
1349
  padding: 12px 16px;
@@ -58,6 +58,7 @@ import {
58
58
  resolveAdvertiseAddresses,
59
59
  resolveAdvertiseGatewayPort,
60
60
  resolveProviderCredential,
61
+ resolveServerAutostart,
61
62
  resolveServerUpstreamApiKey,
62
63
  saveNativeOAuthCredential,
63
64
  savePreferences,
@@ -65,6 +66,7 @@ import {
65
66
  sendJson,
66
67
  setAppPathOverride,
67
68
  setSavedServerPassword,
69
+ setServerAutostart,
68
70
  setServerExposedProviders,
69
71
  setServerFavoritesOnly,
70
72
  setServerFreeModelsOnly,
@@ -75,7 +77,7 @@ import {
75
77
  supportsClaudeTransparentMode,
76
78
  validateCustomEndpointUrl,
77
79
  writeSecureLogLine
78
- } from "./chunk-LNQ46VW7.js";
80
+ } from "./chunk-EUY2MVOS.js";
79
81
  import {
80
82
  __toCommonJS,
81
83
  init_provider_templates,
@@ -397,6 +399,7 @@ async function buildSavedConfig() {
397
399
  exposedProviders: getServerExposedProviders(),
398
400
  maskGatewayIds: getServerMaskGatewayIds(),
399
401
  listenMode: getServerListenMode(),
402
+ autostart: resolveServerAutostart(),
400
403
  hasSavedPassword: await hasSavedPasswordCached(),
401
404
  hasEnvPassword: Boolean(envPassword),
402
405
  ...envPassword ? { prefillPassword: envPassword } : {}
@@ -509,6 +512,9 @@ async function doStartGatewayServer(req, opts) {
509
512
  }
510
513
  setServerMaskGatewayIds(req.maskGatewayIds);
511
514
  setServerListenMode(req.listenMode);
515
+ if (req.autostart !== void 0) {
516
+ setServerAutostart(req.autostart);
517
+ }
512
518
  const host = req.listenMode === "network" ? "0.0.0.0" : "127.0.0.1";
513
519
  const gateway = req.maskGatewayIds ? { maskGatewayIds: true } : void 0;
514
520
  let handle;
@@ -536,7 +542,8 @@ async function doStartGatewayServer(req, opts) {
536
542
  freeModelsOnly: req.freeModelsOnly,
537
543
  exposedProviders: req.exposedProviders,
538
544
  maskGatewayIds: req.maskGatewayIds,
539
- listenMode: req.listenMode
545
+ listenMode: req.listenMode,
546
+ autostart: req.autostart ?? resolveServerAutostart()
540
547
  },
541
548
  providerSummary: summarizeServerProviders(models),
542
549
  modelRows: buildModelRows(models, gateway)
@@ -551,6 +558,23 @@ async function stopGatewayServer() {
551
558
  }
552
559
  return { ok: true, stopped: false };
553
560
  }
561
+ async function startGatewayServerFromSavedConfig(opts) {
562
+ const req = {
563
+ favoritesOnly: getServerFavoritesOnly(),
564
+ freeModelsOnly: getServerFreeModelsOnly(),
565
+ exposedProviders: getServerExposedProviders(),
566
+ maskGatewayIds: getServerMaskGatewayIds(),
567
+ listenMode: getServerListenMode(),
568
+ autostart: resolveServerAutostart(),
569
+ passwordMode: "saved"
570
+ };
571
+ const res = await startGatewayServer(req, opts);
572
+ if (res.ok) {
573
+ return res.status;
574
+ }
575
+ console.warn(`[server-autostart] Warning: ${res.error}`);
576
+ return null;
577
+ }
554
578
 
555
579
  // src/ui/api.ts
556
580
  var execAsync = promisify(exec);
@@ -650,6 +674,8 @@ function handleUiApiRequest(req, res, opts = {}) {
650
674
  handleStartServer(req, res, opts);
651
675
  } else if (url === "/api/server/stop" && req.method === "POST") {
652
676
  handleStopServer(res, opts);
677
+ } else if (url === "/api/server/config" && req.method === "POST") {
678
+ handlePostServerConfig(req, res);
653
679
  } else {
654
680
  sendJson(res, 404, { error: "Not found" });
655
681
  }
@@ -1284,6 +1310,7 @@ async function handleStartServer(req, res, opts) {
1284
1310
  exposedProviders: Array.isArray(body.exposedProviders) ? body.exposedProviders : null,
1285
1311
  maskGatewayIds: body.maskGatewayIds,
1286
1312
  listenMode,
1313
+ autostart: typeof body.autostart === "boolean" ? body.autostart : void 0,
1287
1314
  passwordMode: body.passwordMode === "saved" ? "saved" : "new",
1288
1315
  password: typeof body.password === "string" ? body.password : void 0,
1289
1316
  savePassword: Boolean(body.savePassword)
@@ -1303,6 +1330,17 @@ async function handleStartServer(req, res, opts) {
1303
1330
  sendJson(res, 500, { ok: false, error: String(err) });
1304
1331
  }
1305
1332
  }
1333
+ async function handlePostServerConfig(req, res) {
1334
+ try {
1335
+ const body = JSON.parse(await readBody(req));
1336
+ if (typeof body.autostart === "boolean") {
1337
+ setServerAutostart(body.autostart);
1338
+ }
1339
+ sendJson(res, 200, { ok: true });
1340
+ } catch (err) {
1341
+ sendJson(res, 400, { error: String(err) });
1342
+ }
1343
+ }
1306
1344
  async function handleStopServer(res, opts) {
1307
1345
  try {
1308
1346
  const result = await stopGatewayServer();
@@ -1454,11 +1492,35 @@ function removeLock() {
1454
1492
  } catch {
1455
1493
  }
1456
1494
  }
1457
- function checkExistingServer() {
1495
+ async function probeServerHealth(port) {
1496
+ try {
1497
+ const controller = new AbortController();
1498
+ const timer = setTimeout(() => controller.abort(), 500);
1499
+ const res = await fetch(`http://127.0.0.1:${port}/api/config`, { signal: controller.signal });
1500
+ clearTimeout(timer);
1501
+ if (res.status === 200) {
1502
+ const data = await res.json().catch(() => null);
1503
+ if (data && typeof data === "object") return true;
1504
+ }
1505
+ return false;
1506
+ } catch {
1507
+ return false;
1508
+ }
1509
+ }
1510
+ async function checkExistingServer() {
1458
1511
  if (!existsSync3(LOCK_FILE)) return null;
1459
1512
  try {
1460
1513
  const { pid, port } = JSON.parse(readFileSync(LOCK_FILE, "utf8"));
1514
+ if (pid === process.pid) {
1515
+ removeLock();
1516
+ return null;
1517
+ }
1461
1518
  process.kill(pid, 0);
1519
+ const isAlive = await probeServerHealth(port ?? DEFAULT_SERVER_UI_PORT);
1520
+ if (!isAlive) {
1521
+ removeLock();
1522
+ return null;
1523
+ }
1462
1524
  return `http://127.0.0.1:${port}`;
1463
1525
  } catch {
1464
1526
  removeLock();
@@ -1487,7 +1549,7 @@ async function resolveUiShutdownDecision(signal, promptClose = () => p.confirm({
1487
1549
  async function runUiCommand(opts = {}) {
1488
1550
  const runtime = resolveUiRuntimeConfig(opts);
1489
1551
  await ensureOpencodeCloudProviders();
1490
- const existing = checkExistingServer();
1552
+ const existing = await checkExistingServer();
1491
1553
  if (existing) {
1492
1554
  console.log(`
1493
1555
  ${pc.bold("relay-ai UI")} already running at ${pc.cyan(existing)}
@@ -1542,6 +1604,11 @@ async function runUiCommand(opts = {}) {
1542
1604
  const url = `http://${displayHost}:${port}`;
1543
1605
  mkdirSync(getAppHome(), { recursive: true });
1544
1606
  writeFileSync2(LOCK_FILE, JSON.stringify({ pid: process.pid, port, mode: runtime.mode }));
1607
+ if (resolveServerAutostart()) {
1608
+ startGatewayServerFromSavedConfig().catch((err) => {
1609
+ trace?.(`gateway autostart error: ${err instanceof Error ? err.message : String(err)}`);
1610
+ });
1611
+ }
1545
1612
  const cleanup = () => {
1546
1613
  removeLock();
1547
1614
  server.close();
@@ -1596,11 +1663,13 @@ async function runUiCommand(opts = {}) {
1596
1663
  return 0;
1597
1664
  }
1598
1665
  export {
1666
+ checkExistingServer,
1599
1667
  formatUiServerLifecycleMessage,
1600
1668
  isUiApiRoute,
1669
+ probeServerHealth,
1601
1670
  resolveUiMode,
1602
1671
  resolveUiRuntimeConfig,
1603
1672
  resolveUiShutdownDecision,
1604
1673
  runUiCommand
1605
1674
  };
1606
- //# sourceMappingURL=ui-command-5VEFZ5GM.js.map
1675
+ //# sourceMappingURL=ui-command-CF3IEIZP.js.map