Skip to content
Merged
5 changes: 5 additions & 0 deletions tests/unit/web-ui-behavior-parity.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,7 @@ test('captured bundled app skeleton only exposes expected data key drift versus
];
allowedExtraCurrentKeys.push(
'lang',
'configTemplateContext',
'configTemplateDiffVisible',
'configTemplateDiffLoading',
'configTemplateDiffError',
Expand All @@ -424,6 +425,8 @@ test('captured bundled app skeleton only exposes expected data key drift versus
'healthCheckBatchDone',
'healthCheckBatchFailed',
'showHealthCheckModal',
'showCodexBridgePoolModal',
'showClaudeBridgePoolModal',
'pluginsActiveId',
'pluginsLoading',
'pluginsError',
Expand Down Expand Up @@ -571,6 +574,8 @@ test('captured bundled app skeleton only exposes expected data key drift versus
'normalizeConfigTemplateDiffConfirmEnabled',
'setConfigTemplateDiffConfirmEnabled',
'extractClaudeResumeKeyFromFilePath',
'openClaudeConfigTemplateEditor',
'applyClaudeLocalBridge',
'loadPluginsOverview',
'selectPlugin',
'createPromptTemplate',
Expand Down
3 changes: 3 additions & 0 deletions web-ui/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ document.addEventListener('DOMContentLoaded', () => {
showAgentsModal: false,
showSkillsModal: false,
showHealthCheckModal: false,
showCodexBridgePoolModal: false,
showClaudeBridgePoolModal: false,
// Plugins
pluginsActiveId: 'prompt-templates',
pluginsLoading: false,
Expand Down Expand Up @@ -98,6 +100,7 @@ document.addEventListener('DOMContentLoaded', () => {
confirmDialogResolver: null,
configTemplateContent: '',
configTemplateApplying: false,
configTemplateContext: 'codex',
configTemplateDiffVisible: false,
configTemplateDiffLoading: false,
configTemplateDiffError: '',
Expand Down
4 changes: 4 additions & 0 deletions web-ui/logic.claude.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ export function matchClaudeConfigFromSettings(claudeConfigs = {}, env = {}) {
if (!normalizedSettings.baseUrl || !normalizedSettings.model || !hasClaudeCredential(normalizedSettings)) {
return '';
}
// 检测本地桥接 URL
if (typeof normalizedSettings.baseUrl === 'string' && normalizedSettings.baseUrl.includes('/bridge/claude-local/')) {
return 'claude-local';
}
Comment on lines +114 to +117
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Local-bridge URL detection is too strict and misses valid forms.

The current check only matches /bridge/claude-local/. URLs without trailing slash (e.g., /bridge/claude-local) won’t match.

Proposed fix
-    if (typeof normalizedSettings.baseUrl === 'string' && normalizedSettings.baseUrl.includes('/bridge/claude-local/')) {
+    const normalizedBaseUrl = normalizeClaudeComparableUrl(normalizedSettings.baseUrl).toLowerCase();
+    if (normalizedBaseUrl.includes('/bridge/claude-local')) {
         return 'claude-local';
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/logic.claude.mjs` around lines 114 - 117, The local-bridge URL check
inside web-ui/logic.claude.mjs is too strict — the current conditional that
inspects normalizedSettings.baseUrl only matches '/bridge/claude-local/' and
misses URLs without a trailing slash; update the detection logic (the
conditional referencing normalizedSettings.baseUrl in this module) to treat
'/bridge/claude-local' and '/bridge/claude-local/' (and any URL where
'/bridge/claude-local' is a path segment) as matches — e.g., replace the exact
include check with a test that accepts either a trailing slash or end-of-string
(or use startsWith or a regex like '/bridge/claude-local(\/|$)') so the function
returns 'claude-local' for both forms.

const comparableSettingsUrl = normalizeClaudeComparableUrl(normalizedSettings.baseUrl);
const entries = Object.entries(claudeConfigs || {});
for (const [name, config] of entries) {
Expand Down
37 changes: 37 additions & 0 deletions web-ui/modules/app.methods.claude-config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,43 @@ export function createClaudeConfigMethods(options = {}) {

claudeLocalBridgeConfigured() {
return this.claudeLocalBridgeCandidateProviders().some(p => p.hasKey);
},

async applyClaudeLocalBridge() {
this.currentClaudeConfig = 'claude-local';
try { localStorage.setItem('currentClaudeConfig', 'claude-local'); } catch (_) {}
this.refreshClaudeModelContext();

const candidates = this.claudeLocalBridgeCandidateProviders();
if (candidates.length === 0) {
return this.showMessage('请先添加并配置至少一个 Claude 提供商', 'error');
}

try {
const res = await api('claude-local-bridge-toggle', { enable: true });
if (res.error) {
this.showMessage(res.error || '启用本地负载均衡失败', 'error');
return;
}
this.showMessage('Claude 本地负载均衡已启用', 'success');
} catch (e) {
this.showMessage('启用本地负载均衡失败', 'error');
}
Comment on lines +269 to +288
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Switching to claude-local before enable succeeds leaves stale UI state.

If validation or claude-local-bridge-toggle fails, the app still persists currentClaudeConfig='claude-local'. Keep current selection unchanged until enable succeeds.

Proposed fix
         async applyClaudeLocalBridge() {
-            this.currentClaudeConfig = 'claude-local';
-            try { localStorage.setItem('currentClaudeConfig', 'claude-local'); } catch (_) {}
-            this.refreshClaudeModelContext();
-
             const candidates = this.claudeLocalBridgeCandidateProviders();
             if (candidates.length === 0) {
                 return this.showMessage('请先添加并配置至少一个 Claude 提供商', 'error');
             }
 
             try {
                 const res = await api('claude-local-bridge-toggle', { enable: true });
                 if (res.error) {
                     this.showMessage(res.error || '启用本地负载均衡失败', 'error');
                     return;
                 }
+                this.currentClaudeConfig = 'claude-local';
+                try { localStorage.setItem('currentClaudeConfig', 'claude-local'); } catch (_) {}
+                this.refreshClaudeModelContext();
                 this.showMessage('Claude 本地负载均衡已启用', 'success');
             } catch (e) {
                 this.showMessage('启用本地负载均衡失败', 'error');
             }
         },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async applyClaudeLocalBridge() {
this.currentClaudeConfig = 'claude-local';
try { localStorage.setItem('currentClaudeConfig', 'claude-local'); } catch (_) {}
this.refreshClaudeModelContext();
const candidates = this.claudeLocalBridgeCandidateProviders();
if (candidates.length === 0) {
return this.showMessage('请先添加并配置至少一个 Claude 提供商', 'error');
}
try {
const res = await api('claude-local-bridge-toggle', { enable: true });
if (res.error) {
this.showMessage(res.error || '启用本地负载均衡失败', 'error');
return;
}
this.showMessage('Claude 本地负载均衡已启用', 'success');
} catch (e) {
this.showMessage('启用本地负载均衡失败', 'error');
}
async applyClaudeLocalBridge() {
const candidates = this.claudeLocalBridgeCandidateProviders();
if (candidates.length === 0) {
return this.showMessage('请先添加并配置至少一个 Claude 提供商', 'error');
}
try {
const res = await api('claude-local-bridge-toggle', { enable: true });
if (res.error) {
this.showMessage(res.error || '启用本地负载均衡失败', 'error');
return;
}
this.currentClaudeConfig = 'claude-local';
try { localStorage.setItem('currentClaudeConfig', 'claude-local'); } catch (_) {}
this.refreshClaudeModelContext();
this.showMessage('Claude 本地负载均衡已启用', 'success');
} catch (e) {
this.showMessage('启用本地负载均衡失败', 'error');
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/modules/app.methods.claude-config.mjs` around lines 269 - 288, The
method applyClaudeLocalBridge currently sets this.currentClaudeConfig =
'claude-local' and writes localStorage before the API call, causing stale UI
state if validation or api('claude-local-bridge-toggle', { enable: true })
fails; change the flow in applyClaudeLocalBridge so you first validate
candidates via claudeLocalBridgeCandidateProviders(), then call the API, and
only on successful response set this.currentClaudeConfig = 'claude-local',
persist to localStorage, and call this.refreshClaudeModelContext(); if the API
returns an error or throws, do not modify currentClaudeConfig or localStorage
(or revert them if already changed), and continue to show the existing error
messages via this.showMessage.

},

async openClaudeConfigTemplateEditor() {
try {
const res = await api('get-claude-settings-raw');
if (res.error) {
this.showMessage(res.error, 'error');
return;
}
this.configTemplateContent = res.content || '{}';
this.configTemplateContext = 'claude';
this.showConfigTemplateModal = true;
} catch (e) {
this.showMessage('加载 Claude settings 失败', 'error');
}
}
};
}
14 changes: 11 additions & 3 deletions web-ui/modules/app.methods.codex-config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,7 @@ export function createCodexConfigMethods(options = {}) {
template = `${template.trimEnd()}\n\n${appendBlock}\n`;
}
this.configTemplateContent = template;
this.configTemplateContext = 'codex';
this.showConfigTemplateModal = true;
} catch (e) {
this.showMessage('加载模板失败', 'error');
Expand Down Expand Up @@ -807,9 +808,16 @@ export function createCodexConfigMethods(options = {}) {
const performApply = async () => {
this.configTemplateApplying = true;
try {
const res = await api('apply-config-template', {
template: this.configTemplateContent
});
let res;
if (this.configTemplateContext === 'claude') {
res = await api('apply-claude-settings-raw', {
content: this.configTemplateContent
});
} else {
res = await api('apply-config-template', {
template: this.configTemplateContent
});
}
Comment on lines +811 to +820
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Claude apply path is context-aware, but confirmation preview gating is not.

At Line 812 you route apply correctly, but applyConfigTemplate() still enforces the shared diff-preview gate (preview-config-template-diff) with no Claude context. In Claude mode this can block or mislead apply decisions based on the wrong target.

Suggested frontend-safe fix (bypass codex diff gate for Claude context)
 async applyConfigTemplate() {
     if (this.configTemplateApplying) {
         return;
     }
     if (!this.configTemplateContent || !this.configTemplateContent.trim()) {
         this.showMessage('模板不能为空', 'error');
         return;
     }

+    const isClaudeTemplate = this.configTemplateContext === 'claude';
     // Default to two-step confirmation when the setting is unset.
     // (The normalize helper lives in session-actions; keep a safe fallback here.)
-    const shouldUseTwoStepConfirm = normalizeConfigTemplateDiffConfirmEnabled(this.configTemplateDiffConfirmEnabled);
+    const shouldUseTwoStepConfirm = !isClaudeTemplate
+        && normalizeConfigTemplateDiffConfirmEnabled(this.configTemplateDiffConfirmEnabled);

     const performApply = async () => {
         this.configTemplateApplying = true;
         try {
             let res;
             if (this.configTemplateContext === 'claude') {
                 res = await api('apply-claude-settings-raw', {
                     content: this.configTemplateContent
                 });
             } else {
                 res = await api('apply-config-template', {
                     template: this.configTemplateContent
                 });
             }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/modules/app.methods.codex-config.mjs` around lines 811 - 820, The
preview gating logic for config applies is using the shared
"preview-config-template-diff" path even when configTemplateContext ===
'claude', causing Claude-mode applies to be blocked/misrepresented; update
applyConfigTemplate() (or the code path that checks
preview-config-template-diff) to skip or bypass the shared diff-preview gate
when this.configTemplateContext === 'claude' (or alternatively propagate the
'claude' context so the preview check uses a Claude-aware gate), ensuring the
Claude branch that calls api('apply-claude-settings-raw', ...) is not forced
through the generic preview gate.

if (res.error) {
this.showMessage(res.error, 'error');
return;
Expand Down
26 changes: 26 additions & 0 deletions web-ui/partials/index/modals-basic.html
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,29 @@
</div>
</div>
</div>

<!-- Codex 轮询池控制模态框 -->
<div v-if="showCodexBridgePoolModal" class="modal-overlay" @click.self="showCodexBridgePoolModal = false">
<div class="modal modal-bridge-pool" role="dialog" aria-modal="true" aria-labelledby="codex-bridge-pool-modal-title">
<div class="modal-title" id="codex-bridge-pool-modal-title">
<svg class="modal-title-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18"><circle cx="6" cy="6" r="2"/><circle cx="18" cy="6" r="2"/><circle cx="12" cy="18" r="2"/><path d="M6 8v4h6v4"/><path d="M18 8v4h-6v4"/></svg>
轮询池设置
</div>
<div class="bridge-pool-modal-hint">勾选参与负载均衡的提供商</div>
<div v-if="localBridgeCandidateProviders().length === 0" class="bridge-pool-empty">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="16" height="16"><path d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0z"/></svg>
<span>暂无可用上游 provider,请先添加直连 provider</span>
</div>
<div v-else class="bridge-pool-list">
<label v-for="cp in localBridgeCandidateProviders()" :key="cp.name" class="bridge-pool-item">
<span class="bridge-pool-item-name">{{ cp.name }}</span>
<span class="bridge-pool-item-status" :class="{ active: !isLocalBridgeExcluded(cp.name) }">{{ isLocalBridgeExcluded(cp.name) ? '未启用' : '已启用' }}</span>
<input type="checkbox" :checked="!isLocalBridgeExcluded(cp.name)" @change="toggleLocalBridgeExcluded(cp.name)" />
</label>
</div>
<div class="btn-group">
<button class="btn btn-confirm" @click="showCodexBridgePoolModal = false">{{ t('common.close') }}</button>
</div>
</div>
</div>

36 changes: 1 addition & 35 deletions web-ui/partials/index/panel-config-claude.html
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
:placeholder="t('claude.model.placeholder')"
>
<div class="config-template-hint">{{ t('claude.model.hint') }}</div>
<button class="btn-tool btn-template-editor" @click="openClaudeConfigTemplateEditor" :disabled="loading || !!initError">{{ t('config.template.openEditor') }}</button>
</div>

<div class="selector-section">
Expand All @@ -97,20 +98,6 @@
</div>

<div class="card-list">
<div :class="['card', { active: currentClaudeConfig === 'claude-local' }]" @click="currentClaudeConfig = 'claude-local'" @keydown.enter.self.prevent="currentClaudeConfig = 'claude-local'" @keydown.space.self.prevent="currentClaudeConfig = 'claude-local'" tabindex="0" role="button" :aria-current="currentClaudeConfig === 'claude-local' ? 'true' : null">
<div class="card-leading">
<div class="card-icon">L</div>
<div class="card-content">
<div class="card-title">
<span>local</span>
<span class="provider-readonly-badge">{{ t('config.badge.system') }}</span>
</div>
</div>
</div>
<div class="card-trailing">
<span :class="['pill', claudeLocalBridgeConfigured() ? 'configured' : 'empty']">{{ claudeLocalBridgeConfigured() ? t('claude.configured') : t('claude.notConfigured') }}</span>
</div>
</div>
<div v-for="(config, name) in claudeConfigs" :key="name" :class="['card', { active: currentClaudeConfig === name }]" @click="applyClaudeConfig(name)" @keydown.enter.self.prevent="applyClaudeConfig(name)" @keydown.space.self.prevent="applyClaudeConfig(name)" tabindex="0" role="button" :aria-current="currentClaudeConfig === name ? 'true' : null">
<div class="card-leading">
<div class="card-icon">{{ name.charAt(0).toUpperCase() }}</div>
Expand Down Expand Up @@ -141,26 +128,5 @@
</div>
</div>

<div v-if="currentClaudeConfig === 'claude-local'" class="bridge-pool-panel">
<div class="bridge-pool-header">
<span class="bridge-pool-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><circle cx="6" cy="6" r="2"/><circle cx="18" cy="6" r="2"/><circle cx="12" cy="18" r="2"/><path d="M6 8v4h6v4"/><path d="M18 8v4h-6v4"/></svg>
</span>
<span class="bridge-pool-title">{{ t('claude.localBridge.poolTitle') }}</span>
<span class="bridge-pool-hint">{{ t('claude.localBridge.poolHint') }}</span>
</div>
<div v-if="Object.keys(claudeConfigs || {}).length === 0" class="bridge-pool-empty">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="16" height="16"><path d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0z"/></svg>
<span>{{ t('claude.localBridge.noProviders') }}</span>
</div>
<div v-else class="bridge-pool-list">
<label v-for="(config, name) in claudeConfigs" :key="name" class="bridge-pool-item">
<span class="bridge-pool-item-name">{{ name }}</span>
<span class="bridge-pool-item-status" :class="{ active: !isClaudeLocalBridgeExcluded(name) }">{{ isClaudeLocalBridgeExcluded(name) ? t('claude.localBridge.disabled') : t('claude.localBridge.enabled') }}</span>
<input type="checkbox" :checked="!isClaudeLocalBridgeExcluded(name)" @change="toggleClaudeLocalBridgeExcluded(name)" />
</label>
</div>
</div>

</template>
</div>
28 changes: 7 additions & 21 deletions web-ui/partials/index/panel-config-codex.html
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@
<div class="card-leading">
<div class="card-icon">{{ provider.name.charAt(0).toUpperCase() }}<span v-if="isTransformProvider(provider)" class="card-icon-dot" title="通过内建转换适配"></span></div>
<div class="card-content">
<div v-if="provider.name === 'local'" class="bridge-pool-summary">
<svg class="bridge-pool-summary-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="12" height="12"><circle cx="6" cy="6" r="2"/><circle cx="18" cy="6" r="2"/><circle cx="12" cy="18" r="2"/><path d="M6 8v4h6v4"/><path d="M18 8v4h-6v4"/></svg>
<span class="bridge-pool-summary-text">已启用 {{ localBridgeCandidateProviders().filter(cp => !isLocalBridgeExcluded(cp.name)).length }} / {{ localBridgeCandidateProviders().length }}</span>
</div>
<div class="card-title">
<span>{{ provider.name }}</span>
<span v-if="provider.readOnly" class="provider-readonly-badge">{{ t('config.badge.system') }}</span>
Expand All @@ -136,6 +140,9 @@
<span v-if="speedResults[provider.name]" :class="['latency', speedResults[provider.name].ok ? 'ok' : 'error']">{{ formatLatency(speedResults[provider.name]) }}</span>
<span :class="['pill', providerPillConfigured(provider) ? 'configured' : 'empty']">{{ providerPillText(provider) }}</span>
<div class="card-actions" @click.stop>
<button v-if="provider.name === 'local'" class="card-action-btn bridge-pool-trigger" @click="showCodexBridgePoolModal = true" :aria-label="'轮询池设置'" :title="'轮询池设置'">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="6" cy="6" r="2"/><circle cx="18" cy="6" r="2"/><circle cx="12" cy="18" r="2"/><path d="M6 8v4h6v4"/><path d="M18 8v4h-6v4"/></svg>
</button>
<button class="card-action-btn" :class="{ loading: speedLoading[provider.name] }" :disabled="!!speedLoading[provider.name]" @click="runSpeedTest(provider.name, { silent: true })" :aria-label="t('config.availabilityTestAria', { name: provider.name })" :title="t('config.availabilityTest')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
</button>
Expand Down Expand Up @@ -166,26 +173,5 @@
</div>
</div>
</div>

<div v-if="displayCurrentProvider === 'local'" class="bridge-pool-panel">
<div class="bridge-pool-header">
<span class="bridge-pool-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><circle cx="6" cy="6" r="2"/><circle cx="18" cy="6" r="2"/><circle cx="12" cy="18" r="2"/><path d="M6 8v4h6v4"/><path d="M18 8v4h-6v4"/></svg>
</span>
<span class="bridge-pool-title">轮询池</span>
<span class="bridge-pool-hint">勾选参与负载均衡的提供商</span>
</div>
<div v-if="localBridgeCandidateProviders().length === 0" class="bridge-pool-empty">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="16" height="16"><path d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0z"/></svg>
<span>暂无可用上游 provider,请先添加直连 provider</span>
</div>
<div v-else class="bridge-pool-list">
<label v-for="cp in localBridgeCandidateProviders()" :key="cp.name" class="bridge-pool-item">
<span class="bridge-pool-item-name">{{ cp.name }}</span>
<span class="bridge-pool-item-status" :class="{ active: !isLocalBridgeExcluded(cp.name) }">{{ isLocalBridgeExcluded(cp.name) ? '未启用' : '已启用' }}</span>
<input type="checkbox" :checked="!isLocalBridgeExcluded(cp.name)" @change="toggleLocalBridgeExcluded(cp.name)" />
</label>
</div>
</div>
</template>
</div>
69 changes: 69 additions & 0 deletions web-ui/styles/bridge-pool.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,43 @@
============================================ */


/* ---- 摘要状态(在卡片标题上方) ---- */
.bridge-pool-summary {
display: flex;
align-items: center;
gap: 5px;
margin-bottom: 4px;
}

.bridge-pool-summary-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
border-radius: 5px;
background: var(--color-brand-light);
color: var(--color-brand-dark);
flex-shrink: 0;
}

.bridge-pool-summary-text {
font-size: 11px;
font-weight: 600;
color: var(--color-text-muted);
letter-spacing: -0.01em;
}

/* ---- 轮询池触发按钮 ---- */
.bridge-pool-trigger {
color: var(--color-brand);
background: rgba(200, 121, 99, 0.08);
}

.bridge-pool-trigger:hover {
background: rgba(200, 121, 99, 0.16);
}

.bridge-pool-panel {
margin-top: 18px;
padding: 18px 20px 16px;
Expand Down Expand Up @@ -195,3 +232,35 @@
padding: 10px 12px 10px 14px;
}
}

/* ---- 模态框中的轮询池 ---- */
.modal-bridge-pool {
max-width: 380px;
}

.modal-title-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 6px;
background: var(--color-brand-light);
color: var(--color-brand-dark);
margin-right: 8px;
vertical-align: middle;
}

.bridge-pool-modal-hint {
font-size: 12px;
color: var(--color-text-muted);
margin-bottom: 14px;
padding-bottom: 12px;
border-bottom: 1px solid var(--color-border-soft);
}

.modal-bridge-pool .bridge-pool-list {
max-height: 320px;
overflow-y: auto;
margin-bottom: 16px;
}
Loading