reasonix 0.4.28 → 0.5.2
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 +8 -0
- package/data/deepseek-tokenizer.json.gz +0 -0
- package/dist/cli/index.js +731 -85
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +76 -1
- package/dist/index.js +397 -73
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/cli/index.js
CHANGED
|
@@ -233,6 +233,28 @@ var DeepSeekClient = class {
|
|
|
233
233
|
return null;
|
|
234
234
|
}
|
|
235
235
|
}
|
|
236
|
+
/**
|
|
237
|
+
* Fetch the model catalog DeepSeek currently exposes. Today this is
|
|
238
|
+
* `deepseek-chat` (V3) and `deepseek-reasoner` (R1), but querying is
|
|
239
|
+
* the only way to learn about new ones without a Reasonix release.
|
|
240
|
+
* Returns null on any network/auth failure so callers can degrade
|
|
241
|
+
* gracefully — e.g. `/models` falls back to the hardcoded hint.
|
|
242
|
+
*/
|
|
243
|
+
async listModels(opts = {}) {
|
|
244
|
+
try {
|
|
245
|
+
const resp = await this._fetch(`${this.baseUrl}/models`, {
|
|
246
|
+
method: "GET",
|
|
247
|
+
headers: { Authorization: `Bearer ${this.apiKey}` },
|
|
248
|
+
signal: opts.signal
|
|
249
|
+
});
|
|
250
|
+
if (!resp.ok) return null;
|
|
251
|
+
const data = await resp.json();
|
|
252
|
+
if (!data || !Array.isArray(data.data)) return null;
|
|
253
|
+
return data;
|
|
254
|
+
} catch {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
236
258
|
async chat(opts) {
|
|
237
259
|
const ctrl = new AbortController();
|
|
238
260
|
const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
|
|
@@ -674,6 +696,170 @@ async function runHooks(opts) {
|
|
|
674
696
|
return { event, outcomes, blocked };
|
|
675
697
|
}
|
|
676
698
|
|
|
699
|
+
// src/tokenizer.ts
|
|
700
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
701
|
+
import { createRequire } from "module";
|
|
702
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
703
|
+
import { fileURLToPath } from "url";
|
|
704
|
+
import { gunzipSync } from "zlib";
|
|
705
|
+
function buildByteToChar() {
|
|
706
|
+
const result = new Array(256);
|
|
707
|
+
const bs = [];
|
|
708
|
+
for (let b = 33; b <= 126; b++) bs.push(b);
|
|
709
|
+
for (let b = 161; b <= 172; b++) bs.push(b);
|
|
710
|
+
for (let b = 174; b <= 255; b++) bs.push(b);
|
|
711
|
+
const cs = bs.slice();
|
|
712
|
+
let n = 0;
|
|
713
|
+
for (let b = 0; b < 256; b++) {
|
|
714
|
+
if (!bs.includes(b)) {
|
|
715
|
+
bs.push(b);
|
|
716
|
+
cs.push(256 + n);
|
|
717
|
+
n++;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
for (let i = 0; i < bs.length; i++) {
|
|
721
|
+
result[bs[i]] = String.fromCodePoint(cs[i]);
|
|
722
|
+
}
|
|
723
|
+
return result;
|
|
724
|
+
}
|
|
725
|
+
var cached = null;
|
|
726
|
+
function resolveDataPath() {
|
|
727
|
+
if (process.env.REASONIX_TOKENIZER_PATH) return process.env.REASONIX_TOKENIZER_PATH;
|
|
728
|
+
try {
|
|
729
|
+
const here = dirname2(fileURLToPath(import.meta.url));
|
|
730
|
+
return join3(here, "..", "data", "deepseek-tokenizer.json.gz");
|
|
731
|
+
} catch {
|
|
732
|
+
const req = createRequire(import.meta.url);
|
|
733
|
+
return join3(
|
|
734
|
+
dirname2(req.resolve("reasonix/package.json")),
|
|
735
|
+
"data",
|
|
736
|
+
"deepseek-tokenizer.json.gz"
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
function loadTokenizer() {
|
|
741
|
+
if (cached) return cached;
|
|
742
|
+
const buf = readFileSync3(resolveDataPath());
|
|
743
|
+
const json = gunzipSync(buf).toString("utf8");
|
|
744
|
+
const data = JSON.parse(json);
|
|
745
|
+
const mergeRank = /* @__PURE__ */ new Map();
|
|
746
|
+
for (let i = 0; i < data.model.merges.length; i++) {
|
|
747
|
+
mergeRank.set(data.model.merges[i], i);
|
|
748
|
+
}
|
|
749
|
+
const splitRegexes = [];
|
|
750
|
+
for (const p of data.pre_tokenizer.pretokenizers) {
|
|
751
|
+
if (p.type === "Split") {
|
|
752
|
+
splitRegexes.push(new RegExp(p.pattern.Regex, "gu"));
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
const addedMap = /* @__PURE__ */ new Map();
|
|
756
|
+
const addedContents = [];
|
|
757
|
+
for (const t of data.added_tokens) {
|
|
758
|
+
if (!t.special) {
|
|
759
|
+
addedMap.set(t.content, t.id);
|
|
760
|
+
addedContents.push(t.content);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
addedContents.sort((a, b) => b.length - a.length);
|
|
764
|
+
const addedPattern = addedContents.length ? new RegExp(addedContents.map(escapeRegex).join("|"), "g") : null;
|
|
765
|
+
cached = {
|
|
766
|
+
vocab: data.model.vocab,
|
|
767
|
+
mergeRank,
|
|
768
|
+
splitRegexes,
|
|
769
|
+
byteToChar: buildByteToChar(),
|
|
770
|
+
addedPattern,
|
|
771
|
+
addedMap
|
|
772
|
+
};
|
|
773
|
+
return cached;
|
|
774
|
+
}
|
|
775
|
+
function escapeRegex(s) {
|
|
776
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
777
|
+
}
|
|
778
|
+
function applySplit(chunks, re) {
|
|
779
|
+
const out = [];
|
|
780
|
+
for (const chunk of chunks) {
|
|
781
|
+
if (!chunk) continue;
|
|
782
|
+
re.lastIndex = 0;
|
|
783
|
+
let last = 0;
|
|
784
|
+
for (const m of chunk.matchAll(re)) {
|
|
785
|
+
const idx = m.index ?? 0;
|
|
786
|
+
if (idx > last) out.push(chunk.slice(last, idx));
|
|
787
|
+
if (m[0].length > 0) out.push(m[0]);
|
|
788
|
+
last = idx + m[0].length;
|
|
789
|
+
}
|
|
790
|
+
if (last < chunk.length) out.push(chunk.slice(last));
|
|
791
|
+
}
|
|
792
|
+
return out;
|
|
793
|
+
}
|
|
794
|
+
function byteLevelEncode(s, byteToChar) {
|
|
795
|
+
const bytes = new TextEncoder().encode(s);
|
|
796
|
+
let out = "";
|
|
797
|
+
for (let i = 0; i < bytes.length; i++) out += byteToChar[bytes[i]];
|
|
798
|
+
return out;
|
|
799
|
+
}
|
|
800
|
+
function bpeEncode(piece, mergeRank) {
|
|
801
|
+
if (piece.length <= 1) return piece ? [piece] : [];
|
|
802
|
+
let word = Array.from(piece);
|
|
803
|
+
while (true) {
|
|
804
|
+
let bestIdx = -1;
|
|
805
|
+
let bestRank = Number.POSITIVE_INFINITY;
|
|
806
|
+
for (let i = 0; i < word.length - 1; i++) {
|
|
807
|
+
const pair = `${word[i]} ${word[i + 1]}`;
|
|
808
|
+
const rank = mergeRank.get(pair);
|
|
809
|
+
if (rank !== void 0 && rank < bestRank) {
|
|
810
|
+
bestRank = rank;
|
|
811
|
+
bestIdx = i;
|
|
812
|
+
if (rank === 0) break;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
if (bestIdx < 0) break;
|
|
816
|
+
word = [
|
|
817
|
+
...word.slice(0, bestIdx),
|
|
818
|
+
word[bestIdx] + word[bestIdx + 1],
|
|
819
|
+
...word.slice(bestIdx + 2)
|
|
820
|
+
];
|
|
821
|
+
if (word.length === 1) break;
|
|
822
|
+
}
|
|
823
|
+
return word;
|
|
824
|
+
}
|
|
825
|
+
function encode(text) {
|
|
826
|
+
if (!text) return [];
|
|
827
|
+
const t = loadTokenizer();
|
|
828
|
+
const ids = [];
|
|
829
|
+
const process2 = (segment) => {
|
|
830
|
+
if (!segment) return;
|
|
831
|
+
let chunks = [segment];
|
|
832
|
+
for (const re of t.splitRegexes) chunks = applySplit(chunks, re);
|
|
833
|
+
for (const chunk of chunks) {
|
|
834
|
+
if (!chunk) continue;
|
|
835
|
+
const byteLevel = byteLevelEncode(chunk, t.byteToChar);
|
|
836
|
+
const pieces = bpeEncode(byteLevel, t.mergeRank);
|
|
837
|
+
for (const p of pieces) {
|
|
838
|
+
const id = t.vocab[p];
|
|
839
|
+
if (id !== void 0) ids.push(id);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
};
|
|
843
|
+
if (t.addedPattern) {
|
|
844
|
+
t.addedPattern.lastIndex = 0;
|
|
845
|
+
let last = 0;
|
|
846
|
+
for (const m of text.matchAll(t.addedPattern)) {
|
|
847
|
+
const idx = m.index ?? 0;
|
|
848
|
+
if (idx > last) process2(text.slice(last, idx));
|
|
849
|
+
const id = t.addedMap.get(m[0]);
|
|
850
|
+
if (id !== void 0) ids.push(id);
|
|
851
|
+
last = idx + m[0].length;
|
|
852
|
+
}
|
|
853
|
+
if (last < text.length) process2(text.slice(last));
|
|
854
|
+
} else {
|
|
855
|
+
process2(text);
|
|
856
|
+
}
|
|
857
|
+
return ids;
|
|
858
|
+
}
|
|
859
|
+
function countTokens(text) {
|
|
860
|
+
return encode(text).length;
|
|
861
|
+
}
|
|
862
|
+
|
|
677
863
|
// src/repair/flatten.ts
|
|
678
864
|
function analyzeSchema(schema) {
|
|
679
865
|
if (!schema) return { shouldFlatten: false, leafCount: 0, maxDepth: 0 };
|
|
@@ -823,7 +1009,15 @@ var ToolRegistry = class {
|
|
|
823
1009
|
}
|
|
824
1010
|
try {
|
|
825
1011
|
const result = await tool.fn(args, { signal: opts.signal });
|
|
826
|
-
|
|
1012
|
+
const str = typeof result === "string" ? result : JSON.stringify(result);
|
|
1013
|
+
let clipped = str;
|
|
1014
|
+
if (opts.maxResultTokens !== void 0) {
|
|
1015
|
+
clipped = truncateForModelByTokens(clipped, opts.maxResultTokens);
|
|
1016
|
+
}
|
|
1017
|
+
if (opts.maxResultChars !== void 0) {
|
|
1018
|
+
clipped = truncateForModel(clipped, opts.maxResultChars);
|
|
1019
|
+
}
|
|
1020
|
+
return clipped;
|
|
827
1021
|
} catch (err) {
|
|
828
1022
|
const e = err;
|
|
829
1023
|
if (typeof e.toToolResult === "function") {
|
|
@@ -857,6 +1051,7 @@ function hasDotKey(obj) {
|
|
|
857
1051
|
|
|
858
1052
|
// src/mcp/registry.ts
|
|
859
1053
|
var DEFAULT_MAX_RESULT_CHARS = 32e3;
|
|
1054
|
+
var DEFAULT_MAX_RESULT_TOKENS = 8e3;
|
|
860
1055
|
async function bridgeMcpTools(client, opts = {}) {
|
|
861
1056
|
const registry = opts.registry ?? new ToolRegistry({ autoFlatten: opts.autoFlatten });
|
|
862
1057
|
const prefix = opts.namePrefix ?? "";
|
|
@@ -913,6 +1108,61 @@ function truncateForModel(s, maxChars) {
|
|
|
913
1108
|
|
|
914
1109
|
${tail}`;
|
|
915
1110
|
}
|
|
1111
|
+
function truncateForModelByTokens(s, maxTokens) {
|
|
1112
|
+
if (maxTokens <= 0) return "";
|
|
1113
|
+
if (s.length <= maxTokens) return s;
|
|
1114
|
+
if (s.length <= maxTokens * 4) {
|
|
1115
|
+
const tokens = countTokens(s);
|
|
1116
|
+
if (tokens <= maxTokens) return s;
|
|
1117
|
+
}
|
|
1118
|
+
const markerOverhead = 48;
|
|
1119
|
+
const contentBudget = Math.max(0, maxTokens - markerOverhead);
|
|
1120
|
+
const tailBudget = Math.min(256, Math.floor(contentBudget * 0.1));
|
|
1121
|
+
const headBudget = Math.max(0, contentBudget - tailBudget);
|
|
1122
|
+
const head = sizePrefixToTokens(s, headBudget);
|
|
1123
|
+
const tail = sizeSuffixToTokens(s, tailBudget);
|
|
1124
|
+
const droppedChars = s.length - head.length - tail.length;
|
|
1125
|
+
const headTokens = head ? countTokens(head) : 0;
|
|
1126
|
+
const tailTokens = tail ? countTokens(tail) : 0;
|
|
1127
|
+
const sampleChars = head.length + tail.length;
|
|
1128
|
+
const sampleTokens = headTokens + tailTokens;
|
|
1129
|
+
const ratio = sampleChars > 0 ? sampleTokens / sampleChars : 0.3;
|
|
1130
|
+
const estTotalTokens = Math.ceil(s.length * ratio);
|
|
1131
|
+
const droppedTokens = Math.max(0, estTotalTokens - sampleTokens);
|
|
1132
|
+
return `${head}
|
|
1133
|
+
|
|
1134
|
+
[\u2026truncated ~${droppedTokens} tokens (${droppedChars} chars) \u2014 raise BridgeOptions.maxResultTokens, or call the tool with a narrower scope (filter, head, pagination)\u2026]
|
|
1135
|
+
|
|
1136
|
+
${tail}`;
|
|
1137
|
+
}
|
|
1138
|
+
function sizePrefixToTokens(s, budget) {
|
|
1139
|
+
if (budget <= 0 || s.length === 0) return "";
|
|
1140
|
+
let size = Math.min(s.length, budget * 4);
|
|
1141
|
+
for (let iter = 0; iter < 6; iter++) {
|
|
1142
|
+
if (size <= 0) return "";
|
|
1143
|
+
const slice = s.slice(0, size);
|
|
1144
|
+
const count = countTokens(slice);
|
|
1145
|
+
if (count <= budget) return slice;
|
|
1146
|
+
const next = Math.floor(size * (budget / count) * 0.95);
|
|
1147
|
+
if (next >= size) return s.slice(0, Math.max(0, size - 1));
|
|
1148
|
+
size = next;
|
|
1149
|
+
}
|
|
1150
|
+
return s.slice(0, Math.max(0, size));
|
|
1151
|
+
}
|
|
1152
|
+
function sizeSuffixToTokens(s, budget) {
|
|
1153
|
+
if (budget <= 0 || s.length === 0) return "";
|
|
1154
|
+
let size = Math.min(s.length, budget * 4);
|
|
1155
|
+
for (let iter = 0; iter < 6; iter++) {
|
|
1156
|
+
if (size <= 0) return "";
|
|
1157
|
+
const slice = s.slice(-size);
|
|
1158
|
+
const count = countTokens(slice);
|
|
1159
|
+
if (count <= budget) return slice;
|
|
1160
|
+
const next = Math.floor(size * (budget / count) * 0.95);
|
|
1161
|
+
if (next >= size) return s.slice(-Math.max(0, size - 1));
|
|
1162
|
+
size = next;
|
|
1163
|
+
}
|
|
1164
|
+
return s.slice(-Math.max(0, size));
|
|
1165
|
+
}
|
|
916
1166
|
function blockToString(block) {
|
|
917
1167
|
if (block.type === "text") return block.text;
|
|
918
1168
|
if (block.type === "image") return `[image ${block.mimeType}, ${block.data.length} chars base64]`;
|
|
@@ -1298,19 +1548,19 @@ import {
|
|
|
1298
1548
|
chmodSync as chmodSync2,
|
|
1299
1549
|
existsSync as existsSync2,
|
|
1300
1550
|
mkdirSync as mkdirSync2,
|
|
1301
|
-
readFileSync as
|
|
1551
|
+
readFileSync as readFileSync4,
|
|
1302
1552
|
readdirSync,
|
|
1303
1553
|
statSync,
|
|
1304
1554
|
unlinkSync,
|
|
1305
1555
|
writeFileSync as writeFileSync2
|
|
1306
1556
|
} from "fs";
|
|
1307
1557
|
import { homedir as homedir3 } from "os";
|
|
1308
|
-
import { dirname as
|
|
1558
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
1309
1559
|
function sessionsDir() {
|
|
1310
|
-
return
|
|
1560
|
+
return join4(homedir3(), ".reasonix", "sessions");
|
|
1311
1561
|
}
|
|
1312
1562
|
function sessionPath(name) {
|
|
1313
|
-
return
|
|
1563
|
+
return join4(sessionsDir(), `${sanitizeName(name)}.jsonl`);
|
|
1314
1564
|
}
|
|
1315
1565
|
function sanitizeName(name) {
|
|
1316
1566
|
const cleaned = name.replace(/[^\w\-\u4e00-\u9fa5]/g, "_").slice(0, 64);
|
|
@@ -1320,7 +1570,7 @@ function loadSessionMessages(name) {
|
|
|
1320
1570
|
const path = sessionPath(name);
|
|
1321
1571
|
if (!existsSync2(path)) return [];
|
|
1322
1572
|
try {
|
|
1323
|
-
const raw =
|
|
1573
|
+
const raw = readFileSync4(path, "utf8");
|
|
1324
1574
|
const out = [];
|
|
1325
1575
|
for (const line of raw.split(/\r?\n/)) {
|
|
1326
1576
|
const trimmed = line.trim();
|
|
@@ -1338,7 +1588,7 @@ function loadSessionMessages(name) {
|
|
|
1338
1588
|
}
|
|
1339
1589
|
function appendSessionMessage(name, message) {
|
|
1340
1590
|
const path = sessionPath(name);
|
|
1341
|
-
mkdirSync2(
|
|
1591
|
+
mkdirSync2(dirname3(path), { recursive: true });
|
|
1342
1592
|
appendFileSync(path, `${JSON.stringify(message)}
|
|
1343
1593
|
`, "utf8");
|
|
1344
1594
|
try {
|
|
@@ -1352,7 +1602,7 @@ function listSessions() {
|
|
|
1352
1602
|
try {
|
|
1353
1603
|
const files = readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
|
|
1354
1604
|
return files.map((file) => {
|
|
1355
|
-
const path =
|
|
1605
|
+
const path = join4(dir, file);
|
|
1356
1606
|
const stat = statSync(path);
|
|
1357
1607
|
const name = file.replace(/\.jsonl$/, "");
|
|
1358
1608
|
const messageCount = countLines(path);
|
|
@@ -1373,7 +1623,7 @@ function deleteSession(name) {
|
|
|
1373
1623
|
}
|
|
1374
1624
|
function rewriteSession(name, messages) {
|
|
1375
1625
|
const path = sessionPath(name);
|
|
1376
|
-
mkdirSync2(
|
|
1626
|
+
mkdirSync2(dirname3(path), { recursive: true });
|
|
1377
1627
|
const body = messages.map((m) => JSON.stringify(m)).join("\n");
|
|
1378
1628
|
writeFileSync2(path, body ? `${body}
|
|
1379
1629
|
` : "", "utf8");
|
|
@@ -1384,7 +1634,7 @@ function rewriteSession(name, messages) {
|
|
|
1384
1634
|
}
|
|
1385
1635
|
function countLines(path) {
|
|
1386
1636
|
try {
|
|
1387
|
-
const raw =
|
|
1637
|
+
const raw = readFileSync4(path, "utf8");
|
|
1388
1638
|
return raw.split(/\r?\n/).filter((l) => l.trim()).length;
|
|
1389
1639
|
} catch {
|
|
1390
1640
|
return 0;
|
|
@@ -1393,8 +1643,8 @@ function countLines(path) {
|
|
|
1393
1643
|
|
|
1394
1644
|
// src/telemetry.ts
|
|
1395
1645
|
var DEEPSEEK_PRICING = {
|
|
1396
|
-
"deepseek-chat": { inputCacheHit: 0.
|
|
1397
|
-
"deepseek-reasoner": { inputCacheHit: 0.
|
|
1646
|
+
"deepseek-chat": { inputCacheHit: 0.028, inputCacheMiss: 0.28, output: 0.42 },
|
|
1647
|
+
"deepseek-reasoner": { inputCacheHit: 0.028, inputCacheMiss: 0.28, output: 0.42 }
|
|
1398
1648
|
};
|
|
1399
1649
|
var CLAUDE_SONNET_PRICING = { input: 3, output: 15 };
|
|
1400
1650
|
var DEEPSEEK_CONTEXT_TOKENS = {
|
|
@@ -1949,6 +2199,24 @@ var CacheFirstLoop = class {
|
|
|
1949
2199
|
return;
|
|
1950
2200
|
}
|
|
1951
2201
|
const ctxMax = DEEPSEEK_CONTEXT_TOKENS[this.model] ?? DEFAULT_CONTEXT_TOKENS;
|
|
2202
|
+
if (usage) {
|
|
2203
|
+
const ratio = usage.promptTokens / ctxMax;
|
|
2204
|
+
if (ratio > 0.6 && ratio <= 0.8) {
|
|
2205
|
+
const before = usage.promptTokens;
|
|
2206
|
+
const soft = this.compact(16e3);
|
|
2207
|
+
if (soft.healedCount > 0) {
|
|
2208
|
+
const approxSaved = Math.round(soft.charsSaved / 4);
|
|
2209
|
+
const after = Math.max(0, before - approxSaved);
|
|
2210
|
+
yield {
|
|
2211
|
+
turn: this._turn,
|
|
2212
|
+
role: "warning",
|
|
2213
|
+
content: `context ${before.toLocaleString()}/${ctxMax.toLocaleString()} (${Math.round(
|
|
2214
|
+
ratio * 100
|
|
2215
|
+
)}%) \u2014 proactively compacted ${soft.healedCount} tool result(s) to 16k, saved ~${approxSaved.toLocaleString()} tokens (now ~${after.toLocaleString()}). Staying ahead of the 80% guard.`
|
|
2216
|
+
};
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
1952
2220
|
if (usage && usage.promptTokens / ctxMax > 0.8) {
|
|
1953
2221
|
const before = usage.promptTokens;
|
|
1954
2222
|
const compactResult = this.compact(4e3);
|
|
@@ -2011,7 +2279,10 @@ var CacheFirstLoop = class {
|
|
|
2011
2279
|
result = `[hook block] ${blocking?.hook.command ?? "<unknown>"}
|
|
2012
2280
|
${reason}`;
|
|
2013
2281
|
} else {
|
|
2014
|
-
result = await this.tools.dispatch(name, args, {
|
|
2282
|
+
result = await this.tools.dispatch(name, args, {
|
|
2283
|
+
signal,
|
|
2284
|
+
maxResultTokens: DEFAULT_MAX_RESULT_TOKENS
|
|
2285
|
+
});
|
|
2015
2286
|
const postReport = await runHooks({
|
|
2016
2287
|
hooks: this.hooks,
|
|
2017
2288
|
payload: {
|
|
@@ -3118,6 +3389,50 @@ function tokenizeCommand(cmd) {
|
|
|
3118
3389
|
if (cur.length > 0) out.push(cur);
|
|
3119
3390
|
return out;
|
|
3120
3391
|
}
|
|
3392
|
+
function detectShellOperator(cmd) {
|
|
3393
|
+
const opPrefix = /^(?:2>&1|&>|\|{1,2}|&{1,2}|2>{1,2}|>{1,2}|<{1,2})/;
|
|
3394
|
+
let cur = "";
|
|
3395
|
+
let curQuoted = false;
|
|
3396
|
+
let quote = null;
|
|
3397
|
+
const check = () => {
|
|
3398
|
+
if (cur.length === 0 && !curQuoted) return null;
|
|
3399
|
+
if (!curQuoted) {
|
|
3400
|
+
const m = opPrefix.exec(cur);
|
|
3401
|
+
if (m) return m[0] ?? null;
|
|
3402
|
+
}
|
|
3403
|
+
return null;
|
|
3404
|
+
};
|
|
3405
|
+
for (let i = 0; i < cmd.length; i++) {
|
|
3406
|
+
const ch = cmd[i];
|
|
3407
|
+
if (quote) {
|
|
3408
|
+
if (ch === quote) {
|
|
3409
|
+
quote = null;
|
|
3410
|
+
} else if (ch === "\\" && quote === '"' && i + 1 < cmd.length) {
|
|
3411
|
+
cur += cmd[++i];
|
|
3412
|
+
curQuoted = true;
|
|
3413
|
+
} else {
|
|
3414
|
+
cur += ch;
|
|
3415
|
+
curQuoted = true;
|
|
3416
|
+
}
|
|
3417
|
+
continue;
|
|
3418
|
+
}
|
|
3419
|
+
if (ch === '"' || ch === "'") {
|
|
3420
|
+
quote = ch;
|
|
3421
|
+
curQuoted = true;
|
|
3422
|
+
continue;
|
|
3423
|
+
}
|
|
3424
|
+
if (ch === " " || ch === " ") {
|
|
3425
|
+
const op = check();
|
|
3426
|
+
if (op) return op;
|
|
3427
|
+
cur = "";
|
|
3428
|
+
curQuoted = false;
|
|
3429
|
+
continue;
|
|
3430
|
+
}
|
|
3431
|
+
cur += ch;
|
|
3432
|
+
}
|
|
3433
|
+
if (quote) return null;
|
|
3434
|
+
return check();
|
|
3435
|
+
}
|
|
3121
3436
|
function isAllowed(cmd, extra = []) {
|
|
3122
3437
|
const normalized = cmd.trim().replace(/\s+/g, " ");
|
|
3123
3438
|
const allowlist = [...BUILTIN_ALLOWLIST, ...extra];
|
|
@@ -3130,6 +3445,12 @@ function isAllowed(cmd, extra = []) {
|
|
|
3130
3445
|
async function runCommand(cmd, opts) {
|
|
3131
3446
|
const argv = tokenizeCommand(cmd);
|
|
3132
3447
|
if (argv.length === 0) throw new Error("run_command: empty command");
|
|
3448
|
+
const operator = detectShellOperator(cmd);
|
|
3449
|
+
if (operator !== null) {
|
|
3450
|
+
throw new Error(
|
|
3451
|
+
`run_command: shell operator "${operator}" is not supported \u2014 this tool spawns one process, no shell expansion. Split into separate run_command calls and combine the output in your reasoning (e.g. instead of \`grep foo *.ts | wc -l\`, call \`grep -c foo *.ts\` or two separate commands). To pass "${operator}" as a literal argument, wrap it in quotes.`
|
|
3452
|
+
);
|
|
3453
|
+
}
|
|
3133
3454
|
const timeoutMs = (opts.timeoutSec ?? DEFAULT_TIMEOUT_SEC) * 1e3;
|
|
3134
3455
|
const maxChars = opts.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
|
|
3135
3456
|
const spawnOpts = {
|
|
@@ -3307,7 +3628,7 @@ function registerShellTools(registry, opts) {
|
|
|
3307
3628
|
properties: {
|
|
3308
3629
|
command: {
|
|
3309
3630
|
type: "string",
|
|
3310
|
-
description:
|
|
3631
|
+
description: 'Full command line. Tokenized with POSIX-ish quoting; no shell expansion. Pipes (`|`), redirects (`>`, `<`, `2>`), and `&&`/`||` chaining are rejected with an error \u2014 split into separate calls instead. To pass an operator character as a literal argument (e.g. a regex), wrap it in quotes: `grep "a|b" file.txt`.'
|
|
3311
3632
|
},
|
|
3312
3633
|
timeoutSec: {
|
|
3313
3634
|
type: "integer",
|
|
@@ -3526,12 +3847,12 @@ ${i + 1}. ${r.title}`);
|
|
|
3526
3847
|
}
|
|
3527
3848
|
|
|
3528
3849
|
// src/env.ts
|
|
3529
|
-
import { readFileSync as
|
|
3850
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
3530
3851
|
import { resolve as resolve3 } from "path";
|
|
3531
3852
|
function loadDotenv(path = ".env") {
|
|
3532
3853
|
let raw;
|
|
3533
3854
|
try {
|
|
3534
|
-
raw =
|
|
3855
|
+
raw = readFileSync5(resolve3(process.cwd(), path), "utf8");
|
|
3535
3856
|
} catch {
|
|
3536
3857
|
return;
|
|
3537
3858
|
}
|
|
@@ -3550,7 +3871,7 @@ function loadDotenv(path = ".env") {
|
|
|
3550
3871
|
}
|
|
3551
3872
|
|
|
3552
3873
|
// src/transcript.ts
|
|
3553
|
-
import { createWriteStream, readFileSync as
|
|
3874
|
+
import { createWriteStream, readFileSync as readFileSync6 } from "fs";
|
|
3554
3875
|
function recordFromLoopEvent(ev, extra) {
|
|
3555
3876
|
const rec = {
|
|
3556
3877
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -3601,7 +3922,7 @@ function openTranscriptFile(path, meta) {
|
|
|
3601
3922
|
return stream;
|
|
3602
3923
|
}
|
|
3603
3924
|
function readTranscript(path) {
|
|
3604
|
-
const raw =
|
|
3925
|
+
const raw = readFileSync6(path, "utf8");
|
|
3605
3926
|
return parseTranscript(raw);
|
|
3606
3927
|
}
|
|
3607
3928
|
function isPlanStateEmptyShape(s) {
|
|
@@ -4687,8 +5008,8 @@ async function trySection(load) {
|
|
|
4687
5008
|
}
|
|
4688
5009
|
|
|
4689
5010
|
// src/code/edit-blocks.ts
|
|
4690
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as
|
|
4691
|
-
import { dirname as
|
|
5011
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync7, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
5012
|
+
import { dirname as dirname5, resolve as resolve4 } from "path";
|
|
4692
5013
|
var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
|
|
4693
5014
|
function parseEditBlocks(text) {
|
|
4694
5015
|
const out = [];
|
|
@@ -4726,11 +5047,11 @@ function applyEditBlock(block, rootDir) {
|
|
|
4726
5047
|
message: "file does not exist; to create it, use an empty SEARCH block"
|
|
4727
5048
|
};
|
|
4728
5049
|
}
|
|
4729
|
-
mkdirSync3(
|
|
5050
|
+
mkdirSync3(dirname5(absTarget), { recursive: true });
|
|
4730
5051
|
writeFileSync3(absTarget, block.replace, "utf8");
|
|
4731
5052
|
return { path: block.path, status: "created" };
|
|
4732
5053
|
}
|
|
4733
|
-
const content =
|
|
5054
|
+
const content = readFileSync7(absTarget, "utf8");
|
|
4734
5055
|
if (searchEmpty) {
|
|
4735
5056
|
return {
|
|
4736
5057
|
path: block.path,
|
|
@@ -4769,7 +5090,7 @@ function snapshotBeforeEdits(blocks, rootDir) {
|
|
|
4769
5090
|
continue;
|
|
4770
5091
|
}
|
|
4771
5092
|
try {
|
|
4772
|
-
snapshots.push({ path: b.path, prevContent:
|
|
5093
|
+
snapshots.push({ path: b.path, prevContent: readFileSync7(abs, "utf8") });
|
|
4773
5094
|
} catch {
|
|
4774
5095
|
snapshots.push({ path: b.path, prevContent: null });
|
|
4775
5096
|
}
|
|
@@ -4812,25 +5133,25 @@ function sep() {
|
|
|
4812
5133
|
}
|
|
4813
5134
|
|
|
4814
5135
|
// src/version.ts
|
|
4815
|
-
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as
|
|
5136
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
|
|
4816
5137
|
import { homedir as homedir4 } from "os";
|
|
4817
|
-
import { dirname as
|
|
4818
|
-
import { fileURLToPath } from "url";
|
|
5138
|
+
import { dirname as dirname6, join as join6 } from "path";
|
|
5139
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4819
5140
|
var REGISTRY_URL = "https://registry.npmjs.org/reasonix/latest";
|
|
4820
5141
|
var LATEST_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
4821
5142
|
var LATEST_FETCH_TIMEOUT_MS = 2e3;
|
|
4822
5143
|
function readPackageVersion() {
|
|
4823
5144
|
try {
|
|
4824
|
-
let dir =
|
|
5145
|
+
let dir = dirname6(fileURLToPath2(import.meta.url));
|
|
4825
5146
|
for (let i = 0; i < 6; i++) {
|
|
4826
|
-
const p =
|
|
5147
|
+
const p = join6(dir, "package.json");
|
|
4827
5148
|
if (existsSync5(p)) {
|
|
4828
|
-
const pkg = JSON.parse(
|
|
5149
|
+
const pkg = JSON.parse(readFileSync8(p, "utf8"));
|
|
4829
5150
|
if (pkg?.name === "reasonix" && typeof pkg.version === "string") {
|
|
4830
5151
|
return pkg.version;
|
|
4831
5152
|
}
|
|
4832
5153
|
}
|
|
4833
|
-
const parent =
|
|
5154
|
+
const parent = dirname6(dir);
|
|
4834
5155
|
if (parent === dir) break;
|
|
4835
5156
|
dir = parent;
|
|
4836
5157
|
}
|
|
@@ -4840,11 +5161,11 @@ function readPackageVersion() {
|
|
|
4840
5161
|
}
|
|
4841
5162
|
var VERSION = readPackageVersion();
|
|
4842
5163
|
function cachePath(homeDirOverride) {
|
|
4843
|
-
return
|
|
5164
|
+
return join6(homeDirOverride ?? homedir4(), ".reasonix", "version-cache.json");
|
|
4844
5165
|
}
|
|
4845
5166
|
function readCache(homeDirOverride) {
|
|
4846
5167
|
try {
|
|
4847
|
-
const raw =
|
|
5168
|
+
const raw = readFileSync8(cachePath(homeDirOverride), "utf8");
|
|
4848
5169
|
const parsed = JSON.parse(raw);
|
|
4849
5170
|
if (parsed && typeof parsed.version === "string" && typeof parsed.checkedAt === "number") {
|
|
4850
5171
|
return parsed;
|
|
@@ -4856,7 +5177,7 @@ function readCache(homeDirOverride) {
|
|
|
4856
5177
|
function writeCache(entry, homeDirOverride) {
|
|
4857
5178
|
try {
|
|
4858
5179
|
const p = cachePath(homeDirOverride);
|
|
4859
|
-
mkdirSync4(
|
|
5180
|
+
mkdirSync4(dirname6(p), { recursive: true });
|
|
4860
5181
|
writeFileSync4(p, JSON.stringify(entry), "utf8");
|
|
4861
5182
|
} catch {
|
|
4862
5183
|
}
|
|
@@ -4864,8 +5185,8 @@ function writeCache(entry, homeDirOverride) {
|
|
|
4864
5185
|
async function getLatestVersion(opts = {}) {
|
|
4865
5186
|
const ttl = opts.ttlMs ?? LATEST_CACHE_TTL_MS;
|
|
4866
5187
|
if (!opts.force) {
|
|
4867
|
-
const
|
|
4868
|
-
if (
|
|
5188
|
+
const cached2 = readCache(opts.homeDir);
|
|
5189
|
+
if (cached2 && Date.now() - cached2.checkedAt < ttl) return cached2.version;
|
|
4869
5190
|
}
|
|
4870
5191
|
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
4871
5192
|
if (!fetchImpl) return null;
|
|
@@ -4913,11 +5234,11 @@ function isNpxInstall() {
|
|
|
4913
5234
|
}
|
|
4914
5235
|
|
|
4915
5236
|
// src/usage.ts
|
|
4916
|
-
import { appendFileSync as appendFileSync2, existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as
|
|
5237
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync9, statSync as statSync3 } from "fs";
|
|
4917
5238
|
import { homedir as homedir5 } from "os";
|
|
4918
|
-
import { dirname as
|
|
5239
|
+
import { dirname as dirname7, join as join7 } from "path";
|
|
4919
5240
|
function defaultUsageLogPath(homeDirOverride) {
|
|
4920
|
-
return
|
|
5241
|
+
return join7(homeDirOverride ?? homedir5(), ".reasonix", "usage.jsonl");
|
|
4921
5242
|
}
|
|
4922
5243
|
function appendUsage(input) {
|
|
4923
5244
|
const record = {
|
|
@@ -4933,7 +5254,7 @@ function appendUsage(input) {
|
|
|
4933
5254
|
};
|
|
4934
5255
|
const path = input.path ?? defaultUsageLogPath();
|
|
4935
5256
|
try {
|
|
4936
|
-
mkdirSync5(
|
|
5257
|
+
mkdirSync5(dirname7(path), { recursive: true });
|
|
4937
5258
|
appendFileSync2(path, `${JSON.stringify(record)}
|
|
4938
5259
|
`, "utf8");
|
|
4939
5260
|
} catch {
|
|
@@ -4944,7 +5265,7 @@ function readUsageLog(path = defaultUsageLogPath()) {
|
|
|
4944
5265
|
if (!existsSync6(path)) return [];
|
|
4945
5266
|
let raw;
|
|
4946
5267
|
try {
|
|
4947
|
-
raw =
|
|
5268
|
+
raw = readFileSync9(path, "utf8");
|
|
4948
5269
|
} catch {
|
|
4949
5270
|
return [];
|
|
4950
5271
|
}
|
|
@@ -5117,7 +5438,7 @@ ${skill.body}${argsBlock}`;
|
|
|
5117
5438
|
}
|
|
5118
5439
|
|
|
5119
5440
|
// src/cli/ui/EventLog.tsx
|
|
5120
|
-
import { Box as Box3, Text as Text3 } from "ink";
|
|
5441
|
+
import { Box as Box3, Text as Text3, useStdout } from "ink";
|
|
5121
5442
|
import React4 from "react";
|
|
5122
5443
|
|
|
5123
5444
|
// src/cli/ui/PlanStateBlock.tsx
|
|
@@ -5137,8 +5458,8 @@ function PlanStateBlock({ planState }) {
|
|
|
5137
5458
|
}
|
|
5138
5459
|
|
|
5139
5460
|
// src/cli/ui/markdown.tsx
|
|
5140
|
-
import { readFileSync as
|
|
5141
|
-
import { isAbsolute as isAbsolute3, join as
|
|
5461
|
+
import { readFileSync as readFileSync10, statSync as statSync4 } from "fs";
|
|
5462
|
+
import { isAbsolute as isAbsolute3, join as join8 } from "path";
|
|
5142
5463
|
import { Box as Box2, Text as Text2 } from "ink";
|
|
5143
5464
|
import React2 from "react";
|
|
5144
5465
|
var SUPERSCRIPT = {
|
|
@@ -5216,7 +5537,7 @@ function parseCitationUrl(url) {
|
|
|
5216
5537
|
function validateCitation(url, projectRoot) {
|
|
5217
5538
|
const parts = parseCitationUrl(url);
|
|
5218
5539
|
if (!parts || !parts.path) return { ok: false, reason: "empty path" };
|
|
5219
|
-
const fullPath = isAbsolute3(parts.path) ? parts.path :
|
|
5540
|
+
const fullPath = isAbsolute3(parts.path) ? parts.path : join8(projectRoot, parts.path);
|
|
5220
5541
|
let stat;
|
|
5221
5542
|
try {
|
|
5222
5543
|
stat = statSync4(fullPath);
|
|
@@ -5227,7 +5548,7 @@ function validateCitation(url, projectRoot) {
|
|
|
5227
5548
|
if (parts.startLine === void 0) return { ok: true };
|
|
5228
5549
|
let lineCount;
|
|
5229
5550
|
try {
|
|
5230
|
-
lineCount =
|
|
5551
|
+
lineCount = readFileSync10(fullPath, "utf8").split("\n").length;
|
|
5231
5552
|
} catch {
|
|
5232
5553
|
return { ok: false, reason: "unreadable" };
|
|
5233
5554
|
}
|
|
@@ -5277,7 +5598,7 @@ function InlineMd({
|
|
|
5277
5598
|
const status = citations?.get(url);
|
|
5278
5599
|
if (status && !status.ok) {
|
|
5279
5600
|
parts.push(
|
|
5280
|
-
/* @__PURE__ */ React2.createElement(Text2, { key: `l${idx++}`, color: "red", strikethrough: true }, `${linkText} \
|
|
5601
|
+
/* @__PURE__ */ React2.createElement(Text2, { key: `l${idx++}`, color: "red", strikethrough: true }, `${linkText} \u2717`)
|
|
5281
5602
|
);
|
|
5282
5603
|
} else {
|
|
5283
5604
|
parts.push(
|
|
@@ -5583,7 +5904,7 @@ function Markdown({ text, projectRoot }) {
|
|
|
5583
5904
|
function BrokenCitationsBlock({ items }) {
|
|
5584
5905
|
return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", borderStyle: "round", borderColor: "red", paddingX: 1 }, /* @__PURE__ */ React2.createElement(Text2, { color: "red", bold: true }, `\u26A0 ${items.length} broken citation${items.length > 1 ? "s" : ""} \u2014 the model referenced paths or lines that don't exist`), items.map((b, i) => (
|
|
5585
5906
|
// biome-ignore lint/suspicious/noArrayIndexKey: list is derived from a Map iteration order, stable per render
|
|
5586
|
-
/* @__PURE__ */ React2.createElement(Text2, { key: `bc-${i}`, color: "red" }, ` \
|
|
5907
|
+
/* @__PURE__ */ React2.createElement(Text2, { key: `bc-${i}`, color: "red" }, ` \u2717 ${b.url} \u2192 ${b.reason}`)
|
|
5587
5908
|
)));
|
|
5588
5909
|
}
|
|
5589
5910
|
|
|
@@ -5610,32 +5931,49 @@ function useElapsedSeconds() {
|
|
|
5610
5931
|
}
|
|
5611
5932
|
|
|
5612
5933
|
// src/cli/ui/EventLog.tsx
|
|
5934
|
+
var ROLE_GLYPH = {
|
|
5935
|
+
user: "\u25C7",
|
|
5936
|
+
assistant: "\u25C6",
|
|
5937
|
+
assistantPulse: "\u25C7",
|
|
5938
|
+
// pulse alternate for streaming state
|
|
5939
|
+
toolOk: "\u25A3",
|
|
5940
|
+
toolErr: "\u25A5",
|
|
5941
|
+
warning: "\u25B2",
|
|
5942
|
+
error: "\u2726"
|
|
5943
|
+
};
|
|
5944
|
+
function RoleGlyph({
|
|
5945
|
+
glyph,
|
|
5946
|
+
color
|
|
5947
|
+
}) {
|
|
5948
|
+
return /* @__PURE__ */ React4.createElement(Text3, { color, bold: true }, glyph);
|
|
5949
|
+
}
|
|
5613
5950
|
var EventRow = React4.memo(function EventRow2({
|
|
5614
5951
|
event,
|
|
5615
5952
|
projectRoot
|
|
5616
5953
|
}) {
|
|
5617
5954
|
if (event.role === "user") {
|
|
5618
|
-
return /* @__PURE__ */ React4.createElement(Box3,
|
|
5955
|
+
return /* @__PURE__ */ React4.createElement(Box3, { marginTop: event.leadSeparator ? 1 : 0 }, /* @__PURE__ */ React4.createElement(RoleGlyph, { glyph: ROLE_GLYPH.user, color: "cyan" }), /* @__PURE__ */ React4.createElement(Text3, null, " ", event.text));
|
|
5619
5956
|
}
|
|
5620
5957
|
if (event.role === "assistant") {
|
|
5621
5958
|
if (event.streaming) return /* @__PURE__ */ React4.createElement(StreamingAssistant, { event });
|
|
5622
|
-
return /* @__PURE__ */ React4.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(
|
|
5959
|
+
return /* @__PURE__ */ React4.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(RoleGlyph, { glyph: ROLE_GLYPH.assistant, color: "green" }), event.stats ? /* @__PURE__ */ React4.createElement(Text3, { dimColor: true }, ` ${event.stats.model}`) : null), /* @__PURE__ */ React4.createElement(Box3, { flexDirection: "column", paddingLeft: 2, marginTop: 1 }, event.branch ? /* @__PURE__ */ React4.createElement(BranchBlock, { branch: event.branch }) : null, event.reasoning ? /* @__PURE__ */ React4.createElement(ReasoningBlock, { reasoning: event.reasoning }) : null, !isPlanStateEmpty(event.planState) ? /* @__PURE__ */ React4.createElement(PlanStateBlock, { planState: event.planState }) : null, event.text ? /* @__PURE__ */ React4.createElement(Markdown, { text: event.text, projectRoot }) : /* @__PURE__ */ React4.createElement(Text3, { dimColor: true }, "(no content)"), event.stats ? /* @__PURE__ */ React4.createElement(StatsLine, { stats: event.stats }) : null, event.repair ? /* @__PURE__ */ React4.createElement(Text3, { color: "magenta" }, event.repair) : null));
|
|
5623
5960
|
}
|
|
5624
5961
|
if (event.role === "tool") {
|
|
5625
5962
|
const isError = event.text.startsWith("ERROR:");
|
|
5626
5963
|
const color = isError ? "red" : "yellow";
|
|
5964
|
+
const glyph = isError ? ROLE_GLYPH.toolErr : ROLE_GLYPH.toolOk;
|
|
5627
5965
|
const marker = isError ? "\u2717" : "\u2192";
|
|
5628
5966
|
const isEditFile = (event.toolName === "edit_file" || event.toolName?.endsWith("_edit_file")) && !isError;
|
|
5629
|
-
return /* @__PURE__ */ React4.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React4.createElement(Text3, { color }, `
|
|
5967
|
+
return /* @__PURE__ */ React4.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(RoleGlyph, { glyph, color }), /* @__PURE__ */ React4.createElement(Text3, { color, bold: true }, ` ${event.toolName ?? "?"}`), /* @__PURE__ */ React4.createElement(Text3, { color, dimColor: true }, ` ${marker}`)), /* @__PURE__ */ React4.createElement(Box3, { flexDirection: "column", paddingLeft: 2, marginTop: 1 }, isEditFile ? /* @__PURE__ */ React4.createElement(EditFileDiff, { text: event.text }) : /* @__PURE__ */ React4.createElement(Text3, { color: isError ? "red" : void 0, dimColor: !isError }, truncate2(event.text, 400))));
|
|
5630
5968
|
}
|
|
5631
5969
|
if (event.role === "error") {
|
|
5632
|
-
return /* @__PURE__ */ React4.createElement(Box3, { marginTop: 1 }, /* @__PURE__ */ React4.createElement(
|
|
5970
|
+
return /* @__PURE__ */ React4.createElement(Box3, { marginTop: 1 }, /* @__PURE__ */ React4.createElement(RoleGlyph, { glyph: ROLE_GLYPH.error, color: "red" }), /* @__PURE__ */ React4.createElement(Text3, { color: "red" }, " ", event.text));
|
|
5633
5971
|
}
|
|
5634
5972
|
if (event.role === "info") {
|
|
5635
5973
|
return /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(Text3, { dimColor: true }, event.text));
|
|
5636
5974
|
}
|
|
5637
5975
|
if (event.role === "warning") {
|
|
5638
|
-
return /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(
|
|
5976
|
+
return /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(RoleGlyph, { glyph: ROLE_GLYPH.warning, color: "yellow" }), /* @__PURE__ */ React4.createElement(Text3, { color: "yellow" }, " ", event.text));
|
|
5639
5977
|
}
|
|
5640
5978
|
return /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(Text3, null, event.text));
|
|
5641
5979
|
});
|
|
@@ -5659,13 +5997,13 @@ function BranchBlock({ branch }) {
|
|
|
5659
5997
|
const t = (branch.temperatures[i] ?? 0).toFixed(1);
|
|
5660
5998
|
return `${marker} #${i} T=${t} u=${u}`;
|
|
5661
5999
|
}).join(" ");
|
|
5662
|
-
return /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(Text3, { color: "blue" }, "\
|
|
6000
|
+
return /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(Text3, { color: "blue" }, "\u2387 branched ", /* @__PURE__ */ React4.createElement(Text3, { bold: true }, branch.budget), ` samples \u2192 picked #${branch.chosenIndex} `, /* @__PURE__ */ React4.createElement(Text3, { dimColor: true }, per)));
|
|
5663
6001
|
}
|
|
5664
6002
|
function ReasoningBlock({ reasoning }) {
|
|
5665
6003
|
const max = 260;
|
|
5666
6004
|
const flat = reasoning.replace(/\s+/g, " ").trim();
|
|
5667
6005
|
const preview = flat.length <= max ? flat : `\u2026 (+${flat.length - max} earlier chars) ${flat.slice(-max)}`;
|
|
5668
|
-
return /* @__PURE__ */ React4.createElement(Box3, { marginBottom: 1 }, /* @__PURE__ */ React4.createElement(Text3, { dimColor: true, italic: true }, "
|
|
6006
|
+
return /* @__PURE__ */ React4.createElement(Box3, { marginBottom: 1 }, /* @__PURE__ */ React4.createElement(Text3, { dimColor: true }, "\u258F "), /* @__PURE__ */ React4.createElement(Text3, { dimColor: true, italic: true }, "thinking ", preview));
|
|
5669
6007
|
}
|
|
5670
6008
|
function Elapsed() {
|
|
5671
6009
|
const s = useElapsedSeconds();
|
|
@@ -5673,14 +6011,19 @@ function Elapsed() {
|
|
|
5673
6011
|
const ss = String(s % 60).padStart(2, "0");
|
|
5674
6012
|
return /* @__PURE__ */ React4.createElement(Text3, { dimColor: true }, `${mm}:${ss}`);
|
|
5675
6013
|
}
|
|
6014
|
+
function PulsingAssistantGlyph() {
|
|
6015
|
+
const tick = useTick();
|
|
6016
|
+
const on = Math.floor(tick / 4) % 2 === 0;
|
|
6017
|
+
return /* @__PURE__ */ React4.createElement(Text3, { color: "green", bold: true }, on ? ROLE_GLYPH.assistant : ROLE_GLYPH.assistantPulse);
|
|
6018
|
+
}
|
|
5676
6019
|
function StreamingAssistant({ event }) {
|
|
5677
6020
|
if (event.branchProgress) {
|
|
5678
6021
|
const p = event.branchProgress;
|
|
5679
6022
|
if (p.completed === 0) {
|
|
5680
|
-
return /* @__PURE__ */ React4.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(
|
|
6023
|
+
return /* @__PURE__ */ React4.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(PulsingAssistantGlyph, null), /* @__PURE__ */ React4.createElement(Text3, { color: "blue" }, " \u2387 launching ", p.total, " parallel samples (R1 thinking in parallel)\u2026 "), /* @__PURE__ */ React4.createElement(Elapsed, null)), /* @__PURE__ */ React4.createElement(Text3, { color: "yellow" }, " ", "spread across T=0.0/0.5/1.0 \xB7 reasoner typically takes 30-90s \u2014 this is normal"));
|
|
5681
6024
|
}
|
|
5682
6025
|
const pct2 = Math.round(p.completed / p.total * 100);
|
|
5683
|
-
return /* @__PURE__ */ React4.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(
|
|
6026
|
+
return /* @__PURE__ */ React4.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(PulsingAssistantGlyph, null), /* @__PURE__ */ React4.createElement(Text3, { color: "blue" }, " \u2387 branching ", p.completed, "/", p.total, " (", pct2, "%) "), /* @__PURE__ */ React4.createElement(Elapsed, null)), /* @__PURE__ */ React4.createElement(Text3, { dimColor: true }, " latest #", p.latestIndex, " T=", p.latestTemperature.toFixed(1), " u=", p.latestUncertainties, p.completed < p.total ? " \xB7 waiting for other samples\u2026" : " \xB7 selecting winner\u2026"));
|
|
5684
6027
|
}
|
|
5685
6028
|
const tail = lastLine(event.text, 140);
|
|
5686
6029
|
const reasoningTail = event.reasoning ? lastLine(event.reasoning, 120) : "";
|
|
@@ -5708,7 +6051,11 @@ function StreamingAssistant({ event }) {
|
|
|
5708
6051
|
label = parts.join(" \xB7 ");
|
|
5709
6052
|
labelColor = "green";
|
|
5710
6053
|
}
|
|
5711
|
-
return /* @__PURE__ */ React4.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(
|
|
6054
|
+
return /* @__PURE__ */ React4.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(PulsingAssistantGlyph, null), /* @__PURE__ */ React4.createElement(Text3, null, " "), /* @__PURE__ */ React4.createElement(Pulse, null), /* @__PURE__ */ React4.createElement(Text3, { color: labelColor }, ` ${label} `), /* @__PURE__ */ React4.createElement(Elapsed, null)), reasoningTail ? /* @__PURE__ */ React4.createElement(Text3, { dimColor: true, italic: true }, "\u21B3 thinking: ", reasoningTail) : null, tail ? /* @__PURE__ */ React4.createElement(Text3, { dimColor: true }, "\u25B8 ", tail) : preFirstByte ? (
|
|
6055
|
+
// Non-dim yellow: first-time users misread the dim version as
|
|
6056
|
+
// "app frozen". The reassurance has to be VISIBLE to do its job.
|
|
6057
|
+
/* @__PURE__ */ React4.createElement(Text3, { color: "yellow", italic: true }, " waiting for first byte \u2014 this is normal, typically 5-60s depending on model + load")
|
|
6058
|
+
) : reasoningOnly ? /* @__PURE__ */ React4.createElement(Text3, { color: "yellow", italic: true }, " R1 is thinking before it speaks \u2014 body text arrives when reasoning finishes (typically 20-90s, this is normal)") : toolCallOnly ? /* @__PURE__ */ React4.createElement(Text3, { color: "magenta", italic: true }, " tool-call arguments streaming \u2014 the model is about to dispatch a tool") : event.reasoning ? /* @__PURE__ */ React4.createElement(Text3, { color: "yellow", italic: true }, " R1 still reasoning \u2014 body text or tool call arrives when thinking finishes") : null);
|
|
5712
6059
|
}
|
|
5713
6060
|
function Pulse() {
|
|
5714
6061
|
const tick = useTick();
|
|
@@ -5722,10 +6069,26 @@ function lastLine(s, maxChars) {
|
|
|
5722
6069
|
}
|
|
5723
6070
|
function StatsLine({ stats }) {
|
|
5724
6071
|
const hit = (stats.cacheHitRatio * 100).toFixed(1);
|
|
5725
|
-
return /* @__PURE__ */ React4.createElement(Text3, { dimColor: true }, "
|
|
6072
|
+
return /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(Text3, { dimColor: true }, "\u258F "), /* @__PURE__ */ React4.createElement(Text3, { dimColor: true }, "cache ", hit, "% \xB7 tokens ", stats.usage.promptTokens, " \u2192 ", stats.usage.completionTokens, " \xB7 $", stats.cost.toFixed(6)));
|
|
5726
6073
|
}
|
|
5727
6074
|
function truncate2(s, max) {
|
|
5728
|
-
|
|
6075
|
+
if (s.length <= max) return s;
|
|
6076
|
+
if (s.startsWith("ERROR:")) {
|
|
6077
|
+
const firstNl = s.indexOf("\n");
|
|
6078
|
+
const firstLine = firstNl === -1 ? s : s.slice(0, firstNl);
|
|
6079
|
+
if (firstLine.length >= max) {
|
|
6080
|
+
return `${firstLine.slice(0, max)}\u2026 (+${s.length - max} chars \u2014 /tool N for full)`;
|
|
6081
|
+
}
|
|
6082
|
+
const budget = max - firstLine.length - 10;
|
|
6083
|
+
const tail = s.slice(-budget);
|
|
6084
|
+
const skipped2 = s.length - firstLine.length - tail.length;
|
|
6085
|
+
return `${firstLine}
|
|
6086
|
+
\u2026 (+${skipped2} chars) \u2026
|
|
6087
|
+
${tail}`;
|
|
6088
|
+
}
|
|
6089
|
+
const skipped = s.length - max;
|
|
6090
|
+
return `\u2026 (+${skipped} earlier chars \u2014 /tool N for full) \u2026
|
|
6091
|
+
${s.slice(-max)}`;
|
|
5729
6092
|
}
|
|
5730
6093
|
|
|
5731
6094
|
// src/cli/ui/PlanConfirm.tsx
|
|
@@ -5739,7 +6102,8 @@ function SingleSelect({
|
|
|
5739
6102
|
items,
|
|
5740
6103
|
initialValue,
|
|
5741
6104
|
onSubmit,
|
|
5742
|
-
onCancel
|
|
6105
|
+
onCancel,
|
|
6106
|
+
footer
|
|
5743
6107
|
}) {
|
|
5744
6108
|
const initialIndex = Math.max(
|
|
5745
6109
|
0,
|
|
@@ -5766,7 +6130,7 @@ function SingleSelect({
|
|
|
5766
6130
|
active: i === index,
|
|
5767
6131
|
marker: i === index ? "\u25B8" : " "
|
|
5768
6132
|
}
|
|
5769
|
-
)));
|
|
6133
|
+
)), footer ? /* @__PURE__ */ React5.createElement(Box4, { marginTop: 1 }, /* @__PURE__ */ React5.createElement(Text4, { dimColor: true }, footer)) : null);
|
|
5770
6134
|
}
|
|
5771
6135
|
function MultiSelect({
|
|
5772
6136
|
items,
|
|
@@ -5842,7 +6206,7 @@ function PlanConfirm({ plan, onChoose, maxRenderedChars, projectRoot }) {
|
|
|
5842
6206
|
|
|
5843
6207
|
\u2026 (${plan.length - cap} chars truncated \u2014 use /tool to view the full proposal)` : plan;
|
|
5844
6208
|
const hasOpenQuestions = /^#{1,6}\s*(open[-\s]?questions?|risks?|unknowns?|assumptions?|unclear)/im.test(plan) || /^#{1,6}\s*(待确认|开放问题|风险|未知|假设|不确定)/im.test(plan);
|
|
5845
|
-
return /* @__PURE__ */ React6.createElement(Box5, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React6.createElement(Box5, null, /* @__PURE__ */ React6.createElement(Text5, { bold: true, color: "cyan" }, "\u25B8 plan submitted \u2014 awaiting your review")), /* @__PURE__ */ React6.createElement(Box5, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React6.createElement(Markdown, { text: visible, projectRoot })), hasOpenQuestions ? /* @__PURE__ */ React6.createElement(Box5, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text5, { color: "yellow" }, "\u25B2 the plan has open questions or flagged risks \u2014 pick", " ", /* @__PURE__ */ React6.createElement(Text5, { bold: true }, "Refine / answer questions"), " to write concrete answers before the model moves on.")) : null, /* @__PURE__ */ React6.createElement(Box5, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(
|
|
6209
|
+
return /* @__PURE__ */ React6.createElement(Box5, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React6.createElement(Box5, null, /* @__PURE__ */ React6.createElement(Text5, { bold: true, color: "cyan" }, "\u25B8 plan submitted \u2014 awaiting your review")), /* @__PURE__ */ React6.createElement(Box5, null, /* @__PURE__ */ React6.createElement(Text5, { color: "cyan", dimColor: true }, "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")), /* @__PURE__ */ React6.createElement(Box5, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React6.createElement(Markdown, { text: visible, projectRoot })), hasOpenQuestions ? /* @__PURE__ */ React6.createElement(Box5, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text5, { color: "yellow" }, "\u25B2 the plan has open questions or flagged risks \u2014 pick", " ", /* @__PURE__ */ React6.createElement(Text5, { bold: true }, "Refine / answer questions"), " to write concrete answers before the model moves on.")) : null, /* @__PURE__ */ React6.createElement(Box5, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(
|
|
5846
6210
|
SingleSelect,
|
|
5847
6211
|
{
|
|
5848
6212
|
initialValue: hasOpenQuestions ? "refine" : "approve",
|
|
@@ -5863,7 +6227,9 @@ function PlanConfirm({ plan, onChoose, maxRenderedChars, projectRoot }) {
|
|
|
5863
6227
|
hint: "Exit plan mode. Drop the plan; the model won't implement it."
|
|
5864
6228
|
}
|
|
5865
6229
|
],
|
|
5866
|
-
onSubmit: (v) => onChoose(v)
|
|
6230
|
+
onSubmit: (v) => onChoose(v),
|
|
6231
|
+
onCancel: () => onChoose("cancel"),
|
|
6232
|
+
footer: "[\u2191\u2193] navigate \xB7 [Enter] select \xB7 [Esc] cancel"
|
|
5867
6233
|
}
|
|
5868
6234
|
)));
|
|
5869
6235
|
}
|
|
@@ -6054,7 +6420,7 @@ function PromptInput({
|
|
|
6054
6420
|
},
|
|
6055
6421
|
{ isActive: !disabled }
|
|
6056
6422
|
);
|
|
6057
|
-
const effectivePlaceholder = disabled ? placeholder ?? "\u2026waiting for response\u2026" : placeholder ?? "type a message, or /command
|
|
6423
|
+
const effectivePlaceholder = disabled ? placeholder ?? "\u2026waiting for response\u2026 \xB7 [Esc] to stop" : placeholder ?? "type a message, or /command \xB7 [Shift+Enter] / [Ctrl+J] newline";
|
|
6058
6424
|
const lines = value.length > 0 ? value.split("\n") : [""];
|
|
6059
6425
|
const borderColor = disabled ? "gray" : "cyan";
|
|
6060
6426
|
const { line: cursorLine, col: cursorCol } = lineAndColumn(value, cursor);
|
|
@@ -6095,7 +6461,7 @@ function LineWithCursor({
|
|
|
6095
6461
|
import { Box as Box8, Text as Text8 } from "ink";
|
|
6096
6462
|
import React9 from "react";
|
|
6097
6463
|
function ShellConfirm({ command, allowPrefix, onChoose }) {
|
|
6098
|
-
return /* @__PURE__ */ React9.createElement(Box8, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React9.createElement(Box8, null, /* @__PURE__ */ React9.createElement(Text8, { bold: true, color: "yellow" }, "\u25B8 model wants to run a shell command")), /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text8, null, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, "$ "), /* @__PURE__ */ React9.createElement(Text8, { color: "cyan" }, command))), /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(
|
|
6464
|
+
return /* @__PURE__ */ React9.createElement(Box8, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React9.createElement(Box8, null, /* @__PURE__ */ React9.createElement(Text8, { bold: true, color: "yellow" }, "\u25B8 model wants to run a shell command")), /* @__PURE__ */ React9.createElement(Box8, null, /* @__PURE__ */ React9.createElement(Text8, { color: "yellow", dimColor: true }, "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")), /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text8, null, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, "$ "), /* @__PURE__ */ React9.createElement(Text8, { color: "cyan" }, command))), /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(
|
|
6099
6465
|
SingleSelect,
|
|
6100
6466
|
{
|
|
6101
6467
|
initialValue: "run_once",
|
|
@@ -6116,7 +6482,9 @@ function ShellConfirm({ command, allowPrefix, onChoose }) {
|
|
|
6116
6482
|
hint: "Tell the model the user refused; it will continue without this command."
|
|
6117
6483
|
}
|
|
6118
6484
|
],
|
|
6119
|
-
onSubmit: (v) => onChoose(v)
|
|
6485
|
+
onSubmit: (v) => onChoose(v),
|
|
6486
|
+
onCancel: () => onChoose("deny"),
|
|
6487
|
+
footer: "[\u2191\u2193] navigate \xB7 [Enter] select \xB7 [Esc] deny"
|
|
6120
6488
|
}
|
|
6121
6489
|
)));
|
|
6122
6490
|
}
|
|
@@ -6166,7 +6534,7 @@ function SlashSuggestions({
|
|
|
6166
6534
|
const shown = matches.slice(windowStart, windowStart + MAX);
|
|
6167
6535
|
const hiddenAbove = windowStart;
|
|
6168
6536
|
const hiddenBelow = total - windowStart - shown.length;
|
|
6169
|
-
return /* @__PURE__ */ React10.createElement(Box9, { flexDirection: "column", paddingX: 1 }, hiddenAbove > 0 ? /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, " \u2191 ", hiddenAbove, " more above") : null, shown.map((spec, i) => /* @__PURE__ */ React10.createElement(SuggestionRow, { key: spec.cmd, spec, isSelected: windowStart + i === selectedIndex })), hiddenBelow > 0 ? /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, " \u2193 ", hiddenBelow, " more below") : null, /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, " \u2191
|
|
6537
|
+
return /* @__PURE__ */ React10.createElement(Box9, { flexDirection: "column", paddingX: 1 }, hiddenAbove > 0 ? /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, " \u2191 ", hiddenAbove, " more above") : null, shown.map((spec, i) => /* @__PURE__ */ React10.createElement(SuggestionRow, { key: spec.cmd, spec, isSelected: windowStart + i === selectedIndex })), hiddenBelow > 0 ? /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, " \u2193 ", hiddenBelow, " more below") : null, /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, " [\u2191\u2193] navigate \xB7 [Tab]/[Enter] pick"));
|
|
6170
6538
|
}
|
|
6171
6539
|
function SuggestionRow({ spec, isSelected }) {
|
|
6172
6540
|
const marker = isSelected ? "\u25B8" : " ";
|
|
@@ -6179,8 +6547,37 @@ function SuggestionRow({ spec, isSelected }) {
|
|
|
6179
6547
|
}
|
|
6180
6548
|
|
|
6181
6549
|
// src/cli/ui/StatsPanel.tsx
|
|
6182
|
-
import { Box as Box10, Text as Text10 } from "ink";
|
|
6550
|
+
import { Box as Box10, Text as Text10, useStdout as useStdout2 } from "ink";
|
|
6183
6551
|
import React11 from "react";
|
|
6552
|
+
var WORDMARK_STYLES = [
|
|
6553
|
+
{ ch: "\u25C8", color: "#5eead4", isLogo: true },
|
|
6554
|
+
// teal — brand mark
|
|
6555
|
+
{ ch: " ", color: "#5eead4", isLogo: false },
|
|
6556
|
+
{ ch: "R", color: "#67e8f9", isLogo: false },
|
|
6557
|
+
// cyan
|
|
6558
|
+
{ ch: "E", color: "#7dd3fc", isLogo: false },
|
|
6559
|
+
// sky
|
|
6560
|
+
{ ch: "A", color: "#93c5fd", isLogo: false },
|
|
6561
|
+
// blue
|
|
6562
|
+
{ ch: "S", color: "#a5b4fc", isLogo: false },
|
|
6563
|
+
// indigo
|
|
6564
|
+
{ ch: "O", color: "#c4b5fd", isLogo: false },
|
|
6565
|
+
// violet
|
|
6566
|
+
{ ch: "N", color: "#d8b4fe", isLogo: false },
|
|
6567
|
+
// purple
|
|
6568
|
+
{ ch: "I", color: "#f0abfc", isLogo: false },
|
|
6569
|
+
// fuchsia
|
|
6570
|
+
{ ch: "X", color: "#f0abfc", isLogo: false }
|
|
6571
|
+
// fuchsia
|
|
6572
|
+
];
|
|
6573
|
+
function Wordmark({ busy }) {
|
|
6574
|
+
const tick = useTick();
|
|
6575
|
+
const period = busy ? 5 : 12;
|
|
6576
|
+
const bright = Math.floor(tick / period) % 2 === 0;
|
|
6577
|
+
return /* @__PURE__ */ React11.createElement(Text10, null, WORDMARK_STYLES.map((c) => /* @__PURE__ */ React11.createElement(Text10, { key: `${c.ch}-${c.color}`, color: c.color, bold: c.isLogo ? bright : true }, c.ch)));
|
|
6578
|
+
}
|
|
6579
|
+
var NARROW_BREAKPOINT = 120;
|
|
6580
|
+
var COLD_START_TURNS = 3;
|
|
6184
6581
|
function StatsPanel({
|
|
6185
6582
|
summary,
|
|
6186
6583
|
model,
|
|
@@ -6189,27 +6586,149 @@ function StatsPanel({
|
|
|
6189
6586
|
branchBudget,
|
|
6190
6587
|
planMode,
|
|
6191
6588
|
balance,
|
|
6192
|
-
updateAvailable
|
|
6589
|
+
updateAvailable,
|
|
6590
|
+
busy
|
|
6193
6591
|
}) {
|
|
6194
|
-
const hitPct = (summary.cacheHitRatio * 100).toFixed(1);
|
|
6195
|
-
const hitColor = summary.cacheHitRatio >= 0.7 ? "green" : summary.cacheHitRatio >= 0.4 ? "yellow" : "red";
|
|
6196
6592
|
const branchOn = (branchBudget ?? 1) > 1;
|
|
6197
6593
|
const ctxMax = DEEPSEEK_CONTEXT_TOKENS[model] ?? DEFAULT_CONTEXT_TOKENS;
|
|
6198
6594
|
const ctxRatio = summary.lastPromptTokens / ctxMax;
|
|
6199
|
-
const
|
|
6200
|
-
|
|
6595
|
+
const { stdout: stdout2 } = useStdout2();
|
|
6596
|
+
const columns = stdout2?.columns ?? 80;
|
|
6597
|
+
const narrow = columns < NARROW_BREAKPOINT;
|
|
6598
|
+
const coldStart = summary.turns <= COLD_START_TURNS;
|
|
6599
|
+
return /* @__PURE__ */ React11.createElement(Box10, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React11.createElement(
|
|
6600
|
+
Header,
|
|
6601
|
+
{
|
|
6602
|
+
model,
|
|
6603
|
+
prefixHash,
|
|
6604
|
+
harvestOn,
|
|
6605
|
+
branchOn,
|
|
6606
|
+
branchBudget: branchBudget ?? 1,
|
|
6607
|
+
planMode,
|
|
6608
|
+
turns: summary.turns,
|
|
6609
|
+
updateAvailable,
|
|
6610
|
+
narrow,
|
|
6611
|
+
busy: busy ?? false
|
|
6612
|
+
}
|
|
6613
|
+
), narrow ? /* @__PURE__ */ React11.createElement(
|
|
6614
|
+
StackedMetrics,
|
|
6615
|
+
{
|
|
6616
|
+
summary,
|
|
6617
|
+
ctxRatio,
|
|
6618
|
+
ctxMax,
|
|
6619
|
+
balance,
|
|
6620
|
+
coldStart
|
|
6621
|
+
}
|
|
6622
|
+
) : /* @__PURE__ */ React11.createElement(
|
|
6623
|
+
InlineMetrics,
|
|
6624
|
+
{
|
|
6625
|
+
summary,
|
|
6626
|
+
ctxRatio,
|
|
6627
|
+
ctxMax,
|
|
6628
|
+
balance,
|
|
6629
|
+
coldStart
|
|
6630
|
+
}
|
|
6631
|
+
));
|
|
6632
|
+
}
|
|
6633
|
+
function Header({
|
|
6634
|
+
model,
|
|
6635
|
+
prefixHash,
|
|
6636
|
+
harvestOn,
|
|
6637
|
+
branchOn,
|
|
6638
|
+
branchBudget,
|
|
6639
|
+
planMode,
|
|
6640
|
+
turns,
|
|
6641
|
+
updateAvailable,
|
|
6642
|
+
narrow,
|
|
6643
|
+
busy
|
|
6644
|
+
}) {
|
|
6645
|
+
return /* @__PURE__ */ React11.createElement(Box10, { justifyContent: "space-between" }, /* @__PURE__ */ React11.createElement(Box10, null, /* @__PURE__ */ React11.createElement(Wordmark, { busy }), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, ` v${VERSION}`), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, " \xB7 "), /* @__PURE__ */ React11.createElement(Text10, { color: "yellow" }, model), narrow ? null : /* @__PURE__ */ React11.createElement(React11.Fragment, null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, " \xB7 "), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, prefixHash)), harvestOn ? /* @__PURE__ */ React11.createElement(Text10, { color: "magenta" }, " \xB7 harvest") : null, branchOn ? /* @__PURE__ */ React11.createElement(Text10, { color: "blue" }, " \xB7 branch", branchBudget) : null, planMode ? /* @__PURE__ */ React11.createElement(Text10, { color: "red", bold: true }, " \xB7 PLAN") : null), /* @__PURE__ */ React11.createElement(Text10, null, updateAvailable ? /* @__PURE__ */ React11.createElement(Text10, { color: "yellow", bold: true }, `update: ${updateAvailable} \xB7 `) : null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, narrow ? `turn ${turns}` : `turn ${turns} \xB7 /help`)));
|
|
6646
|
+
}
|
|
6647
|
+
function InlineMetrics({
|
|
6648
|
+
summary,
|
|
6649
|
+
ctxRatio,
|
|
6650
|
+
ctxMax,
|
|
6651
|
+
balance,
|
|
6652
|
+
coldStart
|
|
6653
|
+
}) {
|
|
6654
|
+
return /* @__PURE__ */ React11.createElement(Box10, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React11.createElement(ContextCell, { ratio: ctxRatio, promptTokens: summary.lastPromptTokens, ctxMax }), /* @__PURE__ */ React11.createElement(CacheCell, { hitRatio: summary.cacheHitRatio, coldStart, turns: summary.turns }), /* @__PURE__ */ React11.createElement(CostCell, { summary, coldStart }), balance ? /* @__PURE__ */ React11.createElement(BalanceCell, { balance }) : null);
|
|
6655
|
+
}
|
|
6656
|
+
function StackedMetrics({
|
|
6657
|
+
summary,
|
|
6658
|
+
ctxRatio,
|
|
6659
|
+
ctxMax,
|
|
6660
|
+
balance,
|
|
6661
|
+
coldStart
|
|
6662
|
+
}) {
|
|
6663
|
+
return /* @__PURE__ */ React11.createElement(Box10, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React11.createElement(
|
|
6664
|
+
ContextCell,
|
|
6665
|
+
{
|
|
6666
|
+
ratio: ctxRatio,
|
|
6667
|
+
promptTokens: summary.lastPromptTokens,
|
|
6668
|
+
ctxMax,
|
|
6669
|
+
showBar: true
|
|
6670
|
+
}
|
|
6671
|
+
), balance ? /* @__PURE__ */ React11.createElement(BalanceCell, { balance }) : null, /* @__PURE__ */ React11.createElement(CacheCell, { hitRatio: summary.cacheHitRatio, coldStart, turns: summary.turns }), /* @__PURE__ */ React11.createElement(CostCell, { summary, coldStart }));
|
|
6672
|
+
}
|
|
6673
|
+
function ContextCell({
|
|
6674
|
+
ratio,
|
|
6675
|
+
promptTokens,
|
|
6676
|
+
ctxMax,
|
|
6677
|
+
showBar
|
|
6678
|
+
}) {
|
|
6679
|
+
if (promptTokens === 0) {
|
|
6680
|
+
return /* @__PURE__ */ React11.createElement(Text10, null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "ctx "), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "\u2014 (no turns yet)"));
|
|
6681
|
+
}
|
|
6682
|
+
const color = ratio >= 0.8 ? "red" : ratio >= 0.6 ? "yellow" : "green";
|
|
6683
|
+
const pct2 = Math.round(ratio * 100);
|
|
6684
|
+
return /* @__PURE__ */ React11.createElement(Text10, null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "ctx "), showBar ? /* @__PURE__ */ React11.createElement(Bar, { ratio, color }) : null, showBar ? /* @__PURE__ */ React11.createElement(Text10, null, " ") : null, /* @__PURE__ */ React11.createElement(Text10, { color, bold: true }, formatTokens(promptTokens), "/", formatTokens(ctxMax)), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, " (", pct2, "%)"), ratio >= 0.8 ? /* @__PURE__ */ React11.createElement(Text10, { color: "red", bold: true }, " \xB7 /compact") : null);
|
|
6685
|
+
}
|
|
6686
|
+
function CacheCell({
|
|
6687
|
+
hitRatio,
|
|
6688
|
+
coldStart,
|
|
6689
|
+
turns
|
|
6690
|
+
}) {
|
|
6691
|
+
const pct2 = (hitRatio * 100).toFixed(1);
|
|
6692
|
+
if (turns === 0) {
|
|
6693
|
+
return /* @__PURE__ */ React11.createElement(Text10, null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "cache "), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "\u2014"));
|
|
6694
|
+
}
|
|
6695
|
+
if (coldStart) {
|
|
6696
|
+
return /* @__PURE__ */ React11.createElement(Text10, null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "cache "), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, pct2, "% "), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true, italic: true }, "(cold start)"));
|
|
6697
|
+
}
|
|
6698
|
+
const color = hitRatio >= 0.7 ? "green" : hitRatio >= 0.4 ? "yellow" : "red";
|
|
6699
|
+
return /* @__PURE__ */ React11.createElement(Text10, null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "cache "), /* @__PURE__ */ React11.createElement(Text10, { color, bold: true }, pct2, "%"));
|
|
6700
|
+
}
|
|
6701
|
+
function CostCell({
|
|
6702
|
+
summary,
|
|
6703
|
+
coldStart
|
|
6704
|
+
}) {
|
|
6705
|
+
if (summary.turns === 0) {
|
|
6706
|
+
return /* @__PURE__ */ React11.createElement(Text10, null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "cost "), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "\u2014"));
|
|
6707
|
+
}
|
|
6708
|
+
const primaryColor = coldStart ? void 0 : "green";
|
|
6709
|
+
return /* @__PURE__ */ React11.createElement(Text10, null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "cost "), /* @__PURE__ */ React11.createElement(Text10, { color: primaryColor, bold: !coldStart, dimColor: coldStart }, "$", summary.totalCostUsd.toFixed(6)), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, " (in ", "$", summary.totalInputCostUsd.toFixed(6), " \xB7 out ", "$", summary.totalOutputCostUsd.toFixed(6), ")"));
|
|
6710
|
+
}
|
|
6711
|
+
function BalanceCell({ balance }) {
|
|
6712
|
+
const color = balance.total < 1 ? "red" : balance.total < 5 ? "yellow" : "green";
|
|
6713
|
+
return /* @__PURE__ */ React11.createElement(Text10, null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "balance "), /* @__PURE__ */ React11.createElement(Text10, { color, bold: true }, balance.currency === "USD" ? "$" : "", balance.total.toFixed(2), balance.currency !== "USD" ? ` ${balance.currency}` : ""));
|
|
6714
|
+
}
|
|
6715
|
+
function Bar({ ratio, color }) {
|
|
6716
|
+
const cells = 10;
|
|
6717
|
+
const filled = Math.max(0, Math.min(cells, Math.round(ratio * cells)));
|
|
6718
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(cells - filled);
|
|
6719
|
+
return /* @__PURE__ */ React11.createElement(Text10, { color }, bar);
|
|
6201
6720
|
}
|
|
6202
6721
|
function formatTokens(n) {
|
|
6203
|
-
if (n <
|
|
6204
|
-
const k = n /
|
|
6205
|
-
return k >= 100 ? `${k.toFixed(0)}
|
|
6722
|
+
if (n < 1024) return String(n);
|
|
6723
|
+
const k = n / 1024;
|
|
6724
|
+
return k >= 100 ? `${k.toFixed(0)}K` : `${k.toFixed(1)}K`;
|
|
6206
6725
|
}
|
|
6207
6726
|
|
|
6208
6727
|
// src/cli/ui/slash.ts
|
|
6209
6728
|
import { spawnSync } from "child_process";
|
|
6210
6729
|
|
|
6211
6730
|
// src/cli/commands/stats.ts
|
|
6212
|
-
import { existsSync as existsSync7, readFileSync as
|
|
6731
|
+
import { existsSync as existsSync7, readFileSync as readFileSync11 } from "fs";
|
|
6213
6732
|
function statsCommand(opts) {
|
|
6214
6733
|
if (opts.transcript) {
|
|
6215
6734
|
transcriptSummary(opts.transcript);
|
|
@@ -6222,7 +6741,7 @@ function transcriptSummary(path) {
|
|
|
6222
6741
|
console.error(`no such transcript: ${path}`);
|
|
6223
6742
|
process.exit(1);
|
|
6224
6743
|
}
|
|
6225
|
-
const lines =
|
|
6744
|
+
const lines = readFileSync11(path, "utf8").split(/\r?\n/).filter(Boolean);
|
|
6226
6745
|
let assistantTurns = 0;
|
|
6227
6746
|
let toolCalls = 0;
|
|
6228
6747
|
let lastTurn = 0;
|
|
@@ -6324,6 +6843,7 @@ var SLASH_COMMANDS = [
|
|
|
6324
6843
|
summary: "one-tap model + harvest + branch bundle"
|
|
6325
6844
|
},
|
|
6326
6845
|
{ cmd: "model", argsHint: "<id>", summary: "switch DeepSeek model id" },
|
|
6846
|
+
{ cmd: "models", summary: "list available models fetched from DeepSeek /models" },
|
|
6327
6847
|
{ cmd: "harvest", argsHint: "[on|off]", summary: "toggle Pillar-2 plan-state extraction" },
|
|
6328
6848
|
{ cmd: "branch", argsHint: "<N|off>", summary: "run N parallel samples per turn (N>=2)" },
|
|
6329
6849
|
{ cmd: "mcp", summary: "list MCP servers + tools attached to this session" },
|
|
@@ -6352,6 +6872,10 @@ var SLASH_COMMANDS = [
|
|
|
6352
6872
|
summary: "cross-session cost dashboard (today / week / month / all-time \xB7 cache hit \xB7 vs Claude)"
|
|
6353
6873
|
},
|
|
6354
6874
|
{ cmd: "think", summary: "dump the last turn's full R1 reasoning (reasoner only)" },
|
|
6875
|
+
{
|
|
6876
|
+
cmd: "context",
|
|
6877
|
+
summary: "break down where context tokens are going: system / tools / per-turn log"
|
|
6878
|
+
},
|
|
6355
6879
|
{ cmd: "retry", summary: "truncate & resend your last message (fresh sample)" },
|
|
6356
6880
|
{ cmd: "compact", argsHint: "[cap]", summary: "shrink oversized tool results in the log" },
|
|
6357
6881
|
{ cmd: "sessions", summary: "list saved sessions (current marked with \u25B8)" },
|
|
@@ -6696,6 +7220,66 @@ ${entry.text}`
|
|
|
6696
7220
|
info: ok ? `\u25B8 deleted session "${name}" \u2014 current screen still shows the conversation, but next launch starts fresh` : `could not delete session "${name}" (already gone?)`
|
|
6697
7221
|
};
|
|
6698
7222
|
}
|
|
7223
|
+
case "context": {
|
|
7224
|
+
const systemTokens = countTokens(loop.prefix.system);
|
|
7225
|
+
const toolsTokens = countTokens(JSON.stringify(loop.prefix.toolSpecs));
|
|
7226
|
+
const entries = loop.log.toMessages();
|
|
7227
|
+
let userTokens = 0;
|
|
7228
|
+
let assistantTokens = 0;
|
|
7229
|
+
let toolResultTokens = 0;
|
|
7230
|
+
let toolCallTokens = 0;
|
|
7231
|
+
const toolBreakdown = [];
|
|
7232
|
+
let logTurn = 0;
|
|
7233
|
+
for (const e of entries) {
|
|
7234
|
+
const content = typeof e.content === "string" ? e.content : "";
|
|
7235
|
+
if (e.role === "user") {
|
|
7236
|
+
userTokens += countTokens(content);
|
|
7237
|
+
logTurn += 1;
|
|
7238
|
+
} else if (e.role === "assistant") {
|
|
7239
|
+
assistantTokens += countTokens(content);
|
|
7240
|
+
if (Array.isArray(e.tool_calls) && e.tool_calls.length > 0) {
|
|
7241
|
+
toolCallTokens += countTokens(JSON.stringify(e.tool_calls));
|
|
7242
|
+
}
|
|
7243
|
+
} else if (e.role === "tool") {
|
|
7244
|
+
const n = countTokens(content);
|
|
7245
|
+
toolResultTokens += n;
|
|
7246
|
+
toolBreakdown.push({ name: e.name ?? "?", tokens: n, turn: logTurn });
|
|
7247
|
+
}
|
|
7248
|
+
}
|
|
7249
|
+
const logTokens = userTokens + assistantTokens + toolResultTokens + toolCallTokens;
|
|
7250
|
+
const total = systemTokens + toolsTokens + logTokens;
|
|
7251
|
+
const ctxMax = DEEPSEEK_CONTEXT_TOKENS[loop.model] ?? DEFAULT_CONTEXT_TOKENS;
|
|
7252
|
+
const pct2 = (n) => total > 0 ? `${Math.round(n / total * 100)}%`.padStart(4) : " 0%";
|
|
7253
|
+
const row2 = (label, n, note = "") => ` ${label.padEnd(20)}${compactNum(n).padStart(8)} tokens ${pct2(n)}${note ? ` ${note}` : ""}`;
|
|
7254
|
+
const lines = [
|
|
7255
|
+
`Next-request estimate: ~${compactNum(total)} tokens of ${compactNum(ctxMax)} (${Math.round(
|
|
7256
|
+
total / ctxMax * 100
|
|
7257
|
+
)}% of window)`,
|
|
7258
|
+
"",
|
|
7259
|
+
row2("system prompt", systemTokens),
|
|
7260
|
+
row2("tool specs", toolsTokens, `(${loop.prefix.toolSpecs.length} tools)`),
|
|
7261
|
+
row2("log (all turns)", logTokens, `(${entries.length} messages)`),
|
|
7262
|
+
` user ${compactNum(userTokens).padStart(8)} tokens`,
|
|
7263
|
+
` assistant ${compactNum(assistantTokens).padStart(8)} tokens`,
|
|
7264
|
+
` tool-call args ${compactNum(toolCallTokens).padStart(8)} tokens`,
|
|
7265
|
+
` tool results ${compactNum(toolResultTokens).padStart(8)} tokens`
|
|
7266
|
+
];
|
|
7267
|
+
if (toolBreakdown.length > 0) {
|
|
7268
|
+
const top = [...toolBreakdown].sort((a, b) => b.tokens - a.tokens).slice(0, 5);
|
|
7269
|
+
lines.push("");
|
|
7270
|
+
lines.push(`Top tool results by cost (of ${toolBreakdown.length} total):`);
|
|
7271
|
+
for (const t of top) {
|
|
7272
|
+
lines.push(
|
|
7273
|
+
` turn ${String(t.turn).padStart(3)} ${t.name.padEnd(22)} ${compactNum(t.tokens).padStart(8)} tokens`
|
|
7274
|
+
);
|
|
7275
|
+
}
|
|
7276
|
+
}
|
|
7277
|
+
lines.push("");
|
|
7278
|
+
lines.push(
|
|
7279
|
+
"Count is a local estimate (DeepSeek V3 tokenizer, pure-TS port); server prompt_tokens may add ~3-6% for chat-template role markers."
|
|
7280
|
+
);
|
|
7281
|
+
return { info: lines.join("\n") };
|
|
7282
|
+
}
|
|
6699
7283
|
case "status": {
|
|
6700
7284
|
const branchBudget = loop.branchOptions.budget ?? 1;
|
|
6701
7285
|
const ctxMax = DEEPSEEK_CONTEXT_TOKENS[loop.model] ?? DEFAULT_CONTEXT_TOKENS;
|
|
@@ -6722,10 +7306,44 @@ ${entry.text}`
|
|
|
6722
7306
|
}
|
|
6723
7307
|
case "model": {
|
|
6724
7308
|
const id = args[0];
|
|
6725
|
-
|
|
7309
|
+
const known = ctx.models ?? null;
|
|
7310
|
+
if (!id) {
|
|
7311
|
+
const hint = known && known.length > 0 ? known.join(" | ") : "try deepseek-chat or deepseek-reasoner \u2014 run /models to fetch the live list";
|
|
7312
|
+
return { info: `usage: /model <id> (${hint})` };
|
|
7313
|
+
}
|
|
6726
7314
|
loop.configure({ model: id });
|
|
7315
|
+
if (known && known.length > 0 && !known.includes(id)) {
|
|
7316
|
+
return {
|
|
7317
|
+
info: `model \u2192 ${id} (\u26A0 not in the fetched catalog: ${known.join(", ")}. If this is wrong the next call will 400 \u2014 run /models to refresh.)`
|
|
7318
|
+
};
|
|
7319
|
+
}
|
|
6727
7320
|
return { info: `model \u2192 ${id}` };
|
|
6728
7321
|
}
|
|
7322
|
+
case "models": {
|
|
7323
|
+
const list = ctx.models ?? null;
|
|
7324
|
+
if (list === null) {
|
|
7325
|
+
ctx.refreshModels?.();
|
|
7326
|
+
return {
|
|
7327
|
+
info: "fetching /models from DeepSeek\u2026 run /models again in a moment. If it stays empty, your API key may lack permission or the network is blocked."
|
|
7328
|
+
};
|
|
7329
|
+
}
|
|
7330
|
+
if (list.length === 0) {
|
|
7331
|
+
return {
|
|
7332
|
+
info: "DeepSeek /models returned an empty list. Try /models again, or check your account status at api-docs.deepseek.com."
|
|
7333
|
+
};
|
|
7334
|
+
}
|
|
7335
|
+
const current = loop.model;
|
|
7336
|
+
const lines = list.map((id) => id === current ? `\u25B8 ${id} (current)` : ` ${id}`);
|
|
7337
|
+
return {
|
|
7338
|
+
info: [
|
|
7339
|
+
`Available models (DeepSeek /models \xB7 ${list.length} total):`,
|
|
7340
|
+
"",
|
|
7341
|
+
...lines,
|
|
7342
|
+
"",
|
|
7343
|
+
"Switch with: /model <id>"
|
|
7344
|
+
].join("\n")
|
|
7345
|
+
};
|
|
7346
|
+
}
|
|
6729
7347
|
case "harvest": {
|
|
6730
7348
|
const arg = (args[0] ?? "").toLowerCase();
|
|
6731
7349
|
const on = arg === "" ? !loop.harvestEnabled : arg === "on" || arg === "true" || arg === "1";
|
|
@@ -7137,9 +7755,9 @@ function formatToolList(history) {
|
|
|
7137
7755
|
return lines.join("\n");
|
|
7138
7756
|
}
|
|
7139
7757
|
function compactNum(n) {
|
|
7140
|
-
if (n <
|
|
7141
|
-
const k = n /
|
|
7142
|
-
return k >= 100 ? `${Math.round(k)}
|
|
7758
|
+
if (n < 1024) return String(n);
|
|
7759
|
+
const k = n / 1024;
|
|
7760
|
+
return k >= 100 ? `${Math.round(k)}K` : `${k.toFixed(1)}K`;
|
|
7143
7761
|
}
|
|
7144
7762
|
function stripOuterQuotes(s) {
|
|
7145
7763
|
if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) {
|
|
@@ -7201,6 +7819,7 @@ function App({
|
|
|
7201
7819
|
const [subagentActivity, setSubagentActivity] = useState5(null);
|
|
7202
7820
|
const [statusLine, setStatusLine] = useState5(null);
|
|
7203
7821
|
const [balance, setBalance] = useState5(null);
|
|
7822
|
+
const [models, setModels] = useState5(null);
|
|
7204
7823
|
const [latestVersion, setLatestVersion] = useState5(null);
|
|
7205
7824
|
const updateAvailable = latestVersion && compareVersions(VERSION, latestVersion) < 0 ? latestVersion : null;
|
|
7206
7825
|
const [hookList, setHookList] = useState5(
|
|
@@ -7312,6 +7931,17 @@ function App({
|
|
|
7312
7931
|
cancelled = true;
|
|
7313
7932
|
};
|
|
7314
7933
|
}, [loop]);
|
|
7934
|
+
useEffect2(() => {
|
|
7935
|
+
let cancelled = false;
|
|
7936
|
+
void (async () => {
|
|
7937
|
+
const list = await loop.client.listModels().catch(() => null);
|
|
7938
|
+
if (cancelled || !list) return;
|
|
7939
|
+
setModels(list.data.map((m) => m.id));
|
|
7940
|
+
})();
|
|
7941
|
+
return () => {
|
|
7942
|
+
cancelled = true;
|
|
7943
|
+
};
|
|
7944
|
+
}, [loop]);
|
|
7315
7945
|
useEffect2(() => {
|
|
7316
7946
|
let cancelled = false;
|
|
7317
7947
|
void (async () => {
|
|
@@ -7356,7 +7986,7 @@ function App({
|
|
|
7356
7986
|
}
|
|
7357
7987
|
setSubagentActivity(null);
|
|
7358
7988
|
const seconds = ((ev.elapsedMs ?? 0) / 1e3).toFixed(1);
|
|
7359
|
-
const summary2 = ev.error ? `\
|
|
7989
|
+
const summary2 = ev.error ? `\u232C subagent "${ev.task}" failed after ${seconds}s \xB7 ${ev.iter ?? 0} tool call(s) \u2014 ${ev.error}` : `\u232C subagent "${ev.task}" done in ${seconds}s \xB7 ${ev.iter ?? 0} tool call(s) \xB7 ${ev.turns ?? 0} turn(s)`;
|
|
7360
7990
|
setHistorical((prev) => [
|
|
7361
7991
|
...prev,
|
|
7362
7992
|
{
|
|
@@ -7540,6 +8170,13 @@ function App({
|
|
|
7540
8170
|
const fresh = await getLatestVersion({ force: true });
|
|
7541
8171
|
if (fresh) setLatestVersion(fresh);
|
|
7542
8172
|
})();
|
|
8173
|
+
},
|
|
8174
|
+
models,
|
|
8175
|
+
refreshModels: () => {
|
|
8176
|
+
void (async () => {
|
|
8177
|
+
const list = await loop.client.listModels().catch(() => null);
|
|
8178
|
+
if (list) setModels(list.data.map((m) => m.id));
|
|
8179
|
+
})();
|
|
7543
8180
|
}
|
|
7544
8181
|
});
|
|
7545
8182
|
if (result.exit) {
|
|
@@ -7596,7 +8233,14 @@ function App({
|
|
|
7596
8233
|
if (promptReport.blocked) return;
|
|
7597
8234
|
}
|
|
7598
8235
|
promptHistory.current.push(text);
|
|
7599
|
-
setHistorical((prev) => [
|
|
8236
|
+
setHistorical((prev) => [
|
|
8237
|
+
...prev,
|
|
8238
|
+
// `leadSeparator`: thin rule above this user turn when history
|
|
8239
|
+
// isn't empty — visual pacing for multi-turn sessions. First
|
|
8240
|
+
// user message leaves it off so the UI doesn't open with a
|
|
8241
|
+
// dangling divider.
|
|
8242
|
+
{ id: `u-${Date.now()}`, role: "user", text, leadSeparator: prev.length > 0 }
|
|
8243
|
+
]);
|
|
7600
8244
|
const assistantId = `a-${Date.now()}`;
|
|
7601
8245
|
const streamRef = { id: assistantId, text: "", reasoning: "" };
|
|
7602
8246
|
const contentBuf = { current: "" };
|
|
@@ -7815,6 +8459,7 @@ function App({
|
|
|
7815
8459
|
latestVersion,
|
|
7816
8460
|
mcpSpecs,
|
|
7817
8461
|
mcpServers,
|
|
8462
|
+
models,
|
|
7818
8463
|
planMode,
|
|
7819
8464
|
session,
|
|
7820
8465
|
slashSelected,
|
|
@@ -7978,6 +8623,7 @@ Stay in plan mode \u2014 address the feedback (explore more if needed), then sub
|
|
|
7978
8623
|
branchBudget: loop.branchOptions.budget,
|
|
7979
8624
|
planMode,
|
|
7980
8625
|
balance,
|
|
8626
|
+
busy,
|
|
7981
8627
|
updateAvailable
|
|
7982
8628
|
}
|
|
7983
8629
|
), /* @__PURE__ */ React12.createElement(Static, { items: historical }, (item) => /* @__PURE__ */ React12.createElement(EventRow, { key: item.id, event: item, projectRoot: hookCwd })), !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && streaming ? /* @__PURE__ */ React12.createElement(Box11, { marginY: 1 }, /* @__PURE__ */ React12.createElement(EventRow, { event: streaming, projectRoot: hookCwd })) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && ongoingTool ? /* @__PURE__ */ React12.createElement(OngoingToolRow, { tool: ongoingTool, progress: toolProgress }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && subagentActivity ? /* @__PURE__ */ React12.createElement(SubagentRow, { activity: subagentActivity }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && !ongoingTool && statusLine ? /* @__PURE__ */ React12.createElement(StatusRow, { text: statusLine }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && busy && !streaming && !ongoingTool && !statusLine ? /* @__PURE__ */ React12.createElement(StatusRow, { text: "processing\u2026" }) : null, stagedInput ? /* @__PURE__ */ React12.createElement(
|
|
@@ -8015,7 +8661,7 @@ function SubagentRow({
|
|
|
8015
8661
|
}) {
|
|
8016
8662
|
const tick = useTick();
|
|
8017
8663
|
const seconds = (activity.elapsedMs / 1e3).toFixed(1);
|
|
8018
|
-
return /* @__PURE__ */ React12.createElement(Box11, { paddingLeft: 2 }, /* @__PURE__ */ React12.createElement(Text11, { color: "magenta" }, SPINNER_FRAMES[tick % SPINNER_FRAMES.length]), /* @__PURE__ */ React12.createElement(Text11, { color: "magenta" }, ` \
|
|
8664
|
+
return /* @__PURE__ */ React12.createElement(Box11, { paddingLeft: 2 }, /* @__PURE__ */ React12.createElement(Text11, { color: "magenta" }, SPINNER_FRAMES[tick % SPINNER_FRAMES.length]), /* @__PURE__ */ React12.createElement(Text11, { color: "magenta" }, ` \u232C subagent: ${activity.task}`), /* @__PURE__ */ React12.createElement(Text11, { dimColor: true }, ` \xB7 iter ${activity.iter} \xB7 ${seconds}s`));
|
|
8019
8665
|
}
|
|
8020
8666
|
function OngoingToolRow({
|
|
8021
8667
|
tool,
|