Merge remote-tracking branch 'origin/master' into mergebot/pr1016

This commit is contained in:
imccyu
2026-07-31 14:43:03 +08:00
145 changed files with 2856 additions and 608 deletions
@@ -3,6 +3,7 @@
flex-direction: column;
gap: 12px;
max-width: 720px;
color: var(--dsw-alias-label-primary);
}
.title {
@@ -14,13 +15,13 @@
.intro {
margin: 0;
font-size: 13px;
color: var(--text-tertiary, #888);
color: var(--dsw-alias-label-tertiary);
}
.notice {
margin: 0;
font-size: 12px;
color: var(--text-warning, #a15c00);
color: var(--dsw-alias-state-warn-label);
}
.rows {
@@ -33,13 +34,13 @@
}
.rowCard {
border: 1px solid var(--border, #e2e2e2);
border: 1px solid var(--dsw-alias-border-l2);
border-radius: 12px;
padding: 12px 14px;
display: flex;
flex-direction: column;
gap: 12px;
background: var(--surface, #fff);
background: var(--dsw-alias-bg-layer-3);
}
.rowHead {
@@ -53,58 +54,27 @@
font-weight: 600;
}
.badges {
display: inline-flex;
gap: 6px;
flex: 1;
}
.badgeOk {
display: inline-flex;
align-items: center;
gap: 5px;
color: var(--text-success, #0a7d33);
font-size: 12px;
}
.badgeOk::before {
content: '';
width: 6px;
height: 6px;
border-radius: 999px;
background: currentcolor;
}
.badgeMuted {
color: var(--text-tertiary, #999);
font-size: 12px;
}
.badgeWarn {
color: var(--text-warning, #a15c00);
font-size: 12px;
}
.rowActions {
display: inline-flex;
gap: 8px;
margin-left: auto;
}
.primaryButton {
border: none;
border-radius: 999px;
padding: 8px 18px;
background: var(--accent-strong, #111);
color: var(--text-inverse, #fff);
background: var(--dsw-alias-button-primary-fill);
color: var(--dsw-alias-label-primary-foreground);
font: inherit;
cursor: pointer;
}
.secondaryButton {
border: 1px solid var(--border, #d9d9d9);
border: 1px solid var(--dsw-alias-border-l2);
border-radius: 999px;
padding: 6px 14px;
background: var(--surface, #fff);
background: var(--dsw-alias-bg-layer-3);
color: inherit;
font: inherit;
cursor: pointer;
@@ -113,7 +83,7 @@
.dangerButton {
border: none;
background: none;
color: var(--text-danger, #c0392b);
color: var(--dsw-alias-state-error-primary);
font: inherit;
cursor: pointer;
}
@@ -126,9 +96,9 @@
}
.editor {
border: 1px solid var(--border, #e6e6e6);
border: 1px solid var(--dsw-alias-border-l2);
border-radius: 12px;
background: var(--surface-secondary, #f7f7f8);
background: var(--dsw-alias-bg-layer-2);
padding: 14px 16px;
display: flex;
flex-direction: column;
@@ -148,7 +118,7 @@
.editorRoute {
font-size: 12px;
color: var(--text-tertiary, #999);
color: var(--dsw-alias-label-tertiary);
}
.field {
@@ -163,14 +133,14 @@
gap: 10px;
font-size: 12px;
font-weight: 500;
color: var(--text-secondary, #555);
color: var(--dsw-alias-label-secondary);
}
.linkButton {
border: none;
background: none;
padding: 0;
color: var(--text-tertiary, #888);
color: var(--dsw-alias-label-tertiary);
font: inherit;
font-size: 12px;
text-decoration: underline;
@@ -185,7 +155,7 @@
.advancedHint {
margin: 0;
font-size: 12px;
color: var(--text-tertiary, #999);
color: var(--dsw-alias-label-tertiary);
}
.editorActions {
@@ -202,12 +172,12 @@
.addButton {
align-self: flex-start;
border: 1px solid var(--border, #d9d9d9);
border: 1px solid var(--dsw-alias-border-l2);
border-radius: 999px;
padding: 8px 16px;
font: inherit;
font-size: 13px;
background: var(--surface, #fff);
background: var(--dsw-alias-bg-layer-3);
color: inherit;
cursor: pointer;
}
@@ -219,9 +189,9 @@
.addCard,
.setupCard {
border: 1px solid var(--border, #e6e6e6);
border: 1px solid var(--dsw-alias-border-l2);
border-radius: 12px;
background: var(--surface-secondary, #f7f7f8);
background: var(--dsw-alias-bg-layer-3);
padding: 14px 16px;
display: flex;
flex-direction: column;
@@ -237,7 +207,7 @@
}
.customized {
border-top: 1px solid var(--border, #ececec);
border-top: 1px solid var(--dsw-alias-border-l2);
padding-top: 10px;
}
@@ -245,7 +215,7 @@
cursor: pointer;
font-size: 12px;
font-weight: 500;
color: var(--text-secondary, #555);
color: var(--dsw-alias-label-secondary);
list-style: revert;
}
@@ -259,25 +229,38 @@
.input {
box-sizing: border-box;
padding: 9px 12px;
border: 1px solid var(--border, #d9d9d9);
border: 1px solid var(--dsw-alias-border-l2);
border-radius: 10px;
font: inherit;
font-size: 13px;
background: var(--surface, #fff);
color: inherit;
background: var(--dsw-alias-bg-layer-1);
color: var(--dsw-alias-label-primary);
}
.input:focus {
outline: none;
border-color: var(--accent-strong, #111);
border-color: var(--dsw-alias-brand-primary);
}
.input::placeholder {
color: var(--text-tertiary, #aaa);
color: var(--dsw-alias-label-dimmed);
}
.error {
margin: 0;
font-size: 12px;
color: var(--text-danger, #c0392b);
color: var(--dsw-alias-state-error-primary);
}
.deleteDialog {
width: min(480px, 100%);
}
.deleteConfirm:not(:disabled) {
border-color: var(--dsw-alias-state-error-primary);
color: var(--dsw-alias-state-error-primary);
}
.deleteConfirm:hover:not(:disabled) {
background: var(--dsw-alias-interactive-bg-hover-danger);
}
@@ -4,13 +4,15 @@
* card at a time. A whole-section provider without a configured key (the
* unconfigured DeepSeek posture) renders as its open setup card instead of a
* row; the add flow is a card carrying the dormant-provider select. Every
* mutation writes through the wire; the page re-renders from the pushed
* invalidations or the post-apply reload.
* mutation writes through the wire, while a provider removal first requires
* confirmation; the page re-renders from pushed invalidations or the
* post-apply reload.
*/
import { useState } from 'react'
import type { ReactNode } from 'react'
import type { IApiClient } from '@deepseek-ai/dsh-client-connection/client'
import { Button, Modal } from '@deepseek-ai/dsh-client-ui-primitives'
import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-web-react'
import { messageOf } from './store.ts'
import type { ModelsSettingsState, ModelsSettingsStore, ProviderRow } from './store.ts'
@@ -114,6 +116,8 @@ function Loaded({ injected }: { injected: ModelsSectionInjected }): ReactNode {
const state = injected.useSnapshot(snapshot => snapshot)
const [editing, setEditing] = useState<EditorTarget | undefined>(undefined)
const [adding, setAdding] = useState(false)
const [deleteTarget, setDeleteTarget] = useState<EditorTarget | undefined>(undefined)
const [deleting, setDeleting] = useState(false)
const closeEditor = (changed: boolean): void => {
setEditing(undefined)
@@ -121,6 +125,26 @@ function Loaded({ injected }: { injected: ModelsSectionInjected }): ReactNode {
if (changed) void controller.load()
}
const closeDelete = (): void => {
if (deleting) return
setDeleteTarget(undefined)
}
const confirmDelete = (): void => {
/* v8 ignore next -- the action only renders with a target and is disabled while a deletion is pending */
if (deleteTarget === undefined || deleting) return
setDeleting(true)
void removeProviderProfile(api, controller, deleteTarget)
.then((failure) => {
if (failure !== undefined) {
controller.fail(failure)
return
}
setDeleteTarget(undefined)
})
.finally(() => { setDeleting(false) })
}
if (state.status === 'idle') void controller.load()
if (state.status === 'error') {
/* v8 ignore next -- an error status always carries text; the fallback satisfies the nullable type */
@@ -174,11 +198,6 @@ function Loaded({ injected }: { injected: ModelsSectionInjected }): ReactNode {
<li key={row.entry.provider} className={styles['rowCard']}>
<div className={styles['rowHead']}>
<span className={styles['rowName']}>{row.entry.displayName}</span>
<span className={styles['badges']}>
{row.entry.active
? <span className={styles['badgeOk']}>{t('active')}</span>
: <span className={styles['badgeMuted']}>{t('dormant')}</span>}
</span>
<span className={styles['rowActions']}>
<button
type="button"
@@ -193,11 +212,7 @@ function Loaded({ injected }: { injected: ModelsSectionInjected }): ReactNode {
type="button"
className={styles['dangerButton']}
disabled={!state.writable}
onClick={() => {
void removeProviderProfile(api, controller, target).then((failure) => {
if (failure !== undefined) controller.fail(failure)
})
}}
onClick={() => { setDeleteTarget(target) }}
>
{t('remove')}
</button>
@@ -276,6 +291,29 @@ function Loaded({ injected }: { injected: ModelsSectionInjected }): ReactNode {
</button>
)}
</div>
<Modal
open={deleteTarget !== undefined}
onClose={closeDelete}
title={t('deleteTitle')}
closeLabel={t('close')}
description={t('deleteDescription')}
className={styles['deleteDialog'] as string}
footer={(
<>
<Button variant="outline" autoFocus disabled={deleting} onClick={closeDelete}>
{t('cancel')}
</Button>
<Button
variant="outline"
className={styles['deleteConfirm']}
disabled={deleting}
onClick={confirmDelete}
>
{deleting ? t('deleting') : t('deleteConfirm')}
</Button>
</>
)}
/>
</div>
)
}
@@ -5,12 +5,15 @@ export const en = {
nav: 'Models',
title: 'Models',
intro: 'Enter your API keys to use models from the following providers.',
active: 'Active',
dormant: 'Inactive',
edit: 'Edit',
remove: 'Delete',
deleteTitle: 'Delete model provider?',
deleteDescription: 'Deleting this model provider removes its configuration. You will not be able to use its models until you add the provider again.',
deleteConfirm: 'Delete provider',
deleting: 'Deleting provider…',
add: 'Add provider',
provider: 'Provider',
close: 'Close',
cancel: 'Cancel',
apply: 'Apply',
applying: 'Applying…',
@@ -42,12 +45,15 @@ export const zh: typeof en = {
nav: '模型',
title: '模型',
intro: '填入各提供方的 API 密钥即可使用其模型。',
active: '已启用',
dormant: '未启用',
edit: '编辑',
remove: '删除',
deleteTitle: '删除模型提供方?',
deleteDescription: '删除此模型提供方会移除其配置。在重新添加前,你将无法继续使用其模型。',
deleteConfirm: '删除提供方',
deleting: '正在删除提供方…',
add: '添加提供方',
provider: '提供方',
close: '关闭',
cancel: '取消',
apply: '保存',
applying: '保存中…',