diff --git a/src/renderer/settings/components/ProviderApiConfig.vue b/src/renderer/settings/components/ProviderApiConfig.vue
index a29263063..3564a1ffa 100644
--- a/src/renderer/settings/components/ProviderApiConfig.vue
+++ b/src/renderer/settings/components/ProviderApiConfig.vue
@@ -122,9 +122,17 @@
- {{ maskedApiKey }}
+ {{ maskedApiKey }}
+
!isEditingKey.value && Boolean(props.provider.apiKey?.trim()))
const maskedApiKey = computed(() => {
const key = props.provider.apiKey?.trim() ?? ''
diff --git a/test/renderer/components/ProviderApiConfig.test.ts b/test/renderer/components/ProviderApiConfig.test.ts
index 38f3760d5..a9842ecf0 100644
--- a/test/renderer/components/ProviderApiConfig.test.ts
+++ b/test/renderer/components/ProviderApiConfig.test.ts
@@ -41,6 +41,19 @@ const buttonStub = defineComponent({
template: ''
})
+const copyButtonStub = defineComponent({
+ name: 'CopyButton',
+ inheritAttrs: false,
+ emits: ['copied', 'error'],
+ props: {
+ copyText: {
+ type: String,
+ default: ''
+ }
+ },
+ template: ''
+})
+
const labelStub = defineComponent({
name: 'Label',
inheritAttrs: false,
@@ -114,7 +127,8 @@ async function setup(options?: {
Input: createInputStub()
}))
vi.doMock('@dc-ui/components/button', () => ({
- DcButton: buttonStub
+ DcButton: buttonStub,
+ DcCopyButton: copyButtonStub
}))
vi.doMock('@shadcn/components/ui/label', () => ({
Label: labelStub
@@ -416,6 +430,25 @@ describe('ProviderApiConfig', () => {
expect((input.element as HTMLInputElement).value).toBe('')
})
+ it('renders a hover-revealed copy button in the masked key summary', async () => {
+ const { wrapper } = await setup({
+ provider: createProvider({ apiKey: 'sk-1234567890abcd' })
+ })
+
+ const summary = wrapper.get('[data-testid="provider-api-key-summary"]')
+ const copyButton = wrapper.get('[data-testid="provider-copy-key-button"]')
+
+ // The button lives inside the summary and stays hidden until hovered or focused.
+ expect(summary.find('[data-testid="provider-copy-key-button"]').exists()).toBe(true)
+ expect(copyButton.classes()).toContain('opacity-0')
+ expect(copyButton.classes()).toContain('pointer-events-none')
+ expect(copyButton.classes()).toContain('group-hover:opacity-100')
+ expect(copyButton.classes()).toContain('group-hover:pointer-events-auto')
+ expect(copyButton.classes()).toContain('focus-visible:opacity-100')
+ expect(copyButton.attributes('tooltip')).toBe('common.copy')
+ expect(wrapper.findComponent(copyButtonStub).props('copyText')).toBe('sk-1234567890abcd')
+ })
+
it('keeps the stored key when the Update key editor is left empty', async () => {
const { wrapper } = await setup({
provider: createProvider({ apiKey: 'sk-1234567890abcd' })
@@ -504,7 +537,10 @@ describe('ProviderApiConfig', () => {
useModelCheckStore: () => ({ openDialog: vi.fn() })
}))
vi.doMock('@shadcn/components/ui/input', () => ({ Input: createInputStub() }))
- vi.doMock('@dc-ui/components/button', () => ({ DcButton: buttonStub }))
+ vi.doMock('@dc-ui/components/button', () => ({
+ DcButton: buttonStub,
+ DcCopyButton: copyButtonStub
+ }))
vi.doMock('@shadcn/components/ui/label', () => ({ Label: labelStub }))
vi.doMock('@shadcn/components/ui/tooltip', () => ({
Tooltip: passthrough('Tooltip'),