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.
Files changed (110) hide show
  1. package/dist/cli.js +858 -40
  2. package/dist/webdist/assets/{AgentEditPage-BaaqMpWR.js → AgentEditPage-F6wQ-woa.js} +1 -1
  3. package/dist/webdist/assets/{ChatScreen-C2DYwxeR.js → ChatScreen-JNejy4c5.js} +1 -1
  4. package/dist/webdist/assets/CodeComposer-BMivBZTt.js +1 -0
  5. package/dist/webdist/assets/{CodeHomeScreen-B_rbjGaw.js → CodeHomeScreen-Bvv8dpoH.js} +1 -1
  6. package/dist/webdist/assets/CodeSessionScreen-BZ_n-Q8C.js +5 -0
  7. package/dist/webdist/assets/CustomizeScreen-CfQYShcO.js +4 -0
  8. package/dist/webdist/assets/DeveloperScreen-DlLMgh2N.js +1 -0
  9. package/dist/webdist/assets/{EnginesScreen-jM6G9Lnt.js → EnginesScreen-CCzC8Dth.js} +6 -6
  10. package/dist/webdist/assets/{FsBrowser-CWzT3VbL.js → FsBrowser-d8UO9bVy.js} +1 -1
  11. package/dist/webdist/assets/ModelLoadMenu-BcSALweE.js +2 -0
  12. package/dist/webdist/assets/{ModelsScreen-D0-Onb6V.js → ModelsScreen-DHqnZlFL.js} +1 -1
  13. package/dist/webdist/assets/{SettingsScreen-C-k9Yc55.js → SettingsScreen-Byi4wqlL.js} +1 -1
  14. package/dist/webdist/assets/{SkillEditPage-DVIYJU7N.js → SkillEditPage-CKfpniwp.js} +1 -1
  15. package/dist/webdist/assets/{TokensScreen-DC4sSZK8.js → TokensScreen-CSxwCWhD.js} +1 -1
  16. package/dist/webdist/assets/{ToolApprovalBar-BYyXPiNP.js → ToolApprovalBar-CDRoIK9R.js} +15 -15
  17. package/dist/webdist/assets/WorkspaceScreen-CY5EZKSy.js +1 -0
  18. package/dist/webdist/assets/{agent-api-DBuqrdZq.js → agent-api-DaWwB_7n.js} +1 -1
  19. package/dist/webdist/assets/{arc-Banj5wBf.js → arc-Bw5r0gQ8.js} +1 -1
  20. package/dist/webdist/assets/{architectureDiagram-3BPJPVTR-D-VtkgYv.js → architectureDiagram-3BPJPVTR-D_qgeydj.js} +1 -1
  21. package/dist/webdist/assets/{arrow-left-C1r8Dr2d.js → arrow-left-Bs4KhSqC.js} +1 -1
  22. package/dist/webdist/assets/{badge-DMOKG62I.js → badge-CPXRwtV2.js} +1 -1
  23. package/dist/webdist/assets/{blockDiagram-GPEHLZMM-nvbn1Awx.js → blockDiagram-GPEHLZMM-BbzqG8AR.js} +1 -1
  24. package/dist/webdist/assets/{bot-CTxax61P.js → bot-DUgi89DH.js} +1 -1
  25. package/dist/webdist/assets/{c4Diagram-AAUBKEIU-DoG1JXd6.js → c4Diagram-AAUBKEIU-Bltku6hD.js} +1 -1
  26. package/dist/webdist/assets/channel-Cob2-fKv.js +1 -0
  27. package/dist/webdist/assets/{chat-api-B81lfnq0.js → chat-api-DnoBopyh.js} +1 -1
  28. package/dist/webdist/assets/{chat-queries-BcaqUVgR.js → chat-queries-EOqarO2W.js} +1 -1
  29. package/dist/webdist/assets/check-C-_wc2AD.js +1 -0
  30. package/dist/webdist/assets/{chevron-left-BdgNx91c.js → chevron-left-CFIaBRPc.js} +1 -1
  31. package/dist/webdist/assets/{chevron-right-BXxjw90s.js → chevron-right-DT5tldLF.js} +1 -1
  32. package/dist/webdist/assets/{chunk-2J33WTMH-CI01KUY5.js → chunk-2J33WTMH-jRmwQd0-.js} +1 -1
  33. package/dist/webdist/assets/{chunk-4BX2VUAB-CT4Q6KgA.js → chunk-4BX2VUAB-BkQR5P6X.js} +1 -1
  34. package/dist/webdist/assets/{chunk-55IACEB6-DVqUTluK.js → chunk-55IACEB6-dHYKdZGl.js} +1 -1
  35. package/dist/webdist/assets/{chunk-727SXJPM-2pMnnV6j.js → chunk-727SXJPM-Bac05mkQ.js} +1 -1
  36. package/dist/webdist/assets/{chunk-AQP2D5EJ-q_bI8ZG1.js → chunk-AQP2D5EJ-DQSeJwV8.js} +1 -1
  37. package/dist/webdist/assets/{chunk-FMBD7UC4-B8gQv0l1.js → chunk-FMBD7UC4-fw71OGhl.js} +1 -1
  38. package/dist/webdist/assets/{chunk-ND2GUHAM-DJx_4zvu.js → chunk-ND2GUHAM-xRyemSmx.js} +1 -1
  39. package/dist/webdist/assets/{chunk-QZHKN3VN-CrAA0uxl.js → chunk-QZHKN3VN-BgNNqsB9.js} +1 -1
  40. package/dist/webdist/assets/{circle-check-CzXTbeIt.js → circle-check-CLNNkErq.js} +1 -1
  41. package/dist/webdist/assets/{circle-x-CfrulclP.js → circle-x-DHTcnBYg.js} +1 -1
  42. package/dist/webdist/assets/classDiagram-4FO5ZUOK-emMvNtz-.js +1 -0
  43. package/dist/webdist/assets/classDiagram-v2-Q7XG4LA2-emMvNtz-.js +1 -0
  44. package/dist/webdist/assets/{common-DAcb-w9o.js → common-Bh_ckDae.js} +1 -1
  45. package/dist/webdist/assets/copy-button-B0AUQv8-.js +1 -0
  46. package/dist/webdist/assets/{cose-bilkent-S5V4N54A-EGQLoy7D.js → cose-bilkent-S5V4N54A-BpeA37l_.js} +1 -1
  47. package/dist/webdist/assets/{dagre-BM42HDAG-DC_RQ1B0.js → dagre-BM42HDAG-Dhe-FxnK.js} +1 -1
  48. package/dist/webdist/assets/{diagram-2AECGRRQ-cZcCJYfi.js → diagram-2AECGRRQ-CsOhE15t.js} +1 -1
  49. package/dist/webdist/assets/{diagram-5GNKFQAL-Cb-jxgLV.js → diagram-5GNKFQAL-Dj4NKMxD.js} +1 -1
  50. package/dist/webdist/assets/{diagram-KO2AKTUF-D4INKoTb.js → diagram-KO2AKTUF-BVeDuCrX.js} +1 -1
  51. package/dist/webdist/assets/{diagram-LMA3HP47-CZBo_NP5.js → diagram-LMA3HP47-CrQt3n5J.js} +1 -1
  52. package/dist/webdist/assets/{diagram-OG6HWLK6-Dmb2yOZ2.js → diagram-OG6HWLK6-CVHDvio_.js} +1 -1
  53. package/dist/webdist/assets/{dialog-BKdq0aeC.js → dialog-BPhXn1HL.js} +1 -1
  54. package/dist/webdist/assets/{erDiagram-TEJ5UH35-_ekw2dYE.js → erDiagram-TEJ5UH35-DmEuGD8S.js} +1 -1
  55. package/dist/webdist/assets/{external-link-DJyx1xu4.js → external-link-DgElSULb.js} +1 -1
  56. package/dist/webdist/assets/{flowDiagram-I6XJVG4X-BKFCSX9M.js → flowDiagram-I6XJVG4X-D3ES6R-Y.js} +1 -1
  57. package/dist/webdist/assets/{ganttDiagram-6RSMTGT7-Bun-N5iU.js → ganttDiagram-6RSMTGT7-DzovqV_S.js} +1 -1
  58. package/dist/webdist/assets/{git-branch-DWvtYq2n.js → git-branch-DZp9oCCA.js} +1 -1
  59. package/dist/webdist/assets/{gitGraphDiagram-PVQCEYII-DP97jy0N.js → gitGraphDiagram-PVQCEYII-BV0Hvb-m.js} +1 -1
  60. package/dist/webdist/assets/{index-v0Tdes8H.js → index-BivjMfQ5.js} +2 -2
  61. package/dist/webdist/assets/index-S6YWlmeh.css +1 -0
  62. package/dist/webdist/assets/{infoDiagram-5YYISTIA-XNpJszfx.js → infoDiagram-5YYISTIA-DHmj_pKX.js} +1 -1
  63. package/dist/webdist/assets/{ishikawaDiagram-YF4QCWOH-Ct7Jx-R0.js → ishikawaDiagram-YF4QCWOH-DjAmacek.js} +1 -1
  64. package/dist/webdist/assets/{journeyDiagram-JHISSGLW-CyHwpNbR.js → journeyDiagram-JHISSGLW-InYx9cWN.js} +1 -1
  65. package/dist/webdist/assets/{kanban-definition-UN3LZRKU-BlIiwFL_.js → kanban-definition-UN3LZRKU-BO9O2UDq.js} +1 -1
  66. package/dist/webdist/assets/{linear-BHBdNjY-.js → linear-B6IwpKfz.js} +1 -1
  67. package/dist/webdist/assets/{mermaid.core-DJ7y5RnJ.js → mermaid.core-MlIRHLxX.js} +4 -4
  68. package/dist/webdist/assets/{mindmap-definition-RKZ34NQL-hhkBvUUD.js → mindmap-definition-RKZ34NQL-kvrr7ftb.js} +1 -1
  69. package/dist/webdist/assets/{pencil-CJGwma8c.js → pencil-k4AQARf_.js} +1 -1
  70. package/dist/webdist/assets/{personas-fIGMukzj.js → personas-CL0Kc_Kd.js} +5 -3
  71. package/dist/webdist/assets/{pieDiagram-4H26LBE5-TIZ5Ty6J.js → pieDiagram-4H26LBE5-DaE8CAe0.js} +1 -1
  72. package/dist/webdist/assets/{plus-88_2XyV5.js → plus-CY4mhBkY.js} +1 -1
  73. package/dist/webdist/assets/{quadrantDiagram-W4KKPZXB-DYHxb7at.js → quadrantDiagram-W4KKPZXB-Duu8s5l-.js} +1 -1
  74. package/dist/webdist/assets/{refresh-cw-MdqFeESZ.js → refresh-cw-DCf4F0oQ.js} +1 -1
  75. package/dist/webdist/assets/{requirementDiagram-4Y6WPE33-qnClMgKa.js → requirementDiagram-4Y6WPE33-Cbl7MhTK.js} +1 -1
  76. package/dist/webdist/assets/{rotate-ccw-c1dWBxKq.js → rotate-ccw-Bk6FjMT9.js} +1 -1
  77. package/dist/webdist/assets/{sankeyDiagram-5OEKKPKP-BG_kgFTc.js → sankeyDiagram-5OEKKPKP-BiK0JGus.js} +1 -1
  78. package/dist/webdist/assets/{sequenceDiagram-3UESZ5HK-D6qrIrDg.js → sequenceDiagram-3UESZ5HK-Dr7D9A4w.js} +1 -1
  79. package/dist/webdist/assets/{skeleton-Br4uEDd5.js → skeleton-Cy7DsAmm.js} +1 -1
  80. package/dist/webdist/assets/{sliders-horizontal-Cy_WWOJ2.js → sliders-horizontal-CVf2FXz2.js} +1 -1
  81. package/dist/webdist/assets/{wrench-A33IdAiw.js → sparkles-Bvrl94s5.js} +1 -1
  82. package/dist/webdist/assets/{star-C-2hCQLL.js → star-DLAcO0R_.js} +1 -1
  83. package/dist/webdist/assets/{stateDiagram-AJRCARHV-BHad4Ojy.js → stateDiagram-AJRCARHV-BthDRItX.js} +1 -1
  84. package/dist/webdist/assets/stateDiagram-v2-BHNVJYJU--IOHWZLa.js +1 -0
  85. package/dist/webdist/assets/{switch-CMtwRt96.js → switch-CI322tBg.js} +1 -1
  86. package/dist/webdist/assets/{terminal-BtT1uKGg.js → terminal-Bwy3mmYf.js} +1 -1
  87. package/dist/webdist/assets/{timeline-definition-PNZ67QCA-B4rUcQHc.js → timeline-definition-PNZ67QCA-DkR-Byrz.js} +1 -1
  88. package/dist/webdist/assets/{trash-2-Pyx2qefh.js → trash-2-B9m1cz2x.js} +1 -1
  89. package/dist/webdist/assets/{useIsDesktop-Cm4AYq5A.js → useIsDesktop-xqMNa55J.js} +1 -1
  90. package/dist/webdist/assets/{vennDiagram-CIIHVFJN-Xc9rC-NG.js → vennDiagram-CIIHVFJN-aOYXm4nP.js} +1 -1
  91. package/dist/webdist/assets/{wardley-L42UT6IY-lxhSG_a5.js → wardley-L42UT6IY-DptJ0PbQ.js} +1 -1
  92. package/dist/webdist/assets/{wardleyDiagram-YWT4CUSO-cPWAssfx.js → wardleyDiagram-YWT4CUSO-CN66GNqR.js} +1 -1
  93. package/dist/webdist/assets/wrench-CLvjdoSH.js +1 -0
  94. package/dist/webdist/assets/{xychartDiagram-2RQKCTM6-CKEYbcW3.js → xychartDiagram-2RQKCTM6-CbOWShHZ.js} +1 -1
  95. package/dist/webdist/index.html +2 -2
  96. package/package.json +69 -69
  97. package/dist/webdist/assets/CodeComposer-BBE_Hp0Q.js +0 -1
  98. package/dist/webdist/assets/CodeSessionScreen-DJJY5EKZ.js +0 -4
  99. package/dist/webdist/assets/CustomizeScreen-ClXS3xOu.js +0 -4
  100. package/dist/webdist/assets/DeveloperScreen-X2uqzXVK.js +0 -1
  101. package/dist/webdist/assets/ModelLoadMenu-UGUkfjbF.js +0 -2
  102. package/dist/webdist/assets/WorkspaceScreen-DJr_KuyC.js +0 -1
  103. package/dist/webdist/assets/channel-g0-qHfup.js +0 -1
  104. package/dist/webdist/assets/check-BQ18Nt2_.js +0 -1
  105. package/dist/webdist/assets/classDiagram-4FO5ZUOK-B09K5xH-.js +0 -1
  106. package/dist/webdist/assets/classDiagram-v2-Q7XG4LA2-B09K5xH-.js +0 -1
  107. package/dist/webdist/assets/copy-button-CE2-6pHL.js +0 -1
  108. package/dist/webdist/assets/index-CfEenPkR.css +0 -1
  109. package/dist/webdist/assets/layers-C5s0VHiR.js +0 -1
  110. 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 git = (args) => execFileSync5("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 5e3, windowsHide: true }).trim();
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 (git(["rev-parse", "--is-inside-work-tree"]) !== "true") return c.json({ isRepo: false, branch: "", branches: [] });
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 = git(["rev-parse", "--abbrev-ref", "HEAD"]);
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 = git(["branch", "--format=%(refname:short)"]).split("\n").map((s) => s.trim()).filter(Boolean);
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
- const compactionIdx = run?.compactionUpToMessageId ? all.findIndex((m) => m.id === run.compactionUpToMessageId) : -1;
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
- if (toolLoop.record(toolName, input) > LOOP_BREAK_AFTER) {
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
- const ame = ev.assistantMessageEvent;
16686
- if (ame.type === "text_delta") {
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 === "tool_call" && (data.status === "done" || data.status === "error")) {
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 runs2 = db2.listAgentRuns().filter((r) => {
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 = runs2.map((r) => {
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 followUp = (b.content ?? "").trim();
18319
+ const newContent = (b.content ?? "").trim();
17509
18320
  let userMsgId;
17510
18321
  let task;
17511
- if (followUp) {
17512
- userMsgId = db2.addMessage(run.convId, "user", followUp, { textAttachments: b.contextFiles }).id;
17513
- task = contextFilesBlock(b.contextFiles) + ((b.promptOverride ?? "").trim() || followUp);
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 { queued } = runs.enqueue(id, { convId: run.convId, repoRoot: run.repoRoot, task, userMsgId, thinkingBudget: b.thinkingBudget });
17521
- return c.json({ ok: true, queued, userMessageId: userMsgId }, 202);
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 execFile12, spawn as spawn7, spawnSync as spawnSync2 } from "child_process";
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") execFile12("taskkill", ["/PID", String(child.pid), "/T"], () => {
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") execFile12("taskkill", ["/PID", String(child.pid), "/F", "/T"], () => {
19126
+ if (process.platform === "win32") execFile13("taskkill", ["/PID", String(child.pid), "/F", "/T"], () => {
18309
19127
  });
18310
19128
  else child.kill("SIGKILL");
18311
19129
  }