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.
 
 
 
 
 

313 lines
18 KiB

'use strict';
(function () {
var UI_ID = 'dshmem-panel';
var LOG_CAP = 60;
var state = {
phase: 'off',
connected: false,
loopsLeft: 0, loopsMax: 0,
dailyCount: 0, dailyMax: 0,
resetCbs: [], stopCbs: [], saveCbs: [], startCbs: [], dotMode: null, transport: 'dom', accLimit: 900000, lastMetrics: null, forceInject: false, mcpTimer: null, togglePending: false
};
var el = {};
function byId(id) { return document.getElementById(id); }
function mk(tag, cls, txt) {
var n = document.createElement(tag);
if (cls) n.className = cls;
if (txt !== undefined) n.textContent = txt;
return n;
}
function build() {
if (byId(UI_ID)) return;
var root = mk('div', 'dshmem-panel');
root.id = UI_ID;
var head = mk('div', 'dshmem-head');
el.dot = mk('span', 'dshmem-dot');
el.dot.id = 'dshmem-dot';
var title = mk('span', 'dshmem-title', 'DSH Memory');
el.phase = mk('span', 'dshmem-phase', 'Off');
el.phase.id = 'dshmem-phase';
el.collapse = mk('span', 'dshmem-toggle', '-');
head.appendChild(el.dot); head.appendChild(title); head.appendChild(el.phase); head.appendChild(el.collapse);
el.body = mk('div', 'dshmem-body');
root.appendChild(head); root.appendChild(el.body);
el.root = root; el.head = head;
var row1 = mk('div', 'dshmem-row');
el.toggle = mk('button', 'dshmem-btn primary', 'Start'); el.restart = mk('button', 'dshmem-btn', 'Restart');
row1.appendChild(el.toggle); row1.appendChild(el.restart);
el.body.appendChild(row1); el.stats = mk('div','dshmem-stats'); el.stats.id='dshmem-stats'; el.body.appendChild(el.stats); el.metrics = mk('div','dshmem-stats'); el.metrics.id='dshmem-metrics'; el.metrics.textContent='metrics: —'; el.body.appendChild(el.metrics);
var setBox = mk('div', 'dshmem-fold-box');
el.settingsHead = mk('div', 'dshmem-fold-head');
el.settingsArrow = mk('span', 'dshmem-fold-arrow', '\u25B6');
el.settingsHead.appendChild(el.settingsArrow);
el.settingsHead.appendChild(mk('span', '', 'Settings'));
setBox.appendChild(el.settingsHead);
var set = mk('div', 'dshmem-settings dshmem-fold-body'); el.settings = set; el.settingsBox = setBox;
var lbl = mk('div', 'dshmem-label', 'Token (save, then reload page)');
el.token = mk('input', 'dshmem-input');
el.token.type = 'text'; el.token.autocomplete = 'off'; el.token.setAttribute('autocomplete', 'off');
el.token.placeholder = 'dshmem-bridge-token';
var row2 = mk('div', 'dshmem-row');
el.save = mk('button', 'dshmem-btn primary', 'Save');
el.health = mk('button', 'dshmem-btn', 'Health');
row2.appendChild(el.save); row2.appendChild(el.health); el.stg = mk('button', 'dshmem-btn stg', 'STG'); row2.appendChild(el.stg); el.transportToggle = mk('button', 'dshmem-btn danger', 'DOM'); row2.appendChild(el.transportToggle); var lblAcc = mk('div', 'dshmem-label', 'Acc limit (100%)'); set.appendChild(lblAcc); el.accLimit = mk('input', 'dshmem-input'); el.accLimit.type = 'number'; el.accLimit.autocomplete = 'off'; el.accLimit.setAttribute('autocomplete', 'off'); el.accLimit.autocomplete = 'off'; el.accLimit.setAttribute('autocomplete', 'off'); el.accLimit.placeholder = '900000'; el.accLimit.value = '900000'; set.appendChild(el.accLimit);
set.appendChild(lbl); set.appendChild(el.token); set.appendChild(row2);
setBox.appendChild(set);
el.body.appendChild(setBox);
el.log = mk('div', 'dshmem-log');
el.log.id = 'dshmem-log';
el.body.appendChild(el.log);
el.preview = mk('div', 'dshmem-preview');
el.preview.style.display = 'none';
el.body.appendChild(el.preview);
el.confirm = mk('div', 'dshmem-confirm-box');
el.confirm.style.display = 'none';
el.body.appendChild(el.confirm);
// ArtCraft MCP box
el.mcpBox = mk('div', 'dshmem-mcp-box');
el.mcpHead = mk('div', 'dshmem-mcp-head');
el.mcpArrow = mk('span', 'dshmem-mcp-arrow', '\u25B6');
el.mcpHead.appendChild(el.mcpArrow);
el.mcpHead.appendChild(mk('span', '', 'ArtCraft'));
el.mcpCount = mk('span', 'dshmem-mcp-count', '');
el.mcpHead.appendChild(el.mcpCount);
el.mcpBox.appendChild(el.mcpHead);
el.mcpBody = mk('div', 'dshmem-mcp-body');
var pathRow = mk('div', 'dshmem-mcp-pathrow');
el.mcpPath = mk('input', 'dshmem-input');
el.mcpPath.type = 'text'; el.mcpPath.placeholder = 'V:\\artcraft';
el.mcpRescan = mk('button', 'dshmem-btn', '\u21BB');
el.mcpRescan.title = 'Rescan folder';
pathRow.appendChild(el.mcpPath); pathRow.appendChild(el.mcpRescan);
el.mcpBody.appendChild(pathRow);
el.mcpList = mk('div', 'dshmem-mcp-list');
el.mcpList.textContent = 'click to load';
el.mcpBody.appendChild(el.mcpList);
el.mcpBox.appendChild(el.mcpBody);
el.body.appendChild(el.mcpBox);
document.body.appendChild(root);
}
function wire() {
el.head.addEventListener('click', function () {
var min = el.root.classList.toggle('dshmem-min');
if (el.collapse) el.collapse.textContent = min ? '+' : '-';
});
el.settingsHead.addEventListener('click', function () {
var open = el.settingsBox.classList.toggle('open');
el.settingsArrow.textContent = open ? '\u25BC' : '\u25B6';
}); el.transportToggle.addEventListener('click', function () { state.transport = state.transport === 'dom' ? 'sse' : 'dom'; el.transportToggle.textContent = state.transport === 'dom' ? 'DOM' : 'SSE'; try { chrome.storage.local.set({ 'dshmem-transport': state.transport }) } catch (e) { } });
el.toggle.addEventListener('click', function () {
if (state.togglePending) return;
state.togglePending = true;
setTimeout(function () { state.togglePending = false; }, 400);
var running = (state.phase && state.phase !== 'off');
if (running) {
state.stopCbs.forEach(function (cb) { try { cb(); } catch (e) {} });
} else {
state.startCbs.forEach(function (cb) { try { cb(); } catch (e) {} });
}
});
el.restart.addEventListener('click', function () { API.restartBridge(); });
el.accLimit.addEventListener('change', function () { var n = parseInt(el.accLimit.value, 10); if (!n || n < 1000) n = 900000; state.accLimit = n; try { chrome.storage.local.set({ 'dshmem-acc-limit': n }) } catch (e) { } API.setMetrics(state.lastMetrics); }); el.save.addEventListener('click', function () { var v = el.token.value || ''; if (!v) { API.log('token not changed (empty)'); return; } state.saveCbs.forEach(function (cb) { try { cb(v); } catch (e) {} }); });
el.stg.addEventListener('click', function () { API.toggleForceInject(); });
el.health.addEventListener('click', function () {
API.log('health: click');
if (!globalThis.DSH_BRIDGE) { API.log('health: DSH_BRIDGE undefined (bridge-client.js not loaded?)'); return; }
if (typeof globalThis.DSH_BRIDGE.health !== 'function') { API.log('health: DSH_BRIDGE.health is not a function'); return; }
try { chrome.storage.local.get('dshmem-bridge-token', function (v) {
var t = v && v['dshmem-bridge-token'] ? String(v['dshmem-bridge-token']) : '';
API.log('health: token in storage len=' + t.length + (t ? ' head=' + t.slice(0, 8) + '...' : ' (empty)'));
globalThis.DSH_BRIDGE.health().then(function (r) {
if (r && r.ok) { API.setConnected(true); API.log('health ok, bridge up'); }
else { API.setConnected(false); API.log('health fail: ' + ((r && r.error && (r.error.code + ' ' + r.error.message)) || '?')); }
}).catch(function (e) { API.log('health promise reject: ' + String(e)); });
}); } catch (e) { API.log('health throw: ' + String(e)); }
});
el.mcpHead.addEventListener('click', function () {
var open = el.mcpBox.classList.toggle('open');
el.mcpArrow.textContent = open ? '\u25BC' : '\u25B6';
if (open) {
API.mcpRefresh();
if (state.mcpTimer) clearInterval(state.mcpTimer);
state.mcpTimer = setInterval(function () { API.mcpRefresh(); }, 5000);
} else if (state.mcpTimer) {
clearInterval(state.mcpTimer); state.mcpTimer = null;
}
});
el.mcpRescan.addEventListener('click', function () { API.mcpRescan(); });
}
var API = {
setPhase: function (p) {
state.phase = p;
if (el.phase) el.phase.textContent = p;
if (el.toggle) {
var running = (p && p !== 'off');
el.toggle.textContent = running ? 'Stop' : 'Start';
el.toggle.className = 'dshmem-btn ' + (running ? 'danger' : 'primary');
}
},
setConnected: function (b) { state.connected = !!b; API._applyDot(); }, setDot: function (m) { state.dotMode = m || null; API._applyDot(); }, _applyDot: function () { if (!el.dot) return; var cls = 'dshmem-dot'; if (!state.connected) { cls += ' err'; } else { var m = state.dotMode || 'off'; if (m === 'busy' || m === 'ok-busy') cls += ' busy'; else if (m === 'error') cls += ' error'; else if (m === 'stopped') cls += ' stopped'; else if (m === 'ok') cls += ' ok'; else cls += ' off'; } el.dot.className = cls; },
setStartEnabled: function (b) { if (el.toggle) el.toggle.disabled = !b; }, setStats: function (ws, memN, skillN) { if (!el.stats) return; el.stats.textContent = 'WS: ' + (ws || '?') + ' mem: ' + (memN || 0) + ' skills: ' + (skillN || 0); }, setMetrics: function (m) { if (!el.metrics) return; if (!m) { el.metrics.textContent = 'metrics: —'; return; } state.lastMetrics = m; var __pct = 0; if (state.accLimit > 0) __pct = Math.round((m.accum || 0) * 100 / state.accLimit); var __cls = 'dshmem-acc-ok'; if (__pct > 100) __cls = 'dshmem-acc-crit'; else if (__pct >= 80) __cls = 'dshmem-acc-err'; else if (__pct >= 40) __cls = 'dshmem-acc-warn'; el.metrics.className = 'dshmem-stats ' + __cls; el.metrics.textContent = 'acc: ' + m.accum + ' · turn: ' + m.delta + ' · msg: ' + m.chars + 'ch (~' + m.est_tok + 't) ctx ' + (m.ctx_tok || 0) + 't ' + (m.ctx_pct || 0) + 'pct'; },
toggleForceInject: function () {
state.forceInject = !state.forceInject;
try { chrome.storage.local.set({ 'dshmem-force-inject': state.forceInject }) } catch (e) { }
API._applyStg();
API.log('force inject: ' + (state.forceInject ? 'ARMED - next send will inject' : 'off'));
},
_applyStg: function () {
if (!el.stg) return;
var cls = 'dshmem-btn stg' + (state.forceInject ? ' armed' : '');
el.stg.className = cls;
},
log: function (m) {
if (!el.log) return;
var t = new Date().toLocaleTimeString();
var line = '[' + t + '] ' + m;
var txt = (line + String.fromCharCode(10) + (el.log.textContent || '')).split(String.fromCharCode(10)).slice(0, LOG_CAP).join(String.fromCharCode(10));
el.log.textContent = txt;
},
mcpRender: function (data) {
if (!el.mcpList) return;
var apps = (data && data.apps) || [];
if (data && data.root && el.mcpPath && !el.mcpPath.value) el.mcpPath.value = data.root;
if (el.mcpCount) el.mcpCount.textContent = apps.length ? (apps.length + ' app' + (apps.length === 1 ? '' : 's')) : '';
el.mcpList.textContent = '';
if (!apps.length) { el.mcpList.appendChild(mk('div', 'dshmem-mcp-empty', 'no apps found')); return; }
apps.forEach(function (a) {
var row = mk('div', 'dshmem-mcp-row');
var dotCls = 'dshmem-mcp-dot';
if (a.launching || a.starting) dotCls += ' busy';
else if (a.lastError) dotCls += ' err';
else if (a.guiUp || a.running) dotCls += ' on';
var dot = mk('span', dotCls);
row.appendChild(dot);
var nameEl = mk('span', 'dshmem-mcp-name', a.name);
var parts = [a.name];
if (a.port != null) parts.push('port ' + a.port);
if (a.guiUp) parts.push('window up');
if (a.running) parts.push('client up, ' + a.tools + ' tools');
if (!a.connectKnown) parts.push('no live control (headless)');
if (a.lastError) parts.push('error: ' + a.lastError);
nameEl.title = parts.join(' | ');
row.appendChild(nameEl);
var statusTxt = '';
if (a.running) statusTxt = a.tools + 't';
else if (a.guiUp) statusTxt = 'win';
else if (a.lastError) statusTxt = 'err';
row.appendChild(mk('span', 'dshmem-mcp-tools', statusTxt));
if (a.canLaunch) {
var lb = mk('button', 'dshmem-mcp-btn', a.launching ? '...' : (a.guiUp ? '\u2713' : '\u23FB'));
lb.title = a.guiUp ? (a.name + ' window is already open on port ' + a.port) : ('Launch ' + a.name + ' window (port ' + a.port + ')');
lb.disabled = a.launching;
lb.addEventListener('click', function (ev) { ev.stopPropagation(); API.mcpLaunch(a.name); });
row.appendChild(lb);
}
var sb = mk('button', 'dshmem-mcp-btn', a.running ? 'stop' : (a.guiUp ? 'close' : 'stop'));
sb.title = a.running ? 'Close MCP client (window stays)' : (a.guiUp ? 'Close the ' + a.name + ' window' : 'Nothing to close');
sb.disabled = !a.running && !a.starting && !a.guiUp;
sb.addEventListener('click', function (ev) { ev.stopPropagation(); API.mcpStop(a.name); });
row.appendChild(sb);
el.mcpList.appendChild(row);
if (a.lastError) el.mcpList.appendChild(mk('div', 'dshmem-mcp-err', a.lastError.slice(0, 160)));
});
},
mcpRefresh: function () {
try {
globalThis.DSH_BRIDGE.fetchBridge('GET', '/mcp/apps').then(function (r) {
if (r && r.ok) { API.mcpRender(r); }
else { if (el.mcpList) el.mcpList.textContent = 'bridge: ' + ((r && r.error && r.error.message) || 'fail'); }
});
} catch (e) { if (el.mcpList) el.mcpList.textContent = 'error: ' + String(e); }
},
mcpRescan: function () {
var p = el.mcpPath && el.mcpPath.value ? String(el.mcpPath.value).trim() : '';
var call = p ? globalThis.DSH_BRIDGE.fetchBridge('POST', '/mcp/root', { path: p }) : globalThis.DSH_BRIDGE.fetchBridge('POST', '/mcp/rescan');
Promise.resolve(call).then(function (r) {
if (r && r.ok) { API.mcpRender({ root: r.root, apps: r.apps }); API.log('mcp rescan: ' + ((r.apps || []).length) + ' apps'); }
else { API.log('mcp rescan fail: ' + ((r && r.error && r.error.message) || '?')); }
});
},
mcpLaunch: function (name) {
API.log('mcp launch: ' + name);
globalThis.DSH_BRIDGE.fetchBridge('POST', '/mcp/launch', { app: name }).then(function (r) {
if (r && r.ok) API.log('mcp launch ' + name + ': ' + (r.already ? 'already up' : 'started') + (r.port != null ? ' port ' + r.port : ''));
else API.log('mcp launch ' + name + ' fail: ' + ((r && r.error && r.error.message) || '?'));
API.mcpRefresh();
});
},
mcpStop: function (name) {
globalThis.DSH_BRIDGE.fetchBridge('POST', '/mcp/stop', { app: name }).then(function (r) {
if (!r || !r.ok) { API.log('mcp stop ' + name + ' fail: ' + ((r && r.error && r.error.message) || '?')); API.mcpRefresh(); return; }
var what = [];
if (r.stopped) what.push('client');
if (r.windowClosed) what.push('window');
if (what.length === 0) what.push('nothing');
API.log('mcp stop ' + name + ': closed ' + what.join(' + ') + (r.warning ? ' | ' + r.warning : ''));
API.mcpRefresh();
});
},
restartBridge: function () {
API.log('restart: sending shutdown with restart=1');
globalThis.DSH_BRIDGE.fetchBridge('POST', '/shutdown', { restart: true }).then(function (r) {
if (r && r.ok) { API.log('restart: bridge is going down, supervisor will bring it back in ~5s'); }
else { API.log('restart fail: ' + ((r && r.error && r.error.message) || 'bridge unreachable')); }
var tries = 0;
var iv = setInterval(function () {
tries++;
globalThis.DSH_BRIDGE.health().then(function (h) {
if (h && h.ok) { clearInterval(iv); API.setConnected(true); API.log('restart: bridge is back'); }
else if (tries >= 8) { clearInterval(iv); API.log('restart: still down after ~24s, check the supervisor window'); }
});
}, 3000);
});
},
onResetLoops: function (cb) { state.resetCbs.push(cb); },
onStop: function (cb) { state.stopCbs.push(cb); }, onStart: function (cb) { state.startCbs.push(cb); },
onSaveToken: function (cb) { state.saveCbs.push(cb); },
getToken: function () { return el.token ? el.token.value : ''; }, getTransport: function () { return state.transport; }, setTransport: function (v) { state.transport = v === 'sse' ? 'sse' : 'dom'; if (el.transportToggle) el.transportToggle.textContent = state.transport === 'dom' ? 'DOM' : 'SSE'; },
setToken: function (v) { if (el.token) el.token.value = v || ''; },
showConfirm: function (opt) {
if (!el.confirm) return;
opt = opt || {};
el.confirm.textContent = '';
el.confirm.appendChild(mk('div', 'dshmem-confirm-title', opt.title || 'Confirm'));
el.confirm.appendChild(mk('div', 'dshmem-label', opt.detail || ''));
var r = mk('div', 'dshmem-row');
var ok = mk('button', 'dshmem-btn primary', 'Allow');
var no = mk('button', 'dshmem-btn danger', 'Deny');
ok.addEventListener('click', function () { API.hideConfirm(); if (opt.onAllow) opt.onAllow(); });
no.addEventListener('click', function () { API.hideConfirm(); if (opt.onDeny) opt.onDeny(); });
r.appendChild(ok); r.appendChild(no); el.confirm.appendChild(r);
el.confirm.style.display = 'block';
},
hideConfirm: function () { if (el.confirm) el.confirm.style.display = 'none'; },
showPreview: function (opt) {
if (!el.preview) return;
opt = opt || {};
var sec = opt.seconds || 2;
var text = opt.text || '';
var cancelled = false;
el.preview.style.display = 'block';
var render = function () { el.preview.textContent = 'Sending in ' + sec + 's (click to cancel):' + String.fromCharCode(10) + text.slice(0, 500); };
render();
var iv = setInterval(function () {
sec--;
if (sec <= 0) { clearInterval(iv); el.preview.style.display = 'none'; el.preview.onclick = null; if (!cancelled && opt.onSend) opt.onSend(); }
else render();
}, 1000);
el.preview.onclick = function () { cancelled = true; clearInterval(iv); el.preview.style.display = 'none'; el.preview.onclick = null; if (opt.onCancel) opt.onCancel(); };
},
hidePreview: function () { if (el.preview) el.preview.style.display = 'none'; },
init: function () {
try { build(); } catch (e) { console.error('[dshmem] build failed: ' + e); }
try { wire(); } catch (e) { console.error('[dshmem] wire failed: ' + e); API.log('wire failed: ' + String(e)); }
try { chrome.storage.local.get('dshmem-bridge-token', function (v) { var t = v && v['dshmem-bridge-token'] ? String(v['dshmem-bridge-token']) : ''; if (t && el.token) el.token.value = t; API.log('panel loaded, token in storage len=' + t.length); }) } catch (e) { }
try { chrome.storage.local.get('dshmem-transport', function (v) { if (v && v['dshmem-transport']) { var tv = v['dshmem-transport']; if (tv === 'msg') tv = 'dom'; if (tv === 'xhr') tv = 'sse'; API.setTransport(tv) } }) } catch (e) { } try { chrome.storage.local.get('dshmem-acc-limit', function (v) { var n = v && v['dshmem-acc-limit'] ? parseInt(v['dshmem-acc-limit'], 10) : 0; if (n && n >= 1000) { state.accLimit = n; if (el.accLimit) el.accLimit.value = String(n); API.setMetrics(state.lastMetrics); } }) } catch (e) { }
try { chrome.storage.local.get('dshmem-force-inject', function (v) { state.forceInject = Boolean(v && v['dshmem-force-inject']); API._applyStg(); }) } catch (e) { }
try { chrome.storage.onChanged.addListener(function (ch, area) { if (area !== 'local') return; if (ch['dshmem-force-inject']) { state.forceInject = Boolean(ch['dshmem-force-inject'].newValue); API._applyStg(); } }); } catch (e) { } }
};
globalThis.DSHMEM = API;
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () { API.init(); });
} else { API.init(); }
})();