feat(desktop): bundle element selection into composer prefill

Wire createPicker into the preview-agent IIFE: enter/exit picker via host messages, bind DOM mousemove/click (capture phase) to picker hover/select, and on click emit a selection event carrying ElementMetadata + annotated screenshot. Route the selection event in previewEvents.ts through buildSelectionComposerText into queueComposerPrefill with the screenshot attachment.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
程序员阿江(Relakkes) 2026-05-29 13:33:55 +08:00
parent 96a288425f
commit dbef9f2d0a
4 changed files with 65 additions and 6 deletions

File diff suppressed because one or more lines are too long

View File

@ -26,4 +26,14 @@ describe('subscribePreviewEvents', () => {
attachments: [expect.objectContaining({ type: 'image', data: 'data:image/png;base64,AAAA' })],
}))
})
it('selection event prefills composer with text + annotated screenshot', async () => {
await subscribePreviewEvents('s1')
const payload = { pageUrl: 'http://x/', element: { selector: '#t', tag: 'h1', classes: [] }, change: { description: '改一下' }, screenshot: { dataUrl: 'data:image/png;base64,AAAA', kind: 'element' } }
listeners['preview://event']!({ payload: JSON.stringify({ v: 1, type: 'selection', payload }) })
expect(prefill).toHaveBeenCalledWith('s1', expect.objectContaining({
text: expect.stringContaining('#t'),
attachments: [expect.objectContaining({ type: 'image', data: 'data:image/png;base64,AAAA' })],
}))
})
})

View File

@ -1,6 +1,7 @@
import { listen } from '@tauri-apps/api/event'
import { useBrowserPanelStore } from '../stores/browserPanelStore'
import { useChatStore } from '../stores/chatStore'
import { buildSelectionComposerText, type SelectionPayload } from './selectionComposer'
function kindLabel(kind?: string): string {
if (kind === 'viewport') return 'viewport'
@ -10,7 +11,7 @@ function kindLabel(kind?: string): string {
export async function subscribePreviewEvents(sessionId: string): Promise<() => void> {
return listen<string>('preview://event', (e) => {
let msg: { type?: string; url?: string; title?: string; dataUrl?: string; kind?: string }
let msg: { type?: string; url?: string; title?: string; dataUrl?: string; kind?: string; payload?: unknown }
try { msg = JSON.parse(e.payload) } catch { return }
const store = useBrowserPanelStore.getState()
if (msg.type === 'navigated' && msg.url) store.setNavigated(sessionId, msg.url, msg.title ?? '')
@ -21,5 +22,14 @@ export async function subscribePreviewEvents(sessionId: string): Promise<() => v
attachments: [{ type: 'image', name: `screenshot-${kindLabel(msg.kind)}.png`, mimeType: 'image/png', data: msg.dataUrl }],
})
}
else if (msg.type === 'selection' && msg.payload) {
const p = msg.payload as SelectionPayload & { screenshot?: { dataUrl?: string; kind?: string } }
useChatStore.getState().queueComposerPrefill(sessionId, {
text: buildSelectionComposerText(p),
attachments: p.screenshot?.dataUrl
? [{ type: 'image', name: 'selection.png', mimeType: 'image/png', data: p.screenshot.dataUrl }]
: [],
})
}
})
}

View File

@ -1,5 +1,7 @@
import { createBridge } from './bridge'
import { captureToDataUrl } from './screenshot'
import { createPicker } from './picker'
import { buildElementMetadata } from './metadata'
;(() => {
;(window as unknown as { __PREVIEW_AGENT__?: boolean }).__PREVIEW_AGENT__ = true
@ -19,6 +21,43 @@ import { captureToDataUrl } from './screenshot'
catch (e) { bridge.reportError(String(e)) }
})
let pickerOn = false
const picker = createPicker({ onSelect: () => {} })
const emitSelection = async (el: Element) => {
try {
const dataUrl = await captureToDataUrl('element', el)
bridge.send({
type: 'selection',
payload: {
pageUrl: window.location.href,
sourceHint: document.title || undefined,
element: buildElementMetadata(el),
screenshot: { dataUrl, kind: 'element' },
},
})
} catch (e) { bridge.reportError(String(e)) }
}
bridge.on('enter-picker', () => { pickerOn = true; picker.enter() })
bridge.on('exit-picker', () => { pickerOn = false; picker.exit() })
document.addEventListener('mousemove', (e) => {
if (!pickerOn) return
const t = e.target
if (t instanceof Element) picker.hover(t)
}, true)
document.addEventListener('click', (e) => {
if (!pickerOn) return
e.preventDefault(); e.stopPropagation()
picker.select()
const el = picker.current() // capture BEFORE exit (exit clears current)
pickerOn = false
picker.exit()
if (el) void emitSelection(el)
}, true)
const onReady = () => { bridge.reportReady(); bridge.reportNavigated() }
if (document.readyState !== 'loading') onReady()
else document.addEventListener('DOMContentLoaded', onReady)