mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-19 13:33:35 +08:00
feat(desktop): wire screenshot capture button to composer prefill
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
26018fa40b
commit
acbdd7f274
File diff suppressed because one or more lines are too long
@ -7,7 +7,7 @@ beforeAll(() => {
|
||||
})
|
||||
|
||||
const { bridge } = vi.hoisted(() => ({
|
||||
bridge: { open: vi.fn(), navigate: vi.fn(), setBounds: vi.fn(), setVisible: vi.fn(), close: vi.fn() },
|
||||
bridge: { open: vi.fn(), navigate: vi.fn(), setBounds: vi.fn(), setVisible: vi.fn(), close: vi.fn(), eval: vi.fn() },
|
||||
}))
|
||||
vi.mock('../../lib/previewBridge', () => ({ previewBridge: bridge }))
|
||||
vi.mock('@tauri-apps/api/event', () => ({ listen: () => Promise.resolve(() => {}) }))
|
||||
@ -43,4 +43,11 @@ describe('BrowserSurface', () => {
|
||||
unmount()
|
||||
expect(bridge.setVisible).toHaveBeenLastCalledWith(false)
|
||||
})
|
||||
|
||||
it('截图 button triggers a capture via preview_eval', () => {
|
||||
useBrowserPanelStore.getState().open('s1', 'http://localhost:5173/')
|
||||
render(<BrowserSurface sessionId="s1" />)
|
||||
fireEvent.click(screen.getByLabelText('截图'))
|
||||
expect(bridge.eval).toHaveBeenCalledWith(expect.stringContaining('capture'))
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { useEffect, useLayoutEffect, useRef } from 'react'
|
||||
import { Camera } from 'lucide-react'
|
||||
import { BrowserAddressBar } from './BrowserAddressBar'
|
||||
import { computeWebviewBounds } from './computeWebviewBounds'
|
||||
import { previewBridge } from '../../lib/previewBridge'
|
||||
@ -56,6 +57,15 @@ export function BrowserSurface({ sessionId }: { sessionId: string }) {
|
||||
onForward={() => { store.goForward(sessionId); previewBridge.navigate(useBrowserPanelStore.getState().bySession[sessionId]!.url) }}
|
||||
onReload={() => previewBridge.navigate(session.url)}
|
||||
/>
|
||||
<div className="flex items-center gap-1 border-b px-2 py-1">
|
||||
<button
|
||||
aria-label="截图"
|
||||
className="rounded p-1 hover:bg-muted"
|
||||
onClick={() => previewBridge.eval(`window.__PREVIEW_BRIDGE__?.handleHostRaw('{"v":1,"type":"capture","kind":"full"}')`)}
|
||||
>
|
||||
<Camera size={16} />
|
||||
</button>
|
||||
</div>
|
||||
<div ref={hostRef} className="flex-1" data-testid="preview-host" />
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -21,6 +21,12 @@ describe('previewBridge', () => {
|
||||
expect(invoke).toHaveBeenCalledWith('preview_set_bounds', { bounds: { x: 0, y: 0, width: 10, height: 10 } })
|
||||
})
|
||||
|
||||
it('eval forwards js to preview_eval', async () => {
|
||||
const { previewBridge } = await import('./previewBridge')
|
||||
await previewBridge.eval('window.x=1')
|
||||
expect(invoke).toHaveBeenCalledWith('preview_eval', { js: 'window.x=1' })
|
||||
})
|
||||
|
||||
it('is a no-op outside the Tauri runtime', async () => {
|
||||
vi.resetModules()
|
||||
vi.doMock('./desktopRuntime', () => ({ isTauriRuntime: () => false }))
|
||||
|
||||
@ -13,4 +13,5 @@ export const previewBridge = {
|
||||
setBounds: (bounds: WebviewBounds) => call('preview_set_bounds', { bounds }),
|
||||
setVisible: (visible: boolean) => call('preview_set_visible', { visible }),
|
||||
close: () => call('preview_close'),
|
||||
eval: (js: string) => call('preview_eval', { js }),
|
||||
}
|
||||
|
||||
@ -5,6 +5,9 @@ vi.mock('@tauri-apps/api/event', () => ({
|
||||
listen: (name: string, cb: (e: { payload: string }) => void) => { listeners[name] = cb; return Promise.resolve(() => {}) },
|
||||
}))
|
||||
|
||||
const prefill = vi.hoisted(() => vi.fn())
|
||||
vi.mock('../stores/chatStore', () => ({ useChatStore: { getState: () => ({ queueComposerPrefill: prefill }) } }))
|
||||
|
||||
import { subscribePreviewEvents } from './previewEvents'
|
||||
import { useBrowserPanelStore } from '../stores/browserPanelStore'
|
||||
|
||||
@ -15,4 +18,12 @@ describe('subscribePreviewEvents', () => {
|
||||
listeners['preview://event']!({ payload: JSON.stringify({ v: 1, type: 'navigated', url: 'http://x/c', title: 'C' }) })
|
||||
expect(useBrowserPanelStore.getState().bySession['s1']!.url).toBe('http://x/c')
|
||||
})
|
||||
|
||||
it('screenshot event prefills composer with an image attachment', async () => {
|
||||
await subscribePreviewEvents('s1')
|
||||
listeners['preview://event']!({ payload: JSON.stringify({ v: 1, type: 'screenshot', dataUrl: 'data:image/png;base64,AAAA', kind: 'full' }) })
|
||||
expect(prefill).toHaveBeenCalledWith('s1', expect.objectContaining({
|
||||
attachments: [expect.objectContaining({ type: 'image', data: 'data:image/png;base64,AAAA' })],
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,13 +1,25 @@
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
import { useBrowserPanelStore } from '../stores/browserPanelStore'
|
||||
import { useChatStore } from '../stores/chatStore'
|
||||
|
||||
function kindLabel(kind?: string): string {
|
||||
if (kind === 'viewport') return 'viewport'
|
||||
if (kind === 'element') return 'element'
|
||||
return 'full'
|
||||
}
|
||||
|
||||
export async function subscribePreviewEvents(sessionId: string): Promise<() => void> {
|
||||
return listen<string>('preview://event', (e) => {
|
||||
let msg: { type?: string; url?: string; title?: string }
|
||||
let msg: { type?: string; url?: string; title?: string; dataUrl?: string; kind?: string }
|
||||
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 ?? '')
|
||||
else if (msg.type === 'ready') store.setReady(sessionId)
|
||||
// selection / screenshot 在 M4/M5 接入
|
||||
else if (msg.type === 'screenshot' && msg.dataUrl) {
|
||||
useChatStore.getState().queueComposerPrefill(sessionId, {
|
||||
text: '',
|
||||
attachments: [{ type: 'image', name: `screenshot-${kindLabel(msg.kind)}.png`, mimeType: 'image/png', data: msg.dataUrl }],
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -10,8 +10,10 @@ export function createBridge(deps: Deps) {
|
||||
reportNavigated: () => send({ type: 'navigated', url: deps.location.href, title: deps.title }),
|
||||
reportError: (message: string) => send({ type: 'error', message }),
|
||||
send,
|
||||
on(type: HostMessage['type'], fn: (m: HostMessage) => void) {
|
||||
const arr = handlers.get(type) ?? []; arr.push(fn); handlers.set(type, arr)
|
||||
on<T extends HostMessage['type']>(type: T, fn: (m: Extract<HostMessage, { type: T }>) => void) {
|
||||
const arr = handlers.get(type) ?? []
|
||||
arr.push(fn as (m: HostMessage) => void)
|
||||
handlers.set(type, arr)
|
||||
},
|
||||
handleHostRaw(raw: string) {
|
||||
const msg = parseHostMessage(raw)
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { createBridge } from './bridge'
|
||||
import { captureToDataUrl } from './screenshot'
|
||||
|
||||
;(() => {
|
||||
;(window as unknown as { __PREVIEW_AGENT__?: boolean }).__PREVIEW_AGENT__ = true
|
||||
@ -11,6 +12,12 @@ import { createBridge } from './bridge'
|
||||
|
||||
const bridge = createBridge({ postToHost, location: window.location, title: document.title })
|
||||
;(window as unknown as { __PREVIEW_BRIDGE__?: unknown }).__PREVIEW_BRIDGE__ = bridge
|
||||
;(window as unknown as Record<string, unknown>).__PREVIEW_AGENT_CAPTURE__ = captureToDataUrl
|
||||
|
||||
bridge.on('capture', async (m) => {
|
||||
try { bridge.send({ type: 'screenshot', dataUrl: await captureToDataUrl(m.kind), kind: m.kind }) }
|
||||
catch (e) { bridge.reportError(String(e)) }
|
||||
})
|
||||
|
||||
const onReady = () => { bridge.reportReady(); bridge.reportNavigated() }
|
||||
if (document.readyState !== 'loading') onReady()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user