Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Turn ideas into validated designs and specs through collaborative dialogue before any code is written
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/helper.js
1(function() {2const MIN_RECONNECT_MS = 500;3const MAX_RECONNECT_MS = 30000;4const TOMBSTONE_AFTER_MS = 15000; // show the "paused" overlay after this long disconnected56// Pure: next backoff delay (doubles, capped). Exported for unit tests.7function nextReconnectDelay(current, max) {8return Math.min(current * 2, max);9}10if (typeof module !== 'undefined' && module.exports) {11module.exports = { nextReconnectDelay, MIN_RECONNECT_MS, MAX_RECONNECT_MS, TOMBSTONE_AFTER_MS };12}1314// Everything below is browser-only; bail out when loaded in Node (tests).15if (typeof window === 'undefined') return;1617let ws = null;18let eventQueue = [];19let reconnectDelay = MIN_RECONNECT_MS;20let reconnectTimer = null;21let disconnectedSince = null;22let everConnected = false;23let tombstoneShown = false;2425function sessionKey() {26try {27return window.sessionStorage && window.sessionStorage.getItem('brainstorm-session-key');28} catch (e) {}29return null;30}3132function websocketUrl() {33const key = sessionKey();34return 'ws://' + window.location.host + (key ? '/?key=' + encodeURIComponent(key) : '');35}3637function reloadAfterRecovery() {38const key = sessionKey();39if (key) {40window.location.replace('/?key=' + encodeURIComponent(key));41} else {42window.location.reload();43}44}4546// Reflect connection state in the frame's status pill (absent on full-doc screens).47function setStatus(state) {48const el = document.querySelector('.status');49if (!el) return;50const map = {51connecting: ['Connecting…', 'var(--text-tertiary)'],52connected: ['Connected', 'var(--success)'],53reconnecting: ['Reconnecting…', 'var(--warning)'],54disconnected: ['Disconnected', 'var(--error)']55};56const [text, color] = map[state] || map.disconnected;57el.textContent = text;58el.style.setProperty('--status-color', color);59}6061// Self-styled so it works on framed and full-document screens alike.62function showTombstone() {63if (tombstoneShown) return;64tombstoneShown = true;65const el = document.createElement('div');66el.id = 'bs-tombstone';67el.style.cssText = 'position:fixed;inset:0;z-index:99999;display:flex;' +68'align-items:center;justify-content:center;padding:2rem;text-align:center;' +69'background:rgba(20,20,22,0.92);color:#f5f5f7;font-family:system-ui,sans-serif';70el.innerHTML = '<div style="max-width:480px">' +71'<h2 style="margin:0 0 .5rem;font-weight:600">Companion paused</h2>' +72'<p style="margin:0;opacity:.85">This brainstorm companion has stopped. ' +73'Ask your coding agent to bring it back — this page reconnects automatically.</p></div>';74if (document.body) document.body.appendChild(el);75}7677function connect() {78if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }79setStatus(everConnected ? 'reconnecting' : 'connecting');80ws = new WebSocket(websocketUrl());8182ws.onopen = () => {83const recovered = tombstoneShown;84everConnected = true;85disconnectedSince = null;86reconnectDelay = MIN_RECONNECT_MS;87tombstoneShown = false;88setStatus('connected');89eventQueue.forEach(e => ws.send(JSON.stringify(e)));90eventQueue = [];91// Recovered from a tombstoned outage (e.g. the server restarted on the same92// port) — reload through the keyed bootstrap when possible so the cookie is93// refreshed before the visible URL returns to bare /.94if (recovered) reloadAfterRecovery();95};9697ws.onmessage = (msg) => {98let data;99try { data = JSON.parse(msg.data); } catch (e) { return; }100if (data.type === 'reload') window.location.reload();101};102103ws.onclose = () => {104ws = null;105if (disconnectedSince === null) disconnectedSince = Date.now();106if (Date.now() - disconnectedSince >= TOMBSTONE_AFTER_MS) {107setStatus('disconnected');108showTombstone();109} else {110setStatus('reconnecting');111}112reconnectTimer = setTimeout(connect, reconnectDelay);113reconnectDelay = nextReconnectDelay(reconnectDelay, MAX_RECONNECT_MS);114};115116// Let onclose own reconnection so we don't schedule it twice.117ws.onerror = () => { try { ws.close(); } catch (e) {} };118}119120function sendEvent(event) {121event.timestamp = Date.now();122if (ws && ws.readyState === WebSocket.OPEN) {123ws.send(JSON.stringify(event));124} else {125eventQueue.push(event);126}127}128129// Capture clicks on choice elements130document.addEventListener('click', (e) => {131const target = e.target.closest('[data-choice]');132if (!target) return;133134sendEvent({135type: 'click',136text: target.textContent.trim(),137choice: target.dataset.choice,138id: target.id || null139});140141});142143// Frame UI: selection tracking144window.selectedChoice = null;145146window.toggleSelect = function(el) {147const container = el.closest('.options') || el.closest('.cards');148const multi = container && container.dataset.multiselect !== undefined;149if (container && !multi) {150container.querySelectorAll('.option, .card').forEach(o => o.classList.remove('selected'));151}152if (multi) {153el.classList.toggle('selected');154} else {155el.classList.add('selected');156}157window.selectedChoice = el.dataset.choice;158};159160// Expose API for explicit use161window.brainstorm = {162send: sendEvent,163choice: (value, metadata = {}) => sendEvent({ type: 'choice', value, ...metadata })164};165166connect();167})();168