openzoo 0.48.82 → 0.48.83
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 +186 -71
- package/package.json +1 -1
package/lib/grokui.mjs
CHANGED
|
@@ -497,17 +497,25 @@ function newGroupThread(names) {
|
|
|
497
497
|
// chunk after the first carries the PREVIOUS chunk's context_id so the
|
|
498
498
|
// sidecar appends to the same bound context instead of starting fresh.
|
|
499
499
|
const BIND_CHUNK_BYTES = 512 * 1024;
|
|
500
|
-
//
|
|
501
|
-
//
|
|
502
|
-
//
|
|
503
|
-
// the
|
|
504
|
-
//
|
|
505
|
-
//
|
|
506
|
-
const AUTO_MAX_STEPS = Number(process.env.OZ_AUTO_MAX_STEPS ||
|
|
500
|
+
// Soft hop counter per user message. Each hop is a paid call. 8 parked a real
|
|
501
|
+
// job mid-toolchain and told the user to type "continue" — the thing auto
|
|
502
|
+
// exists to avoid. 60 was still a "type continue" trap on anything with a
|
|
503
|
+
// crew. Hitting the number now INJECTS continue and keeps going until the
|
|
504
|
+
// model emits DONE or the user flips to ask. The env var is a nudge interval,
|
|
505
|
+
// not a stop.
|
|
506
|
+
const AUTO_MAX_STEPS = Number(process.env.OZ_AUTO_MAX_STEPS || 500);
|
|
507
507
|
// An empty completion is a provider hiccup, not an answer. In auto we retry it
|
|
508
508
|
// silently rather than parking the thread behind a "say continue" note the
|
|
509
509
|
// user then has to answer — that turned a transient blip into a manual step.
|
|
510
510
|
const AUTO_EMPTY_RETRIES = Number(process.env.OZ_AUTO_EMPTY_RETRIES || 3);
|
|
511
|
+
const NUDGE = 'That reply announced work instead of doing it — no directive line reached the harness, '
|
|
512
|
+
+ 'so nothing ran. Emit the directive NOW, as the first line of your reply, with no preamble: '
|
|
513
|
+
+ 'RUN:, SPAWN:, READ:, WRITE:, GLOB:, FETCH:, MCP: or SERVE:. '
|
|
514
|
+
+ 'Exactly the syntax from your instructions — not [TOOL_CALL], not JSON, not a function-call envelope. '
|
|
515
|
+
+ 'If several steps are needed, emit the FIRST one; you get its real output back and continue from there.';
|
|
516
|
+
const AUTO_CONTINUE = 'AUTO is still on — do not stop and do not ask the user to type continue. '
|
|
517
|
+
+ 'Emit the next directive now (RUN:/SPAWN:/SEND:/READ:/WRITE:/GLOB:/FETCH:/MCP:/SERVE:), '
|
|
518
|
+
+ 'or DONE: if the job is actually finished.';
|
|
511
519
|
// Ceiling on subagents per thread. Spawning is fire-and-forget and each child
|
|
512
520
|
// can spawn too, so without a count it is unbounded — MEASURED as 15+ threads
|
|
513
521
|
// all named tetris-contract, every one of them a live agent making paid calls.
|
|
@@ -1345,17 +1353,81 @@ function unwrapDirectiveLine(reply) {
|
|
|
1345
1353
|
* last wrote to the parent, which is where constraints like "this is
|
|
1346
1354
|
* ProofNetwork" and "use token22 <mint>" always live.
|
|
1347
1355
|
*/
|
|
1348
|
-
function
|
|
1356
|
+
function isHarnessUserText(text) {
|
|
1357
|
+
return /^\((command output|directive result)\)/.test(String(text || ''))
|
|
1358
|
+
|| String(text || '') === NUDGE
|
|
1359
|
+
|| String(text || '') === AUTO_CONTINUE;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
function firstUserAsk(t) {
|
|
1363
|
+
return (t?.history || []).find((m) => m.who === 'user' && !isHarnessUserText(m.text));
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
function lastUserAsk(t) {
|
|
1367
|
+
return [...(t?.history || [])].reverse().find((m) => m.who === 'user' && !isHarnessUserText(m.text));
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
function siblingJob(sib) {
|
|
1371
|
+
const text = String(firstUserAsk(sib)?.text || '');
|
|
1372
|
+
const job = text.includes('--- your specific job ---')
|
|
1373
|
+
? text.split('--- your specific job ---').pop().split('--- your place in the team ---')[0]
|
|
1374
|
+
: text;
|
|
1375
|
+
return job.trim().replace(/\s+/g, ' ').slice(0, 400);
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
function spawnBrief(parent, { refresh = false } = {}) {
|
|
1349
1379
|
if (!parent) return '';
|
|
1350
|
-
const
|
|
1351
|
-
|
|
1352
|
-
const
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1380
|
+
const rootId = rootOf(parent).rootId;
|
|
1381
|
+
const root = threads.get(rootId) || parent;
|
|
1382
|
+
const rootAsk = String(firstUserAsk(root)?.text || '').trim();
|
|
1383
|
+
const latest = String(lastUserAsk(parent)?.text || '').trim();
|
|
1384
|
+
const recent = (parent.history || []).filter((m) => !isHarnessUserText(m.text)).slice(-8);
|
|
1385
|
+
const siblings = [...threads.values()].filter((x) => x.parent === parent.id);
|
|
1386
|
+
const cwd = parent.dir || WORKSPACE_DIR;
|
|
1387
|
+
const mode = parent.runMode || 'ask';
|
|
1388
|
+
const tier = parent.tier || 'medium';
|
|
1389
|
+
const race = Number(parent.race) || 0;
|
|
1390
|
+
const raceNeed = Number(parent.raceNeed) || 1;
|
|
1391
|
+
const model = parent.model || '';
|
|
1392
|
+
const lines = [
|
|
1393
|
+
refresh
|
|
1394
|
+
? 'CONTEXT REFRESH — you already exist; this is the current brief. Constraints (stack, token, MCP URL, what NOT to do) outrank your training prior.'
|
|
1395
|
+
: 'CONTEXT — this is what was asked of the team you were just spawned into. Constraints in here (the stack, addresses, what NOT to do) apply to you, and outrank any assumption you would otherwise make from your own training.',
|
|
1396
|
+
];
|
|
1397
|
+
// The root ask is kept WHOLE. A 4000-char slice ate "proofnetwork can do it
|
|
1398
|
+
// all" / token / MCP URL and the child invented a stack from training prior.
|
|
1399
|
+
if (rootAsk) {
|
|
1400
|
+
lines.push('', 'ROOT ASK — the original user request for this project (not a paraphrase):', rootAsk);
|
|
1401
|
+
}
|
|
1402
|
+
if (latest && latest !== rootAsk) {
|
|
1403
|
+
lines.push('', 'LATEST from the parent thread:', latest);
|
|
1404
|
+
}
|
|
1405
|
+
if (recent.length) {
|
|
1406
|
+
lines.push('', 'RECENT PARENT TURNS (what the orchestrator already decided):');
|
|
1407
|
+
for (const m of recent) {
|
|
1408
|
+
const who = m.who === 'user' ? 'user' : (m.name || parent.name);
|
|
1409
|
+
lines.push('[' + who + '] ' + String(m.text || '').trim().slice(0, 1200));
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
lines.push('', 'WORKING SET:');
|
|
1413
|
+
lines.push('cwd: ' + cwd);
|
|
1414
|
+
lines.push('run mode: ' + mode);
|
|
1415
|
+
lines.push('tier: ' + tier
|
|
1416
|
+
+ (race >= 2 ? ' · race ' + (raceNeed > 1 ? raceNeed + ' of ' + race : race) : '')
|
|
1417
|
+
+ (model ? ' · pinned model: ' + model : ''));
|
|
1418
|
+
if (siblings.length) {
|
|
1419
|
+
lines.push('', 'CREW — the split already happened. Do not re-split work they own:');
|
|
1420
|
+
for (const s of siblings) {
|
|
1421
|
+
const job = siblingJob(s);
|
|
1422
|
+
lines.push('- ' + s.name + ': ' + (job || '(no task recorded yet)'));
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
lines.push('', '--- your specific job ---', '');
|
|
1426
|
+
return lines.join('\n');
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
function childKickoff(parent, childName, task, { fresh = true } = {}) {
|
|
1430
|
+
return spawnBrief(parent, { refresh: !fresh }) + task + spawnPosition(parent, childName);
|
|
1359
1431
|
}
|
|
1360
1432
|
|
|
1361
1433
|
/**
|
|
@@ -1375,14 +1447,14 @@ function spawnBrief(parent) {
|
|
|
1375
1447
|
*/
|
|
1376
1448
|
function spawnPosition(parent, childName) {
|
|
1377
1449
|
if (!parent) return '';
|
|
1378
|
-
const siblings = [...threads.values()].filter((x) => x.parent === parent.id).map((x) => x.name);
|
|
1379
1450
|
const depth = rootOf(parent).depth + 1;
|
|
1380
|
-
const others =
|
|
1451
|
+
const others = [...threads.values()].filter((x) => x.parent === parent.id && x.name !== childName);
|
|
1381
1452
|
return '\n\n--- your place in the team ---\n'
|
|
1382
1453
|
+ `You are "${childName}", spawned by "${parent.name}". You are at tier ${depth} of this project.\n`
|
|
1383
1454
|
+ (others.length
|
|
1384
|
-
? `The work was ALREADY SPLIT before you existed. Your siblings under ${parent.name} are
|
|
1385
|
-
+ others.
|
|
1455
|
+
? `The work was ALREADY SPLIT before you existed. Your siblings under ${parent.name} are:\n`
|
|
1456
|
+
+ others.map((s) => `- ${s.name}: ${siblingJob(s) || '(no task recorded yet)'}`).join('\n')
|
|
1457
|
+
+ '\nThey hold the other slices. Yours is yours to BUILD.\n'
|
|
1386
1458
|
: '')
|
|
1387
1459
|
+ 'Do NOT re-split your own slice into more agents just because it has several parts — '
|
|
1388
1460
|
+ 'that is how a team becomes sixteen bots and zero artifacts. SPAWN only if your slice '
|
|
@@ -1489,8 +1561,7 @@ async function tryDirective(reply, originId) {
|
|
|
1489
1561
|
}
|
|
1490
1562
|
// Every thread now exists, so spawnPosition sees the COMPLETE cohort.
|
|
1491
1563
|
for (const { t: sub, task, fresh } of made) {
|
|
1492
|
-
runTurn(sub.id, (fresh
|
|
1493
|
-
+ (fresh ? spawnPosition(parent, sub.name) : '')).catch(() => {});
|
|
1564
|
+
runTurn(sub.id, childKickoff(parent, sub.name, task, { fresh })).catch(() => {});
|
|
1494
1565
|
}
|
|
1495
1566
|
const fresh = made.filter((m) => m.fresh).map((m) => m.t.name);
|
|
1496
1567
|
return [fresh.length ? `Spawned ${fresh.length} together (they can each see the full crew): ${fresh.join(', ')}` : '', ...notes]
|
|
@@ -1509,7 +1580,7 @@ async function tryDirective(reply, originId) {
|
|
|
1509
1580
|
// is what SEND already does.
|
|
1510
1581
|
const existing = findByName(name);
|
|
1511
1582
|
if (existing) {
|
|
1512
|
-
runTurn(existing.id, task).catch(() => {});
|
|
1583
|
+
runTurn(existing.id, childKickoff(threads.get(originId), existing.name, task, { fresh: false })).catch(() => {});
|
|
1513
1584
|
return `${name} already exists — sent it the task instead of spawning a duplicate.`;
|
|
1514
1585
|
}
|
|
1515
1586
|
// Storm guard. Fire-and-forget spawning is unbounded by construction: each
|
|
@@ -1522,7 +1593,7 @@ async function tryDirective(reply, originId) {
|
|
|
1522
1593
|
}
|
|
1523
1594
|
const sub = newThread(name, originId);
|
|
1524
1595
|
// The child gets the ORIGINATING brief plus its own job — see spawnBrief.
|
|
1525
|
-
runTurn(sub.id,
|
|
1596
|
+
runTurn(sub.id, childKickoff(threads.get(originId), name, task)).catch(() => {}); // fire and forget
|
|
1526
1597
|
return `Spawned ${name} — working on it.`;
|
|
1527
1598
|
}
|
|
1528
1599
|
// SEND TO A NAME THAT DOES NOT EXIST YET *SPAWNS* IT.
|
|
@@ -1551,7 +1622,7 @@ async function tryDirective(reply, originId) {
|
|
|
1551
1622
|
const msg = sendM[2].trim();
|
|
1552
1623
|
const target = findByName(name);
|
|
1553
1624
|
if (target) {
|
|
1554
|
-
runTurn(target.id, msg).catch(() => {});
|
|
1625
|
+
runTurn(target.id, childKickoff(threads.get(originId), target.name, msg, { fresh: false })).catch(() => {});
|
|
1555
1626
|
return `Messaged ${name}.`;
|
|
1556
1627
|
}
|
|
1557
1628
|
// The SAME storm guard SPAWN uses — promoting a SEND must not be a way
|
|
@@ -1563,7 +1634,7 @@ async function tryDirective(reply, originId) {
|
|
|
1563
1634
|
+ `(limit ${SPAWN_MAX_CHILDREN}). Reuse one with SEND: <existing name> | <task>.`;
|
|
1564
1635
|
}
|
|
1565
1636
|
const sub = newThread(name, originId);
|
|
1566
|
-
runTurn(sub.id,
|
|
1637
|
+
runTurn(sub.id, childKickoff(threads.get(originId), name, msg)).catch(() => {});
|
|
1567
1638
|
return `${name} did not exist — spawned it with that message as its task.`;
|
|
1568
1639
|
}
|
|
1569
1640
|
// PING had the same anchor bug — no `m`, so a PING after any preamble (or
|
|
@@ -2011,11 +2082,12 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
2011
2082
|
bindThread(t).catch(() => {});
|
|
2012
2083
|
return;
|
|
2013
2084
|
}
|
|
2014
|
-
// A real message from the user resets the auto budget AND the
|
|
2015
|
-
//
|
|
2016
|
-
//
|
|
2017
|
-
//
|
|
2018
|
-
|
|
2085
|
+
// A real message from the user resets the auto budget AND the announcement
|
|
2086
|
+
// nudge. Harness-injected hops (command output, directive result, nudge,
|
|
2087
|
+
// auto-continue) must not re-arm — that would make AUTO_MAX_STEPS a no-op
|
|
2088
|
+
// on the directive path (it used to reset every hop because only
|
|
2089
|
+
// "(command output)" was excluded).
|
|
2090
|
+
if (!isHarnessUserText(userText) && !/^\((command output|directive result)\)/.test(userText)) {
|
|
2019
2091
|
t.autoSteps = 0;
|
|
2020
2092
|
delete t.autoNudged;
|
|
2021
2093
|
}
|
|
@@ -2126,11 +2198,11 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
2126
2198
|
bindThread(t).catch(() => {});
|
|
2127
2199
|
runTurn(threadId, condense('(command output)', output), onEvent).catch(() => {});
|
|
2128
2200
|
} else {
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
saveThreads();
|
|
2201
|
+
// Do not park. A "say continue" note is the failure mode auto exists
|
|
2202
|
+
// to avoid — inject continue and keep going until DONE or ask.
|
|
2203
|
+
t.autoSteps = 0;
|
|
2133
2204
|
bindThread(t).catch(() => {});
|
|
2205
|
+
runTurn(threadId, AUTO_CONTINUE, onEvent).catch(() => {});
|
|
2134
2206
|
}
|
|
2135
2207
|
return;
|
|
2136
2208
|
}
|
|
@@ -2164,17 +2236,17 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
2164
2236
|
//
|
|
2165
2237
|
// Same budget as RUN (shared t.autoSteps, reset when the user speaks), so
|
|
2166
2238
|
// this cannot spend more than a chained RUN loop already could.
|
|
2167
|
-
if (t.runMode === 'auto' && ack !== null && ack !== undefined
|
|
2239
|
+
if (t.runMode === 'auto' && ack !== null && ack !== undefined
|
|
2240
|
+
&& !/^[ \t>*-]*DONE:/m.test(reply)) {
|
|
2168
2241
|
t.autoSteps = (t.autoSteps || 0) + 1;
|
|
2242
|
+
bindThread(t).catch(() => {}); // bind every hop, not just the last one
|
|
2169
2243
|
if (t.autoSteps < AUTO_MAX_STEPS) {
|
|
2170
|
-
bindThread(t).catch(() => {}); // bind every hop, not just the last one
|
|
2171
2244
|
runTurn(threadId, condense('(directive result)', ack), onEvent).catch(() => {});
|
|
2172
|
-
|
|
2245
|
+
} else {
|
|
2246
|
+
t.autoSteps = 0;
|
|
2247
|
+
runTurn(threadId, AUTO_CONTINUE, onEvent).catch(() => {});
|
|
2173
2248
|
}
|
|
2174
|
-
|
|
2175
|
-
t.history.push({ who: 'bot', text: note });
|
|
2176
|
-
onEvent?.({ type: 'final', name: t.name, color: t.color, text: note });
|
|
2177
|
-
saveThreads();
|
|
2249
|
+
return;
|
|
2178
2250
|
}
|
|
2179
2251
|
|
|
2180
2252
|
// ANNOUNCING IS NOT DOING — so do not let it end the turn.
|
|
@@ -2193,13 +2265,22 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
2193
2265
|
// finished reply would spend a turn arguing with a bot that already
|
|
2194
2266
|
// succeeded. Once per turn (autoNudged), so a model that announces twice
|
|
2195
2267
|
// still stops instead of looping on the user's wallet.
|
|
2196
|
-
if (t.runMode === 'auto' && (ack === null || ack === undefined)
|
|
2197
|
-
&& (
|
|
2268
|
+
if (t.runMode === 'auto' && (ack === null || ack === undefined)
|
|
2269
|
+
&& (STALLED_OFFER.test(reply) || ANNOUNCEMENT.test(reply))) {
|
|
2270
|
+
// Offers and "Spawned X — working on it" with no directive must not end
|
|
2271
|
+
// the run. The old once-only autoNudged gate parked the thread after one
|
|
2272
|
+
// hedge and the user typed continue. Keep going; AUTO_MAX_STEPS injects
|
|
2273
|
+
// another continue rather than stopping.
|
|
2198
2274
|
t.autoNudged = true;
|
|
2199
2275
|
t.autoSteps = (t.autoSteps || 0) + 1;
|
|
2200
2276
|
saveThreads();
|
|
2201
2277
|
bindThread(t).catch(() => {});
|
|
2202
|
-
|
|
2278
|
+
if (t.autoSteps < AUTO_MAX_STEPS) {
|
|
2279
|
+
runTurn(threadId, NUDGE, onEvent).catch(() => {});
|
|
2280
|
+
} else {
|
|
2281
|
+
t.autoSteps = 0;
|
|
2282
|
+
runTurn(threadId, AUTO_CONTINUE, onEvent).catch(() => {});
|
|
2283
|
+
}
|
|
2203
2284
|
return;
|
|
2204
2285
|
}
|
|
2205
2286
|
bindThread(t).catch(() => {});
|
|
@@ -2209,12 +2290,7 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
2209
2290
|
// in here because they are FALSE without a SPAWN: in the same reply — the bot
|
|
2210
2291
|
// reports success for something the harness never saw.
|
|
2211
2292
|
const ANNOUNCEMENT = /\b(?:I(?:'| a)?ll |I will |let me |I'm going to |I am going to |first,? |next,? |now I'll |starting|kicking off|spawn(?:ing|ed)|about to|going to (?:check|run|create|start|install))\b/i;
|
|
2212
|
-
|
|
2213
|
-
const NUDGE = 'That reply announced work instead of doing it — no directive line reached the harness, '
|
|
2214
|
-
+ 'so nothing ran. Emit the directive NOW, as the first line of your reply, with no preamble: '
|
|
2215
|
-
+ 'RUN:, SPAWN:, READ:, WRITE:, GLOB:, FETCH:, MCP: or SERVE:. '
|
|
2216
|
-
+ 'Exactly the syntax from your instructions — not [TOOL_CALL], not JSON, not a function-call envelope. '
|
|
2217
|
-
+ 'If several steps are needed, emit the FIRST one; you get its real output back and continue from there.';
|
|
2293
|
+
const STALLED_OFFER = /\b(?:if you want(?:ed)?,? I can|should I\b|let me know(?: and I['’]ll)?|ready to proceed|want me to|would you like(?: me to)?|spawned \S[\s\S]{0,80}working on it|kicked that off)\b/i;
|
|
2218
2294
|
|
|
2219
2295
|
/**
|
|
2220
2296
|
* The PROJECT a thread belongs to = the root of its spawn tree, and how deep
|
|
@@ -2420,17 +2496,22 @@ const APP_HTML = `<!doctype html>
|
|
|
2420
2496
|
@keyframes twarnpulse { 0%,100% { opacity: 1; } 50% { opacity: .45; } }
|
|
2421
2497
|
@media (prefers-reduced-motion: reduce) { .twarn { animation: none; } }
|
|
2422
2498
|
#main { flex: 1; min-width: 0; display: flex; flex-direction: column; height: 100vh; }
|
|
2423
|
-
#chatHeader { padding: 14px 20px; border-bottom: 1px solid #1c1c1e; display: flex; align-items: center;
|
|
2424
|
-
font-weight: 600; }
|
|
2499
|
+
#chatHeader { padding: 14px 20px; border-bottom: 1px solid #1c1c1e; display: flex; align-items: center;
|
|
2500
|
+
flex-wrap: wrap; gap: 8px 10px; font-weight: 600; }
|
|
2425
2501
|
#chatHeader .tavatar { width: 26px; height: 26px; border-radius: 7px; font-size: 11px; flex: 0 0 26px; }
|
|
2426
|
-
|
|
2502
|
+
/* Title shrinks and wraps; the spend dials must stay on screen. margin-left:auto
|
|
2503
|
+
on #modeToggle used to shove cheap/race/wallet off the right edge. */
|
|
2504
|
+
#chatHeaderId { display: flex; align-items: center; gap: 10px; flex: 1 1 120px; min-width: 0; overflow: hidden; }
|
|
2505
|
+
#headerDials { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-left: auto; flex: 0 1 auto; }
|
|
2506
|
+
.hname { display: flex; flex-direction: column; gap: 1px; min-width: 0; overflow: hidden; }
|
|
2507
|
+
.hname > div:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2427
2508
|
.hdir { font-weight: 400; font-size: 11px; color: #8e8e93; white-space: nowrap; overflow: hidden;
|
|
2428
2509
|
text-overflow: ellipsis; max-width: 420px; }
|
|
2429
2510
|
/* Run-mode toggle. The "/mode auto|ask" chat command still works and is
|
|
2430
2511
|
still what the bots are told about, but it is invisible until you know it
|
|
2431
2512
|
exists — and it controls whether shell commands run without asking, which
|
|
2432
2513
|
is exactly the setting a user should be able to SEE at a glance. */
|
|
2433
|
-
#modeToggle {
|
|
2514
|
+
#modeToggle { display: flex; align-items: center; gap: 0;
|
|
2434
2515
|
background: #1c1c1e; border: 1px solid #333340; border-radius: 999px; padding: 2px; }
|
|
2435
2516
|
.modebtn { border: 0; background: none; color: #8e8e93; font: inherit; font-size: 11px;
|
|
2436
2517
|
font-weight: 600; padding: 4px 11px; border-radius: 999px; cursor: pointer;
|
|
@@ -2453,9 +2534,9 @@ const APP_HTML = `<!doctype html>
|
|
|
2453
2534
|
.dial.pinned { color: #4c4d5a; border-color: #1c1c1e; }
|
|
2454
2535
|
/* WALLET. GET /wallet shipped in 1.5.22 with nothing pointing at it, so the
|
|
2455
2536
|
box's own deposit addresses were reachable only by curl — on a product
|
|
2456
|
-
whose entire premise is that the box pays for itself.
|
|
2457
|
-
|
|
2458
|
-
|
|
2537
|
+
whose entire premise is that the box pays for itself. Click-to-copy is a
|
|
2538
|
+
shortcut; the address is selectable so Cmd/Ctrl+C still works if copy
|
|
2539
|
+
fails. */
|
|
2459
2540
|
#walletOverlay, #assetsOverlay { position: fixed; inset: 0; background: rgba(0,0,0,.66); z-index: 1200;
|
|
2460
2541
|
display: none; align-items: center; justify-content: center; padding: 24px; }
|
|
2461
2542
|
#walletOverlay.show, #assetsOverlay.show { display: flex; }
|
|
@@ -2471,8 +2552,10 @@ const APP_HTML = `<!doctype html>
|
|
|
2471
2552
|
pushes the copy affordance out of the box — the same flexbox trap that
|
|
2472
2553
|
once pushed the cost button off-screen. */
|
|
2473
2554
|
.wrow .waddr { flex: 1; min-width: 0; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
2474
|
-
font-size: 12px; word-break: break-all; line-height: 1.45;
|
|
2475
|
-
|
|
2555
|
+
font-size: 12px; word-break: break-all; line-height: 1.45; cursor: text;
|
|
2556
|
+
-webkit-user-select: all; user-select: all; -webkit-app-region: no-drag; }
|
|
2557
|
+
.wrow .wcopy { flex: 0 0 auto; color: #6f7080; font-size: 10px; text-transform: uppercase; letter-spacing: .06em;
|
|
2558
|
+
user-select: none; }
|
|
2476
2559
|
.wrow:hover .wcopy { color: #b8f240; }
|
|
2477
2560
|
.wbal { border: 1px solid #1c1c1e; border-radius: 12px; padding: 10px 12px; margin-bottom: 10px;
|
|
2478
2561
|
font-size: 12px; color: #ececec; line-height: 1.7; word-break: break-word; }
|
|
@@ -2501,8 +2584,7 @@ const APP_HTML = `<!doctype html>
|
|
|
2501
2584
|
.scmd b { color: #b8f240; font-weight: 600; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
2502
2585
|
.scmd i { color: #6f7080; font-style: normal; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
2503
2586
|
.scmd span { color: #999aa8; margin-left: auto; text-align: right; }
|
|
2504
|
-
#hudBtn { margin-left:
|
|
2505
|
-
#chatHeaderId { display: flex; align-items: center; gap: 10px; }
|
|
2587
|
+
#hudBtn { margin-left: 2px; }
|
|
2506
2588
|
#hud { position: fixed; top: 40px; right: 14px; width: 250px; background: rgba(14,14,17,.94);
|
|
2507
2589
|
border: 1px solid #333340; border-radius: 10px; padding: 12px 14px; font: 11px/1.5 Menlo, monospace;
|
|
2508
2590
|
display: none; z-index: 300; box-shadow: 0 12px 30px rgba(0,0,0,.5); }
|
|
@@ -2520,7 +2602,9 @@ const APP_HTML = `<!doctype html>
|
|
|
2520
2602
|
color: #f0c9a8; font-size: 10.5px; line-height: 1.45; }
|
|
2521
2603
|
#hud .hhint.show { display: block; }
|
|
2522
2604
|
#hud .hhint b { color: #f28c4d; font-weight: 600; }
|
|
2523
|
-
#sidebar, #main
|
|
2605
|
+
#sidebar, #main, #walletOverlay, #assetsOverlay, #composeOverlay,
|
|
2606
|
+
#inp, #search, #composeInp, .wpass, .bubble, .md-pre, .runoutput, .runcmd {
|
|
2607
|
+
-webkit-app-region: no-drag; }
|
|
2524
2608
|
#log { flex: 1; min-width: 0; overflow-y: auto; overflow-x: hidden; padding: 20px 24px 12px;
|
|
2525
2609
|
display: flex; flex-direction: column; gap: 6px;
|
|
2526
2610
|
-webkit-user-select: text; user-select: text; }
|
|
@@ -2573,9 +2657,10 @@ const APP_HTML = `<!doctype html>
|
|
|
2573
2657
|
padding: 3px 8px; border: 1px solid #3a3a3d; border-radius: 8px;
|
|
2574
2658
|
background: #1c1c1e; color: #b9b9c0;
|
|
2575
2659
|
font: 500 10.5px/1.4 ui-sans-serif, -apple-system, system-ui, sans-serif;
|
|
2576
|
-
letter-spacing: .02em; cursor: pointer;
|
|
2660
|
+
letter-spacing: .02em; cursor: pointer; user-select: none;
|
|
2577
2661
|
transition: opacity .12s ease, background .12s ease, color .12s ease;
|
|
2578
2662
|
}
|
|
2663
|
+
.md-pre, .runoutput, .runcmd { -webkit-user-select: text; user-select: text; }
|
|
2579
2664
|
.row.bot .copybtn { right: -2px; }
|
|
2580
2665
|
.row.user .copybtn { left: -2px; }
|
|
2581
2666
|
.row:hover .copybtn, .row:focus-within .copybtn { opacity: 1; pointer-events: auto; }
|
|
@@ -2710,14 +2795,15 @@ const APP_HTML = `<!doctype html>
|
|
|
2710
2795
|
</div>
|
|
2711
2796
|
<div id="walletOverlay" data-component="wallet-modal">
|
|
2712
2797
|
<div id="walletBox">
|
|
2713
|
-
<h3>
|
|
2714
|
-
<div class="wsub">
|
|
2798
|
+
<h3>Your wallet</h3>
|
|
2799
|
+
<div class="wsub">This is <b>your</b> local burner on this machine (or this box). Keys stay in ~/.openzoo/wallet.json. It is not openzoo’s wallet, not a shared zoo account, not the model’s. You fund these deposit addresses; the app pays x402 per call from this wallet. Public addresses only — the UI never shows the key.</div>
|
|
2715
2800
|
<div id="walletBody">loading…</div>
|
|
2716
2801
|
</div>
|
|
2717
2802
|
</div>
|
|
2718
2803
|
<div id="main">
|
|
2719
2804
|
<div id="chatHeader">
|
|
2720
2805
|
<div id="chatHeaderId"></div>
|
|
2806
|
+
<div id="headerDials">
|
|
2721
2807
|
<div id="modeToggle" data-component="run-mode-toggle" role="group" aria-label="Shell command mode">
|
|
2722
2808
|
<button class="modebtn ask on" id="modeAsk" data-mode="ask"
|
|
2723
2809
|
title="Shell commands pause and wait for your approval">ask</button>
|
|
@@ -2748,9 +2834,10 @@ const APP_HTML = `<!doctype html>
|
|
|
2748
2834
|
<button class="dial" id="assetsBtn" data-component="assets-unlock"
|
|
2749
2835
|
title="Unlock the encrypted assets baked into this box's image">assets</button>
|
|
2750
2836
|
<button class="dial" id="walletBtn" data-component="wallet-open"
|
|
2751
|
-
title="
|
|
2837
|
+
title="Your local burner wallet — deposit addresses and live balances">wallet</button>
|
|
2752
2838
|
<button class="icon-btn" id="reloadBtn" title="Restart grokui on this box">↻</button>
|
|
2753
2839
|
<button class="icon-btn" id="hudBtn">◎</button>
|
|
2840
|
+
</div>
|
|
2754
2841
|
</div>
|
|
2755
2842
|
<div id="hud">
|
|
2756
2843
|
<div class="htitle">YOUR WALLET · THIS SESSION</div>
|
|
@@ -3060,7 +3147,7 @@ const APP_HTML = `<!doctype html>
|
|
|
3060
3147
|
// did not say, and guessing "empty" there would send someone to top up a
|
|
3061
3148
|
// wallet that is fine.
|
|
3062
3149
|
note.textContent = w.funded === false
|
|
3063
|
-
? 'This wallet is EMPTY — calls will fail with HTTP 402 until
|
|
3150
|
+
? 'This wallet is EMPTY — calls will fail with HTTP 402 until you fund the addresses above. ' + (w.funding || '')
|
|
3064
3151
|
: (w.funding || '');
|
|
3065
3152
|
if (w.funded === false) note.classList.add('wempty');
|
|
3066
3153
|
if (note.textContent.trim()) walletBody.appendChild(note);
|
|
@@ -3113,6 +3200,27 @@ const APP_HTML = `<!doctype html>
|
|
|
3113
3200
|
});
|
|
3114
3201
|
|
|
3115
3202
|
document.getElementById('walletBtn').addEventListener('click', openWallet);
|
|
3203
|
+
// First launch: if the burner is empty, open the wallet once so they see
|
|
3204
|
+
// addresses they can copy — and whose wallet this is. localStorage so a
|
|
3205
|
+
// funded session or a dismiss does not keep popping it.
|
|
3206
|
+
(async function maybeOpenWalletOnce() {
|
|
3207
|
+
if (localStorage.getItem('openzoo.wallet.seen')) return;
|
|
3208
|
+
for (let i = 0; i < 8; i++) {
|
|
3209
|
+
try {
|
|
3210
|
+
const r = await fetch(API + '/wallet');
|
|
3211
|
+
const w = r.ok ? await r.json() : null;
|
|
3212
|
+
if (!w || (!w.solana && !w.evm)) {
|
|
3213
|
+
await new Promise((res) => setTimeout(res, 400));
|
|
3214
|
+
continue;
|
|
3215
|
+
}
|
|
3216
|
+
localStorage.setItem('openzoo.wallet.seen', '1');
|
|
3217
|
+
if (w.funded === false) await openWallet();
|
|
3218
|
+
return;
|
|
3219
|
+
} catch (e) {
|
|
3220
|
+
await new Promise((res) => setTimeout(res, 400));
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
})();
|
|
3116
3224
|
// Click-outside and Escape both close it — a modal you can only dismiss one
|
|
3117
3225
|
// way is a modal people get stuck in.
|
|
3118
3226
|
walletOverlay.addEventListener('click', (e) => {
|
|
@@ -3175,15 +3283,22 @@ const APP_HTML = `<!doctype html>
|
|
|
3175
3283
|
instead of silently doing nothing, which is the worst outcome for a button
|
|
3176
3284
|
whose entire job is invisible. */
|
|
3177
3285
|
async function copyText(text) {
|
|
3286
|
+
const value = String(text ?? '');
|
|
3287
|
+
try {
|
|
3288
|
+
if (window.electronAPI && typeof window.electronAPI.copyText === 'function') {
|
|
3289
|
+
const ok = await window.electronAPI.copyText(value);
|
|
3290
|
+
if (ok) return true;
|
|
3291
|
+
}
|
|
3292
|
+
} catch (e) { /* fall through */ }
|
|
3178
3293
|
try {
|
|
3179
3294
|
if (navigator.clipboard && window.isSecureContext) {
|
|
3180
|
-
await navigator.clipboard.writeText(
|
|
3295
|
+
await navigator.clipboard.writeText(value);
|
|
3181
3296
|
return true;
|
|
3182
3297
|
}
|
|
3183
3298
|
} catch (e) { /* fall through to the legacy path */ }
|
|
3184
3299
|
try {
|
|
3185
3300
|
const ta = document.createElement('textarea');
|
|
3186
|
-
ta.value =
|
|
3301
|
+
ta.value = value;
|
|
3187
3302
|
// Off-screen but focusable: display:none or visibility:hidden make
|
|
3188
3303
|
// execCommand('copy') a no-op in several browsers.
|
|
3189
3304
|
ta.style.cssText = 'position:fixed;top:-1000px;left:-1000px;opacity:0';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.48.
|
|
3
|
+
"version": "0.48.83",
|
|
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",
|