openzoo 0.48.7 → 0.48.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/grokui.mjs +80 -3
- package/package.json +1 -1
package/lib/grokui.mjs
CHANGED
|
@@ -524,6 +524,18 @@ const SLASH_COMMANDS = [
|
|
|
524
524
|
|
|
525
525
|
const usd = (n) => (n >= 0.01 || n === 0 ? '$' + n.toFixed(2) : '$' + n.toFixed(5));
|
|
526
526
|
|
|
527
|
+
// threadId -> open SSE responses. A Set because the same thread can be open in
|
|
528
|
+
// two tabs, and both should see the same tokens.
|
|
529
|
+
const streamListeners = new Map();
|
|
530
|
+
function emitToThread(threadId, ev) {
|
|
531
|
+
const set = streamListeners.get(threadId);
|
|
532
|
+
if (!set?.size) return; // nobody watching — free
|
|
533
|
+
const line = `data: ${JSON.stringify(ev)}\n\n`;
|
|
534
|
+
for (const res of set) {
|
|
535
|
+
try { res.write(line); } catch { set.delete(res); }
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
527
539
|
async function sessionStats() {
|
|
528
540
|
try { return await (await fetch(`${PROXY}/session`)).json(); }
|
|
529
541
|
catch { return null; }
|
|
@@ -1765,10 +1777,45 @@ const APP_HTML = `<!doctype html>
|
|
|
1765
1777
|
addRow(h.who, h.text, h.color || t.color, h.name || t.name,
|
|
1766
1778
|
h.runId ? { id: h.runId, status: h.runStatus, output: h.runOutput } : undefined, h.images);
|
|
1767
1779
|
}
|
|
1768
|
-
if (full.status === 'thinking')
|
|
1780
|
+
if (full.status === 'thinking') {
|
|
1781
|
+
addRow('bot', streamBuf || '…', t.color, t.name);
|
|
1782
|
+
// Tag the live bubble so deltas can repaint just this node instead of
|
|
1783
|
+
// re-rendering (and re-fetching) the whole thread on every token.
|
|
1784
|
+
const b = log.querySelector('.row:last-child .bubble');
|
|
1785
|
+
if (b) b.id = 'streamBubble';
|
|
1786
|
+
}
|
|
1769
1787
|
if (wasNearBottom) log.scrollTop = log.scrollHeight;
|
|
1770
1788
|
}
|
|
1771
1789
|
|
|
1790
|
+
// --- live token stream ---------------------------------------------------
|
|
1791
|
+
// The server has always been able to stream; /drive just never asked for it,
|
|
1792
|
+
// so a turn showed "…" for its whole duration and then arrived in one lump.
|
|
1793
|
+
let streamBuf = '';
|
|
1794
|
+
let es = null, esId = null;
|
|
1795
|
+
function paintStream() {
|
|
1796
|
+
const b = document.getElementById('streamBubble');
|
|
1797
|
+
if (!b) { render(); return; }
|
|
1798
|
+
// textContent, not markdown: the partial text is frequently mid-fence or
|
|
1799
|
+
// mid-link, and half-parsed markdown flickers. The final render formats it.
|
|
1800
|
+
b.textContent = streamBuf || '…';
|
|
1801
|
+
if (log.scrollHeight - log.scrollTop - log.clientHeight < 140) log.scrollTop = log.scrollHeight;
|
|
1802
|
+
}
|
|
1803
|
+
function connectStream(id) {
|
|
1804
|
+
if (!id || esId === id) return;
|
|
1805
|
+
if (es) es.close();
|
|
1806
|
+
esId = id;
|
|
1807
|
+
streamBuf = '';
|
|
1808
|
+
es = new EventSource('/stream/' + id); // EventSource reconnects on its own
|
|
1809
|
+
es.onmessage = (e) => {
|
|
1810
|
+
let ev;
|
|
1811
|
+
try { ev = JSON.parse(e.data); } catch { return; }
|
|
1812
|
+
if (ev.type === 'start') { streamBuf = ''; paintStream(); }
|
|
1813
|
+
else if (ev.type === 'delta') { streamBuf += ev.delta || ''; paintStream(); }
|
|
1814
|
+
else if (ev.type === 'final' || ev.type === 'run-pending') { streamBuf = ''; render(); }
|
|
1815
|
+
};
|
|
1816
|
+
es.onerror = () => { /* EventSource retries; the 1.2s poll is the backstop */ };
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1772
1819
|
let pendingFiles = [];
|
|
1773
1820
|
let pendingImages = [];
|
|
1774
1821
|
const attachChips = document.getElementById('attachChips');
|
|
@@ -2011,7 +2058,7 @@ const APP_HTML = `<!doctype html>
|
|
|
2011
2058
|
}
|
|
2012
2059
|
});
|
|
2013
2060
|
|
|
2014
|
-
async function tick() { await loadThreads(); await render(); }
|
|
2061
|
+
async function tick() { connectStream(activeId); await loadThreads(); await render(); }
|
|
2015
2062
|
tick();
|
|
2016
2063
|
setInterval(tick, 1200);
|
|
2017
2064
|
|
|
@@ -2094,6 +2141,34 @@ const server = http.createServer((req, res) => {
|
|
|
2094
2141
|
})();
|
|
2095
2142
|
return;
|
|
2096
2143
|
}
|
|
2144
|
+
// LIVE TOKENS. runTurn has always been able to stream — it takes an onEvent
|
|
2145
|
+
// and calls brainStream — but /drive never passed one, so every turn used
|
|
2146
|
+
// the non-streaming brain() and the UI just polled /threads every 1.2s.
|
|
2147
|
+
// The user watched a "…" bubble for the whole generation and then got the
|
|
2148
|
+
// answer in one lump. Same work, all of the latency, none of the feedback.
|
|
2149
|
+
const sse = /^\/stream\/([^/?]+)$/.exec(req.url || '');
|
|
2150
|
+
if (req.method === 'GET' && sse) {
|
|
2151
|
+
const id = sse[1];
|
|
2152
|
+
res.writeHead(200, {
|
|
2153
|
+
'content-type': 'text/event-stream',
|
|
2154
|
+
'cache-control': 'no-cache',
|
|
2155
|
+
connection: 'keep-alive',
|
|
2156
|
+
// Proxies in front of a box (RunPod, nginx) will happily buffer an
|
|
2157
|
+
// event stream into nothing until it ends, which looks exactly like
|
|
2158
|
+
// streaming being broken.
|
|
2159
|
+
'x-accel-buffering': 'no',
|
|
2160
|
+
});
|
|
2161
|
+
res.write(': open\n\n');
|
|
2162
|
+
if (!streamListeners.has(id)) streamListeners.set(id, new Set());
|
|
2163
|
+
streamListeners.get(id).add(res);
|
|
2164
|
+
const ka = setInterval(() => { try { res.write(': ka\n\n'); } catch { /* gone */ } }, 20000);
|
|
2165
|
+
req.on('close', () => {
|
|
2166
|
+
clearInterval(ka);
|
|
2167
|
+
streamListeners.get(id)?.delete(res);
|
|
2168
|
+
if (!streamListeners.get(id)?.size) streamListeners.delete(id);
|
|
2169
|
+
});
|
|
2170
|
+
return;
|
|
2171
|
+
}
|
|
2097
2172
|
// One source of truth for the composer's autocomplete — a hand-kept menu in
|
|
2098
2173
|
// the client would drift from what the server actually handles.
|
|
2099
2174
|
if (req.method === 'GET' && req.url === '/slash-commands') {
|
|
@@ -2231,7 +2306,9 @@ const server = http.createServer((req, res) => {
|
|
|
2231
2306
|
saveThreads();
|
|
2232
2307
|
return;
|
|
2233
2308
|
}
|
|
2234
|
-
|
|
2309
|
+
// Stream to whoever is watching this thread. emitToThread is a no-op
|
|
2310
|
+
// when nobody is, so a spawned subagent nobody has open costs nothing.
|
|
2311
|
+
runTurn(threadId, task, (ev) => emitToThread(threadId, ev), images).catch(() => {});
|
|
2235
2312
|
});
|
|
2236
2313
|
return;
|
|
2237
2314
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.48.
|
|
3
|
+
"version": "0.48.8",
|
|
4
4
|
"description": "Local x402-paying proxy + MCP server for openzoo.fun — point any OpenAI-compatible harness (Cursor, Claude Code, aider, SDKs) at localhost and it pays per call from a local burner wallet. Solana and Base rails live; Robinhood experimental.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|