reasonix 0.4.1 → 0.4.5
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/dist/cli/index.js +368 -29
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +119 -5
- package/dist/index.js +157 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -722,7 +722,19 @@ function scavengeToolCalls(reasoningContent, opts) {
|
|
|
722
722
|
const max = opts.maxCalls ?? 4;
|
|
723
723
|
const notes = [];
|
|
724
724
|
const out = [];
|
|
725
|
-
for (const
|
|
725
|
+
for (const invoke of iterateDsmlInvokes(reasoningContent)) {
|
|
726
|
+
if (out.length >= max) break;
|
|
727
|
+
if (!opts.allowedNames.has(invoke.name)) continue;
|
|
728
|
+
out.push({
|
|
729
|
+
function: {
|
|
730
|
+
name: invoke.name,
|
|
731
|
+
arguments: JSON.stringify(invoke.args)
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
notes.push(`scavenged DSML call: ${invoke.name}`);
|
|
735
|
+
}
|
|
736
|
+
const nonDsml = stripDsmlBlocks(reasoningContent);
|
|
737
|
+
for (const candidate of iterateJsonObjects(nonDsml)) {
|
|
726
738
|
if (out.length >= max) break;
|
|
727
739
|
const call = coerceToToolCall(candidate, opts.allowedNames);
|
|
728
740
|
if (call) {
|
|
@@ -732,6 +744,40 @@ function scavengeToolCalls(reasoningContent, opts) {
|
|
|
732
744
|
}
|
|
733
745
|
return { calls: out, notes };
|
|
734
746
|
}
|
|
747
|
+
function stripDsmlBlocks(text) {
|
|
748
|
+
let out = text;
|
|
749
|
+
out = out.replace(/<[||]DSML[||]function_calls>[\s\S]*?<\/?[||]DSML[||]function_calls>/g, "");
|
|
750
|
+
out = out.replace(/<[||]DSML[||]invoke\s+[^>]*>[\s\S]*?<\/[||]DSML[||]invoke>/g, "");
|
|
751
|
+
return out;
|
|
752
|
+
}
|
|
753
|
+
function* iterateDsmlInvokes(text) {
|
|
754
|
+
const INVOKE_RE = /<[||]DSML[||]invoke\s+name="([^"]+)">([\s\S]*?)<\/[||]DSML[||]invoke>/g;
|
|
755
|
+
for (const match of text.matchAll(INVOKE_RE)) {
|
|
756
|
+
const name = match[1];
|
|
757
|
+
const body = match[2];
|
|
758
|
+
if (!name || body === void 0) continue;
|
|
759
|
+
yield { name, args: parseDsmlParameters(body) };
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
function parseDsmlParameters(body) {
|
|
763
|
+
const PARAM_RE = /<[||]DSML[||]parameter\s+name="([^"]+)"(?:\s+string="(true|false)")?\s*>([\s\S]*?)<\/[||]DSML[||]parameter>/g;
|
|
764
|
+
const args = {};
|
|
765
|
+
for (const m of body.matchAll(PARAM_RE)) {
|
|
766
|
+
const key = m[1];
|
|
767
|
+
const stringFlag = m[2];
|
|
768
|
+
const raw = (m[3] ?? "").trim();
|
|
769
|
+
if (!key) continue;
|
|
770
|
+
if (stringFlag === "false") {
|
|
771
|
+
try {
|
|
772
|
+
args[key] = JSON.parse(raw);
|
|
773
|
+
continue;
|
|
774
|
+
} catch {
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
args[key] = raw;
|
|
778
|
+
}
|
|
779
|
+
return args;
|
|
780
|
+
}
|
|
735
781
|
function* iterateJsonObjects(text) {
|
|
736
782
|
for (let i = 0; i < text.length; i++) {
|
|
737
783
|
if (text[i] !== "{") continue;
|
|
@@ -917,14 +963,15 @@ var ToolCallRepair = class {
|
|
|
917
963
|
this.opts = opts;
|
|
918
964
|
this.storm = new StormBreaker(opts.stormWindow ?? 6, opts.stormThreshold ?? 3);
|
|
919
965
|
}
|
|
920
|
-
process(declaredCalls, reasoningContent) {
|
|
966
|
+
process(declaredCalls, reasoningContent, content = null) {
|
|
921
967
|
const report = {
|
|
922
968
|
scavenged: 0,
|
|
923
969
|
truncationsFixed: 0,
|
|
924
970
|
stormsBroken: 0,
|
|
925
971
|
notes: []
|
|
926
972
|
};
|
|
927
|
-
const
|
|
973
|
+
const combined = [reasoningContent ?? "", content ?? ""].filter(Boolean).join("\n");
|
|
974
|
+
const scavenged = scavengeToolCalls(combined || null, {
|
|
928
975
|
allowedNames: this.opts.allowedToolNames,
|
|
929
976
|
maxCalls: this.opts.maxScavenge ?? 4
|
|
930
977
|
});
|
|
@@ -1283,6 +1330,39 @@ var CacheFirstLoop = class {
|
|
|
1283
1330
|
abort() {
|
|
1284
1331
|
this._aborted = true;
|
|
1285
1332
|
}
|
|
1333
|
+
/**
|
|
1334
|
+
* Drop everything in the log after (and including) the most recent
|
|
1335
|
+
* user message. Used by `/retry` so the caller can re-send that
|
|
1336
|
+
* message with a fresh turn instead of layering another response on
|
|
1337
|
+
* top of the prior exchange. Returns the content of the dropped user
|
|
1338
|
+
* message, or `null` if there isn't one yet.
|
|
1339
|
+
*
|
|
1340
|
+
* Persists by rewriting the session file — otherwise the next
|
|
1341
|
+
* launch would rehydrate the old exchange and `/retry` would seem
|
|
1342
|
+
* to have done nothing.
|
|
1343
|
+
*/
|
|
1344
|
+
retryLastUser() {
|
|
1345
|
+
const entries = this.log.entries;
|
|
1346
|
+
let lastUserIdx = -1;
|
|
1347
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
1348
|
+
if (entries[i].role === "user") {
|
|
1349
|
+
lastUserIdx = i;
|
|
1350
|
+
break;
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
if (lastUserIdx < 0) return null;
|
|
1354
|
+
const raw = entries[lastUserIdx].content;
|
|
1355
|
+
const userText = typeof raw === "string" ? raw : "";
|
|
1356
|
+
const preserved = entries.slice(0, lastUserIdx).map((m) => ({ ...m }));
|
|
1357
|
+
this.log.compactInPlace(preserved);
|
|
1358
|
+
if (this.sessionName) {
|
|
1359
|
+
try {
|
|
1360
|
+
rewriteSession(this.sessionName, preserved);
|
|
1361
|
+
} catch {
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
return userText;
|
|
1365
|
+
}
|
|
1286
1366
|
async *step(userInput) {
|
|
1287
1367
|
this._turn++;
|
|
1288
1368
|
this.scratch.reset();
|
|
@@ -1296,9 +1376,17 @@ var CacheFirstLoop = class {
|
|
|
1296
1376
|
yield {
|
|
1297
1377
|
turn: this._turn,
|
|
1298
1378
|
role: "warning",
|
|
1299
|
-
content: `aborted at iter ${iter}/${this.maxToolIters} \u2014
|
|
1379
|
+
content: `aborted at iter ${iter}/${this.maxToolIters} \u2014 stopped without producing a summary (press \u2191 + Enter or /retry to resume)`
|
|
1380
|
+
};
|
|
1381
|
+
const stoppedMsg = "[aborted by user (Esc) \u2014 no summary produced. Ask again or /retry when ready; prior tool output is still in the log.]";
|
|
1382
|
+
this.appendAndPersist({ role: "assistant", content: stoppedMsg });
|
|
1383
|
+
yield {
|
|
1384
|
+
turn: this._turn,
|
|
1385
|
+
role: "assistant_final",
|
|
1386
|
+
content: stoppedMsg,
|
|
1387
|
+
forcedSummary: true
|
|
1300
1388
|
};
|
|
1301
|
-
yield
|
|
1389
|
+
yield { turn: this._turn, role: "done", content: stoppedMsg };
|
|
1302
1390
|
return;
|
|
1303
1391
|
}
|
|
1304
1392
|
if (!warnedForIterBudget && iter >= warnAt) {
|
|
@@ -1461,7 +1549,8 @@ var CacheFirstLoop = class {
|
|
|
1461
1549
|
const planState = preHarvestedPlanState ? preHarvestedPlanState : this.harvestEnabled ? await harvest(reasoningContent || null, this.client, this.harvestOptions) : emptyPlanState();
|
|
1462
1550
|
const { calls: repairedCalls, report } = this.repair.process(
|
|
1463
1551
|
toolCalls,
|
|
1464
|
-
reasoningContent || null
|
|
1552
|
+
reasoningContent || null,
|
|
1553
|
+
assistantContent || null
|
|
1465
1554
|
);
|
|
1466
1555
|
this.appendAndPersist(this.assistantMessage(assistantContent, repairedCalls));
|
|
1467
1556
|
yield {
|
|
@@ -1520,12 +1609,18 @@ var CacheFirstLoop = class {
|
|
|
1520
1609
|
async *forceSummaryAfterIterLimit(opts = { reason: "budget" }) {
|
|
1521
1610
|
try {
|
|
1522
1611
|
const messages = this.buildMessages(null);
|
|
1612
|
+
messages.push({
|
|
1613
|
+
role: "user",
|
|
1614
|
+
content: "I'm out of tool-call budget for this turn. Summarize in plain prose what you learned from the tool results above. Do NOT emit any tool calls, function-call markup, DSML invocations, or SEARCH/REPLACE edit blocks \u2014 they will be silently discarded. Just plain text."
|
|
1615
|
+
});
|
|
1523
1616
|
const resp = await this.client.chat({
|
|
1524
1617
|
model: this.model,
|
|
1525
1618
|
messages
|
|
1526
1619
|
// no tools → model is forced to answer in text
|
|
1527
1620
|
});
|
|
1528
|
-
const
|
|
1621
|
+
const rawContent = resp.content?.trim() ?? "";
|
|
1622
|
+
const cleaned = stripHallucinatedToolMarkup(rawContent);
|
|
1623
|
+
const summary = cleaned || "(model emitted fake tool-call markup instead of a prose summary \u2014 try /retry with a narrower question, or /think to inspect R1's reasoning)";
|
|
1529
1624
|
const reasonPrefix = reasonPrefixFor(opts.reason, this.maxToolIters);
|
|
1530
1625
|
const annotated = `${reasonPrefix}
|
|
1531
1626
|
|
|
@@ -1566,6 +1661,14 @@ ${summary}`;
|
|
|
1566
1661
|
return msg;
|
|
1567
1662
|
}
|
|
1568
1663
|
};
|
|
1664
|
+
function stripHallucinatedToolMarkup(s) {
|
|
1665
|
+
let out = s;
|
|
1666
|
+
out = out.replace(/<|DSML|function_calls>[\s\S]*?<\/?|DSML|function_calls>/g, "");
|
|
1667
|
+
out = out.replace(/<\|DSML\|function_calls>[\s\S]*?<\/?\|DSML\|function_calls>/g, "");
|
|
1668
|
+
out = out.replace(/<function_calls>[\s\S]*?<\/function_calls>/g, "");
|
|
1669
|
+
out = out.replace(/<|DSML|[\s\S]*$/g, "");
|
|
1670
|
+
return out.trim();
|
|
1671
|
+
}
|
|
1569
1672
|
function reasonPrefixFor(reason, iterCap) {
|
|
1570
1673
|
if (reason === "aborted") return "[aborted by user (Esc) \u2014 summarizing what I found so far]";
|
|
1571
1674
|
if (reason === "context-guard") {
|
|
@@ -2191,7 +2294,12 @@ var McpClient = class {
|
|
|
2191
2294
|
this.startReaderIfNeeded();
|
|
2192
2295
|
const result = await this.request("initialize", {
|
|
2193
2296
|
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
2194
|
-
|
|
2297
|
+
// Advertise every method the client can consume so servers know
|
|
2298
|
+
// they can send listChanged notifications etc. Sub-feature flags
|
|
2299
|
+
// (e.g. `resources.subscribe`) are omitted — we don't implement
|
|
2300
|
+
// those yet and the empty object means "method-level support, no
|
|
2301
|
+
// sub-features."
|
|
2302
|
+
capabilities: { tools: {}, resources: {}, prompts: {} },
|
|
2195
2303
|
clientInfo: this.clientInfo
|
|
2196
2304
|
});
|
|
2197
2305
|
this._serverCapabilities = result.capabilities ?? {};
|
|
@@ -2215,6 +2323,45 @@ var McpClient = class {
|
|
|
2215
2323
|
arguments: args ?? {}
|
|
2216
2324
|
});
|
|
2217
2325
|
}
|
|
2326
|
+
/**
|
|
2327
|
+
* List resources the server exposes. Supports a pagination cursor;
|
|
2328
|
+
* callers interested in the full set should loop on `nextCursor`.
|
|
2329
|
+
* Servers that don't support resources respond with method-not-found
|
|
2330
|
+
* (−32601) — we surface that as a thrown Error so callers can gate
|
|
2331
|
+
* on the `serverCapabilities.resources` field first.
|
|
2332
|
+
*/
|
|
2333
|
+
async listResources(cursor) {
|
|
2334
|
+
this.assertInitialized();
|
|
2335
|
+
return this.request("resources/list", {
|
|
2336
|
+
...cursor ? { cursor } : {}
|
|
2337
|
+
});
|
|
2338
|
+
}
|
|
2339
|
+
/** Read the contents of a resource by URI. */
|
|
2340
|
+
async readResource(uri) {
|
|
2341
|
+
this.assertInitialized();
|
|
2342
|
+
return this.request("resources/read", {
|
|
2343
|
+
uri
|
|
2344
|
+
});
|
|
2345
|
+
}
|
|
2346
|
+
/** List prompt templates the server exposes. */
|
|
2347
|
+
async listPrompts(cursor) {
|
|
2348
|
+
this.assertInitialized();
|
|
2349
|
+
return this.request("prompts/list", {
|
|
2350
|
+
...cursor ? { cursor } : {}
|
|
2351
|
+
});
|
|
2352
|
+
}
|
|
2353
|
+
/**
|
|
2354
|
+
* Fetch a rendered prompt by name. `args` supplies values for any
|
|
2355
|
+
* required template arguments; the server validates. Returns messages
|
|
2356
|
+
* ready to prepend to the model's input.
|
|
2357
|
+
*/
|
|
2358
|
+
async getPrompt(name, args) {
|
|
2359
|
+
this.assertInitialized();
|
|
2360
|
+
return this.request("prompts/get", {
|
|
2361
|
+
name,
|
|
2362
|
+
...args ? { arguments: args } : {}
|
|
2363
|
+
});
|
|
2364
|
+
}
|
|
2218
2365
|
/** Close the transport and reject any outstanding requests. */
|
|
2219
2366
|
async close() {
|
|
2220
2367
|
for (const [, pending] of this.pending) {
|
|
@@ -2729,7 +2876,7 @@ function sep() {
|
|
|
2729
2876
|
}
|
|
2730
2877
|
|
|
2731
2878
|
// src/index.ts
|
|
2732
|
-
var VERSION = "0.4.
|
|
2879
|
+
var VERSION = "0.4.3";
|
|
2733
2880
|
|
|
2734
2881
|
// src/cli/commands/chat.tsx
|
|
2735
2882
|
import { render } from "ink";
|
|
@@ -2998,7 +3145,10 @@ var EventRow = React3.memo(function EventRow2({ event }) {
|
|
|
2998
3145
|
return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Text3, { bold: true, color: "green" }, "assistant")), event.branch ? /* @__PURE__ */ React3.createElement(BranchBlock, { branch: event.branch }) : null, event.reasoning ? /* @__PURE__ */ React3.createElement(ReasoningBlock, { reasoning: event.reasoning }) : null, !isPlanStateEmpty(event.planState) ? /* @__PURE__ */ React3.createElement(PlanStateBlock, { planState: event.planState }) : null, event.text ? /* @__PURE__ */ React3.createElement(Markdown, { text: event.text }) : /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, "(no content)"), event.stats ? /* @__PURE__ */ React3.createElement(StatsLine, { stats: event.stats }) : null, event.repair ? /* @__PURE__ */ React3.createElement(Text3, { color: "magenta" }, event.repair) : null);
|
|
2999
3146
|
}
|
|
3000
3147
|
if (event.role === "tool") {
|
|
3001
|
-
|
|
3148
|
+
const isError = event.text.startsWith("ERROR:");
|
|
3149
|
+
const color = isError ? "red" : "yellow";
|
|
3150
|
+
const marker = isError ? "\u2717" : "\u2192";
|
|
3151
|
+
return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React3.createElement(Text3, { color }, `tool<${event.toolName ?? "?"}> ${marker}`), /* @__PURE__ */ React3.createElement(Text3, { color: isError ? "red" : void 0, dimColor: !isError }, " ", truncate2(event.text, 400)));
|
|
3002
3152
|
}
|
|
3003
3153
|
if (event.role === "error") {
|
|
3004
3154
|
return /* @__PURE__ */ React3.createElement(Box3, { marginTop: 1 }, /* @__PURE__ */ React3.createElement(Text3, { color: "red", bold: true }, "error", " "), /* @__PURE__ */ React3.createElement(Text3, { color: "red" }, event.text));
|
|
@@ -3020,9 +3170,9 @@ function BranchBlock({ branch }) {
|
|
|
3020
3170
|
return /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Text3, { color: "blue" }, "\u{1F500} branched ", /* @__PURE__ */ React3.createElement(Text3, { bold: true }, branch.budget), ` samples \u2192 picked #${branch.chosenIndex} `, /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, per)));
|
|
3021
3171
|
}
|
|
3022
3172
|
function ReasoningBlock({ reasoning }) {
|
|
3023
|
-
const max =
|
|
3173
|
+
const max = 260;
|
|
3024
3174
|
const flat = reasoning.replace(/\s+/g, " ").trim();
|
|
3025
|
-
const preview = flat.length <= max ? flat :
|
|
3175
|
+
const preview = flat.length <= max ? flat : `\u2026 (+${flat.length - max} earlier chars) ${flat.slice(-max)}`;
|
|
3026
3176
|
return /* @__PURE__ */ React3.createElement(Box3, { marginBottom: 1 }, /* @__PURE__ */ React3.createElement(Text3, { dimColor: true, italic: true }, "\u21B3 thinking: ", preview));
|
|
3027
3177
|
}
|
|
3028
3178
|
function Elapsed() {
|
|
@@ -3150,6 +3300,9 @@ function handleSlash(cmd, args, loop, ctx = {}) {
|
|
|
3150
3300
|
" /mcp list MCP servers + tools attached to this session",
|
|
3151
3301
|
" /setup (exit + reconfigure) \u2192 run `reasonix setup`",
|
|
3152
3302
|
" /compact [cap] shrink large tool results in history (default 4k/result)",
|
|
3303
|
+
" /think dump the most recent turn's full R1 reasoning (reasoner only)",
|
|
3304
|
+
" /tool [N] list tool calls (or dump full output of #N, 1=most recent)",
|
|
3305
|
+
" /retry truncate & resend your last message (fresh sample from the model)",
|
|
3153
3306
|
" /apply (code mode) commit the pending edit blocks to disk",
|
|
3154
3307
|
" /discard (code mode) drop pending edits without writing",
|
|
3155
3308
|
" /undo (code mode) roll back the last applied edit batch",
|
|
@@ -3195,6 +3348,63 @@ function handleSlash(cmd, args, loop, ctx = {}) {
|
|
|
3195
3348
|
return {
|
|
3196
3349
|
info: "To reconfigure (preset, MCP servers, API key), exit this chat and run `reasonix setup`. Changes take effect on next launch."
|
|
3197
3350
|
};
|
|
3351
|
+
case "retry": {
|
|
3352
|
+
const prev = loop.retryLastUser();
|
|
3353
|
+
if (!prev) {
|
|
3354
|
+
return {
|
|
3355
|
+
info: "nothing to retry \u2014 no prior user message in this session's log."
|
|
3356
|
+
};
|
|
3357
|
+
}
|
|
3358
|
+
const preview = prev.length > 80 ? `${prev.slice(0, 80)}\u2026` : prev;
|
|
3359
|
+
return {
|
|
3360
|
+
info: `\u25B8 retrying: "${preview}"`,
|
|
3361
|
+
resubmit: prev
|
|
3362
|
+
};
|
|
3363
|
+
}
|
|
3364
|
+
case "think":
|
|
3365
|
+
case "reasoning": {
|
|
3366
|
+
const raw = loop.scratch.reasoning;
|
|
3367
|
+
if (!raw || !raw.trim()) {
|
|
3368
|
+
return {
|
|
3369
|
+
info: "no reasoning cached. `/think` shows the full R1 thought for the most recent turn \u2014 only `deepseek-reasoner` produces it, and only once the turn completes."
|
|
3370
|
+
};
|
|
3371
|
+
}
|
|
3372
|
+
return { info: `\u21B3 full thinking (${raw.length} chars):
|
|
3373
|
+
|
|
3374
|
+
${raw.trim()}` };
|
|
3375
|
+
}
|
|
3376
|
+
case "tool": {
|
|
3377
|
+
const history = ctx.toolHistory?.() ?? [];
|
|
3378
|
+
if (history.length === 0) {
|
|
3379
|
+
return {
|
|
3380
|
+
info: "no tool calls yet in this session. `/tool` lists them once the model has actually used a tool; `/tool N` dumps the full (untruncated) output of the Nth-most-recent."
|
|
3381
|
+
};
|
|
3382
|
+
}
|
|
3383
|
+
const raw = (args[0] ?? "").toLowerCase();
|
|
3384
|
+
if (raw === "" || raw === "list" || raw === "ls") {
|
|
3385
|
+
return { info: formatToolList(history) };
|
|
3386
|
+
}
|
|
3387
|
+
const n = Number.parseInt(raw, 10);
|
|
3388
|
+
if (!Number.isFinite(n) || n < 1) {
|
|
3389
|
+
return {
|
|
3390
|
+
info: "usage: /tool [N] (no arg \u2192 list; N=1 \u2192 most recent result in full, N=2 \u2192 previous, \u2026)"
|
|
3391
|
+
};
|
|
3392
|
+
}
|
|
3393
|
+
if (n > history.length) {
|
|
3394
|
+
return {
|
|
3395
|
+
info: `only ${history.length} tool call(s) in history \u2014 asked for #${n}. Try /tool with no arg to see the list.`
|
|
3396
|
+
};
|
|
3397
|
+
}
|
|
3398
|
+
const entry = history[history.length - n];
|
|
3399
|
+
if (!entry) {
|
|
3400
|
+
return { info: `could not read tool call #${n}` };
|
|
3401
|
+
}
|
|
3402
|
+
return {
|
|
3403
|
+
info: `\u21B3 tool<${entry.toolName}> #${n} (${entry.text.length} chars):
|
|
3404
|
+
|
|
3405
|
+
${entry.text}`
|
|
3406
|
+
};
|
|
3407
|
+
}
|
|
3198
3408
|
case "undo": {
|
|
3199
3409
|
if (!ctx.codeUndo) {
|
|
3200
3410
|
return {
|
|
@@ -3279,9 +3489,25 @@ function handleSlash(cmd, args, loop, ctx = {}) {
|
|
|
3279
3489
|
}
|
|
3280
3490
|
case "status": {
|
|
3281
3491
|
const branchBudget = loop.branchOptions.budget ?? 1;
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3492
|
+
const ctxMax = DEEPSEEK_CONTEXT_TOKENS[loop.model] ?? DEFAULT_CONTEXT_TOKENS;
|
|
3493
|
+
const lastPromptTokens = loop.stats.summary().lastPromptTokens;
|
|
3494
|
+
const ctxPct = ctxMax > 0 ? Math.round(lastPromptTokens / ctxMax * 100) : 0;
|
|
3495
|
+
const ctxLine = lastPromptTokens > 0 ? ` ctx ${compactNum(lastPromptTokens)}/${compactNum(ctxMax)} (${ctxPct}%)` : " ctx no turns yet";
|
|
3496
|
+
const pending = ctx.pendingEditCount ?? 0;
|
|
3497
|
+
const sessionLine = loop.sessionName ? ` session "${loop.sessionName}" \xB7 ${loop.log.length} messages in log (resumed ${loop.resumedMessageCount})` : " session (ephemeral \u2014 no persistence)";
|
|
3498
|
+
const mcpCount = ctx.mcpSpecs?.length ?? 0;
|
|
3499
|
+
const toolCount = loop.prefix.toolSpecs.length;
|
|
3500
|
+
const mcpLine = ` mcp ${mcpCount} server(s), ${toolCount} tool(s) in registry`;
|
|
3501
|
+
const pendingLine = pending > 0 ? ` edits ${pending} pending (/apply to commit, /discard to drop)` : "";
|
|
3502
|
+
const lines = [
|
|
3503
|
+
` model ${loop.model}`,
|
|
3504
|
+
` flags harvest=${loop.harvestEnabled ? "on" : "off"} \xB7 branch=${branchBudget > 1 ? branchBudget : "off"} \xB7 stream=${loop.stream ? "on" : "off"}`,
|
|
3505
|
+
ctxLine,
|
|
3506
|
+
mcpLine,
|
|
3507
|
+
sessionLine
|
|
3508
|
+
];
|
|
3509
|
+
if (pendingLine) lines.push(pendingLine);
|
|
3510
|
+
return { info: lines.join("\n") };
|
|
3285
3511
|
}
|
|
3286
3512
|
case "model": {
|
|
3287
3513
|
const id = args[0];
|
|
@@ -3333,6 +3559,34 @@ function handleSlash(cmd, args, loop, ctx = {}) {
|
|
|
3333
3559
|
return { unknown: true, info: `unknown command: /${cmd} (try /help)` };
|
|
3334
3560
|
}
|
|
3335
3561
|
}
|
|
3562
|
+
function formatToolList(history) {
|
|
3563
|
+
const total = history.length;
|
|
3564
|
+
const header = `Tool calls in this session (${total}, most recent first):`;
|
|
3565
|
+
const shown = Math.min(total, 10);
|
|
3566
|
+
const lines = [header];
|
|
3567
|
+
for (let i = 0; i < shown; i++) {
|
|
3568
|
+
const entry = history[total - 1 - i];
|
|
3569
|
+
if (!entry) continue;
|
|
3570
|
+
const idx = i + 1;
|
|
3571
|
+
const flat = entry.text.replace(/\s+/g, " ").trim();
|
|
3572
|
+
const preview = flat.length > 80 ? `${flat.slice(0, 80)}\u2026` : flat;
|
|
3573
|
+
const name = entry.toolName.length > 24 ? `${entry.toolName.slice(0, 23)}\u2026` : entry.toolName;
|
|
3574
|
+
lines.push(
|
|
3575
|
+
` #${String(idx).padStart(2)} ${name.padEnd(24)} ${String(entry.text.length).padStart(6)} chars ${preview}`
|
|
3576
|
+
);
|
|
3577
|
+
}
|
|
3578
|
+
if (total > shown) {
|
|
3579
|
+
lines.push(` \u2026 (${total - shown} earlier, reach with /tool N)`);
|
|
3580
|
+
}
|
|
3581
|
+
lines.push("");
|
|
3582
|
+
lines.push("View full output: /tool N (N=1 \u2192 most recent)");
|
|
3583
|
+
return lines.join("\n");
|
|
3584
|
+
}
|
|
3585
|
+
function compactNum(n) {
|
|
3586
|
+
if (n < 1e3) return String(n);
|
|
3587
|
+
const k = n / 1e3;
|
|
3588
|
+
return k >= 100 ? `${Math.round(k)}k` : `${k.toFixed(1)}k`;
|
|
3589
|
+
}
|
|
3336
3590
|
function stripOuterQuotes(s) {
|
|
3337
3591
|
if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) {
|
|
3338
3592
|
return s.slice(1, -1);
|
|
@@ -3388,6 +3642,9 @@ function App({
|
|
|
3388
3642
|
const [ongoingTool, setOngoingTool] = useState2(null);
|
|
3389
3643
|
const lastEditSnapshots = useRef(null);
|
|
3390
3644
|
const pendingEdits = useRef([]);
|
|
3645
|
+
const promptHistory = useRef([]);
|
|
3646
|
+
const historyCursor = useRef(-1);
|
|
3647
|
+
const toolHistoryRef = useRef([]);
|
|
3391
3648
|
const [summary, setSummary] = useState2({
|
|
3392
3649
|
turns: 0,
|
|
3393
3650
|
totalCostUsd: 0,
|
|
@@ -3456,11 +3713,28 @@ function App({
|
|
|
3456
3713
|
}
|
|
3457
3714
|
}, [session, loop]);
|
|
3458
3715
|
useInput((_input, key) => {
|
|
3459
|
-
if (
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3716
|
+
if (key.escape && busy) {
|
|
3717
|
+
if (abortedThisTurn.current) return;
|
|
3718
|
+
abortedThisTurn.current = true;
|
|
3719
|
+
loop.abort();
|
|
3720
|
+
return;
|
|
3721
|
+
}
|
|
3722
|
+
if (busy) return;
|
|
3723
|
+
const hist = promptHistory.current;
|
|
3724
|
+
if (key.upArrow) {
|
|
3725
|
+
if (hist.length === 0) return;
|
|
3726
|
+
const nextCursor = Math.min(historyCursor.current + 1, hist.length - 1);
|
|
3727
|
+
historyCursor.current = nextCursor;
|
|
3728
|
+
setInput(hist[hist.length - 1 - nextCursor] ?? "");
|
|
3729
|
+
return;
|
|
3730
|
+
}
|
|
3731
|
+
if (key.downArrow) {
|
|
3732
|
+
if (historyCursor.current < 0) return;
|
|
3733
|
+
const nextCursor = historyCursor.current - 1;
|
|
3734
|
+
historyCursor.current = nextCursor;
|
|
3735
|
+
setInput(nextCursor < 0 ? "" : hist[hist.length - 1 - nextCursor] ?? "");
|
|
3736
|
+
return;
|
|
3737
|
+
}
|
|
3464
3738
|
});
|
|
3465
3739
|
const codeUndo = useCallback(() => {
|
|
3466
3740
|
if (!codeMode) return "not in code mode";
|
|
@@ -3502,9 +3776,16 @@ function App({
|
|
|
3502
3776
|
);
|
|
3503
3777
|
const handleSubmit = useCallback(
|
|
3504
3778
|
async (raw) => {
|
|
3505
|
-
|
|
3779
|
+
let text = raw.trim();
|
|
3506
3780
|
if (!text || busy) return;
|
|
3507
3781
|
setInput("");
|
|
3782
|
+
historyCursor.current = -1;
|
|
3783
|
+
if (codeMode && pendingEdits.current.length > 0 && (text === "y" || text === "n")) {
|
|
3784
|
+
const out = text === "y" ? codeApply() : codeDiscard();
|
|
3785
|
+
setHistorical((prev) => [...prev, { id: `sys-${Date.now()}`, role: "info", text: out }]);
|
|
3786
|
+
promptHistory.current.push(text);
|
|
3787
|
+
return;
|
|
3788
|
+
}
|
|
3508
3789
|
const slash = parseSlash(text);
|
|
3509
3790
|
if (slash) {
|
|
3510
3791
|
const result = handleSlash(slash.cmd, slash.args, loop, {
|
|
@@ -3512,7 +3793,9 @@ function App({
|
|
|
3512
3793
|
codeUndo: codeMode ? codeUndo : void 0,
|
|
3513
3794
|
codeApply: codeMode ? codeApply : void 0,
|
|
3514
3795
|
codeDiscard: codeMode ? codeDiscard : void 0,
|
|
3515
|
-
codeRoot: codeMode?.rootDir
|
|
3796
|
+
codeRoot: codeMode?.rootDir,
|
|
3797
|
+
pendingEditCount: codeMode ? pendingEdits.current.length : void 0,
|
|
3798
|
+
toolHistory: () => toolHistoryRef.current
|
|
3516
3799
|
});
|
|
3517
3800
|
if (result.exit) {
|
|
3518
3801
|
transcriptRef.current?.end();
|
|
@@ -3533,8 +3816,14 @@ function App({
|
|
|
3533
3816
|
}
|
|
3534
3817
|
]);
|
|
3535
3818
|
}
|
|
3536
|
-
|
|
3819
|
+
if (result.resubmit) {
|
|
3820
|
+
text = result.resubmit;
|
|
3821
|
+
} else {
|
|
3822
|
+
promptHistory.current.push(text);
|
|
3823
|
+
return;
|
|
3824
|
+
}
|
|
3537
3825
|
}
|
|
3826
|
+
promptHistory.current.push(text);
|
|
3538
3827
|
setHistorical((prev) => [...prev, { id: `u-${Date.now()}`, role: "user", text }]);
|
|
3539
3828
|
const assistantId = `a-${Date.now()}`;
|
|
3540
3829
|
const streamRef = { id: assistantId, text: "", reasoning: "" };
|
|
@@ -3619,6 +3908,10 @@ function App({
|
|
|
3619
3908
|
} else if (ev.role === "tool") {
|
|
3620
3909
|
flush();
|
|
3621
3910
|
setOngoingTool(null);
|
|
3911
|
+
toolHistoryRef.current.push({
|
|
3912
|
+
toolName: ev.toolName ?? "?",
|
|
3913
|
+
text: ev.content
|
|
3914
|
+
});
|
|
3622
3915
|
setHistorical((prev) => [
|
|
3623
3916
|
...prev,
|
|
3624
3917
|
{
|
|
@@ -3664,16 +3957,62 @@ function App({
|
|
|
3664
3957
|
}
|
|
3665
3958
|
function OngoingToolRow({ tool }) {
|
|
3666
3959
|
const [tick, setTick] = useState2(0);
|
|
3960
|
+
const [elapsed, setElapsed] = useState2(0);
|
|
3667
3961
|
useEffect2(() => {
|
|
3668
|
-
const
|
|
3669
|
-
|
|
3962
|
+
const start = Date.now();
|
|
3963
|
+
const frameId = setInterval(() => setTick((t) => t + 1), 120);
|
|
3964
|
+
const secId = setInterval(() => setElapsed(Math.floor((Date.now() - start) / 1e3)), 1e3);
|
|
3965
|
+
return () => {
|
|
3966
|
+
clearInterval(frameId);
|
|
3967
|
+
clearInterval(secId);
|
|
3968
|
+
};
|
|
3670
3969
|
}, []);
|
|
3671
3970
|
const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
3672
|
-
const
|
|
3673
|
-
return /* @__PURE__ */ React6.createElement(Box6, { marginY: 1 }, /* @__PURE__ */ React6.createElement(Text6, { color: "cyan" }, frames[tick % frames.length]), /* @__PURE__ */ React6.createElement(Text6, { color: "yellow" }, ` tool<${tool.name}> running\u2026`),
|
|
3971
|
+
const summary = summarizeToolArgs(tool.name, tool.args);
|
|
3972
|
+
return /* @__PURE__ */ React6.createElement(Box6, { marginY: 1, flexDirection: "column" }, /* @__PURE__ */ React6.createElement(Box6, null, /* @__PURE__ */ React6.createElement(Text6, { color: "cyan" }, frames[tick % frames.length]), /* @__PURE__ */ React6.createElement(Text6, { color: "yellow" }, ` tool<${tool.name}> running\u2026`), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, ` ${elapsed}s`)), summary ? /* @__PURE__ */ React6.createElement(Box6, { paddingLeft: 2 }, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, summary)) : null);
|
|
3973
|
+
}
|
|
3974
|
+
function summarizeToolArgs(name, args) {
|
|
3975
|
+
if (!args || args === "{}") return "";
|
|
3976
|
+
let parsed;
|
|
3977
|
+
try {
|
|
3978
|
+
parsed = JSON.parse(args);
|
|
3979
|
+
} catch {
|
|
3980
|
+
return args.length > 80 ? `${args.slice(0, 80)}\u2026` : args;
|
|
3981
|
+
}
|
|
3982
|
+
const hasSuffix = (s) => name === s || name.endsWith(`_${s}`);
|
|
3983
|
+
const path = typeof parsed.path === "string" ? parsed.path : void 0;
|
|
3984
|
+
if (hasSuffix("read_file")) {
|
|
3985
|
+
const head = typeof parsed.head === "number" ? `, head=${parsed.head}` : "";
|
|
3986
|
+
const tail = typeof parsed.tail === "number" ? `, tail=${parsed.tail}` : "";
|
|
3987
|
+
return `path: ${path ?? "?"}${head}${tail}`;
|
|
3988
|
+
}
|
|
3989
|
+
if (hasSuffix("write_file")) {
|
|
3990
|
+
const content = typeof parsed.content === "string" ? parsed.content : "";
|
|
3991
|
+
return `path: ${path ?? "?"} (${content.length} chars)`;
|
|
3992
|
+
}
|
|
3993
|
+
if (hasSuffix("edit_file")) {
|
|
3994
|
+
const edits = Array.isArray(parsed.edits) ? parsed.edits.length : 0;
|
|
3995
|
+
return `path: ${path ?? "?"} (${edits} edit${edits === 1 ? "" : "s"})`;
|
|
3996
|
+
}
|
|
3997
|
+
if (hasSuffix("list_directory") || hasSuffix("directory_tree")) {
|
|
3998
|
+
return `path: ${path ?? "?"}`;
|
|
3999
|
+
}
|
|
4000
|
+
if (hasSuffix("search_files")) {
|
|
4001
|
+
const pattern = typeof parsed.pattern === "string" ? parsed.pattern : "?";
|
|
4002
|
+
return `path: ${path ?? "?"} \xB7 pattern: ${pattern}`;
|
|
4003
|
+
}
|
|
4004
|
+
if (hasSuffix("move_file")) {
|
|
4005
|
+
const src = typeof parsed.source === "string" ? parsed.source : "?";
|
|
4006
|
+
const dst = typeof parsed.destination === "string" ? parsed.destination : "?";
|
|
4007
|
+
return `${src} \u2192 ${dst}`;
|
|
4008
|
+
}
|
|
4009
|
+
if (hasSuffix("get_file_info")) {
|
|
4010
|
+
return `path: ${path ?? "?"}`;
|
|
4011
|
+
}
|
|
4012
|
+
return args.length > 80 ? `${args.slice(0, 80)}\u2026` : args;
|
|
3674
4013
|
}
|
|
3675
4014
|
function CommandStrip({ codeMode }) {
|
|
3676
|
-
return /* @__PURE__ */ React6.createElement(Box6, { paddingX: 2, flexDirection: "column" }, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "/help \xB7 /preset ", "<fast|smart|max>", " \xB7 /mcp \xB7 /compact \xB7 /sessions \xB7 /setup \xB7 /clear \xB7 /exit"), codeMode ? /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, 'code mode: /apply \xB7 /discard \xB7 /undo \xB7 /commit "msg" \u2014 edits NEVER write without /apply') : null, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "Esc (while thinking) \u2014 abort & summarize what was found so far"));
|
|
4015
|
+
return /* @__PURE__ */ React6.createElement(Box6, { paddingX: 2, flexDirection: "column" }, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "/help \xB7 /preset ", "<fast|smart|max>", " \xB7 /mcp \xB7 /compact \xB7 /sessions \xB7 /setup \xB7 /clear \xB7 /exit"), codeMode ? /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, 'code mode: /apply (y) \xB7 /discard (n) \xB7 /undo \xB7 /commit "msg" \u2014 edits NEVER write without /apply') : null, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "\u2191/\u2193 recall prompts \xB7 /retry re-send last \xB7 /think see R1 reasoning \xB7 /tool N full tool output"), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "Esc (while thinking) \u2014 abort & summarize what was found so far"));
|
|
3677
4016
|
}
|
|
3678
4017
|
function formatEditResults(results) {
|
|
3679
4018
|
const lines = results.map((r) => {
|
|
@@ -3693,7 +4032,7 @@ function formatPendingPreview(blocks) {
|
|
|
3693
4032
|
const tag = b.search === "" ? "NEW " : " ";
|
|
3694
4033
|
return ` ${tag}${b.path} (-${removed} +${added} lines)`;
|
|
3695
4034
|
});
|
|
3696
|
-
const header = `\u25B8 ${blocks.length} pending edit block(s) \u2014 /apply to commit
|
|
4035
|
+
const header = `\u25B8 ${blocks.length} pending edit block(s) \u2014 /apply (or y) to commit \xB7 /discard (or n) to drop`;
|
|
3697
4036
|
return [header, ...lines].join("\n");
|
|
3698
4037
|
}
|
|
3699
4038
|
function countLines2(s) {
|