mini-coder 0.0.11 → 0.0.13
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/mc.js +180 -22
- package/docs/configs.md +1 -1
- package/package.json +1 -1
- package/hanging-bug.md +0 -79
package/dist/mc.js
CHANGED
|
@@ -931,6 +931,8 @@ function fmtTokens(n) {
|
|
|
931
931
|
function renderStatusBar(opts) {
|
|
932
932
|
const cols = process.stdout.columns ?? 80;
|
|
933
933
|
const left = [c5.cyan(opts.model)];
|
|
934
|
+
if (opts.thinkingEffort)
|
|
935
|
+
left.push(c5.dim(`\u2726 ${opts.thinkingEffort}`));
|
|
934
936
|
if (opts.provider && opts.provider !== "zen")
|
|
935
937
|
left.push(c5.dim(opts.provider));
|
|
936
938
|
left.push(c5.dim(opts.sessionId.slice(0, 8)));
|
|
@@ -976,7 +978,7 @@ function renderError(err, context = "render") {
|
|
|
976
978
|
|
|
977
979
|
// src/cli/output.ts
|
|
978
980
|
var HOME2 = homedir3();
|
|
979
|
-
var PACKAGE_VERSION = "0.0.
|
|
981
|
+
var PACKAGE_VERSION = "0.0.12";
|
|
980
982
|
function tildePath(p) {
|
|
981
983
|
return p.startsWith(HOME2) ? `~${p.slice(HOME2.length)}` : p;
|
|
982
984
|
}
|
|
@@ -1347,6 +1349,87 @@ var CONTEXT_WINDOW_TABLE = [
|
|
|
1347
1349
|
[/^glm-/, 128000],
|
|
1348
1350
|
[/^qwen3-/, 131000]
|
|
1349
1351
|
];
|
|
1352
|
+
var REASONING_MODELS = [
|
|
1353
|
+
/^claude-3-5-sonnet/,
|
|
1354
|
+
/^claude-3-7/,
|
|
1355
|
+
/^claude-sonnet-4/,
|
|
1356
|
+
/^claude-opus-4/,
|
|
1357
|
+
/^o1/,
|
|
1358
|
+
/^o3/,
|
|
1359
|
+
/^o4/,
|
|
1360
|
+
/^gpt-5/,
|
|
1361
|
+
/^gemini-2\.5/,
|
|
1362
|
+
/^gemini-3/
|
|
1363
|
+
];
|
|
1364
|
+
function supportsThinking(modelString) {
|
|
1365
|
+
const { modelId } = parseModelString(modelString);
|
|
1366
|
+
return REASONING_MODELS.some((p) => p.test(modelId));
|
|
1367
|
+
}
|
|
1368
|
+
var ANTHROPIC_BUDGET = {
|
|
1369
|
+
low: 4096,
|
|
1370
|
+
medium: 8192,
|
|
1371
|
+
high: 16384,
|
|
1372
|
+
xhigh: 32768
|
|
1373
|
+
};
|
|
1374
|
+
function clampEffort(effort, max) {
|
|
1375
|
+
const ORDER = ["low", "medium", "high", "xhigh"];
|
|
1376
|
+
const i = ORDER.indexOf(effort);
|
|
1377
|
+
const m = ORDER.indexOf(max);
|
|
1378
|
+
return ORDER[Math.min(i, m)];
|
|
1379
|
+
}
|
|
1380
|
+
function getThinkingProviderOptions(modelString, effort) {
|
|
1381
|
+
if (!supportsThinking(modelString))
|
|
1382
|
+
return null;
|
|
1383
|
+
const { provider, modelId } = parseModelString(modelString);
|
|
1384
|
+
if (provider === "anthropic" || provider === "zen" && modelId.startsWith("claude-")) {
|
|
1385
|
+
const isAdaptive = /^claude-3-7/.test(modelId) || /^claude-sonnet-4/.test(modelId) || /^claude-opus-4/.test(modelId);
|
|
1386
|
+
if (isAdaptive) {
|
|
1387
|
+
const isOpus = /^claude-opus-4/.test(modelId);
|
|
1388
|
+
const mapped = effort === "xhigh" ? isOpus ? "max" : "high" : effort;
|
|
1389
|
+
return { anthropic: { thinking: { type: "adaptive" }, effort: mapped } };
|
|
1390
|
+
}
|
|
1391
|
+
const budget = ANTHROPIC_BUDGET[effort];
|
|
1392
|
+
return {
|
|
1393
|
+
anthropic: {
|
|
1394
|
+
thinking: { type: "enabled", budgetTokens: budget },
|
|
1395
|
+
betas: ["interleaved-thinking-2025-05-14"]
|
|
1396
|
+
}
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
if (provider === "openai" || provider === "zen" && (modelId.startsWith("o") || modelId.startsWith("gpt-5"))) {
|
|
1400
|
+
const supportsXhigh = /^gpt-5\.[2-9]/.test(modelId) || /^o4/.test(modelId);
|
|
1401
|
+
const clamped = supportsXhigh ? effort : clampEffort(effort, "high");
|
|
1402
|
+
return { openai: { reasoningEffort: clamped } };
|
|
1403
|
+
}
|
|
1404
|
+
if (provider === "google" || provider === "zen" && modelId.startsWith("gemini-")) {
|
|
1405
|
+
if (/^gemini-3/.test(modelId)) {
|
|
1406
|
+
const level = clampEffort(effort, "high");
|
|
1407
|
+
return {
|
|
1408
|
+
google: {
|
|
1409
|
+
thinkingConfig: {
|
|
1410
|
+
includeThoughts: true,
|
|
1411
|
+
thinkingLevel: level
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
const GEMINI_BUDGET = {
|
|
1417
|
+
low: 4096,
|
|
1418
|
+
medium: 8192,
|
|
1419
|
+
high: 16384,
|
|
1420
|
+
xhigh: 24575
|
|
1421
|
+
};
|
|
1422
|
+
return {
|
|
1423
|
+
google: {
|
|
1424
|
+
thinkingConfig: {
|
|
1425
|
+
includeThoughts: true,
|
|
1426
|
+
thinkingBudget: GEMINI_BUDGET[effort]
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
return null;
|
|
1432
|
+
}
|
|
1350
1433
|
function getContextWindow(modelString) {
|
|
1351
1434
|
const { modelId } = parseModelString(modelString);
|
|
1352
1435
|
for (const [pattern, tokens] of CONTEXT_WINDOW_TABLE) {
|
|
@@ -1698,6 +1781,19 @@ function getPreferredModel() {
|
|
|
1698
1781
|
function setPreferredModel(model) {
|
|
1699
1782
|
setSetting("preferred_model", model);
|
|
1700
1783
|
}
|
|
1784
|
+
function getPreferredThinkingEffort() {
|
|
1785
|
+
const v = getSetting("preferred_thinking_effort");
|
|
1786
|
+
if (v === "low" || v === "medium" || v === "high" || v === "xhigh")
|
|
1787
|
+
return v;
|
|
1788
|
+
return null;
|
|
1789
|
+
}
|
|
1790
|
+
function setPreferredThinkingEffort(effort) {
|
|
1791
|
+
if (effort === null) {
|
|
1792
|
+
getDb().run("DELETE FROM settings WHERE key = 'preferred_thinking_effort'");
|
|
1793
|
+
} else {
|
|
1794
|
+
setSetting("preferred_thinking_effort", effort);
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1701
1797
|
// src/session/db/mcp-repo.ts
|
|
1702
1798
|
function listMcpServers() {
|
|
1703
1799
|
return getDb().query("SELECT name, transport, url, command, args, env FROM mcp_servers ORDER BY name").all();
|
|
@@ -1786,7 +1882,15 @@ function isOpenAIGPT(modelString) {
|
|
|
1786
1882
|
return (provider === "openai" || provider === "zen") && modelId.startsWith("gpt-");
|
|
1787
1883
|
}
|
|
1788
1884
|
async function* runTurn(options) {
|
|
1789
|
-
const {
|
|
1885
|
+
const {
|
|
1886
|
+
model,
|
|
1887
|
+
modelString,
|
|
1888
|
+
messages,
|
|
1889
|
+
tools,
|
|
1890
|
+
systemPrompt,
|
|
1891
|
+
signal,
|
|
1892
|
+
thinkingEffort
|
|
1893
|
+
} = options;
|
|
1790
1894
|
let stepCount = 0;
|
|
1791
1895
|
let warningClaimed = false;
|
|
1792
1896
|
function claimWarning() {
|
|
@@ -1805,6 +1909,18 @@ async function* runTurn(options) {
|
|
|
1805
1909
|
try {
|
|
1806
1910
|
const useInstructions = systemPrompt !== undefined && isOpenAIGPT(modelString);
|
|
1807
1911
|
logApiEvent("turn start", { modelString, messageCount: messages.length });
|
|
1912
|
+
const thinkingOpts = thinkingEffort ? getThinkingProviderOptions(modelString, thinkingEffort) : null;
|
|
1913
|
+
const mergedProviderOptions = {
|
|
1914
|
+
...useInstructions ? { openai: { instructions: systemPrompt, store: false } } : {},
|
|
1915
|
+
...thinkingOpts ?? {},
|
|
1916
|
+
...useInstructions && thinkingOpts?.openai ? {
|
|
1917
|
+
openai: {
|
|
1918
|
+
instructions: systemPrompt,
|
|
1919
|
+
store: false,
|
|
1920
|
+
...thinkingOpts.openai
|
|
1921
|
+
}
|
|
1922
|
+
} : {}
|
|
1923
|
+
};
|
|
1808
1924
|
const streamOpts = {
|
|
1809
1925
|
model,
|
|
1810
1926
|
messages,
|
|
@@ -1829,14 +1945,7 @@ async function* runTurn(options) {
|
|
|
1829
1945
|
return;
|
|
1830
1946
|
},
|
|
1831
1947
|
...systemPrompt ? { system: systemPrompt } : {},
|
|
1832
|
-
...
|
|
1833
|
-
providerOptions: {
|
|
1834
|
-
openai: {
|
|
1835
|
-
instructions: systemPrompt,
|
|
1836
|
-
store: false
|
|
1837
|
-
}
|
|
1838
|
-
}
|
|
1839
|
-
} : {},
|
|
1948
|
+
...Object.keys(mergedProviderOptions).length > 0 ? { providerOptions: mergedProviderOptions } : {},
|
|
1840
1949
|
...signal ? { abortSignal: signal } : {}
|
|
1841
1950
|
};
|
|
1842
1951
|
const result = streamText(streamOpts);
|
|
@@ -2885,7 +2994,7 @@ function buildReadOnlyToolSet(opts) {
|
|
|
2885
2994
|
}
|
|
2886
2995
|
|
|
2887
2996
|
// src/agent/subagent-runner.ts
|
|
2888
|
-
function createSubagentRunner(cwd, reporter, getCurrentModel) {
|
|
2997
|
+
function createSubagentRunner(cwd, reporter, getCurrentModel, getThinkingEffort) {
|
|
2889
2998
|
let nextLaneId = 1;
|
|
2890
2999
|
const activeLanes = new Set;
|
|
2891
3000
|
const runSubagent = async (prompt, depth = 0, agentName, modelOverride, parentLabel) => {
|
|
@@ -2913,12 +3022,14 @@ function createSubagentRunner(cwd, reporter, getCurrentModel) {
|
|
|
2913
3022
|
let result = "";
|
|
2914
3023
|
let inputTokens = 0;
|
|
2915
3024
|
let outputTokens = 0;
|
|
3025
|
+
const effort = getThinkingEffort();
|
|
2916
3026
|
const events = runTurn({
|
|
2917
3027
|
model: subLlm,
|
|
2918
3028
|
modelString: model,
|
|
2919
3029
|
messages: subMessages,
|
|
2920
3030
|
tools: subTools,
|
|
2921
|
-
systemPrompt
|
|
3031
|
+
systemPrompt,
|
|
3032
|
+
...effort ? { thinkingEffort: effort } : {}
|
|
2922
3033
|
});
|
|
2923
3034
|
for await (const event of events) {
|
|
2924
3035
|
reporter.stopSpinner();
|
|
@@ -3375,20 +3486,50 @@ async function expandTemplate(template, args, cwd) {
|
|
|
3375
3486
|
|
|
3376
3487
|
// src/cli/commands.ts
|
|
3377
3488
|
async function handleModel(ctx, args) {
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
if (
|
|
3489
|
+
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
3490
|
+
if (parts.length > 0) {
|
|
3491
|
+
if (parts[0] === "effort") {
|
|
3492
|
+
const effortArg2 = parts[1] ?? "";
|
|
3493
|
+
if (effortArg2 === "off") {
|
|
3494
|
+
ctx.setThinkingEffort(null);
|
|
3495
|
+
writeln(`${PREFIX.success} thinking effort disabled`);
|
|
3496
|
+
} else if (["low", "medium", "high", "xhigh"].includes(effortArg2)) {
|
|
3497
|
+
ctx.setThinkingEffort(effortArg2);
|
|
3498
|
+
writeln(`${PREFIX.success} thinking effort \u2192 ${c10.cyan(effortArg2)}`);
|
|
3499
|
+
} else {
|
|
3500
|
+
writeln(`${PREFIX.error} usage: /model effort <low|medium|high|xhigh|off>`);
|
|
3501
|
+
}
|
|
3502
|
+
return;
|
|
3503
|
+
}
|
|
3504
|
+
const idArg = parts[0] ?? "";
|
|
3505
|
+
let modelId = idArg;
|
|
3506
|
+
if (!idArg.includes("/")) {
|
|
3381
3507
|
const models2 = await fetchAvailableModels();
|
|
3382
|
-
const match = models2.find((m) => m.id.split("/").slice(1).join("/") ===
|
|
3508
|
+
const match = models2.find((m) => m.id.split("/").slice(1).join("/") === idArg || m.id === idArg);
|
|
3383
3509
|
if (match) {
|
|
3384
3510
|
modelId = match.id;
|
|
3385
3511
|
} else {
|
|
3386
|
-
writeln(`${PREFIX.error} unknown model ${c10.cyan(
|
|
3512
|
+
writeln(`${PREFIX.error} unknown model ${c10.cyan(idArg)} ${c10.dim("\u2014 run /models for the full list")}`);
|
|
3387
3513
|
return;
|
|
3388
3514
|
}
|
|
3389
3515
|
}
|
|
3390
3516
|
ctx.setModel(modelId);
|
|
3391
|
-
|
|
3517
|
+
const effortArg = parts[1];
|
|
3518
|
+
if (effortArg) {
|
|
3519
|
+
if (effortArg === "off") {
|
|
3520
|
+
ctx.setThinkingEffort(null);
|
|
3521
|
+
writeln(`${PREFIX.success} model \u2192 ${c10.cyan(modelId)} ${c10.dim("(thinking disabled)")}`);
|
|
3522
|
+
} else if (["low", "medium", "high", "xhigh"].includes(effortArg)) {
|
|
3523
|
+
ctx.setThinkingEffort(effortArg);
|
|
3524
|
+
writeln(`${PREFIX.success} model \u2192 ${c10.cyan(modelId)} ${c10.dim(`(\u2726 ${effortArg})`)}`);
|
|
3525
|
+
} else {
|
|
3526
|
+
writeln(`${PREFIX.success} model \u2192 ${c10.cyan(modelId)}`);
|
|
3527
|
+
writeln(`${PREFIX.error} unknown effort level ${c10.cyan(effortArg)} (use low, medium, high, xhigh, off)`);
|
|
3528
|
+
}
|
|
3529
|
+
} else {
|
|
3530
|
+
const e = ctx.thinkingEffort ? c10.dim(` (\u2726 ${ctx.thinkingEffort})`) : "";
|
|
3531
|
+
writeln(`${PREFIX.success} model \u2192 ${c10.cyan(modelId)}${e}`);
|
|
3532
|
+
}
|
|
3392
3533
|
return;
|
|
3393
3534
|
}
|
|
3394
3535
|
writeln(`${c10.dim(" fetching models\u2026")}`);
|
|
@@ -3415,13 +3556,15 @@ async function handleModel(ctx, args) {
|
|
|
3415
3556
|
const isCurrent = ctx.currentModel === m.id;
|
|
3416
3557
|
const freeTag = m.free ? c10.green(" free") : "";
|
|
3417
3558
|
const ctxTag = m.context ? c10.dim(` ${Math.round(m.context / 1000)}k`) : "";
|
|
3559
|
+
const effortTag = isCurrent && ctx.thinkingEffort ? c10.dim(` \u2726 ${ctx.thinkingEffort}`) : "";
|
|
3418
3560
|
const cur = isCurrent ? c10.cyan(" \u25C0") : "";
|
|
3419
|
-
writeln(` ${c10.dim("\xB7")} ${m.displayName}${freeTag}${ctxTag}${cur}`);
|
|
3561
|
+
writeln(` ${c10.dim("\xB7")} ${m.displayName}${freeTag}${ctxTag}${cur}${effortTag}`);
|
|
3420
3562
|
writeln(` ${c10.dim(m.id)}`);
|
|
3421
3563
|
}
|
|
3422
3564
|
}
|
|
3423
3565
|
writeln();
|
|
3424
3566
|
writeln(c10.dim(" /model <id> to switch \xB7 e.g. /model zen/claude-sonnet-4-6"));
|
|
3567
|
+
writeln(c10.dim(" /model effort <low|medium|high|xhigh|off> to set thinking effort"));
|
|
3425
3568
|
}
|
|
3426
3569
|
function handlePlan(ctx) {
|
|
3427
3570
|
ctx.setPlanMode(!ctx.planMode);
|
|
@@ -4288,6 +4431,7 @@ class SessionRunner {
|
|
|
4288
4431
|
tools;
|
|
4289
4432
|
mcpTools;
|
|
4290
4433
|
currentModel;
|
|
4434
|
+
currentThinkingEffort;
|
|
4291
4435
|
session;
|
|
4292
4436
|
coreHistory;
|
|
4293
4437
|
turnIndex = 1;
|
|
@@ -4303,6 +4447,7 @@ class SessionRunner {
|
|
|
4303
4447
|
this.tools = opts.tools;
|
|
4304
4448
|
this.mcpTools = opts.mcpTools;
|
|
4305
4449
|
this.currentModel = opts.initialModel;
|
|
4450
|
+
this.currentThinkingEffort = opts.initialThinkingEffort;
|
|
4306
4451
|
this.initSession(opts.sessionId);
|
|
4307
4452
|
}
|
|
4308
4453
|
initSession(sessionId) {
|
|
@@ -4385,7 +4530,8 @@ class SessionRunner {
|
|
|
4385
4530
|
messages: this.coreHistory,
|
|
4386
4531
|
tools: this.planMode ? [...buildReadOnlyToolSet({ cwd: this.cwd }), ...this.mcpTools] : this.tools,
|
|
4387
4532
|
systemPrompt,
|
|
4388
|
-
signal: abortController.signal
|
|
4533
|
+
signal: abortController.signal,
|
|
4534
|
+
...this.currentThinkingEffort ? { thinkingEffort: this.currentThinkingEffort } : {}
|
|
4389
4535
|
});
|
|
4390
4536
|
const { inputTokens, outputTokens, contextTokens, newMessages } = await this.reporter.renderTurn(events);
|
|
4391
4537
|
if (newMessages.length > 0) {
|
|
@@ -4431,7 +4577,8 @@ class SessionRunner {
|
|
|
4431
4577
|
async function runAgent(opts) {
|
|
4432
4578
|
const cwd = opts.cwd;
|
|
4433
4579
|
let currentModel = opts.model;
|
|
4434
|
-
|
|
4580
|
+
let currentThinkingEffort = opts.initialThinkingEffort;
|
|
4581
|
+
const runSubagent = createSubagentRunner(cwd, opts.reporter, () => currentModel, () => currentThinkingEffort);
|
|
4435
4582
|
const agents = loadAgents(cwd);
|
|
4436
4583
|
const tools = buildToolSet({
|
|
4437
4584
|
cwd,
|
|
@@ -4472,6 +4619,7 @@ async function runAgent(opts) {
|
|
|
4472
4619
|
tools,
|
|
4473
4620
|
mcpTools,
|
|
4474
4621
|
initialModel: currentModel,
|
|
4622
|
+
initialThinkingEffort: opts.initialThinkingEffort,
|
|
4475
4623
|
sessionId: opts.sessionId
|
|
4476
4624
|
});
|
|
4477
4625
|
const cmdCtx = {
|
|
@@ -4484,6 +4632,14 @@ async function runAgent(opts) {
|
|
|
4484
4632
|
setPreferredModel(m);
|
|
4485
4633
|
currentModel = m;
|
|
4486
4634
|
},
|
|
4635
|
+
get thinkingEffort() {
|
|
4636
|
+
return runner.currentThinkingEffort;
|
|
4637
|
+
},
|
|
4638
|
+
setThinkingEffort: (e) => {
|
|
4639
|
+
runner.currentThinkingEffort = e;
|
|
4640
|
+
setPreferredThinkingEffort(e);
|
|
4641
|
+
currentThinkingEffort = e;
|
|
4642
|
+
},
|
|
4487
4643
|
get planMode() {
|
|
4488
4644
|
return runner.planMode;
|
|
4489
4645
|
},
|
|
@@ -4527,7 +4683,8 @@ async function runAgent(opts) {
|
|
|
4527
4683
|
outputTokens: runner.totalOut,
|
|
4528
4684
|
contextTokens: runner.lastContextTokens,
|
|
4529
4685
|
contextWindow: getContextWindow(runner.currentModel) ?? 0,
|
|
4530
|
-
ralphMode: runner.ralphMode
|
|
4686
|
+
ralphMode: runner.ralphMode,
|
|
4687
|
+
thinkingEffort: runner.currentThinkingEffort
|
|
4531
4688
|
});
|
|
4532
4689
|
}
|
|
4533
4690
|
if (opts.initialPrompt) {
|
|
@@ -4703,6 +4860,7 @@ async function main() {
|
|
|
4703
4860
|
const agentOpts = {
|
|
4704
4861
|
model,
|
|
4705
4862
|
cwd: args.cwd,
|
|
4863
|
+
initialThinkingEffort: getPreferredThinkingEffort(),
|
|
4706
4864
|
reporter: new CliReporter
|
|
4707
4865
|
};
|
|
4708
4866
|
if (sessionId)
|
package/docs/configs.md
CHANGED
|
@@ -65,7 +65,7 @@ Custom subagents are configured in `.agents`:
|
|
|
65
65
|
- `./.agents/agents/*.md`
|
|
66
66
|
- `~/.agents/agents/*.md`
|
|
67
67
|
|
|
68
|
-
(There is no `.claude` compatibility path for agents.)
|
|
68
|
+
(There is no `.claude` compatibility path for agents. Claude doesn't have custom subagents)
|
|
69
69
|
|
|
70
70
|
## Precedence and conflicts
|
|
71
71
|
|
package/package.json
CHANGED
package/hanging-bug.md
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
# Hanging Bug Investigation
|
|
2
|
-
|
|
3
|
-
## Symptoms
|
|
4
|
-
|
|
5
|
-
After a shell tool call completes successfully, **SOMETIMES** the app hangs indefinitely instead of
|
|
6
|
-
returning to the prompt. Two observed states:
|
|
7
|
-
|
|
8
|
-
```
|
|
9
|
-
$ $ rm src/session/db.ts && bun run test
|
|
10
|
-
⠇ shell
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
Stayed spinning with shell label for well over the timeout time. I had to kill the app.
|
|
14
|
-
|
|
15
|
-
Another example with a different shell command:
|
|
16
|
-
```
|
|
17
|
-
$ $ git diff
|
|
18
|
-
✔ 0
|
|
19
|
-
│ diff --git a/src/cli/tool-render.ts b/src/cli/tool-render.ts
|
|
20
|
-
│ index f8d33af..e224932 100644
|
|
21
|
-
│ --- a/src/cli/tool-render.ts
|
|
22
|
-
│ +++ b/src/cli/tool-render.ts
|
|
23
|
-
│ @@ -148,21 +148,25 @@ export function renderToolResultInline(
|
|
24
|
-
│ }
|
|
25
|
-
│
|
|
26
|
-
│ if (toolName === "glob") {
|
|
27
|
-
│ - const r = result as { files: string[]; truncated: boolean };
|
|
28
|
-
│ - const n = r.files.length;
|
|
29
|
-
│ - writeln(
|
|
30
|
-
│ - `${indent}${G.info} ${c.dim(n === 0 ? "no matches" : `${n} file${n === 1 ? "" : "s"}${r.truncated ? " (capped)" : ""}`)}`,
|
|
31
|
-
│ - );
|
|
32
|
-
│ - return;
|
|
33
|
-
│ + const r = result as { files?: string[]; truncated?: boolean };
|
|
34
|
-
│ + if (Array.isArray(r.files)) {
|
|
35
|
-
│ + const n = r.files.length;
|
|
36
|
-
│ + writeln(
|
|
37
|
-
│ + `${indent}${G.info} ${c.dim(n === 0 ? "no matches" : `${n} file${n === 1 ? "" : "s"}${r.truncated ? " (capped)" : ""}`)}`,
|
|
38
|
-
│ + );
|
|
39
|
-
│ … +150 lines
|
|
40
|
-
⠦ thinking
|
|
41
|
-
```
|
|
42
|
-
User also had to kill the app.
|
|
43
|
-
|
|
44
|
-
Only seen with Gemini/Google models.
|
|
45
|
-
Only happens after shell tool calls.
|
|
46
|
-
Other tools calls worked just fine during the same session with the same model and mini-coder version.
|
|
47
|
-
There are no shell tool related hooks configured anywhere.
|
|
48
|
-
|
|
49
|
-
The expectation was the turn to continue as normal since the tool calls succeded.
|
|
50
|
-
|
|
51
|
-
Find the root cause, show proof.
|
|
52
|
-
|
|
53
|
-
## Findings:
|
|
54
|
-
|
|
55
|
-
Here is the concrete proof I just ran, no edits:
|
|
56
|
-
- I executed renderTurn with an event stream that does:
|
|
57
|
-
1) tool-call-start(shell)
|
|
58
|
-
2) tool-result(shell success)
|
|
59
|
-
3) then never yields again
|
|
60
|
-
- Result: the promise did not resolve within 700ms (timed_out_700ms).
|
|
61
|
-
Then I ran the same sequence but added turn-complete:
|
|
62
|
-
- Result: it resolved immediately (resolved).
|
|
63
|
-
So this is now proven in your codepath:
|
|
64
|
-
- renderTurn waits forever when the upstream async stream goes silent after a shell tool result.
|
|
65
|
-
- This exactly matches the visible hang states.
|
|
66
|
-
|
|
67
|
-
### Root Cause 1: Hangs spinning on `"shell"`
|
|
68
|
-
**Proof in code:** `src/tools/shell.ts`
|
|
69
|
-
* When a command times out, `proc.kill("SIGTERM")` only kills the parent process (e.g., `bash`). Any child processes (e.g., `bun`) become orphaned but stay alive, holding the write end of the `stdout`/`stderr` pipes open.
|
|
70
|
-
* Because the pipe never closes, `await reader.read()` inside `collectStream()` hangs indefinitely.
|
|
71
|
-
* Because `collectStream()` never resolves, the tool execution never finishes, `tool-result` is never yielded, and the stream goes completely silent while the spinner stays stuck on "shell".
|
|
72
|
-
- **FIXED**
|
|
73
|
-
|
|
74
|
-
### Root Cause 2: Hangs spinning on `"thinking"`
|
|
75
|
-
**Proof in code:** `src/llm-api/turn.ts`
|
|
76
|
-
* After `git diff` completes, the tool resolves and `renderTurn` switches the spinner to `"thinking"`.
|
|
77
|
-
* The AI SDK automatically makes a new HTTP request to the Gemini API containing the tool result to generate the next step.
|
|
78
|
-
* Gemini's API occasionally hangs indefinitely or silently drops connections when receiving certain payloads (like large tool outputs or ANSI color codes, which `git diff` outputs).
|
|
79
|
-
* Because there is no timeout configured on the `streamText` call in `runTurn` (unless the user manually aborts), the underlying fetch request waits forever. The `result.fullStream` never yields the next chunk, but also never closes or errors.
|