// Functions injected into a page the IDE agent controls. Each one runs through // chrome.scripting.executeScript in Chrome's isolated world, so every function // must be self-contained (no imports, no outer variables). They share one // global per document: the element map of the latest snapshot, which turns the // model's "@e5" back into the element it saw. // Accessibility-style outline of the page with refs for interactive elements. // full=false lists interactive elements and headings; full=true adds visible // text in reading order. Same-origin iframes and open shadow roots are walked. export function snapshotPage(options) { const full = Boolean(options && options.full) const limit = Math.max(2000, Math.min(Number(options && options.limit) || 15000, 40000)) const refs = new Map() const lines = [] let counter = 0, length = 0, omitted = 0 const ROLE_BY_TAG = { a: 'link', button: 'button', select: 'combobox', textarea: 'textbox', summary: 'button', h1: 'heading', h2: 'heading', h3: 'heading', h4: 'heading', h5: 'heading', h6: 'heading', img: 'img', iframe: 'iframe' } const INTERACTIVE = new Set(['button', 'link', 'textbox', 'searchbox', 'combobox', 'checkbox', 'radio', 'switch', 'tab', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'slider', 'spinbutton', 'treeitem', 'listbox']) const SKIP = new Set(['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEMPLATE', 'SVG', 'HEAD', 'META', 'LINK']) const clean = (value, max = 100) => String(value || '').replace(/\s+/g, ' ').trim().slice(0, max) const quote = value => JSON.stringify(value) function visible(element) { const rect = element.getBoundingClientRect() if (rect.width < 1 || rect.height < 1) return false const style = element.ownerDocument.defaultView.getComputedStyle(element) return style.visibility !== 'hidden' && style.display !== 'none' && style.opacity !== '0' } function inputRole(element) { const type = (element.getAttribute('type') || 'text').toLowerCase() if (['button', 'submit', 'reset', 'image'].includes(type)) return 'button' if (type === 'checkbox') return 'checkbox' if (type === 'radio') return 'radio' if (type === 'range') return 'slider' if (type === 'number') return 'spinbutton' if (type === 'search') return 'searchbox' if (type === 'hidden') return '' return 'textbox' } function roleOf(element) { const explicit = clean(element.getAttribute('role'), 40).split(' ')[0] if (explicit) return explicit const tag = element.tagName.toLowerCase() if (tag === 'input') return inputRole(element) if (tag === 'a') return element.hasAttribute('href') ? 'link' : '' if (element.isContentEditable && element.getAttribute('contenteditable') !== null) return 'textbox' return ROLE_BY_TAG[tag] || '' } function nameOf(element, role) { const doc = element.ownerDocument const labelled = element.getAttribute('aria-labelledby') if (labelled) { const text = labelled.split(/\s+/).map(id => doc.getElementById(id)?.textContent || '').join(' ') if (clean(text)) return clean(text) } const aria = element.getAttribute('aria-label') if (clean(aria)) return clean(aria) if (element.id && ['textbox', 'searchbox', 'combobox', 'checkbox', 'radio', 'slider', 'spinbutton', 'switch'].includes(role)) { const label = Array.from(doc.getElementsByTagName('label')).find(candidate => candidate.htmlFor === element.id) if (label && clean(label.textContent)) return clean(label.textContent) } const wrapping = element.closest('label') if (wrapping && wrapping !== element && clean(wrapping.textContent)) return clean(wrapping.textContent) if (element.tagName === 'IMG') return clean(element.getAttribute('alt')) if (element.tagName === 'INPUT' && ['button', 'submit', 'reset'].includes((element.type || '').toLowerCase())) { return clean(element.value || element.type) } const text = role === 'textbox' || role === 'searchbox' || role === 'combobox' ? '' : clean(element.innerText || element.textContent) return text || clean(element.getAttribute('title')) || clean(element.getAttribute('placeholder')) } function states(element, role) { const out = [] if (role === 'heading') out.push(`level=${element.tagName.match(/\d/)?.[0] || element.getAttribute('aria-level') || 2}`) if (element.disabled || element.getAttribute('aria-disabled') === 'true') out.push('disabled') if (element.checked || element.getAttribute('aria-checked') === 'true') out.push('checked') if (element.getAttribute('aria-expanded') === 'true') out.push('expanded') if (element.getAttribute('aria-selected') === 'true') out.push('selected') if (element.required) out.push('required') return out } function valueOf(element, role) { if (!['textbox', 'searchbox', 'combobox', 'spinbutton', 'slider'].includes(role)) return '' const type = (element.getAttribute('type') || '').toLowerCase() if (type === 'password') return element.value ? '••••' : '' if (element.tagName === 'SELECT') return clean(element.selectedOptions?.[0]?.textContent, 80) if (element.isContentEditable) return clean(element.innerText, 120) return clean(element.value, 120) } function push(line) { if (length + line.length + 1 > limit) { omitted++; return } lines.push(line) length += line.length + 1 } function describe(element, depth) { const role = roleOf(element) if (!role) return false const interactive = INTERACTIVE.has(role) || (element.tagName === 'INPUT' && role) if (!interactive && role !== 'heading' && !(full && (role === 'img' || role === 'iframe'))) return false if (!visible(element)) return false const name = nameOf(element, role) const parts = [`${' '.repeat(depth)}- ${role}`] if (name) parts.push(quote(name)) const flags = states(element, role) let ref = '' if (interactive) { ref = `e${++counter}` refs.set(ref, new WeakRef(element)) flags.push(`ref=${ref}`) } if (flags.length) parts.push(`[${flags.join('] [')}]`) const value = valueOf(element, role) if (value) parts.push(`(değer: ${quote(value)})`) if ((element.getAttribute('type') || '').toLowerCase() === 'password') parts.push('(şifre alanı: ajan yazamaz)') if (role === 'link' && full) { try { parts.push(`→ ${new URL(element.getAttribute('href'), element.baseURI).href.slice(0, 160)}`) } catch {} } push(parts.join(' ')) if (full && element.tagName === 'SELECT') { Array.from(element.options).slice(0, 20).forEach(option => push(`${' '.repeat(depth + 1)}- option ${quote(clean(option.textContent))}${option.selected ? ' [selected]' : ''}`)) } return interactive } function directText(element) { let text = '' for (const node of element.childNodes) if (node.nodeType === 3) text += node.textContent return clean(text, 300) } function walk(root, depth) { for (const element of root.children || []) { if (SKIP.has(element.tagName.toUpperCase())) continue if (element.getAttribute('aria-hidden') === 'true' || element.hidden) continue if (element.tagName === 'IFRAME') { let inner = null try { inner = element.contentDocument } catch {} if (inner?.body && visible(element)) { push(`${' '.repeat(depth)}- iframe ${quote(clean(element.title || element.name || ''))}`); walk(inner.body, depth + 1) } else if (visible(element)) push(`${' '.repeat(depth)}- iframe ${quote(clean(element.title || element.src, 80))} (farklı alan adı: içerik okunamaz)`) continue } const interactive = describe(element, depth) if (!interactive && full && visible(element)) { const text = directText(element) if (text && !roleOf(element)) push(`${' '.repeat(depth)}- text: ${text}`) } if (element.shadowRoot) walk(element.shadowRoot, depth) // A described control's own children rarely add meaning (button labels). if (!interactive || element.tagName === 'SELECT' || element.getAttribute('role') === 'listbox') walk(element, depth) } } walk(document.body || document.documentElement, 0) globalThis.__aiturkRefs = refs const header = [`Sayfa: ${clean(document.title, 200) || '(başlıksız)'}`, `Adres: ${location.href.slice(0, 300)}`] if (!lines.length) lines.push('(Görünür etkileşimli öğe yok. Sayfa yükleniyor olabilir; browser_snapshot ile tekrar bakın ya da full=true deneyin.)') if (omitted) lines.push(`… (${omitted} satır daha var; kısaltıldı. Sayfayı kaydırıp tekrar bakın.)`) return { snapshot: header.concat(lines).join('\n'), element_count: counter, url: location.href, title: document.title, truncated: omitted > 0 } } // Where to click/type for a ref, after scrolling it into view. Coordinates are // top-level viewport CSS pixels (same-origin iframe offsets added). export function locateRef(ref) { const key = String(ref || '').trim().replace(/^@/, '').replace(/^ref=/, '').replace(/^\[|\]$/g, '') const element = globalThis.__aiturkRefs?.get(key)?.deref() if (!element || !element.isConnected) { return { error: `Öğe bulunamadı (@${key}). Sayfa değişmiş olabilir; browser_snapshot ile yeni numaraları alın.` } } element.scrollIntoView({ block: 'center', inline: 'center', behavior: 'instant' }) const rect = element.getBoundingClientRect() let x = rect.left + rect.width / 2, y = rect.top + rect.height / 2 let view = element.ownerDocument.defaultView while (view && view !== window && view.frameElement) { const frame = view.frameElement.getBoundingClientRect() x += frame.left; y += frame.top view = view.parent } const type = (element.getAttribute('type') || '').toLowerCase() const autocomplete = (element.getAttribute('autocomplete') || '').toLowerCase() let covered = '' if (element.ownerDocument === document) { const top = document.elementFromPoint(x, y) if (top && top !== element && !element.contains(top) && !top.contains(element)) { covered = String(top.innerText || top.getAttribute('aria-label') || top.tagName).replace(/\s+/g, ' ').trim().slice(0, 80) } } return { x: Math.round(x), y: Math.round(y), tag: element.tagName.toLowerCase(), name: String(element.getAttribute('aria-label') || element.innerText || element.value || element.getAttribute('title') || '').replace(/\s+/g, ' ').trim().slice(0, 80), editable: element.isContentEditable || ['INPUT', 'TEXTAREA', 'SELECT'].includes(element.tagName), password: type === 'password', payment: /^cc-|^card|card-?number|cvc|cvv/.test(autocomplete) || /cvc|cvv|card.?number/i.test(element.name || ''), disabled: Boolean(element.disabled || element.getAttribute('aria-disabled') === 'true'), covered, } } // Focus a ref and select its current content so typed text replaces it. // Refuses password and payment-card fields: those stay with the user. export function prepareTyping(ref) { const key = String(ref || '').trim().replace(/^@/, '').replace(/^ref=/, '').replace(/^[|]$/g, '') const element = globalThis.__aiturkRefs?.get(key)?.deref() if (!element || !element.isConnected) { return { error: `Öğe bulunamadı (@${key}). Sayfa değişmiş olabilir; browser_snapshot ile yeni numaraları alın.` } } const type = (element.getAttribute('type') || '').toLowerCase() const autocomplete = (element.getAttribute('autocomplete') || '').toLowerCase() if (type === 'password' || autocomplete.includes('password')) { return { error: 'Şifre alanına ajan yazamaz. Bu adımı kullanıcıya bırakın.', handoff: true } } if (/^cc-|card-?number|cvc|cvv/.test(autocomplete) || /cvc|cvv|card.?number/i.test(element.name || '')) { return { error: 'Kart bilgisi alanına ajan yazamaz. Bu adımı kullanıcıya bırakın.', handoff: true } } const editable = element.isContentEditable || ['INPUT', 'TEXTAREA', 'SELECT'].includes(element.tagName) if (!editable) return { error: `@${key} bir yazı alanı değil (${element.tagName.toLowerCase()}).` } if (element.disabled || element.readOnly || element.getAttribute('aria-disabled') === 'true') { return { error: `@${key} şu an devre dışı ya da salt okunur.` } } element.scrollIntoView({ block: 'center', inline: 'center', behavior: 'instant' }) if (element.tagName === 'SELECT') return { select: true } element.focus() if (element.isContentEditable) { const range = element.ownerDocument.createRange() range.selectNodeContents(element) const selection = element.ownerDocument.getSelection() selection.removeAllRanges(); selection.addRange(range) } else if (typeof element.select === 'function' && element.tagName !== 'SELECT') { element.select() } return { focused: element.ownerDocument.activeElement === element } } // Script fallback when trusted input is unavailable: replace the value and fire // the events frameworks listen to. export function typeWithEvents(ref, text) { const key = String(ref || '').trim().replace(/^@/, '').replace(/^ref=/, '') const element = globalThis.__aiturkRefs?.get(key)?.deref() if (!element) return { error: `Öğe bulunamadı (@${key}).` } if (element.tagName === 'SELECT') { const wanted = String(text).trim().toLocaleLowerCase() const option = Array.from(element.options).find(item => item.textContent.trim().toLocaleLowerCase() === wanted || item.value.toLocaleLowerCase() === wanted) || Array.from(element.options).find(item => item.textContent.trim().toLocaleLowerCase().includes(wanted)) if (!option) return { error: `Listede "${String(text).slice(0, 80)}" seçeneği yok.` } element.value = option.value } else if (element.isContentEditable) { element.textContent = text } else { const proto = element.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype const setter = Object.getOwnPropertyDescriptor(proto, 'value')?.set setter ? setter.call(element, text) : (element.value = text) } element.dispatchEvent(new Event('input', { bubbles: true })) element.dispatchEvent(new Event('change', { bubbles: true })) return { value: element.isContentEditable ? element.textContent : element.value } } // Read back what a field holds after typing (password fields never echoed). export function readValue(ref) { const key = String(ref || '').trim().replace(/^@/, '').replace(/^ref=/, '') const element = globalThis.__aiturkRefs?.get(key)?.deref() if (!element) return { error: `Öğe bulunamadı (@${key}).` } return { value: String(element.isContentEditable ? element.innerText : element.value ?? '').slice(0, 2000) } } export function clickWithEvents(ref) { const key = String(ref || '').trim().replace(/^@/, '').replace(/^ref=/, '') const element = globalThis.__aiturkRefs?.get(key)?.deref() if (!element) return { error: `Öğe bulunamadı (@${key}).` } element.click() return { clicked: true } } // Scroll the page, or the scrollable area under the viewport centre when the // window itself does not move (single-page apps). export function scrollPage(direction) { const sign = direction === 'up' ? -1 : 1 const amount = Math.round(window.innerHeight * 0.8) * sign const before = window.scrollY window.scrollBy({ top: amount, behavior: 'instant' }) if (window.scrollY !== before) { return { scrolled: true, atEnd: sign > 0 ? window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 2 : window.scrollY <= 0 } } let element = document.elementFromPoint(window.innerWidth / 2, window.innerHeight / 2) while (element && element !== document.body) { const style = getComputedStyle(element) if (/(auto|scroll)/.test(style.overflowY) && element.scrollHeight > element.clientHeight) { const previous = element.scrollTop element.scrollBy({ top: amount, behavior: 'instant' }) if (element.scrollTop !== previous) { return { scrolled: true, atEnd: sign > 0 ? element.scrollTop + element.clientHeight >= element.scrollHeight - 2 : element.scrollTop <= 0 } } } element = element.parentElement } return { scrolled: false, atEnd: true } } export function listImages() { const images = [] for (const image of document.images) { if (images.length >= 100) break const rect = image.getBoundingClientRect() if (rect.width < 8 || rect.height < 8) continue const src = image.currentSrc || image.src || '' images.push({ src: src.startsWith('data:') ? `${src.slice(0, 60)}…` : src.slice(0, 500), alt: String(image.alt || '').slice(0, 200), width: Math.round(rect.width), height: Math.round(rect.height) }) } return { images, count: images.length, url: location.href, title: document.title } } // Numbered labels over the refs of the latest snapshot, for annotated // screenshots; call again with show=false to remove them. export function annotateRefs(show) { document.getElementById('__aiturk_labels')?.remove() if (!show) return { removed: true } const layer = document.createElement('div') layer.id = '__aiturk_labels' layer.style.cssText = 'position:fixed;inset:0;pointer-events:none;z-index:2147483647' const annotations = [] for (const [ref, weak] of globalThis.__aiturkRefs || []) { const element = weak.deref() if (!element || element.ownerDocument !== document) continue const rect = element.getBoundingClientRect() if (rect.bottom < 0 || rect.top > innerHeight || rect.right < 0 || rect.left > innerWidth) continue const label = document.createElement('span') label.textContent = ref.slice(1) label.style.cssText = `position:fixed;left:${Math.max(0, rect.left)}px;top:${Math.max(0, rect.top)}px;background:#007f79;color:#fff;font:600 11px/14px system-ui;padding:0 3px;border-radius:3px` layer.append(label) annotations.push({ ref: `@${ref}`, label: `[${ref.slice(1)}]` }) } document.documentElement.append(layer) return { annotations } }