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
11 changes: 11 additions & 0 deletions web/src/design-system/helpers/clipboard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,15 @@ describe("copyToClipboard", () => {
it("does not report success where no clipboard mechanism exists at all", async () => {
expect(await copyToClipboard("openai:gpt-4o", undefined)).toBe(false)
})

it("removes the scratch textarea even when select() throws (#1149)", async () => {
// The textarea holds the plaintext, often a one-time API key, so a throw
// between the append and the removal leaves a credential in the DOM.
vi.spyOn(HTMLTextAreaElement.prototype, "select").mockImplementation(() => {
throw new Error("detached document")
})

expect(await copyToClipboard("provider secret", undefined)).toBe(false)
expect(document.querySelectorAll("textarea")).toHaveLength(0)
})
})
20 changes: 12 additions & 8 deletions web/src/design-system/helpers/clipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,20 +42,24 @@ function legacyCopy(text: string): boolean {
document.activeElement instanceof HTMLElement
? document.activeElement
: null
source.select()

let copied = false
try {
source.select()
copied = document.execCommand("copy")
} catch {
// Covers select() too, not just execCommand: a throw there is still a copy
// that did not happen, and the caller reads the return value to say so.
copied = false
} finally {
// In a finally: the textarea holds the plaintext, so it must not outlive a
// throw above it (#1149).
source.remove()
if (selection && previous) {
selection.removeAllRanges()
selection.addRange(previous)
}
previousFocus?.focus()
}

source.remove()
if (selection && previous) {
selection.removeAllRanges()
selection.addRange(previous)
}
previousFocus?.focus()
return copied
}
Loading