turbollm 1.8.6 → 1.8.7
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.js +858 -40
- package/dist/webdist/assets/{AgentEditPage-BaaqMpWR.js → AgentEditPage-F6wQ-woa.js} +1 -1
- package/dist/webdist/assets/{ChatScreen-C2DYwxeR.js → ChatScreen-JNejy4c5.js} +1 -1
- package/dist/webdist/assets/CodeComposer-BMivBZTt.js +1 -0
- package/dist/webdist/assets/{CodeHomeScreen-B_rbjGaw.js → CodeHomeScreen-Bvv8dpoH.js} +1 -1
- package/dist/webdist/assets/CodeSessionScreen-BZ_n-Q8C.js +5 -0
- package/dist/webdist/assets/CustomizeScreen-CfQYShcO.js +4 -0
- package/dist/webdist/assets/DeveloperScreen-DlLMgh2N.js +1 -0
- package/dist/webdist/assets/{EnginesScreen-jM6G9Lnt.js → EnginesScreen-CCzC8Dth.js} +6 -6
- package/dist/webdist/assets/{FsBrowser-CWzT3VbL.js → FsBrowser-d8UO9bVy.js} +1 -1
- package/dist/webdist/assets/ModelLoadMenu-BcSALweE.js +2 -0
- package/dist/webdist/assets/{ModelsScreen-D0-Onb6V.js → ModelsScreen-DHqnZlFL.js} +1 -1
- package/dist/webdist/assets/{SettingsScreen-C-k9Yc55.js → SettingsScreen-Byi4wqlL.js} +1 -1
- package/dist/webdist/assets/{SkillEditPage-DVIYJU7N.js → SkillEditPage-CKfpniwp.js} +1 -1
- package/dist/webdist/assets/{TokensScreen-DC4sSZK8.js → TokensScreen-CSxwCWhD.js} +1 -1
- package/dist/webdist/assets/{ToolApprovalBar-BYyXPiNP.js → ToolApprovalBar-CDRoIK9R.js} +15 -15
- package/dist/webdist/assets/WorkspaceScreen-CY5EZKSy.js +1 -0
- package/dist/webdist/assets/{agent-api-DBuqrdZq.js → agent-api-DaWwB_7n.js} +1 -1
- package/dist/webdist/assets/{arc-Banj5wBf.js → arc-Bw5r0gQ8.js} +1 -1
- package/dist/webdist/assets/{architectureDiagram-3BPJPVTR-D-VtkgYv.js → architectureDiagram-3BPJPVTR-D_qgeydj.js} +1 -1
- package/dist/webdist/assets/{arrow-left-C1r8Dr2d.js → arrow-left-Bs4KhSqC.js} +1 -1
- package/dist/webdist/assets/{badge-DMOKG62I.js → badge-CPXRwtV2.js} +1 -1
- package/dist/webdist/assets/{blockDiagram-GPEHLZMM-nvbn1Awx.js → blockDiagram-GPEHLZMM-BbzqG8AR.js} +1 -1
- package/dist/webdist/assets/{bot-CTxax61P.js → bot-DUgi89DH.js} +1 -1
- package/dist/webdist/assets/{c4Diagram-AAUBKEIU-DoG1JXd6.js → c4Diagram-AAUBKEIU-Bltku6hD.js} +1 -1
- package/dist/webdist/assets/channel-Cob2-fKv.js +1 -0
- package/dist/webdist/assets/{chat-api-B81lfnq0.js → chat-api-DnoBopyh.js} +1 -1
- package/dist/webdist/assets/{chat-queries-BcaqUVgR.js → chat-queries-EOqarO2W.js} +1 -1
- package/dist/webdist/assets/check-C-_wc2AD.js +1 -0
- package/dist/webdist/assets/{chevron-left-BdgNx91c.js → chevron-left-CFIaBRPc.js} +1 -1
- package/dist/webdist/assets/{chevron-right-BXxjw90s.js → chevron-right-DT5tldLF.js} +1 -1
- package/dist/webdist/assets/{chunk-2J33WTMH-CI01KUY5.js → chunk-2J33WTMH-jRmwQd0-.js} +1 -1
- package/dist/webdist/assets/{chunk-4BX2VUAB-CT4Q6KgA.js → chunk-4BX2VUAB-BkQR5P6X.js} +1 -1
- package/dist/webdist/assets/{chunk-55IACEB6-DVqUTluK.js → chunk-55IACEB6-dHYKdZGl.js} +1 -1
- package/dist/webdist/assets/{chunk-727SXJPM-2pMnnV6j.js → chunk-727SXJPM-Bac05mkQ.js} +1 -1
- package/dist/webdist/assets/{chunk-AQP2D5EJ-q_bI8ZG1.js → chunk-AQP2D5EJ-DQSeJwV8.js} +1 -1
- package/dist/webdist/assets/{chunk-FMBD7UC4-B8gQv0l1.js → chunk-FMBD7UC4-fw71OGhl.js} +1 -1
- package/dist/webdist/assets/{chunk-ND2GUHAM-DJx_4zvu.js → chunk-ND2GUHAM-xRyemSmx.js} +1 -1
- package/dist/webdist/assets/{chunk-QZHKN3VN-CrAA0uxl.js → chunk-QZHKN3VN-BgNNqsB9.js} +1 -1
- package/dist/webdist/assets/{circle-check-CzXTbeIt.js → circle-check-CLNNkErq.js} +1 -1
- package/dist/webdist/assets/{circle-x-CfrulclP.js → circle-x-DHTcnBYg.js} +1 -1
- package/dist/webdist/assets/classDiagram-4FO5ZUOK-emMvNtz-.js +1 -0
- package/dist/webdist/assets/classDiagram-v2-Q7XG4LA2-emMvNtz-.js +1 -0
- package/dist/webdist/assets/{common-DAcb-w9o.js → common-Bh_ckDae.js} +1 -1
- package/dist/webdist/assets/copy-button-B0AUQv8-.js +1 -0
- package/dist/webdist/assets/{cose-bilkent-S5V4N54A-EGQLoy7D.js → cose-bilkent-S5V4N54A-BpeA37l_.js} +1 -1
- package/dist/webdist/assets/{dagre-BM42HDAG-DC_RQ1B0.js → dagre-BM42HDAG-Dhe-FxnK.js} +1 -1
- package/dist/webdist/assets/{diagram-2AECGRRQ-cZcCJYfi.js → diagram-2AECGRRQ-CsOhE15t.js} +1 -1
- package/dist/webdist/assets/{diagram-5GNKFQAL-Cb-jxgLV.js → diagram-5GNKFQAL-Dj4NKMxD.js} +1 -1
- package/dist/webdist/assets/{diagram-KO2AKTUF-D4INKoTb.js → diagram-KO2AKTUF-BVeDuCrX.js} +1 -1
- package/dist/webdist/assets/{diagram-LMA3HP47-CZBo_NP5.js → diagram-LMA3HP47-CrQt3n5J.js} +1 -1
- package/dist/webdist/assets/{diagram-OG6HWLK6-Dmb2yOZ2.js → diagram-OG6HWLK6-CVHDvio_.js} +1 -1
- package/dist/webdist/assets/{dialog-BKdq0aeC.js → dialog-BPhXn1HL.js} +1 -1
- package/dist/webdist/assets/{erDiagram-TEJ5UH35-_ekw2dYE.js → erDiagram-TEJ5UH35-DmEuGD8S.js} +1 -1
- package/dist/webdist/assets/{external-link-DJyx1xu4.js → external-link-DgElSULb.js} +1 -1
- package/dist/webdist/assets/{flowDiagram-I6XJVG4X-BKFCSX9M.js → flowDiagram-I6XJVG4X-D3ES6R-Y.js} +1 -1
- package/dist/webdist/assets/{ganttDiagram-6RSMTGT7-Bun-N5iU.js → ganttDiagram-6RSMTGT7-DzovqV_S.js} +1 -1
- package/dist/webdist/assets/{git-branch-DWvtYq2n.js → git-branch-DZp9oCCA.js} +1 -1
- package/dist/webdist/assets/{gitGraphDiagram-PVQCEYII-DP97jy0N.js → gitGraphDiagram-PVQCEYII-BV0Hvb-m.js} +1 -1
- package/dist/webdist/assets/{index-v0Tdes8H.js → index-BivjMfQ5.js} +2 -2
- package/dist/webdist/assets/index-S6YWlmeh.css +1 -0
- package/dist/webdist/assets/{infoDiagram-5YYISTIA-XNpJszfx.js → infoDiagram-5YYISTIA-DHmj_pKX.js} +1 -1
- package/dist/webdist/assets/{ishikawaDiagram-YF4QCWOH-Ct7Jx-R0.js → ishikawaDiagram-YF4QCWOH-DjAmacek.js} +1 -1
- package/dist/webdist/assets/{journeyDiagram-JHISSGLW-CyHwpNbR.js → journeyDiagram-JHISSGLW-InYx9cWN.js} +1 -1
- package/dist/webdist/assets/{kanban-definition-UN3LZRKU-BlIiwFL_.js → kanban-definition-UN3LZRKU-BO9O2UDq.js} +1 -1
- package/dist/webdist/assets/{linear-BHBdNjY-.js → linear-B6IwpKfz.js} +1 -1
- package/dist/webdist/assets/{mermaid.core-DJ7y5RnJ.js → mermaid.core-MlIRHLxX.js} +4 -4
- package/dist/webdist/assets/{mindmap-definition-RKZ34NQL-hhkBvUUD.js → mindmap-definition-RKZ34NQL-kvrr7ftb.js} +1 -1
- package/dist/webdist/assets/{pencil-CJGwma8c.js → pencil-k4AQARf_.js} +1 -1
- package/dist/webdist/assets/{personas-fIGMukzj.js → personas-CL0Kc_Kd.js} +5 -3
- package/dist/webdist/assets/{pieDiagram-4H26LBE5-TIZ5Ty6J.js → pieDiagram-4H26LBE5-DaE8CAe0.js} +1 -1
- package/dist/webdist/assets/{plus-88_2XyV5.js → plus-CY4mhBkY.js} +1 -1
- package/dist/webdist/assets/{quadrantDiagram-W4KKPZXB-DYHxb7at.js → quadrantDiagram-W4KKPZXB-Duu8s5l-.js} +1 -1
- package/dist/webdist/assets/{refresh-cw-MdqFeESZ.js → refresh-cw-DCf4F0oQ.js} +1 -1
- package/dist/webdist/assets/{requirementDiagram-4Y6WPE33-qnClMgKa.js → requirementDiagram-4Y6WPE33-Cbl7MhTK.js} +1 -1
- package/dist/webdist/assets/{rotate-ccw-c1dWBxKq.js → rotate-ccw-Bk6FjMT9.js} +1 -1
- package/dist/webdist/assets/{sankeyDiagram-5OEKKPKP-BG_kgFTc.js → sankeyDiagram-5OEKKPKP-BiK0JGus.js} +1 -1
- package/dist/webdist/assets/{sequenceDiagram-3UESZ5HK-D6qrIrDg.js → sequenceDiagram-3UESZ5HK-Dr7D9A4w.js} +1 -1
- package/dist/webdist/assets/{skeleton-Br4uEDd5.js → skeleton-Cy7DsAmm.js} +1 -1
- package/dist/webdist/assets/{sliders-horizontal-Cy_WWOJ2.js → sliders-horizontal-CVf2FXz2.js} +1 -1
- package/dist/webdist/assets/{wrench-A33IdAiw.js → sparkles-Bvrl94s5.js} +1 -1
- package/dist/webdist/assets/{star-C-2hCQLL.js → star-DLAcO0R_.js} +1 -1
- package/dist/webdist/assets/{stateDiagram-AJRCARHV-BHad4Ojy.js → stateDiagram-AJRCARHV-BthDRItX.js} +1 -1
- package/dist/webdist/assets/stateDiagram-v2-BHNVJYJU--IOHWZLa.js +1 -0
- package/dist/webdist/assets/{switch-CMtwRt96.js → switch-CI322tBg.js} +1 -1
- package/dist/webdist/assets/{terminal-BtT1uKGg.js → terminal-Bwy3mmYf.js} +1 -1
- package/dist/webdist/assets/{timeline-definition-PNZ67QCA-B4rUcQHc.js → timeline-definition-PNZ67QCA-DkR-Byrz.js} +1 -1
- package/dist/webdist/assets/{trash-2-Pyx2qefh.js → trash-2-B9m1cz2x.js} +1 -1
- package/dist/webdist/assets/{useIsDesktop-Cm4AYq5A.js → useIsDesktop-xqMNa55J.js} +1 -1
- package/dist/webdist/assets/{vennDiagram-CIIHVFJN-Xc9rC-NG.js → vennDiagram-CIIHVFJN-aOYXm4nP.js} +1 -1
- package/dist/webdist/assets/{wardley-L42UT6IY-lxhSG_a5.js → wardley-L42UT6IY-DptJ0PbQ.js} +1 -1
- package/dist/webdist/assets/{wardleyDiagram-YWT4CUSO-cPWAssfx.js → wardleyDiagram-YWT4CUSO-CN66GNqR.js} +1 -1
- package/dist/webdist/assets/wrench-CLvjdoSH.js +1 -0
- package/dist/webdist/assets/{xychartDiagram-2RQKCTM6-CKEYbcW3.js → xychartDiagram-2RQKCTM6-CbOWShHZ.js} +1 -1
- package/dist/webdist/index.html +2 -2
- package/package.json +69 -69
- package/dist/webdist/assets/CodeComposer-BBE_Hp0Q.js +0 -1
- package/dist/webdist/assets/CodeSessionScreen-DJJY5EKZ.js +0 -4
- package/dist/webdist/assets/CustomizeScreen-ClXS3xOu.js +0 -4
- package/dist/webdist/assets/DeveloperScreen-X2uqzXVK.js +0 -1
- package/dist/webdist/assets/ModelLoadMenu-UGUkfjbF.js +0 -2
- package/dist/webdist/assets/WorkspaceScreen-DJr_KuyC.js +0 -1
- package/dist/webdist/assets/channel-g0-qHfup.js +0 -1
- package/dist/webdist/assets/check-BQ18Nt2_.js +0 -1
- package/dist/webdist/assets/classDiagram-4FO5ZUOK-B09K5xH-.js +0 -1
- package/dist/webdist/assets/classDiagram-v2-Q7XG4LA2-B09K5xH-.js +0 -1
- package/dist/webdist/assets/copy-button-CE2-6pHL.js +0 -1
- package/dist/webdist/assets/index-CfEenPkR.css +0 -1
- package/dist/webdist/assets/layers-C5s0VHiR.js +0 -1
- package/dist/webdist/assets/stateDiagram-v2-BHNVJYJU-BDM3H2wn.js +0 -1
package/dist/cli.js
CHANGED
|
@@ -6199,6 +6199,30 @@ var ConversationStore = class {
|
|
|
6199
6199
|
if (!from) return 0;
|
|
6200
6200
|
return this.db.prepare(`UPDATE messages SET is_active = 1 WHERE conv_id = $cid AND seq >= $seq`).run({ $cid: convId, $seq: from.seq }).changes;
|
|
6201
6201
|
}
|
|
6202
|
+
/** /clear (v34): deactivates `uptoMessageId` and every message BEFORE it (by `seq`) — a real,
|
|
6203
|
+
* resumable PREFIX deactivation (is_active=0), the mirror of {@link deactivateMessagesFrom}'s
|
|
6204
|
+
* suffix and the same mechanism /revert and Chat branching use. Replaces /clear's old
|
|
6205
|
+
* clearedUpToMessageId DISPLAY cursor, which left the "cleared" rows is_active=1 — so
|
|
6206
|
+
* getMessages()/getConversation() (hence session export AND the model's own history replay via
|
|
6207
|
+
* resolveEffectiveHistory) still saw them, applying the cut only client-side. /clear hides
|
|
6208
|
+
* everything up to AND INCLUDING the cut (showing only turns added AFTER it), so this is a
|
|
6209
|
+
* prefix. Returns the number of rows deactivated (0 if `uptoMessageId` doesn't exist). Only ever
|
|
6210
|
+
* called on Code conversations, which have no regenerate/branch siblings, so every prefix row is
|
|
6211
|
+
* active at clear time — same coarse assumption reactivateMessagesFrom already relies on. */
|
|
6212
|
+
deactivateMessagesUpTo(convId, uptoMessageId) {
|
|
6213
|
+
const upto = this.db.prepare(`SELECT seq FROM messages WHERE id = $id AND conv_id = $cid`).get({ $id: uptoMessageId, $cid: convId });
|
|
6214
|
+
if (!upto) return 0;
|
|
6215
|
+
return this.db.prepare(`UPDATE messages SET is_active = 0 WHERE conv_id = $cid AND seq <= $seq`).run({ $cid: convId, $seq: upto.seq }).changes;
|
|
6216
|
+
}
|
|
6217
|
+
/** Undoes {@link deactivateMessagesUpTo} — reactivates `uptoMessageId` and every message before
|
|
6218
|
+
* it (by `seq`). A NEW turn appended after a /clear has a HIGHER seq than the cut, so /resume
|
|
6219
|
+
* never touches it — only the originally-cleared prefix comes back. Safe if `uptoMessageId` no
|
|
6220
|
+
* longer exists (0 rows). */
|
|
6221
|
+
reactivateMessagesUpTo(convId, uptoMessageId) {
|
|
6222
|
+
const upto = this.db.prepare(`SELECT seq FROM messages WHERE id = $id AND conv_id = $cid`).get({ $id: uptoMessageId, $cid: convId });
|
|
6223
|
+
if (!upto) return 0;
|
|
6224
|
+
return this.db.prepare(`UPDATE messages SET is_active = 1 WHERE conv_id = $cid AND seq <= $seq`).run({ $cid: convId, $seq: upto.seq }).changes;
|
|
6225
|
+
}
|
|
6202
6226
|
/** Permanently delete a Code session: its messages, working doc, and the agent_run +
|
|
6203
6227
|
* conversation rows. There is no FK cascade on these tables (each is deleted explicitly,
|
|
6204
6228
|
* same as chat's own conversation delete never cascaded to messages) — miss one and it's
|
|
@@ -14098,18 +14122,18 @@ function registerApi(app2, d) {
|
|
|
14098
14122
|
if (!existsSync22(cwd) || !statSync5(cwd).isDirectory()) {
|
|
14099
14123
|
return c.json({ isRepo: false, branch: "", branches: [] });
|
|
14100
14124
|
}
|
|
14101
|
-
const
|
|
14125
|
+
const git2 = (args) => execFileSync5("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 5e3, windowsHide: true }).trim();
|
|
14102
14126
|
try {
|
|
14103
|
-
if (
|
|
14127
|
+
if (git2(["rev-parse", "--is-inside-work-tree"]) !== "true") return c.json({ isRepo: false, branch: "", branches: [] });
|
|
14104
14128
|
let branch = "";
|
|
14105
14129
|
try {
|
|
14106
|
-
branch =
|
|
14130
|
+
branch = git2(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
14107
14131
|
} catch {
|
|
14108
14132
|
}
|
|
14109
14133
|
if (branch === "HEAD") branch = "";
|
|
14110
14134
|
let branches = [];
|
|
14111
14135
|
try {
|
|
14112
|
-
branches =
|
|
14136
|
+
branches = git2(["branch", "--format=%(refname:short)"]).split("\n").map((s) => s.trim()).filter(Boolean);
|
|
14113
14137
|
} catch {
|
|
14114
14138
|
}
|
|
14115
14139
|
return c.json({ isRepo: true, branch, branches });
|
|
@@ -15808,6 +15832,16 @@ function dependencyDisciplineGuidance() {
|
|
|
15808
15832
|
"training knowledge, which is frequently stale for fast-moving libraries."
|
|
15809
15833
|
].join(" ");
|
|
15810
15834
|
}
|
|
15835
|
+
function todoTrackerGuidance() {
|
|
15836
|
+
return [
|
|
15837
|
+
"For a task with several distinct steps, call update_todos to give the user a live checklist:",
|
|
15838
|
+
"send the full list of steps (each { content, status }) when you start, mark a step in_progress",
|
|
15839
|
+
"when you begin it and completed as soon as it is done, and re-send the WHOLE list on every",
|
|
15840
|
+
"update (it replaces the previous one). This shows the user the plan and real progress instead",
|
|
15841
|
+
"of an opaque run of tool calls. Do NOT use it for trivial single-step requests \u2014 a one-item",
|
|
15842
|
+
"checklist is just noise."
|
|
15843
|
+
].join(" ");
|
|
15844
|
+
}
|
|
15811
15845
|
function toolsForMode(mode) {
|
|
15812
15846
|
return mode === "plan" ? ["read", "grep", "find", "ls"] : void 0;
|
|
15813
15847
|
}
|
|
@@ -15839,6 +15873,12 @@ function readAgentsFile(path) {
|
|
|
15839
15873
|
return null;
|
|
15840
15874
|
}
|
|
15841
15875
|
}
|
|
15876
|
+
function agentsMdPresence(repoRoot, globalDir) {
|
|
15877
|
+
return {
|
|
15878
|
+
project: readAgentsFile(join27(repoRoot, "AGENTS.md")) !== null,
|
|
15879
|
+
global: readAgentsFile(join27(globalDir, "agents.md")) !== null
|
|
15880
|
+
};
|
|
15881
|
+
}
|
|
15842
15882
|
function agentsMdBlock(repoRoot, globalDir) {
|
|
15843
15883
|
const global = readAgentsFile(join27(globalDir, "agents.md"));
|
|
15844
15884
|
const project = readAgentsFile(join27(repoRoot, "AGENTS.md"));
|
|
@@ -15857,6 +15897,7 @@ function buildAppendPrompt(mode, skills = [], agentsMd, hasWebTools = false) {
|
|
|
15857
15897
|
if (mode !== "plan") {
|
|
15858
15898
|
blocks.push(editReliabilityGuidance());
|
|
15859
15899
|
blocks.push(lspGuidance());
|
|
15900
|
+
blocks.push(todoTrackerGuidance());
|
|
15860
15901
|
}
|
|
15861
15902
|
if (hasWebTools) {
|
|
15862
15903
|
blocks.push(antiFallbackGuidance());
|
|
@@ -16128,12 +16169,9 @@ var COMPACTION_SUFFIX = "\n\n(End of summary \u2014 the actual conversation cont
|
|
|
16128
16169
|
function resolveEffectiveHistory(d, convId, sessionId) {
|
|
16129
16170
|
const all = d.db.getConversation(convId, true)?.messages ?? [];
|
|
16130
16171
|
const run = d.db.getAgentRun(sessionId);
|
|
16131
|
-
|
|
16132
|
-
const clearedIdx = run?.clearedUpToMessageId ? all.findIndex((m) => m.id === run.clearedUpToMessageId) : -1;
|
|
16133
|
-
if (clearedIdx !== -1 && clearedIdx >= compactionIdx) {
|
|
16134
|
-
return { summaryText: null, messages: all.slice(clearedIdx + 1) };
|
|
16135
|
-
}
|
|
16172
|
+
if (run?.clearedUpToMessageId) return { summaryText: null, messages: all };
|
|
16136
16173
|
if (!run?.compactionUpToMessageId) return { summaryText: null, messages: all };
|
|
16174
|
+
const compactionIdx = all.findIndex((m) => m.id === run.compactionUpToMessageId);
|
|
16137
16175
|
const rest = compactionIdx === -1 ? all : all.slice(compactionIdx + 1);
|
|
16138
16176
|
return { summaryText: `${COMPACTION_PREFIX}${run.compactionSummary}${COMPACTION_SUFFIX}`, messages: rest };
|
|
16139
16177
|
}
|
|
@@ -16162,6 +16200,7 @@ function keepRecentTokensFor(messages, baseKeepRecentTokens) {
|
|
|
16162
16200
|
var MUTATING_TOOLS = /* @__PURE__ */ new Set(["edit", "write", "bash"]);
|
|
16163
16201
|
var PATH_TOOLS = /* @__PURE__ */ new Set(["read", "edit", "write", "grep", "find", "ls"]);
|
|
16164
16202
|
var LOOP_BREAK_AFTER = 3;
|
|
16203
|
+
var LOOP_ABORT_AFTER = LOOP_BREAK_AFTER + 3;
|
|
16165
16204
|
function toolCallSignature(name, input) {
|
|
16166
16205
|
const stable = (v) => {
|
|
16167
16206
|
if (v === null || typeof v !== "object") return JSON.stringify(v) ?? "undefined";
|
|
@@ -16210,6 +16249,99 @@ function disposeLspClientsForConv(convId) {
|
|
|
16210
16249
|
for (const c of clients.values()) c.dispose();
|
|
16211
16250
|
lspClientsByConv.delete(convId);
|
|
16212
16251
|
}
|
|
16252
|
+
function extractPartialText(partialResult) {
|
|
16253
|
+
const pr = partialResult;
|
|
16254
|
+
if (!pr || !Array.isArray(pr.content)) return "";
|
|
16255
|
+
return pr.content.map((c) => c?.type === "text" ? c.text ?? "" : "").join("");
|
|
16256
|
+
}
|
|
16257
|
+
function codeEventToFrame(ev, turnCounter) {
|
|
16258
|
+
switch (ev.type) {
|
|
16259
|
+
case "message_update": {
|
|
16260
|
+
const ame = ev.assistantMessageEvent;
|
|
16261
|
+
if (ame.type === "text_delta") return { event: "delta", data: { delta: ame.delta } };
|
|
16262
|
+
if (ame.type === "thinking_delta") return { event: "reasoning", data: { delta: ame.delta } };
|
|
16263
|
+
return null;
|
|
16264
|
+
}
|
|
16265
|
+
case "compaction_start":
|
|
16266
|
+
return { event: "compaction", data: { phase: "start", reason: ev.reason } };
|
|
16267
|
+
case "compaction_end":
|
|
16268
|
+
return { event: "compaction", data: { phase: "end", reason: ev.reason, aborted: ev.aborted, tokensBefore: ev.result?.tokensBefore } };
|
|
16269
|
+
case "turn_start":
|
|
16270
|
+
return { event: "turn", data: { phase: "start", index: turnCounter.index } };
|
|
16271
|
+
case "turn_end": {
|
|
16272
|
+
const frame = { event: "turn", data: { phase: "end", index: turnCounter.index, toolResults: ev.toolResults.length } };
|
|
16273
|
+
turnCounter.index++;
|
|
16274
|
+
return frame;
|
|
16275
|
+
}
|
|
16276
|
+
case "auto_retry_start":
|
|
16277
|
+
return { event: "retry", data: { phase: "start", attempt: ev.attempt, maxAttempts: ev.maxAttempts, delayMs: ev.delayMs, message: ev.errorMessage } };
|
|
16278
|
+
case "auto_retry_end":
|
|
16279
|
+
return { event: "retry", data: { phase: "end", attempt: ev.attempt, success: ev.success, message: ev.finalError } };
|
|
16280
|
+
case "tool_execution_update":
|
|
16281
|
+
return { event: "tool_progress", data: { id: ev.toolCallId, name: ev.toolName, partial: extractPartialText(ev.partialResult) } };
|
|
16282
|
+
default:
|
|
16283
|
+
return null;
|
|
16284
|
+
}
|
|
16285
|
+
}
|
|
16286
|
+
var DELEGATE_SUBAGENT_TIMEOUT_MS = 5 * 6e4;
|
|
16287
|
+
function validateDelegateTask(raw) {
|
|
16288
|
+
const task = typeof raw === "string" ? raw.trim() : "";
|
|
16289
|
+
if (!task) return { ok: false, message: "delegate_task: `task` must be a non-empty description of the sub-task to delegate." };
|
|
16290
|
+
return { ok: true, task };
|
|
16291
|
+
}
|
|
16292
|
+
function normalizeDelegateResult(finalText, opts) {
|
|
16293
|
+
const text = finalText.trim();
|
|
16294
|
+
if (opts?.timedOut) {
|
|
16295
|
+
const mins = Math.max(1, Math.round((opts.timeoutMs ?? DELEGATE_SUBAGENT_TIMEOUT_MS) / 6e4));
|
|
16296
|
+
const partial = text ? `
|
|
16297
|
+
|
|
16298
|
+
Partial progress before it was stopped:
|
|
16299
|
+
${text}` : "";
|
|
16300
|
+
return `(the delegated sub-task did not finish within ~${mins} minute(s) and was stopped \u2014 treat it as INCOMPLETE; consider doing it yourself or delegating a smaller piece.${partial})`;
|
|
16301
|
+
}
|
|
16302
|
+
return text || "(the delegated sub-agent produced no output.)";
|
|
16303
|
+
}
|
|
16304
|
+
var DELEGATED_SUBAGENT_PREAMBLE = "You are an isolated sub-agent handling ONE delegated sub-task on behalf of a parent coding agent. You do NOT see the parent conversation or its history \u2014 everything you need is in the task message below. Do exactly that task and nothing more, using your tools against the same repository. You cannot delegate further or invoke skills. When finished, end your reply with a concise, self-contained summary of what you did, what you found, and anything the parent needs in order to continue \u2014 that summary is the ONLY thing returned to the parent.";
|
|
16305
|
+
var TODO_STATUSES = /* @__PURE__ */ new Set(["pending", "in_progress", "completed"]);
|
|
16306
|
+
var MAX_TODOS = 50;
|
|
16307
|
+
function normalizeTodos(raw) {
|
|
16308
|
+
if (!Array.isArray(raw)) return [];
|
|
16309
|
+
const out = [];
|
|
16310
|
+
for (const item of raw) {
|
|
16311
|
+
if (!item || typeof item !== "object") continue;
|
|
16312
|
+
const rec = item;
|
|
16313
|
+
const content = typeof rec.content === "string" ? rec.content.trim() : "";
|
|
16314
|
+
if (!content) continue;
|
|
16315
|
+
const status = typeof rec.status === "string" && TODO_STATUSES.has(rec.status) ? rec.status : "pending";
|
|
16316
|
+
out.push({ content, status });
|
|
16317
|
+
if (out.length >= MAX_TODOS) break;
|
|
16318
|
+
}
|
|
16319
|
+
return out;
|
|
16320
|
+
}
|
|
16321
|
+
function summarizeTodos(todos) {
|
|
16322
|
+
if (todos.length === 0) return "Todo list cleared.";
|
|
16323
|
+
const completed = todos.filter((t) => t.status === "completed").length;
|
|
16324
|
+
const inProgress = todos.filter((t) => t.status === "in_progress").length;
|
|
16325
|
+
const parts = [`${completed}/${todos.length} done`];
|
|
16326
|
+
if (inProgress > 0) parts.push(`${inProgress} in progress`);
|
|
16327
|
+
return `Tracking ${todos.length} step(s): ${parts.join(", ")}.`;
|
|
16328
|
+
}
|
|
16329
|
+
var PREFILL_POLL_MS = 300;
|
|
16330
|
+
function isProcessingSlot(s) {
|
|
16331
|
+
if (!s || typeof s !== "object") return false;
|
|
16332
|
+
const r = s;
|
|
16333
|
+
return r.is_processing === true && typeof r.n_prompt_tokens === "number" && typeof r.n_prompt_tokens_processed === "number";
|
|
16334
|
+
}
|
|
16335
|
+
function pickPrefillProgress(slots) {
|
|
16336
|
+
if (!Array.isArray(slots)) return null;
|
|
16337
|
+
const processing = slots.filter(isProcessingSlot);
|
|
16338
|
+
if (processing.length !== 1) return null;
|
|
16339
|
+
const total = processing[0].n_prompt_tokens;
|
|
16340
|
+
const processed = processing[0].n_prompt_tokens_processed;
|
|
16341
|
+
if (!(total > 0) || processed < 0) return null;
|
|
16342
|
+
const pct = Math.min(100, Math.max(0, Math.round(processed / total * 100)));
|
|
16343
|
+
return { processed, total, pct };
|
|
16344
|
+
}
|
|
16213
16345
|
async function runCodeSession(params) {
|
|
16214
16346
|
const { d, convId, sessionId, repoRoot, mode, thinkingBudget, task: rawTask, signal, sink } = params;
|
|
16215
16347
|
const ms = d.manager.status();
|
|
@@ -16409,12 +16541,192 @@ async function runCodeSession(params) {
|
|
|
16409
16541
|
skillSession.dispose();
|
|
16410
16542
|
return finalText2.trim() || "(the skill produced no output)";
|
|
16411
16543
|
}
|
|
16544
|
+
async function runDelegatedSubSession(subMode, task2) {
|
|
16545
|
+
const subAuth = AuthStorage.inMemory();
|
|
16546
|
+
const subRegistry = ModelRegistry.inMemory(subAuth);
|
|
16547
|
+
subRegistry.registerProvider("local", {
|
|
16548
|
+
baseUrl: `${target}/v1`,
|
|
16549
|
+
apiKey: "agent-key",
|
|
16550
|
+
authHeader: true,
|
|
16551
|
+
api: "openai-completions",
|
|
16552
|
+
models: [{
|
|
16553
|
+
id: modelId,
|
|
16554
|
+
name: modelDisplayName,
|
|
16555
|
+
reasoning: false,
|
|
16556
|
+
input: ["text"],
|
|
16557
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
16558
|
+
contextWindow,
|
|
16559
|
+
maxTokens: 8192,
|
|
16560
|
+
compat: { supportsDeveloperRole: false, thinkingFormat: "qwen-chat-template" }
|
|
16561
|
+
}]
|
|
16562
|
+
});
|
|
16563
|
+
const subModel = subRegistry.find("local", modelId);
|
|
16564
|
+
if (!subModel) throw new Error("Failed to register local model with pi.");
|
|
16565
|
+
const subAc = new AbortController();
|
|
16566
|
+
let timedOut = false;
|
|
16567
|
+
const onParentAbort = () => subAc.abort();
|
|
16568
|
+
if (signal.aborted) subAc.abort();
|
|
16569
|
+
else signal.addEventListener("abort", onParentAbort, { once: true });
|
|
16570
|
+
const timer = setTimeout(() => {
|
|
16571
|
+
timedOut = true;
|
|
16572
|
+
subAc.abort();
|
|
16573
|
+
}, DELEGATE_SUBAGENT_TIMEOUT_MS);
|
|
16574
|
+
let subHeldGate;
|
|
16575
|
+
const releaseSubGate = () => {
|
|
16576
|
+
const r = subHeldGate;
|
|
16577
|
+
subHeldGate = void 0;
|
|
16578
|
+
r?.();
|
|
16579
|
+
};
|
|
16580
|
+
const subExtension = (pi) => {
|
|
16581
|
+
pi.on("before_provider_request", async (event) => {
|
|
16582
|
+
releaseSubGate();
|
|
16583
|
+
if (d.gate) subHeldGate = await d.gate.acquire("bg", { signal: subAc.signal });
|
|
16584
|
+
const payload = { ...event.payload };
|
|
16585
|
+
delete payload.max_tokens;
|
|
16586
|
+
delete payload.max_completion_tokens;
|
|
16587
|
+
return payload;
|
|
16588
|
+
});
|
|
16589
|
+
pi.on("after_provider_response", () => {
|
|
16590
|
+
releaseSubGate();
|
|
16591
|
+
});
|
|
16592
|
+
pi.on("tool_call", async (event) => {
|
|
16593
|
+
const toolName = event.toolName;
|
|
16594
|
+
const toolCallId = event.toolCallId;
|
|
16595
|
+
const input = event.input;
|
|
16596
|
+
await sink({ event: "tool_call", data: { id: toolCallId, name: toolName, args: input, status: "pending" } });
|
|
16597
|
+
if (PATH_TOOLS.has(toolName)) {
|
|
16598
|
+
const p = input.path;
|
|
16599
|
+
const pathRequired = toolName === "read" || toolName === "edit" || toolName === "write";
|
|
16600
|
+
if (p !== void 0 && typeof p === "string") {
|
|
16601
|
+
if (!isContainedFromRoot(p, repoRoot)) {
|
|
16602
|
+
const reason = "path is outside the allowed repo root";
|
|
16603
|
+
await sink({ event: "tool_call", data: { id: toolCallId, name: toolName, args: input, status: "error", result: reason } });
|
|
16604
|
+
return { block: true, reason };
|
|
16605
|
+
}
|
|
16606
|
+
} else if (pathRequired) {
|
|
16607
|
+
const reason = `${toolName}: a valid path is required`;
|
|
16608
|
+
await sink({ event: "tool_call", data: { id: toolCallId, name: toolName, args: input, status: "error", result: reason } });
|
|
16609
|
+
return { block: true, reason };
|
|
16610
|
+
}
|
|
16611
|
+
}
|
|
16612
|
+
if (subMode === "ask" && MUTATING_TOOLS.has(toolName)) {
|
|
16613
|
+
await sink({ event: "tool_call", data: { id: toolCallId, name: toolName, args: input, status: "awaiting_approval" } });
|
|
16614
|
+
const decision = await waitForToolApproval(`${convId}:${toolCallId}`, subAc.signal);
|
|
16615
|
+
if (decision === "deny") {
|
|
16616
|
+
const reason = "denied by user";
|
|
16617
|
+
await sink({ event: "tool_call", data: { id: toolCallId, name: toolName, args: input, status: "error", result: reason } });
|
|
16618
|
+
return { block: true, reason };
|
|
16619
|
+
}
|
|
16620
|
+
}
|
|
16621
|
+
return;
|
|
16622
|
+
});
|
|
16623
|
+
pi.on("tool_result", async (event) => {
|
|
16624
|
+
const text = event.content.map((c) => c.type === "text" ? c.text : "").join("");
|
|
16625
|
+
const data = {
|
|
16626
|
+
id: event.toolCallId,
|
|
16627
|
+
name: event.toolName,
|
|
16628
|
+
args: event.input,
|
|
16629
|
+
status: event.isError ? "error" : "done",
|
|
16630
|
+
result: text
|
|
16631
|
+
};
|
|
16632
|
+
if (isEditToolResult(event) && event.details) {
|
|
16633
|
+
data.diff = event.details.diff;
|
|
16634
|
+
data.patch = event.details.patch;
|
|
16635
|
+
data.firstChangedLine = event.details.firstChangedLine;
|
|
16636
|
+
}
|
|
16637
|
+
await sink({ event: "tool_call", data });
|
|
16638
|
+
});
|
|
16639
|
+
};
|
|
16640
|
+
const subAgentDir = join28(d.store.dir(), "pi-agent");
|
|
16641
|
+
const subResourceLoader = new DefaultResourceLoader({
|
|
16642
|
+
cwd: repoRoot,
|
|
16643
|
+
agentDir: subAgentDir,
|
|
16644
|
+
settingsManager: SettingsManager.inMemory(),
|
|
16645
|
+
extensionFactories: [{ name: "turbollm-delegate", factory: subExtension }],
|
|
16646
|
+
// The normal mode persona (no skill catalog — a delegated sub-agent can't invoke skills),
|
|
16647
|
+
// plus the delegation preamble that frames it as a focused, summary-returning worker.
|
|
16648
|
+
appendSystemPrompt: [...buildAppendPrompt(subMode, [], { repoRoot, globalDir: d.store.dir() }, !!d.tools), DELEGATED_SUBAGENT_PREAMBLE],
|
|
16649
|
+
noSkills: true,
|
|
16650
|
+
noPromptTemplates: true,
|
|
16651
|
+
noThemes: true
|
|
16652
|
+
});
|
|
16653
|
+
await subResourceLoader.reload();
|
|
16654
|
+
const subTools = toolsForMode(subMode);
|
|
16655
|
+
const subBash = createBashTool(repoRoot, { operations: createRobustBashOperations({ sessionLabel: `${sessionId}:delegate` }) });
|
|
16656
|
+
const { session: subSession } = await createAgentSession({
|
|
16657
|
+
cwd: repoRoot,
|
|
16658
|
+
agentDir: subAgentDir,
|
|
16659
|
+
model: subModel,
|
|
16660
|
+
authStorage: subAuth,
|
|
16661
|
+
modelRegistry: subRegistry,
|
|
16662
|
+
resourceLoader: subResourceLoader,
|
|
16663
|
+
sessionManager: SessionManager.inMemory(repoRoot),
|
|
16664
|
+
settingsManager: SettingsManager.inMemory({ compaction: compactionSettingsFor(contextWindow) }),
|
|
16665
|
+
customTools: [subBash],
|
|
16666
|
+
...subTools ? { tools: subTools } : {}
|
|
16667
|
+
// No invoke_skill / delegate_task / web / mcp / lsp tools registered → depth-1, bounded set.
|
|
16668
|
+
});
|
|
16669
|
+
const onSubAbort = () => {
|
|
16670
|
+
void subSession.abort();
|
|
16671
|
+
};
|
|
16672
|
+
if (subAc.signal.aborted) onSubAbort();
|
|
16673
|
+
else subAc.signal.addEventListener("abort", onSubAbort, { once: true });
|
|
16674
|
+
try {
|
|
16675
|
+
await subSession.prompt(task2);
|
|
16676
|
+
} catch (e) {
|
|
16677
|
+
if (!timedOut && !subAc.signal.aborted) throw e;
|
|
16678
|
+
} finally {
|
|
16679
|
+
clearTimeout(timer);
|
|
16680
|
+
signal.removeEventListener("abort", onParentAbort);
|
|
16681
|
+
releaseSubGate();
|
|
16682
|
+
}
|
|
16683
|
+
const finalText2 = subSession.getLastAssistantText() ?? "";
|
|
16684
|
+
subSession.dispose();
|
|
16685
|
+
return normalizeDelegateResult(finalText2, timedOut ? { timedOut: true, timeoutMs: DELEGATE_SUBAGENT_TIMEOUT_MS } : void 0);
|
|
16686
|
+
}
|
|
16412
16687
|
const mcpToolDefs = d.tools ? (await d.tools.buildToolDefinitions()).filter((t) => t.function.name.startsWith("mcp__")) : [];
|
|
16413
16688
|
const mcpToolNames = new Set(mcpToolDefs.map((t) => t.function.name));
|
|
16689
|
+
let prefillTimer;
|
|
16690
|
+
let prefillActive = false;
|
|
16691
|
+
const stopPrefillPoll = () => {
|
|
16692
|
+
prefillActive = false;
|
|
16693
|
+
if (prefillTimer) {
|
|
16694
|
+
clearTimeout(prefillTimer);
|
|
16695
|
+
prefillTimer = void 0;
|
|
16696
|
+
}
|
|
16697
|
+
};
|
|
16698
|
+
const startPrefillPoll = () => {
|
|
16699
|
+
stopPrefillPoll();
|
|
16700
|
+
prefillActive = true;
|
|
16701
|
+
let lastPct = -1;
|
|
16702
|
+
const tick2 = async () => {
|
|
16703
|
+
if (!prefillActive) return;
|
|
16704
|
+
try {
|
|
16705
|
+
const res = await fetch(`${target}/slots`, { signal });
|
|
16706
|
+
if (res.ok) {
|
|
16707
|
+
const progress = pickPrefillProgress(await res.json());
|
|
16708
|
+
if (progress && prefillActive) {
|
|
16709
|
+
if (progress.pct !== lastPct) {
|
|
16710
|
+
lastPct = progress.pct;
|
|
16711
|
+
void sink({ event: "prefill", data: { ...progress } });
|
|
16712
|
+
}
|
|
16713
|
+
if (progress.processed >= progress.total) {
|
|
16714
|
+
stopPrefillPoll();
|
|
16715
|
+
return;
|
|
16716
|
+
}
|
|
16717
|
+
}
|
|
16718
|
+
}
|
|
16719
|
+
} catch {
|
|
16720
|
+
}
|
|
16721
|
+
if (prefillActive) prefillTimer = setTimeout(() => void tick2(), PREFILL_POLL_MS);
|
|
16722
|
+
};
|
|
16723
|
+
prefillTimer = setTimeout(() => void tick2(), PREFILL_POLL_MS);
|
|
16724
|
+
};
|
|
16414
16725
|
const extension = (pi) => {
|
|
16415
16726
|
pi.on("before_provider_request", async (event) => {
|
|
16416
16727
|
releaseGate();
|
|
16417
16728
|
if (d.gate) heldGate = await d.gate.acquire("bg", { signal });
|
|
16729
|
+
startPrefillPoll();
|
|
16418
16730
|
const payload = { ...event.payload };
|
|
16419
16731
|
delete payload.max_tokens;
|
|
16420
16732
|
delete payload.max_completion_tokens;
|
|
@@ -16431,6 +16743,7 @@ async function runCodeSession(params) {
|
|
|
16431
16743
|
return payload;
|
|
16432
16744
|
});
|
|
16433
16745
|
pi.on("after_provider_response", () => {
|
|
16746
|
+
stopPrefillPoll();
|
|
16434
16747
|
releaseGate();
|
|
16435
16748
|
});
|
|
16436
16749
|
pi.on("tool_call", async (event) => {
|
|
@@ -16438,7 +16751,14 @@ async function runCodeSession(params) {
|
|
|
16438
16751
|
const toolCallId = event.toolCallId;
|
|
16439
16752
|
const input = event.input;
|
|
16440
16753
|
await sink({ event: "tool_call", data: { id: toolCallId, name: toolName, args: input, status: "pending" } });
|
|
16441
|
-
|
|
16754
|
+
const loopCount = toolLoop.record(toolName, input);
|
|
16755
|
+
if (loopCount > LOOP_ABORT_AFTER) {
|
|
16756
|
+
const reason = `[SYSTEM: \`${toolName}\` was called with identical arguments ${loopCount} times in a row, including after being told to stop \u2014 this run has been stopped automatically rather than continue looping. Start a new turn with different instructions to try again.]`;
|
|
16757
|
+
await sink({ event: "tool_call", data: { id: toolCallId, name: toolName, args: input, status: "error", result: reason } });
|
|
16758
|
+
void session.abort();
|
|
16759
|
+
return { block: true, reason };
|
|
16760
|
+
}
|
|
16761
|
+
if (loopCount > LOOP_BREAK_AFTER) {
|
|
16442
16762
|
const reason = `[SYSTEM: you have called \`${toolName}\` with identical arguments too many times in a row and it is not making progress \u2014 this call was NOT executed. Stop repeating it: take a different action, call it with different arguments, or, if you already have what you need, give your final answer now.]`;
|
|
16443
16763
|
await sink({ event: "tool_call", data: { id: toolCallId, name: toolName, args: input, status: "error", result: reason } });
|
|
16444
16764
|
return { block: true, reason };
|
|
@@ -16561,6 +16881,50 @@ ${lines.join("\n")}${more}]`
|
|
|
16561
16881
|
}
|
|
16562
16882
|
}
|
|
16563
16883
|
});
|
|
16884
|
+
pi.registerTool({
|
|
16885
|
+
name: "delegate_task",
|
|
16886
|
+
label: "Delegate task",
|
|
16887
|
+
description: `Delegate a focused, self-contained sub-task to an isolated sub-agent that has your same repository access and tools (subject to the current mode), runs it to completion, and returns ONLY its final summary \u2014 the sub-agent's step-by-step work never enters this conversation, so your own context stays focused. Use it for a well-scoped chunk you can describe completely up front (e.g. "investigate how X works and report back", or "apply this specific refactor across these files"). The sub-agent does NOT see this conversation, so put everything it needs in the task; it cannot delegate further or invoke skills.`,
|
|
16888
|
+
promptSnippet: "delegate_task(task) - hand a focused, fully-described sub-task to an isolated sub-agent and get back its summary",
|
|
16889
|
+
parameters: Type.Object({
|
|
16890
|
+
task: Type.String({ description: "The complete, self-contained sub-task, including every bit of context the sub-agent needs (it does not see this conversation or its history)." })
|
|
16891
|
+
}),
|
|
16892
|
+
async execute(_toolCallId, params2) {
|
|
16893
|
+
const valid = validateDelegateTask(params2.task);
|
|
16894
|
+
if (!valid.ok) return { content: [{ type: "text", text: valid.message }], details: {} };
|
|
16895
|
+
const dbMode = d.db.getConversation(convId)?.agentMode ?? mode;
|
|
16896
|
+
const liveMode = dbMode === "plan" ? mode : dbMode;
|
|
16897
|
+
try {
|
|
16898
|
+
const text = await runDelegatedSubSession(liveMode, valid.task);
|
|
16899
|
+
return { content: [{ type: "text", text }], details: {} };
|
|
16900
|
+
} catch (e) {
|
|
16901
|
+
return { content: [{ type: "text", text: `delegate_task: failed (${e instanceof Error ? e.message : String(e)}) \u2014 try again, or do the task yourself.` }], details: {} };
|
|
16902
|
+
}
|
|
16903
|
+
}
|
|
16904
|
+
});
|
|
16905
|
+
pi.registerTool({
|
|
16906
|
+
name: "update_todos",
|
|
16907
|
+
label: "Update todos",
|
|
16908
|
+
description: "Maintain a visible checklist of the steps for a multi-step task so the user can see your plan and live progress. Call it when you START a task that has several distinct steps (send the full list, each step pending), and again whenever a step's state changes: mark a step in_progress when you begin it and completed as soon as it is done. ALWAYS send the ENTIRE current list each call \u2014 it REPLACES the previous one, it is not a diff. Skip it for trivial single-step requests; a one-item checklist is just noise.",
|
|
16909
|
+
promptSnippet: "update_todos(todos) - show/refresh a checklist of steps for a multi-step task (send the whole list each call)",
|
|
16910
|
+
parameters: Type.Object({
|
|
16911
|
+
todos: Type.Array(
|
|
16912
|
+
Type.Object({
|
|
16913
|
+
content: Type.String({ description: 'The step as a short imperative phrase, e.g. "Add the /export route".' }),
|
|
16914
|
+
status: Type.Union(
|
|
16915
|
+
[Type.Literal("pending"), Type.Literal("in_progress"), Type.Literal("completed")],
|
|
16916
|
+
{ description: "pending, in_progress, or completed." }
|
|
16917
|
+
)
|
|
16918
|
+
}),
|
|
16919
|
+
{ description: "The COMPLETE current step list, in order \u2014 replaces the previous list entirely." }
|
|
16920
|
+
)
|
|
16921
|
+
}),
|
|
16922
|
+
async execute(_toolCallId, params2) {
|
|
16923
|
+
const todos = normalizeTodos(params2.todos);
|
|
16924
|
+
await sink({ event: "todos", data: { todos } });
|
|
16925
|
+
return { content: [{ type: "text", text: summarizeTodos(todos) }], details: {} };
|
|
16926
|
+
}
|
|
16927
|
+
});
|
|
16564
16928
|
if (d.tools) {
|
|
16565
16929
|
const tools2 = d.tools;
|
|
16566
16930
|
pi.registerTool({
|
|
@@ -16680,24 +17044,24 @@ ${lines.join("\n")}${more}]`
|
|
|
16680
17044
|
};
|
|
16681
17045
|
if (signal.aborted) onAbort();
|
|
16682
17046
|
else signal.addEventListener("abort", onAbort, { once: true });
|
|
17047
|
+
const turnCounter = { index: 0 };
|
|
16683
17048
|
const unsubscribe = session.subscribe((ev) => {
|
|
16684
|
-
if (ev.type === "message_update")
|
|
16685
|
-
|
|
16686
|
-
|
|
16687
|
-
void sink({ event: "delta", data: { delta: ame.delta } });
|
|
16688
|
-
} else if (ame.type === "thinking_delta") {
|
|
16689
|
-
void sink({ event: "reasoning", data: { delta: ame.delta } });
|
|
16690
|
-
}
|
|
16691
|
-
} else if (ev.type === "compaction_start") {
|
|
16692
|
-
void sink({ event: "compaction", data: { phase: "start", reason: ev.reason } });
|
|
16693
|
-
} else if (ev.type === "compaction_end") {
|
|
16694
|
-
void sink({ event: "compaction", data: { phase: "end", reason: ev.reason, aborted: ev.aborted, tokensBefore: ev.result?.tokensBefore } });
|
|
16695
|
-
}
|
|
17049
|
+
if (ev.type === "message_update") stopPrefillPoll();
|
|
17050
|
+
const frame = codeEventToFrame(ev, turnCounter);
|
|
17051
|
+
if (frame) void sink(frame);
|
|
16696
17052
|
});
|
|
17053
|
+
const steerHandle = async (text) => {
|
|
17054
|
+
if (!session.isStreaming) return false;
|
|
17055
|
+
await session.steer(text);
|
|
17056
|
+
return true;
|
|
17057
|
+
};
|
|
16697
17058
|
d.manager.generationStart();
|
|
17059
|
+
params.onSteerable?.(steerHandle);
|
|
16698
17060
|
try {
|
|
16699
17061
|
await session.prompt(task);
|
|
16700
17062
|
} finally {
|
|
17063
|
+
params.onSteerable?.(null);
|
|
17064
|
+
stopPrefillPoll();
|
|
16701
17065
|
unsubscribe();
|
|
16702
17066
|
releaseGate();
|
|
16703
17067
|
d.manager.generationEnd();
|
|
@@ -16857,7 +17221,12 @@ var CodeRunManager = class {
|
|
|
16857
17221
|
* (not just index) identifies each entry so a per-chip action (sendNow) can target one
|
|
16858
17222
|
* specific queued turn even if two queued tasks have identical text. */
|
|
16859
17223
|
queued(sessionId) {
|
|
16860
|
-
return this.sessions.get(sessionId)?.queue.map((t) => ({ userMsgId: t.userMsgId, task: t.task })) ?? [];
|
|
17224
|
+
return this.sessions.get(sessionId)?.queue.map((t) => ({ userMsgId: t.userMsgId, task: t.task, kind: t.kind })) ?? [];
|
|
17225
|
+
}
|
|
17226
|
+
/** The active turn's current todo/step checklist (ADR-255), for a (re)connecting client to show
|
|
17227
|
+
* up front. Empty when no turn has emitted one this turn (or the session is unknown). */
|
|
17228
|
+
todos(sessionId) {
|
|
17229
|
+
return this.sessions.get(sessionId)?.todos ?? [];
|
|
16861
17230
|
}
|
|
16862
17231
|
/** The in-flight turn's message ids, so a reconnecting stream can synthesize a `meta` frame
|
|
16863
17232
|
* even if the real one has already aged out of the ring buffer. */
|
|
@@ -16870,7 +17239,7 @@ var CodeRunManager = class {
|
|
|
16870
17239
|
if (!s) {
|
|
16871
17240
|
const emitter = new EventEmitter();
|
|
16872
17241
|
emitter.setMaxListeners(0);
|
|
16873
|
-
s = { convId, repoRoot, buffer: new RingBuffer(), emitter, queue: [], active: null, replayFloor: 0 };
|
|
17242
|
+
s = { convId, repoRoot, buffer: new RingBuffer(), emitter, queue: [], active: null, replayFloor: 0, todos: [] };
|
|
16874
17243
|
this.sessions.set(sessionId, s);
|
|
16875
17244
|
}
|
|
16876
17245
|
if (s.cleanupTimer) {
|
|
@@ -16889,11 +17258,36 @@ var CodeRunManager = class {
|
|
|
16889
17258
|
enqueue(sessionId, params) {
|
|
16890
17259
|
const s = this.ensure(sessionId, params.convId, params.repoRoot);
|
|
16891
17260
|
const willQueue = s.active !== null;
|
|
16892
|
-
s.queue.push({ task: params.task, userMsgId: params.userMsgId, thinkingBudget: params.thinkingBudget ?? -1 });
|
|
17261
|
+
s.queue.push({ task: params.task, userMsgId: params.userMsgId, thinkingBudget: params.thinkingBudget ?? -1, kind: params.kind ?? "followUp" });
|
|
16893
17262
|
this.emitQueue(sessionId);
|
|
16894
17263
|
void this.pump(sessionId);
|
|
16895
17264
|
return { queued: willQueue };
|
|
16896
17265
|
}
|
|
17266
|
+
/**
|
|
17267
|
+
* Steer a message into the CURRENTLY ACTIVE turn (pi's session.steer, reached via the handle
|
|
17268
|
+
* runCodeSession registers) so it redirects the running turn instead of queueing a fresh one
|
|
17269
|
+
* behind it.
|
|
17270
|
+
*
|
|
17271
|
+
* Falls back to enqueue() (follow-up/queue) rather than erroring when there is nothing live to
|
|
17272
|
+
* steer — either no active turn at all, or the active turn stopped streaming in the moment
|
|
17273
|
+
* before we reached it (a race: the turn just finished). In the fallback the message is never
|
|
17274
|
+
* dropped; it simply runs as the next turn instead, and its queue entry keeps kind:'steer' so
|
|
17275
|
+
* the UI can still tell it apart from a plain follow-up.
|
|
17276
|
+
*
|
|
17277
|
+
* Returns `steered` (delivered into the live turn) and, when it wasn't, `queued` (whether the
|
|
17278
|
+
* fallback had to wait behind an active turn, mirroring enqueue's own return).
|
|
17279
|
+
*/
|
|
17280
|
+
async steer(sessionId, params) {
|
|
17281
|
+
const active = this.sessions.get(sessionId)?.active;
|
|
17282
|
+
if (active?.steer) {
|
|
17283
|
+
try {
|
|
17284
|
+
if (await active.steer(params.task)) return { steered: true, queued: false };
|
|
17285
|
+
} catch {
|
|
17286
|
+
}
|
|
17287
|
+
}
|
|
17288
|
+
const { queued } = this.enqueue(sessionId, { ...params, kind: "steer" });
|
|
17289
|
+
return { steered: false, queued };
|
|
17290
|
+
}
|
|
16897
17291
|
/** The most recent real ctxUsed/ctxMax on record for this conversation (the last assistant
|
|
16898
17292
|
* turn that actually completed and got a genuine stats.ctxUsed from runCodeSession), or
|
|
16899
17293
|
* undefined if none exists yet. Used when a turn ABORTS before runCodeSession returns a
|
|
@@ -16937,8 +17331,9 @@ var CodeRunManager = class {
|
|
|
16937
17331
|
this.emitQueue(sessionId);
|
|
16938
17332
|
const assistantMsg = this.d.db.addMessage(s.convId, "assistant", "", { stats: { aborted: false } });
|
|
16939
17333
|
const ac = new AbortController();
|
|
16940
|
-
s.active = { ac, assistantMsgId: assistantMsg.id, userMsgId: turn.userMsgId };
|
|
17334
|
+
s.active = { ac, assistantMsgId: assistantMsg.id, userMsgId: turn.userMsgId, steer: null };
|
|
16941
17335
|
s.replayFloor = s.buffer.head();
|
|
17336
|
+
s.todos = [];
|
|
16942
17337
|
const push = (event, data) => {
|
|
16943
17338
|
const ss = this.sessions.get(sessionId);
|
|
16944
17339
|
if (!ss) return;
|
|
@@ -16960,7 +17355,10 @@ var CodeRunManager = class {
|
|
|
16960
17355
|
if (last && last.type === "text") last.text += delta;
|
|
16961
17356
|
else timeline.push({ type: "text", text: delta });
|
|
16962
17357
|
} else if (ev.event === "reasoning") reasoning += String(data.delta ?? "");
|
|
16963
|
-
else if (ev.event === "
|
|
17358
|
+
else if (ev.event === "todos") {
|
|
17359
|
+
const ss = this.sessions.get(sessionId);
|
|
17360
|
+
if (ss) ss.todos = Array.isArray(data.todos) ? data.todos : [];
|
|
17361
|
+
} else if (ev.event === "tool_call" && (data.status === "done" || data.status === "error")) {
|
|
16964
17362
|
const id = String(data.id ?? "");
|
|
16965
17363
|
toolCalls.push({
|
|
16966
17364
|
id,
|
|
@@ -16992,7 +17390,14 @@ var CodeRunManager = class {
|
|
|
16992
17390
|
thinkingBudget: turn.thinkingBudget,
|
|
16993
17391
|
task: turn.task,
|
|
16994
17392
|
signal: ac.signal,
|
|
16995
|
-
sink
|
|
17393
|
+
sink,
|
|
17394
|
+
// Publish/withdraw the live turn's steer handle so steer() can inject into THIS turn.
|
|
17395
|
+
// Reads s.active fresh each call (rather than closing over the ActiveTurn captured above)
|
|
17396
|
+
// so a null clear can't accidentally resurrect a handle onto a since-replaced turn.
|
|
17397
|
+
onSteerable: (steer) => {
|
|
17398
|
+
const cur = this.sessions.get(sessionId)?.active;
|
|
17399
|
+
if (cur && cur.ac === ac) cur.steer = steer;
|
|
17400
|
+
}
|
|
16996
17401
|
});
|
|
16997
17402
|
const finalContent = content.trim() || result.finalText;
|
|
16998
17403
|
if (!content.trim() && result.finalText.trim()) timeline.push({ type: "text", text: result.finalText });
|
|
@@ -17218,6 +17623,297 @@ function revertFileEdits(messages, repoRoot) {
|
|
|
17218
17623
|
return { reverted, failed };
|
|
17219
17624
|
}
|
|
17220
17625
|
|
|
17626
|
+
// src/code/session-export.ts
|
|
17627
|
+
var EXPORT_TRUNCATE_LIMIT = 2e4;
|
|
17628
|
+
function truncateForExport(text, limit = EXPORT_TRUNCATE_LIMIT) {
|
|
17629
|
+
if (text.length <= limit) return text;
|
|
17630
|
+
const cut = text.length - limit;
|
|
17631
|
+
return `${text.slice(0, limit)}
|
|
17632
|
+
|
|
17633
|
+
\u2026 [truncated \u2014 ${cut.toLocaleString()} more character${cut === 1 ? "" : "s"}] \u2026`;
|
|
17634
|
+
}
|
|
17635
|
+
function fence(lang, body6) {
|
|
17636
|
+
return `\`\`\`${lang}
|
|
17637
|
+
${body6}
|
|
17638
|
+
\`\`\``;
|
|
17639
|
+
}
|
|
17640
|
+
function renderReasoning(reasoning) {
|
|
17641
|
+
if (!reasoning.trim()) return "";
|
|
17642
|
+
return `<details>
|
|
17643
|
+
<summary>Reasoning</summary>
|
|
17644
|
+
|
|
17645
|
+
${reasoning.trim()}
|
|
17646
|
+
|
|
17647
|
+
</details>
|
|
17648
|
+
|
|
17649
|
+
`;
|
|
17650
|
+
}
|
|
17651
|
+
function renderToolCall(call) {
|
|
17652
|
+
const parts = [`### Tool call: \`${call.name}\``];
|
|
17653
|
+
const argsJson = (() => {
|
|
17654
|
+
try {
|
|
17655
|
+
return JSON.stringify(call.args, null, 2);
|
|
17656
|
+
} catch {
|
|
17657
|
+
return String(call.args);
|
|
17658
|
+
}
|
|
17659
|
+
})();
|
|
17660
|
+
if (argsJson && argsJson !== "{}") parts.push(fence("json", truncateForExport(argsJson)));
|
|
17661
|
+
if (call.diff) {
|
|
17662
|
+
parts.push(fence("diff", truncateForExport(call.diff)));
|
|
17663
|
+
} else if (call.error) {
|
|
17664
|
+
parts.push(`**Error:**
|
|
17665
|
+
|
|
17666
|
+
${fence("text", truncateForExport(call.error))}`);
|
|
17667
|
+
} else if (call.result) {
|
|
17668
|
+
parts.push(fence("text", truncateForExport(call.result)));
|
|
17669
|
+
}
|
|
17670
|
+
return parts.join("\n\n");
|
|
17671
|
+
}
|
|
17672
|
+
function legacyTimeline(message) {
|
|
17673
|
+
const blocks = [];
|
|
17674
|
+
if (message.content) blocks.push({ type: "text", text: message.content });
|
|
17675
|
+
for (const call of message.toolCalls) blocks.push({ type: "tool", id: call.id });
|
|
17676
|
+
return blocks;
|
|
17677
|
+
}
|
|
17678
|
+
function renderAssistantMessage(message) {
|
|
17679
|
+
const toolsById = new Map(message.toolCalls.map((t) => [t.id, t]));
|
|
17680
|
+
const timeline = message.timeline?.length ? message.timeline : legacyTimeline(message);
|
|
17681
|
+
const parts = ["## Assistant"];
|
|
17682
|
+
const reasoning = renderReasoning(message.reasoning);
|
|
17683
|
+
if (reasoning) parts.push(reasoning.trimEnd());
|
|
17684
|
+
for (const block of timeline) {
|
|
17685
|
+
if (block.type === "text") {
|
|
17686
|
+
if (block.text.trim()) parts.push(block.text.trim());
|
|
17687
|
+
} else {
|
|
17688
|
+
const call = toolsById.get(block.id);
|
|
17689
|
+
if (call) parts.push(renderToolCall(call));
|
|
17690
|
+
}
|
|
17691
|
+
}
|
|
17692
|
+
if (parts.length === 1) parts.push("*(no content)*");
|
|
17693
|
+
return parts.join("\n\n");
|
|
17694
|
+
}
|
|
17695
|
+
function renderUserMessage(message) {
|
|
17696
|
+
const parts = ["## You", message.content.trim() || "*(empty message)*"];
|
|
17697
|
+
if (message.textAttachments.length) {
|
|
17698
|
+
parts.push(`**Attached:** ${message.textAttachments.map((p) => `\`${p}\``).join(", ")}`);
|
|
17699
|
+
}
|
|
17700
|
+
return parts.join("\n\n");
|
|
17701
|
+
}
|
|
17702
|
+
function renderMessage(message) {
|
|
17703
|
+
return message.role === "user" ? renderUserMessage(message) : renderAssistantMessage(message);
|
|
17704
|
+
}
|
|
17705
|
+
function serializeCodeSessionMarkdown(run, conv, opts = {}) {
|
|
17706
|
+
const exportedAt = opts.exportedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
17707
|
+
const title = run.title.trim() || "Untitled Code session";
|
|
17708
|
+
const meta = [
|
|
17709
|
+
`- **Session ID:** ${run.id}`,
|
|
17710
|
+
run.repoRoot ? `- **Repo:** ${run.repoRoot}${run.repoBranch ? ` (branch: ${run.repoBranch})` : ""}` : void 0,
|
|
17711
|
+
`- **Mode:** ${conv.agentMode ?? "auto"}`,
|
|
17712
|
+
`- **Status:** ${run.status}`,
|
|
17713
|
+
`- **Created:** ${run.createdAt}`,
|
|
17714
|
+
`- **Exported:** ${exportedAt}`
|
|
17715
|
+
].filter((line) => !!line);
|
|
17716
|
+
const messages = (conv.messages ?? []).slice().sort((a, b) => a.seq - b.seq);
|
|
17717
|
+
const body6 = messages.length ? messages.map(renderMessage).join("\n\n---\n\n") : "*(no messages in this session yet)*";
|
|
17718
|
+
return `# ${title}
|
|
17719
|
+
|
|
17720
|
+
${meta.join("\n")}
|
|
17721
|
+
|
|
17722
|
+
---
|
|
17723
|
+
|
|
17724
|
+
${body6}
|
|
17725
|
+
`;
|
|
17726
|
+
}
|
|
17727
|
+
var MAX_FILENAME_SLUG = 80;
|
|
17728
|
+
function codeSessionExportFilename(title, createdAt, ext) {
|
|
17729
|
+
const slug = title.replace(/[^a-zA-Z0-9 _-]/g, "").trim().replace(/\s+/g, "-").slice(0, MAX_FILENAME_SLUG).replace(/-+$/, "") || "code-session";
|
|
17730
|
+
const dateStr = createdAt.slice(0, 10) || (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
17731
|
+
return `${slug}-${dateStr}.${ext}`;
|
|
17732
|
+
}
|
|
17733
|
+
|
|
17734
|
+
// src/code/git-actions.ts
|
|
17735
|
+
import { execFile as execFile12 } from "child_process";
|
|
17736
|
+
import { promisify as promisify9 } from "util";
|
|
17737
|
+
var execFileAsync = promisify9(execFile12);
|
|
17738
|
+
var GIT_TIMEOUT_MS = 15e3;
|
|
17739
|
+
var GIT_NETWORK_TIMEOUT_MS = 3e4;
|
|
17740
|
+
var GitError = class extends Error {
|
|
17741
|
+
stderr;
|
|
17742
|
+
exitCode;
|
|
17743
|
+
constructor(message, stderr, exitCode) {
|
|
17744
|
+
super(message);
|
|
17745
|
+
this.name = "GitError";
|
|
17746
|
+
this.stderr = stderr;
|
|
17747
|
+
this.exitCode = exitCode;
|
|
17748
|
+
}
|
|
17749
|
+
};
|
|
17750
|
+
async function git(args, cwd, timeoutMs = GIT_TIMEOUT_MS) {
|
|
17751
|
+
try {
|
|
17752
|
+
const { stdout } = await execFileAsync("git", args, {
|
|
17753
|
+
cwd,
|
|
17754
|
+
timeout: timeoutMs,
|
|
17755
|
+
windowsHide: true,
|
|
17756
|
+
// Never block on an interactive username/password/passphrase prompt — surface the auth
|
|
17757
|
+
// failure as a normal command error instead of hanging the request.
|
|
17758
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
|
|
17759
|
+
});
|
|
17760
|
+
return stdout.trim();
|
|
17761
|
+
} catch (e) {
|
|
17762
|
+
const failure = e;
|
|
17763
|
+
if (failure.killed) throw new GitError(`git ${args[0]} timed out after ${timeoutMs}ms`, failure.stderr ?? "", null);
|
|
17764
|
+
throw new GitError((failure.stderr ?? "").trim() || failure.message, failure.stderr ?? "", failure.code ?? null);
|
|
17765
|
+
}
|
|
17766
|
+
}
|
|
17767
|
+
var EMPTY_STATUS = {
|
|
17768
|
+
isRepo: false,
|
|
17769
|
+
branch: "",
|
|
17770
|
+
detached: false,
|
|
17771
|
+
files: [],
|
|
17772
|
+
hasRemote: false,
|
|
17773
|
+
hasUpstream: false,
|
|
17774
|
+
ahead: 0,
|
|
17775
|
+
behind: 0
|
|
17776
|
+
};
|
|
17777
|
+
async function getGitStatus(repoRoot) {
|
|
17778
|
+
try {
|
|
17779
|
+
if (await git(["rev-parse", "--is-inside-work-tree"], repoRoot) !== "true") return EMPTY_STATUS;
|
|
17780
|
+
} catch {
|
|
17781
|
+
return EMPTY_STATUS;
|
|
17782
|
+
}
|
|
17783
|
+
let branch = "";
|
|
17784
|
+
let detached = false;
|
|
17785
|
+
try {
|
|
17786
|
+
branch = await git(["rev-parse", "--abbrev-ref", "HEAD"], repoRoot);
|
|
17787
|
+
if (branch === "HEAD") {
|
|
17788
|
+
detached = true;
|
|
17789
|
+
branch = "";
|
|
17790
|
+
}
|
|
17791
|
+
} catch {
|
|
17792
|
+
}
|
|
17793
|
+
let hasRemote = false;
|
|
17794
|
+
try {
|
|
17795
|
+
hasRemote = (await git(["remote"], repoRoot)).length > 0;
|
|
17796
|
+
} catch {
|
|
17797
|
+
}
|
|
17798
|
+
let hasUpstream = false;
|
|
17799
|
+
let ahead = 0;
|
|
17800
|
+
let behind = 0;
|
|
17801
|
+
if (!detached && branch) {
|
|
17802
|
+
try {
|
|
17803
|
+
await git(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], repoRoot);
|
|
17804
|
+
hasUpstream = true;
|
|
17805
|
+
const counts = await git(["rev-list", "--left-right", "--count", "HEAD...@{u}"], repoRoot);
|
|
17806
|
+
const [a, b] = counts.split(/\s+/).map((n) => parseInt(n, 10) || 0);
|
|
17807
|
+
ahead = a ?? 0;
|
|
17808
|
+
behind = b ?? 0;
|
|
17809
|
+
} catch {
|
|
17810
|
+
}
|
|
17811
|
+
}
|
|
17812
|
+
const raw = await git(["status", "--porcelain=v1"], repoRoot);
|
|
17813
|
+
const files = raw ? raw.split("\n").filter(Boolean).map((line) => ({ code: line.slice(0, 2), path: line.slice(3) })) : [];
|
|
17814
|
+
return { isRepo: true, branch, detached, files, hasRemote, hasUpstream, ahead, behind };
|
|
17815
|
+
}
|
|
17816
|
+
async function commitGitChanges(repoRoot, message, files) {
|
|
17817
|
+
const trimmedMessage = message.trim();
|
|
17818
|
+
if (!trimmedMessage) throw new GitError("A commit message is required.", "", null);
|
|
17819
|
+
const status = await getGitStatus(repoRoot);
|
|
17820
|
+
if (!status.isRepo) throw new GitError("Not a git repository.", "", null);
|
|
17821
|
+
if (status.files.length === 0) throw new GitError("Nothing to commit \u2014 the working tree is clean.", "", null);
|
|
17822
|
+
if (files && files.length > 0) {
|
|
17823
|
+
for (const f of files) {
|
|
17824
|
+
if (!isContainedFromRoot(f, repoRoot)) throw new GitError(`Refusing to stage a path outside the repo: ${f}`, "", null);
|
|
17825
|
+
}
|
|
17826
|
+
await git(["add", "--", ...files], repoRoot);
|
|
17827
|
+
} else {
|
|
17828
|
+
await git(["add", "-A"], repoRoot);
|
|
17829
|
+
}
|
|
17830
|
+
const staged = await git(["diff", "--cached", "--name-only"], repoRoot).catch(() => "");
|
|
17831
|
+
const stagedList = staged.split("\n").filter(Boolean);
|
|
17832
|
+
if (stagedList.length === 0) throw new GitError("Nothing staged \u2014 the selected file(s) have no changes.", "", null);
|
|
17833
|
+
await git(["commit", "-m", trimmedMessage], repoRoot);
|
|
17834
|
+
const hash = await git(["rev-parse", "HEAD"], repoRoot);
|
|
17835
|
+
return { hash, filesCommitted: stagedList.length };
|
|
17836
|
+
}
|
|
17837
|
+
async function pushGitBranch(repoRoot) {
|
|
17838
|
+
const status = await getGitStatus(repoRoot);
|
|
17839
|
+
if (!status.isRepo) return { ok: false, reason: "not_a_repo", message: "Not a git repository." };
|
|
17840
|
+
if (status.detached) {
|
|
17841
|
+
return { ok: false, reason: "detached_head", message: "Cannot push from a detached HEAD \u2014 check out a branch first." };
|
|
17842
|
+
}
|
|
17843
|
+
if (!status.hasRemote) return { ok: false, reason: "no_remote", message: "This repo has no configured remote." };
|
|
17844
|
+
const args = status.hasUpstream ? ["push"] : ["push", "--set-upstream", "origin", status.branch];
|
|
17845
|
+
try {
|
|
17846
|
+
await git(args, repoRoot, GIT_NETWORK_TIMEOUT_MS);
|
|
17847
|
+
return { ok: true, remote: "origin", branch: status.branch };
|
|
17848
|
+
} catch (e) {
|
|
17849
|
+
const ge = e;
|
|
17850
|
+
if (/\[rejected\]|non-fast-forward|fetch first|tip of your current branch is behind/i.test(ge.stderr)) {
|
|
17851
|
+
return {
|
|
17852
|
+
ok: false,
|
|
17853
|
+
reason: "diverged",
|
|
17854
|
+
message: "The remote has commits this branch doesn't have. Pull/rebase first \u2014 never force-pushed automatically."
|
|
17855
|
+
};
|
|
17856
|
+
}
|
|
17857
|
+
return { ok: false, reason: "push_failed", message: ge.message };
|
|
17858
|
+
}
|
|
17859
|
+
}
|
|
17860
|
+
function buildGithubCompareUrl(remoteUrl, branch) {
|
|
17861
|
+
const trimmed = remoteUrl.trim();
|
|
17862
|
+
const httpsMatch = trimmed.match(/^https?:\/\/(?:[^@/]+@)?github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/);
|
|
17863
|
+
const sshMatch = trimmed.match(/^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
17864
|
+
const m = httpsMatch ?? sshMatch;
|
|
17865
|
+
if (!m) return null;
|
|
17866
|
+
const [, owner, repo] = m;
|
|
17867
|
+
return `https://github.com/${owner}/${repo}/compare/${encodeURIComponent(branch)}?expand=1`;
|
|
17868
|
+
}
|
|
17869
|
+
async function getGithubCompareUrl(repoRoot, branch) {
|
|
17870
|
+
let remoteUrl;
|
|
17871
|
+
try {
|
|
17872
|
+
remoteUrl = await git(["remote", "get-url", "origin"], repoRoot);
|
|
17873
|
+
} catch {
|
|
17874
|
+
return null;
|
|
17875
|
+
}
|
|
17876
|
+
return buildGithubCompareUrl(remoteUrl, branch);
|
|
17877
|
+
}
|
|
17878
|
+
|
|
17879
|
+
// src/code/code-shell.ts
|
|
17880
|
+
var SHELL_MAX_OUTPUT = 1e5;
|
|
17881
|
+
var SHELL_TIMEOUT_SEC = 60;
|
|
17882
|
+
async function runShellCommand(command, repoRoot, sessionLabel, opts) {
|
|
17883
|
+
const maxOutput = opts?.maxOutput ?? SHELL_MAX_OUTPUT;
|
|
17884
|
+
const timeoutSec = opts?.timeoutSec ?? SHELL_TIMEOUT_SEC;
|
|
17885
|
+
const ops = createRobustBashOperations({ sessionLabel });
|
|
17886
|
+
let output = "";
|
|
17887
|
+
let truncated = false;
|
|
17888
|
+
const onData = (chunk) => {
|
|
17889
|
+
if (truncated) return;
|
|
17890
|
+
output += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
17891
|
+
if (output.length > maxOutput) {
|
|
17892
|
+
output = output.slice(0, maxOutput);
|
|
17893
|
+
truncated = true;
|
|
17894
|
+
}
|
|
17895
|
+
};
|
|
17896
|
+
const signal = opts?.signal ?? new AbortController().signal;
|
|
17897
|
+
try {
|
|
17898
|
+
const { exitCode } = await ops.exec(command, repoRoot, { onData, signal, timeout: timeoutSec, env: process.env });
|
|
17899
|
+
return { command, output, exitCode, timedOut: false, truncated };
|
|
17900
|
+
} catch (e) {
|
|
17901
|
+
const msg2 = e instanceof Error ? e.message : String(e);
|
|
17902
|
+
if (msg2.startsWith("timeout:")) return { command, output, exitCode: null, timedOut: true, truncated };
|
|
17903
|
+
throw e;
|
|
17904
|
+
}
|
|
17905
|
+
}
|
|
17906
|
+
function shellContextText(result) {
|
|
17907
|
+
const body6 = result.output.trim() || "(no output)";
|
|
17908
|
+
const status = result.timedOut ? ` (timed out after ${SHELL_TIMEOUT_SEC}s)` : result.exitCode !== null && result.exitCode !== 0 ? ` (exit code ${result.exitCode})` : "";
|
|
17909
|
+
const trunc = result.truncated ? "\n\u2026(output truncated)" : "";
|
|
17910
|
+
return `I ran a shell command in the repo${status}:
|
|
17911
|
+
|
|
17912
|
+
$ ${result.command}
|
|
17913
|
+
|
|
17914
|
+
${body6}${trunc}`;
|
|
17915
|
+
}
|
|
17916
|
+
|
|
17221
17917
|
// src/code/code-routes.ts
|
|
17222
17918
|
function err5(c, s, code, msg2) {
|
|
17223
17919
|
return c.json({ error: { code, message: msg2 } }, s);
|
|
@@ -17282,6 +17978,8 @@ function toSidebarRow(run) {
|
|
|
17282
17978
|
del: run.linesRemoved ?? 0,
|
|
17283
17979
|
mode: void 0,
|
|
17284
17980
|
// filled from conv below when available
|
|
17981
|
+
running: void 0,
|
|
17982
|
+
// filled from CodeRunManager below when available
|
|
17285
17983
|
createdAt: run.createdAt,
|
|
17286
17984
|
repoRoot: run.repoRoot ?? "",
|
|
17287
17985
|
error: run.error,
|
|
@@ -17327,16 +18025,17 @@ function registerCodeRoutes(app2, d) {
|
|
|
17327
18025
|
});
|
|
17328
18026
|
app2.get("/api/v1/code/sessions", (c) => {
|
|
17329
18027
|
const filter = c.req.query("filter") ?? "active";
|
|
17330
|
-
const
|
|
18028
|
+
const agentRuns = db2.listAgentRuns().filter((r) => {
|
|
17331
18029
|
const conv = db2.getConversation(r.convId);
|
|
17332
18030
|
if (conv?.kind !== "code") return false;
|
|
17333
18031
|
if (filter === "active") return !r.archivedAt;
|
|
17334
18032
|
if (filter === "archived") return !!r.archivedAt;
|
|
17335
18033
|
return true;
|
|
17336
18034
|
});
|
|
17337
|
-
const rows =
|
|
18035
|
+
const rows = agentRuns.map((r) => {
|
|
17338
18036
|
const row = toSidebarRow(r);
|
|
17339
18037
|
row.mode = db2.getConversation(r.convId)?.agentMode;
|
|
18038
|
+
row.running = runs.isActive(r.id);
|
|
17340
18039
|
return row;
|
|
17341
18040
|
});
|
|
17342
18041
|
return c.json({ sessions: rows });
|
|
@@ -17356,9 +18055,31 @@ function registerCodeRoutes(app2, d) {
|
|
|
17356
18055
|
running: runs.isActive(run.id),
|
|
17357
18056
|
// Tasks waiting behind the active turn — the server-side message queue, so its chips
|
|
17358
18057
|
// survive a page reload / reconnect.
|
|
17359
|
-
queued: runs.queued(run.id)
|
|
18058
|
+
queued: runs.queued(run.id),
|
|
18059
|
+
// The active turn's current step checklist (ADR-255), so a reopened session shows live
|
|
18060
|
+
// progress immediately without waiting for the next update_todos frame. [] when none.
|
|
18061
|
+
todos: runs.todos(run.id),
|
|
18062
|
+
// Whether an AGENTS.md is actually loaded for this session (ADR-262's loaded-resources
|
|
18063
|
+
// header) — project (<repoRoot>/AGENTS.md) and/or global (~/.turbollm/agents.md). Computed
|
|
18064
|
+
// with the SAME lookup persona.ts injects into the prompt, so "shown as loaded" == "actually
|
|
18065
|
+
// fed to the model". Both false when the session has no repoRoot.
|
|
18066
|
+
hasAgentsMd: run.repoRoot ? agentsMdPresence(run.repoRoot, d.store.dir()) : { project: false, global: false }
|
|
17360
18067
|
});
|
|
17361
18068
|
});
|
|
18069
|
+
app2.get("/api/v1/code/sessions/:id/export", (c) => {
|
|
18070
|
+
const id = c.req.param("id");
|
|
18071
|
+
const run = db2.getAgentRun(id);
|
|
18072
|
+
if (!run) return err5(c, 404, "not_found", "Session not found.");
|
|
18073
|
+
const conv = db2.getConversation(run.convId, true);
|
|
18074
|
+
if (!conv) return err5(c, 404, "not_found", "Session conversation not found.");
|
|
18075
|
+
const format = c.req.query("format") === "html" ? "html" : "markdown";
|
|
18076
|
+
if (format === "html") return err5(c, 400, "unsupported_format", "HTML export is not available yet \u2014 use format=markdown.");
|
|
18077
|
+
const md = serializeCodeSessionMarkdown(run, conv);
|
|
18078
|
+
const filename = codeSessionExportFilename(run.title, run.createdAt, "md");
|
|
18079
|
+
c.header("Content-Disposition", `attachment; filename="${filename}"`);
|
|
18080
|
+
c.header("Content-Type", "text/markdown; charset=utf-8");
|
|
18081
|
+
return c.body(md);
|
|
18082
|
+
});
|
|
17362
18083
|
app2.patch("/api/v1/code/sessions/:id/mode", async (c) => {
|
|
17363
18084
|
const id = c.req.param("id");
|
|
17364
18085
|
const run = db2.getAgentRun(id);
|
|
@@ -17434,6 +18155,7 @@ function registerCodeRoutes(app2, d) {
|
|
|
17434
18155
|
const lastMsg = (conv?.messages ?? []).at(-1);
|
|
17435
18156
|
if (!lastMsg) return err5(c, 400, "nothing_to_clear", "Nothing to clear yet.");
|
|
17436
18157
|
if (run.clearedUpToMessageId === lastMsg.id) return err5(c, 400, "nothing_to_clear", "Already cleared up to the latest message.");
|
|
18158
|
+
db2.deactivateMessagesUpTo(run.convId, lastMsg.id);
|
|
17437
18159
|
db2.setClearedUpToMessageId(id, lastMsg.id);
|
|
17438
18160
|
return c.json({ ok: true, clearedUpToMessageId: lastMsg.id });
|
|
17439
18161
|
});
|
|
@@ -17448,6 +18170,7 @@ function registerCodeRoutes(app2, d) {
|
|
|
17448
18170
|
return c.json({ ok: true });
|
|
17449
18171
|
}
|
|
17450
18172
|
if (!run.clearedUpToMessageId) return err5(c, 400, "not_cleared", "This session has not been cleared.");
|
|
18173
|
+
db2.reactivateMessagesUpTo(run.convId, run.clearedUpToMessageId);
|
|
17451
18174
|
db2.setClearedUpToMessageId(id, null);
|
|
17452
18175
|
return c.json({ ok: true });
|
|
17453
18176
|
});
|
|
@@ -17483,6 +18206,93 @@ function registerCodeRoutes(app2, d) {
|
|
|
17483
18206
|
db2.setRevertedFromMessageId(id, messageId);
|
|
17484
18207
|
return c.json({ ok: true, revertedFromMessageId: messageId, revertText: cut.revertText, revertedFiles, failedFiles });
|
|
17485
18208
|
});
|
|
18209
|
+
app2.get("/api/v1/code/sessions/:id/git/status", async (c) => {
|
|
18210
|
+
const run = db2.getAgentRun(c.req.param("id"));
|
|
18211
|
+
if (!run) return err5(c, 404, "not_found", "Session not found.");
|
|
18212
|
+
if (!run.repoRoot) return err5(c, 400, "invalid_input", "Session has no repo root.");
|
|
18213
|
+
const status = await getGitStatus(run.repoRoot);
|
|
18214
|
+
return c.json({ ok: true, status });
|
|
18215
|
+
});
|
|
18216
|
+
app2.post("/api/v1/code/sessions/:id/git/commit", async (c) => {
|
|
18217
|
+
const id = c.req.param("id");
|
|
18218
|
+
const run = db2.getAgentRun(id);
|
|
18219
|
+
if (!run) return err5(c, 404, "not_found", "Session not found.");
|
|
18220
|
+
if (!run.repoRoot) return err5(c, 400, "invalid_input", "Session has no repo root.");
|
|
18221
|
+
if (runs.isActive(id)) return err5(c, 409, "run_active", "Stop or wait for the current run before committing.");
|
|
18222
|
+
const b = await body5(c);
|
|
18223
|
+
try {
|
|
18224
|
+
const result = await commitGitChanges(run.repoRoot, b.message ?? "", b.files);
|
|
18225
|
+
return c.json({ ok: true, ...result });
|
|
18226
|
+
} catch (e) {
|
|
18227
|
+
const message = e instanceof Error ? e.message : "Commit failed.";
|
|
18228
|
+
if (/commit message is required/i.test(message)) return err5(c, 400, "invalid_input", message);
|
|
18229
|
+
if (/not a git repository/i.test(message)) return err5(c, 400, "not_a_git_repo", message);
|
|
18230
|
+
if (/nothing to commit|nothing staged/i.test(message)) return err5(c, 400, "nothing_to_commit", message);
|
|
18231
|
+
if (/outside the repo/i.test(message)) return err5(c, 400, "invalid_input", message);
|
|
18232
|
+
return err5(c, 500, "git_commit_failed", message);
|
|
18233
|
+
}
|
|
18234
|
+
});
|
|
18235
|
+
app2.post("/api/v1/code/sessions/:id/git/push", async (c) => {
|
|
18236
|
+
const id = c.req.param("id");
|
|
18237
|
+
const run = db2.getAgentRun(id);
|
|
18238
|
+
if (!run) return err5(c, 404, "not_found", "Session not found.");
|
|
18239
|
+
if (!run.repoRoot) return err5(c, 400, "invalid_input", "Session has no repo root.");
|
|
18240
|
+
if (runs.isActive(id)) return err5(c, 409, "run_active", "Stop or wait for the current run before pushing.");
|
|
18241
|
+
const result = await pushGitBranch(run.repoRoot);
|
|
18242
|
+
if (!result.ok) {
|
|
18243
|
+
const status = result.reason === "diverged" ? 409 : 400;
|
|
18244
|
+
return err5(c, status, result.reason, result.message);
|
|
18245
|
+
}
|
|
18246
|
+
const compareUrl = await getGithubCompareUrl(run.repoRoot, result.branch);
|
|
18247
|
+
return c.json({ ok: true, remote: result.remote, branch: result.branch, compareUrl });
|
|
18248
|
+
});
|
|
18249
|
+
app2.get("/api/v1/code/sessions/:id/git/compare-url", async (c) => {
|
|
18250
|
+
const run = db2.getAgentRun(c.req.param("id"));
|
|
18251
|
+
if (!run) return err5(c, 404, "not_found", "Session not found.");
|
|
18252
|
+
if (!run.repoRoot) return err5(c, 400, "invalid_input", "Session has no repo root.");
|
|
18253
|
+
const status = await getGitStatus(run.repoRoot);
|
|
18254
|
+
if (!status.isRepo || status.detached || !status.branch) return c.json({ ok: true, compareUrl: null });
|
|
18255
|
+
const compareUrl = await getGithubCompareUrl(run.repoRoot, status.branch);
|
|
18256
|
+
return c.json({ ok: true, compareUrl });
|
|
18257
|
+
});
|
|
18258
|
+
app2.post("/api/v1/code/sessions/:id/exec", async (c) => {
|
|
18259
|
+
const id = c.req.param("id");
|
|
18260
|
+
const run = db2.getAgentRun(id);
|
|
18261
|
+
if (!run) return err5(c, 404, "not_found", "Session not found.");
|
|
18262
|
+
if (!run.repoRoot) return err5(c, 400, "invalid_input", "Session has no repo root.");
|
|
18263
|
+
if (runs.isActive(id)) return err5(c, 409, "run_active", "Stop or wait for the current run before running a command.");
|
|
18264
|
+
const b = await body5(c);
|
|
18265
|
+
const command = (b.command ?? "").trim();
|
|
18266
|
+
if (!command) return err5(c, 400, "invalid_input", "A command is required.");
|
|
18267
|
+
const feedToModel = b.feedToModel !== false;
|
|
18268
|
+
let result;
|
|
18269
|
+
try {
|
|
18270
|
+
result = await runShellCommand(command, run.repoRoot, id);
|
|
18271
|
+
} catch (e) {
|
|
18272
|
+
return err5(c, 500, "exec_failed", e instanceof Error ? e.message : "Command failed to run.");
|
|
18273
|
+
}
|
|
18274
|
+
let messageId;
|
|
18275
|
+
if (feedToModel) {
|
|
18276
|
+
const msg2 = db2.addMessage(run.convId, "user", shellContextText(result), {
|
|
18277
|
+
toolCalls: [{
|
|
18278
|
+
id: `shell-${Date.now()}`,
|
|
18279
|
+
name: "shell",
|
|
18280
|
+
args: { command, exitCode: result.exitCode, timedOut: result.timedOut },
|
|
18281
|
+
result: result.output
|
|
18282
|
+
}]
|
|
18283
|
+
});
|
|
18284
|
+
messageId = msg2.id;
|
|
18285
|
+
}
|
|
18286
|
+
return c.json({
|
|
18287
|
+
ok: true,
|
|
18288
|
+
command,
|
|
18289
|
+
output: result.output,
|
|
18290
|
+
exitCode: result.exitCode,
|
|
18291
|
+
timedOut: result.timedOut,
|
|
18292
|
+
truncated: result.truncated,
|
|
18293
|
+
messageId
|
|
18294
|
+
}, 200);
|
|
18295
|
+
});
|
|
17486
18296
|
app2.post("/api/v1/code/sessions/:id/stop", (c) => {
|
|
17487
18297
|
const id = c.req.param("id");
|
|
17488
18298
|
runs.stop(id);
|
|
@@ -17497,6 +18307,7 @@ function registerCodeRoutes(app2, d) {
|
|
|
17497
18307
|
app2.post("/api/v1/code/sessions/:id/messages", async (c) => {
|
|
17498
18308
|
const id = c.req.param("id");
|
|
17499
18309
|
const b = await body5(c);
|
|
18310
|
+
const kind = b.kind === "steer" ? "steer" : "followUp";
|
|
17500
18311
|
const run = db2.getAgentRun(id);
|
|
17501
18312
|
if (!run) return err5(c, 404, "not_found", "Session not found.");
|
|
17502
18313
|
const conv = db2.getConversation(run.convId, true);
|
|
@@ -17505,20 +18316,25 @@ function registerCodeRoutes(app2, d) {
|
|
|
17505
18316
|
const ms = d.manager.status();
|
|
17506
18317
|
if (ms.state !== "running" || !ms.model) return err5(c, 409, "model_not_loaded", "Load a model first.");
|
|
17507
18318
|
if (!d.manager.target()) return err5(c, 409, "model_not_loaded", "Engine not running.");
|
|
17508
|
-
const
|
|
18319
|
+
const newContent = (b.content ?? "").trim();
|
|
17509
18320
|
let userMsgId;
|
|
17510
18321
|
let task;
|
|
17511
|
-
if (
|
|
17512
|
-
userMsgId = db2.addMessage(run.convId, "user",
|
|
17513
|
-
task = contextFilesBlock(b.contextFiles) + ((b.promptOverride ?? "").trim() ||
|
|
18322
|
+
if (newContent) {
|
|
18323
|
+
userMsgId = db2.addMessage(run.convId, "user", newContent, { textAttachments: b.contextFiles }).id;
|
|
18324
|
+
task = contextFilesBlock(b.contextFiles) + ((b.promptOverride ?? "").trim() || newContent);
|
|
17514
18325
|
} else {
|
|
17515
18326
|
const lastUser = (conv.messages ?? []).filter((m) => m.role === "user").at(-1);
|
|
17516
18327
|
if (!lastUser) return err5(c, 400, "no_task", "No task to run.");
|
|
17517
18328
|
userMsgId = lastUser.id;
|
|
17518
18329
|
task = contextFilesBlock(lastUser.textAttachments) + lastUser.content;
|
|
17519
18330
|
}
|
|
17520
|
-
const
|
|
17521
|
-
|
|
18331
|
+
const enqueueParams = { convId: run.convId, repoRoot: run.repoRoot, task, userMsgId, thinkingBudget: b.thinkingBudget };
|
|
18332
|
+
if (kind === "steer") {
|
|
18333
|
+
const { steered, queued: queued2 } = await runs.steer(id, enqueueParams);
|
|
18334
|
+
return c.json({ ok: true, queued: queued2, steered, userMessageId: userMsgId }, 202);
|
|
18335
|
+
}
|
|
18336
|
+
const { queued } = runs.enqueue(id, enqueueParams);
|
|
18337
|
+
return c.json({ ok: true, queued, steered: false, userMessageId: userMsgId }, 202);
|
|
17522
18338
|
});
|
|
17523
18339
|
app2.get("/api/v1/code/sessions/:id/stream", (c) => {
|
|
17524
18340
|
const id = c.req.param("id");
|
|
@@ -17534,6 +18350,8 @@ function registerCodeRoutes(app2, d) {
|
|
|
17534
18350
|
const meta = runs.activeMeta(id);
|
|
17535
18351
|
if (meta) await stream.writeSSE({ event: "meta", data: JSON.stringify(meta) });
|
|
17536
18352
|
await stream.writeSSE({ event: "queue", data: JSON.stringify({ queued: runs.queued(id) }) });
|
|
18353
|
+
const currentTodos = runs.todos(id);
|
|
18354
|
+
if (currentTodos.length > 0) await stream.writeSSE({ event: "todos", data: JSON.stringify({ todos: currentTodos }) });
|
|
17537
18355
|
for await (const ev of sub) {
|
|
17538
18356
|
await stream.writeSSE({ id: String(ev.seq), event: ev.event, data: JSON.stringify(ev.data) });
|
|
17539
18357
|
}
|
|
@@ -18219,7 +19037,7 @@ function contentType(file) {
|
|
|
18219
19037
|
}
|
|
18220
19038
|
|
|
18221
19039
|
// src/tunnel/manager.ts
|
|
18222
|
-
import { execFile as
|
|
19040
|
+
import { execFile as execFile13, spawn as spawn7, spawnSync as spawnSync2 } from "child_process";
|
|
18223
19041
|
import { mkdirSync as mkdirSync15, readFileSync as readFileSync14, readdirSync as readdirSync9, rmSync as rmSync14, writeFileSync as writeFileSync13 } from "fs";
|
|
18224
19042
|
import { join as join31 } from "path";
|
|
18225
19043
|
|
|
@@ -18299,13 +19117,13 @@ function sleep4(ms) {
|
|
|
18299
19117
|
}
|
|
18300
19118
|
function signalTerm2(child) {
|
|
18301
19119
|
if (!child.pid) return;
|
|
18302
|
-
if (process.platform === "win32")
|
|
19120
|
+
if (process.platform === "win32") execFile13("taskkill", ["/PID", String(child.pid), "/T"], () => {
|
|
18303
19121
|
});
|
|
18304
19122
|
else child.kill("SIGTERM");
|
|
18305
19123
|
}
|
|
18306
19124
|
function forceKill2(child) {
|
|
18307
19125
|
if (!child.pid) return;
|
|
18308
|
-
if (process.platform === "win32")
|
|
19126
|
+
if (process.platform === "win32") execFile13("taskkill", ["/PID", String(child.pid), "/F", "/T"], () => {
|
|
18309
19127
|
});
|
|
18310
19128
|
else child.kill("SIGKILL");
|
|
18311
19129
|
}
|