// Lets the AITURK IDE agent use this Chrome, the way the ChatGPT extension // lets its agent work in the user's browser. Runs in the service worker and // keeps its own native connection, so it works with the side panel closed. // // Boundaries: the agent works only in its own tabs (grouped as "AITURK") or a // tab the user explicitly hands over; never on chrome://, extension, file or // Web Store pages; never types into password or payment-card fields. Chrome // shows its own "debugging" bar while the agent is in control; cancelling it, // the panel's Stop button or the switch turns control off at once. import { annotateRefs, clickWithEvents, listImages, locateRef, prepareTyping, readValue, scrollPage, snapshotPage, typeWithEvents, } from './page-agent.js' export const HOST = 'online.turkservis.aiturk' export const CAPABILITIES = Object.freeze(['browser_navigate', 'browser_snapshot', 'browser_click', 'browser_type', 'browser_scroll', 'browser_back', 'browser_press', 'browser_get_images', 'browser_vision']) const COMMAND_BUDGET_MS = 27000 const LOAD_WAIT_MS = 20000 const DEBUGGER_IDLE_MS = 45000 const SCREENSHOT_LIMIT = 380000 const LOG_SIZE = 30 export class ToolError extends Error {} const KEY_ALIASES = { esc: 'Escape', return: 'Enter', up: 'ArrowUp', down: 'ArrowDown', left: 'ArrowLeft', right: 'ArrowRight', space: ' ', spacebar: ' ', del: 'Delete', pgup: 'PageUp', pgdn: 'PageDown', ctrl: 'Control', cmd: 'Meta', command: 'Meta', option: 'Alt' } const NAMED_KEYS = { Enter: [13, 'Enter', '\r'], Tab: [9, 'Tab'], Escape: [27, 'Escape'], Backspace: [8, 'Backspace'], Delete: [46, 'Delete'], ArrowUp: [38, 'ArrowUp'], ArrowDown: [40, 'ArrowDown'], ArrowLeft: [37, 'ArrowLeft'], ArrowRight: [39, 'ArrowRight'], Home: [36, 'Home'], End: [35, 'End'], PageUp: [33, 'PageUp'], PageDown: [34, 'PageDown'], ' ': [32, 'Space', ' '], F5: [116, 'F5'] } const MODIFIERS = { Alt: 1, Control: 2, Meta: 4, Shift: 8 } // "Control+a" → one CDP key description; unknown names fail before any input. export function parseKey(value) { const parts = String(value || '').split('+').map(part => part.trim()).filter(Boolean) if (!parts.length || parts.length > 4) throw new ToolError('Tuş adı geçersiz. Örnek: Enter, Tab, Escape, Control+a') let modifiers = 0 const keyName = parts.pop() for (const raw of parts) { const name = KEY_ALIASES[raw.toLowerCase()] || raw[0].toUpperCase() + raw.slice(1).toLowerCase() if (!MODIFIERS[name]) throw new ToolError(`Bilinmeyen değiştirici tuş: ${raw}`) modifiers |= MODIFIERS[name] } const alias = KEY_ALIASES[keyName.toLowerCase()] const name = alias || (keyName.length === 1 ? keyName : keyName[0].toUpperCase() + keyName.slice(1)) const named = NAMED_KEYS[name] || NAMED_KEYS[Object.keys(NAMED_KEYS).find(key => key.toLowerCase() === name.toLowerCase())] if (named) { const [code, keyCode, text] = named return { key: name === ' ' ? ' ' : keyCode, code: keyCode === ' ' ? 'Space' : keyCode, windowsVirtualKeyCode: code, text: modifiers & ~MODIFIERS.Shift ? undefined : text, modifiers } } if (name.length !== 1) throw new ToolError(`Bilinmeyen tuş: ${keyName}`) const upper = name.toUpperCase() const letter = /[A-Z]/.test(upper), digit = /[0-9]/.test(name) return { key: name, code: letter ? `Key${upper}` : digit ? `Digit${name}` : '', windowsVirtualKeyCode: letter || digit ? upper.charCodeAt(0) : name.charCodeAt(0), text: modifiers & ~MODIFIERS.Shift ? undefined : name, modifiers } } // Only ordinary web pages. Local development servers are allowed; browser, // extension, file and Web Store pages are not. export function checkUrl(value, { allowBlank = false } = {}) { let text = String(value || '').trim() if (!text) throw new ToolError('Adres boş.') if (!/^[a-z][a-z0-9+.-]*:/i.test(text)) text = `https://${text}` let url try { url = new URL(text) } catch { throw new ToolError(`Geçersiz adres: ${String(value).slice(0, 200)}`) } if (allowBlank && url.href === 'about:blank') return url.href if (!['http:', 'https:'].includes(url.protocol)) { throw new ToolError(`Ajan bu tür sayfada çalışamaz (${url.protocol}). Yalnız http/https sayfaları açılabilir.`) } if (/^(chrome\.google\.com|chromewebstore\.google\.com)$/i.test(url.hostname) && (url.hostname.startsWith('chromewebstore') || url.pathname.startsWith('/webstore'))) { throw new ToolError('Chrome Web Mağazası sayfalarında ajan çalışamaz.') } if (url.username || url.password) throw new ToolError('Adreste kullanıcı adı/şifre olamaz.') return url.href } const shortHost = value => { try { return new URL(value).host } catch { return '' } } function describeCommand(action, args) { switch (action) { case 'browser_navigate': return `Sayfa açılıyor: ${shortHost(args.url) || String(args.url || '').slice(0, 60)}` case 'browser_snapshot': return 'Sayfa okunuyor' case 'browser_click': return `Tıklanıyor: ${String(args.ref || '').slice(0, 20)}` case 'browser_type': return `Yazılıyor: ${String(args.ref || '').slice(0, 20)}` case 'browser_scroll': return args.direction === 'up' ? 'Yukarı kaydırılıyor' : 'Aşağı kaydırılıyor' case 'browser_back': return 'Geri gidiliyor' case 'browser_press': return `Tuşa basılıyor: ${String(args.key || '').slice(0, 20)}` case 'browser_get_images': return 'Görseller listeleniyor' case 'browser_vision': return 'Ekran görüntüsü alınıyor' default: return action } } export function createController({ chrome, setTimer = setTimeout, clearTimer = clearTimeout, now = () => Date.now(), random = () => crypto.randomUUID() }) { let port = null, connected = false, registered = false, lastError = '', profile = '' let enabled = true, instanceId = '', settingsLoaded = null, reconnectTimer = null, backoff = 2000 let nextId = 0, titles = new Map(), titlesAt = 0 const pending = new Map() const active = new Map() const queues = new Map() const debuggers = new Map() let state = null // ── persistent pieces ──────────────────────────────────────────────── async function settings() { settingsLoaded ||= (async () => { const saved = await chrome.storage.local.get(['controlEnabled', 'controllerInstance']) enabled = saved.controlEnabled !== false instanceId = saved.controllerInstance || random() if (!saved.controllerInstance) await chrome.storage.local.set({ controllerInstance: instanceId }) })() return settingsLoaded } async function load() { if (state) return state const saved = await chrome.storage.session.get('controlState') state = { tabs: {}, groups: {}, shared: null, log: [], ...(saved.controlState || {}) } return state } async function save() { await chrome.storage.session.set({ controlState: state }) } // ── native connection ──────────────────────────────────────────────── function request(action, params = {}) { if (!port) return Promise.reject(new Error('IDE bağlantısı yok.')) const id = `c${++nextId}` return new Promise((resolve, reject) => { const timer = setTimer(() => { pending.delete(id); reject(new Error('IDE yanıt vermedi.')) }, 30000) pending.set(id, { resolve, reject, timer }) try { port.postMessage({ id, action, params }) } catch (error) { clearTimer(timer); pending.delete(id); reject(error) } }) } function scheduleReconnect() { if (!enabled || reconnectTimer) return reconnectTimer = setTimer(() => { reconnectTimer = null; void connect() }, backoff) backoff = Math.min(backoff * 2, 60000) chrome.alarms?.create('aiturk-reconnect', { periodInMinutes: 1 }) } async function connect() { await settings() if (!enabled || port) return const current = chrome.runtime.connectNative(HOST) port = current current.onMessage.addListener(frame => onFrame(current, frame)) current.onDisconnect.addListener(() => { if (port !== current) return port = null; connected = false; registered = false for (const item of pending.values()) { clearTimer(item.timer); item.reject(new Error('IDE bağlantısı kapandı.')) } pending.clear() abortAll('IDE bağlantısı kapandı.') lastError ||= 'AITURK IDE kapalı ya da Chrome bağlantısı kurulmamış.' broadcast() scheduleReconnect() }) try { const status = await request('status') profile = status?.profile || '' connected = true; lastError = '' await register() backoff = 2000 chrome.alarms?.clear('aiturk-reconnect') } catch (error) { lastError = error.message } broadcast() } async function register() { if (!port || !enabled) return const result = await request('browser.register', { enabled: true, capabilities: [...CAPABILITIES], controllerId: `aiturk-chrome-${instanceId}`, browserProfileId: instanceId }) registered = Boolean(result?.registered) } function onFrame(source, frame) { if (source !== port || !frame || typeof frame !== 'object') return if (frame.id !== undefined) { const item = pending.get(frame.id) if (!item) return pending.delete(frame.id); clearTimer(item.timer) frame.error ? item.reject(new Error(frame.error)) : item.resolve(frame.result) return } if (frame.event === 'browser.command') void runCommand(frame) else if (frame.event === 'browser.cancel') cancelCommand(frame.commandId, 'Ajan bu adımı iptal etti.') else if (frame.event === 'disconnected') { // The IDE restarted its backend; the native host is still here. registered = false; connected = false abortAll('IDE yeniden bağlanıyor.') broadcast() setTimer(() => { void reconnectBackend() }, 2000) } else if (frame.event === 'error') { lastError = String(frame.message || '').slice(0, 300); broadcast() } } async function reconnectBackend() { if (!port || !enabled) return try { const status = await request('status') profile = status?.profile || '' connected = true; lastError = '' await register() } catch (error) { lastError = error.message; setTimer(() => { void reconnectBackend() }, 10000) } broadcast() } // ── commands ───────────────────────────────────────────────────────── async function runCommand(frame) { const { commandId, action, sessionId } = frame const args = frame.arguments && typeof frame.arguments === 'object' ? frame.arguments : {} const record = { commandId, cancelled: false, reason: '' } active.set(commandId, record) const entry = await logEntry({ at: now(), sessionId, action, text: describeCommand(action, args), status: 'running' }) let reply try { if (!CAPABILITIES.includes(action)) throw new ToolError('Chrome bu işlemi desteklemiyor.') const result = await withBudget(execute(action, args, sessionId, record), COMMAND_BUDGET_MS) reply = { commandId, ok: true, result } entry.status = result?.success === false ? 'failed' : 'ok' if (result?.success === false) entry.note = String(result.error || '').slice(0, 200) } catch (error) { const message = String(error?.message || error).slice(0, 2000) // Expected refusals reach the model as a normal tool result it can act on. reply = error instanceof ToolError ? { commandId, ok: true, result: { success: false, error: message } } : { commandId, ok: false, error: message } entry.status = 'failed'; entry.note = message.slice(0, 200) } if (record.cancelled) { entry.status = 'cancelled'; entry.note = record.reason } active.delete(commandId) await save().catch(() => {}) broadcast() if (record.cancelled) return try { await request('browser.result', reply) } catch {} } function cancelCommand(commandId, reason) { const record = active.get(commandId) if (record) { record.cancelled = true; record.reason = reason } } function abortAll(reason) { for (const record of active.values()) { record.cancelled = true; record.reason = reason } } function guard(record) { if (record?.cancelled) throw new ToolError(record.reason || 'Durduruldu.') } async function withBudget(promise, ms) { let timer const timeout = new Promise((_, reject) => { timer = setTimer(() => reject(new ToolError('Tarayıcı adımı zaman aşımına uğradı.')), ms) }) try { return await Promise.race([promise, timeout]) } finally { clearTimer(timer) } } // One step at a time per tab. The stored tail never rejects: the caller // handles the step's own error, the queue only orders the steps. function serial(tabId, task) { const next = (queues.get(tabId) || Promise.resolve()).then(task) const tail = next.catch(() => {}).then(() => { if (queues.get(tabId) === tail) queues.delete(tabId) }) queues.set(tabId, tail) return next } async function execute(action, args, sessionId, record) { if (action === 'browser_navigate') { const url = checkUrl(args.url) const tab = await agentTab(sessionId, true) return serial(tab.id, async () => { guard(record) await chrome.tabs.update(tab.id, { url }) const loaded = await waitForLoad(tab.id, LOAD_WAIT_MS) guard(record) const snap = await inject(tab.id, snapshotPage, [{ full: false }]) return { success: true, url: snap.url, title: snap.title, snapshot: snap.snapshot, element_count: snap.element_count, ...(loaded ? {} : { note: 'Sayfa yüklenmeye devam ediyor; gerekirse browser_snapshot ile tekrar bakın.' }) } }) } const tab = await agentTab(sessionId, false) return serial(tab.id, async () => { guard(record) const current = await chrome.tabs.get(tab.id) const address = current.pendingUrl || current.url || '' if (address === 'about:blank' || !address) { if (action === 'browser_snapshot') return { success: true, snapshot: 'Boş sekme. Önce browser_navigate ile bir sayfa açın.', element_count: 0, url: 'about:blank', title: '' } throw new ToolError('Sekme boş. Önce browser_navigate ile bir sayfa açın.') } checkUrl(address) switch (action) { case 'browser_snapshot': { const snap = await inject(tab.id, snapshotPage, [{ full: args.full === true }]) return { success: true, ...snap } } case 'browser_click': return click(tab, args.ref, record) case 'browser_type': return type(tab, args.ref, typeof args.text === 'string' ? args.text : String(args.text ?? ''), record) case 'browser_scroll': { const result = await inject(tab.id, scrollPage, [args.direction === 'up' ? 'up' : 'down']) return { success: true, direction: args.direction === 'up' ? 'up' : 'down', ...result } } case 'browser_back': { try { await chrome.tabs.goBack(tab.id) } catch { throw new ToolError('Geri gidilecek sayfa yok.') } await waitForLoad(tab.id, LOAD_WAIT_MS) const after = await chrome.tabs.get(tab.id) return { success: true, url: after.url, title: after.title } } case 'browser_press': return press(tab, args.key) case 'browser_get_images': return { success: true, ...(await inject(tab.id, listImages, [])) } case 'browser_vision': return vision(tab, args.annotate === true) } throw new ToolError('Chrome bu işlemi desteklemiyor.') }) } async function click(tab, ref, record) { const located = await inject(tab.id, locateRef, [ref]) if (located.error) throw new ToolError(located.error) if (located.disabled) throw new ToolError(`${ref} şu an devre dışı.`) guard(record) let trusted = true try { const send = await debuggerFor(tab.id) // No mouseMoved first: on a background tab Chrome holds a hover move // for 5 s waiting for a frame, while press/release go straight through. for (const type of ['mousePressed', 'mouseReleased']) { await send('Input.dispatchMouseEvent', { type, x: located.x, y: located.y, button: 'left', clickCount: 1 }) } } catch { trusted = false const result = await inject(tab.id, clickWithEvents, [ref]) if (result.error) throw new ToolError(result.error) } await settle(tab.id) const after = await chrome.tabs.get(tab.id) return { success: true, clicked: ref, element: located.name, url: after.url, title: after.title, ...(located.covered ? { warning: `Tıklanan noktada başka bir öğe vardı: "${located.covered}". Gerekirse önce onu kapatın.` } : {}), ...(trusted ? {} : { note: 'Gerçek fare tıklaması yerine sayfa olayı kullanıldı.' }) } } async function type(tab, ref, text, record) { const prepared = await inject(tab.id, prepareTyping, [ref]) if (prepared.error) { if (prepared.handoff) notify('AITURK senin girmeni bekliyor', prepared.error) throw new ToolError(prepared.error) } guard(record) if (prepared.select) { const result = await inject(tab.id, typeWithEvents, [ref, text]) if (result.error) throw new ToolError(result.error) return { success: true, typed: ref, value: result.value } } try { const send = await debuggerFor(tab.id) if (text) await send('Input.insertText', { text }) else for (const type of ['rawKeyDown', 'keyUp']) await send('Input.dispatchKeyEvent', { type, key: 'Delete', code: 'Delete', windowsVirtualKeyCode: 46 }) } catch { const result = await inject(tab.id, typeWithEvents, [ref, text]) if (result.error) throw new ToolError(result.error) } const value = await inject(tab.id, readValue, [ref]) return { success: true, typed: ref, value: value.value ?? '' } } async function press(tab, key) { const description = parseKey(key) try { const send = await debuggerFor(tab.id) await send('Input.dispatchKeyEvent', { type: description.text ? 'keyDown' : 'rawKeyDown', ...description, unmodifiedText: description.text }) await send('Input.dispatchKeyEvent', { type: 'keyUp', key: description.key, code: description.code, windowsVirtualKeyCode: description.windowsVirtualKeyCode, modifiers: description.modifiers }) } catch { throw new ToolError('Tuşa basılamadı: Chrome kontrol izni alınamadı (başka bir geliştirici aracı açık olabilir).') } await settle(tab.id) const after = await chrome.tabs.get(tab.id) return { success: true, pressed: String(key), url: after.url, title: after.title } } async function vision(tab, annotate) { let annotations = [] if (annotate) { await inject(tab.id, snapshotPage, [{ full: false }]) annotations = (await inject(tab.id, annotateRefs, [true])).annotations || [] } try { let data = '' try { const send = await debuggerFor(tab.id) // A background tab paints on demand for the capture (about 1-3 s). for (const options of [{ quality: 65, budget: 15000 }, { quality: 40, budget: 5000 }, { quality: 40, scale: 0.6, budget: 5000 }]) { let clip if (options.scale) { const metrics = await send('Page.getLayoutMetrics', {}) const view = metrics.cssVisualViewport || metrics.layoutViewport clip = { x: view.pageX || 0, y: view.pageY || 0, width: view.clientWidth, height: view.clientHeight, scale: options.scale } } const shot = await withBudget(send('Page.captureScreenshot', { format: 'jpeg', quality: options.quality, ...(clip ? { clip } : {}) }), options.budget) data = shot?.data || '' if (data && data.length <= SCREENSHOT_LIMIT) break } } catch { const current = await chrome.tabs.get(tab.id) if (!current.active) throw new ToolError('Ekran görüntüsü alınamadı: ajanın sekmesi görünür değil ve Chrome kontrol izni alınamadı.') const url = await chrome.tabs.captureVisibleTab(current.windowId, { format: 'jpeg', quality: 55 }) data = String(url || '').replace(/^data:image\/jpeg;base64,/, '') } if (!data || data.length > SCREENSHOT_LIMIT) throw new ToolError('Ekran görüntüsü çok büyük ya da boş geldi.') const after = await chrome.tabs.get(tab.id) return { success: true, screenshot: data, mime: 'image/jpeg', url: after.url, title: after.title, ...(annotate ? { annotations } : {}) } } finally { if (annotate) await inject(tab.id, annotateRefs, [false]).catch(() => {}) } } // ── tabs ───────────────────────────────────────────────────────────── async function agentTab(sessionId, create) { const current = await load() const key = sessionId || 'default' const known = current.tabs[key] if (known) { try { return await chrome.tabs.get(known) } catch { delete current.tabs[key] } } if (current.shared && (!current.shared.sessionId || current.shared.sessionId === key)) { try { const tab = await chrome.tabs.get(current.shared.tabId) current.tabs[key] = tab.id current.shared = null await save() return tab } catch { current.shared = null } } if (!create) throw new ToolError('Bu konuşma için açık bir tarayıcı sekmesi yok. Önce browser_navigate ile bir sayfa açın.') const window = await chrome.windows.getLastFocused({ windowTypes: ['normal'] }).catch(() => null) const tab = await chrome.tabs.create({ url: 'about:blank', active: false, ...(window?.id ? { windowId: window.id } : {}) }) current.tabs[key] = tab.id await save() await groupTab(tab, key).catch(() => {}) return tab } async function sessionTitle(sessionId) { if (now() - titlesAt > 60000 && port) { titlesAt = now() try { const data = await request('sessions.list') titles = new Map((data?.sessions || []).map(row => [row.id, row.title])) } catch {} } return titles.get(sessionId) || '' } async function groupTab(tab, key) { const current = await load() let groupId = current.groups[key] if (groupId !== undefined) { try { const group = await chrome.tabGroups.get(groupId) if (group.windowId !== tab.windowId) groupId = undefined } catch { groupId = undefined } } groupId = await chrome.tabs.group({ tabIds: [tab.id], ...(groupId === undefined ? { createProperties: { windowId: tab.windowId } } : { groupId }) }) current.groups[key] = groupId await save() const title = await sessionTitle(key) await chrome.tabGroups.update(groupId, { title: `AITURK${title ? ` · ${title.slice(0, 28)}` : ''}`, color: 'cyan' }) } function waitForLoad(tabId, ms) { return new Promise(resolve => { let done = false const finish = value => { if (done) return done = true; clearTimer(timer) chrome.tabs.onUpdated.removeListener(listener) resolve(value) } const listener = (id, change) => { if (id === tabId && change.status === 'complete') finish(true) } const timer = setTimer(() => finish(false), ms) chrome.tabs.onUpdated.addListener(listener) // A page that finished before the listener existed. // pendingUrl is set until the new document commits, so an old page's // "complete" is never mistaken for the new one. setTimer(() => { chrome.tabs.get(tabId).then(tab => { if (tab.status === 'complete' && !tab.pendingUrl) finish(true) }).catch(() => finish(false)) }, 250) }) } async function settle(tabId) { await new Promise(resolve => setTimer(resolve, 400)) const tab = await chrome.tabs.get(tabId).catch(() => null) if (tab?.status === 'loading') await waitForLoad(tabId, 8000) } async function inject(tabId, func, args) { let results try { results = await chrome.scripting.executeScript({ target: { tabId }, func, args }) } catch (error) { throw new ToolError(`Sayfaya erişilemedi: ${String(error?.message || error).slice(0, 200)}`) } const value = results?.[0]?.result if (value === undefined || value === null) throw new ToolError('Sayfa yanıt vermedi; yükleniyor olabilir.') return value } async function debuggerFor(tabId) { let entry = debuggers.get(tabId) if (!entry) { await chrome.debugger.attach({ tabId }, '1.3') entry = { timer: null } debuggers.set(tabId, entry) } clearTimer(entry.timer) entry.timer = setTimer(() => { void detachDebugger(tabId) }, DEBUGGER_IDLE_MS) return (method, params) => chrome.debugger.sendCommand({ tabId }, method, params) } async function detachDebugger(tabId) { const entry = debuggers.get(tabId) if (!entry) return clearTimer(entry.timer) debuggers.delete(tabId) await chrome.debugger.detach({ tabId }).catch(() => {}) } // ── state for the panel ───────────────────────────────────────────── async function logEntry(entry) { const current = await load() current.log.unshift(entry) current.log.length = Math.min(current.log.length, LOG_SIZE) await save().catch(() => {}) broadcast() return entry } function notify(title, message) { chrome.notifications?.create({ type: 'basic', iconUrl: 'icon128.png', title, message: String(message).slice(0, 200), priority: 1 }) ?.catch?.(() => {}) } async function snapshotState() { const current = await load() const tabs = [] for (const [sessionId, tabId] of Object.entries(current.tabs)) { try { const tab = await chrome.tabs.get(tabId) tabs.push({ sessionId, tabId, title: (tab.title || '').slice(0, 120), url: shortHost(tab.url) }) } catch { delete current.tabs[sessionId] } } return { enabled, connected, registered, error: lastError, profile, running: active.size, log: current.log.slice(0, 15), tabs, shared: current.shared } } function broadcast() { chrome.action?.setBadgeText?.({ text: active.size ? '●' : '' })?.catch?.(() => {}) void snapshotState().then(view => chrome.runtime.sendMessage({ type: 'control.state', state: view })).catch(() => {}) } async function setEnabled(value) { await settings() enabled = value === true await chrome.storage.local.set({ controlEnabled: enabled }) if (enabled) { backoff = 2000 if (port && !registered) { try { await register() } catch (error) { lastError = error.message } } else await connect() } else { abortAll('Kullanıcı ajanın tarayıcı kullanımını kapattı.') for (const tabId of [...debuggers.keys()]) await detachDebugger(tabId) if (port) { try { await request('browser.register', { enabled: false }) } catch {} } registered = false const previous = port port = null; connected = false previous?.disconnect() chrome.alarms?.clear('aiturk-reconnect') } broadcast() return snapshotState() } async function shareTab(tab, sessionId) { if (!tab?.id) throw new ToolError('Paylaşılacak sekme bulunamadı.') checkUrl(tab.url || '') const current = await load() if (sessionId) current.tabs[sessionId] = tab.id else current.shared = { tabId: tab.id, sessionId: null, title: (tab.title || '').slice(0, 120) } await save() broadcast() return snapshotState() } async function releaseTab(sessionId) { const current = await load() const ids = sessionId ? [current.tabs[sessionId]] : Object.values(current.tabs) if (sessionId) delete current.tabs[sessionId] else { current.tabs = {}; current.shared = null } for (const tabId of ids) if (tabId) await detachDebugger(tabId) await save() broadcast() return snapshotState() } async function onMessage(message, sender) { switch (message?.type) { case 'control.get': await settings() if (enabled && !port) void connect() return snapshotState() case 'control.set': return setEnabled(message.enabled === true) case 'control.stop': return setEnabled(false) case 'control.share': { const [tab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true }) return shareTab(tab, typeof message.sessionId === 'string' ? message.sessionId : null) } case 'control.release': return releaseTab(typeof message.sessionId === 'string' ? message.sessionId : null) } return undefined } function onDebuggerDetach(source, reason) { const entry = debuggers.get(source?.tabId) if (!entry) return clearTimer(entry.timer) debuggers.delete(source.tabId) if (reason === 'canceled_by_user') { notify('AITURK durduruldu', 'Chrome uyarısındaki İptal ile ajanın tarayıcı kullanımı kapatıldı. Panelden tekrar açabilirsiniz.') void setEnabled(false) } } async function onTabRemoved(tabId) { const current = await load() let changed = false for (const [key, value] of Object.entries(current.tabs)) if (value === tabId) { delete current.tabs[key]; changed = true } if (current.shared?.tabId === tabId) { current.shared = null; changed = true } debuggers.delete(tabId) if (changed) { await save(); broadcast() } } return { connect, onMessage, onDebuggerDetach, onTabRemoved, setEnabled, shareTab, releaseTab, snapshotState, onAlarm: name => { if (name === 'aiturk-reconnect') void connect() }, // exposed for tests _execute: execute, _runCommand: runCommand, _state: () => ({ port, connected, registered, enabled, active }) } }