feat(agent): support pasting clipboard images (#739)

This commit is contained in:
Sebastian
2026-09-01 15:34:24 +08:00
committed by GitHub
parent 1aaff8f8cc
commit d7a6e9d142
2 changed files with 65 additions and 4 deletions
+22 -4
View File
@@ -1721,10 +1721,8 @@ function openFilePicker() {
fileInputRef.value?.click()
}
// 处理文件选择并生成待发送附件预览。
function handleFileSelection(event: Event) {
const input = event.target as HTMLInputElement
const files = Array.from(input.files || [])
// 将文件加入待发送附件并生成图片预览。
function addPendingAttachments(files: File[]) {
const nextAttachments = files.map(file => {
const kind = getFileKind(file)
@@ -1740,9 +1738,28 @@ function handleFileSelection(event: Event) {
})
pendingAttachments.value.push(...nextAttachments)
}
// 处理文件选择并生成待发送附件预览。
function handleFileSelection(event: Event) {
const input = event.target as HTMLInputElement
addPendingAttachments(Array.from(input.files || []))
input.value = ''
}
// 将剪贴板中的截图或复制图片加入待发送附件。
function handleInputPaste(event: ClipboardEvent) {
if (isBusy.value || recording.value) return
const images = Array.from(event.clipboardData?.items || [])
.filter(item => item.kind === 'file' && item.type.startsWith('image/'))
.map(item => item.getAsFile())
.filter((file): file is File => Boolean(file))
if (!images.length) return
event.preventDefault()
addPendingAttachments(images)
}
// 移除一条待发送附件。
function removePendingAttachment(id: string) {
const attachment = pendingAttachments.value.find(item => item.id === id)
@@ -2798,6 +2815,7 @@ onScopeDispose(() => {
:placeholder="inputPlaceholder"
@input="handleInputChange"
@keydown="handleInputKeydown"
@paste="handleInputPaste"
@compositionstart="handleCompositionStart"
@compositionend="handleCompositionEnd"
/>
@@ -519,6 +519,49 @@ describe('AgentAssistantPanel stream recovery', () => {
wrapper.unmount()
})
it('adds a pasted clipboard image to the pending attachments', async () => {
vi.stubGlobal('fetch', vi.fn(async () => createAgentResponse([])))
const createObjectURL = vi.fn(() => 'blob:pasted-image')
Object.defineProperty(URL, 'createObjectURL', { configurable: true, value: createObjectURL })
Object.defineProperty(URL, 'revokeObjectURL', { configurable: true, value: vi.fn() })
const screenshot = new File(['screenshot'], 'image.png', { type: 'image/png' })
const wrapper = shallowMount(AgentAssistantPanel, {
props: { modelValue: true },
global: {
stubs: {
AgentMarkdownContent: agentMarkdownContentStub,
IconBtn: { template: '<button><slot /></button>' },
PerfectScrollbar: { template: '<div><slot /></div>' },
VIcon: true,
},
},
})
await flushPromises()
const pasteEvent = new Event('paste', { bubbles: true, cancelable: true })
Object.defineProperty(pasteEvent, 'clipboardData', {
value: {
items: [
{
kind: 'file',
type: 'image/png',
getAsFile: () => screenshot,
},
],
},
})
wrapper.get('textarea').element.dispatchEvent(pasteEvent)
await flushPromises()
expect(pasteEvent.defaultPrevented).toBe(true)
expect(createObjectURL).toHaveBeenCalledWith(screenshot)
expect(wrapper.get('.agent-assistant-pending-file').text()).toContain('image.png')
wrapper.unmount()
Reflect.deleteProperty(URL, 'createObjectURL')
Reflect.deleteProperty(URL, 'revokeObjectURL')
})
it('renders protected delivery only as transient literal text and advertises the stream capability', async () => {
const protectedMarker = 'MP-PROTECTED-MARKER **not bold** <script>literal</script>'
const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {