@jacobbd/relay-ai 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -2
- package/dist/{chunk-MVBA7ABV.js → chunk-EJONCU3B.js} +4 -4
- package/dist/chunk-EJONCU3B.js.map +1 -0
- package/dist/{chunk-44KQK6Y5.js → chunk-P4S42QJK.js} +2805 -2778
- package/dist/chunk-P4S42QJK.js.map +1 -0
- package/dist/cli.js +364 -226
- package/dist/cli.js.map +1 -1
- package/dist/{provider-templates-6XYKAZB5.js → provider-templates-BPGB5V2L.js} +2 -2
- package/dist/ui/public/app.js +92 -21
- package/dist/ui/public/index.html +1 -1
- package/dist/ui/public/oauth-device.js +44 -19
- package/dist/ui/public/style.css +24 -0
- package/dist/{ui-command-M7KMKIQC.js → ui-command-EQJIZRZZ.js} +135 -57
- package/dist/ui-command-EQJIZRZZ.js.map +1 -0
- package/package.json +2 -1
- package/dist/chunk-44KQK6Y5.js.map +0 -1
- package/dist/chunk-MVBA7ABV.js.map +0 -1
- package/dist/ui-command-M7KMKIQC.js.map +0 -1
- /package/dist/{provider-templates-6XYKAZB5.js.map → provider-templates-BPGB5V2L.js.map} +0 -0
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
listAddableTemplates,
|
|
8
8
|
listSupportedTemplates,
|
|
9
9
|
listVisibleOAuthTemplates
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-EJONCU3B.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-BPGB5V2L.js.map
|
package/dist/ui/public/app.js
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
getProviderModelPage,
|
|
4
4
|
PROVIDER_MODEL_PAGE_SIZE,
|
|
5
5
|
} from './provider-model-browser.js';
|
|
6
|
-
import { copyDeviceCode, oauthConnectionLabel } from './oauth-device.js';
|
|
6
|
+
import { copyDeviceCode, copyTextToClipboard, oauthConnectionLabel } from './oauth-device.js';
|
|
7
7
|
import { providerInitial, providerLogoHtml } from './provider-logo.js';
|
|
8
8
|
|
|
9
9
|
// ─── State ───────────────────────────────────────────────────────────────────
|
|
@@ -53,6 +53,7 @@ const state = {
|
|
|
53
53
|
listenMode: 'local', // 'local' | 'network'
|
|
54
54
|
passwordMode: 'new', // 'saved' | 'new'
|
|
55
55
|
password: '',
|
|
56
|
+
passwordVisible: false,
|
|
56
57
|
savePassword: false,
|
|
57
58
|
},
|
|
58
59
|
},
|
|
@@ -207,7 +208,7 @@ async function initUpdateIndicator() {
|
|
|
207
208
|
|
|
208
209
|
document.getElementById('update-copy-btn')?.addEventListener('click', async () => {
|
|
209
210
|
try {
|
|
210
|
-
await
|
|
211
|
+
await copyTextToClipboard(UPDATE_COMMAND);
|
|
211
212
|
showToast('Update command copied');
|
|
212
213
|
} catch {
|
|
213
214
|
showToast('Could not copy the update command');
|
|
@@ -1059,6 +1060,8 @@ function buildProviderBodyContent(provider) {
|
|
|
1059
1060
|
const countEl = document.querySelector(`[data-id="${CSS.escape(provider.id)}"] .provider-models-count`);
|
|
1060
1061
|
if (countEl) countEl.textContent = `${result.count} models`;
|
|
1061
1062
|
provider.modelCount = result.count;
|
|
1063
|
+
state.modelsLoaded = false;
|
|
1064
|
+
await initModels();
|
|
1062
1065
|
} else {
|
|
1063
1066
|
feedback.textContent = result.error ?? 'Refresh failed';
|
|
1064
1067
|
feedback.className = 'key-feedback error';
|
|
@@ -1179,6 +1182,8 @@ function buildOAuthProviderBodyContent(provider) {
|
|
|
1179
1182
|
feedback.className = 'key-feedback success';
|
|
1180
1183
|
const countEl = document.querySelector(`[data-id="${CSS.escape(provider.id)}"] .provider-models-count`);
|
|
1181
1184
|
if (countEl) countEl.textContent = `${result.count} models`;
|
|
1185
|
+
state.modelsLoaded = false;
|
|
1186
|
+
await initModels();
|
|
1182
1187
|
} else {
|
|
1183
1188
|
feedback.textContent = result.error ?? 'Refresh failed';
|
|
1184
1189
|
feedback.className = 'key-feedback error';
|
|
@@ -1519,8 +1524,15 @@ function updateAgyCounter() {
|
|
|
1519
1524
|
|
|
1520
1525
|
// ─── Sidebar nav highlight ────────────────────────────────────────────────────
|
|
1521
1526
|
|
|
1527
|
+
function isServerAdminUi() {
|
|
1528
|
+
return document.documentElement.dataset.uiMode === 'server';
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1522
1531
|
function initNav() {
|
|
1523
|
-
|
|
1532
|
+
// Host-only sections stay in the DOM but are CSS-hidden in server admin mode.
|
|
1533
|
+
const sectionIds = isServerAdminUi()
|
|
1534
|
+
? ['providers', 'favorites', 'server']
|
|
1535
|
+
: ['providers', 'favorites', 'antigravity', 'apps', 'server'];
|
|
1524
1536
|
const navItems = document.querySelectorAll('.nav-item');
|
|
1525
1537
|
const content = document.getElementById('content');
|
|
1526
1538
|
|
|
@@ -1605,6 +1617,18 @@ async function initModels() {
|
|
|
1605
1617
|
state.modelsError = String(err);
|
|
1606
1618
|
state.modelsLoaded = true;
|
|
1607
1619
|
}
|
|
1620
|
+
// Keep the Providers header counts in sync after add / delete / OAuth / refresh.
|
|
1621
|
+
renderProviderStats();
|
|
1622
|
+
// Server tab caches its provider checklist — invalidate so new providers appear without a full reload.
|
|
1623
|
+
invalidateServerProviderCache();
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
function invalidateServerProviderCache() {
|
|
1627
|
+
state.server.form.providersLoaded = false;
|
|
1628
|
+
if (state.server.status?.running) return;
|
|
1629
|
+
if (state.server.form.expose !== 'specific') return;
|
|
1630
|
+
if (!document.getElementById('server-panel')) return;
|
|
1631
|
+
void loadServerProviders().then(() => renderServerPanel());
|
|
1608
1632
|
}
|
|
1609
1633
|
|
|
1610
1634
|
async function init() {
|
|
@@ -1616,11 +1640,13 @@ async function init() {
|
|
|
1616
1640
|
|
|
1617
1641
|
await loadConfig();
|
|
1618
1642
|
renderFavList();
|
|
1619
|
-
renderAgyList();
|
|
1620
|
-
updateAgyCounter();
|
|
1621
1643
|
updateGeneralCounter();
|
|
1622
|
-
|
|
1623
|
-
|
|
1644
|
+
if (!isServerAdminUi()) {
|
|
1645
|
+
renderAgyList();
|
|
1646
|
+
updateAgyCounter();
|
|
1647
|
+
await loadApps();
|
|
1648
|
+
renderApps();
|
|
1649
|
+
}
|
|
1624
1650
|
renderProviders(); // shows skeletons
|
|
1625
1651
|
|
|
1626
1652
|
initServerPolling();
|
|
@@ -1631,12 +1657,12 @@ async function init() {
|
|
|
1631
1657
|
initModels().then(() => {
|
|
1632
1658
|
renderProviders();
|
|
1633
1659
|
renderProviderStats();
|
|
1634
|
-
renderApps();
|
|
1660
|
+
if (!isServerAdminUi()) renderApps();
|
|
1635
1661
|
// Re-render favorites now that we have full provider names
|
|
1636
1662
|
renderFavList();
|
|
1637
|
-
renderAgyList();
|
|
1663
|
+
if (!isServerAdminUi()) renderAgyList();
|
|
1638
1664
|
if (state.modelFilter) buildModelResults(state.modelFilter, 'general');
|
|
1639
|
-
if (state.agyFilter)
|
|
1665
|
+
if (!isServerAdminUi() && state.agyFilter) buildModelResults(state.agyFilter, 'agy');
|
|
1640
1666
|
syncProviderModelBrowserFromHash();
|
|
1641
1667
|
});
|
|
1642
1668
|
|
|
@@ -2220,15 +2246,28 @@ document.addEventListener('click', (e) => {
|
|
|
2220
2246
|
async function refreshServerStatus() {
|
|
2221
2247
|
try {
|
|
2222
2248
|
const data = await api('GET', '/api/server/status');
|
|
2249
|
+
const wasRunning = Boolean(state.server.status?.running);
|
|
2223
2250
|
state.server.status = data;
|
|
2224
2251
|
if (!data.running) seedServerFormFromSaved(data.saved);
|
|
2252
|
+
const runningChanged = wasRunning !== Boolean(data.running);
|
|
2253
|
+
// Full innerHTML replace steals focus from password / search fields — skip while typing.
|
|
2254
|
+
if (runningChanged || !isEditingInside(document.getElementById('server-panel'))) {
|
|
2255
|
+
renderServerPanel();
|
|
2256
|
+
}
|
|
2225
2257
|
} catch {
|
|
2226
2258
|
// Transient poll failure — keep showing the last known state.
|
|
2227
2259
|
}
|
|
2228
|
-
renderServerPanel();
|
|
2229
2260
|
updateServerNavBadge();
|
|
2230
2261
|
}
|
|
2231
2262
|
|
|
2263
|
+
function isEditingInside(root) {
|
|
2264
|
+
if (!root) return false;
|
|
2265
|
+
const el = document.activeElement;
|
|
2266
|
+
if (!el || !root.contains(el)) return false;
|
|
2267
|
+
const tag = el.tagName;
|
|
2268
|
+
return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || el.isContentEditable;
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2232
2271
|
function seedServerFormFromSaved(saved) {
|
|
2233
2272
|
const f = state.server.form;
|
|
2234
2273
|
if (f.seeded || !saved) return;
|
|
@@ -2240,8 +2279,16 @@ function seedServerFormFromSaved(saved) {
|
|
|
2240
2279
|
f.freeModelsOnly = Boolean(saved.freeModelsOnly);
|
|
2241
2280
|
f.exposedProviders = saved.exposedProviders ? [...saved.exposedProviders] : [];
|
|
2242
2281
|
f.maskGatewayIds = saved.maskGatewayIds;
|
|
2243
|
-
|
|
2244
|
-
f.
|
|
2282
|
+
// Published Docker ports only reach 0.0.0.0 inside the container.
|
|
2283
|
+
f.listenMode = isServerAdminUi() || saved.listenMode === 'network' ? 'network' : 'local';
|
|
2284
|
+
// Env password (Docker): prefill the field so Start works and clients can copy it.
|
|
2285
|
+
// Keychain-saved password: keep "Use saved" (value never sent to the browser).
|
|
2286
|
+
if (saved.prefillPassword && !f.password) {
|
|
2287
|
+
f.password = saved.prefillPassword;
|
|
2288
|
+
f.passwordMode = 'new';
|
|
2289
|
+
} else {
|
|
2290
|
+
f.passwordMode = saved.hasSavedPassword ? 'saved' : 'new';
|
|
2291
|
+
}
|
|
2245
2292
|
if (f.expose === 'specific') loadServerProviders().then(renderServerPanel);
|
|
2246
2293
|
}
|
|
2247
2294
|
|
|
@@ -2282,6 +2329,10 @@ function renderServerPanel() {
|
|
|
2282
2329
|
panel.innerHTML = '<div class="skeleton skeleton-card"></div>';
|
|
2283
2330
|
return;
|
|
2284
2331
|
}
|
|
2332
|
+
// Lazy-load (or re-load after invalidate) when the specific-provider checklist is visible.
|
|
2333
|
+
if (!s.status.running && s.form.expose === 'specific' && !s.form.providersLoaded) {
|
|
2334
|
+
void loadServerProviders().then(() => renderServerPanel());
|
|
2335
|
+
}
|
|
2285
2336
|
panel.innerHTML = s.status.running ? renderServerRunning(s.status) : renderServerSetup(s);
|
|
2286
2337
|
}
|
|
2287
2338
|
|
|
@@ -2341,16 +2392,22 @@ function renderServerNetworkOptions() {
|
|
|
2341
2392
|
const s = state.server;
|
|
2342
2393
|
const f = s.form;
|
|
2343
2394
|
const hasSaved = Boolean(s.status?.saved?.hasSavedPassword);
|
|
2395
|
+
const hasEnv = Boolean(s.status?.saved?.hasEnvPassword);
|
|
2344
2396
|
const showPasswordInput = f.passwordMode === 'new' || !hasSaved;
|
|
2397
|
+
const inputType = f.passwordVisible ? 'text' : 'password';
|
|
2345
2398
|
|
|
2346
2399
|
return `
|
|
2347
2400
|
<div class="server-network-options">
|
|
2348
|
-
${hasSaved ? toggleGroupHtml([
|
|
2401
|
+
${hasSaved && !hasEnv ? toggleGroupHtml([
|
|
2349
2402
|
{ label: 'Use saved password', active: f.passwordMode === 'saved', onClick: "setServerPasswordMode('saved')" },
|
|
2350
2403
|
{ label: 'Enter new password', active: f.passwordMode === 'new', onClick: "setServerPasswordMode('new')" },
|
|
2351
2404
|
]) : ''}
|
|
2405
|
+
${hasEnv && f.passwordMode === 'new' ? '<div class="server-field-hint">Prefilled from RELAY_AI_SERVER_PASSWORD</div>' : ''}
|
|
2352
2406
|
${showPasswordInput ? `
|
|
2353
|
-
<
|
|
2407
|
+
<div class="server-password-row">
|
|
2408
|
+
<input type="${inputType}" class="key-input" id="server-password-input" placeholder="Server password" value="${escapeHtml(f.password)}" oninput="setServerPassword(this.value)" autocomplete="off">
|
|
2409
|
+
<button type="button" class="btn btn-ghost server-password-reveal" onclick="toggleServerPasswordVisible()" aria-pressed="${f.passwordVisible ? 'true' : 'false'}">${f.passwordVisible ? 'Hide' : 'Reveal'}</button>
|
|
2410
|
+
</div>
|
|
2354
2411
|
<label class="server-toggle-row">
|
|
2355
2412
|
<input type="checkbox" ${f.savePassword ? 'checked' : ''} onchange="setServerSavePassword(this.checked)">
|
|
2356
2413
|
<span>Save this password for next time</span>
|
|
@@ -2360,6 +2417,17 @@ function renderServerNetworkOptions() {
|
|
|
2360
2417
|
`;
|
|
2361
2418
|
}
|
|
2362
2419
|
|
|
2420
|
+
function renderServerListenField(f) {
|
|
2421
|
+
if (isServerAdminUi()) {
|
|
2422
|
+
return `<div class="server-field-hint">Network (required in container — use the published host port from the URL cards)</div>${renderServerNetworkOptions()}`;
|
|
2423
|
+
}
|
|
2424
|
+
const toggle = toggleGroupHtml([
|
|
2425
|
+
{ label: 'Local only', active: f.listenMode === 'local', onClick: "setServerListenMode('local')" },
|
|
2426
|
+
{ label: 'Network', active: f.listenMode === 'network', onClick: "setServerListenMode('network')" },
|
|
2427
|
+
]);
|
|
2428
|
+
return f.listenMode === 'network' ? `${toggle}${renderServerNetworkOptions()}` : toggle;
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2363
2431
|
function renderServerSetup(s) {
|
|
2364
2432
|
const f = s.form;
|
|
2365
2433
|
const specific = f.expose === 'specific';
|
|
@@ -2410,11 +2478,7 @@ function renderServerSetup(s) {
|
|
|
2410
2478
|
|
|
2411
2479
|
<div class="server-field">
|
|
2412
2480
|
<div class="server-field-label">Listen</div>
|
|
2413
|
-
${
|
|
2414
|
-
{ label: 'Local only', active: f.listenMode === 'local', onClick: "setServerListenMode('local')" },
|
|
2415
|
-
{ label: 'Network', active: f.listenMode === 'network', onClick: "setServerListenMode('network')" },
|
|
2416
|
-
])}
|
|
2417
|
-
${f.listenMode === 'network' ? renderServerNetworkOptions() : ''}
|
|
2481
|
+
${renderServerListenField(f)}
|
|
2418
2482
|
</div>
|
|
2419
2483
|
|
|
2420
2484
|
${s.error ? `<div class="key-feedback error server-start-error">${escapeHtml(s.error)}</div>` : ''}
|
|
@@ -2551,6 +2615,12 @@ function setServerPassword(value) {
|
|
|
2551
2615
|
state.server.form.password = value;
|
|
2552
2616
|
}
|
|
2553
2617
|
|
|
2618
|
+
function toggleServerPasswordVisible() {
|
|
2619
|
+
state.server.form.passwordVisible = !state.server.form.passwordVisible;
|
|
2620
|
+
renderServerPanel();
|
|
2621
|
+
document.getElementById('server-password-input')?.focus();
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2554
2624
|
function setServerSavePassword(checked) {
|
|
2555
2625
|
state.server.form.savePassword = checked;
|
|
2556
2626
|
}
|
|
@@ -2618,7 +2688,7 @@ function serverIdCell(id) {
|
|
|
2618
2688
|
|
|
2619
2689
|
async function copyPlainText(text) {
|
|
2620
2690
|
try {
|
|
2621
|
-
await
|
|
2691
|
+
await copyTextToClipboard(text);
|
|
2622
2692
|
showToast('Copied to clipboard');
|
|
2623
2693
|
} catch {
|
|
2624
2694
|
showToast('Could not copy — copy manually');
|
|
@@ -2646,6 +2716,7 @@ window.setServerFreeModelsOnly = setServerFreeModelsOnly;
|
|
|
2646
2716
|
window.setServerListenMode = setServerListenMode;
|
|
2647
2717
|
window.setServerPasswordMode = setServerPasswordMode;
|
|
2648
2718
|
window.setServerPassword = setServerPassword;
|
|
2719
|
+
window.toggleServerPasswordVisible = toggleServerPasswordVisible;
|
|
2649
2720
|
window.setServerSavePassword = setServerSavePassword;
|
|
2650
2721
|
window.submitServerStart = submitServerStart;
|
|
2651
2722
|
window.stopServerGateway = stopServerGateway;
|
|
@@ -1,30 +1,55 @@
|
|
|
1
|
-
/**
|
|
2
|
-
export function oauthConnectionLabel(provider) {
|
|
3
|
-
if (!provider?.hasKey) return provider?.freeAccess ? 'Free models' : 'Not configured';
|
|
4
|
-
if (provider.authType !== 'oauth') return 'Key stored';
|
|
5
|
-
const planLabel = provider.subscription?.label;
|
|
6
|
-
return typeof planLabel === 'string' && planLabel.trim()
|
|
7
|
-
? `Connected · ${planLabel.trim()}`
|
|
8
|
-
: 'Connected';
|
|
9
|
-
}
|
|
1
|
+
/** Clipboard helpers for the web UI (works on http://LAN as well as https/localhost). */
|
|
10
2
|
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Copy text to the clipboard. Prefer the async Clipboard API in secure contexts;
|
|
5
|
+
* fall back to a hidden textarea + execCommand for plain HTTP (LAN Docker UI).
|
|
6
|
+
*/
|
|
7
|
+
export async function copyTextToClipboard(
|
|
8
|
+
text,
|
|
9
|
+
clipboard = globalThis.navigator?.clipboard,
|
|
10
|
+
doc = globalThis.document,
|
|
11
|
+
) {
|
|
12
|
+
const secure = typeof globalThis.isSecureContext === 'boolean'
|
|
13
|
+
? globalThis.isSecureContext
|
|
14
|
+
: Boolean(clipboard?.writeText);
|
|
15
|
+
|
|
16
|
+
if (secure && clipboard?.writeText) {
|
|
17
|
+
try {
|
|
18
|
+
await clipboard.writeText(text);
|
|
19
|
+
return;
|
|
20
|
+
} catch {
|
|
21
|
+
// Fall through — common on http://192.168.x.x where the API exists but throws.
|
|
22
|
+
}
|
|
16
23
|
}
|
|
17
|
-
|
|
24
|
+
|
|
25
|
+
if (!doc?.body || typeof doc.execCommand !== 'function') {
|
|
18
26
|
throw new Error('Clipboard access is unavailable');
|
|
19
27
|
}
|
|
20
|
-
const input =
|
|
21
|
-
input.value =
|
|
28
|
+
const input = doc.createElement('textarea');
|
|
29
|
+
input.value = text;
|
|
22
30
|
input.setAttribute('readonly', '');
|
|
23
31
|
input.style.position = 'fixed';
|
|
32
|
+
input.style.left = '-9999px';
|
|
24
33
|
input.style.opacity = '0';
|
|
25
|
-
|
|
34
|
+
doc.body.appendChild(input);
|
|
26
35
|
input.select();
|
|
27
|
-
|
|
36
|
+
input.setSelectionRange(0, input.value.length);
|
|
37
|
+
const copied = doc.execCommand('copy');
|
|
28
38
|
input.remove();
|
|
29
39
|
if (!copied) throw new Error('Copy failed');
|
|
30
40
|
}
|
|
41
|
+
|
|
42
|
+
/** @deprecated Prefer copyTextToClipboard — kept for existing call sites / tests. */
|
|
43
|
+
export async function copyDeviceCode(code, clipboard = globalThis.navigator?.clipboard) {
|
|
44
|
+
return copyTextToClipboard(code, clipboard);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Return the short credential label used on a provider card. */
|
|
48
|
+
export function oauthConnectionLabel(provider) {
|
|
49
|
+
if (!provider?.hasKey) return provider?.freeAccess ? 'Free models' : 'Not configured';
|
|
50
|
+
if (provider.authType !== 'oauth') return 'Key stored';
|
|
51
|
+
const planLabel = provider.subscription?.label;
|
|
52
|
+
return typeof planLabel === 'string' && planLabel.trim()
|
|
53
|
+
? `Connected · ${planLabel.trim()}`
|
|
54
|
+
: 'Connected';
|
|
55
|
+
}
|
package/dist/ui/public/style.css
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
/* ─── Tokens ─────────────────────────────────────────────────────────────── */
|
|
2
|
+
/* Server admin UI (Docker): hide host-only sections before JS runs */
|
|
3
|
+
html[data-ui-mode="server"] .nav-item[data-section="apps"],
|
|
4
|
+
html[data-ui-mode="server"] .nav-item[data-section="antigravity"],
|
|
5
|
+
html[data-ui-mode="server"] #apps,
|
|
6
|
+
html[data-ui-mode="server"] #antigravity {
|
|
7
|
+
display: none !important;
|
|
8
|
+
}
|
|
9
|
+
|
|
2
10
|
:root {
|
|
3
11
|
/* Deep navy background — NOT flat black */
|
|
4
12
|
--bg: oklch(12% 0.038 265);
|
|
@@ -1868,6 +1876,22 @@ input { font-family: inherit; }
|
|
|
1868
1876
|
gap: 10px;
|
|
1869
1877
|
}
|
|
1870
1878
|
|
|
1879
|
+
.server-password-row {
|
|
1880
|
+
display: flex;
|
|
1881
|
+
align-items: center;
|
|
1882
|
+
gap: 8px;
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
.server-password-row .key-input {
|
|
1886
|
+
flex: 1;
|
|
1887
|
+
min-width: 0;
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
.server-password-reveal {
|
|
1891
|
+
flex-shrink: 0;
|
|
1892
|
+
white-space: nowrap;
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1871
1895
|
.server-provider-chips {
|
|
1872
1896
|
display: flex;
|
|
1873
1897
|
flex-wrap: wrap;
|