The AITURK Chrome extension now registers the user's Chrome as the desktop backend's browser controller, the way the ChatGPT extension lets its agent work in the user's browser. The agent's existing browser tools (navigate, snapshot, click, type, scroll, back, press, get_images, vision) run in its own background tabs grouped as "AITURK", or a tab the user hands over. Backend: a local desktop lane in the browser-control broker. One controller registered from the loopback desktop transport is offered to every desktop session as a lane template; each session still gets its own exact scope on first use. The lane has its own switch (browser.extension_control.local_desktop, on by default); with no extension registered, or after it disconnects, the normal browser backend runs unchanged. browser_vision turns the extension's capture into the usual native/auxiliary vision result. Bridge: forwards controller commands only to a connection that registered, refuses unknown or oversized commands, relays results. Extension: service-worker controller with its own native connection, trusted chrome.debugger input (press/release only: a hover move waits 5 s on background tabs), refusal of password and payment-card fields and of browser, extension, file and Web Store pages, panel switch, Stop, tab hand-over, recent actions, Alt+Shift+A and right-click "send to AITURK". Cancelling Chrome's debugging bar stops the agent. Existing panel flows and privacy rules are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
297 lines
15 KiB
JavaScript
297 lines
15 KiB
JavaScript
import { capturePageContext, composePrompt } from './page-context.js'
|
||
|
||
const $ = id => document.getElementById(id)
|
||
let port, connected = false, sessions = [], selected = '', generation = 0, nextId = 0
|
||
let stream, attachment = null, sending = false, opening = false, connectionGeneration = 0
|
||
let bufferedEvents = [], turnGeneration = 0
|
||
const pending = new Map()
|
||
const notice = text => { $('notice').textContent = text; $('notice').hidden = !text }
|
||
const activity = text => { $('activity').textContent = text }
|
||
|
||
function controls() {
|
||
$('search').disabled = $('refresh').disabled = !connected
|
||
$('sessions').disabled = !connected || sending || opening
|
||
$('message').disabled = $('add-page').disabled = !connected || !selected || opening
|
||
$('send').disabled = !connected || !selected || sending || opening || !$('message').value.trim()
|
||
}
|
||
|
||
function request(action, params = {}) {
|
||
if (!port) return Promise.reject(new Error('Önce IDE’ye bağlanın.'))
|
||
const id = String(++nextId)
|
||
return new Promise((resolve, reject) => {
|
||
const timer = setTimeout(() => { pending.delete(id); reject(new Error('IDE yanıt vermedi. Tekrar göndermeden önce geçmişi kontrol edin.')) }, 75000)
|
||
pending.set(id, { resolve, reject, timer })
|
||
try { port.postMessage({ id, action, params }) }
|
||
catch (error) { clearTimeout(timer); pending.delete(id); reject(error) }
|
||
})
|
||
}
|
||
|
||
function disconnect(message) {
|
||
connected = false
|
||
generation++
|
||
$('dot').classList.remove('online')
|
||
$('connection').textContent = 'IDE bağlantısı yok'
|
||
$('stop').hidden = true
|
||
for (const item of pending.values()) { clearTimeout(item.timer); item.reject(new Error(message)) }
|
||
pending.clear()
|
||
notice(message)
|
||
controls()
|
||
}
|
||
|
||
async function connect() {
|
||
const epoch = ++connectionGeneration
|
||
if (port) { const previous = port; port = null; previous.disconnect() }
|
||
disconnect('')
|
||
$('connection').textContent = 'IDE’ye bağlanıyor…'
|
||
const current = chrome.runtime.connectNative('online.turkservis.aiturk')
|
||
port = current
|
||
current.onMessage.addListener(frame => {
|
||
if (epoch !== connectionGeneration) return
|
||
if (frame.id !== undefined) {
|
||
const item = pending.get(frame.id)
|
||
if (!item) return
|
||
pending.delete(frame.id); clearTimeout(item.timer)
|
||
frame.error ? item.reject(new Error(frame.error)) : item.resolve(frame.result)
|
||
} else handleEvent(frame)
|
||
})
|
||
current.onDisconnect.addListener(() => {
|
||
const details = chrome.runtime.lastError?.message || ''
|
||
if (epoch !== connectionGeneration) return
|
||
port = null
|
||
disconnect(`Bağlantı kurulamadı veya kapandı. Uyumlu AITURK IDE’yi açın ve ZIP içindeki bağlantı kurulumunu çalıştırın.${details ? ' (' + details + ')' : ''}`)
|
||
})
|
||
try {
|
||
const result = await request('status')
|
||
if (epoch !== connectionGeneration) return
|
||
connected = true
|
||
$('connection').textContent = `Yerel IDE bağlı · ${result.profile}`
|
||
$('dot').classList.add('online')
|
||
notice(''); controls()
|
||
await refresh()
|
||
const saved = await chrome.storage.local.get('selectedSession')
|
||
if (epoch !== connectionGeneration) return
|
||
const target = selected || saved.selectedSession
|
||
if (sessions.some(row => row.id === target)) { $('sessions').value = target; await openSession(target) }
|
||
} catch (error) { if (epoch === connectionGeneration) { notice(error.message); controls() } }
|
||
}
|
||
|
||
function renderOptions() {
|
||
const query = $('search').value.toLocaleLowerCase('tr')
|
||
const rows = sessions.filter(row => row.id === selected || row.title.toLocaleLowerCase('tr').includes(query))
|
||
$('sessions').replaceChildren(new Option(rows.length ? 'Bir konuşma seçin' : 'Konuşma bulunamadı', ''))
|
||
rows.forEach(row => $('sessions').add(new Option(row.title, row.id)))
|
||
$('sessions').value = selected
|
||
}
|
||
|
||
async function refresh() {
|
||
const epoch = connectionGeneration
|
||
try {
|
||
const data = await request('sessions.list')
|
||
if (epoch !== connectionGeneration) return
|
||
sessions = data.sessions
|
||
renderOptions()
|
||
$('scope').textContent = `${data.profile} profili · Son 200 kayıttaki IDE konuşmaları · ${sessions.length} konuşma`
|
||
} catch (error) { notice(error.message) }
|
||
}
|
||
|
||
function bubble(role, text, streaming = false) {
|
||
const item = document.createElement('article')
|
||
item.className = `message ${role}${streaming ? ' streaming' : ''}`
|
||
const label = document.createElement('div'); label.className = 'role'; label.textContent = role === 'user' ? 'SEN' : 'AITURK'
|
||
const body = document.createElement('div'); body.className = 'text'; body.textContent = text
|
||
item.append(label, body); $('transcript').append(item)
|
||
return body
|
||
}
|
||
|
||
function scrollEnd() { $('transcript').scrollTop = $('transcript').scrollHeight }
|
||
function paintHistory(messages) {
|
||
$('transcript').replaceChildren(); stream = null
|
||
messages.forEach(row => bubble(row.role, row.text))
|
||
scrollEnd()
|
||
}
|
||
|
||
async function openSession(id) {
|
||
const intent = ++generation
|
||
// A page sent from the context menu before any conversation was chosen
|
||
// waits for the first choice; switching conversations still clears it.
|
||
if (selected) { attachment = null; $('attachment').hidden = true }
|
||
selected = id; opening = Boolean(id); stream = null
|
||
bufferedEvents = []
|
||
$('message').value = ''; $('stop').hidden = true
|
||
notice(''); controls()
|
||
if (!id) { opening = false; $('transcript').replaceChildren(); activity('Bir konuşma seçin.'); return }
|
||
activity('Konuşma açılıyor…')
|
||
try {
|
||
const data = await request('session.open', { sessionId: id })
|
||
if (intent !== generation) return
|
||
paintHistory(data.messages)
|
||
if (data.inflight) { stream = bubble('assistant', data.inflight, true); scrollEnd() }
|
||
$('stop').hidden = !data.running
|
||
activity(data.running ? 'IDE çalışıyor…' : `${data.model || 'IDE modeli'} · Son 40 kayıt gösteriliyor`)
|
||
if (data.attention) notice('Bu konuşma IDE’de onay veya ek bilgi bekliyor.')
|
||
await chrome.storage.local.set({ selectedSession: id })
|
||
} catch (error) { if (intent === generation) { selected = ''; notice(error.message) } }
|
||
finally {
|
||
if (intent === generation) {
|
||
opening = false; controls()
|
||
const events = bufferedEvents; bufferedEvents = []
|
||
events.forEach(handleEvent)
|
||
}
|
||
}
|
||
}
|
||
|
||
async function reconcile() {
|
||
const intent = generation, sessionId = selected, turn = turnGeneration
|
||
try {
|
||
const data = await request('session.history', { sessionId })
|
||
if (intent === generation && sessionId === selected && !opening && !stream && turn === turnGeneration) paintHistory(data.messages)
|
||
} catch (error) { if (intent === generation) notice(error.message) }
|
||
}
|
||
|
||
function handleEvent(frame) {
|
||
if (frame.event === 'disconnected') { disconnect(frame.message); return }
|
||
if (frame.event === 'error') { notice(frame.message); return }
|
||
if (frame.sessionId !== selected) return
|
||
if (opening) { if (bufferedEvents.length < 2000) bufferedEvents.push(frame); return }
|
||
const nearBottom = $('transcript').scrollHeight - $('transcript').scrollTop - $('transcript').clientHeight < 120
|
||
switch (frame.event) {
|
||
case 'message.start': turnGeneration++; stream = null; $('stop').hidden = false; activity('AITURK düşünüyor…'); break
|
||
case 'message.delta':
|
||
stream ||= bubble('assistant', '', true)
|
||
stream.textContent = (stream.textContent + frame.text).slice(-100000)
|
||
activity('Yanıt yazılıyor…'); break
|
||
case 'message.complete':
|
||
if (frame.text) { stream ||= bubble('assistant', '', true); stream.textContent = frame.text }
|
||
stream?.parentElement.classList.remove('streaming'); stream = null
|
||
$('stop').hidden = true; activity(frame.status === 'error' ? 'IDE yanıtı hata ile tamamlandı.' : 'IDE ile aynı konuşma · Güncel')
|
||
void reconcile(); break
|
||
case 'attention': notice(frame.message); activity('IDE’de yanıtınız bekleniyor.'); break
|
||
case 'activity': activity(frame.message); break
|
||
case 'error': case 'session.error': notice(frame.text || 'IDE hata bildirdi.'); break
|
||
}
|
||
if (nearBottom) scrollEnd()
|
||
}
|
||
|
||
$('composer').addEventListener('submit', async event => {
|
||
event.preventDefault()
|
||
if (sending || opening || !connected || !selected) return
|
||
const draft = $('message').value.trim()
|
||
if (!draft) return
|
||
const text = composePrompt(draft, attachment)
|
||
const intent = generation, sessionId = selected
|
||
sending = true; controls(); notice(''); activity('IDE’ye gönderiliyor…')
|
||
const optimistic = bubble('user', text); scrollEnd()
|
||
try {
|
||
await request('message.send', { sessionId, text })
|
||
if (intent !== generation) return
|
||
// Keep server history authoritative; optimistic text is replaced on finish.
|
||
$('message').value = ''; attachment = null; $('attachment').hidden = true
|
||
$('stop').hidden = false; activity('Mesaj IDE’ye iletildi; iş sürüyorsa sıraya alınır.')
|
||
} catch (error) {
|
||
optimistic.parentElement?.remove()
|
||
notice(error.message + ' Taslağınız korundu. Otomatik tekrar gönderilmedi.')
|
||
}
|
||
finally { sending = false; controls() }
|
||
})
|
||
$('message').addEventListener('input', controls)
|
||
$('message').addEventListener('keydown', event => {
|
||
if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) { event.preventDefault(); $('composer').requestSubmit() }
|
||
})
|
||
$('sessions').addEventListener('change', () => void openSession($('sessions').value))
|
||
$('search').addEventListener('input', renderOptions)
|
||
$('refresh').addEventListener('click', () => { void refresh(); if (selected) void reconcile() })
|
||
$('reconnect').addEventListener('click', () => void connect())
|
||
$('stop').addEventListener('click', async () => {
|
||
try { await request('message.stop', { sessionId: selected }); activity('Durdurma isteği IDE’ye iletildi.') }
|
||
catch (error) { notice(error.message) }
|
||
})
|
||
function setAttachment(context) {
|
||
if (!context?.text) throw new Error('Bu sayfadan okunabilir metin alınamadı.')
|
||
attachment = context
|
||
$('attachment-label').textContent = `${context.selection ? 'Seçim' : 'Sayfa'}: ${context.title} · ${context.text.length.toLocaleString('tr')} karakter`
|
||
$('attachment').hidden = false
|
||
notice(selected ? 'Sayfa içeriği mesajınızla birlikte IDE’ye ve seçili model sağlayıcısına gönderilecek. Hassas bilgi içermediğinden emin olun.'
|
||
: 'Ek hazır. Göndermek için yukarıdan bir IDE konuşması seçin.')
|
||
}
|
||
|
||
async function capture(tabId) {
|
||
const results = await chrome.scripting.executeScript({ target: { tabId }, func: capturePageContext })
|
||
return results[0]?.result
|
||
}
|
||
|
||
$('add-page').addEventListener('click', async () => {
|
||
const intent = generation
|
||
try {
|
||
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true })
|
||
if (!tab?.id || !/^https?:\/\//.test(tab.url || '')) throw new Error('Yalnızca normal HTTP/HTTPS sayfaları eklenebilir.')
|
||
const context = await capture(tab.id)
|
||
if (intent !== generation) return
|
||
setAttachment(context)
|
||
} catch (error) { notice(error.message) }
|
||
})
|
||
|
||
// Text or a page sent from the right-click menu while the panel opens.
|
||
async function takePendingContext() {
|
||
const saved = await chrome.storage.session.get(['pendingContext', 'pendingNotice'])
|
||
if (saved.pendingNotice) { notice(saved.pendingNotice); await chrome.storage.session.remove('pendingNotice') }
|
||
const pending = saved.pendingContext
|
||
if (!pending) return
|
||
await chrome.storage.session.remove('pendingContext')
|
||
if (Date.now() - pending.at > 120000) return
|
||
try {
|
||
if (pending.kind === 'selection' && pending.text) {
|
||
const tab = await chrome.tabs.get(pending.tabId).catch(() => null)
|
||
let address = ''
|
||
try { const url = new URL(tab?.url || ''); address = url.origin + url.pathname } catch {}
|
||
setAttachment({ title: (tab?.title || '').slice(0, 200), url: address, text: pending.text.slice(0, 24000), selection: true })
|
||
} else setAttachment(await capture(pending.tabId))
|
||
} catch (error) { notice(error.message) }
|
||
}
|
||
|
||
// ── The agent using this Chrome ───────────────────────────────────────
|
||
const STATUS_MARK = { ok: '✓', failed: '✕', cancelled: '■', running: '…' }
|
||
|
||
function renderControl(state) {
|
||
if (!state || typeof state !== 'object') return
|
||
$('control-enabled').checked = Boolean(state.enabled)
|
||
$('control-stop').hidden = !(state.enabled && state.running)
|
||
$('control-release').hidden = !(state.tabs?.length || state.shared)
|
||
let text
|
||
if (!state.enabled) text = 'Kapalı. Açarsan AITURK ajanı tarayıcı işlerini bu Chrome’da, kendi “AITURK” sekmelerinde yapar.'
|
||
else if (!state.registered) text = `IDE bekleniyor… AITURK IDE açık olmalı.${state.error ? ' (' + state.error + ')' : ''}`
|
||
else if (state.running) text = 'Ajan şu an bu Chrome’da çalışıyor. Durdurmak için “Durdur”a bas.'
|
||
else text = 'Hazır: ajan tarayıcı işlerini bu Chrome’da, “AITURK” sekme grubunda yapar. Şifre ve kart alanlarına yazmaz.'
|
||
if (state.shared) text += ` Paylaşılan sekme: “${state.shared.title || 'sekme'}”; ajan bir sonraki tarayıcı işinde bunu kullanır.`
|
||
if (state.tabs?.length) text += ` Ajanın sekmeleri: ${state.tabs.map(tab => tab.url || tab.title).filter(Boolean).join(', ')}.`
|
||
$('control-status').textContent = text
|
||
const items = (state.log || []).map(entry => {
|
||
const item = document.createElement('li')
|
||
item.className = entry.status
|
||
item.textContent = `${STATUS_MARK[entry.status] || ''} ${entry.text}${entry.note ? ' — ' + entry.note : ''}`
|
||
return item
|
||
})
|
||
$('control-log').replaceChildren(...items)
|
||
$('control-details').hidden = !items.length
|
||
}
|
||
|
||
async function control(type, extra = {}) {
|
||
try {
|
||
const reply = await chrome.runtime.sendMessage({ type, ...extra })
|
||
if (reply?.failure) notice(reply.failure)
|
||
else renderControl(reply)
|
||
} catch (error) { notice(`Eklenti arka planına ulaşılamadı: ${error.message}`) }
|
||
}
|
||
|
||
chrome.runtime.onMessage.addListener(message => { if (message?.type === 'control.state') renderControl(message.state) })
|
||
chrome.storage.onChanged.addListener((changes, area) => {
|
||
if (area === 'session' && (changes.pendingContext?.newValue || changes.pendingNotice?.newValue)) void takePendingContext()
|
||
})
|
||
$('control-enabled').addEventListener('change', () => void control('control.set', { enabled: $('control-enabled').checked }))
|
||
$('control-stop').addEventListener('click', () => void control('control.stop'))
|
||
$('control-share').addEventListener('click', () => void control('control.share', { sessionId: selected || null }))
|
||
$('control-release').addEventListener('click', () => void control('control.release'))
|
||
$('remove-attachment').addEventListener('click', () => { attachment = null; $('attachment').hidden = true; notice('') })
|
||
void connect()
|
||
void control('control.get')
|
||
void takePendingContext()
|