diff --git a/ccw/src/templates/dashboard-css/31-api-settings.css b/ccw/src/templates/dashboard-css/31-api-settings.css index c5475386..29dc1a4e 100644 --- a/ccw/src/templates/dashboard-css/31-api-settings.css +++ b/ccw/src/templates/dashboard-css/31-api-settings.css @@ -1122,6 +1122,55 @@ select.cli-input { opacity: 0.5; } +/* =========================== + Model Pools (High Availability) + =========================== */ + +.model-pools-list { + flex: 1; + overflow-y: auto; +} + +.pool-type-group { + margin-bottom: 1rem; +} + +.pool-type-header { + padding: 0.5rem 0.75rem; + font-size: 0.7rem; + font-weight: 600; + text-transform: uppercase; + color: hsl(var(--muted-foreground)); + background: hsl(var(--muted) / 0.3); + border-bottom: 1px solid hsl(var(--border)); +} + +.pool-item { + padding: 0.75rem; + cursor: pointer; + border-bottom: 1px solid hsl(var(--border)); + transition: background-color 0.15s; +} + +.pool-item:hover { + background: hsl(var(--muted) / 0.3); +} + +.pool-item.selected { + background: hsl(var(--primary) / 0.1); + border-left: 3px solid hsl(var(--primary)); +} + +.pool-item .pool-name { + font-weight: 500; + margin-bottom: 0.25rem; +} + +.pool-item .pool-target { + font-size: 0.75rem; + color: hsl(var(--muted-foreground)); +} + /* Responsive adjustments for tabs */ @media (max-width: 768px) { .sidebar-tab { diff --git a/ccw/src/templates/dashboard-js/views/api-settings.js b/ccw/src/templates/dashboard-js/views/api-settings.js index 3bbbda51..05bf9c71 100644 --- a/ccw/src/templates/dashboard-js/views/api-settings.js +++ b/ccw/src/templates/dashboard-js/views/api-settings.js @@ -2396,7 +2396,7 @@ function deleteModel(providerId, modelId, modelType) { }); }) .then(function() { - return loadApiSettings(); + return loadApiSettings(true); // Force refresh to get updated data }) .then(function() { if (selectedProviderId === providerId) { @@ -4091,23 +4091,21 @@ function renderModelPoolsList() { type === 'llm' ? t('apiSettings.llmPools') : t('apiSettings.rerankerPools'); - html += '
' + - '
' + - typeLabel + - '
'; + html += '
' + + '
' + typeLabel + '
'; pools.forEach(function(pool) { var isSelected = selectedPoolId === pool.id; var statusClass = pool.enabled ? 'status-enabled' : 'status-disabled'; var statusText = pool.enabled ? t('common.enabled') : t('common.disabled'); - html += '
' + + html += '
' + '
' + '
' + - '
' + escapeHtml(pool.name || pool.targetModel) + '
' + - '
' + escapeHtml(pool.targetModel) + '
' + + '
' + escapeHtml(pool.name || pool.targetModel) + '
' + + '
' + escapeHtml(pool.targetModel) + '
' + '
' + - '' + statusText + '' + + '' + statusText + '' + '
' + '
'; }); diff --git a/ccw/src/utils/path-resolver.ts b/ccw/src/utils/path-resolver.ts index db1a4eaa..25f3c22b 100644 --- a/ccw/src/utils/path-resolver.ts +++ b/ccw/src/utils/path-resolver.ts @@ -23,6 +23,7 @@ export interface ValidatePathOptions { /** * Resolve a path, handling ~ for home directory + * Also handles Windows drive-relative paths (e.g., "D:path" -> "D:\path") * @param inputPath - Path to resolve * @returns Absolute path */ @@ -34,6 +35,17 @@ export function resolvePath(inputPath: string): string { return join(homedir(), inputPath.slice(1)); } + // Handle Windows drive-relative paths (e.g., "D:path" without backslash) + // Pattern: single letter followed by colon, then immediately a non-slash character + // This converts "D:path" to "D:\path" to make it absolute + if (process.platform === 'win32' || /^[a-zA-Z]:/.test(inputPath)) { + const driveRelativeMatch = inputPath.match(/^([a-zA-Z]:)([^/\\].*)$/); + if (driveRelativeMatch) { + // Insert backslash after drive letter + inputPath = driveRelativeMatch[1] + '\\' + driveRelativeMatch[2]; + } + } + return resolve(inputPath); }