openzoo 0.49.9 → 0.49.11
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 +18 -4
- package/bin/openzoo.js +2 -3
- package/lib/claudecode.js +107 -723
- package/lib/grokui.mjs +125 -548
- package/lib/launch.js +110 -150
- package/lib/models.js +57 -19
- package/lib/proxy.js +79 -5
- package/lib/racesettle.js +23 -2
- package/lib/responses-stream.js +176 -0
- package/package.json +2 -2
- package/lib/relay.js +0 -275
package/lib/grokui.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { exec } from 'node:child_process';
|
|
9
9
|
import http from 'node:http';
|
|
10
10
|
import { randomUUID } from 'node:crypto';
|
|
11
|
-
import {
|
|
11
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
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';
|
|
@@ -36,7 +36,6 @@ import { BIND_MIN_CHARS } from './hrr.js';
|
|
|
36
36
|
import { stripThinkTags, takeThink } from './think.js';
|
|
37
37
|
import {
|
|
38
38
|
runClaudeCode, setClaudeRunnerForTest, toolStatusLine, CLAUDE_MISSING,
|
|
39
|
-
sanitizeClaudeCanvas, closeClaudeSession, GROKUI_RESERVED_SLASH, claudeModelArg,
|
|
40
39
|
} from './claudecode.js';
|
|
41
40
|
|
|
42
41
|
const PORT = Number(process.env.OZ_GROKUI_PORT || 4173);
|
|
@@ -432,16 +431,8 @@ const turnAborts = new WeakMap();
|
|
|
432
431
|
function saveThreads() {
|
|
433
432
|
try {
|
|
434
433
|
mkdirSync(STORE_DIR, { recursive: true });
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
try {
|
|
438
|
-
writeSync(fd, data);
|
|
439
|
-
fsyncSync(fd);
|
|
440
|
-
} finally {
|
|
441
|
-
closeSync(fd);
|
|
442
|
-
}
|
|
443
|
-
return true;
|
|
444
|
-
} catch { return false; }
|
|
434
|
+
writeFileSync(STORE_FILE, JSON.stringify([...threads.values()]));
|
|
435
|
+
} catch { /* best effort */ }
|
|
445
436
|
}
|
|
446
437
|
|
|
447
438
|
function loadThreads() {
|
|
@@ -465,9 +456,6 @@ function loadThreads() {
|
|
|
465
456
|
else delete h.thinking;
|
|
466
457
|
}
|
|
467
458
|
}
|
|
468
|
-
// AUTO continue / nudge / command-output hops must never come back
|
|
469
|
-
// as user bubbles after a remount. They are harness-only.
|
|
470
|
-
t.history = t.history.filter(isVisibleHistoryEntry);
|
|
471
459
|
}
|
|
472
460
|
if (Array.isArray(t.messages)) {
|
|
473
461
|
for (const m of t.messages) {
|
|
@@ -683,7 +671,6 @@ function buildMemberMessages(t, member) {
|
|
|
683
671
|
{ role: 'system', content: CHAT_NOT_PROXY },
|
|
684
672
|
];
|
|
685
673
|
for (const h of t.history) {
|
|
686
|
-
if (h.who === 'user' && isHarnessUserText(h.text)) continue;
|
|
687
674
|
if (h.who === 'user') msgs.push({ role: 'user', content: contentFor(h.text, h.images) });
|
|
688
675
|
else if (h.name === member.name) msgs.push({ role: 'assistant', content: h.text });
|
|
689
676
|
else msgs.push({ role: 'user', content: `[${h.name}]: ${h.text}` });
|
|
@@ -746,34 +733,6 @@ function isTransientModelFail(text) {
|
|
|
746
733
|
if (/returned nothing \d+ times|each returned nothing/i.test(s)) return true;
|
|
747
734
|
return false;
|
|
748
735
|
}
|
|
749
|
-
// Quiet Claude TUI / --model miss / chrome-only canvas. 1.5.99 PTY Auto
|
|
750
|
-
// treated that as a finished turn, painted "(no response)" / "upstream HTTP N",
|
|
751
|
-
// and went idle. Fall through to Ask/Auto chat-completions instead.
|
|
752
|
-
function isClaudeFallbackReply(text) {
|
|
753
|
-
const s = String(text || '').trim();
|
|
754
|
-
if (!s) return true;
|
|
755
|
-
if (s === '(no response)') return true;
|
|
756
|
-
if (/^(?:error:\s*)?upstream HTTP \d+\s*$/i.test(s)) return true;
|
|
757
|
-
return false;
|
|
758
|
-
}
|
|
759
|
-
function isVisibleBotReply(h) {
|
|
760
|
-
if (!h || h.who !== 'bot') return false;
|
|
761
|
-
if (!isVisibleHistoryEntry(h)) return false;
|
|
762
|
-
const text = String(h.text || '').trim();
|
|
763
|
-
if (!text) return false;
|
|
764
|
-
if (isClaudeFallbackReply(text)) return false;
|
|
765
|
-
return true;
|
|
766
|
-
}
|
|
767
|
-
function threadHasVisibleBotReply(t) {
|
|
768
|
-
return (t?.history || []).some(isVisibleBotReply);
|
|
769
|
-
}
|
|
770
|
-
function popClaudeFallbackBot(t) {
|
|
771
|
-
if (!t || !Array.isArray(t.history) || !t.history.length) return false;
|
|
772
|
-
const last = t.history[t.history.length - 1];
|
|
773
|
-
if (last.who !== 'bot' || !isClaudeFallbackReply(last.text)) return false;
|
|
774
|
-
t.history.pop();
|
|
775
|
-
return true;
|
|
776
|
-
}
|
|
777
736
|
function isEmptyWalletPayment(text) {
|
|
778
737
|
// Empty/underfunded only — not a generic HTTP 402 handshake.
|
|
779
738
|
return /\b(?:wallet is empty|empty wallet|wallet underfunded|underfunded)\b/i.test(String(text || ''));
|
|
@@ -1159,19 +1118,6 @@ const SLASH_COMMANDS = [
|
|
|
1159
1118
|
{ name: '/mode', args: 'auto|ask', help: 'Auto = Claude Code via OpenZoo; ask = chat + approve RUN' },
|
|
1160
1119
|
];
|
|
1161
1120
|
|
|
1162
|
-
/** Claude TUI slashes that grokui must not intercept in Auto. /model is
|
|
1163
|
-
* Claude's picker here — grokui's /model pin stays on ask mode. */
|
|
1164
|
-
const CLAUDE_SLASH_IN_AUTO = new Set(['agents', 'tasks', 'context', 'model']);
|
|
1165
|
-
|
|
1166
|
-
function isGrokuiOwnedSlash(line, runMode) {
|
|
1167
|
-
const cmd = /^\/(\w+)/.exec(String(line || '').trim())?.[1]?.toLowerCase();
|
|
1168
|
-
if (!cmd) return false;
|
|
1169
|
-
if (GROKUI_RESERVED_SLASH.includes(cmd)) return true;
|
|
1170
|
-
if (runMode === 'auto' && CLAUDE_SLASH_IN_AUTO.has(cmd)) return false;
|
|
1171
|
-
return SLASH_COMMANDS.some((c) => c.name.slice(1).toLowerCase() === cmd)
|
|
1172
|
-
|| cmd === 'pay' || cmd === 'hud';
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
1121
|
const usd = (n) => (n >= 0.01 || n === 0 ? '$' + n.toFixed(2) : '$' + n.toFixed(5));
|
|
1176
1122
|
|
|
1177
1123
|
/**
|
|
@@ -1432,6 +1378,18 @@ async function handleSlash(task, t) {
|
|
|
1432
1378
|
const cmd = m[1].toLowerCase();
|
|
1433
1379
|
const arg = m[2].trim();
|
|
1434
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
|
+
}
|
|
1435
1393
|
if (cmd === 'sitrep') return null; // drawer-only — never a transcript line
|
|
1436
1394
|
if (cmd === 'help') {
|
|
1437
1395
|
return 'Commands:\n'
|
|
@@ -2023,69 +1981,6 @@ function isHarnessUserText(text) {
|
|
|
2023
1981
|
|| String(text || '').startsWith('AUTO_EMPTY_RETRY:');
|
|
2024
1982
|
}
|
|
2025
1983
|
|
|
2026
|
-
function isVisibleHistoryEntry(h) {
|
|
2027
|
-
if (!h) return false;
|
|
2028
|
-
if (h.who === 'user' && (h.harness || isHarnessUserText(h.text))) return false;
|
|
2029
|
-
return true;
|
|
2030
|
-
}
|
|
2031
|
-
|
|
2032
|
-
function visibleHistory(history) {
|
|
2033
|
-
return (history || []).filter(isVisibleHistoryEntry);
|
|
2034
|
-
}
|
|
2035
|
-
|
|
2036
|
-
function sameUserTurn(entry, userText, images) {
|
|
2037
|
-
if (!entry || entry.who !== 'user') return false;
|
|
2038
|
-
if (String(entry.text || '') !== String(userText || '')) return false;
|
|
2039
|
-
const a = entry.images || [];
|
|
2040
|
-
const b = images || [];
|
|
2041
|
-
if (a.length !== b.length) return false;
|
|
2042
|
-
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
2043
|
-
return true;
|
|
2044
|
-
}
|
|
2045
|
-
|
|
2046
|
-
// Flush the user's turn to RAM + grokui-threads.json BEFORE any model call
|
|
2047
|
-
// or UI refresh. A 500 / sidecar bounce / sitrep redraw used to wipe the
|
|
2048
|
-
// bubble because it lived only in the live stream. Do not roll this back
|
|
2049
|
-
// if the upstream request later fails. Harness continues (AUTO_CONTINUE,
|
|
2050
|
-
// AUTO_RACE_RETRY, NUDGE, command-output hops) are not user bubbles.
|
|
2051
|
-
function persistUserTurn(t, userText, images) {
|
|
2052
|
-
if (!t) return { ok: false };
|
|
2053
|
-
if (isHarnessUserText(userText)) return { ok: true, skipped: true };
|
|
2054
|
-
if (!Array.isArray(t.history)) t.history = [];
|
|
2055
|
-
const last = t.history[t.history.length - 1];
|
|
2056
|
-
if (sameUserTurn(last, userText, images) && isVisibleHistoryEntry(last)) {
|
|
2057
|
-
t.lastActivityAt = Date.now();
|
|
2058
|
-
saveThreads();
|
|
2059
|
-
return { ok: true, already: true, entry: last };
|
|
2060
|
-
}
|
|
2061
|
-
const entry = images && images.length
|
|
2062
|
-
? { who: 'user', text: userText, images: images.slice() }
|
|
2063
|
-
: { who: 'user', text: userText };
|
|
2064
|
-
t.history.push(entry);
|
|
2065
|
-
let pushedMsg = false;
|
|
2066
|
-
if (Array.isArray(t.messages)) {
|
|
2067
|
-
const lastMsg = t.messages[t.messages.length - 1];
|
|
2068
|
-
let alreadyMsg = false;
|
|
2069
|
-
if (lastMsg && lastMsg.role === 'user') {
|
|
2070
|
-
const want = contentFor(userText, images);
|
|
2071
|
-
alreadyMsg = lastMsg.content === want
|
|
2072
|
-
|| (typeof lastMsg.content === 'string' && lastMsg.content === String(userText || ''));
|
|
2073
|
-
}
|
|
2074
|
-
if (!alreadyMsg) {
|
|
2075
|
-
t.messages.push({ role: 'user', content: contentFor(userText, images) });
|
|
2076
|
-
pushedMsg = true;
|
|
2077
|
-
}
|
|
2078
|
-
}
|
|
2079
|
-
t.lastActivityAt = Date.now();
|
|
2080
|
-
const saved = saveThreads();
|
|
2081
|
-
if (!saved) {
|
|
2082
|
-
t.history.pop();
|
|
2083
|
-
if (pushedMsg && Array.isArray(t.messages)) t.messages.pop();
|
|
2084
|
-
return { ok: false };
|
|
2085
|
-
}
|
|
2086
|
-
return { ok: true, entry };
|
|
2087
|
-
}
|
|
2088
|
-
|
|
2089
1984
|
function firstUserAsk(t) {
|
|
2090
1985
|
return (t?.history || []).find((m) => m.who === 'user' && !isHarnessUserText(m.text));
|
|
2091
1986
|
}
|
|
@@ -2836,119 +2731,62 @@ function autoClaudePrompt(t, userText, images) {
|
|
|
2836
2731
|
}
|
|
2837
2732
|
|
|
2838
2733
|
/**
|
|
2839
|
-
* Orange Auto =
|
|
2840
|
-
*
|
|
2841
|
-
* PTY stdin; TUI output is stripped / folded onto the canvas.
|
|
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).
|
|
2842
2736
|
*/
|
|
2843
2737
|
async function runAutoClaudeTurn(t, userText, images, paint, stillMine, turnAbort) {
|
|
2844
2738
|
paint({ type: 'start', name: t.name, color: t.color, detail: t.liveStatus || 'Claude Code via OpenZoo…' });
|
|
2845
|
-
// Child is alive: canvas must show a folded thinking… row immediately,
|
|
2846
|
-
// before the first thinking_delta or tool_use.
|
|
2847
|
-
paint({ type: 'think', name: t.name, color: t.color, delta: '' });
|
|
2848
2739
|
let visible = '';
|
|
2849
2740
|
let thinking = '';
|
|
2850
|
-
const toolLines = [];
|
|
2851
2741
|
let streamedText = false;
|
|
2852
|
-
const noteTool = (name, input) => {
|
|
2853
|
-
const line = toolStatusLine(name, input);
|
|
2854
|
-
if (!line || toolLines[toolLines.length - 1] === line) return;
|
|
2855
|
-
toolLines.push(line);
|
|
2856
|
-
paint({ type: 'tool', name: t.name, color: t.color, detail: line });
|
|
2857
|
-
};
|
|
2858
|
-
const keepFold = () => paint({ type: 'think', name: t.name, color: t.color, delta: '' });
|
|
2859
2742
|
const result = await runClaudeCode({
|
|
2860
2743
|
prompt: autoClaudePrompt(t, userText, images),
|
|
2861
2744
|
cwd: dirFor(t.id),
|
|
2862
2745
|
sessionId: t.claudeSessionId,
|
|
2863
|
-
|
|
2864
|
-
model: claudeModelArg(t.model),
|
|
2746
|
+
model: t.model || undefined,
|
|
2865
2747
|
env: process.env,
|
|
2866
2748
|
signal: turnAbort.signal,
|
|
2867
2749
|
onEvent(folded) {
|
|
2868
2750
|
if (!stillMine() || !folded) return;
|
|
2869
2751
|
if (folded.sessionId) t.claudeSessionId = folded.sessionId;
|
|
2870
|
-
if (folded.kind === 'init'
|
|
2871
|
-
|
|
2752
|
+
if (folded.kind === 'init') {
|
|
2753
|
+
paint({ type: 'status', name: t.name, color: t.color, detail: folded.model || 'Claude Code' });
|
|
2872
2754
|
return;
|
|
2873
2755
|
}
|
|
2874
|
-
if (folded.kind === '
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
}
|
|
2878
|
-
if (folded.kind === 'think') {
|
|
2879
|
-
if (folded.text) {
|
|
2880
|
-
thinking += folded.text;
|
|
2881
|
-
paint({ type: 'think', name: t.name, color: t.color, delta: folded.text });
|
|
2882
|
-
} else {
|
|
2883
|
-
keepFold();
|
|
2884
|
-
}
|
|
2756
|
+
if (folded.kind === 'think' && folded.text) {
|
|
2757
|
+
thinking += folded.text;
|
|
2758
|
+
paint({ type: 'think', name: t.name, color: t.color, delta: folded.text });
|
|
2885
2759
|
return;
|
|
2886
2760
|
}
|
|
2887
2761
|
if (folded.kind === 'text' && folded.text) {
|
|
2888
|
-
const clean = sanitizeClaudeCanvas(folded.text);
|
|
2889
|
-
if (!clean) {
|
|
2890
|
-
keepFold();
|
|
2891
|
-
return;
|
|
2892
|
-
}
|
|
2893
2762
|
streamedText = true;
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
paint({ type: 'delta', name: t.name, color: t.color, delta: clean, replace: Boolean(folded.replace) });
|
|
2897
|
-
return;
|
|
2898
|
-
}
|
|
2899
|
-
if (folded.kind === 'tui') {
|
|
2900
|
-
if (folded.thinking && folded.thinking !== thinking) {
|
|
2901
|
-
thinking = folded.thinking;
|
|
2902
|
-
paint({ type: 'think', name: t.name, color: t.color, delta: folded.thinking, replace: true });
|
|
2903
|
-
} else {
|
|
2904
|
-
keepFold();
|
|
2905
|
-
}
|
|
2906
|
-
const tuiText = folded.text ? sanitizeClaudeCanvas(folded.text) : '';
|
|
2907
|
-
if (tuiText && tuiText !== visible) {
|
|
2908
|
-
visible = tuiText;
|
|
2909
|
-
streamedText = true;
|
|
2910
|
-
paint({ type: 'delta', name: t.name, color: t.color, delta: tuiText, replace: true });
|
|
2911
|
-
}
|
|
2912
|
-
for (const tool of folded.tools || []) noteTool(tool.name, tool.input);
|
|
2763
|
+
visible += folded.text;
|
|
2764
|
+
paint({ type: 'delta', name: t.name, color: t.color, delta: folded.text });
|
|
2913
2765
|
return;
|
|
2914
2766
|
}
|
|
2915
2767
|
if (folded.kind !== 'assistant') return;
|
|
2916
2768
|
if (folded.thinking && !thinking) {
|
|
2917
2769
|
thinking = folded.thinking;
|
|
2918
2770
|
paint({ type: 'think', name: t.name, color: t.color, delta: folded.thinking });
|
|
2919
|
-
} else {
|
|
2920
|
-
keepFold();
|
|
2921
2771
|
}
|
|
2922
2772
|
if (folded.text && !streamedText) {
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
}
|
|
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) });
|
|
2928
2778
|
}
|
|
2929
|
-
for (const tool of folded.tools || []) noteTool(tool.name, tool.input);
|
|
2930
2779
|
},
|
|
2931
2780
|
});
|
|
2932
|
-
if (!stillMine())
|
|
2933
|
-
return sanitizeClaudeCanvas(result.paymentFailed || result.text || visible || '', { error: result.error });
|
|
2934
|
-
}
|
|
2781
|
+
if (!stillMine()) return result.paymentFailed || result.text || visible || '';
|
|
2935
2782
|
if (result.sessionId) t.claudeSessionId = result.sessionId;
|
|
2936
|
-
const
|
|
2937
|
-
let finalText = result.paymentFailed
|
|
2783
|
+
const finalText = result.paymentFailed
|
|
2938
2784
|
|| result.text
|
|
2939
2785
|
|| visible
|
|
2940
|
-
|| (result.missing ? CLAUDE_MISSING : '')
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
}
|
|
2944
|
-
if (!finalText) finalText = result.error ? 'upstream HTTP 400' : '(no response)';
|
|
2945
|
-
// Empty / chrome-only / HTTP-N: do not commit a dead bot row. runTurn pops
|
|
2946
|
-
// any that slipped through and falls through to Ask/Auto chat-completions.
|
|
2947
|
-
if (isClaudeFallbackReply(finalText) && !result.paymentFailed && !result.missing) {
|
|
2948
|
-
return finalText;
|
|
2949
|
-
}
|
|
2950
|
-
t.history.push({ who: 'bot', text: finalText, thinking: thinkAll || undefined });
|
|
2951
|
-
paint({ type: 'final', name: t.name, color: t.color, text: finalText, thinking: thinkAll || undefined });
|
|
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 });
|
|
2952
2790
|
return finalText;
|
|
2953
2791
|
}
|
|
2954
2792
|
|
|
@@ -2978,15 +2816,14 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
2978
2816
|
if (!stillMine()) return;
|
|
2979
2817
|
if (ev.type === 'status' && ev.detail && t.status === 'thinking') t.liveStatus = ev.detail;
|
|
2980
2818
|
if (ev.type === 'race' && ev.race && t.status === 'thinking') t.liveRace = ev.race;
|
|
2981
|
-
if (ev.type === 'delta' || ev.type === 'think' || ev.type === 'status' || ev.type === 'start' || ev.type === 'race'
|
|
2819
|
+
if (ev.type === 'delta' || ev.type === 'think' || ev.type === 'status' || ev.type === 'start' || ev.type === 'race') t.lastDeltaAt = Date.now();
|
|
2982
2820
|
onEvent?.(ev);
|
|
2983
2821
|
};
|
|
2984
|
-
|
|
2822
|
+
t.history.push(images && images.length ? { who: 'user', text: userText, images } : { who: 'user', text: userText });
|
|
2985
2823
|
t.lastActivityAt = Date.now();
|
|
2986
2824
|
t.status = 'thinking';
|
|
2987
2825
|
t.thinkingAt = Date.now();
|
|
2988
2826
|
t.lastDeltaAt = Date.now();
|
|
2989
|
-
saveThreads();
|
|
2990
2827
|
try { turnAborts.get(t)?.abort(); } catch { /* none */ }
|
|
2991
2828
|
const turnAbort = new AbortController();
|
|
2992
2829
|
turnAborts.set(t, turnAbort);
|
|
@@ -2998,7 +2835,6 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
2998
2835
|
let chained = false;
|
|
2999
2836
|
let parked = false;
|
|
3000
2837
|
let usedClaude = false;
|
|
3001
|
-
let claudeFallback = false;
|
|
3002
2838
|
let lastReply = '';
|
|
3003
2839
|
try {
|
|
3004
2840
|
if (t.members) {
|
|
@@ -3063,37 +2899,13 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
3063
2899
|
}
|
|
3064
2900
|
return;
|
|
3065
2901
|
}
|
|
3066
|
-
// Orange Auto is
|
|
3067
|
-
//
|
|
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.
|
|
3068
2904
|
if (t.runMode === 'auto') {
|
|
3069
2905
|
usedClaude = true;
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
// text is skipped — it must not replace the just-sent bubble.
|
|
3074
|
-
persistUserTurn(t, userText, images);
|
|
3075
|
-
saveThreads();
|
|
3076
|
-
// 1.5.99 PTY Auto swallowed the first send: persist worked, waitIdle
|
|
3077
|
-
// treated a quiet TUI as done, sanitizeClaudeCanvas wiped chrome, no bot
|
|
3078
|
-
// bubble. Skip PTY until a send has produced a visible reply; serve
|
|
3079
|
-
// Ask/Auto chat-completions. Once a visible bot row exists, try PTY and
|
|
3080
|
-
// still fall through if that turn comes back empty / HTTP-N.
|
|
3081
|
-
if (threadHasVisibleBotReply(t)) {
|
|
3082
|
-
usedClaude = true;
|
|
3083
|
-
lastReply = await runAutoClaudeTurn(t, userText, images, paint, stillMine, turnAbort);
|
|
3084
|
-
if (!stillMine()) return;
|
|
3085
|
-
if (!isClaudeFallbackReply(lastReply)) {
|
|
3086
|
-
bindThread(t).catch(() => {});
|
|
3087
|
-
return;
|
|
3088
|
-
}
|
|
3089
|
-
popClaudeFallbackBot(t);
|
|
3090
|
-
usedClaude = false;
|
|
3091
|
-
claudeFallback = true;
|
|
3092
|
-
paint({ type: 'status', name: t.name, color: t.color, detail: 'retrying…' });
|
|
3093
|
-
} else {
|
|
3094
|
-
usedClaude = false;
|
|
3095
|
-
claudeFallback = true;
|
|
3096
|
-
}
|
|
2906
|
+
lastReply = await runAutoClaudeTurn(t, userText, images, paint, stillMine, turnAbort);
|
|
2907
|
+
if (stillMine()) bindThread(t).catch(() => {});
|
|
2908
|
+
return;
|
|
3097
2909
|
}
|
|
3098
2910
|
// A real message from the user resets the auto budget AND the announcement
|
|
3099
2911
|
// nudge. Harness-injected hops (command output, directive result, nudge,
|
|
@@ -3104,12 +2916,7 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
3104
2916
|
t.autoSteps = 0;
|
|
3105
2917
|
delete t.autoNudged;
|
|
3106
2918
|
}
|
|
3107
|
-
|
|
3108
|
-
// Harness hops stay out of the transcript; ask-mode still needs them in
|
|
3109
|
-
// t.messages so the model sees the continue without a user bubble.
|
|
3110
|
-
if (isHarnessUserText(userText) && Array.isArray(t.messages)) {
|
|
3111
|
-
t.messages.push({ role: 'user', content: contentFor(userText, images) });
|
|
3112
|
-
}
|
|
2919
|
+
t.messages.push({ role: 'user', content: contentFor(userText, images) });
|
|
3113
2920
|
let reply = '';
|
|
3114
2921
|
paint({ type: 'start', name: t.name, color: t.color, detail: t.liveStatus || 'waiting on model…' });
|
|
3115
2922
|
// Transient: the nudge is appended for THIS call only and never pushed into
|
|
@@ -3127,7 +2934,7 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
3127
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')}` });
|
|
3128
2935
|
}
|
|
3129
2936
|
extras.push({ role: 'system', content: CHAT_NOT_PROXY });
|
|
3130
|
-
if (t.runMode === 'auto'
|
|
2937
|
+
if (t.runMode === 'auto') extras.push({ role: 'system', content: AUTO_DIRECTIVE });
|
|
3131
2938
|
const callMsgs = extras.length ? [...t.messages, ...extras] : t.messages;
|
|
3132
2939
|
// WHICH MODEL SERVES THIS TURN.
|
|
3133
2940
|
// /model <id> pins one explicitly and always wins — an explicit choice is
|
|
@@ -3229,7 +3036,7 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
3229
3036
|
const runCmd = parseRun(reply);
|
|
3230
3037
|
if (runCmd) {
|
|
3231
3038
|
const command = runCmd;
|
|
3232
|
-
if (t.runMode === 'auto'
|
|
3039
|
+
if (t.runMode === 'auto') {
|
|
3233
3040
|
emitLiveStatus(t.id, paint, peekDirectiveStatus('', command));
|
|
3234
3041
|
const output = await execCommand(command, dirFor(t.id));
|
|
3235
3042
|
noteRunForCorpus(t.id, command, { cwd: dirFor(t.id) });
|
|
@@ -3284,7 +3091,7 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
3284
3091
|
//
|
|
3285
3092
|
// Same budget as RUN (shared t.autoSteps, reset when the user speaks), so
|
|
3286
3093
|
// this cannot spend more than a chained RUN loop already could.
|
|
3287
|
-
if (t.runMode === 'auto' &&
|
|
3094
|
+
if (t.runMode === 'auto' && ack !== null && ack !== undefined
|
|
3288
3095
|
&& (isEmptyDirectiveAck(ack) || !isDoneReply(reply))) {
|
|
3289
3096
|
chained = enqueueAutoHop(
|
|
3290
3097
|
t, threadId,
|
|
@@ -3308,7 +3115,7 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
3308
3115
|
// NUDGE announcements (stronger than a bare continue). Plain replies used
|
|
3309
3116
|
// to park here; they now fall through to AUTO_CONTINUE unless DONE: or a
|
|
3310
3117
|
// real blocking question. The step cap is the wallet bound.
|
|
3311
|
-
if (t.runMode === 'auto' &&
|
|
3118
|
+
if (t.runMode === 'auto' && (ack === null || ack === undefined)
|
|
3312
3119
|
&& (STALLED_OFFER.test(reply) || ANNOUNCEMENT.test(reply))) {
|
|
3313
3120
|
// Offers and "Spawned X — working on it" with no directive must not end
|
|
3314
3121
|
// the run. The old once-only autoNudged gate parked the thread after one
|
|
@@ -3320,7 +3127,7 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
3320
3127
|
|
|
3321
3128
|
// After any auto reply that is not DONE: and not waiting on approval,
|
|
3322
3129
|
// kick immediately. Race/empty/error uses AUTO_RACE_RETRY.
|
|
3323
|
-
if (
|
|
3130
|
+
if (shouldKeepAuto(t, reply, userText)) {
|
|
3324
3131
|
chained = enqueueAutoHop(t, threadId, autoHopText(reply, userText), onEvent);
|
|
3325
3132
|
return;
|
|
3326
3133
|
}
|
|
@@ -3330,7 +3137,7 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
3330
3137
|
// ask mode, 402/empty-wallet, or the hard cap. Empty /(no output) is not
|
|
3331
3138
|
// DONE — AUTO_EMPTY_RETRY. Otherwise kick again.
|
|
3332
3139
|
if (stillMine() && !chained && !parked) {
|
|
3333
|
-
if (!usedClaude &&
|
|
3140
|
+
if (!usedClaude && shouldKeepAuto(t, lastReply, userText)) {
|
|
3334
3141
|
enqueueAutoHop(t, threadId, autoHopText(lastReply, userText), onEvent);
|
|
3335
3142
|
} else if (!t.pendingRun) {
|
|
3336
3143
|
t.status = 'idle';
|
|
@@ -3459,8 +3266,7 @@ function subtreeOf(id, includeSelf = false) {
|
|
|
3459
3266
|
}
|
|
3460
3267
|
|
|
3461
3268
|
function threadSummary(t) {
|
|
3462
|
-
const
|
|
3463
|
-
const last = vis[vis.length - 1];
|
|
3269
|
+
const last = t.history[t.history.length - 1];
|
|
3464
3270
|
return { id: t.id, name: t.name, color: t.color, parent: t.parent, status: t.status,
|
|
3465
3271
|
preview: last ? (last.who === 'user' ? last.text : last.text).slice(0, 60) : '',
|
|
3466
3272
|
createdAt: t.createdAt, lastActivityAt: t.lastActivityAt || t.createdAt,
|
|
@@ -3837,7 +3643,7 @@ const APP_HTML = `<!doctype html>
|
|
|
3837
3643
|
.row.bot .bubble { background: #262626; color: #ececec; }
|
|
3838
3644
|
/* Folded chain-of-thought. Not the Auto run-mode chip — that is /mode auto.
|
|
3839
3645
|
Default collapsed; click the label to unfurl, click again to furl.
|
|
3840
|
-
|
|
3646
|
+
No empty chip: the fold is omitted when the model did not reason. */
|
|
3841
3647
|
.msgcol { display: flex; flex-direction: column; gap: 6px; min-width: 0; max-width: 100%; flex: 1; }
|
|
3842
3648
|
.thinkfold { align-self: flex-start; max-width: 100%; min-width: 0; }
|
|
3843
3649
|
.thinkchip {
|
|
@@ -3854,15 +3660,6 @@ const APP_HTML = `<!doctype html>
|
|
|
3854
3660
|
border-left: 2px solid #3a3a3c; max-height: 240px; overflow-y: auto;
|
|
3855
3661
|
}
|
|
3856
3662
|
.thinkfold.open .thinkbody { display: block; }
|
|
3857
|
-
/* Compact Claude TUI surface. Stripped text is the bubble; this is the
|
|
3858
|
-
optional monospace frame (folded by default, same chip pattern). */
|
|
3859
|
-
.claudeterm {
|
|
3860
|
-
display: none; margin: 4px 0 2px; padding: 8px 10px;
|
|
3861
|
-
font: 11.5px/1.4 Menlo, ui-monospace, monospace; color: #c8c8d0;
|
|
3862
|
-
background: #141416; border: 1px solid #2a2a2d; border-radius: 8px;
|
|
3863
|
-
white-space: pre-wrap; word-break: break-word; max-height: 220px; overflow: auto;
|
|
3864
|
-
}
|
|
3865
|
-
.thinkfold.open .claudeterm { display: block; }
|
|
3866
3663
|
.row.bot.pending .bubble { color: #8e8e93; }
|
|
3867
3664
|
.dots span { display: inline-block; width: 5px; height: 5px; margin-right: 3px; border-radius: 50%;
|
|
3868
3665
|
background: #8e8e93; animation: blink 1.2s infinite ease-in-out; }
|
|
@@ -4863,10 +4660,9 @@ const APP_HTML = `<!doctype html>
|
|
|
4863
4660
|
const spent = Number(you.spentUsd) || 0;
|
|
4864
4661
|
const cogs = Number(you.cogsUsd) || 0;
|
|
4865
4662
|
const direct = Number(you.directUsd) || 0;
|
|
4866
|
-
const proxyDown = you.proxyReachable === false;
|
|
4867
4663
|
const sav = formatSavingLabel(you);
|
|
4868
|
-
const saved =
|
|
4869
|
-
const savedCls =
|
|
4664
|
+
const saved = sav.text;
|
|
4665
|
+
const savedCls = sav.mult == null ? '' : (sav.mult >= 1 ? 'hlime' : 'hember');
|
|
4870
4666
|
const thinking = (full && full.status === 'thinking') || t.status === 'thinking';
|
|
4871
4667
|
const race = (full && full.liveRace) || null;
|
|
4872
4668
|
let flight = 'idle';
|
|
@@ -4883,12 +4679,11 @@ const APP_HTML = `<!doctype html>
|
|
|
4883
4679
|
+ sitrepRow('cwd', cwd)
|
|
4884
4680
|
+ sitrepRow('in flight', flight)
|
|
4885
4681
|
+ '<div class="wlanetitle" style="margin-top:16px">this session</div>'
|
|
4886
|
-
+ sitrepRow('
|
|
4887
|
-
+ sitrepRow('
|
|
4888
|
-
+ sitrepRow('
|
|
4889
|
-
+ sitrepRow('direct', proxyDown ? '—' : ('$' + (direct >= 0.01 || direct === 0 ? direct.toFixed(2) : direct.toFixed(5))))
|
|
4682
|
+
+ sitrepRow('paid', '$' + (spent >= 0.01 || spent === 0 ? spent.toFixed(2) : spent.toFixed(5)))
|
|
4683
|
+
+ sitrepRow('cogs', '$' + (cogs >= 0.01 || cogs === 0 ? cogs.toFixed(2) : cogs.toFixed(5)))
|
|
4684
|
+
+ sitrepRow('direct', '$' + (direct >= 0.01 || direct === 0 ? direct.toFixed(2) : direct.toFixed(5)))
|
|
4890
4685
|
+ sitrepRow('saved vs naked', saved, savedCls)
|
|
4891
|
-
+ sitrepRow('paid calls',
|
|
4686
|
+
+ sitrepRow('paid calls', String(you.paidCalls || 0))
|
|
4892
4687
|
+ sitrepRow('prepaid', (Number(you.creditUsd) > 0) ? 'yes' : 'no');
|
|
4893
4688
|
}
|
|
4894
4689
|
function closeSitrep() { sitrepOverlay.classList.remove('show'); }
|
|
@@ -4922,17 +4717,6 @@ const APP_HTML = `<!doctype html>
|
|
|
4922
4717
|
document.getElementById('modeAuto').addEventListener('click', () => setMode('auto'));
|
|
4923
4718
|
|
|
4924
4719
|
function escapeHtml(s) { return s.replace(/[&<>]/g, (c) => ({ '&': '&', '<': '<', '>': '>' }[c])); }
|
|
4925
|
-
function scrubBotText(s) {
|
|
4926
|
-
s = String(s == null ? '' : s);
|
|
4927
|
-
s = s.replace(/<system-reminder\\b[^>]*>[\\s\\S]*?<\\/system-reminder>/gi, '');
|
|
4928
|
-
s = s.replace(/<system-reminder\\b[^>]*>[\\s\\S]*$/i, '');
|
|
4929
|
-
s = s.replace(/^[ \\t]*(?:#\\s*)?currentDir\\b[^\\n]*\\n?/gim, '');
|
|
4930
|
-
if (/\\uFFFD/.test(s) || /^\\s*API Error:\\s*(\\d{3})\\b/im.test(s)) {
|
|
4931
|
-
var m = /API Error:\\s*(\\d{3})/i.exec(s);
|
|
4932
|
-
return 'upstream HTTP ' + (m ? m[1] : '400');
|
|
4933
|
-
}
|
|
4934
|
-
return s;
|
|
4935
|
-
}
|
|
4936
4720
|
function splitThinkTags(s) {
|
|
4937
4721
|
s = String(s == null ? '' : s);
|
|
4938
4722
|
var bits = [];
|
|
@@ -5337,48 +5121,7 @@ const APP_HTML = `<!doctype html>
|
|
|
5337
5121
|
|
|
5338
5122
|
let lastSpeaker = null;
|
|
5339
5123
|
let liveThinkOpen = false;
|
|
5340
|
-
let streamTools = [];
|
|
5341
|
-
let pendingTurns = [];
|
|
5342
|
-
let sending = false;
|
|
5343
|
-
// Mirror of server isHarnessUserText. No regex — APP_HTML template
|
|
5344
|
-
// literals eat \\b / \\/ and kill the whole script.
|
|
5345
|
-
function isHarnessUserText(text) {
|
|
5346
|
-
const s = String(text || '');
|
|
5347
|
-
if (s.indexOf('(command output)') === 0 || s.indexOf('(directive result)') === 0) return true;
|
|
5348
|
-
if (s === 'Please continue the current job. Do not stop to ask the user.') return true;
|
|
5349
|
-
if (s.indexOf('AUTO is still on ') === 0) return true;
|
|
5350
|
-
if (s.indexOf('AUTO_EMPTY_RETRY:') === 0) return true;
|
|
5351
|
-
if (s.indexOf('That reply announced work instead of doing it') === 0) return true;
|
|
5352
|
-
return false;
|
|
5353
|
-
}
|
|
5354
|
-
function pendingStoreKey(id) { return 'openzoo.userTurn.' + id; }
|
|
5355
|
-
function rememberUserTurn(id, text, images) {
|
|
5356
|
-
if (!id || !text || isHarnessUserText(text)) return;
|
|
5357
|
-
try { localStorage.setItem(pendingStoreKey(id), JSON.stringify({ text: text, images: images || [] })); } catch (e) {}
|
|
5358
|
-
}
|
|
5359
|
-
function recalledUserTurn(id) {
|
|
5360
|
-
try {
|
|
5361
|
-
const raw = localStorage.getItem(pendingStoreKey(id));
|
|
5362
|
-
if (!raw) return null;
|
|
5363
|
-
const j = JSON.parse(raw);
|
|
5364
|
-
if (!j || !j.text || isHarnessUserText(j.text)) return null;
|
|
5365
|
-
return j;
|
|
5366
|
-
} catch (e) { return null; }
|
|
5367
|
-
}
|
|
5368
|
-
function forgetUserTurn(id, text) {
|
|
5369
|
-
try {
|
|
5370
|
-
const j = recalledUserTurn(id);
|
|
5371
|
-
if (!id) return;
|
|
5372
|
-
if (!text || (j && j.text === text)) localStorage.removeItem(pendingStoreKey(id));
|
|
5373
|
-
} catch (e) {}
|
|
5374
|
-
}
|
|
5375
5124
|
function thinkLabel(live) { return live ? 'thinking...' : 'thought'; }
|
|
5376
|
-
function foldBodyText(think) {
|
|
5377
|
-
const tools = streamTools.length ? streamTools.join('\\n') : '';
|
|
5378
|
-
const t = String(think || '').trim();
|
|
5379
|
-
if (t && tools) return t + '\\n' + tools;
|
|
5380
|
-
return t || tools;
|
|
5381
|
-
}
|
|
5382
5125
|
function makeThinkFold(text, live, open) {
|
|
5383
5126
|
const fold = document.createElement('div');
|
|
5384
5127
|
fold.className = 'thinkfold' + (open ? ' open' : '');
|
|
@@ -5465,7 +5208,6 @@ const APP_HTML = `<!doctype html>
|
|
|
5465
5208
|
}
|
|
5466
5209
|
function addRow(who, text, color, name, run, images, thinking, live) {
|
|
5467
5210
|
if (who === 'bot') {
|
|
5468
|
-
text = scrubBotText(text);
|
|
5469
5211
|
const parts = splitThinkTags(text);
|
|
5470
5212
|
text = parts.visible;
|
|
5471
5213
|
if (!thinking) thinking = parts.thinking;
|
|
@@ -5484,9 +5226,9 @@ const APP_HTML = `<!doctype html>
|
|
|
5484
5226
|
col.className = 'msgcol';
|
|
5485
5227
|
const thinkText = (who === 'bot' && thinking) ? String(thinking).trim() : '';
|
|
5486
5228
|
if (live) {
|
|
5487
|
-
const fold = makeThinkFold(
|
|
5229
|
+
const fold = makeThinkFold(thinkText, true, liveThinkOpen && !!thinkText);
|
|
5488
5230
|
fold.id = 'streamThink';
|
|
5489
|
-
fold.hidden =
|
|
5231
|
+
if (!thinkText) fold.hidden = true;
|
|
5490
5232
|
col.appendChild(fold);
|
|
5491
5233
|
} else if (thinkText) {
|
|
5492
5234
|
col.appendChild(makeThinkFold(thinkText, false, false));
|
|
@@ -5537,7 +5279,6 @@ const APP_HTML = `<!doctype html>
|
|
|
5537
5279
|
}
|
|
5538
5280
|
const textEl = document.createElement('div');
|
|
5539
5281
|
textEl.innerHTML = renderMentions(text);
|
|
5540
|
-
if (live && (!text || text === '…')) bubble.hidden = true;
|
|
5541
5282
|
if (who === 'bot') {
|
|
5542
5283
|
const preview = htmlPreviewUrl(text);
|
|
5543
5284
|
if (preview) textEl.appendChild(previewFrame(preview, htmlPreviewKey(text, preview)));
|
|
@@ -5582,8 +5323,7 @@ const APP_HTML = `<!doctype html>
|
|
|
5582
5323
|
if (!full || full.id !== activeId) return;
|
|
5583
5324
|
const renderKey = String(workspacePort) + '|' + full.id + '|' + full.status + '|' + (full.history || []).map(function (h) {
|
|
5584
5325
|
return [h.who, h.text, h.thinking, h.runStatus, h.runOutput, (h.images || []).join(',')].join('|#');
|
|
5585
|
-
}).join('||')
|
|
5586
|
-
+ '|recall:' + ((recalledUserTurn(activeId) || {}).text || '');
|
|
5326
|
+
}).join('||');
|
|
5587
5327
|
if (renderKey === lastRenderKey) {
|
|
5588
5328
|
if (streamBuf || streamThink) paintStream();
|
|
5589
5329
|
return;
|
|
@@ -5595,10 +5335,7 @@ const APP_HTML = `<!doctype html>
|
|
|
5595
5335
|
parkedPreviews = parkPreviews();
|
|
5596
5336
|
log.innerHTML = '';
|
|
5597
5337
|
lastSpeaker = null;
|
|
5598
|
-
const seenUser = {};
|
|
5599
5338
|
for (const h of full.history) {
|
|
5600
|
-
if (h.who === 'user' && isHarnessUserText(h.text)) continue;
|
|
5601
|
-
if (h.who === 'user') seenUser[h.text] = true;
|
|
5602
5339
|
let run;
|
|
5603
5340
|
if (h.runId || h.runStatus) {
|
|
5604
5341
|
run = { id: h.runId, status: h.runStatus, output: h.runOutput, command: h.text };
|
|
@@ -5608,21 +5345,6 @@ const APP_HTML = `<!doctype html>
|
|
|
5608
5345
|
}
|
|
5609
5346
|
addRow(h.who, h.text, h.color || t.color, h.name || t.name, run, h.images, h.thinking);
|
|
5610
5347
|
}
|
|
5611
|
-
const keepPending = [];
|
|
5612
|
-
for (let i = 0; i < pendingTurns.length; i++) {
|
|
5613
|
-
const p = pendingTurns[i];
|
|
5614
|
-
if (p.threadId !== activeId) { keepPending.push(p); continue; }
|
|
5615
|
-
if (seenUser[p.text]) { forgetUserTurn(p.threadId, p.text); continue; }
|
|
5616
|
-
addRow('user', p.text, t.color, t.name, undefined, p.images);
|
|
5617
|
-
seenUser[p.text] = true;
|
|
5618
|
-
keepPending.push(p);
|
|
5619
|
-
}
|
|
5620
|
-
pendingTurns = keepPending;
|
|
5621
|
-
const recalled = recalledUserTurn(activeId);
|
|
5622
|
-
if (recalled && recalled.text) {
|
|
5623
|
-
if (seenUser[recalled.text]) forgetUserTurn(activeId, recalled.text);
|
|
5624
|
-
else addRow('user', recalled.text, t.color, t.name, undefined, recalled.images);
|
|
5625
|
-
}
|
|
5626
5348
|
if (full.status === 'thinking') {
|
|
5627
5349
|
if (full.liveStatus) streamStatus = full.liveStatus;
|
|
5628
5350
|
if (full.liveRace && full.liveRace.racers && full.liveRace.racers.length >= 2) {
|
|
@@ -5708,93 +5430,55 @@ const APP_HTML = `<!doctype html>
|
|
|
5708
5430
|
function liveBubbleHtml() {
|
|
5709
5431
|
if (raceIsLive(streamRace)) return raceGridHtml(streamRace);
|
|
5710
5432
|
const vis = streamParts().visible;
|
|
5711
|
-
|
|
5433
|
+
if (vis) {
|
|
5434
|
+
const trail = streamStatus && /^(RUN|READ|WRITE|EDIT|SPAWN|SEND|GLOB|GREP|MCP|FETCH|TODO|SERVE|PING|PEEK|MULTIEDIT|NOTEBOOK):/i.test(streamStatus)
|
|
5435
|
+
? '<span class="ttrail">' + escapeHtml(streamStatus) + '</span>' : '';
|
|
5436
|
+
return escapeHtml(vis) + trail;
|
|
5437
|
+
}
|
|
5438
|
+
const dots = '<span class="dots"><span></span><span></span><span></span></span>';
|
|
5439
|
+
const st = streamStatus ? '<span class="tstatus">' + escapeHtml(streamStatus) + '</span>' : '';
|
|
5440
|
+
return dots + (st ? ' ' + st : '');
|
|
5712
5441
|
}
|
|
5713
5442
|
function paintThinkFoldLive(think) {
|
|
5714
5443
|
const fold = document.getElementById('streamThink');
|
|
5715
5444
|
if (!fold) return;
|
|
5716
5445
|
const chip = fold.querySelector('.thinkchip');
|
|
5717
5446
|
const body = fold.querySelector('.thinkbody');
|
|
5718
|
-
const
|
|
5719
|
-
fold.hidden =
|
|
5720
|
-
|
|
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);
|
|
5721
5454
|
fold.classList.toggle('open', !!liveThinkOpen);
|
|
5722
5455
|
if (chip) {
|
|
5723
5456
|
chip.textContent = 'thinking...';
|
|
5724
5457
|
chip.setAttribute('aria-expanded', liveThinkOpen ? 'true' : 'false');
|
|
5725
5458
|
}
|
|
5726
|
-
if (body) body.textContent = liveThinkOpen ?
|
|
5727
|
-
}
|
|
5728
|
-
// Follow the live assistant bubble only while the reader is still on
|
|
5729
|
-
// it. A post-paint pad check is what broke autoscroll: a lump >140px
|
|
5730
|
-
// (long reply, 400 dump, table) put them past the pad, then every
|
|
5731
|
-
// later token also refused to follow. Snapshot first; sticky-follow
|
|
5732
|
-
// the turn they are watching; drop follow if they scroll up.
|
|
5733
|
-
let followLive = false;
|
|
5734
|
-
let pinningLog = false;
|
|
5735
|
-
let pinRaf = 0;
|
|
5736
|
-
function pinLogBottom() {
|
|
5737
|
-
pinningLog = true;
|
|
5738
|
-
log.scrollTop = log.scrollHeight;
|
|
5739
|
-
// Tables / markdown can grow the bubble after this paint's layout.
|
|
5740
|
-
// A second pin on the next frame follows that growth. Do not
|
|
5741
|
-
// re-check the pad here — the snapshot already decided to follow.
|
|
5742
|
-
if (pinRaf) cancelAnimationFrame(pinRaf);
|
|
5743
|
-
pinRaf = requestAnimationFrame(function () {
|
|
5744
|
-
pinRaf = 0;
|
|
5745
|
-
log.scrollTop = log.scrollHeight;
|
|
5746
|
-
pinningLog = false;
|
|
5747
|
-
});
|
|
5748
|
-
}
|
|
5749
|
-
log.addEventListener('scroll', function () {
|
|
5750
|
-
if (pinningLog) return;
|
|
5751
|
-
followLive = log.scrollHeight - log.scrollTop - log.clientHeight < 140;
|
|
5752
|
-
});
|
|
5753
|
-
function ensureLiveBotRow() {
|
|
5754
|
-
if (document.getElementById('streamBubble')) return;
|
|
5755
|
-
const t = knownThreads.find((x) => x.id === activeId);
|
|
5756
|
-
if (!t) return;
|
|
5757
|
-
addRow('bot', streamBuf || '…', t.color, t.name, undefined, undefined, streamThink, true);
|
|
5758
|
-
const row = log.querySelector('.row:last-child');
|
|
5759
|
-
if (!row) return;
|
|
5760
|
-
const b = row.querySelector('.bubble');
|
|
5761
|
-
if (b) b.id = 'streamBubble';
|
|
5762
|
-
const fold = row.querySelector('.thinkfold');
|
|
5763
|
-
if (fold) fold.id = 'streamThink';
|
|
5459
|
+
if (body) body.textContent = liveThinkOpen ? think : '';
|
|
5764
5460
|
}
|
|
5765
5461
|
function paintStream() {
|
|
5766
|
-
|
|
5767
|
-
if (!b) {
|
|
5768
|
-
// Claude TUI start/replace used to call render(), which wiped #log
|
|
5769
|
-
// and rebuilt from a fetch that could miss the just-sent user turn.
|
|
5770
|
-
// Keep existing user rows; only attach the live assistant bubble.
|
|
5771
|
-
ensureLiveBotRow();
|
|
5772
|
-
b = document.getElementById('streamBubble');
|
|
5773
|
-
if (!b) return;
|
|
5774
|
-
}
|
|
5775
|
-
// Snapshot BEFORE the bubble grows. Same idea as render()'s wasNearBottom.
|
|
5776
|
-
const wasNearBottom = log.scrollHeight - log.scrollTop - log.clientHeight < 140;
|
|
5777
|
-
if (wasNearBottom) followLive = true;
|
|
5462
|
+
const b = document.getElementById('streamBubble');
|
|
5463
|
+
if (!b) { render(); return; }
|
|
5778
5464
|
const parts = streamParts();
|
|
5779
5465
|
paintThinkFoldLive(parts.think);
|
|
5780
5466
|
if (raceIsLive(streamRace)) {
|
|
5781
|
-
b.hidden = false;
|
|
5782
5467
|
b.classList.add('raceboard');
|
|
5783
5468
|
b.innerHTML = liveBubbleHtml();
|
|
5784
|
-
if (
|
|
5469
|
+
if (log.scrollHeight - log.scrollTop - log.clientHeight < 140) log.scrollTop = log.scrollHeight;
|
|
5785
5470
|
scheduleFindPaint();
|
|
5786
5471
|
return;
|
|
5787
5472
|
}
|
|
5788
5473
|
b.classList.remove('raceboard');
|
|
5789
|
-
//
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
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;
|
|
5793
5478
|
} else {
|
|
5794
|
-
b.hidden = true;
|
|
5795
5479
|
b.innerHTML = liveBubbleHtml();
|
|
5796
5480
|
}
|
|
5797
|
-
if (
|
|
5481
|
+
if (log.scrollHeight - log.scrollTop - log.clientHeight < 140) log.scrollTop = log.scrollHeight;
|
|
5798
5482
|
scheduleFindPaint();
|
|
5799
5483
|
}
|
|
5800
5484
|
function connectStream(id) {
|
|
@@ -5803,13 +5487,10 @@ const APP_HTML = `<!doctype html>
|
|
|
5803
5487
|
esId = id;
|
|
5804
5488
|
streamBuf = '';
|
|
5805
5489
|
streamThink = '';
|
|
5806
|
-
streamTools = [];
|
|
5807
5490
|
streamStatus = '';
|
|
5808
5491
|
streamRace = null;
|
|
5809
5492
|
streamRaceId = id;
|
|
5810
5493
|
liveThinkOpen = false;
|
|
5811
|
-
// New thread: do not inherit follow from the previous log position.
|
|
5812
|
-
followLive = log.scrollHeight - log.scrollTop - log.clientHeight < 140;
|
|
5813
5494
|
raceHandoff += 1;
|
|
5814
5495
|
es = new EventSource('/stream/' + id); // EventSource reconnects on its own
|
|
5815
5496
|
es.onmessage = (e) => {
|
|
@@ -5818,18 +5499,12 @@ const APP_HTML = `<!doctype html>
|
|
|
5818
5499
|
if (ev.type === 'start') {
|
|
5819
5500
|
streamBuf = '';
|
|
5820
5501
|
streamThink = '';
|
|
5821
|
-
streamTools = [];
|
|
5822
5502
|
streamStatus = ev.detail || 'waiting on model…';
|
|
5823
5503
|
streamRace = null;
|
|
5824
5504
|
streamRaceId = id;
|
|
5825
5505
|
liveThinkOpen = false;
|
|
5826
5506
|
paintStream();
|
|
5827
5507
|
}
|
|
5828
|
-
else if (ev.type === 'tool') {
|
|
5829
|
-
const line = ev.detail || '';
|
|
5830
|
-
if (line && streamTools[streamTools.length - 1] !== line) streamTools.push(line);
|
|
5831
|
-
paintStream();
|
|
5832
|
-
}
|
|
5833
5508
|
else if (ev.type === 'status') { streamStatus = ev.detail || streamStatus; paintStream(); }
|
|
5834
5509
|
else if (ev.type === 'race') {
|
|
5835
5510
|
if (ev.race && ev.race.racers && ev.race.racers.length >= 2) {
|
|
@@ -5839,13 +5514,12 @@ const APP_HTML = `<!doctype html>
|
|
|
5839
5514
|
paintStream();
|
|
5840
5515
|
}
|
|
5841
5516
|
else if (ev.type === 'think') {
|
|
5842
|
-
streamThink
|
|
5517
|
+
streamThink += ev.delta || '';
|
|
5843
5518
|
paintStream();
|
|
5844
5519
|
}
|
|
5845
5520
|
else if (ev.type === 'delta') {
|
|
5846
5521
|
streamBuf = ev.replace ? (ev.delta || '') : streamBuf + (ev.delta || '');
|
|
5847
|
-
|
|
5848
|
-
if (ev.replace && /<think/i.test(ev.delta || '')) streamThink = '';
|
|
5522
|
+
if (ev.replace) streamThink = '';
|
|
5849
5523
|
paintStream();
|
|
5850
5524
|
}
|
|
5851
5525
|
else if (ev.type === 'final' || ev.type === 'run-pending') {
|
|
@@ -5856,7 +5530,6 @@ const APP_HTML = `<!doctype html>
|
|
|
5856
5530
|
if (token !== raceHandoff) return;
|
|
5857
5531
|
streamBuf = '';
|
|
5858
5532
|
streamThink = '';
|
|
5859
|
-
streamTools = [];
|
|
5860
5533
|
streamStatus = '';
|
|
5861
5534
|
streamRace = null;
|
|
5862
5535
|
render();
|
|
@@ -5865,7 +5538,6 @@ const APP_HTML = `<!doctype html>
|
|
|
5865
5538
|
}
|
|
5866
5539
|
streamBuf = '';
|
|
5867
5540
|
streamThink = '';
|
|
5868
|
-
streamTools = [];
|
|
5869
5541
|
streamStatus = '';
|
|
5870
5542
|
streamRace = null;
|
|
5871
5543
|
render();
|
|
@@ -5937,11 +5609,8 @@ const APP_HTML = `<!doctype html>
|
|
|
5937
5609
|
return;
|
|
5938
5610
|
}
|
|
5939
5611
|
if ((!task && !pendingFiles.length && !pendingImages.length) || !activeId) return;
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
const draft = inp.value;
|
|
5943
|
-
const hadFiles = pendingFiles.slice();
|
|
5944
|
-
const hadImages = pendingImages.slice();
|
|
5612
|
+
inp.value = '';
|
|
5613
|
+
send.classList.remove('show');
|
|
5945
5614
|
let full = task;
|
|
5946
5615
|
// an image with no caption still needs SOME text — an empty text block
|
|
5947
5616
|
// alongside image_url content gets rejected (400) by at least one path
|
|
@@ -5952,39 +5621,13 @@ const APP_HTML = `<!doctype html>
|
|
|
5952
5621
|
: '\\n\\n(attached binary file: ' + f.name + ', ' + f.size + ' bytes — content not readable as text)';
|
|
5953
5622
|
}
|
|
5954
5623
|
const images = pendingImages.map((i) => i.dataUrl);
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
try {
|
|
5963
|
-
const r = await fetch(API + '/drive', {
|
|
5964
|
-
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
5965
|
-
body: JSON.stringify({ threadId: activeId, task: full, images }),
|
|
5966
|
-
});
|
|
5967
|
-
let body = {};
|
|
5968
|
-
try { body = await r.json(); } catch (e) { body = {}; }
|
|
5969
|
-
persisted = r.ok && body.persisted !== false;
|
|
5970
|
-
} catch (e) { persisted = false; }
|
|
5971
|
-
if (persisted) {
|
|
5972
|
-
inp.value = '';
|
|
5973
|
-
send.classList.remove('show');
|
|
5974
|
-
pendingFiles = [];
|
|
5975
|
-
pendingImages = [];
|
|
5976
|
-
renderAttachChips();
|
|
5977
|
-
} else {
|
|
5978
|
-
pendingTurns = pendingTurns.filter((p) => !(p.threadId === activeId && p.text === full));
|
|
5979
|
-
forgetUserTurn(activeId, full);
|
|
5980
|
-
inp.value = draft;
|
|
5981
|
-
pendingFiles = hadFiles;
|
|
5982
|
-
pendingImages = hadImages;
|
|
5983
|
-
renderAttachChips();
|
|
5984
|
-
send.classList.toggle('show', inp.value.trim().length > 0 || pendingFiles.length > 0 || pendingImages.length > 0);
|
|
5985
|
-
lastRenderKey = '';
|
|
5986
|
-
}
|
|
5987
|
-
sending = false;
|
|
5624
|
+
pendingFiles = [];
|
|
5625
|
+
pendingImages = [];
|
|
5626
|
+
renderAttachChips();
|
|
5627
|
+
await fetch(API + '/drive', {
|
|
5628
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
5629
|
+
body: JSON.stringify({ threadId: activeId, task: full, images }),
|
|
5630
|
+
});
|
|
5988
5631
|
render();
|
|
5989
5632
|
}
|
|
5990
5633
|
|
|
@@ -6405,16 +6048,6 @@ const APP_HTML = `<!doctype html>
|
|
|
6405
6048
|
const bindEl = document.getElementById('dockBind');
|
|
6406
6049
|
const callsEl = document.getElementById('dockCalls');
|
|
6407
6050
|
if (!spillEl || !sessEl || !paidEl || !bindEl || !callsEl) return;
|
|
6408
|
-
if (you && you.proxyReachable === false) {
|
|
6409
|
-
spillEl.textContent = '—';
|
|
6410
|
-
spillEl.className = 'dv';
|
|
6411
|
-
sessEl.textContent = '—';
|
|
6412
|
-
sessEl.className = 'dv';
|
|
6413
|
-
paidEl.textContent = 'proxy unreachable';
|
|
6414
|
-
bindEl.textContent = '—';
|
|
6415
|
-
callsEl.textContent = '—';
|
|
6416
|
-
return;
|
|
6417
|
-
}
|
|
6418
6051
|
const spent = Number(you && you.spentUsd) || 0;
|
|
6419
6052
|
const direct = Number(you && you.directUsd) || 0;
|
|
6420
6053
|
const spillX = Number(you && you.spilled && you.spilled.savingX);
|
|
@@ -6442,22 +6075,6 @@ const APP_HTML = `<!doctype html>
|
|
|
6442
6075
|
// straight to localhost:8402 would work fine, but routing it through
|
|
6443
6076
|
// our own backend keeps one fetch path if that ever needs to change.
|
|
6444
6077
|
const you = await (await fetch(API + '/hud-summary')).json();
|
|
6445
|
-
const proxyDown = you.proxyReachable === false;
|
|
6446
|
-
setHudTick(proxyDown ? 2000 : 30000);
|
|
6447
|
-
if (proxyDown) {
|
|
6448
|
-
const creditEl = document.getElementById('hCredit');
|
|
6449
|
-
if (creditEl) creditEl.textContent = '—';
|
|
6450
|
-
document.getElementById('hYouSpent').textContent = 'proxy unreachable';
|
|
6451
|
-
document.getElementById('hYouCogs').textContent = '—';
|
|
6452
|
-
document.getElementById('hYouMargin').textContent = '—';
|
|
6453
|
-
document.getElementById('hYouDirect').textContent = '—';
|
|
6454
|
-
document.getElementById('hYouSaved').textContent = '—';
|
|
6455
|
-
document.getElementById('hFoot').textContent = 'proxy unreachable';
|
|
6456
|
-
const hintEl = document.getElementById('hHint');
|
|
6457
|
-
if (hintEl) hintEl.className = 'hhint';
|
|
6458
|
-
paintDock(you);
|
|
6459
|
-
return;
|
|
6460
|
-
}
|
|
6461
6078
|
const creditEl = document.getElementById('hCredit');
|
|
6462
6079
|
if (creditEl) creditEl.textContent = (you.creditUsd == null) ? '—' : usd(Number(you.creditUsd) || 0);
|
|
6463
6080
|
const subRow = document.getElementById('hSubRow');
|
|
@@ -6524,16 +6141,9 @@ const APP_HTML = `<!doctype html>
|
|
|
6524
6141
|
}
|
|
6525
6142
|
}
|
|
6526
6143
|
let hudTimer = null;
|
|
6527
|
-
let hudTickMs = 30000;
|
|
6528
|
-
function setHudTick(ms) {
|
|
6529
|
-
if (hudTimer && hudTickMs === ms) return;
|
|
6530
|
-
hudTickMs = ms;
|
|
6531
|
-
if (hudTimer) clearInterval(hudTimer);
|
|
6532
|
-
hudTimer = setInterval(refreshHud, hudTickMs);
|
|
6533
|
-
}
|
|
6534
6144
|
function ensureHudTick() {
|
|
6535
6145
|
if (hudTimer) return;
|
|
6536
|
-
hudTimer = setInterval(refreshHud,
|
|
6146
|
+
hudTimer = setInterval(refreshHud, 30000);
|
|
6537
6147
|
}
|
|
6538
6148
|
hudBtn.addEventListener('click', (e) => {
|
|
6539
6149
|
e.stopPropagation();
|
|
@@ -6734,24 +6344,9 @@ const server = http.createServer((req, res) => {
|
|
|
6734
6344
|
|
|
6735
6345
|
if (req.method === 'GET' && req.url === '/hud-summary') {
|
|
6736
6346
|
(async () => {
|
|
6737
|
-
let you = { spentUsd: 0, cogsUsd: 0, directUsd: 0, paidCalls: 0, creditUsd: null, chainUsd: null
|
|
6738
|
-
try {
|
|
6739
|
-
|
|
6740
|
-
// 402 = empty wallet / Pay — the sidecar is up. Do not paint that as dead.
|
|
6741
|
-
if (r.ok || r.status === 402) {
|
|
6742
|
-
you.proxyReachable = true;
|
|
6743
|
-
if (r.ok) {
|
|
6744
|
-
try { you = { ...you, ...(await r.json()), proxyReachable: true }; }
|
|
6745
|
-
catch { /* body optional */ }
|
|
6746
|
-
}
|
|
6747
|
-
}
|
|
6748
|
-
} catch { /* session down — try /v1/models before calling the proxy dead */ }
|
|
6749
|
-
if (!you.proxyReachable) {
|
|
6750
|
-
try {
|
|
6751
|
-
const r = await fetch('http://127.0.0.1:8402/v1/models', { signal: AbortSignal.timeout(2000) });
|
|
6752
|
-
if (r.ok || r.status === 402) you.proxyReachable = true;
|
|
6753
|
-
} catch { /* local proxy not running — HUD must not pretend spend is $0 */ }
|
|
6754
|
-
}
|
|
6347
|
+
let you = { spentUsd: 0, cogsUsd: 0, directUsd: 0, paidCalls: 0, creditUsd: null, chainUsd: null };
|
|
6348
|
+
try { you = { ...you, ...(await (await fetch('http://127.0.0.1:8402/v1/session', { signal: AbortSignal.timeout(2000) })).json()) }; }
|
|
6349
|
+
catch { /* local proxy not running — HUD shows zeros rather than guessing */ }
|
|
6755
6350
|
await attachSpilled(you);
|
|
6756
6351
|
try {
|
|
6757
6352
|
you.creditUsd = await creditBalance();
|
|
@@ -6838,7 +6433,7 @@ const server = http.createServer((req, res) => {
|
|
|
6838
6433
|
const t = threads.get(req.url.split('/')[2]);
|
|
6839
6434
|
res.writeHead(200, { 'content-type': 'application/json' });
|
|
6840
6435
|
res.end(t ? JSON.stringify({
|
|
6841
|
-
id: t.id, history:
|
|
6436
|
+
id: t.id, history: t.history, status: t.status,
|
|
6842
6437
|
liveStatus: t.status === 'thinking' ? (t.liveStatus || '') : '',
|
|
6843
6438
|
liveRace: t.status === 'thinking' ? (t.liveRace || null) : null,
|
|
6844
6439
|
lastRaceFail: t.lastRaceFail || null,
|
|
@@ -6927,39 +6522,32 @@ const server = http.createServer((req, res) => {
|
|
|
6927
6522
|
threadId = j.threadId; task = (j.task || '').toString();
|
|
6928
6523
|
images = Array.isArray(j.images) ? j.images.filter((u) => typeof u === 'string') : [];
|
|
6929
6524
|
} catch { /* ignore */ }
|
|
6930
|
-
const ack = (ok, extra = {}) => {
|
|
6931
|
-
if (res.writableEnded) return;
|
|
6932
|
-
const status = extra.status || (ok ? 200 : 500);
|
|
6933
|
-
res.writeHead(status, { 'content-type': 'application/json' });
|
|
6934
|
-
res.end(JSON.stringify({ ok: Boolean(ok), persisted: extra.persisted !== false, ...extra.body }));
|
|
6935
|
-
};
|
|
6936
6525
|
const t = threads.get(threadId);
|
|
6937
|
-
|
|
6938
|
-
|
|
6939
|
-
|
|
6940
|
-
|
|
6941
|
-
|
|
6942
|
-
//
|
|
6943
|
-
//
|
|
6944
|
-
|
|
6526
|
+
const driveOk = () => {
|
|
6527
|
+
if (res.headersSent) return;
|
|
6528
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
6529
|
+
res.end(JSON.stringify({ ok: true }));
|
|
6530
|
+
};
|
|
6531
|
+
// Every OTHER slash command. Local, free, instant — no model call, so
|
|
6532
|
+
// checking your spend or clearing a thread never costs anything.
|
|
6533
|
+
// /dir and /mode keep their own handlers below, untouched.
|
|
6534
|
+
if (t && /^\//.test(task.trim())) {
|
|
6945
6535
|
// Drawer-only. Never dump sitrep into the transcript.
|
|
6946
|
-
if (/^\/sitrep\b/i.test(task.trim())) {
|
|
6947
|
-
|
|
6948
|
-
|
|
6949
|
-
|
|
6950
|
-
|
|
6951
|
-
|
|
6952
|
-
|
|
6953
|
-
|
|
6954
|
-
return;
|
|
6955
|
-
}
|
|
6536
|
+
if (/^\/sitrep\b/i.test(task.trim())) { driveOk(); return; }
|
|
6537
|
+
const handled = await handleSlash(task.trim(), t).catch((e) => `error: ${e.message}`);
|
|
6538
|
+
if (handled !== null && handled !== undefined) {
|
|
6539
|
+
t.history.push({ who: 'bot', text: handled });
|
|
6540
|
+
t.lastActivityAt = Date.now();
|
|
6541
|
+
saveThreads();
|
|
6542
|
+
driveOk();
|
|
6543
|
+
return;
|
|
6956
6544
|
}
|
|
6957
6545
|
}
|
|
6958
6546
|
// "/dir <path>" is a LOCAL control command, not sent to the model at
|
|
6959
6547
|
// all — free, instant, sets which folder this thread's WRITE/READ/SERVE
|
|
6960
6548
|
// are scoped to. Respecify any time by sending it again.
|
|
6961
6549
|
const dirCmd = /^\/dir\s+(.+)/.exec(task.trim());
|
|
6962
|
-
if (dirCmd) {
|
|
6550
|
+
if (dirCmd && t) {
|
|
6963
6551
|
const full = path.resolve(expandHome(dirCmd[1].trim()));
|
|
6964
6552
|
let ok = false;
|
|
6965
6553
|
try { ok = statSync(full).isDirectory(); } catch { /* not a dir / doesn't exist */ }
|
|
@@ -6970,31 +6558,22 @@ const server = http.createServer((req, res) => {
|
|
|
6970
6558
|
t.history.push({ who: 'bot', text: `"${full}" isn't a directory that exists.` });
|
|
6971
6559
|
}
|
|
6972
6560
|
saveThreads();
|
|
6973
|
-
|
|
6561
|
+
driveOk();
|
|
6974
6562
|
return;
|
|
6975
6563
|
}
|
|
6976
6564
|
// "/mode auto|ask" — Auto is the Claude Code harness (openzoo claude
|
|
6977
6565
|
// env). Ask stays a chat completion; RUN: waits for approve/deny.
|
|
6978
6566
|
const modeCmd = /^\/mode\s+(auto|ask)\b/.exec(task.trim());
|
|
6979
|
-
if (modeCmd) {
|
|
6567
|
+
if (modeCmd && t) {
|
|
6980
6568
|
t.runMode = modeCmd[1];
|
|
6981
|
-
|
|
6982
|
-
t.history.push({ who: 'bot', text: `Run mode set to ${modeCmd[1]}${modeCmd[1] === 'auto' ? ' — Claude Code via OpenZoo (x402). Interactive TUI on a PTY, not --print.' : ' — chat completion; RUN: waits for your approval.'}` });
|
|
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.'}` });
|
|
6983
6570
|
saveThreads();
|
|
6984
|
-
|
|
6985
|
-
return;
|
|
6986
|
-
}
|
|
6987
|
-
// Persist the user bubble to disk BEFORE the model call and before
|
|
6988
|
-
// the HTTP ACK. A proxy 500 / sidecar bounce / remount must not
|
|
6989
|
-
// forget the turn, and must not rewrite it as an AUTO continue.
|
|
6990
|
-
const flushed = persistUserTurn(t, task, images);
|
|
6991
|
-
if (!flushed.ok) {
|
|
6992
|
-
ack(false, { status: 500, persisted: false });
|
|
6571
|
+
driveOk();
|
|
6993
6572
|
return;
|
|
6994
6573
|
}
|
|
6995
|
-
ack(true, { persisted: true });
|
|
6996
6574
|
// Stream to whoever is watching this thread. emitToThread is a no-op
|
|
6997
6575
|
// when nobody is, so a spawned subagent nobody has open costs nothing.
|
|
6576
|
+
driveOk();
|
|
6998
6577
|
runTurn(threadId, task, (ev) => emitToThread(threadId, ev), images).catch(() => {});
|
|
6999
6578
|
});
|
|
7000
6579
|
return;
|
|
@@ -7008,7 +6587,7 @@ server.listen(PORT, BIND, () => console.log(`[grokui] http://${BIND === '0.0.0.0
|
|
|
7008
6587
|
export {
|
|
7009
6588
|
tryDirective, ensureWorkspacePort, isPreviewableRel, previewAck, workspaceFileUrl,
|
|
7010
6589
|
parseRun, looksLikeMcpAsBash, stripThinkTags, takeThink, safeResolveIn, inDir, listDir,
|
|
7011
|
-
handleSlash,
|
|
6590
|
+
handleSlash, newThread, setRunTurnForTest, setBrainAskForTest, setClaudeRunnerForTest, runTurn,
|
|
7012
6591
|
runAutoClaudeTurn, autoClaudePrompt,
|
|
7013
6592
|
AUTO_CONTINUE, AUTO_RACE_RETRY, AUTO_EMPTY_RETRY, pingWakeText, pingCanWake, shouldKeepAuto,
|
|
7014
6593
|
isDoneReply, isTransientModelFail, isPaymentFailed, isEmptyWalletPayment, isEmptyToolResult, enqueueAutoHop, childKickoff, findByName,
|
|
@@ -7017,6 +6596,4 @@ export {
|
|
|
7017
6596
|
filesForCorpus, noteFileForCorpus, noteRunForCorpus, resetFilesForCorpus,
|
|
7018
6597
|
filesForCorpusKeys, scheduleFilesForCorpus, inFlightChars, BIND_MIN_CHARS, KEEP_MAX,
|
|
7019
6598
|
formatSavingLabel, holobrainOf, looksLikeProxyShell, CHAT_NOT_PROXY, PROXY_SHELL_REFUSE,
|
|
7020
|
-
persistUserTurn, isHarnessUserText, visibleHistory, isVisibleHistoryEntry, saveThreads,
|
|
7021
|
-
isClaudeFallbackReply, popClaudeFallbackBot, threadHasVisibleBotReply,
|
|
7022
6599
|
};
|