Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions src/renderer/settings/components/ProviderApiConfig.vue
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,17 @@
<div v-if="showKeySummary" class="flex w-full items-center gap-2">
<div
data-testid="provider-api-key-summary"
class="flex h-9 flex-1 items-center rounded-md border border-input bg-muted px-3 text-sm text-muted-foreground"
class="group flex h-9 flex-1 items-center gap-1 rounded-md border border-input bg-muted px-3 text-sm text-muted-foreground"
>
<span class="truncate font-mono">{{ maskedApiKey }}</span>
<span class="min-w-0 flex-1 truncate font-mono">{{ maskedApiKey }}</span>
<DcCopyButton
data-testid="provider-copy-key-button"
:copy-text="provider.apiKey"
variant="ghost"
size="icon-xs"
:tooltip="t('common.copy')"
class="shrink-0 opacity-0 pointer-events-none transition-opacity duration-[var(--dc-motion-fast)] focus-visible:opacity-100 focus-visible:pointer-events-auto group-hover:opacity-100 group-hover:pointer-events-auto"
/>
</div>
<DcButton
data-testid="provider-update-key-button"
Expand Down Expand Up @@ -243,7 +251,7 @@ import { computed, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { Label } from '@shadcn/components/ui/label'
import { Input } from '@shadcn/components/ui/input'
import { DcButton } from '@dc-ui/components/button'
import { DcButton, DcCopyButton } from '@dc-ui/components/button'
import {
Tooltip,
TooltipContent,
Expand Down Expand Up @@ -308,6 +316,8 @@ const isEditingKey = ref(false)
const baseUrlUnlocked = ref(false)
// After setup the stored key renders as a masked summary; the full secret is
// never shown again — replacing it goes through the explicit Update key action.
// The copy button writes the plaintext key to the clipboard as an explicit
// user action without ever displaying it on screen.
const showKeySummary = computed(() => !isEditingKey.value && Boolean(props.provider.apiKey?.trim()))
const maskedApiKey = computed(() => {
const key = props.provider.apiKey?.trim() ?? ''
Expand Down
40 changes: 38 additions & 2 deletions test/renderer/components/ProviderApiConfig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,19 @@ const buttonStub = defineComponent({
template: '<button v-bind="$attrs" type="button" @click="$emit(\'click\')"><slot /></button>'
})

const copyButtonStub = defineComponent({
name: 'CopyButton',
inheritAttrs: false,
emits: ['copied', 'error'],
props: {
copyText: {
type: String,
default: ''
}
},
template: '<button v-bind="$attrs" type="button" @click="$emit(\'copied\')"><slot /></button>'
})

const labelStub = defineComponent({
name: 'Label',
inheritAttrs: false,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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' })
Expand Down Expand Up @@ -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'),
Expand Down