feat(desktop): open edit bubble on pick, bundle diff+description+annotated screenshot, signal picker-exited

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
程序员阿江(Relakkes) 2026-05-29 14:41:59 +08:00
parent 83a1a6a789
commit c99472222a
5 changed files with 38 additions and 12 deletions

File diff suppressed because one or more lines are too long

View File

@ -52,4 +52,12 @@ describe('subscribePreviewEvents', () => {
expect(() => listeners['preview://event']!({ payload: JSON.stringify({ v: 1, type: 'selection', payload: { pageUrl: 'http://x/' } }) })).not.toThrow()
expect(useBrowserPanelStore.getState().bySession['s1']!.pickerActive).toBe(false)
})
it('picker-exited event resets pickerActive', async () => {
useBrowserPanelStore.getState().open('s1', 'http://x/a')
useBrowserPanelStore.getState().setPicker('s1', true)
await subscribePreviewEvents('s1')
listeners['preview://event']!({ payload: JSON.stringify({ v: 1, type: 'picker-exited' }) })
expect(useBrowserPanelStore.getState().bySession['s1']!.pickerActive).toBe(false)
})
})

View File

@ -34,6 +34,9 @@ export async function subscribePreviewEvents(sessionId: string): Promise<() => v
: [],
})
}
else if (msg.type === 'picker-exited') {
store.setPicker(sessionId, false)
}
else if (msg.type === 'error') {
console.warn('[preview-agent]', msg)
}

View File

@ -2,6 +2,7 @@ import { createBridge } from './bridge'
import { captureToDataUrl, captureAnnotatedRegion } from './screenshot'
import { createPicker } from './picker'
import { buildElementMetadata } from './metadata'
import { createEditBubble } from './editBubble'
;(() => {
;(window as unknown as { __PREVIEW_AGENT__?: boolean }).__PREVIEW_AGENT__ = true
@ -22,9 +23,18 @@ import { buildElementMetadata } from './metadata'
})
let pickerOn = false
let activeBubble: { destroy: () => void } | null = null
const picker = createPicker({ onSelect: () => {} })
const emitSelection = async (el: Element) => {
const teardown = () => {
activeBubble?.destroy()
activeBubble = null
pickerOn = false
picker.exit()
bridge.send({ type: 'picker-exited' })
}
const emitSelection = async (el: Element, change?: unknown) => {
try {
const dataUrl = await captureAnnotatedRegion(el)
bridge.send({
@ -33,6 +43,7 @@ import { buildElementMetadata } from './metadata'
pageUrl: window.location.href,
sourceHint: document.title || undefined,
element: buildElementMetadata(el),
change,
screenshot: { dataUrl, kind: 'region' },
},
})
@ -40,7 +51,7 @@ import { buildElementMetadata } from './metadata'
}
bridge.on('enter-picker', () => { pickerOn = true; picker.enter() })
bridge.on('exit-picker', () => { pickerOn = false; picker.exit() })
bridge.on('exit-picker', () => { teardown() })
document.addEventListener('mousemove', (e) => {
if (!pickerOn) return
@ -49,13 +60,16 @@ import { buildElementMetadata } from './metadata'
}, true)
document.addEventListener('click', (e) => {
if (!pickerOn) return
if (!pickerOn || activeBubble) 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)
const el = picker.current()
pickerOn = false // stop hovering; keep highlight on the selected element while the bubble is open
if (!(el instanceof HTMLElement)) { teardown(); return }
activeBubble = createEditBubble(el, {
onConfirm: (change) => { teardown(); void emitSelection(el, change) },
onCancel: () => { teardown() },
})
}, true)
const onReady = () => { bridge.reportReady(); bridge.reportNavigated() }

View File

@ -4,6 +4,7 @@ export type AgentMessage =
| { type: 'error'; message: string }
| { type: 'selection'; payload: unknown } // M5 填充结构
| { type: 'screenshot'; dataUrl: string; kind: 'full' | 'viewport' | 'element' } // M4
| { type: 'picker-exited' }
export type HostMessage =
| { type: 'enter-picker' }