You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
299 lines
12 KiB
299 lines
12 KiB
"use strict"; |
|
|
|
(function () { |
|
var SEL = globalThis.DSH_SELECTORS; |
|
var BRIDGE = globalThis.DSH_BRIDGE; |
|
var extractCalls = null; |
|
if (typeof globalThis.extractCalls === "function") { |
|
extractCalls = globalThis.extractCalls; |
|
} |
|
var UI = globalThis.DSHMEM || null; |
|
var canStart = false; var started = false; |
|
var lastProcessedKey = ''; |
|
var lastSentAssistantKey = ''; |
|
var seenCallsHashes = new Set(); |
|
var CALL_HASH_CAP = 100; |
|
if (UI) { |
|
UI.onResetLoops(function () { queue.length = 0; pending.clear(); seenHashes.clear(); pumping = false; if (UI) UI.log('loops reset'); }); |
|
UI.onStop(function () { queue.length = 0; started = false; try { obs.disconnect(); } catch (e) {} if (UI) { UI.setPhase('off'); UI.setStartEnabled(true); UI.log('stopped'); } }); |
|
UI.onSaveToken(function (v) { chrome.storage.local.set({ 'dshmem-bridge-token': v }, function () { if (UI) UI.log('token saved, reload page'); }); }); |
|
} |
|
if (UI) { UI.onStart(function () { if (!canStart) { if (UI.setStartEnabled) UI.setStartEnabled(true); return; } started = true; markExistingAsProcessed(); obs.observe(document.documentElement, { childList: true, subtree: true, characterData: true }); UI.setPhase('idle'); UI.setConnected(true); UI.log('started'); }); } |
|
window.addEventListener('dshmem:metrics', function (e) { try { var mm = JSON.parse(e.detail); if (UI && UI.setMetrics) UI.setMetrics(mm); } catch (err) {} }); if (!SEL) return; |
|
if (!BRIDGE) return; |
|
if (typeof extractCalls !== "function") return; |
|
|
|
var DEBOUNCE_MS = 800; |
|
var processed = new WeakSet(); |
|
var seenHashes = new Map(); |
|
var HASH_CAP = 50; |
|
var queue = []; |
|
var pumping = false; |
|
var timer = null; |
|
var pending = new Set(); var lastPath = location.pathname; function __onPathChange() { var p = location.pathname; if (p === lastPath) return; var __prev = lastPath; lastPath = p; var __fromRoot = (__prev === '/' || __prev === ''); queue.length = 0; pending.clear(); seenHashes.clear(); seenCallsHashes.clear(); pumping = false; if (!__fromRoot) setTimeout(function () { try { markExistingAsProcessed(); } catch (e) { } }, 200); if (!__fromRoot) setTimeout(function () { try { markExistingAsProcessed(); } catch (e) { } }, 1200); if (UI) { UI.setPhase('idle'); if (UI.log) UI.log('chat changed ' + p); } try { window.dispatchEvent(new CustomEvent('dshmem:chat-change', { detail: p })); } catch (e) {} } window.addEventListener('popstate', __onPathChange); try { var __ps = history.pushState; var __rs = history.replaceState; history.pushState = function () { var r = __ps.apply(this, arguments); setTimeout(__onPathChange, 0); return r; }; history.replaceState = function () { var r = __rs.apply(this, arguments); setTimeout(__onPathChange, 0); return r; }; } catch (e) {} setInterval(__onPathChange, 1000); |
|
|
|
function hashText(s) { |
|
var h = 5381; |
|
var i; |
|
for (i = 0; i < s.length; i++) { |
|
h = ((h << 5) + h + s.charCodeAt(i)) >>> 0; |
|
} |
|
return h.toString(36) + ":" + s.length; |
|
} |
|
|
|
function rememberHash(h) { |
|
seenHashes.set(h, true); |
|
if (seenHashes.size > HASH_CAP) { |
|
var first = seenHashes.keys().next().value; |
|
seenHashes.delete(first); |
|
} |
|
} |
|
|
|
|
|
function markExistingAsProcessed() { |
|
var sels = SEL.MESSAGE_SELECTORS; |
|
for (var i = 0; i < sels.length; i++) { |
|
try { |
|
var all = document.querySelectorAll(sels[i]); |
|
for (var j = 0; j < all.length; j++) { |
|
var el = all[j]; |
|
processed.add(el); |
|
try { |
|
var t = SEL.collectText(el); |
|
if (t && t.length > 0) rememberHash(hashText(t)); |
|
} catch (e2) {} |
|
} |
|
} catch (e) {} |
|
} |
|
} |
|
|
|
function rememberCallsHash(h) { |
|
seenCallsHashes.add(h); |
|
if (seenCallsHashes.size > CALL_HASH_CAP) { |
|
var first = seenCallsHashes.values().next().value; |
|
seenCallsHashes.delete(first); |
|
} |
|
} |
|
|
|
function keyOf(el) { |
|
if (!el) return ''; |
|
var t = el.textContent || el.innerText || ''; |
|
return t.length + ':' + t.slice(-64); |
|
} |
|
|
|
function lastVisible(list) { |
|
if (!list) return null; |
|
for (var i = 0; i < list.length; i++) { |
|
try { |
|
var all = document.querySelectorAll(list[i]); |
|
for (var j = all.length - 1; j >= 0; j--) { |
|
var el = all[j]; |
|
var r = el.getBoundingClientRect(); |
|
if (r.width > 0 || r.height > 0) return el; |
|
} |
|
} catch (e) {} |
|
} |
|
return null; |
|
} |
|
|
|
function findLastAssistant() { return lastVisible(SEL && SEL.assistantMarkdown); } |
|
function findInput() { return lastVisible(SEL && SEL.input); } |
|
function findSendButton() { return lastVisible(SEL && SEL.sendButton); } |
|
|
|
function sleep(ms) { return new Promise(function (r) { setTimeout(r, ms); }); } |
|
|
|
function setNativeValue(el, text) { |
|
if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') { |
|
var proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype; |
|
var setter = Object.getOwnPropertyDescriptor(proto, 'value').set; |
|
setter.call(el, text); |
|
el.dispatchEvent(new Event('input', { bubbles: true })); |
|
} else { |
|
el.focus(); |
|
var ok = false; |
|
try { ok = document.execCommand('insertText', false, text); } catch (e) { ok = false; } |
|
if (!ok) { |
|
el.textContent = text; |
|
el.dispatchEvent(new InputEvent('beforeinput', { bubbles: true, cancelable: true, inputType: 'insertText', data: text })); |
|
el.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: text })); |
|
} |
|
} |
|
} |
|
|
|
function sendText(text) { |
|
var input = findInput(); |
|
if (!input) { console.warn('[dsh-memory] input not found'); return Promise.resolve(false); } |
|
var la = findLastAssistant(); |
|
if (la) lastSentAssistantKey = keyOf(la); |
|
setNativeValue(input, text); |
|
return sleep(150).then(function () { |
|
var btn = findSendButton(); |
|
if (btn && !btn.disabled && btn.getAttribute('aria-disabled') !== 'true') { |
|
btn.click(); |
|
} else { |
|
input.focus(); |
|
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true })); |
|
} |
|
return sleep(600); |
|
}).then(function () { return true; }); |
|
} |
|
|
|
function buildResultMessage(results) { |
|
var BT3 = String.fromCharCode(96) + String.fromCharCode(96) + String.fromCharCode(96); |
|
var blocks = results.map(function (r) { return BT3 + 'dsh-result' + String.fromCharCode(10) + JSON.stringify(r) + String.fromCharCode(10) + BT3; }).join(String.fromCharCode(10) + String.fromCharCode(10)); |
|
var remind = '[dsh-memory] continue. When done output on its own line: DSH:DONE:summary.'; |
|
return '[dsh-memory tool result]' + String.fromCharCode(10) + String.fromCharCode(10) + blocks + String.fromCharCode(10) + String.fromCharCode(10) + remind; |
|
} |
|
|
|
function findMessages(node) { |
|
var out = []; |
|
if (!node) return out; |
|
var i; |
|
var j; |
|
for (i = 0; i < SEL.MESSAGE_SELECTORS.length; i++) { |
|
var sel = SEL.MESSAGE_SELECTORS[i]; |
|
if (node.matches) { |
|
if (node.matches(sel)) out.push(node); |
|
} |
|
if (node.querySelectorAll) { |
|
var kids = node.querySelectorAll(sel); |
|
for (j = 0; j < kids.length; j++) out.push(kids[j]); |
|
} |
|
} |
|
return out; |
|
} |
|
|
|
function handleNode(node) { |
|
if (!node) return; |
|
if (node.nodeType !== 1) return; |
|
var msgs = findMessages(node); |
|
var i; |
|
var k; |
|
for (i = 0; i < msgs.length; i++) { |
|
var m = msgs[i]; |
|
if (processed.has(m)) continue; |
|
var text = SEL.collectText(m); if (!started) { processed.add(m); continue; } |
|
if (!text) continue; |
|
var h = hashText(text); |
|
if (seenHashes.has(h)) { |
|
processed.add(m); |
|
continue; |
|
} |
|
var hasMarker = text.indexOf(String.fromCharCode(10214)) !== -1 || text.indexOf('[DSH') !== -1; if (hasMarker && UI && UI.setDot) UI.setDot('busy'); var parsed = extractCalls(text, 3); |
|
if (!parsed) continue; |
|
if (!parsed.calls) continue; |
|
if (parsed.calls.length === 0) continue; |
|
var ch = hashText(JSON.stringify(parsed.calls)); |
|
if (seenCallsHashes.has(ch)) { if (UI) { if (UI.setDot) UI.setDot('ok'); UI.log('dup call skipped'); } processed.add(m); rememberHash(h); continue; } |
|
rememberCallsHash(ch); |
|
processed.add(m); |
|
rememberHash(h); |
|
for (k = 0; k < parsed.calls.length; k++) queue.push(parsed.calls[k]); if (UI && UI.setDot) UI.setDot('ok-busy'); |
|
if (UI) UI.setPhase('executing'); |
|
} |
|
pump(); |
|
} |
|
function executeOne(call) { |
|
return BRIDGE.sendCall(call).then(function (res) { |
|
var e = res && !res.ok && res.error; |
|
var needConfirm = e && e.code === "E_NEEDS_CONFIRM" && e.extra && e.extra.confirmToken; |
|
if (needConfirm) { |
|
console.log("[dsh-memory] needs confirm, asking user", call.tool); |
|
if (!UI || typeof UI.showConfirm !== 'function') { return { ok: false, error: { code: 'E_CONFIRM_UNAVAILABLE', message: 'confirm UI not available' } }; } return new Promise(function (resolve) { UI.showConfirm({ title: 'Allow ' + call.tool + '?', detail: JSON.stringify(call.args || {}).slice(0, 300), onAllow: function () { BRIDGE.sendCall({ tool: call.tool, args: call.args, confirmToken: e.extra.confirmToken }).then(resolve); }, onDeny: function () { resolve({ ok: false, error: { code: 'E_USER_DENIED', message: 'denied by user' } }); } }); }); |
|
} |
|
return res; |
|
}); |
|
} |
|
function pump() { |
|
if (pumping) return; |
|
if (queue.length === 0) return; |
|
pumping = true; |
|
var results = []; |
|
var step = function () { |
|
if (queue.length === 0) { |
|
pumping = false; |
|
if (results.length > 0) { |
|
var payload = buildResultMessage(results); |
|
if (UI) UI.setPhase('sending'); |
|
var anyFail = results.some(function (r) { return !r || !r.ok; }); sendText(payload).then(function () { if (UI) { UI.setPhase('idle'); if (UI.setDot) UI.setDot(anyFail ? 'busy' : 'ok'); } }).catch(function (err) { console.warn('[dsh-memory] sendText failed', String(err)); if (UI) { UI.setPhase('idle'); if (UI.setDot) UI.setDot('busy'); } }); |
|
} else { if (UI) UI.setPhase('idle'); } |
|
return; |
|
} |
|
var call = queue.shift(); |
|
executeOne(call).then(function (res) { |
|
if (res) { |
|
results.push(res); |
|
if (res.ok) { |
|
console.log('[dsh-memory] call ok', call.tool, res.durationMs); |
|
if (UI) UI.log('ok ' + call.tool); |
|
} else { |
|
try { console.warn('[dsh-memory] call failed', call.tool, JSON.stringify(res.error)); } catch (e) { } |
|
if (UI) UI.log('fail ' + call.tool + ' ' + JSON.stringify((res && res.error) || 'nores')); |
|
} |
|
} else { |
|
console.warn('[dsh-memory] call failed (no res)', call.tool); |
|
if (UI) UI.log('fail ' + call.tool + ' ' + JSON.stringify((res && res.error) || 'nores')); |
|
} |
|
}).catch(function (err) { console.warn('[dsh-memory] call throw', call.tool, String(err)); }).then(step); |
|
}; |
|
step(); |
|
} |
|
|
|
function flush() { |
|
timer = null; |
|
var nodes = Array.from(pending); |
|
pending.clear(); |
|
var i; |
|
for (i = 0; i < nodes.length; i++) handleNode(nodes[i]); |
|
} |
|
|
|
function schedule() { |
|
if (timer) clearTimeout(timer); |
|
timer = setTimeout(flush, DEBOUNCE_MS); |
|
} |
|
|
|
var obs = new MutationObserver(function (muts) { |
|
var i; |
|
var j; |
|
for (i = 0; i < muts.length; i++) { |
|
var m = muts[i]; |
|
if (m.type === "childList") { |
|
for (j = 0; j < m.addedNodes.length; j++) { |
|
var n = m.addedNodes[j]; |
|
if (n) { |
|
if (n.nodeType === 1) pending.add(n); |
|
} |
|
} |
|
} else if (m.type === "characterData") { |
|
if (m.target) { |
|
if (m.target.parentNode) pending.add(m.target.parentNode); |
|
} |
|
} |
|
} |
|
if (pending.size > 0) schedule(); |
|
}); |
|
|
|
BRIDGE.isEnabled().then(function (enabled) { |
|
if (!enabled) { |
|
console.warn("[dsh-memory] disabled (dsh-memory-enabled=false), idle"); |
|
if (UI) { UI.setPhase('off'); UI.setConnected(false); UI.log('disabled'); } |
|
return; |
|
} |
|
return BRIDGE.hasToken().then(function (has) { |
|
if (!has) { |
|
console.warn("[dsh-memory] no bridge token, idle"); |
|
if (UI) { UI.setPhase('off'); UI.setConnected(false); UI.log('no token'); } |
|
return; |
|
} |
|
markExistingAsProcessed(); |
|
obs.observe(document.documentElement, { |
|
childList: true, |
|
subtree: true, |
|
characterData: true |
|
}); |
|
console.log("[dsh-memory] content.js active"); |
|
if (UI) { UI.setPhase('off'); UI.setConnected(true); canStart = true; UI.log('ready, press Start'); } __refreshStats(); |
|
}); |
|
}); |
|
function __refreshStats() { try { var SB = globalThis.STORAGE_BRIDGE; if (!SB || !UI || !UI.setStats) return; SB.getState().then(function (st) { var mN = st && st.memories ? Object.keys(st.memories).length : 0; var sN = st && st.skills && st.skills.length ? st.skills.length : 0; return SB.authHeaders().then(function (h) { return fetch(SB.BU + '/health', { headers: h }); }).then(function (r) { return r.json(); }).then(function (j) { UI.setStats((j && j.workspace) || '?', mN, sN); }); }).catch(function () { UI.setStats('?', 0, 0); }); } catch (e) { console.warn('[dsh-memory] stats ' + String(e)); } } |
|
})();
|
|
|