openzoo 0.49.8 → 0.49.10
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/README.md +40 -7
- package/lib/claudecode.js +231 -0
- package/lib/grokui.mjs +850 -175
- package/lib/launch.js +72 -73
- package/lib/livestatus.js +4 -2
- package/lib/modelroute/README.md +1 -0
- package/lib/modelroute/catalog.json +1 -0
- package/lib/modelroute/outcomes.json +1566 -0
- package/lib/modelroute/router.json +1 -0
- package/lib/modelroute.js +737 -0
- package/lib/models.js +191 -62
- package/lib/package.json +3 -0
- package/lib/podagent.mjs +73 -27
- package/lib/proxy.js +141 -7
- package/lib/racesettle.js +23 -2
- package/lib/responses-stream.js +176 -0
- package/lib/runguard.js +31 -0
- package/lib/spill.js +9 -1
- package/lib/think.js +126 -0
- package/package.json +4 -3
- package/vendor/modelroute/CURRENT_STATE.md +132 -0
- package/vendor/modelroute/HANDOFF.md +159 -0
- package/vendor/modelroute/catalog.json +1 -0
- package/vendor/modelroute/holographic_modelroute.py +809 -0
- package/vendor/modelroute/outcomes.json +1566 -0
- package/vendor/modelroute/router.json +1 -0
package/lib/grokui.mjs
CHANGED
|
@@ -12,8 +12,10 @@ import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSyn
|
|
|
12
12
|
import { cpus, homedir } from 'node:os';
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import { adaptiveTopK, brain, brainRace, brainStream, tierModels, MODEL, PROXY, TIER_NAMES, normalizeTier } from './podagent.mjs';
|
|
15
|
+
import { routeChatBody } from './modelroute.js';
|
|
15
16
|
import { peekDirectiveStatus, formatRaceStatus, STALE_THINKING_MS, summarizeRaceFailures, RACE_EVERY_FAILED } from './livestatus.js';
|
|
16
17
|
import { creditBalance } from './info.js';
|
|
18
|
+
import { guardFindCwd } from './runguard.js';
|
|
17
19
|
import {
|
|
18
20
|
SUBSCRIPTIONS_PAGE,
|
|
19
21
|
saveSubscription, clearSubscription,
|
|
@@ -31,6 +33,10 @@ import {
|
|
|
31
33
|
extractBashPaths,
|
|
32
34
|
} from './spill.js';
|
|
33
35
|
import { BIND_MIN_CHARS } from './hrr.js';
|
|
36
|
+
import { stripThinkTags, takeThink } from './think.js';
|
|
37
|
+
import {
|
|
38
|
+
runClaudeCode, setClaudeRunnerForTest, toolStatusLine, CLAUDE_MISSING,
|
|
39
|
+
} from './claudecode.js';
|
|
34
40
|
|
|
35
41
|
const PORT = Number(process.env.OZ_GROKUI_PORT || 4173);
|
|
36
42
|
// BIND HOST. Default 127.0.0.1 so the desktop app never exposes a shell-capable
|
|
@@ -95,18 +101,11 @@ function stripBasePrefix(base, spec) {
|
|
|
95
101
|
}
|
|
96
102
|
|
|
97
103
|
/**
|
|
98
|
-
* Reasoning
|
|
99
|
-
*
|
|
100
|
-
* the
|
|
101
|
-
*
|
|
104
|
+
* Reasoning used to leak `<think>…</think>` into the visible bubble and then
|
|
105
|
+
* get sent back to the model. Visible text is still stripped (see takeThink);
|
|
106
|
+
* the plaintext is kept on the history row as `thinking` and folded in the
|
|
107
|
+
* canvas. Encrypted blobs never become a chip — that lives in lib/think.js.
|
|
102
108
|
*/
|
|
103
|
-
function stripThinkTags(text) {
|
|
104
|
-
let s = String(text ?? '');
|
|
105
|
-
s = s.replace(/<think(?:ing)?\b[^>]*>[\s\S]*?<\/think(?:ing)?>/gi, '');
|
|
106
|
-
s = s.replace(/<think(?:ing)?\b[^>]*>[\s\S]*$/i, '');
|
|
107
|
-
s = s.replace(/<\/think(?:ing)?>/gi, '');
|
|
108
|
-
return s.replace(/^\n+|\n+$/g, '').trim();
|
|
109
|
-
}
|
|
110
109
|
const MIME = { html: 'text/html', htm: 'text/html', css: 'text/css', js: 'application/javascript',
|
|
111
110
|
mjs: 'application/javascript', json: 'application/json', png: 'image/png', jpg: 'image/jpeg',
|
|
112
111
|
jpeg: 'image/jpeg', gif: 'image/gif', svg: 'image/svg+xml', txt: 'text/plain', md: 'text/plain' };
|
|
@@ -211,6 +210,23 @@ function colorFor(name) {
|
|
|
211
210
|
return PALETTE[h % PALETTE.length];
|
|
212
211
|
}
|
|
213
212
|
|
|
213
|
+
// Frozen SYSTEM is baked into old threads at creation. This string is also
|
|
214
|
+
// injected every turn (extras + AUTO_DIRECTIVE) so live Auto cannot "shell
|
|
215
|
+
// the proxy" just because the thread was born with the old "you CAN curl
|
|
216
|
+
// :8402" paragraph. Site-check curls of localhost:8080 stay allowed.
|
|
217
|
+
const CHAT_NOT_PROXY = `You already ARE the chat. Never RUN curl, wget, or fetch against localhost:8402 or
|
|
218
|
+
/v1/chat/completions — that dumps another model's JSON into the canvas and pays twice.
|
|
219
|
+
Orange Auto = WRITE / READ / RUN / GLOB for real work, not "shell the proxy."
|
|
220
|
+
Never mkdir empty trees and declare DONE — WRITE the files.`;
|
|
221
|
+
const PROXY_SHELL_REFUSE = 'refused: you already ARE the chat. Never curl/wget/fetch localhost:8402 or /v1/chat/completions — Orange Auto is WRITE/READ/RUN for real work, not shelling the proxy.';
|
|
222
|
+
function looksLikeProxyShell(cmd) {
|
|
223
|
+
const s = String(cmd || '');
|
|
224
|
+
if (!/\b(curl|wget|fetch)\b/i.test(s)) return false;
|
|
225
|
+
if (/(?:localhost|127\.0\.0\.1|\[::1\])/i.test(s) && /:8402\b/.test(s)) return true;
|
|
226
|
+
if (/(?:localhost|127\.0\.0\.1|\[::1\])[^\s'"]*\/v1\/chat\/completions/i.test(s)) return true;
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
|
|
214
230
|
const SYSTEM = `You are a helpful assistant served over openzoo (pay-per-call access to ~435
|
|
215
231
|
models, no API key, no account). Reply normally in plain text, concisely.
|
|
216
232
|
|
|
@@ -361,7 +377,7 @@ call. Ask for everything you know you need at once instead of discovering it one
|
|
|
361
377
|
time. Mutating directives (RUN, WRITE, EDIT, SPAWN, SEND) stay sequential on purpose — racing
|
|
362
378
|
them against each other corrupts the tree.
|
|
363
379
|
RUN: <shell command> run a REAL shell command in this
|
|
364
|
-
thread's directory —
|
|
380
|
+
thread's directory. Stay in that directory. Never find / — use GLOB: or find . -maxdepth N. By default this
|
|
365
381
|
pauses and waits for the user to
|
|
366
382
|
approve or deny it before anything
|
|
367
383
|
executes ("/mode auto" in chat skips
|
|
@@ -385,17 +401,7 @@ output and exit code back; that, not silence, is what failure looks like.
|
|
|
385
401
|
Never fabricate command output, file contents, or payment receipts. If you did not run it,
|
|
386
402
|
say so and then actually run it.
|
|
387
403
|
|
|
388
|
-
|
|
389
|
-
http://localhost:8402/v1/chat/completions (or /v1/hrr/bind) with curl/python/etc. Auth is
|
|
390
|
-
"Authorization: Bearer sk-openzoo" — any string works, x402 pays per call, not the key. Do
|
|
391
|
-
NOT tell the user you "can't fire the paid calls" or need "their client's bearer key" —
|
|
392
|
-
that's wrong, you can make these calls yourself via RUN. When you do, set max_tokens
|
|
393
|
-
generously (1000+, not 50) — a reasoning model can burn its ENTIRE budget on internal
|
|
394
|
-
reasoning before writing any visible answer, especially against a large bound corpus, and
|
|
395
|
-
comes back with content:null and finish_reason:"length" (confirmed live) if you starve it.
|
|
396
|
-
/v1/hrr/bind also caps around ~8MB per request after JSON-escaping — chunk large corpora
|
|
397
|
-
(e.g. 512KB raw per request) and pass the PREVIOUS chunk's context_id on each next request
|
|
398
|
-
to append to the same bound context, rather than one giant request that silently fails partway.
|
|
404
|
+
${CHAT_NOT_PROXY}
|
|
399
405
|
|
|
400
406
|
COST ACCOUNTING — do NOT compute this yourself from token counts. Every response carries an
|
|
401
407
|
"x402" object; read the numbers off it: x402.billedUsd (what the user paid — OpenRouter price, plus 33% of savings vs
|
|
@@ -443,7 +449,12 @@ function loadThreads() {
|
|
|
443
449
|
}
|
|
444
450
|
if (Array.isArray(t.history)) {
|
|
445
451
|
for (const h of t.history) {
|
|
446
|
-
if (h && h.who === 'bot' && typeof h.text === 'string')
|
|
452
|
+
if (h && h.who === 'bot' && typeof h.text === 'string') {
|
|
453
|
+
const parts = takeThink(h.text, h.thinking);
|
|
454
|
+
h.text = parts.text;
|
|
455
|
+
if (parts.thinking) h.thinking = parts.thinking;
|
|
456
|
+
else delete h.thinking;
|
|
457
|
+
}
|
|
447
458
|
}
|
|
448
459
|
}
|
|
449
460
|
if (Array.isArray(t.messages)) {
|
|
@@ -507,6 +518,10 @@ function newThread(name, parent, members, spec) {
|
|
|
507
518
|
...(p?.race ? { race: p.race } : {}),
|
|
508
519
|
...(p?.raceNeed ? { raceNeed: p.raceNeed } : {}) };
|
|
509
520
|
if (p) attachChildDir(t, p, spec);
|
|
521
|
+
// Brand-new chat (no parent) starts empty: own holobrain, no contextId,
|
|
522
|
+
// no boundItems. Do not copy the previous thread's bind / corpus.
|
|
523
|
+
// SPAWN kids also start unbound here; bindThread shares the parent root
|
|
524
|
+
// on first bind. Existing threads on disk keep whatever they already have.
|
|
510
525
|
threads.set(id, t);
|
|
511
526
|
saveThreads();
|
|
512
527
|
return t;
|
|
@@ -630,6 +645,7 @@ savedUsd, savesVsDirect). Never derive it by summing usage.cost or usage.prompt_
|
|
|
630
645
|
provider's list price: on a bound context prompt_tokens counts only the slice leCore
|
|
631
646
|
recalled, not the corpus it stands in for, so that math prices the discount against itself
|
|
632
647
|
and wrongly concludes the zoo cost more.
|
|
648
|
+
${CHAT_NOT_PROXY}
|
|
633
649
|
For normal replies just answer directly — do not use any of these unless the request
|
|
634
650
|
actually calls for delegation or file work.` };
|
|
635
651
|
}
|
|
@@ -650,7 +666,10 @@ function contentFor(text, images) {
|
|
|
650
666
|
}
|
|
651
667
|
|
|
652
668
|
function buildMemberMessages(t, member) {
|
|
653
|
-
const msgs = [
|
|
669
|
+
const msgs = [
|
|
670
|
+
{ role: 'system', content: member.systemPrompt || SYSTEM },
|
|
671
|
+
{ role: 'system', content: CHAT_NOT_PROXY },
|
|
672
|
+
];
|
|
654
673
|
for (const h of t.history) {
|
|
655
674
|
if (h.who === 'user') msgs.push({ role: 'user', content: contentFor(h.text, h.images) });
|
|
656
675
|
else if (h.name === member.name) msgs.push({ role: 'assistant', content: h.text });
|
|
@@ -693,9 +712,7 @@ const NUDGE = 'That reply announced work instead of doing it — no directive li
|
|
|
693
712
|
+ 'RUN:, SPAWN:, READ:, WRITE:, GLOB:, FETCH:, MCP: or SERVE:. '
|
|
694
713
|
+ 'Exactly the syntax from your instructions — not [TOOL_CALL], not JSON, not a function-call envelope. '
|
|
695
714
|
+ 'If several steps are needed, emit the FIRST one; you get its real output back and continue from there.';
|
|
696
|
-
const AUTO_CONTINUE = '
|
|
697
|
-
+ 'Emit the next directive now (RUN:/SPAWN:/SEND:/READ:/WRITE:/GLOB:/FETCH:/MCP:/SERVE:), '
|
|
698
|
-
+ 'or DONE: if the job is actually finished.';
|
|
715
|
+
const AUTO_CONTINUE = 'Please continue the current job. Do not stop to ask the user.';
|
|
699
716
|
const AUTO_RACE_RETRY = 'AUTO is still on — the last model call failed (race/empty/error). '
|
|
700
717
|
+ 'Do not stop and do not ask the user to type continue. '
|
|
701
718
|
+ 'Emit the next directive now (RUN:/SPAWN:/SEND:/READ:/WRITE:/GLOB:/FETCH:/MCP:/SERVE:), '
|
|
@@ -852,7 +869,22 @@ without a fact only the user has.
|
|
|
852
869
|
When the job is actually finished, emit DONE: as the first line. A status
|
|
853
870
|
sentence is not a stop — the harness keeps this thread working until DONE:,
|
|
854
871
|
a real blocking question, or the step cap. Empty output, "(no output)", and
|
|
855
|
-
GLOB/GREP with no matches are not finished — try a different command or path
|
|
872
|
+
GLOB/GREP with no matches are not finished — try a different command or path.
|
|
873
|
+
|
|
874
|
+
${CHAT_NOT_PROXY}`;
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* Holobrain owner for a thread.
|
|
878
|
+
* SPAWN kids share the project root (current SPAWN semantics).
|
|
879
|
+
* A brand-new chat (no parent) is its own root — never attach another
|
|
880
|
+
* thread's contextId / boundItems.
|
|
881
|
+
*/
|
|
882
|
+
function holobrainOf(t) {
|
|
883
|
+
if (!t) return null;
|
|
884
|
+
if (t.parent) return threads.get(rootOf(t).rootId) || t;
|
|
885
|
+
return t;
|
|
886
|
+
}
|
|
887
|
+
|
|
856
888
|
async function bindThread(t) {
|
|
857
889
|
// Only bind what's NEW since the last successful bind, continuing the
|
|
858
890
|
// existing context_id — previously this rebuilt and re-sent the WHOLE
|
|
@@ -867,15 +899,13 @@ async function bindThread(t) {
|
|
|
867
899
|
const corpus = delta.map((h) => '[' + t.name + '] ' + (h.who === 'user' ? 'you' : (h.name || t.name)) + ': ' + h.text).join('\n');
|
|
868
900
|
if (!corpus.trim()) { t.boundHistoryCount = t.history.length; return; }
|
|
869
901
|
try {
|
|
870
|
-
//
|
|
871
|
-
//
|
|
872
|
-
//
|
|
873
|
-
//
|
|
874
|
-
//
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
const root = threads.get(rootOf(t).rootId) || t;
|
|
878
|
-
let ctx = root.contextId || t.contextId;
|
|
902
|
+
// SPAWN kids share the project root holobrain so sibling agents can
|
|
903
|
+
// recall what the crew already bound. A brand-new chat (no parent) is
|
|
904
|
+
// its own root — do not attach the previous thread's contextId.
|
|
905
|
+
// Existing threads that already have a bind keep it (we never delete
|
|
906
|
+
// contextId here).
|
|
907
|
+
const brain = holobrainOf(t) || t;
|
|
908
|
+
let ctx = brain.contextId || t.contextId;
|
|
879
909
|
for (let i = 0; i < corpus.length; i += BIND_CHUNK_BYTES) {
|
|
880
910
|
const part = corpus.slice(i, i + BIND_CHUNK_BYTES);
|
|
881
911
|
const body = ctx ? { corpus: part, context_id: ctx } : { corpus: part };
|
|
@@ -889,12 +919,12 @@ async function bindThread(t) {
|
|
|
889
919
|
// How many chunks the project's holobrain now holds. This is the number
|
|
890
920
|
// adaptive top_k scales on — without it we would be guessing, which is
|
|
891
921
|
// exactly how top_k ended up pinned at 8 in the first place.
|
|
892
|
-
if (Number(j?.bound))
|
|
922
|
+
if (Number(j?.bound)) brain.boundItems = (brain.boundItems || 0) + Number(j.bound);
|
|
893
923
|
else break; // this chunk failed — stop, keep whatever bound so far rather than lose it all
|
|
894
924
|
}
|
|
895
|
-
// Write to the
|
|
896
|
-
// the per-call header is
|
|
897
|
-
if (ctx) {
|
|
925
|
+
// Write to the holobrain owner (project root for SPAWN kids, this thread
|
|
926
|
+
// for a new chat) and to this thread so the per-call header is local.
|
|
927
|
+
if (ctx) { brain.contextId = ctx; t.contextId = ctx; t.boundHistoryCount = t.history.length; saveThreads(); }
|
|
898
928
|
} catch { /* leCore sidecar unreachable — thread still works, just not bound this round */ }
|
|
899
929
|
}
|
|
900
930
|
|
|
@@ -1035,8 +1065,10 @@ const RUN_SHELL = process.platform !== 'win32' && existsSync('/bin/bash') ? '/bi
|
|
|
1035
1065
|
const RUN_TIMEOUT_MS = Number(process.env.OZ_RUN_TIMEOUT_MS || 600000);
|
|
1036
1066
|
|
|
1037
1067
|
function execCommand(command, cwd) {
|
|
1068
|
+
if (looksLikeProxyShell(command)) return Promise.resolve(PROXY_SHELL_REFUSE);
|
|
1069
|
+
const guarded = guardFindCwd(command, cwd);
|
|
1038
1070
|
return new Promise((resolve) => {
|
|
1039
|
-
exec(
|
|
1071
|
+
exec(guarded, { cwd, shell: RUN_SHELL, timeout: RUN_TIMEOUT_MS, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
1040
1072
|
let out = (stdout || '') + (stderr ? '\n' + stderr : '');
|
|
1041
1073
|
if (err) out += `\n(exit ${err.code ?? 1})`;
|
|
1042
1074
|
resolve(keepWhole(out).trim() || '(no output)');
|
|
@@ -1083,11 +1115,28 @@ const SLASH_COMMANDS = [
|
|
|
1083
1115
|
{ name: '/cron', args: '<mins> | <message>', help: 'repeat a message on a timer' },
|
|
1084
1116
|
{ name: '/crons', args: '', help: 'list timers (/cron del <id> removes one)' },
|
|
1085
1117
|
{ name: '/dir', args: '<path>', help: 'set this thread’s working directory' },
|
|
1086
|
-
{ name: '/mode', args: 'auto|ask', help: '
|
|
1118
|
+
{ name: '/mode', args: 'auto|ask', help: 'Auto = Claude Code via OpenZoo; ask = chat + approve RUN' },
|
|
1087
1119
|
];
|
|
1088
1120
|
|
|
1089
1121
|
const usd = (n) => (n >= 0.01 || n === 0 ? '$' + n.toFixed(2) : '$' + n.toFixed(5));
|
|
1090
1122
|
|
|
1123
|
+
/**
|
|
1124
|
+
* HUD / sitrep / /cost: prefer spilled-call x when anything bound.
|
|
1125
|
+
* Never an unlabeled Nx — "2.10x spilled" vs "2.10x session".
|
|
1126
|
+
* Rounding: 100+ integer, 10+ 1dp, else 2dp.
|
|
1127
|
+
*/
|
|
1128
|
+
function formatSavingLabel(you) {
|
|
1129
|
+
const spent = Number(you && you.spentUsd) || 0;
|
|
1130
|
+
if (spent <= 0) return { text: '—', mult: null, spilled: false };
|
|
1131
|
+
const spillX = Number(you && you.spilled && you.spilled.savingX);
|
|
1132
|
+
const sessionX = (Number(you && you.directUsd) || 0) / spent;
|
|
1133
|
+
const spilled = Number.isFinite(spillX) && spillX > 0;
|
|
1134
|
+
const mult = spilled ? spillX : sessionX;
|
|
1135
|
+
if (!Number.isFinite(mult)) return { text: '—', mult: null, spilled: false };
|
|
1136
|
+
const num = (mult >= 100 ? String(Math.round(mult)) : Number(mult).toFixed(mult >= 10 ? 1 : 2)) + 'x';
|
|
1137
|
+
return { text: num + (spilled ? ' spilled' : ' session'), mult, spilled };
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1091
1140
|
// BIND, DON'T PASTE — the whole point of the thing this runs on.
|
|
1092
1141
|
//
|
|
1093
1142
|
// Directive results get fed back to the model as a user message, verbatim. A
|
|
@@ -1215,8 +1264,8 @@ function inFlightChars(t) {
|
|
|
1215
1264
|
*/
|
|
1216
1265
|
function scheduleFilesForCorpus(t, collected, opts = {}) {
|
|
1217
1266
|
if (!collected?.pending?.length) return null;
|
|
1218
|
-
const
|
|
1219
|
-
const ctx = opts.contextId ||
|
|
1267
|
+
const brain = t ? holobrainOf(t) : null;
|
|
1268
|
+
const ctx = opts.contextId || brain?.contextId || t?.contextId || null;
|
|
1220
1269
|
const chars = opts.sentChars ?? inFlightChars(t);
|
|
1221
1270
|
const background = chars < BIND_MIN_CHARS;
|
|
1222
1271
|
const fetchImpl = opts.fetchImpl || fetch;
|
|
@@ -1233,7 +1282,7 @@ function scheduleFilesForCorpus(t, collected, opts = {}) {
|
|
|
1233
1282
|
}).then(async (r) => {
|
|
1234
1283
|
const j = await r.json().catch(() => ({}));
|
|
1235
1284
|
if (j?.context_id && t) {
|
|
1236
|
-
const live =
|
|
1285
|
+
const live = holobrainOf(t) || t;
|
|
1237
1286
|
live.contextId = j.context_id;
|
|
1238
1287
|
t.contextId = j.context_id;
|
|
1239
1288
|
if (Number(j.bound)) live.boundItems = (live.boundItems || 0) + Number(j.bound);
|
|
@@ -1301,9 +1350,20 @@ function emitToThread(threadId, ev) {
|
|
|
1301
1350
|
}
|
|
1302
1351
|
}
|
|
1303
1352
|
|
|
1353
|
+
async function attachSpilled(you) {
|
|
1354
|
+
if (!you || you.spilled != null) return you;
|
|
1355
|
+
try {
|
|
1356
|
+
const info = await (await fetch(`${PROXY}/info`, { signal: AbortSignal.timeout(2000) })).json();
|
|
1357
|
+
if (info && info.spilled) you.spilled = info.spilled;
|
|
1358
|
+
} catch { /* session label stays honest if info is down */ }
|
|
1359
|
+
return you;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1304
1362
|
async function sessionStats() {
|
|
1305
|
-
try {
|
|
1306
|
-
|
|
1363
|
+
try {
|
|
1364
|
+
const s = await (await fetch(`${PROXY}/session`, { signal: AbortSignal.timeout(2000) })).json();
|
|
1365
|
+
return attachSpilled(s);
|
|
1366
|
+
} catch { return null; }
|
|
1307
1367
|
}
|
|
1308
1368
|
|
|
1309
1369
|
function todoBlock(t) {
|
|
@@ -1318,6 +1378,18 @@ async function handleSlash(task, t) {
|
|
|
1318
1378
|
const cmd = m[1].toLowerCase();
|
|
1319
1379
|
const arg = m[2].trim();
|
|
1320
1380
|
|
|
1381
|
+
if (cmd === 'pay') {
|
|
1382
|
+
return 'Pay — card checkout or the local wallet/x402 burner.';
|
|
1383
|
+
}
|
|
1384
|
+
if (cmd === 'hud') {
|
|
1385
|
+
const st = await sessionStats();
|
|
1386
|
+
const spent = st ? usd(Number(st.spentUsd) || 0) : '—';
|
|
1387
|
+
const calls = st ? (st.paidCalls || 0) : 0;
|
|
1388
|
+
const mode = t.runMode || 'ask';
|
|
1389
|
+
const tier = t.tier || 'auto';
|
|
1390
|
+
const sav = st ? formatSavingLabel(st) : { text: '—' };
|
|
1391
|
+
return `Sitrep — mode ${mode} · ${tier} · paid ${spent} · ${sav.text} · ${calls} calls.`;
|
|
1392
|
+
}
|
|
1321
1393
|
if (cmd === 'sitrep') return null; // drawer-only — never a transcript line
|
|
1322
1394
|
if (cmd === 'help') {
|
|
1323
1395
|
return 'Commands:\n'
|
|
@@ -1328,7 +1400,7 @@ async function handleSlash(task, t) {
|
|
|
1328
1400
|
}
|
|
1329
1401
|
if (cmd === 'tools') {
|
|
1330
1402
|
return 'Directives:\n'
|
|
1331
|
-
+ ' RUN: <cmd> real shell, in this thread’s dir\n'
|
|
1403
|
+
+ ' RUN: <cmd> real shell, in this thread’s dir (never find /)\n'
|
|
1332
1404
|
+ ' WRITE: <path> | <content> create/overwrite a file\n'
|
|
1333
1405
|
+ ' EDIT: <path> | <old> ||| <new> change part of a file\n'
|
|
1334
1406
|
+ ' MULTIEDIT: <path> | a|||b ;; c|||d several edits, all-or-nothing\n'
|
|
@@ -1349,6 +1421,20 @@ async function handleSlash(task, t) {
|
|
|
1349
1421
|
+ 'READ, LS, GLOB, GREP, FETCH, PEEK and MCP run CONCURRENTLY when several\n'
|
|
1350
1422
|
+ 'appear in one reply — four files cost one round trip, not four.';
|
|
1351
1423
|
}
|
|
1424
|
+
// Header Pay / ◎ echo through the same /drive → history.push path as
|
|
1425
|
+
// /mode and /tier. Short lines only — never wallet JSON or a sitrep dump.
|
|
1426
|
+
if (cmd === 'pay') {
|
|
1427
|
+
return 'Pay — card checkout or the local wallet/x402 burner. Drawer opened.';
|
|
1428
|
+
}
|
|
1429
|
+
if (cmd === 'hud') {
|
|
1430
|
+
const s = await sessionStats();
|
|
1431
|
+
const mode = t.runMode || 'ask';
|
|
1432
|
+
const tier = t.tier || 'auto';
|
|
1433
|
+
if (!s) return `Sitrep — mode ${mode} · ${tier} · proxy unreachable.`;
|
|
1434
|
+
const spent = Number(s.spentUsd) || 0;
|
|
1435
|
+
const sav = formatSavingLabel(s);
|
|
1436
|
+
return `Sitrep — mode ${mode} · ${tier} · paid ${usd(spent)} · ${sav.text} · ${s.paidCalls || 0} calls`;
|
|
1437
|
+
}
|
|
1352
1438
|
if (cmd === 'cost' || cmd === 'tokens') {
|
|
1353
1439
|
const s = await sessionStats();
|
|
1354
1440
|
if (!s) return 'The local openzoo proxy isn’t reachable, so there are no real numbers to show. (Not zero — unknown.)';
|
|
@@ -1360,9 +1446,9 @@ async function handleSlash(task, t) {
|
|
|
1360
1446
|
` paid calls ${s.paidCalls || 0}`,
|
|
1361
1447
|
];
|
|
1362
1448
|
if (spent > 0) {
|
|
1363
|
-
const
|
|
1364
|
-
lines.push(` multiple ${
|
|
1365
|
-
+ (mult < 1 ? ' — under 1x: small inputs cost MORE than sending them directly' : ''));
|
|
1449
|
+
const sav = formatSavingLabel(s);
|
|
1450
|
+
lines.push(` multiple ${sav.text}`
|
|
1451
|
+
+ (sav.mult != null && sav.mult < 1 ? ' — under 1x: small inputs cost MORE than sending them directly' : ''));
|
|
1366
1452
|
}
|
|
1367
1453
|
return 'This session:\n' + lines.join('\n');
|
|
1368
1454
|
}
|
|
@@ -1377,16 +1463,27 @@ async function handleSlash(task, t) {
|
|
|
1377
1463
|
// tier silently overriding an explicit id would make /model a suggestion.
|
|
1378
1464
|
if (cmd === 'tier') {
|
|
1379
1465
|
if (!arg) {
|
|
1380
|
-
const
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1466
|
+
const cur = t.tier || 'auto';
|
|
1467
|
+
if (cur === 'auto') {
|
|
1468
|
+
return `This thread: auto (classifier — openzoo/auto)\n`
|
|
1469
|
+
+ `Tiers: auto · ${TIER_NAMES.join(' · ')}\n`
|
|
1470
|
+
+ (t.model ? `NOTE: /model ${t.model} is pinned on this thread, so Auto is ignored until you /model default.\n` : '')
|
|
1471
|
+
+ 'Switch with /tier <name> · /race <n> to ask several at once.';
|
|
1472
|
+
}
|
|
1473
|
+
const picks = await tierModels(cur, 3);
|
|
1474
|
+
return `This thread: ${cur}\n`
|
|
1475
|
+
+ `Tiers: auto · ${TIER_NAMES.join(' · ')}\n`
|
|
1476
|
+
+ `Top of ${cur} right now: ${picks.join(', ')}\n`
|
|
1384
1477
|
+ (t.model ? `NOTE: /model ${t.model} is pinned on this thread, so the tier is ignored until you /model default.\n` : '')
|
|
1385
1478
|
+ 'Switch with /tier <name> · /race <n> to ask several at once.';
|
|
1386
1479
|
}
|
|
1387
1480
|
const want = normalizeTier(arg);
|
|
1388
|
-
if (!want) return `Unknown tier "${arg}". One of: ${TIER_NAMES.join(', ')} (also: grok 4.6).`;
|
|
1481
|
+
if (!want) return `Unknown tier "${arg}". One of: auto, ${TIER_NAMES.join(', ')} (also: grok 4.6).`;
|
|
1389
1482
|
t.tier = want; saveThreads();
|
|
1483
|
+
if (want === 'auto') {
|
|
1484
|
+
return 'This thread now uses Auto — cheapest model that clears the bar (openzoo/auto).'
|
|
1485
|
+
+ (t.model ? `\nBut /model ${t.model} is still pinned and wins. Run /model default to let Auto take over.` : '');
|
|
1486
|
+
}
|
|
1390
1487
|
const picks = await tierModels(want, 3);
|
|
1391
1488
|
return `This thread now runs on the ${want} tier — ${picks.join(', ')}…`
|
|
1392
1489
|
+ (t.model ? `\nBut /model ${t.model} is still pinned and wins. Run /model default to let the tier take over.` : '');
|
|
@@ -1521,7 +1618,7 @@ async function handleSlash(task, t) {
|
|
|
1521
1618
|
|
|
1522
1619
|
// Wake the room. Used to be a free last-line dump — idle children stayed
|
|
1523
1620
|
// idle, and a parent reading "kid: <old reply>" thought they had acted.
|
|
1524
|
-
// Empty extra is a
|
|
1621
|
+
// Empty extra is a continue wake (Claude Code on Auto), not a cancel. Thinking stays
|
|
1525
1622
|
// thinking; pendingRun stays on the human. Same branch scope as /all.
|
|
1526
1623
|
if (cmd === 'ping') {
|
|
1527
1624
|
const crew = subtreeOf(t.id, true);
|
|
@@ -1911,7 +2008,7 @@ function spawnBrief(parent, { refresh = false, child } = {}) {
|
|
|
1911
2008
|
const cwd = child?.dir || WORKSPACE_DIR;
|
|
1912
2009
|
const branch = child?.worktree?.branch;
|
|
1913
2010
|
const mode = parent.runMode || 'ask';
|
|
1914
|
-
const tier = parent.tier || '
|
|
2011
|
+
const tier = parent.tier || 'auto';
|
|
1915
2012
|
const race = Number(parent.race) || 0;
|
|
1916
2013
|
const raceNeed = Number(parent.raceNeed) || 1;
|
|
1917
2014
|
const model = parent.model || '';
|
|
@@ -2622,12 +2719,84 @@ async function mcpDirective(url, tool, args) {
|
|
|
2622
2719
|
}
|
|
2623
2720
|
}
|
|
2624
2721
|
|
|
2722
|
+
function autoClaudePrompt(t, userText, images) {
|
|
2723
|
+
const bits = [];
|
|
2724
|
+
if (t.memory?.length) bits.push(`Remember, for this thread:\n${t.memory.map((x) => `- ${x}`).join('\n')}`);
|
|
2725
|
+
if (t.todos?.length) {
|
|
2726
|
+
bits.push(`Current checklist:\n${t.todos.map((x, i) => `${i + 1}. [${x.done ? 'x' : ' '}] ${x.text}`).join('\n')}`);
|
|
2727
|
+
}
|
|
2728
|
+
if (images?.length) bits.push(`(${images.length} image(s) were attached in the desktop UI; work from the text ask.)`);
|
|
2729
|
+
bits.push(String(userText || ''));
|
|
2730
|
+
return bits.join('\n\n');
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
/**
|
|
2734
|
+
* Orange Auto = Claude Code print loop, paid through OpenZoo. Not the
|
|
2735
|
+
* RUN:/WRITE:/DONE: text harness (that parked on "Done." after empty mkdirs).
|
|
2736
|
+
*/
|
|
2737
|
+
async function runAutoClaudeTurn(t, userText, images, paint, stillMine, turnAbort) {
|
|
2738
|
+
paint({ type: 'start', name: t.name, color: t.color, detail: t.liveStatus || 'Claude Code via OpenZoo…' });
|
|
2739
|
+
let visible = '';
|
|
2740
|
+
let thinking = '';
|
|
2741
|
+
let streamedText = false;
|
|
2742
|
+
const result = await runClaudeCode({
|
|
2743
|
+
prompt: autoClaudePrompt(t, userText, images),
|
|
2744
|
+
cwd: dirFor(t.id),
|
|
2745
|
+
sessionId: t.claudeSessionId,
|
|
2746
|
+
model: t.model || undefined,
|
|
2747
|
+
env: process.env,
|
|
2748
|
+
signal: turnAbort.signal,
|
|
2749
|
+
onEvent(folded) {
|
|
2750
|
+
if (!stillMine() || !folded) return;
|
|
2751
|
+
if (folded.sessionId) t.claudeSessionId = folded.sessionId;
|
|
2752
|
+
if (folded.kind === 'init') {
|
|
2753
|
+
paint({ type: 'status', name: t.name, color: t.color, detail: folded.model || 'Claude Code' });
|
|
2754
|
+
return;
|
|
2755
|
+
}
|
|
2756
|
+
if (folded.kind === 'think' && folded.text) {
|
|
2757
|
+
thinking += folded.text;
|
|
2758
|
+
paint({ type: 'think', name: t.name, color: t.color, delta: folded.text });
|
|
2759
|
+
return;
|
|
2760
|
+
}
|
|
2761
|
+
if (folded.kind === 'text' && folded.text) {
|
|
2762
|
+
streamedText = true;
|
|
2763
|
+
visible += folded.text;
|
|
2764
|
+
paint({ type: 'delta', name: t.name, color: t.color, delta: folded.text });
|
|
2765
|
+
return;
|
|
2766
|
+
}
|
|
2767
|
+
if (folded.kind !== 'assistant') return;
|
|
2768
|
+
if (folded.thinking && !thinking) {
|
|
2769
|
+
thinking = folded.thinking;
|
|
2770
|
+
paint({ type: 'think', name: t.name, color: t.color, delta: folded.thinking });
|
|
2771
|
+
}
|
|
2772
|
+
if (folded.text && !streamedText) {
|
|
2773
|
+
visible = folded.text;
|
|
2774
|
+
paint({ type: 'delta', name: t.name, color: t.color, delta: folded.text, replace: true });
|
|
2775
|
+
}
|
|
2776
|
+
for (const tool of folded.tools || []) {
|
|
2777
|
+
paint({ type: 'status', name: t.name, color: t.color, detail: toolStatusLine(tool.name, tool.input) });
|
|
2778
|
+
}
|
|
2779
|
+
},
|
|
2780
|
+
});
|
|
2781
|
+
if (!stillMine()) return result.paymentFailed || result.text || visible || '';
|
|
2782
|
+
if (result.sessionId) t.claudeSessionId = result.sessionId;
|
|
2783
|
+
const finalText = result.paymentFailed
|
|
2784
|
+
|| result.text
|
|
2785
|
+
|| visible
|
|
2786
|
+
|| (result.missing ? CLAUDE_MISSING : '')
|
|
2787
|
+
|| '(no response)';
|
|
2788
|
+
t.history.push({ who: 'bot', text: finalText, thinking: thinking || undefined });
|
|
2789
|
+
paint({ type: 'final', name: t.name, color: t.color, text: finalText, thinking: thinking || undefined });
|
|
2790
|
+
return finalText;
|
|
2791
|
+
}
|
|
2792
|
+
|
|
2625
2793
|
// onEvent (optional) gets live progress for whoever's actually watching this
|
|
2626
2794
|
// call: {type:'start',name,color} when a bot begins its turn, {type:'status',
|
|
2627
2795
|
// detail} while paying / waiting / racing / walking tools, {type:'race',race}
|
|
2628
2796
|
// for the spectator grid (one cell per launched model + a judging beat),
|
|
2629
2797
|
// {type:'delta',name,color,delta} per streamed token (replace:true swaps the
|
|
2630
|
-
// bubble once), {type:'
|
|
2798
|
+
// bubble once), {type:'think',delta} for folded chain-of-thought (not the
|
|
2799
|
+
// Auto run-mode chip), {type:'final',name,color,text} once its full reply (or
|
|
2631
2800
|
// directive ack) is settled. Background turns go through kickTurn →
|
|
2632
2801
|
// emitToThread, which is a no-op if nobody has the thread open.
|
|
2633
2802
|
async function runTurn(threadId, userText, onEvent, images) {
|
|
@@ -2647,7 +2816,7 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
2647
2816
|
if (!stillMine()) return;
|
|
2648
2817
|
if (ev.type === 'status' && ev.detail && t.status === 'thinking') t.liveStatus = ev.detail;
|
|
2649
2818
|
if (ev.type === 'race' && ev.race && t.status === 'thinking') t.liveRace = ev.race;
|
|
2650
|
-
if (ev.type === 'delta' || ev.type === 'status' || ev.type === 'start' || ev.type === 'race') t.lastDeltaAt = Date.now();
|
|
2819
|
+
if (ev.type === 'delta' || ev.type === 'think' || ev.type === 'status' || ev.type === 'start' || ev.type === 'race') t.lastDeltaAt = Date.now();
|
|
2651
2820
|
onEvent?.(ev);
|
|
2652
2821
|
};
|
|
2653
2822
|
t.history.push(images && images.length ? { who: 'user', text: userText, images } : { who: 'user', text: userText });
|
|
@@ -2665,6 +2834,7 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
2665
2834
|
t.liveStatus = (!t.model && raceN >= 2) ? formatRaceStatus(0, raceNeed) : 'waiting on model…';
|
|
2666
2835
|
let chained = false;
|
|
2667
2836
|
let parked = false;
|
|
2837
|
+
let usedClaude = false;
|
|
2668
2838
|
let lastReply = '';
|
|
2669
2839
|
try {
|
|
2670
2840
|
if (t.members) {
|
|
@@ -2680,11 +2850,15 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
2680
2850
|
const emitStatus = (detail) => paint({ type: 'status', name: m.name, color: m.color, detail });
|
|
2681
2851
|
try {
|
|
2682
2852
|
r = onEvent
|
|
2683
|
-
? (await brainStream(msgs, (delta) =>
|
|
2853
|
+
? (await brainStream(msgs, (delta, meta) => {
|
|
2854
|
+
if (meta?.think) paint({ type: 'think', name: m.name, color: m.color, delta });
|
|
2855
|
+
else paint({ type: 'delta', name: m.name, color: m.color, delta });
|
|
2856
|
+
}, t.contextId, undefined, undefined, 0, 0, emitStatus)).trim()
|
|
2684
2857
|
: (await brain(msgs, t.contextId)).trim();
|
|
2685
2858
|
} catch (e) { r = `error: ${e.message}`; }
|
|
2686
2859
|
if (!stillMine()) return;
|
|
2687
|
-
|
|
2860
|
+
const memberThink = takeThink(r);
|
|
2861
|
+
r = memberThink.text;
|
|
2688
2862
|
memberReply = r;
|
|
2689
2863
|
const runCmd = parseRun(r);
|
|
2690
2864
|
if (runCmd) {
|
|
@@ -2694,16 +2868,19 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
2694
2868
|
const output = await execCommand(command, dirFor(t.id));
|
|
2695
2869
|
noteRunForCorpus(t.id, command, { cwd: dirFor(t.id) });
|
|
2696
2870
|
const shown = `$ ${command}\n${output}`;
|
|
2697
|
-
t.history.push({
|
|
2698
|
-
|
|
2871
|
+
t.history.push({
|
|
2872
|
+
who: 'bot', text: command, runStatus: 'done', runOutput: output,
|
|
2873
|
+
name: m.name, color: m.color, thinking: memberThink.thinking,
|
|
2874
|
+
});
|
|
2875
|
+
paint({ type: 'final', name: m.name, color: m.color, text: command, thinking: memberThink.thinking });
|
|
2699
2876
|
memberReply = shown;
|
|
2700
2877
|
// this member's turn is done; the round continues to the next member
|
|
2701
2878
|
continue;
|
|
2702
2879
|
}
|
|
2703
2880
|
const runId = randomUUID();
|
|
2704
2881
|
t.pendingRun = { runId, command, cwd: dirFor(t.id) };
|
|
2705
|
-
t.history.push({ who: 'bot', text: command, runId, runStatus: 'pending', name: m.name, color: m.color });
|
|
2706
|
-
paint({ type: 'run-pending', runId, command, name: m.name, color: m.color });
|
|
2882
|
+
t.history.push({ who: 'bot', text: command, runId, runStatus: 'pending', name: m.name, color: m.color, thinking: memberThink.thinking });
|
|
2883
|
+
paint({ type: 'run-pending', runId, command, name: m.name, color: m.color, thinking: memberThink.thinking });
|
|
2707
2884
|
// pauses the WHOLE round here — the rest of the group gets their turn
|
|
2708
2885
|
// on the round that runs after the user approves/denies
|
|
2709
2886
|
parked = true;
|
|
@@ -2711,8 +2888,8 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
2711
2888
|
}
|
|
2712
2889
|
const ack = await tryDirective(r, t.id, paint);
|
|
2713
2890
|
const finalText = ack ?? (r || '(no response)');
|
|
2714
|
-
t.history.push({ who: 'bot', text: finalText, name: m.name, color: m.color });
|
|
2715
|
-
paint({ type: 'final', name: m.name, color: m.color, text: finalText });
|
|
2891
|
+
t.history.push({ who: 'bot', text: finalText, name: m.name, color: m.color, thinking: memberThink.thinking });
|
|
2892
|
+
paint({ type: 'final', name: m.name, color: m.color, text: finalText, thinking: memberThink.thinking });
|
|
2716
2893
|
memberReply = r;
|
|
2717
2894
|
}
|
|
2718
2895
|
bindThread(t).catch(() => {});
|
|
@@ -2722,6 +2899,14 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
2722
2899
|
}
|
|
2723
2900
|
return;
|
|
2724
2901
|
}
|
|
2902
|
+
// Orange Auto is Claude Code (`openzoo claude` env), not the RUN:/WRITE:
|
|
2903
|
+
// text loop. Ask (and group members above) still use chat/completions.
|
|
2904
|
+
if (t.runMode === 'auto') {
|
|
2905
|
+
usedClaude = true;
|
|
2906
|
+
lastReply = await runAutoClaudeTurn(t, userText, images, paint, stillMine, turnAbort);
|
|
2907
|
+
if (stillMine()) bindThread(t).catch(() => {});
|
|
2908
|
+
return;
|
|
2909
|
+
}
|
|
2725
2910
|
// A real message from the user resets the auto budget AND the announcement
|
|
2726
2911
|
// nudge. Harness-injected hops (command output, directive result, nudge,
|
|
2727
2912
|
// auto-continue) must not re-arm — that would make AUTO_MAX_STEPS a no-op
|
|
@@ -2748,6 +2933,7 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
2748
2933
|
if (t.todos?.length) {
|
|
2749
2934
|
extras.push({ role: 'system', content: `Current checklist (TODO: done <n> to tick one off):\n${t.todos.map((x, i) => `${i + 1}. [${x.done ? 'x' : ' '}] ${x.text}`).join('\n')}` });
|
|
2750
2935
|
}
|
|
2936
|
+
extras.push({ role: 'system', content: CHAT_NOT_PROXY });
|
|
2751
2937
|
if (t.runMode === 'auto') extras.push({ role: 'system', content: AUTO_DIRECTIVE });
|
|
2752
2938
|
const callMsgs = extras.length ? [...t.messages, ...extras] : t.messages;
|
|
2753
2939
|
// WHICH MODEL SERVES THIS TURN.
|
|
@@ -2763,18 +2949,34 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
2763
2949
|
thread: t, attempt, userText, messages: callMsgs,
|
|
2764
2950
|
}) ?? '').trim();
|
|
2765
2951
|
}
|
|
2766
|
-
const emit = (delta, meta) =>
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2952
|
+
const emit = (delta, meta) => {
|
|
2953
|
+
if (meta?.think) {
|
|
2954
|
+
paint({ type: 'think', name: t.name, color: t.color, delta });
|
|
2955
|
+
return;
|
|
2956
|
+
}
|
|
2957
|
+
paint({
|
|
2958
|
+
type: 'delta', name: t.name, color: t.color, delta,
|
|
2959
|
+
...(meta?.replace ? { replace: true } : {}),
|
|
2960
|
+
...(meta?.model ? { model: meta.model } : {}),
|
|
2961
|
+
});
|
|
2962
|
+
};
|
|
2771
2963
|
const emitStatus = (detail) => paint({ type: 'status', name: t.name, color: t.color, detail });
|
|
2772
|
-
//
|
|
2773
|
-
//
|
|
2774
|
-
const topK = adaptiveTopK((
|
|
2964
|
+
// SPAWN kids search the project root's corpus. A brand-new chat is its
|
|
2965
|
+
// own holobrain — do not scale top_k off another thread's boundItems.
|
|
2966
|
+
const topK = adaptiveTopK((holobrainOf(t) || t).boundItems);
|
|
2775
2967
|
const race = Math.min(Number(t.race) || 0, 4);
|
|
2776
2968
|
if (!t.model && race >= 2) {
|
|
2777
|
-
const
|
|
2969
|
+
const useAuto = !t.tier || t.tier === 'auto';
|
|
2970
|
+
let models = [];
|
|
2971
|
+
if (useAuto) {
|
|
2972
|
+
try {
|
|
2973
|
+
const routed = routeChatBody({ messages: callMsgs }, { k: race, allow_free: false, bindable: true });
|
|
2974
|
+
models = (routed.shortlist || []).map((s) => s.model).filter(Boolean);
|
|
2975
|
+
if (routed.model && !models.includes(routed.model)) models.unshift(routed.model);
|
|
2976
|
+
models = [...new Set(models)].slice(0, race);
|
|
2977
|
+
} catch { models = []; }
|
|
2978
|
+
}
|
|
2979
|
+
if (!models.length) models = await tierModels(useAuto ? 'medium' : (t.tier || 'medium'), race, true);
|
|
2778
2980
|
// need = how many must come BACK before judging. need 1 is a plain
|
|
2779
2981
|
// first-past-the-post race; need N waits for all of them. The point of
|
|
2780
2982
|
// the middle (2 of 3) is a judged answer without the slowest entrant
|
|
@@ -2789,7 +2991,7 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
2789
2991
|
})).trim();
|
|
2790
2992
|
}
|
|
2791
2993
|
// A retry draws a DIFFERENT model from the tier rather than the same one.
|
|
2792
|
-
const model = t.model ||
|
|
2994
|
+
const model = t.model || 'openzoo/auto';
|
|
2793
2995
|
return (onEvent
|
|
2794
2996
|
? (await brainStream(callMsgs, emit, t.contextId, model, undefined, 0, topK, emitStatus)).trim()
|
|
2795
2997
|
: (await brain(callMsgs, t.contextId, model, topK)).trim());
|
|
@@ -2827,7 +3029,8 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
2827
3029
|
reply = `error: ${e.message}`;
|
|
2828
3030
|
}
|
|
2829
3031
|
if (!stillMine()) return;
|
|
2830
|
-
|
|
3032
|
+
const settled = takeThink(reply);
|
|
3033
|
+
reply = settled.text;
|
|
2831
3034
|
lastReply = reply;
|
|
2832
3035
|
t.messages.push({ role: 'assistant', content: reply });
|
|
2833
3036
|
const runCmd = parseRun(reply);
|
|
@@ -2839,8 +3042,8 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
2839
3042
|
noteRunForCorpus(t.id, command, { cwd: dirFor(t.id) });
|
|
2840
3043
|
if (!stillMine()) return;
|
|
2841
3044
|
const shown = `$ ${command}\n${output}`;
|
|
2842
|
-
t.history.push({ who: 'bot', text:
|
|
2843
|
-
paint({ type: 'final', name: t.name, color: t.color, text:
|
|
3045
|
+
t.history.push({ who: 'bot', text: command, runStatus: 'done', runOutput: output, thinking: settled.thinking });
|
|
3046
|
+
paint({ type: 'final', name: t.name, color: t.color, text: command, thinking: settled.thinking });
|
|
2844
3047
|
// FEED THE OUTPUT BACK. The 'ask' path already does this on approve, so
|
|
2845
3048
|
// auto mode was strictly LESS capable than the gated one: the command
|
|
2846
3049
|
// ran, the result was shown, and the model never saw it — no diagnosis,
|
|
@@ -2865,8 +3068,8 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
2865
3068
|
{
|
|
2866
3069
|
const runId = randomUUID();
|
|
2867
3070
|
t.pendingRun = { runId, command, cwd: dirFor(t.id) };
|
|
2868
|
-
t.history.push({ who: 'bot', text: command, runId, runStatus: 'pending' });
|
|
2869
|
-
paint({ type: 'run-pending', runId, command, name: t.name, color: t.color });
|
|
3071
|
+
t.history.push({ who: 'bot', text: command, runId, runStatus: 'pending', thinking: settled.thinking });
|
|
3072
|
+
paint({ type: 'run-pending', runId, command, name: t.name, color: t.color, thinking: settled.thinking });
|
|
2870
3073
|
}
|
|
2871
3074
|
parked = true;
|
|
2872
3075
|
return;
|
|
@@ -2874,8 +3077,8 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
2874
3077
|
const ack = await tryDirective(reply, t.id, paint);
|
|
2875
3078
|
if (!stillMine()) return;
|
|
2876
3079
|
const finalText = ack ?? (reply || '(no response)');
|
|
2877
|
-
t.history.push({ who: 'bot', text: finalText });
|
|
2878
|
-
paint({ type: 'final', name: t.name, color: t.color, text: finalText });
|
|
3080
|
+
t.history.push({ who: 'bot', text: finalText, thinking: settled.thinking });
|
|
3081
|
+
paint({ type: 'final', name: t.name, color: t.color, text: finalText, thinking: settled.thinking });
|
|
2879
3082
|
|
|
2880
3083
|
// AUTO CONTINUES AFTER *ANY* DIRECTIVE, not just RUN.
|
|
2881
3084
|
//
|
|
@@ -2934,7 +3137,7 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
2934
3137
|
// ask mode, 402/empty-wallet, or the hard cap. Empty /(no output) is not
|
|
2935
3138
|
// DONE — AUTO_EMPTY_RETRY. Otherwise kick again.
|
|
2936
3139
|
if (stillMine() && !chained && !parked) {
|
|
2937
|
-
if (shouldKeepAuto(t, lastReply, userText)) {
|
|
3140
|
+
if (!usedClaude && shouldKeepAuto(t, lastReply, userText)) {
|
|
2938
3141
|
enqueueAutoHop(t, threadId, autoHopText(lastReply, userText), onEvent);
|
|
2939
3142
|
} else if (!t.pendingRun) {
|
|
2940
3143
|
t.status = 'idle';
|
|
@@ -3095,19 +3298,19 @@ const APP_HTML = `<!doctype html>
|
|
|
3095
3298
|
<style>
|
|
3096
3299
|
:root { color-scheme: dark; }
|
|
3097
3300
|
* { box-sizing: border-box; }
|
|
3098
|
-
html, body { margin: 0; height: 100%; background: #000; }
|
|
3301
|
+
html, body { margin: 0; height: 100%; width: 100%; overflow: hidden; background: #000; }
|
|
3099
3302
|
body { color: #ececec; font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
3100
3303
|
display: flex; }
|
|
3101
3304
|
#dragbar { -webkit-app-region: drag; position: fixed; top: 0; left: 0; right: 0; height: 28px; z-index: 1000; }
|
|
3102
3305
|
#sidebar { width: 280px; flex: 0 0 280px; border-right: 1px solid #1c1c1e; display: flex; flex-direction: column;
|
|
3103
|
-
height:
|
|
3306
|
+
height: 100%; padding-top: 28px; min-height: 0; position: relative; }
|
|
3104
3307
|
#main { padding-top: 28px; }
|
|
3105
3308
|
#sideTop { display: flex; align-items: center; gap: 4px; padding: 0 8px; }
|
|
3106
3309
|
#sideTop #search { flex: 1; }
|
|
3107
3310
|
#search { margin: 12px; padding: 8px 12px; background: #1c1c1e; border-radius: 10px; color: #ececec;
|
|
3108
3311
|
border: none; font: inherit; }
|
|
3109
3312
|
#search::placeholder { color: #8e8e93; }
|
|
3110
|
-
#threads { flex: 1; overflow-y: auto; }
|
|
3313
|
+
#threads { flex: 1; min-height: 0; overflow-y: auto; }
|
|
3111
3314
|
.trow { display: flex; align-items: center; gap: 10px; padding: 8px 12px; cursor: pointer; border-radius: 10px;
|
|
3112
3315
|
margin: 0 6px 2px; }
|
|
3113
3316
|
/* PROJECT HEADER. The tree indentation shows who spawned whom, but there was
|
|
@@ -3172,7 +3375,7 @@ const APP_HTML = `<!doctype html>
|
|
|
3172
3375
|
@media (prefers-reduced-motion: reduce) {
|
|
3173
3376
|
.twarn, .bot-pfp .bot-bob, .bot-pfp .bot-eyes { animation: none; }
|
|
3174
3377
|
}
|
|
3175
|
-
#main { position: relative; flex: 1; min-width: 0; display: flex; flex-direction: column; height:
|
|
3378
|
+
#main { position: relative; flex: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column; height: 100%; }
|
|
3176
3379
|
#chatHeader { padding: 14px 20px; border-bottom: 1px solid #1c1c1e; display: flex; align-items: center;
|
|
3177
3380
|
flex-wrap: wrap; gap: 8px 10px; font-weight: 600; }
|
|
3178
3381
|
#chatHeader .tavatar { width: 26px; height: 26px; border-radius: 50%; font-size: 11px; flex: 0 0 26px; }
|
|
@@ -3299,6 +3502,21 @@ const APP_HTML = `<!doctype html>
|
|
|
3299
3502
|
#hud { position: absolute; right: 14px; width: 270px; background: rgba(14,14,17,.94);
|
|
3300
3503
|
border: 1px solid #333340; border-radius: 10px; padding: 12px 14px; font: 11px/1.5 Menlo, monospace;
|
|
3301
3504
|
display: none; z-index: 300; box-shadow: 0 12px 30px rgba(0,0,0,.5); }
|
|
3505
|
+
/* Always-on strip. Sidebar footer — bottom-left of the window, inside the
|
|
3506
|
+
bot list column. Never a child of #main: position:absolute; left:14px
|
|
3507
|
+
there sat on the transcript and covered the last bubbles. ◎ stays a
|
|
3508
|
+
toggle; this one never hides. pointer-events none so it cannot steal
|
|
3509
|
+
thread-row clicks. */
|
|
3510
|
+
#dockHud { flex: 0 0 auto; position: relative; left: auto; bottom: auto;
|
|
3511
|
+
width: 100%; max-width: 100%; z-index: 1;
|
|
3512
|
+
pointer-events: none; background: rgba(14,14,17,.94);
|
|
3513
|
+
border: 0; border-top: 1px solid #333340; border-radius: 0;
|
|
3514
|
+
padding: 8px 12px 12px; color: #8e8e93;
|
|
3515
|
+
font: 10.5px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
3516
|
+
display: flex; flex-wrap: wrap; gap: 6px 10px; align-items: baseline; }
|
|
3517
|
+
#dockHud .dk { color: #6f7080; }
|
|
3518
|
+
#dockHud .dv { color: #c8c8c2; }
|
|
3519
|
+
#dockHud .dv.hlime { color: #b8f240; }
|
|
3302
3520
|
#hud.show { display: block; }
|
|
3303
3521
|
#hud .htitle { color: #b8f240; font-size: 10px; letter-spacing: .04em; margin-bottom: 10px; }
|
|
3304
3522
|
#hud .htitle.hsession { margin-top: 12px; padding-top: 10px; border-top: 1px solid #333340; color: #6f7080; }
|
|
@@ -3316,8 +3534,8 @@ const APP_HTML = `<!doctype html>
|
|
|
3316
3534
|
color: #f0c9a8; font-size: 10.5px; line-height: 1.45; }
|
|
3317
3535
|
#hud .hhint.show { display: block; }
|
|
3318
3536
|
#hud .hhint b { color: #f28c4d; font-weight: 600; }
|
|
3319
|
-
#sidebar, #main, #walletOverlay, #sitrepOverlay, #composeOverlay,
|
|
3320
|
-
#inp, #search, #composeInp, .bubble, .md-pre, .runoutput, .runcmd {
|
|
3537
|
+
#sidebar, #main, #walletOverlay, #sitrepOverlay, #composeOverlay, #findBar,
|
|
3538
|
+
#inp, #search, #composeInp, #findInp, .bubble, .md-pre, .runoutput, .runcmd {
|
|
3321
3539
|
-webkit-app-region: no-drag; }
|
|
3322
3540
|
#log { flex: 1; min-width: 0; overflow-y: auto; overflow-x: hidden; padding: 20px 24px 12px;
|
|
3323
3541
|
display: flex; flex-direction: column; gap: 6px;
|
|
@@ -3399,6 +3617,18 @@ const APP_HTML = `<!doctype html>
|
|
|
3399
3617
|
}
|
|
3400
3618
|
.runcard { background: #1c1c1e; border: 1px solid #333; border-radius: 14px; padding: 12px 14px;
|
|
3401
3619
|
max-width: 100%; min-width: 0; overflow: hidden; }
|
|
3620
|
+
/* Done RUNs collapse like thinking… — raw curl JSON must not be the message. */
|
|
3621
|
+
.runcard.folded { background: transparent; border: none; padding: 0; }
|
|
3622
|
+
.runfold { align-self: flex-start; max-width: 100%; min-width: 0; }
|
|
3623
|
+
.runchip {
|
|
3624
|
+
border: 0; background: transparent; color: #8e8e93; padding: 0;
|
|
3625
|
+
font: 12px/1.4 inherit; cursor: pointer; letter-spacing: .01em;
|
|
3626
|
+
-webkit-user-select: none; user-select: none;
|
|
3627
|
+
}
|
|
3628
|
+
.runchip:hover { color: #c8c8d0; }
|
|
3629
|
+
.runchip:focus-visible { outline: 2px solid #ff9500; outline-offset: 2px; border-radius: 4px; }
|
|
3630
|
+
.runbody { display: none; margin: 4px 0 2px; }
|
|
3631
|
+
.runfold.open .runbody { display: block; }
|
|
3402
3632
|
.runcmd { font-family: Menlo, monospace; font-size: 12.5px; color: #ececec; white-space: pre-wrap;
|
|
3403
3633
|
word-break: break-word; margin-bottom: 8px; }
|
|
3404
3634
|
.runactions { display: flex; gap: 8px; }
|
|
@@ -3411,6 +3641,25 @@ const APP_HTML = `<!doctype html>
|
|
|
3411
3641
|
word-break: break-word; max-height: 240px; overflow-y: auto; margin: 0; }
|
|
3412
3642
|
.row.user .bubble { background: #57575c; }
|
|
3413
3643
|
.row.bot .bubble { background: #262626; color: #ececec; }
|
|
3644
|
+
/* Folded chain-of-thought. Not the Auto run-mode chip — that is /mode auto.
|
|
3645
|
+
Default collapsed; click the label to unfurl, click again to furl.
|
|
3646
|
+
No empty chip: the fold is omitted when the model did not reason. */
|
|
3647
|
+
.msgcol { display: flex; flex-direction: column; gap: 6px; min-width: 0; max-width: 100%; flex: 1; }
|
|
3648
|
+
.thinkfold { align-self: flex-start; max-width: 100%; min-width: 0; }
|
|
3649
|
+
.thinkchip {
|
|
3650
|
+
border: 0; background: transparent; color: #8e8e93; padding: 0;
|
|
3651
|
+
font: 12px/1.4 inherit; cursor: pointer; letter-spacing: .01em;
|
|
3652
|
+
-webkit-user-select: none; user-select: none;
|
|
3653
|
+
}
|
|
3654
|
+
.thinkchip:hover { color: #c8c8d0; }
|
|
3655
|
+
.thinkchip:focus-visible { outline: 2px solid #b8f240; outline-offset: 2px; border-radius: 4px; }
|
|
3656
|
+
.thinkbody {
|
|
3657
|
+
display: none; margin: 4px 0 2px; padding: 8px 12px;
|
|
3658
|
+
color: #8e8e93; font-size: 12.5px; line-height: 1.45;
|
|
3659
|
+
white-space: pre-wrap; word-break: break-word;
|
|
3660
|
+
border-left: 2px solid #3a3a3c; max-height: 240px; overflow-y: auto;
|
|
3661
|
+
}
|
|
3662
|
+
.thinkfold.open .thinkbody { display: block; }
|
|
3414
3663
|
.row.bot.pending .bubble { color: #8e8e93; }
|
|
3415
3664
|
.dots span { display: inline-block; width: 5px; height: 5px; margin-right: 3px; border-radius: 50%;
|
|
3416
3665
|
background: #8e8e93; animation: blink 1.2s infinite ease-in-out; }
|
|
@@ -3535,6 +3784,23 @@ const APP_HTML = `<!doctype html>
|
|
|
3535
3784
|
transition: opacity .14s ease, transform .14s ease;
|
|
3536
3785
|
}
|
|
3537
3786
|
#copiedToast.show { opacity: 1; transform: translate(-50%, 0); }
|
|
3787
|
+
/* FIND IN THREAD. Cmd/Ctrl+F — not the sidebar thread search (that's Cmd+K).
|
|
3788
|
+
Sits over #log so matches stay in the conversation, not the thread list. */
|
|
3789
|
+
#findBar { display: none; position: absolute; right: 16px; z-index: 320;
|
|
3790
|
+
align-items: center; gap: 6px; background: rgba(18,18,22,.98);
|
|
3791
|
+
border: 1px solid #3a3a3c; border-radius: 12px; padding: 6px 8px;
|
|
3792
|
+
box-shadow: 0 10px 28px rgba(0,0,0,.45); }
|
|
3793
|
+
#findBar.show { display: flex; }
|
|
3794
|
+
#findInp { width: 180px; background: #0b0b0d; border: 1px solid #2c2c2e; border-radius: 8px;
|
|
3795
|
+
color: #ececec; font: inherit; font-size: 13px; padding: 5px 8px; }
|
|
3796
|
+
#findInp:focus { outline: 2px solid #6ab0ff; outline-offset: 1px; }
|
|
3797
|
+
#findCount { min-width: 48px; color: #8e8e93; font-size: 12px; text-align: right;
|
|
3798
|
+
font-variant-numeric: tabular-nums; }
|
|
3799
|
+
#findBar button { border: 0; background: transparent; color: #ececec; width: 26px; height: 26px;
|
|
3800
|
+
border-radius: 8px; cursor: pointer; font: inherit; line-height: 1; }
|
|
3801
|
+
#findBar button:hover { background: #3a3a3c; }
|
|
3802
|
+
mark.findhit { background: #f2d64b; color: #111; border-radius: 3px; padding: 0 1px; }
|
|
3803
|
+
mark.findhit.cur { background: #b8f240; }
|
|
3538
3804
|
</style></head>
|
|
3539
3805
|
<body>
|
|
3540
3806
|
<div id="copiedToast" role="status" aria-live="polite">copied</div>
|
|
@@ -3547,6 +3813,13 @@ const APP_HTML = `<!doctype html>
|
|
|
3547
3813
|
<input id="search" placeholder="Search">
|
|
3548
3814
|
</div>
|
|
3549
3815
|
<div id="threads"></div>
|
|
3816
|
+
<div id="dockHud" data-component="dock-hud">
|
|
3817
|
+
<span><span class="dk">spill</span> <span id="dockSpill" class="dv">—</span></span>
|
|
3818
|
+
<span><span class="dk">session</span> <span id="dockSession" class="dv">—</span></span>
|
|
3819
|
+
<span><span class="dk">paid</span> <span id="dockPaid" class="dv">—</span></span>
|
|
3820
|
+
<span><span class="dk">bind</span> <span id="dockBind" class="dv">no</span></span>
|
|
3821
|
+
<span><span class="dk">calls</span> <span id="dockCalls" class="dv">0</span></span>
|
|
3822
|
+
</div>
|
|
3550
3823
|
</div>
|
|
3551
3824
|
<div id="composeOverlay">
|
|
3552
3825
|
<div id="composeBox">
|
|
@@ -3607,9 +3880,10 @@ const APP_HTML = `<!doctype html>
|
|
|
3607
3880
|
title="Shell commands run immediately, with no approval prompt">auto</button>
|
|
3608
3881
|
</div>
|
|
3609
3882
|
<select class="dial" id="tierSel" data-component="model-tier" aria-label="Model tier"
|
|
3610
|
-
title="
|
|
3883
|
+
title="Auto = cheapest model that clears the bar. Other tiers only apply to /race.">
|
|
3884
|
+
<option value="auto" selected>auto</option>
|
|
3611
3885
|
<option value="cheap">cheap</option>
|
|
3612
|
-
<option value="medium"
|
|
3886
|
+
<option value="medium">medium</option>
|
|
3613
3887
|
<option value="expensive">expensive</option>
|
|
3614
3888
|
<option value="grok4.6">grok 4.6</option>
|
|
3615
3889
|
</select>
|
|
@@ -3647,6 +3921,13 @@ const APP_HTML = `<!doctype html>
|
|
|
3647
3921
|
<div class="hfoot" id="hFoot">loading…</div>
|
|
3648
3922
|
</div>
|
|
3649
3923
|
<div id="log"></div>
|
|
3924
|
+
<div id="findBar" role="search" data-component="find-in-thread">
|
|
3925
|
+
<input id="findInp" type="search" placeholder="Find in conversation" autocomplete="off" spellcheck="false">
|
|
3926
|
+
<span id="findCount" aria-live="polite"></span>
|
|
3927
|
+
<button type="button" id="findPrev" title="Previous" aria-label="Previous match">↑</button>
|
|
3928
|
+
<button type="button" id="findNext" title="Next" aria-label="Next match">↓</button>
|
|
3929
|
+
<button type="button" id="findClose" title="Close" aria-label="Close find">×</button>
|
|
3930
|
+
</div>
|
|
3650
3931
|
<div id="bar">
|
|
3651
3932
|
<div id="plusMenu">
|
|
3652
3933
|
<div class="pop-item" id="attachBtn">
|
|
@@ -3678,6 +3959,17 @@ const APP_HTML = `<!doctype html>
|
|
|
3678
3959
|
</div>
|
|
3679
3960
|
</div>
|
|
3680
3961
|
<script>
|
|
3962
|
+
function formatSavingLabel(you) {
|
|
3963
|
+
const spent = Number(you && you.spentUsd) || 0;
|
|
3964
|
+
if (spent <= 0) return { text: '—', mult: null, spilled: false };
|
|
3965
|
+
const spillX = Number(you && you.spilled && you.spilled.savingX);
|
|
3966
|
+
const sessionX = (Number(you && you.directUsd) || 0) / spent;
|
|
3967
|
+
const spilled = Number.isFinite(spillX) && spillX > 0;
|
|
3968
|
+
const mult = spilled ? spillX : sessionX;
|
|
3969
|
+
if (!Number.isFinite(mult)) return { text: '—', mult: null, spilled: false };
|
|
3970
|
+
const num = (mult >= 100 ? String(Math.round(mult)) : Number(mult).toFixed(mult >= 10 ? 1 : 2)) + 'x';
|
|
3971
|
+
return { text: num + (spilled ? ' spilled' : ' session'), mult, spilled };
|
|
3972
|
+
}
|
|
3681
3973
|
const threadsEl = document.getElementById('threads');
|
|
3682
3974
|
const chatHeader = document.getElementById('chatHeader');
|
|
3683
3975
|
const log = document.getElementById('log');
|
|
@@ -4008,7 +4300,7 @@ const APP_HTML = `<!doctype html>
|
|
|
4008
4300
|
const tierSel = document.getElementById('tierSel');
|
|
4009
4301
|
const raceSel = document.getElementById('raceSel');
|
|
4010
4302
|
if (!tierSel || !raceSel) return;
|
|
4011
|
-
tierSel.value = t.tier || '
|
|
4303
|
+
tierSel.value = t.tier || 'auto';
|
|
4012
4304
|
raceSel.value = (t.race || 0) < 2 ? '0'
|
|
4013
4305
|
: ((t.raceNeed || 1) > 1 ? t.raceNeed + ' ' + t.race : String(t.race));
|
|
4014
4306
|
// A pinned /model makes BOTH dials inert. Showing them live while they do
|
|
@@ -4031,15 +4323,18 @@ const APP_HTML = `<!doctype html>
|
|
|
4031
4323
|
}
|
|
4032
4324
|
}
|
|
4033
4325
|
|
|
4034
|
-
|
|
4326
|
+
// Header dials and Pay / ◎ all go through /drive so handleSlash (or the
|
|
4327
|
+
// /mode handler) appends the same short bot line as typing the command.
|
|
4328
|
+
async function echoSlash(task) {
|
|
4035
4329
|
if (!activeId) return;
|
|
4036
|
-
// Reuses the SAME slash-command path, so there is one implementation of the
|
|
4037
|
-
// rule rather than a second that can disagree with it.
|
|
4038
4330
|
await fetch(API + '/drive', { method: 'POST', headers: { 'content-type': 'application/json' },
|
|
4039
|
-
body: JSON.stringify({ threadId: activeId, task:
|
|
4331
|
+
body: JSON.stringify({ threadId: activeId, task: task }) });
|
|
4040
4332
|
await loadThreads();
|
|
4041
4333
|
await render();
|
|
4042
4334
|
}
|
|
4335
|
+
async function setDial(cmd, value) {
|
|
4336
|
+
await echoSlash('/' + cmd + ' ' + value);
|
|
4337
|
+
}
|
|
4043
4338
|
// WALLET MODAL.
|
|
4044
4339
|
//
|
|
4045
4340
|
// Public addresses and balances only — /wallet proxies the proxy's own
|
|
@@ -4066,7 +4361,13 @@ const APP_HTML = `<!doctype html>
|
|
|
4066
4361
|
return row;
|
|
4067
4362
|
}
|
|
4068
4363
|
function isEmptyWalletPayment(text) {
|
|
4069
|
-
|
|
4364
|
+
// String ops. A word-boundary regex inside APP_HTML is eaten: \b
|
|
4365
|
+
// becomes a literal backspace and the client never matches a real 402.
|
|
4366
|
+
const s = String(text || '').toLowerCase();
|
|
4367
|
+
return s.includes('wallet is empty')
|
|
4368
|
+
|| s.includes('empty wallet')
|
|
4369
|
+
|| s.includes('wallet underfunded')
|
|
4370
|
+
|| s.includes('underfunded');
|
|
4070
4371
|
}
|
|
4071
4372
|
var openedPayForEmpty = false;
|
|
4072
4373
|
function maybeOpenPayForEmptyWallet(text) {
|
|
@@ -4287,7 +4588,10 @@ const APP_HTML = `<!doctype html>
|
|
|
4287
4588
|
setSubNote('Subscription key removed. Wallet/x402 is the pay method again.');
|
|
4288
4589
|
await openWallet();
|
|
4289
4590
|
}
|
|
4290
|
-
document.getElementById('walletBtn').addEventListener('click',
|
|
4591
|
+
document.getElementById('walletBtn').addEventListener('click', () => {
|
|
4592
|
+
echoSlash('/pay');
|
|
4593
|
+
openWallet();
|
|
4594
|
+
});
|
|
4291
4595
|
const subKeyBtn = document.getElementById('subKeyBtn');
|
|
4292
4596
|
if (subKeyBtn) subKeyBtn.addEventListener('click', savePastedSub);
|
|
4293
4597
|
const subForgetBtn = document.getElementById('subForgetBtn');
|
|
@@ -4356,10 +4660,9 @@ const APP_HTML = `<!doctype html>
|
|
|
4356
4660
|
const spent = Number(you.spentUsd) || 0;
|
|
4357
4661
|
const cogs = Number(you.cogsUsd) || 0;
|
|
4358
4662
|
const direct = Number(you.directUsd) || 0;
|
|
4359
|
-
const
|
|
4360
|
-
const saved =
|
|
4361
|
-
|
|
4362
|
-
const savedCls = mult == null ? '' : (mult >= 1 ? 'hlime' : 'hember');
|
|
4663
|
+
const sav = formatSavingLabel(you);
|
|
4664
|
+
const saved = sav.text;
|
|
4665
|
+
const savedCls = sav.mult == null ? '' : (sav.mult >= 1 ? 'hlime' : 'hember');
|
|
4363
4666
|
const thinking = (full && full.status === 'thinking') || t.status === 'thinking';
|
|
4364
4667
|
const race = (full && full.liveRace) || null;
|
|
4365
4668
|
let flight = 'idle';
|
|
@@ -4408,22 +4711,33 @@ const APP_HTML = `<!doctype html>
|
|
|
4408
4711
|
setModeButtons(mode); // optimistic: the click should feel instant
|
|
4409
4712
|
// Reuses the SAME "/mode" path the chat command takes, so there is one
|
|
4410
4713
|
// implementation of the rule rather than a second one that can disagree.
|
|
4411
|
-
await
|
|
4412
|
-
body: JSON.stringify({ threadId: activeId, task: '/mode ' + mode }) });
|
|
4413
|
-
await loadThreads(); // refresh runMode + the confirmation line /mode appends
|
|
4414
|
-
await render();
|
|
4714
|
+
await echoSlash('/mode ' + mode);
|
|
4415
4715
|
}
|
|
4416
4716
|
document.getElementById('modeAsk').addEventListener('click', () => setMode('ask'));
|
|
4417
4717
|
document.getElementById('modeAuto').addEventListener('click', () => setMode('auto'));
|
|
4418
4718
|
|
|
4419
4719
|
function escapeHtml(s) { return s.replace(/[&<>]/g, (c) => ({ '&': '&', '<': '<', '>': '>' }[c])); }
|
|
4420
|
-
function
|
|
4720
|
+
function splitThinkTags(s) {
|
|
4421
4721
|
s = String(s == null ? '' : s);
|
|
4422
|
-
|
|
4423
|
-
s = s.replace(/<think(?:ing)?\\b[^>]*>[\\s\\S]
|
|
4722
|
+
var bits = [];
|
|
4723
|
+
s = s.replace(/<think(?:ing)?\\b[^>]*>[\\s\\S]*?<\\/think(?:ing)?>/gi, function (m) {
|
|
4724
|
+
var a = m.indexOf('>');
|
|
4725
|
+
var b = m.toLowerCase().lastIndexOf('</think');
|
|
4726
|
+
if (a >= 0 && b > a) bits.push(m.slice(a + 1, b));
|
|
4727
|
+
return '';
|
|
4728
|
+
});
|
|
4729
|
+
s = s.replace(/<think(?:ing)?\\b[^>]*>[\\s\\S]*$/i, function (m) {
|
|
4730
|
+
var a = m.indexOf('>');
|
|
4731
|
+
bits.push(a >= 0 ? m.slice(a + 1) : '');
|
|
4732
|
+
return '';
|
|
4733
|
+
});
|
|
4424
4734
|
s = s.replace(/<\\/think(?:ing)?>/gi, '');
|
|
4425
|
-
return
|
|
4735
|
+
return {
|
|
4736
|
+
visible: s.replace(/^\\n+|\\n+$/g, '').trim(),
|
|
4737
|
+
thinking: bits.join('\\n').replace(/^\\n+|\\n+$/g, '').trim()
|
|
4738
|
+
};
|
|
4426
4739
|
}
|
|
4740
|
+
function stripThinkTags(s) { return splitThinkTags(s).visible; }
|
|
4427
4741
|
function clientWorkspaceUrl(rel) {
|
|
4428
4742
|
if (!workspacePort || !activeId) return '';
|
|
4429
4743
|
rel = String(rel || '').replace(/^\\/+/, '');
|
|
@@ -4806,8 +5120,98 @@ const APP_HTML = `<!doctype html>
|
|
|
4806
5120
|
}
|
|
4807
5121
|
|
|
4808
5122
|
let lastSpeaker = null;
|
|
4809
|
-
|
|
4810
|
-
|
|
5123
|
+
let liveThinkOpen = false;
|
|
5124
|
+
function thinkLabel(live) { return live ? 'thinking...' : 'thought'; }
|
|
5125
|
+
function makeThinkFold(text, live, open) {
|
|
5126
|
+
const fold = document.createElement('div');
|
|
5127
|
+
fold.className = 'thinkfold' + (open ? ' open' : '');
|
|
5128
|
+
const chip = document.createElement('button');
|
|
5129
|
+
chip.type = 'button';
|
|
5130
|
+
chip.className = 'thinkchip';
|
|
5131
|
+
chip.textContent = thinkLabel(live);
|
|
5132
|
+
chip.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
5133
|
+
const body = document.createElement('div');
|
|
5134
|
+
body.className = 'thinkbody';
|
|
5135
|
+
if (open) body.textContent = text;
|
|
5136
|
+
chip.addEventListener('click', function (e) {
|
|
5137
|
+
e.preventDefault();
|
|
5138
|
+
const next = !fold.classList.contains('open');
|
|
5139
|
+
fold.classList.toggle('open', next);
|
|
5140
|
+
chip.setAttribute('aria-expanded', next ? 'true' : 'false');
|
|
5141
|
+
if (live) liveThinkOpen = next;
|
|
5142
|
+
if (next) body.textContent = fold.getAttribute('data-think') || text || '';
|
|
5143
|
+
else body.textContent = '';
|
|
5144
|
+
if (live) paintStream();
|
|
5145
|
+
});
|
|
5146
|
+
fold.setAttribute('data-think', text || '');
|
|
5147
|
+
fold.appendChild(chip);
|
|
5148
|
+
fold.appendChild(body);
|
|
5149
|
+
return fold;
|
|
5150
|
+
}
|
|
5151
|
+
function runFoldLabel(status) {
|
|
5152
|
+
if (status === 'pending') return 'run';
|
|
5153
|
+
if (status === 'running') return 'running...';
|
|
5154
|
+
if (status === 'denied') return 'denied';
|
|
5155
|
+
return 'ran';
|
|
5156
|
+
}
|
|
5157
|
+
function parseLegacyRun(text) {
|
|
5158
|
+
const raw = String(text || '');
|
|
5159
|
+
if (!raw.startsWith('$ ')) return null;
|
|
5160
|
+
const nl = raw.indexOf('\\n');
|
|
5161
|
+
if (nl < 0) return { command: raw.slice(2), output: '', status: 'done' };
|
|
5162
|
+
return { command: raw.slice(2, nl), output: raw.slice(nl + 1), status: 'done' };
|
|
5163
|
+
}
|
|
5164
|
+
function makeRunFold(command, output, status) {
|
|
5165
|
+
const pending = status === 'pending' || status === 'running';
|
|
5166
|
+
const fold = document.createElement('div');
|
|
5167
|
+
fold.className = 'runfold' + (pending ? ' open' : '');
|
|
5168
|
+
const chip = document.createElement('button');
|
|
5169
|
+
chip.type = 'button';
|
|
5170
|
+
chip.className = 'runchip';
|
|
5171
|
+
chip.textContent = runFoldLabel(status);
|
|
5172
|
+
chip.setAttribute('aria-expanded', pending ? 'true' : 'false');
|
|
5173
|
+
const body = document.createElement('div');
|
|
5174
|
+
body.className = 'runbody';
|
|
5175
|
+
function fillBody() {
|
|
5176
|
+
body.innerHTML = '';
|
|
5177
|
+
const cmdEl = document.createElement('div');
|
|
5178
|
+
cmdEl.className = 'runcmd';
|
|
5179
|
+
cmdEl.textContent = '$ ' + command;
|
|
5180
|
+
cmdEl.appendChild(copyBtn(() => command, 'copy'));
|
|
5181
|
+
body.appendChild(cmdEl);
|
|
5182
|
+
if (status && status !== 'pending') {
|
|
5183
|
+
const st = document.createElement('div');
|
|
5184
|
+
st.className = 'runstatus';
|
|
5185
|
+
st.textContent = status === 'running' ? 'Running…' : status === 'denied' ? 'Denied' : 'Done';
|
|
5186
|
+
body.appendChild(st);
|
|
5187
|
+
}
|
|
5188
|
+
if (output) {
|
|
5189
|
+
const out = document.createElement('pre');
|
|
5190
|
+
out.className = 'runoutput';
|
|
5191
|
+
out.textContent = output;
|
|
5192
|
+
out.appendChild(copyBtn(() => output, 'copy'));
|
|
5193
|
+
body.appendChild(out);
|
|
5194
|
+
}
|
|
5195
|
+
}
|
|
5196
|
+
if (pending) fillBody();
|
|
5197
|
+
chip.addEventListener('click', function (e) {
|
|
5198
|
+
e.preventDefault();
|
|
5199
|
+
const next = !fold.classList.contains('open');
|
|
5200
|
+
fold.classList.toggle('open', next);
|
|
5201
|
+
chip.setAttribute('aria-expanded', next ? 'true' : 'false');
|
|
5202
|
+
if (next) fillBody();
|
|
5203
|
+
else body.innerHTML = '';
|
|
5204
|
+
});
|
|
5205
|
+
fold.appendChild(chip);
|
|
5206
|
+
fold.appendChild(body);
|
|
5207
|
+
return fold;
|
|
5208
|
+
}
|
|
5209
|
+
function addRow(who, text, color, name, run, images, thinking, live) {
|
|
5210
|
+
if (who === 'bot') {
|
|
5211
|
+
const parts = splitThinkTags(text);
|
|
5212
|
+
text = parts.visible;
|
|
5213
|
+
if (!thinking) thinking = parts.thinking;
|
|
5214
|
+
}
|
|
4811
5215
|
const speakerKey = who + '|' + name;
|
|
4812
5216
|
if (who === 'bot' && speakerKey !== lastSpeaker) {
|
|
4813
5217
|
const hdr = document.createElement('div');
|
|
@@ -4818,17 +5222,25 @@ const APP_HTML = `<!doctype html>
|
|
|
4818
5222
|
lastSpeaker = speakerKey;
|
|
4819
5223
|
const row = document.createElement('div');
|
|
4820
5224
|
row.className = 'row ' + who;
|
|
5225
|
+
const col = document.createElement('div');
|
|
5226
|
+
col.className = 'msgcol';
|
|
5227
|
+
const thinkText = (who === 'bot' && thinking) ? String(thinking).trim() : '';
|
|
5228
|
+
if (live) {
|
|
5229
|
+
const fold = makeThinkFold(thinkText, true, liveThinkOpen && !!thinkText);
|
|
5230
|
+
fold.id = 'streamThink';
|
|
5231
|
+
if (!thinkText) fold.hidden = true;
|
|
5232
|
+
col.appendChild(fold);
|
|
5233
|
+
} else if (thinkText) {
|
|
5234
|
+
col.appendChild(makeThinkFold(thinkText, false, false));
|
|
5235
|
+
}
|
|
4821
5236
|
if (run) {
|
|
5237
|
+
const cmd = run.command || text;
|
|
5238
|
+
const st = run.status || 'pending';
|
|
5239
|
+
const pending = st === 'pending' || st === 'running';
|
|
4822
5240
|
const card = document.createElement('div');
|
|
4823
|
-
card.className = 'runcard';
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
cmdEl.textContent = '$ ' + text;
|
|
4827
|
-
// Copy WITHOUT the '$ ' prompt — pasting that into a shell is a syntax
|
|
4828
|
-
// error, and this is the single most re-run thing in the UI.
|
|
4829
|
-
cmdEl.appendChild(copyBtn(() => text, 'copy'));
|
|
4830
|
-
card.appendChild(cmdEl);
|
|
4831
|
-
if (run.status === 'pending') {
|
|
5241
|
+
card.className = 'runcard' + (pending ? ' pending' : ' folded');
|
|
5242
|
+
card.appendChild(makeRunFold(cmd, run.output || '', st));
|
|
5243
|
+
if (run.id && st === 'pending') {
|
|
4832
5244
|
const actions = document.createElement('div');
|
|
4833
5245
|
actions.className = 'runactions';
|
|
4834
5246
|
const approve = document.createElement('button');
|
|
@@ -4850,20 +5262,8 @@ const APP_HTML = `<!doctype html>
|
|
|
4850
5262
|
actions.appendChild(approve);
|
|
4851
5263
|
actions.appendChild(deny);
|
|
4852
5264
|
card.appendChild(actions);
|
|
4853
|
-
} else {
|
|
4854
|
-
const status = document.createElement('div');
|
|
4855
|
-
status.className = 'runstatus';
|
|
4856
|
-
status.textContent = run.status === 'running' ? 'Running…' : run.status === 'denied' ? 'Denied' : 'Done';
|
|
4857
|
-
card.appendChild(status);
|
|
4858
|
-
if (run.output) {
|
|
4859
|
-
const out = document.createElement('pre');
|
|
4860
|
-
out.className = 'runoutput';
|
|
4861
|
-
out.textContent = run.output;
|
|
4862
|
-
out.appendChild(copyBtn(() => run.output, 'copy'));
|
|
4863
|
-
card.appendChild(out);
|
|
4864
|
-
}
|
|
4865
5265
|
}
|
|
4866
|
-
|
|
5266
|
+
col.appendChild(card);
|
|
4867
5267
|
} else {
|
|
4868
5268
|
const bubble = document.createElement('div');
|
|
4869
5269
|
bubble.className = 'bubble';
|
|
@@ -4893,7 +5293,7 @@ const APP_HTML = `<!doctype html>
|
|
|
4893
5293
|
maybeOpenPayForEmptyWallet(text);
|
|
4894
5294
|
}
|
|
4895
5295
|
bubble.appendChild(textEl);
|
|
4896
|
-
|
|
5296
|
+
col.appendChild(bubble);
|
|
4897
5297
|
// Copy the message SOURCE, not rendered HTML — markdown, code fences and
|
|
4898
5298
|
// directive lines are what people want back; innerText drops fences and
|
|
4899
5299
|
// mangles indentation.
|
|
@@ -4909,6 +5309,7 @@ const APP_HTML = `<!doctype html>
|
|
|
4909
5309
|
}, 'copy'));
|
|
4910
5310
|
}
|
|
4911
5311
|
}
|
|
5312
|
+
row.appendChild(col);
|
|
4912
5313
|
log.appendChild(row);
|
|
4913
5314
|
}
|
|
4914
5315
|
|
|
@@ -4921,10 +5322,10 @@ const APP_HTML = `<!doctype html>
|
|
|
4921
5322
|
const full = await loadActiveMessages();
|
|
4922
5323
|
if (!full || full.id !== activeId) return;
|
|
4923
5324
|
const renderKey = String(workspacePort) + '|' + full.id + '|' + full.status + '|' + (full.history || []).map(function (h) {
|
|
4924
|
-
return [h.who, h.text, h.runStatus, h.runOutput, (h.images || []).join(',')].join('|#');
|
|
5325
|
+
return [h.who, h.text, h.thinking, h.runStatus, h.runOutput, (h.images || []).join(',')].join('|#');
|
|
4925
5326
|
}).join('||');
|
|
4926
5327
|
if (renderKey === lastRenderKey) {
|
|
4927
|
-
if (streamBuf) paintStream();
|
|
5328
|
+
if (streamBuf || streamThink) paintStream();
|
|
4928
5329
|
return;
|
|
4929
5330
|
}
|
|
4930
5331
|
lastRenderKey = renderKey;
|
|
@@ -4935,8 +5336,14 @@ const APP_HTML = `<!doctype html>
|
|
|
4935
5336
|
log.innerHTML = '';
|
|
4936
5337
|
lastSpeaker = null;
|
|
4937
5338
|
for (const h of full.history) {
|
|
4938
|
-
|
|
4939
|
-
|
|
5339
|
+
let run;
|
|
5340
|
+
if (h.runId || h.runStatus) {
|
|
5341
|
+
run = { id: h.runId, status: h.runStatus, output: h.runOutput, command: h.text };
|
|
5342
|
+
} else if (h.who === 'bot') {
|
|
5343
|
+
const legacy = parseLegacyRun(h.text);
|
|
5344
|
+
if (legacy) run = { status: legacy.status, output: legacy.output, command: legacy.command };
|
|
5345
|
+
}
|
|
5346
|
+
addRow(h.who, h.text, h.color || t.color, h.name || t.name, run, h.images, h.thinking);
|
|
4940
5347
|
}
|
|
4941
5348
|
if (full.status === 'thinking') {
|
|
4942
5349
|
if (full.liveStatus) streamStatus = full.liveStatus;
|
|
@@ -4946,19 +5353,21 @@ const APP_HTML = `<!doctype html>
|
|
|
4946
5353
|
} else if (streamRaceId !== full.id) {
|
|
4947
5354
|
streamRace = null;
|
|
4948
5355
|
}
|
|
4949
|
-
addRow('bot', streamBuf || '…', t.color, t.name);
|
|
5356
|
+
addRow('bot', streamBuf || '…', t.color, t.name, undefined, undefined, streamThink, true);
|
|
4950
5357
|
// Tag the live bubble so deltas can repaint just this node instead of
|
|
4951
5358
|
// re-rendering (and re-fetching) the whole thread on every token.
|
|
4952
5359
|
const b = log.querySelector('.row:last-child .bubble');
|
|
4953
5360
|
if (b) { b.id = 'streamBubble'; paintStream(); }
|
|
4954
5361
|
}
|
|
4955
5362
|
if (wasNearBottom) log.scrollTop = log.scrollHeight;
|
|
5363
|
+
if (findBarOpen()) applyFind(true);
|
|
4956
5364
|
}
|
|
4957
5365
|
|
|
4958
5366
|
// --- live token stream ---------------------------------------------------
|
|
4959
5367
|
// The server has always been able to stream; /drive just never asked for it,
|
|
4960
5368
|
// so a turn showed "…" for its whole duration and then arrived in one lump.
|
|
4961
5369
|
let streamBuf = '';
|
|
5370
|
+
let streamThink = '';
|
|
4962
5371
|
let streamStatus = '';
|
|
4963
5372
|
let streamRace = null;
|
|
4964
5373
|
let streamRaceId = '';
|
|
@@ -5012,44 +5421,76 @@ const APP_HTML = `<!doctype html>
|
|
|
5012
5421
|
return '<div class="racewrap"><div class="racecaption">' + escapeHtml(caption) + '</div>'
|
|
5013
5422
|
+ '<div class="racegrid n' + n + '">' + cells + '</div>' + judge + '</div>';
|
|
5014
5423
|
}
|
|
5424
|
+
function streamParts() {
|
|
5425
|
+
const parts = splitThinkTags(streamBuf);
|
|
5426
|
+
var think = streamThink;
|
|
5427
|
+
if (parts.thinking) think = think ? (think + '\\n' + parts.thinking) : parts.thinking;
|
|
5428
|
+
return { visible: parts.visible, think: think };
|
|
5429
|
+
}
|
|
5015
5430
|
function liveBubbleHtml() {
|
|
5016
5431
|
if (raceIsLive(streamRace)) return raceGridHtml(streamRace);
|
|
5017
|
-
|
|
5432
|
+
const vis = streamParts().visible;
|
|
5433
|
+
if (vis) {
|
|
5018
5434
|
const trail = streamStatus && /^(RUN|READ|WRITE|EDIT|SPAWN|SEND|GLOB|GREP|MCP|FETCH|TODO|SERVE|PING|PEEK|MULTIEDIT|NOTEBOOK):/i.test(streamStatus)
|
|
5019
5435
|
? '<span class="ttrail">' + escapeHtml(streamStatus) + '</span>' : '';
|
|
5020
|
-
return escapeHtml(
|
|
5436
|
+
return escapeHtml(vis) + trail;
|
|
5021
5437
|
}
|
|
5022
5438
|
const dots = '<span class="dots"><span></span><span></span><span></span></span>';
|
|
5023
5439
|
const st = streamStatus ? '<span class="tstatus">' + escapeHtml(streamStatus) + '</span>' : '';
|
|
5024
5440
|
return dots + (st ? ' ' + st : '');
|
|
5025
5441
|
}
|
|
5442
|
+
function paintThinkFoldLive(think) {
|
|
5443
|
+
const fold = document.getElementById('streamThink');
|
|
5444
|
+
if (!fold) return;
|
|
5445
|
+
const chip = fold.querySelector('.thinkchip');
|
|
5446
|
+
const body = fold.querySelector('.thinkbody');
|
|
5447
|
+
const has = !!(think && String(think).trim());
|
|
5448
|
+
fold.hidden = !has;
|
|
5449
|
+
if (!has) {
|
|
5450
|
+
if (body) body.textContent = '';
|
|
5451
|
+
return;
|
|
5452
|
+
}
|
|
5453
|
+
fold.setAttribute('data-think', think);
|
|
5454
|
+
fold.classList.toggle('open', !!liveThinkOpen);
|
|
5455
|
+
if (chip) {
|
|
5456
|
+
chip.textContent = 'thinking...';
|
|
5457
|
+
chip.setAttribute('aria-expanded', liveThinkOpen ? 'true' : 'false');
|
|
5458
|
+
}
|
|
5459
|
+
if (body) body.textContent = liveThinkOpen ? think : '';
|
|
5460
|
+
}
|
|
5026
5461
|
function paintStream() {
|
|
5027
5462
|
const b = document.getElementById('streamBubble');
|
|
5028
5463
|
if (!b) { render(); return; }
|
|
5464
|
+
const parts = streamParts();
|
|
5465
|
+
paintThinkFoldLive(parts.think);
|
|
5029
5466
|
if (raceIsLive(streamRace)) {
|
|
5030
5467
|
b.classList.add('raceboard');
|
|
5031
5468
|
b.innerHTML = liveBubbleHtml();
|
|
5032
5469
|
if (log.scrollHeight - log.scrollTop - log.clientHeight < 140) log.scrollTop = log.scrollHeight;
|
|
5470
|
+
scheduleFindPaint();
|
|
5033
5471
|
return;
|
|
5034
5472
|
}
|
|
5035
5473
|
b.classList.remove('raceboard');
|
|
5036
|
-
//
|
|
5037
|
-
//
|
|
5038
|
-
if (
|
|
5039
|
-
b.textContent =
|
|
5474
|
+
// Visible tokens only. Chain-of-thought stays in the fold — and only
|
|
5475
|
+
// paints into the fold body when the user has unfurled it.
|
|
5476
|
+
if (parts.visible && !(streamStatus && /^(RUN|READ|WRITE|EDIT|SPAWN|SEND|GLOB|GREP|MCP|FETCH|TODO|SERVE|PING|PEEK|MULTIEDIT|NOTEBOOK):/i.test(streamStatus))) {
|
|
5477
|
+
b.textContent = parts.visible;
|
|
5040
5478
|
} else {
|
|
5041
5479
|
b.innerHTML = liveBubbleHtml();
|
|
5042
5480
|
}
|
|
5043
5481
|
if (log.scrollHeight - log.scrollTop - log.clientHeight < 140) log.scrollTop = log.scrollHeight;
|
|
5482
|
+
scheduleFindPaint();
|
|
5044
5483
|
}
|
|
5045
5484
|
function connectStream(id) {
|
|
5046
5485
|
if (!id || esId === id) return;
|
|
5047
5486
|
if (es) es.close();
|
|
5048
5487
|
esId = id;
|
|
5049
5488
|
streamBuf = '';
|
|
5489
|
+
streamThink = '';
|
|
5050
5490
|
streamStatus = '';
|
|
5051
5491
|
streamRace = null;
|
|
5052
5492
|
streamRaceId = id;
|
|
5493
|
+
liveThinkOpen = false;
|
|
5053
5494
|
raceHandoff += 1;
|
|
5054
5495
|
es = new EventSource('/stream/' + id); // EventSource reconnects on its own
|
|
5055
5496
|
es.onmessage = (e) => {
|
|
@@ -5057,9 +5498,11 @@ const APP_HTML = `<!doctype html>
|
|
|
5057
5498
|
try { ev = JSON.parse(e.data); } catch { return; }
|
|
5058
5499
|
if (ev.type === 'start') {
|
|
5059
5500
|
streamBuf = '';
|
|
5501
|
+
streamThink = '';
|
|
5060
5502
|
streamStatus = ev.detail || 'waiting on model…';
|
|
5061
5503
|
streamRace = null;
|
|
5062
5504
|
streamRaceId = id;
|
|
5505
|
+
liveThinkOpen = false;
|
|
5063
5506
|
paintStream();
|
|
5064
5507
|
}
|
|
5065
5508
|
else if (ev.type === 'status') { streamStatus = ev.detail || streamStatus; paintStream(); }
|
|
@@ -5070,8 +5513,13 @@ const APP_HTML = `<!doctype html>
|
|
|
5070
5513
|
}
|
|
5071
5514
|
paintStream();
|
|
5072
5515
|
}
|
|
5516
|
+
else if (ev.type === 'think') {
|
|
5517
|
+
streamThink += ev.delta || '';
|
|
5518
|
+
paintStream();
|
|
5519
|
+
}
|
|
5073
5520
|
else if (ev.type === 'delta') {
|
|
5074
5521
|
streamBuf = ev.replace ? (ev.delta || '') : streamBuf + (ev.delta || '');
|
|
5522
|
+
if (ev.replace) streamThink = '';
|
|
5075
5523
|
paintStream();
|
|
5076
5524
|
}
|
|
5077
5525
|
else if (ev.type === 'final' || ev.type === 'run-pending') {
|
|
@@ -5081,6 +5529,7 @@ const APP_HTML = `<!doctype html>
|
|
|
5081
5529
|
setTimeout(function () {
|
|
5082
5530
|
if (token !== raceHandoff) return;
|
|
5083
5531
|
streamBuf = '';
|
|
5532
|
+
streamThink = '';
|
|
5084
5533
|
streamStatus = '';
|
|
5085
5534
|
streamRace = null;
|
|
5086
5535
|
render();
|
|
@@ -5088,6 +5537,7 @@ const APP_HTML = `<!doctype html>
|
|
|
5088
5537
|
return;
|
|
5089
5538
|
}
|
|
5090
5539
|
streamBuf = '';
|
|
5540
|
+
streamThink = '';
|
|
5091
5541
|
streamStatus = '';
|
|
5092
5542
|
streamRace = null;
|
|
5093
5543
|
render();
|
|
@@ -5148,7 +5598,11 @@ const APP_HTML = `<!doctype html>
|
|
|
5148
5598
|
|
|
5149
5599
|
async function submit() {
|
|
5150
5600
|
const task = inp.value.trim();
|
|
5151
|
-
|
|
5601
|
+
// String compare — never a regex. A sitrep word-boundary regex inside
|
|
5602
|
+
// this template literal is eaten by the backtick parser and the whole
|
|
5603
|
+
// client script dies (empty sidebar, send is a no-op).
|
|
5604
|
+
const s = String(task).trim().toLowerCase();
|
|
5605
|
+
if (s === '/sitrep' || s.startsWith('/sitrep ')) {
|
|
5152
5606
|
inp.value = '';
|
|
5153
5607
|
send.classList.remove('show');
|
|
5154
5608
|
openSitrep();
|
|
@@ -5366,26 +5820,196 @@ const APP_HTML = `<!doctype html>
|
|
|
5366
5820
|
// It cannot change the version baked into the IMAGE — only the site's spawn
|
|
5367
5821
|
// path can — so it says restart, not update. Promising an upgrade it cannot
|
|
5368
5822
|
// deliver is how a UI teaches people to distrust it.
|
|
5369
|
-
// Cmd/Ctrl+K ->
|
|
5370
|
-
//
|
|
5371
|
-
//
|
|
5372
|
-
//
|
|
5823
|
+
// Cmd/Ctrl+K -> sidebar thread search (GET /search). Cmd/Ctrl+F is find
|
|
5824
|
+
// inside the current #log — a different box, on purpose. Bound on document
|
|
5825
|
+
// keydown so it works no matter which pane has focus; preventDefault so
|
|
5826
|
+
// Chromium cannot swallow F for a page-find that was never wired.
|
|
5373
5827
|
document.addEventListener('keydown', (e) => {
|
|
5374
5828
|
const k = (e.key || '').toLowerCase();
|
|
5375
|
-
|
|
5829
|
+
const withMod = e.metaKey || e.ctrlKey;
|
|
5830
|
+
if (withMod && k === 'k') {
|
|
5376
5831
|
e.preventDefault();
|
|
5377
5832
|
const el = document.getElementById('search');
|
|
5378
5833
|
if (el) { el.focus(); el.select(); }
|
|
5379
5834
|
return;
|
|
5380
5835
|
}
|
|
5836
|
+
if (withMod && k === 'f') {
|
|
5837
|
+
e.preventDefault();
|
|
5838
|
+
openFindBar();
|
|
5839
|
+
return;
|
|
5840
|
+
}
|
|
5841
|
+
if (withMod && k === 'g' && findBarOpen()) {
|
|
5842
|
+
e.preventDefault();
|
|
5843
|
+
findStep(e.shiftKey ? -1 : 1);
|
|
5844
|
+
return;
|
|
5845
|
+
}
|
|
5846
|
+
if (k === 'escape' && findBarOpen()) {
|
|
5847
|
+
e.preventDefault();
|
|
5848
|
+
closeFindBar();
|
|
5849
|
+
return;
|
|
5850
|
+
}
|
|
5381
5851
|
// Cmd/Ctrl+Enter sends from anywhere — useful when focus drifted into a
|
|
5382
5852
|
// RUN card or a copy button mid-thought.
|
|
5383
|
-
if (
|
|
5853
|
+
if (withMod && k === 'enter') {
|
|
5384
5854
|
e.preventDefault();
|
|
5385
5855
|
try { submit(); } catch (err) { /* not ready */ }
|
|
5386
5856
|
}
|
|
5387
5857
|
});
|
|
5388
5858
|
|
|
5859
|
+
// FIND IN THREAD. Highlights visible .bubble text only — not sidebar
|
|
5860
|
+
// threads, not think folds, not RUN cards, not tool JSON dumps.
|
|
5861
|
+
let findMarks = [];
|
|
5862
|
+
let findIndex = 0;
|
|
5863
|
+
let findPaintTimer = null;
|
|
5864
|
+
const findBar = document.getElementById('findBar');
|
|
5865
|
+
const findInp = document.getElementById('findInp');
|
|
5866
|
+
const findCount = document.getElementById('findCount');
|
|
5867
|
+
function scheduleFindPaint() {
|
|
5868
|
+
if (!findBarOpen()) return;
|
|
5869
|
+
clearTimeout(findPaintTimer);
|
|
5870
|
+
findPaintTimer = setTimeout(function () { applyFind(true); }, 160);
|
|
5871
|
+
}
|
|
5872
|
+
function findBarOpen() {
|
|
5873
|
+
const el = document.getElementById('findBar');
|
|
5874
|
+
return !!(el && el.classList.contains('show'));
|
|
5875
|
+
}
|
|
5876
|
+
function placeFindBar() {
|
|
5877
|
+
const bar = document.getElementById('findBar');
|
|
5878
|
+
const main = document.getElementById('main');
|
|
5879
|
+
if (!bar || !chatHeader || !main) return;
|
|
5880
|
+
const top = chatHeader.getBoundingClientRect().bottom - main.getBoundingClientRect().top + 8;
|
|
5881
|
+
bar.style.top = Math.max(8, Math.round(top)) + 'px';
|
|
5882
|
+
}
|
|
5883
|
+
function clearFindMarks() {
|
|
5884
|
+
const logEl = document.getElementById('log');
|
|
5885
|
+
if (!logEl) { findMarks = []; return; }
|
|
5886
|
+
const marks = logEl.querySelectorAll('mark.findhit');
|
|
5887
|
+
for (let i = 0; i < marks.length; i++) {
|
|
5888
|
+
const mark = marks[i];
|
|
5889
|
+
const parent = mark.parentNode;
|
|
5890
|
+
if (!parent) continue;
|
|
5891
|
+
parent.replaceChild(document.createTextNode(mark.textContent), mark);
|
|
5892
|
+
parent.normalize();
|
|
5893
|
+
}
|
|
5894
|
+
findMarks = [];
|
|
5895
|
+
}
|
|
5896
|
+
function paintFindCount() {
|
|
5897
|
+
if (!findCount) return;
|
|
5898
|
+
const n = findMarks.length;
|
|
5899
|
+
findCount.textContent = n ? ((findIndex + 1) + ' / ' + n) : (findInp && findInp.value.trim() ? '0 / 0' : '');
|
|
5900
|
+
}
|
|
5901
|
+
function skipFindNode(node) {
|
|
5902
|
+
const p = node && node.parentNode;
|
|
5903
|
+
if (!p || !p.closest) return true;
|
|
5904
|
+
return !!p.closest('.copybtn, .html-preview-wrap, button, script, style');
|
|
5905
|
+
}
|
|
5906
|
+
function highlightTextNode(node, query) {
|
|
5907
|
+
const text = node.nodeValue || '';
|
|
5908
|
+
const hay = text.toLowerCase();
|
|
5909
|
+
const needle = query.toLowerCase();
|
|
5910
|
+
if (!needle) return;
|
|
5911
|
+
let from = 0;
|
|
5912
|
+
const parts = [];
|
|
5913
|
+
let idx = hay.indexOf(needle, from);
|
|
5914
|
+
while (idx !== -1) {
|
|
5915
|
+
if (idx > from) parts.push({ t: text.slice(from, idx), hit: false });
|
|
5916
|
+
parts.push({ t: text.slice(idx, idx + needle.length), hit: true });
|
|
5917
|
+
from = idx + needle.length;
|
|
5918
|
+
idx = hay.indexOf(needle, from);
|
|
5919
|
+
}
|
|
5920
|
+
if (!parts.length) return;
|
|
5921
|
+
if (from < text.length) parts.push({ t: text.slice(from), hit: false });
|
|
5922
|
+
const frag = document.createDocumentFragment();
|
|
5923
|
+
for (let i = 0; i < parts.length; i++) {
|
|
5924
|
+
if (parts[i].hit) {
|
|
5925
|
+
const mark = document.createElement('mark');
|
|
5926
|
+
mark.className = 'findhit';
|
|
5927
|
+
mark.textContent = parts[i].t;
|
|
5928
|
+
findMarks.push(mark);
|
|
5929
|
+
frag.appendChild(mark);
|
|
5930
|
+
} else {
|
|
5931
|
+
frag.appendChild(document.createTextNode(parts[i].t));
|
|
5932
|
+
}
|
|
5933
|
+
}
|
|
5934
|
+
node.parentNode.replaceChild(frag, node);
|
|
5935
|
+
}
|
|
5936
|
+
function applyFind(preserve) {
|
|
5937
|
+
const q = findInp ? findInp.value.trim() : '';
|
|
5938
|
+
const keep = preserve ? findIndex : 0;
|
|
5939
|
+
clearFindMarks();
|
|
5940
|
+
if (!q) { findIndex = 0; paintFindCount(); return; }
|
|
5941
|
+
const logEl = document.getElementById('log');
|
|
5942
|
+
if (!logEl) { paintFindCount(); return; }
|
|
5943
|
+
const bubbles = logEl.querySelectorAll('.bubble');
|
|
5944
|
+
for (let b = 0; b < bubbles.length; b++) {
|
|
5945
|
+
const nodes = [];
|
|
5946
|
+
const walker = document.createTreeWalker(bubbles[b], NodeFilter.SHOW_TEXT, null);
|
|
5947
|
+
let n = walker.nextNode();
|
|
5948
|
+
while (n) {
|
|
5949
|
+
if (n.nodeValue && !skipFindNode(n)) nodes.push(n);
|
|
5950
|
+
n = walker.nextNode();
|
|
5951
|
+
}
|
|
5952
|
+
for (let i = 0; i < nodes.length; i++) highlightTextNode(nodes[i], q);
|
|
5953
|
+
}
|
|
5954
|
+
if (!findMarks.length) { findIndex = 0; paintFindCount(); return; }
|
|
5955
|
+
findIndex = keep % findMarks.length;
|
|
5956
|
+
if (findIndex < 0) findIndex = 0;
|
|
5957
|
+
focusFindHit(findIndex);
|
|
5958
|
+
}
|
|
5959
|
+
function focusFindHit(i) {
|
|
5960
|
+
for (let m = 0; m < findMarks.length; m++) findMarks[m].classList.remove('cur');
|
|
5961
|
+
const mark = findMarks[i];
|
|
5962
|
+
if (!mark) { paintFindCount(); return; }
|
|
5963
|
+
mark.classList.add('cur');
|
|
5964
|
+
if (mark.scrollIntoView) mark.scrollIntoView({ block: 'center', inline: 'nearest' });
|
|
5965
|
+
paintFindCount();
|
|
5966
|
+
}
|
|
5967
|
+
function findStep(dir) {
|
|
5968
|
+
if (!findMarks.length) return;
|
|
5969
|
+
findIndex = (findIndex + dir + findMarks.length) % findMarks.length;
|
|
5970
|
+
focusFindHit(findIndex);
|
|
5971
|
+
}
|
|
5972
|
+
function openFindBar() {
|
|
5973
|
+
if (!findBar || !findInp) return;
|
|
5974
|
+
findBar.classList.add('show');
|
|
5975
|
+
placeFindBar();
|
|
5976
|
+
findInp.focus();
|
|
5977
|
+
findInp.select();
|
|
5978
|
+
if (findInp.value.trim()) applyFind(true);
|
|
5979
|
+
else paintFindCount();
|
|
5980
|
+
}
|
|
5981
|
+
function closeFindBar() {
|
|
5982
|
+
if (findBar) findBar.classList.remove('show');
|
|
5983
|
+
clearFindMarks();
|
|
5984
|
+
findIndex = 0;
|
|
5985
|
+
if (findCount) findCount.textContent = '';
|
|
5986
|
+
if (inp) inp.focus();
|
|
5987
|
+
}
|
|
5988
|
+
if (findInp) {
|
|
5989
|
+
findInp.addEventListener('input', () => applyFind(false));
|
|
5990
|
+
findInp.addEventListener('keydown', (e) => {
|
|
5991
|
+
if (e.key === 'Enter') {
|
|
5992
|
+
e.preventDefault();
|
|
5993
|
+
findStep(e.shiftKey ? -1 : 1);
|
|
5994
|
+
}
|
|
5995
|
+
if (e.key === 'Escape') {
|
|
5996
|
+
e.preventDefault();
|
|
5997
|
+
closeFindBar();
|
|
5998
|
+
}
|
|
5999
|
+
});
|
|
6000
|
+
}
|
|
6001
|
+
if (findBar) {
|
|
6002
|
+
const prevBtn = document.getElementById('findPrev');
|
|
6003
|
+
const nextBtn = document.getElementById('findNext');
|
|
6004
|
+
const closeBtn = document.getElementById('findClose');
|
|
6005
|
+
if (prevBtn) prevBtn.addEventListener('click', () => findStep(-1));
|
|
6006
|
+
if (nextBtn) nextBtn.addEventListener('click', () => findStep(1));
|
|
6007
|
+
if (closeBtn) closeBtn.addEventListener('click', closeFindBar);
|
|
6008
|
+
}
|
|
6009
|
+
if (window.electronAPI && typeof window.electronAPI.onFindInThread === 'function') {
|
|
6010
|
+
window.electronAPI.onFindInThread(function () { openFindBar(); });
|
|
6011
|
+
}
|
|
6012
|
+
|
|
5389
6013
|
const reloadBtn = document.getElementById('reloadBtn');
|
|
5390
6014
|
if (reloadBtn) {
|
|
5391
6015
|
reloadBtn.addEventListener('click', async () => {
|
|
@@ -5413,6 +6037,32 @@ const APP_HTML = `<!doctype html>
|
|
|
5413
6037
|
const top = chatHeader.getBoundingClientRect().bottom - mainEl.getBoundingClientRect().top + 8;
|
|
5414
6038
|
hud.style.top = Math.max(8, Math.round(top)) + 'px';
|
|
5415
6039
|
}
|
|
6040
|
+
function fmtDockX(n) {
|
|
6041
|
+
if (n == null || !Number.isFinite(n)) return '—';
|
|
6042
|
+
return (n >= 100 ? String(Math.round(n)) : Number(n).toFixed(n >= 10 ? 1 : 2)) + 'x';
|
|
6043
|
+
}
|
|
6044
|
+
function paintDock(you) {
|
|
6045
|
+
const spillEl = document.getElementById('dockSpill');
|
|
6046
|
+
const sessEl = document.getElementById('dockSession');
|
|
6047
|
+
const paidEl = document.getElementById('dockPaid');
|
|
6048
|
+
const bindEl = document.getElementById('dockBind');
|
|
6049
|
+
const callsEl = document.getElementById('dockCalls');
|
|
6050
|
+
if (!spillEl || !sessEl || !paidEl || !bindEl || !callsEl) return;
|
|
6051
|
+
const spent = Number(you && you.spentUsd) || 0;
|
|
6052
|
+
const direct = Number(you && you.directUsd) || 0;
|
|
6053
|
+
const spillX = Number(you && you.spilled && you.spilled.savingX);
|
|
6054
|
+
const spillCalls = Number(you && you.spilled && you.spilled.calls) || 0;
|
|
6055
|
+
const spillOn = Number.isFinite(spillX) && spillX > 0;
|
|
6056
|
+
const bound = spillOn || spillCalls > 0;
|
|
6057
|
+
const sessionX = spent > 0 ? direct / spent : null;
|
|
6058
|
+
spillEl.textContent = spillOn ? fmtDockX(spillX) : '—';
|
|
6059
|
+
spillEl.className = spillOn ? 'dv hlime' : 'dv';
|
|
6060
|
+
sessEl.textContent = sessionX == null ? '—' : fmtDockX(sessionX);
|
|
6061
|
+
sessEl.className = 'dv';
|
|
6062
|
+
paidEl.textContent = usd(spent);
|
|
6063
|
+
bindEl.textContent = bound ? 'yes' : 'no';
|
|
6064
|
+
callsEl.textContent = String((you && you.paidCalls) || 0);
|
|
6065
|
+
}
|
|
5416
6066
|
function usd(n) {
|
|
5417
6067
|
if (n === null || n === undefined) return '—';
|
|
5418
6068
|
if (n === 0) return '$0';
|
|
@@ -5453,13 +6103,15 @@ const APP_HTML = `<!doctype html>
|
|
|
5453
6103
|
const savedEl = document.getElementById('hYouSaved');
|
|
5454
6104
|
const hintEl = document.getElementById('hHint');
|
|
5455
6105
|
if (spent > 0) {
|
|
5456
|
-
const
|
|
6106
|
+
const sav = formatSavingLabel(you);
|
|
6107
|
+
const mult = sav.mult;
|
|
5457
6108
|
// honest either way: >=1x is a real saving vs a naked direct call,
|
|
5458
6109
|
// <1x means you're currently paying MORE than direct would cost —
|
|
5459
6110
|
// don't dress that up as green when it isn't one.
|
|
5460
6111
|
// Asking a bound corpus makes this genuinely large (the counterfactual
|
|
5461
6112
|
// is shipping the WHOLE corpus), so 2dp would read as noise up there.
|
|
5462
|
-
|
|
6113
|
+
// Label the number: "Nx spilled" when bound, else "Nx session".
|
|
6114
|
+
savedEl.textContent = sav.text;
|
|
5463
6115
|
savedEl.className = mult >= 1 ? 'hlime' : 'hember';
|
|
5464
6116
|
// Session direct/spent (never first-call). Ember when cogs > spent —
|
|
5465
6117
|
// house losing. Do not treat race_unused as a user refund.
|
|
@@ -5471,7 +6123,7 @@ const APP_HTML = `<!doctype html>
|
|
|
5471
6123
|
hintEl.className = mult >= 1 ? 'hhint' : 'hhint show';
|
|
5472
6124
|
hintEl.innerHTML = '<b>feed it more.</b> you\\'re billed on the slice actually sent, '
|
|
5473
6125
|
+ 'not the corpus — so the more you bind, the further ahead this gets. '
|
|
5474
|
-
+ 'small inputs cost more than sending them straight.';
|
|
6126
|
+
+ 'small inputs cost more than sending them straight. HUD is spilled-call x when any call bound.';
|
|
5475
6127
|
}
|
|
5476
6128
|
} else {
|
|
5477
6129
|
savedEl.textContent = '—';
|
|
@@ -5483,25 +6135,38 @@ const APP_HTML = `<!doctype html>
|
|
|
5483
6135
|
}
|
|
5484
6136
|
}
|
|
5485
6137
|
document.getElementById('hFoot').textContent = (you.paidCalls || 0) + ' paid calls this session';
|
|
6138
|
+
paintDock(you);
|
|
5486
6139
|
} catch (e) {
|
|
5487
6140
|
document.getElementById('hFoot').textContent = 'error: ' + e.message;
|
|
5488
6141
|
}
|
|
5489
6142
|
}
|
|
5490
6143
|
let hudTimer = null;
|
|
6144
|
+
function ensureHudTick() {
|
|
6145
|
+
if (hudTimer) return;
|
|
6146
|
+
hudTimer = setInterval(refreshHud, 30000);
|
|
6147
|
+
}
|
|
5491
6148
|
hudBtn.addEventListener('click', (e) => {
|
|
5492
6149
|
e.stopPropagation();
|
|
5493
6150
|
hud.classList.toggle('show');
|
|
5494
6151
|
if (hud.classList.contains('show')) {
|
|
5495
6152
|
placeHud();
|
|
5496
6153
|
refreshHud();
|
|
5497
|
-
hudTimer = setInterval(refreshHud, 30000);
|
|
5498
|
-
} else if (hudTimer) {
|
|
5499
|
-
clearInterval(hudTimer); hudTimer = null;
|
|
5500
6154
|
}
|
|
6155
|
+
echoSlash('/hud');
|
|
6156
|
+
});
|
|
6157
|
+
refreshHud();
|
|
6158
|
+
ensureHudTick();
|
|
6159
|
+
window.addEventListener('resize', () => {
|
|
6160
|
+
if (hud.classList.contains('show')) placeHud();
|
|
6161
|
+
if (findBarOpen()) placeFindBar();
|
|
5501
6162
|
});
|
|
5502
|
-
|
|
5503
|
-
|
|
5504
|
-
|
|
6163
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
6164
|
+
if (chatHeader) {
|
|
6165
|
+
new ResizeObserver(() => {
|
|
6166
|
+
if (hud.classList.contains('show')) placeHud();
|
|
6167
|
+
if (findBarOpen()) placeFindBar();
|
|
6168
|
+
}).observe(chatHeader);
|
|
6169
|
+
}
|
|
5505
6170
|
}
|
|
5506
6171
|
document.addEventListener('click', (e) => { if (!hud.contains(e.target)) hud.classList.remove('show'); });
|
|
5507
6172
|
</script>
|
|
@@ -5682,6 +6347,7 @@ const server = http.createServer((req, res) => {
|
|
|
5682
6347
|
let you = { spentUsd: 0, cogsUsd: 0, directUsd: 0, paidCalls: 0, creditUsd: null, chainUsd: null };
|
|
5683
6348
|
try { you = { ...you, ...(await (await fetch('http://127.0.0.1:8402/v1/session', { signal: AbortSignal.timeout(2000) })).json()) }; }
|
|
5684
6349
|
catch { /* local proxy not running — HUD shows zeros rather than guessing */ }
|
|
6350
|
+
await attachSpilled(you);
|
|
5685
6351
|
try {
|
|
5686
6352
|
you.creditUsd = await creditBalance();
|
|
5687
6353
|
} catch { /* credit is advisory */ }
|
|
@@ -5856,20 +6522,24 @@ const server = http.createServer((req, res) => {
|
|
|
5856
6522
|
threadId = j.threadId; task = (j.task || '').toString();
|
|
5857
6523
|
images = Array.isArray(j.images) ? j.images.filter((u) => typeof u === 'string') : [];
|
|
5858
6524
|
} catch { /* ignore */ }
|
|
5859
|
-
res.writeHead(200, { 'content-type': 'application/json' });
|
|
5860
|
-
res.end(JSON.stringify({ ok: true }));
|
|
5861
6525
|
const t = threads.get(threadId);
|
|
6526
|
+
const driveOk = () => {
|
|
6527
|
+
if (res.headersSent) return;
|
|
6528
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
6529
|
+
res.end(JSON.stringify({ ok: true }));
|
|
6530
|
+
};
|
|
5862
6531
|
// Every OTHER slash command. Local, free, instant — no model call, so
|
|
5863
6532
|
// checking your spend or clearing a thread never costs anything.
|
|
5864
6533
|
// /dir and /mode keep their own handlers below, untouched.
|
|
5865
6534
|
if (t && /^\//.test(task.trim())) {
|
|
5866
6535
|
// Drawer-only. Never dump sitrep into the transcript.
|
|
5867
|
-
if (/^\/sitrep\b/i.test(task.trim())) return;
|
|
6536
|
+
if (/^\/sitrep\b/i.test(task.trim())) { driveOk(); return; }
|
|
5868
6537
|
const handled = await handleSlash(task.trim(), t).catch((e) => `error: ${e.message}`);
|
|
5869
6538
|
if (handled !== null && handled !== undefined) {
|
|
5870
6539
|
t.history.push({ who: 'bot', text: handled });
|
|
5871
6540
|
t.lastActivityAt = Date.now();
|
|
5872
6541
|
saveThreads();
|
|
6542
|
+
driveOk();
|
|
5873
6543
|
return;
|
|
5874
6544
|
}
|
|
5875
6545
|
}
|
|
@@ -5888,19 +6558,22 @@ const server = http.createServer((req, res) => {
|
|
|
5888
6558
|
t.history.push({ who: 'bot', text: `"${full}" isn't a directory that exists.` });
|
|
5889
6559
|
}
|
|
5890
6560
|
saveThreads();
|
|
6561
|
+
driveOk();
|
|
5891
6562
|
return;
|
|
5892
6563
|
}
|
|
5893
|
-
// "/mode auto|ask"
|
|
5894
|
-
//
|
|
6564
|
+
// "/mode auto|ask" — Auto is the Claude Code harness (openzoo claude
|
|
6565
|
+
// env). Ask stays a chat completion; RUN: waits for approve/deny.
|
|
5895
6566
|
const modeCmd = /^\/mode\s+(auto|ask)\b/.exec(task.trim());
|
|
5896
6567
|
if (modeCmd && t) {
|
|
5897
6568
|
t.runMode = modeCmd[1];
|
|
5898
|
-
t.history.push({ who: 'bot', text: `Run mode set to ${modeCmd[1]}${modeCmd[1] === 'auto' ? ' —
|
|
6569
|
+
t.history.push({ who: 'bot', text: `Run mode set to ${modeCmd[1]}${modeCmd[1] === 'auto' ? ' — Claude Code via OpenZoo (x402). Native tools, not RUN: text.' : ' — chat completion; RUN: waits for your approval.'}` });
|
|
5899
6570
|
saveThreads();
|
|
6571
|
+
driveOk();
|
|
5900
6572
|
return;
|
|
5901
6573
|
}
|
|
5902
6574
|
// Stream to whoever is watching this thread. emitToThread is a no-op
|
|
5903
6575
|
// when nobody is, so a spawned subagent nobody has open costs nothing.
|
|
6576
|
+
driveOk();
|
|
5904
6577
|
runTurn(threadId, task, (ev) => emitToThread(threadId, ev), images).catch(() => {});
|
|
5905
6578
|
});
|
|
5906
6579
|
return;
|
|
@@ -5913,12 +6586,14 @@ server.listen(PORT, BIND, () => console.log(`[grokui] http://${BIND === '0.0.0.0
|
|
|
5913
6586
|
|
|
5914
6587
|
export {
|
|
5915
6588
|
tryDirective, ensureWorkspacePort, isPreviewableRel, previewAck, workspaceFileUrl,
|
|
5916
|
-
parseRun, looksLikeMcpAsBash, stripThinkTags, safeResolveIn, inDir, listDir,
|
|
5917
|
-
handleSlash, newThread, setRunTurnForTest, setBrainAskForTest, runTurn,
|
|
6589
|
+
parseRun, looksLikeMcpAsBash, stripThinkTags, takeThink, safeResolveIn, inDir, listDir,
|
|
6590
|
+
handleSlash, newThread, setRunTurnForTest, setBrainAskForTest, setClaudeRunnerForTest, runTurn,
|
|
6591
|
+
runAutoClaudeTurn, autoClaudePrompt,
|
|
5918
6592
|
AUTO_CONTINUE, AUTO_RACE_RETRY, AUTO_EMPTY_RETRY, pingWakeText, pingCanWake, shouldKeepAuto,
|
|
5919
6593
|
isDoneReply, isTransientModelFail, isPaymentFailed, isEmptyWalletPayment, isEmptyToolResult, enqueueAutoHop, childKickoff, findByName,
|
|
5920
6594
|
attachChildDir, finishChildDir,
|
|
5921
6595
|
lockWorktree, unlockWorktree, parsePrRef, fetchSpecsForOrigin, agentSlug,
|
|
5922
6596
|
filesForCorpus, noteFileForCorpus, noteRunForCorpus, resetFilesForCorpus,
|
|
5923
6597
|
filesForCorpusKeys, scheduleFilesForCorpus, inFlightChars, BIND_MIN_CHARS, KEEP_MAX,
|
|
6598
|
+
formatSavingLabel, holobrainOf, looksLikeProxyShell, CHAT_NOT_PROXY, PROXY_SHELL_REFUSE,
|
|
5924
6599
|
};
|