diff --git a/desktop/src/preview-agent/metadata.test.ts b/desktop/src/preview-agent/metadata.test.ts
new file mode 100644
index 00000000..fca41d8d
--- /dev/null
+++ b/desktop/src/preview-agent/metadata.test.ts
@@ -0,0 +1,20 @@
+import { describe, expect, it, beforeEach, vi } from 'vitest'
+import { buildElementMetadata } from './metadata'
+
+beforeEach(() => { document.body.innerHTML = `
Hello World
` })
+
+describe('buildElementMetadata', () => {
+ it('captures tag/id/classes/text/selector and styles', () => {
+ const el = document.getElementById('t')!
+ el.getBoundingClientRect = () => ({ x: 1, y: 2, width: 3, height: 4, top: 2, left: 1, right: 4, bottom: 6, toJSON: () => ({}) }) as DOMRect
+ vi.spyOn(window, 'getComputedStyle').mockReturnValue({ color: 'rgb(0,0,0)', backgroundColor: 'rgba(0,0,0,0)', opacity: '1', fontFamily: 'serif', fontSize: '40px' } as unknown as CSSStyleDeclaration)
+ const m = buildElementMetadata(el)
+ expect(m.tag).toBe('h1')
+ expect(m.id).toBe('t')
+ expect(m.classes).toEqual(['a', 'b'])
+ expect(m.text).toBe('Hello World')
+ expect(m.boundingBox).toEqual({ x: 1, y: 2, w: 3, h: 4 })
+ expect(m.computedStyles.color).toBe('rgb(0,0,0)')
+ expect(m.selector).toBe('#t')
+ })
+})
diff --git a/desktop/src/preview-agent/metadata.ts b/desktop/src/preview-agent/metadata.ts
new file mode 100644
index 00000000..fe98a244
--- /dev/null
+++ b/desktop/src/preview-agent/metadata.ts
@@ -0,0 +1,25 @@
+import { buildSelector, buildNthPath } from './selector'
+
+export type ElementMetadata = {
+ selector: string; nthPath: string; tag: string; id?: string; classes: string[]
+ text?: string; boundingBox: { x: number; y: number; w: number; h: number }
+ computedStyles: Record; outerHtmlSnippet?: string
+}
+
+const STYLE_KEYS = ['color', 'backgroundColor', 'opacity', 'fontFamily', 'fontSize', 'fontWeight', 'textAlign', 'padding', 'margin'] as const
+
+export function buildElementMetadata(el: Element): ElementMetadata {
+ const cs = window.getComputedStyle(el)
+ const styles: Record = {}
+ for (const k of STYLE_KEYS) styles[k] = (cs as unknown as Record)[k] ?? ''
+ const r = el.getBoundingClientRect()
+ const sel = buildSelector(el)
+ return {
+ selector: sel, nthPath: buildNthPath(el), tag: el.tagName.toLowerCase(),
+ id: el.id || undefined, classes: Array.from(el.classList),
+ text: (el.textContent ?? '').trim().slice(0, 200) || undefined,
+ boundingBox: { x: r.x, y: r.y, w: r.width, h: r.height },
+ computedStyles: styles,
+ outerHtmlSnippet: sel.includes(':nth') ? el.outerHTML.slice(0, 300) : undefined,
+ }
+}