Add Windows Chrome side panel and scoped IDE native bridge

This commit is contained in:
yilsem committed 2026-09-07 12:45:45 +03:00
1 parent 75afd01b4b
commit fb6f57cd36
14 files changed
+1018 -1

No files matched your search

+210
View File
@@ -0,0 +1,210 @@
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
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
selected = id; opening = Boolean(id); stream = null
attachment = null; $('attachment').hidden = true
$('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() } }
}
async function reconcile() {
const intent = generation, sessionId = selected
try {
const data = await request('session.history', { sessionId })
if (intent === generation && sessionId === selected && !opening) 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 || opening) return
const nearBottom = $('transcript').scrollHeight - $('transcript').scrollTop - $('transcript').clientHeight < 120
switch (frame.event) {
case 'message.start': 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…')
try {
await request('message.send', { sessionId, text })
if (intent !== generation) return
// Keep server history authoritative; optimistic text is replaced on finish.
bubble('user', text); scrollEnd()
$('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) { 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) }
})
$('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. Önce AITURK araç çubuğu simgesine tıklayarak bu sekmeye erişim verin.')
const results = await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: capturePageContext })
if (intent !== generation) return
const context = results[0]?.result
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('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.')
} catch (error) { notice(error.message) }
})
$('remove-attachment').addEventListener('click', () => { attachment = null; $('attachment').hidden = true; notice('') })
void connect()