turbollm 1.8.5 → 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 (111) hide show
  1. package/dist/cli.js +963 -57
  2. package/dist/webdist/assets/{AgentEditPage-VoVmMCf1.js → AgentEditPage-F6wQ-woa.js} +1 -1
  3. package/dist/webdist/assets/{ChatScreen-DHGEKPZr.js → ChatScreen-JNejy4c5.js} +1 -1
  4. package/dist/webdist/assets/CodeComposer-BMivBZTt.js +1 -0
  5. package/dist/webdist/assets/{CodeHomeScreen-BHIrDU_t.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-CCzC8Dth.js +7 -0
  10. package/dist/webdist/assets/{FsBrowser-5VN4Yj8z.js → FsBrowser-d8UO9bVy.js} +1 -1
  11. package/dist/webdist/assets/ModelLoadMenu-BcSALweE.js +2 -0
  12. package/dist/webdist/assets/{ModelsScreen-GUtj_sux.js → ModelsScreen-DHqnZlFL.js} +1 -1
  13. package/dist/webdist/assets/{SettingsScreen-D65xd3_e.js → SettingsScreen-Byi4wqlL.js} +1 -1
  14. package/dist/webdist/assets/{SkillEditPage-CkKqavr8.js → SkillEditPage-CKfpniwp.js} +1 -1
  15. package/dist/webdist/assets/{TokensScreen-CCS_y7DZ.js → TokensScreen-CSxwCWhD.js} +1 -1
  16. package/dist/webdist/assets/{ToolApprovalBar-CRZPKrcR.js → ToolApprovalBar-CDRoIK9R.js} +15 -15
  17. package/dist/webdist/assets/WorkspaceScreen-CY5EZKSy.js +1 -0
  18. package/dist/webdist/assets/{agent-api-BZQuCIAd.js → agent-api-DaWwB_7n.js} +1 -1
  19. package/dist/webdist/assets/{arc-CG_F6y0n.js → arc-Bw5r0gQ8.js} +1 -1
  20. package/dist/webdist/assets/{architectureDiagram-3BPJPVTR-bK0viBCO.js → architectureDiagram-3BPJPVTR-D_qgeydj.js} +1 -1
  21. package/dist/webdist/assets/{arrow-left-Cxqlqe4Q.js → arrow-left-Bs4KhSqC.js} +1 -1
  22. package/dist/webdist/assets/{badge-D1brk9rd.js → badge-CPXRwtV2.js} +1 -1
  23. package/dist/webdist/assets/{blockDiagram-GPEHLZMM-DqMi7LYs.js → blockDiagram-GPEHLZMM-BbzqG8AR.js} +1 -1
  24. package/dist/webdist/assets/{bot-CldCBtRY.js → bot-DUgi89DH.js} +1 -1
  25. package/dist/webdist/assets/{c4Diagram-AAUBKEIU-D5JOtr9l.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-uAJNrEFg.js → chat-api-DnoBopyh.js} +1 -1
  28. package/dist/webdist/assets/{chat-queries-OevUA_jH.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-Dv-zay0l.js → chevron-left-CFIaBRPc.js} +1 -1
  31. package/dist/webdist/assets/{chevron-right-DvvJS6ye.js → chevron-right-DT5tldLF.js} +1 -1
  32. package/dist/webdist/assets/{chunk-2J33WTMH-D5FvNAKn.js → chunk-2J33WTMH-jRmwQd0-.js} +1 -1
  33. package/dist/webdist/assets/{chunk-4BX2VUAB-PiXdGtQT.js → chunk-4BX2VUAB-BkQR5P6X.js} +1 -1
  34. package/dist/webdist/assets/{chunk-55IACEB6-BcFnIoE2.js → chunk-55IACEB6-dHYKdZGl.js} +1 -1
  35. package/dist/webdist/assets/{chunk-727SXJPM-BigbNnpE.js → chunk-727SXJPM-Bac05mkQ.js} +1 -1
  36. package/dist/webdist/assets/{chunk-AQP2D5EJ-ksTbyzzH.js → chunk-AQP2D5EJ-DQSeJwV8.js} +1 -1
  37. package/dist/webdist/assets/{chunk-FMBD7UC4-DpDAzpmq.js → chunk-FMBD7UC4-fw71OGhl.js} +1 -1
  38. package/dist/webdist/assets/{chunk-ND2GUHAM-Ch7eIfE0.js → chunk-ND2GUHAM-xRyemSmx.js} +1 -1
  39. package/dist/webdist/assets/{chunk-QZHKN3VN-DMDG8lEf.js → chunk-QZHKN3VN-BgNNqsB9.js} +1 -1
  40. package/dist/webdist/assets/{circle-check-BFtGWNjh.js → circle-check-CLNNkErq.js} +1 -1
  41. package/dist/webdist/assets/{circle-x-BmwEPZU0.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-BEcCBxuG.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-BBGTUfOp.js → cose-bilkent-S5V4N54A-BpeA37l_.js} +1 -1
  47. package/dist/webdist/assets/{dagre-BM42HDAG-D8W6jcq_.js → dagre-BM42HDAG-Dhe-FxnK.js} +1 -1
  48. package/dist/webdist/assets/{diagram-2AECGRRQ-CvCrWf_H.js → diagram-2AECGRRQ-CsOhE15t.js} +1 -1
  49. package/dist/webdist/assets/{diagram-5GNKFQAL-DZS4yZU1.js → diagram-5GNKFQAL-Dj4NKMxD.js} +1 -1
  50. package/dist/webdist/assets/{diagram-KO2AKTUF-BlKSql5d.js → diagram-KO2AKTUF-BVeDuCrX.js} +1 -1
  51. package/dist/webdist/assets/{diagram-LMA3HP47-CKo-tRYQ.js → diagram-LMA3HP47-CrQt3n5J.js} +1 -1
  52. package/dist/webdist/assets/{diagram-OG6HWLK6-NSePJGx1.js → diagram-OG6HWLK6-CVHDvio_.js} +1 -1
  53. package/dist/webdist/assets/{dialog-B-GLhgc2.js → dialog-BPhXn1HL.js} +1 -1
  54. package/dist/webdist/assets/{erDiagram-TEJ5UH35-haPVT0D3.js → erDiagram-TEJ5UH35-DmEuGD8S.js} +1 -1
  55. package/dist/webdist/assets/{external-link-UbaLGQ_h.js → external-link-DgElSULb.js} +1 -1
  56. package/dist/webdist/assets/{flowDiagram-I6XJVG4X-BzKZFBHV.js → flowDiagram-I6XJVG4X-D3ES6R-Y.js} +1 -1
  57. package/dist/webdist/assets/{ganttDiagram-6RSMTGT7-CCmAecLV.js → ganttDiagram-6RSMTGT7-DzovqV_S.js} +1 -1
  58. package/dist/webdist/assets/{git-branch-Bj6Xfs8n.js → git-branch-DZp9oCCA.js} +1 -1
  59. package/dist/webdist/assets/{gitGraphDiagram-PVQCEYII-OD-Aumfw.js → gitGraphDiagram-PVQCEYII-BV0Hvb-m.js} +1 -1
  60. package/dist/webdist/assets/{index-D4oBHgyO.js → index-BivjMfQ5.js} +2 -2
  61. package/dist/webdist/assets/index-S6YWlmeh.css +1 -0
  62. package/dist/webdist/assets/{infoDiagram-5YYISTIA-CXiblcgy.js → infoDiagram-5YYISTIA-DHmj_pKX.js} +1 -1
  63. package/dist/webdist/assets/{ishikawaDiagram-YF4QCWOH-C8sCKBhs.js → ishikawaDiagram-YF4QCWOH-DjAmacek.js} +1 -1
  64. package/dist/webdist/assets/{journeyDiagram-JHISSGLW-B3be8ACK.js → journeyDiagram-JHISSGLW-InYx9cWN.js} +1 -1
  65. package/dist/webdist/assets/{kanban-definition-UN3LZRKU-BbfHizAI.js → kanban-definition-UN3LZRKU-BO9O2UDq.js} +1 -1
  66. package/dist/webdist/assets/{linear-C0lkBXID.js → linear-B6IwpKfz.js} +1 -1
  67. package/dist/webdist/assets/{mermaid.core-CW2TnRwR.js → mermaid.core-MlIRHLxX.js} +4 -4
  68. package/dist/webdist/assets/{mindmap-definition-RKZ34NQL-h-ro0B1A.js → mindmap-definition-RKZ34NQL-kvrr7ftb.js} +1 -1
  69. package/dist/webdist/assets/{pencil-BJsQ5uv5.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-DEGupnzV.js → pieDiagram-4H26LBE5-DaE8CAe0.js} +1 -1
  72. package/dist/webdist/assets/{plus-BESpOgWu.js → plus-CY4mhBkY.js} +1 -1
  73. package/dist/webdist/assets/{quadrantDiagram-W4KKPZXB-CMkivigQ.js → quadrantDiagram-W4KKPZXB-Duu8s5l-.js} +1 -1
  74. package/dist/webdist/assets/{refresh-cw-CuuCQSpX.js → refresh-cw-DCf4F0oQ.js} +1 -1
  75. package/dist/webdist/assets/{requirementDiagram-4Y6WPE33-DyChhWBw.js → requirementDiagram-4Y6WPE33-Cbl7MhTK.js} +1 -1
  76. package/dist/webdist/assets/{rotate-ccw-D99lmEA-.js → rotate-ccw-Bk6FjMT9.js} +1 -1
  77. package/dist/webdist/assets/{sankeyDiagram-5OEKKPKP-BDPQ7Ai5.js → sankeyDiagram-5OEKKPKP-BiK0JGus.js} +1 -1
  78. package/dist/webdist/assets/{sequenceDiagram-3UESZ5HK-C_jdr66e.js → sequenceDiagram-3UESZ5HK-Dr7D9A4w.js} +1 -1
  79. package/dist/webdist/assets/{skeleton-nGnGDZ2Z.js → skeleton-Cy7DsAmm.js} +1 -1
  80. package/dist/webdist/assets/{sliders-horizontal-BXU5PC3A.js → sliders-horizontal-CVf2FXz2.js} +1 -1
  81. package/dist/webdist/assets/{wrench-CUU3qZM0.js → sparkles-Bvrl94s5.js} +1 -1
  82. package/dist/webdist/assets/{star-BGgeiEjL.js → star-DLAcO0R_.js} +1 -1
  83. package/dist/webdist/assets/{stateDiagram-AJRCARHV-CwtPbKxE.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-BQT_NipX.js → switch-CI322tBg.js} +1 -1
  86. package/dist/webdist/assets/{terminal-CrCLNyjB.js → terminal-Bwy3mmYf.js} +1 -1
  87. package/dist/webdist/assets/{timeline-definition-PNZ67QCA-B1bN1YFW.js → timeline-definition-PNZ67QCA-DkR-Byrz.js} +1 -1
  88. package/dist/webdist/assets/{trash-2-CuUIi8kE.js → trash-2-B9m1cz2x.js} +1 -1
  89. package/dist/webdist/assets/{useIsDesktop-BwBLPtTS.js → useIsDesktop-xqMNa55J.js} +1 -1
  90. package/dist/webdist/assets/{vennDiagram-CIIHVFJN-B23LrfT3.js → vennDiagram-CIIHVFJN-aOYXm4nP.js} +1 -1
  91. package/dist/webdist/assets/{wardley-L42UT6IY-Bkk_DpTu.js → wardley-L42UT6IY-DptJ0PbQ.js} +1 -1
  92. package/dist/webdist/assets/{wardleyDiagram-YWT4CUSO-Di9GYhE4.js → wardleyDiagram-YWT4CUSO-CN66GNqR.js} +1 -1
  93. package/dist/webdist/assets/wrench-CLvjdoSH.js +1 -0
  94. package/dist/webdist/assets/{xychartDiagram-2RQKCTM6-BF1BnBYj.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-Cxhvsus4.js +0 -1
  98. package/dist/webdist/assets/CodeSessionScreen-CqogdVEN.js +0 -4
  99. package/dist/webdist/assets/CustomizeScreen-B4XGEyxA.js +0 -4
  100. package/dist/webdist/assets/DeveloperScreen-jGamwaJG.js +0 -1
  101. package/dist/webdist/assets/EnginesScreen-BRkOTacr.js +0 -7
  102. package/dist/webdist/assets/ModelLoadMenu-CgM5a39X.js +0 -2
  103. package/dist/webdist/assets/WorkspaceScreen-D060hDIn.js +0 -1
  104. package/dist/webdist/assets/channel-C-X5wnqi.js +0 -1
  105. package/dist/webdist/assets/check-r_BeDgvE.js +0 -1
  106. package/dist/webdist/assets/classDiagram-4FO5ZUOK-Dw2O5mDo.js +0 -1
  107. package/dist/webdist/assets/classDiagram-v2-Q7XG4LA2-Dw2O5mDo.js +0 -1
  108. package/dist/webdist/assets/copy-button-B3sedfsF.js +0 -1
  109. package/dist/webdist/assets/index-CfEenPkR.css +0 -1
  110. package/dist/webdist/assets/layers-BqbkDoHi.js +0 -1
  111. package/dist/webdist/assets/stateDiagram-v2-BHNVJYJU-DlCLpW5_.js +0 -1
package/dist/cli.js CHANGED
@@ -2310,6 +2310,7 @@ function trimLen(s, n) {
2310
2310
 
2311
2311
  // src/engines/build-runner.ts
2312
2312
  import { execFile as execFile9, spawn as spawn2 } from "child_process";
2313
+ import { createHash as createHash2 } from "crypto";
2313
2314
  import { copyFileSync, existsSync as existsSync13, mkdirSync as mkdirSync7, readFileSync as readFileSync3, readdirSync as readdirSync4, rmSync as rmSync6, writeFileSync as writeFileSync4 } from "fs";
2314
2315
  import { delimiter as delimiter2, dirname as dirname5, join as join12 } from "path";
2315
2316
  import { promisify as promisify7 } from "util";
@@ -2533,8 +2534,8 @@ function sameRepo(a, b) {
2533
2534
  function normRepoUrl(s) {
2534
2535
  return (s ?? "").trim().toLowerCase().replace(/^https?:\/\//, "").replace(/^github\.com\//, "").replace(/\.git$/, "").replace(/\/+$/, "");
2535
2536
  }
2536
- function sourceBuildBinary(enginesRoot, repoUrl, branch) {
2537
- const root = join12(enginesRoot, "build", buildDirName(repoUrl, branch));
2537
+ function sourceBuildBinary(enginesRoot, repoUrl, branch, commit) {
2538
+ const root = join12(enginesRoot, "build", buildDirName(repoUrl, branch, commit));
2538
2539
  return resolveServerBinary(join12(root, "build")) ?? resolveServerBinary(root);
2539
2540
  }
2540
2541
  function sourceBuildDirOf(binPath, enginesRoot) {
@@ -2553,6 +2554,20 @@ function notCmakeProjectError(hasCMakeLists) {
2553
2554
  if (hasCMakeLists) return null;
2554
2555
  return "This repository doesn't look like a llama.cpp-based (CMake) project \u2014 no CMakeLists.txt was found at its root. 1-click build currently only supports llama.cpp-family engines built with CMake. If this project exposes a llama-server-compatible binary some other way, add it as a custom engine by pointing TurboLLM directly at that binary instead of building from source.";
2555
2556
  }
2557
+ function missingPatchShaError(patchUrl, patchSha256) {
2558
+ if (!(patchUrl ?? "").trim()) return null;
2559
+ if ((patchSha256 ?? "").trim()) return null;
2560
+ return "Refusing to apply a build patch with no pinned SHA-256 checksum \u2014 a patch may only be applied when its exact downloaded bytes can be verified against a known hash.";
2561
+ }
2562
+ function sha256Hex(bytes) {
2563
+ return createHash2("sha256").update(bytes).digest("hex");
2564
+ }
2565
+ function patchChecksumMismatchError(expectedSha256, actualSha256) {
2566
+ const want = expectedSha256.trim().toLowerCase();
2567
+ const got = actualSha256.trim().toLowerCase();
2568
+ if (want === got) return null;
2569
+ return `The downloaded patch did not match its pinned SHA-256 checksum (expected ${want}, got ${got}). TurboLLM refused to apply an unverified patch \u2014 the remote file may have changed or been tampered with. The build was stopped before any patch was applied.`;
2570
+ }
2556
2571
  function vcvarsBatch(vcvars, cmakeArgs) {
2557
2572
  const quoted = cmakeArgs.map((a) => /[\s"]/.test(a) ? `"${a}"` : a).join(" ");
2558
2573
  return [
@@ -2830,17 +2845,44 @@ async function runBuild(req, hooks, signal) {
2830
2845
  if (pinnedCommit) {
2831
2846
  mkdirSync7(srcDir, { recursive: true });
2832
2847
  await runStep("git", ["init"], { cwd: srcDir, env, signal, onLine: hooks.log });
2848
+ if (isWindows) await runStep("git", ["config", "core.longpaths", "true"], { cwd: srcDir, env, signal, onLine: hooks.log });
2833
2849
  await runStep("git", ["remote", "add", "origin", req.repoUrl], { cwd: srcDir, env, signal, onLine: hooks.log });
2834
2850
  await runStep("git", ["fetch", "--depth", "1", "origin", pinnedCommit], { cwd: srcDir, env, signal, onLine: hooks.log });
2835
2851
  await runStep("git", ["checkout", "FETCH_HEAD"], { cwd: srcDir, env, signal, onLine: hooks.log });
2836
2852
  } else {
2837
- const cloneArgs = ["clone", "--depth", "1"];
2853
+ const cloneArgs = isWindows ? ["-c", "core.longpaths=true", "clone", "--depth", "1"] : ["clone", "--depth", "1"];
2838
2854
  if ((req.branch ?? "").trim()) cloneArgs.push("--branch", req.branch.trim());
2839
2855
  cloneArgs.push(req.repoUrl, srcDir);
2840
2856
  await runStep("git", cloneArgs, { env, signal, onLine: hooks.log });
2841
2857
  }
2842
2858
  const commit = (await runStep("git", ["-C", srcDir, "rev-parse", "HEAD"], { env, signal, onLine: () => {
2843
2859
  } })).trim();
2860
+ const patchUrl = (req.patchUrl ?? "").trim();
2861
+ if (patchUrl) {
2862
+ const expectedSha = (req.patchSha256 ?? "").trim();
2863
+ const guard = missingPatchShaError(patchUrl, expectedSha);
2864
+ if (guard) throw new Error(guard);
2865
+ hooks.log(`Downloading build patch from ${patchUrl} \u2026`);
2866
+ const res = await fetch(patchUrl, { signal });
2867
+ if (!res.ok) throw new Error(`Failed to download the build patch (HTTP ${res.status} ${res.statusText}) from ${patchUrl}.`);
2868
+ const patchBytes = Buffer.from(await res.arrayBuffer());
2869
+ const actualSha = sha256Hex(patchBytes);
2870
+ const mismatch = patchChecksumMismatchError(expectedSha, actualSha);
2871
+ if (mismatch) throw new Error(mismatch);
2872
+ const patchFile = join12(buildRoot, "_tllm_patch.diff");
2873
+ writeFileSync4(patchFile, patchBytes);
2874
+ hooks.log(`Patch verified against its pinned checksum (sha256 ${actualSha.slice(0, 12)}\u2026).`);
2875
+ try {
2876
+ await runStep("git", ["-C", srcDir, "apply", "--check", patchFile], { env, signal, onLine: hooks.log });
2877
+ } catch (e) {
2878
+ if (e?.name === "AbortError") throw e;
2879
+ throw new Error(
2880
+ "The verified patch failed to apply against the checked-out commit (most likely it has drifted from the commit it was authored against, but see the underlying error below for the exact cause). " + (e instanceof Error ? e.message : String(e))
2881
+ );
2882
+ }
2883
+ await runStep("git", ["-C", srcDir, "apply", patchFile], { env, signal, onLine: hooks.log });
2884
+ hooks.log(`Applied verified patch (sha256 ${actualSha.slice(0, 12)}\u2026) on top of ${commit.slice(0, 8)}.`);
2885
+ }
2844
2886
  const notCmake = notCmakeProjectError(existsSync13(join12(srcDir, "CMakeLists.txt")));
2845
2887
  if (notCmake) throw new Error(notCmake);
2846
2888
  neutralizeGratuitousAsm(srcDir, hooks.log);
@@ -2931,6 +2973,7 @@ var Registry = class {
2931
2973
  const sourceRepo = source?.sourceRepo?.trim() || void 0;
2932
2974
  const sourceBranch = source?.sourceBranch?.trim() || void 0;
2933
2975
  const sourceCommit = source?.sourceCommit?.trim() || void 0;
2976
+ const sourcePatchUrl = source?.sourcePatchUrl?.trim() || void 0;
2934
2977
  const eng = {
2935
2978
  id: randomUUID2(),
2936
2979
  name: finalName,
@@ -2941,7 +2984,8 @@ var Registry = class {
2941
2984
  addedAt: (/* @__PURE__ */ new Date()).toISOString(),
2942
2985
  ...sourceRepo ? { sourceRepo } : {},
2943
2986
  ...sourceBranch ? { sourceBranch } : {},
2944
- ...sourceCommit ? { sourceCommit } : {}
2987
+ ...sourceCommit ? { sourceCommit } : {},
2988
+ ...sourcePatchUrl ? { sourcePatchUrl } : {}
2945
2989
  };
2946
2990
  this.store.update((c) => {
2947
2991
  if (this.nameClash(c.engines, finalName)) throw new NameTakenError();
@@ -4695,7 +4739,7 @@ function seedDefaultModelDir(store2, scanner2) {
4695
4739
  }
4696
4740
 
4697
4741
  // src/models/hashes.ts
4698
- import { createHash as createHash2 } from "crypto";
4742
+ import { createHash as createHash3 } from "crypto";
4699
4743
  import { createReadStream, mkdirSync as mkdirSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "fs";
4700
4744
  import { join as join16 } from "path";
4701
4745
  var HashStore = class {
@@ -4722,7 +4766,7 @@ var HashStore = class {
4722
4766
  }
4723
4767
  compute(path, size, mtime) {
4724
4768
  return new Promise((resolve5) => {
4725
- const hash = createHash2("sha256");
4769
+ const hash = createHash3("sha256");
4726
4770
  const rs = createReadStream(path);
4727
4771
  rs.on("error", () => resolve5());
4728
4772
  rs.on("data", (c) => hash.update(c));
@@ -6155,6 +6199,30 @@ var ConversationStore = class {
6155
6199
  if (!from) return 0;
6156
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;
6157
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
+ }
6158
6226
  /** Permanently delete a Code session: its messages, working doc, and the agent_run +
6159
6227
  * conversation rows. There is no FK cascade on these tables (each is deleted explicitly,
6160
6228
  * same as chat's own conversation delete never cascaded to messages) — miss one and it's
@@ -6640,7 +6708,7 @@ function dirOf(p) {
6640
6708
  }
6641
6709
 
6642
6710
  // src/downloads/downloads.ts
6643
- import { createHash as createHash3 } from "crypto";
6711
+ import { createHash as createHash4 } from "crypto";
6644
6712
  import {
6645
6713
  createWriteStream as createWriteStream3,
6646
6714
  existsSync as existsSync17,
@@ -6920,7 +6988,7 @@ var DownloadManager = class {
6920
6988
  const clen = Number(res.headers.get("content-length") ?? 0);
6921
6989
  if (clen > 0) rec.total = resuming ? startAt + clen : clen;
6922
6990
  if (clen > 0) this.assertDisk(dirname7(part), clen);
6923
- const hash = rec.sha256 ? createHash3("sha256") : null;
6991
+ const hash = rec.sha256 ? createHash4("sha256") : null;
6924
6992
  const verifyHash = hash !== null && startAt === 0;
6925
6993
  let lastTick = Date.now();
6926
6994
  let lastBytes = startAt;
@@ -12386,11 +12454,11 @@ async function autoTitleFromConversation(d, convId) {
12386
12454
  }
12387
12455
 
12388
12456
  // src/auth.ts
12389
- import { createHash as createHash4, randomBytes, randomUUID as randomUUID6 } from "crypto";
12457
+ import { createHash as createHash5, randomBytes, randomUUID as randomUUID6 } from "crypto";
12390
12458
  import { getConnInfo } from "@hono/node-server/conninfo";
12391
12459
  var LOOPBACK = /* @__PURE__ */ new Set(["127.0.0.1", "::1", "::ffff:127.0.0.1"]);
12392
12460
  function hashKey(raw) {
12393
- return createHash4("sha256").update(raw).digest("hex");
12461
+ return createHash5("sha256").update(raw).digest("hex");
12394
12462
  }
12395
12463
  function generateApiKey() {
12396
12464
  const charset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
@@ -12398,7 +12466,7 @@ function generateApiKey() {
12398
12466
  let key = "";
12399
12467
  for (let i = 0; i < 40; i++) key += charset[buf[i] % 62];
12400
12468
  const full = `tllm-${key}`;
12401
- const hash = createHash4("sha256").update(full).digest("hex");
12469
+ const hash = createHash5("sha256").update(full).digest("hex");
12402
12470
  return { full, hash, prefix: full.slice(0, 12) };
12403
12471
  }
12404
12472
  function provisionTunnelApiKey(d) {
@@ -12901,6 +12969,38 @@ var ALL = [
12901
12969
  hasPrebuilt: false
12902
12970
  }
12903
12971
  ]
12972
+ },
12973
+ {
12974
+ id: "solar-open2",
12975
+ name: "Solar Open 2 (patched llama.cpp)",
12976
+ kind: "llama-server",
12977
+ description: "Upstage's Solar Open 2 (250B-A15B MoE) isn't in mainline llama.cpp yet \u2014 this builds official llama.cpp at a pinned commit with a community patch that adds the solar_open2 architecture, then runs it on the standard llama-server path.",
12978
+ provision: "github-release",
12979
+ // Builds the OFFICIAL llama.cpp repo (like the prism/beellama build-from-source cards, no
12980
+ // real github-release asset is resolved) — pinned to the exact commit the patch was authored
12981
+ // against, then the checksum-verified patch is applied before compiling. The patch is an
12982
+ // INDEPENDENT community diff, NOT reviewed or endorsed by Upstage or upstream llama.cpp — same
12983
+ // fork-provenance honesty as the prism/beellama entries above (Honesty rule at the top).
12984
+ homepage: "https://github.com/ggml-org/llama.cpp",
12985
+ repo: "ggml-org/llama.cpp",
12986
+ sourceCommit: "846e991ec3c7ccec49112ff2c5b00b710e5f551d",
12987
+ patchUrl: "https://huggingface.co/prometheusAIR/Solar-Open2-250B-GGUF/resolve/main/solar_open2-llama.cpp.patch",
12988
+ patchSha256: "998a9cef479d3b01f25e473793890b05b887a8ce85563431b3b053b14bb21fa8",
12989
+ platforms: ["win32", "darwin", "linux"],
12990
+ support: "experimental",
12991
+ installEndpoint: "",
12992
+ note: "Unofficial: applies an independent community patch (not affiliated with or reviewed by Upstage or upstream llama.cpp) on top of a pinned llama.cpp commit, verified against a pinned SHA-256 before it is applied. This is a 250B-total / ~15B-active MoE \u2014 the upstream port was tested on workstation-class hardware (128GB DDR5 + a 96GB RTX PRO 6000), not a typical consumer box; GGUF quants run ~89 GiB (Q2_K) to 191 GiB (Q6_K), with IQ4_XS (~127 GiB) recommended. Two known quirks on this arch: pass -ub 512 or -ub 4096 (a batch size of -ub 2048 crashes with a CUDA illegal-memory-access), and add --no-mmap for any CPU-offloaded setup (mmap page-faulting collapses throughput ~10x on this port).",
12993
+ variants: [
12994
+ {
12995
+ id: "solar-open2-source",
12996
+ label: "Build from source (patched)",
12997
+ repo: "ggml-org/llama.cpp",
12998
+ requires: {},
12999
+ stability: "experimental",
13000
+ speed: "baseline",
13001
+ hasPrebuilt: false
13002
+ }
13003
+ ]
12904
13004
  }
12905
13005
  ];
12906
13006
  function catalogForPlatform(platform = process.platform) {
@@ -13520,9 +13620,11 @@ function registerApi(app2, d) {
13520
13620
  installed2 = existsSync22(llamafileBinPath(enginesRoot));
13521
13621
  enabled = regEngines.some((x) => x.kind === "llamafile");
13522
13622
  }
13523
- const srcEng = regEngines.find((x) => sameRepo(x.sourceRepo, e.homepage));
13623
+ const srcEng = regEngines.find(
13624
+ (x) => sameRepo(x.sourceRepo, e.homepage) && (x.sourceCommit ?? "") === (e.sourceCommit ?? "") && (x.sourcePatchUrl ?? "") === (e.patchUrl ?? "")
13625
+ );
13524
13626
  let sourceBinPath = srcEng?.binPath;
13525
- if (!srcEng) sourceBinPath = sourceBuildBinary(enginesRoot, e.homepage) ?? void 0;
13627
+ if (!srcEng) sourceBinPath = sourceBuildBinary(enginesRoot, e.homepage, void 0, e.sourceCommit) ?? void 0;
13526
13628
  const sourceBuilt = !!srcEng || !!sourceBinPath;
13527
13629
  if (srcEng) {
13528
13630
  installed2 = true;
@@ -13567,6 +13669,14 @@ function registerApi(app2, d) {
13567
13669
  const commit = (b.commit ?? "").trim() || void 0;
13568
13670
  if (commit && !/^[0-9a-f]{7,40}$/i.test(commit))
13569
13671
  return err2(c, 400, "invalid_config_value", "commit must be a hex SHA (7-40 chars).");
13672
+ const patchUrl = (b.patchUrl ?? "").trim() || void 0;
13673
+ const patchSha256 = (b.patchSha256 ?? "").trim() || void 0;
13674
+ if (!!patchUrl !== !!patchSha256)
13675
+ return err2(c, 400, "invalid_config_value", "patchUrl and patchSha256 must be provided together \u2014 a patch may only be applied with a pinned checksum.");
13676
+ if (patchUrl && !/^https?:\/\//i.test(patchUrl))
13677
+ return err2(c, 400, "invalid_config_value", "patchUrl must be an http(s) URL.");
13678
+ if (patchSha256 && !/^[0-9a-f]{64}$/i.test(patchSha256))
13679
+ return err2(c, 400, "invalid_config_value", "patchSha256 must be a 64-char hex SHA-256.");
13570
13680
  const busy = engineWorkBusy(d);
13571
13681
  if (busy) return err2(c, 409, "engine_already_running", busy);
13572
13682
  const name = (b.name ?? "").trim() || void 0;
@@ -13581,7 +13691,7 @@ function registerApi(app2, d) {
13581
13691
  let out;
13582
13692
  try {
13583
13693
  out = await runBuild(
13584
- { repoUrl, branch, commit, enginesRoot, toolchainDirs },
13694
+ { repoUrl, branch, commit, patchUrl, patchSha256, enginesRoot, toolchainDirs },
13585
13695
  { phase: (p) => d.build.phase(p), log: (line) => d.build.log(line) },
13586
13696
  ac.signal
13587
13697
  );
@@ -13616,11 +13726,12 @@ function registerApi(app2, d) {
13616
13726
  const eng = (await d.registry.add(prior?.name ?? name ?? "", out.binPath, {
13617
13727
  sourceRepo: repoUrl,
13618
13728
  sourceBranch: branch,
13619
- sourceCommit: commit
13729
+ sourceCommit: commit,
13730
+ sourcePatchUrl: patchUrl
13620
13731
  })).engine;
13621
13732
  d.registry.activate(eng.id);
13622
13733
  if (!isCatalogRepo(repoUrl)) {
13623
- d.registry.recordCustomSource({ name: eng.name, binPath: eng.binPath, kind: eng.kind, sourceRepo: repoUrl, sourceBranch: branch, sourceCommit: commit });
13734
+ d.registry.recordCustomSource({ name: eng.name, binPath: eng.binPath, kind: eng.kind, sourceRepo: repoUrl, sourceBranch: branch, sourceCommit: commit, sourcePatchUrl: patchUrl });
13624
13735
  }
13625
13736
  d.build.log(`Registered "${eng.name}" \u2014 built from ${out.commit.slice(0, 8)}.`);
13626
13737
  d.build.done();
@@ -13893,7 +14004,8 @@ function registerApi(app2, d) {
13893
14004
  kind: eng.kind,
13894
14005
  sourceRepo: eng.sourceRepo,
13895
14006
  sourceBranch: eng.sourceBranch,
13896
- sourceCommit: eng.sourceCommit
14007
+ sourceCommit: eng.sourceCommit,
14008
+ sourcePatchUrl: eng.sourcePatchUrl
13897
14009
  });
13898
14010
  }
13899
14011
  d.registry.remove(id);
@@ -14010,18 +14122,18 @@ function registerApi(app2, d) {
14010
14122
  if (!existsSync22(cwd) || !statSync5(cwd).isDirectory()) {
14011
14123
  return c.json({ isRepo: false, branch: "", branches: [] });
14012
14124
  }
14013
- 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();
14014
14126
  try {
14015
- 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: [] });
14016
14128
  let branch = "";
14017
14129
  try {
14018
- branch = git(["rev-parse", "--abbrev-ref", "HEAD"]);
14130
+ branch = git2(["rev-parse", "--abbrev-ref", "HEAD"]);
14019
14131
  } catch {
14020
14132
  }
14021
14133
  if (branch === "HEAD") branch = "";
14022
14134
  let branches = [];
14023
14135
  try {
14024
- 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);
14025
14137
  } catch {
14026
14138
  }
14027
14139
  return c.json({ isRepo: true, branch, branches });
@@ -15720,6 +15832,16 @@ function dependencyDisciplineGuidance() {
15720
15832
  "training knowledge, which is frequently stale for fast-moving libraries."
15721
15833
  ].join(" ");
15722
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
+ }
15723
15845
  function toolsForMode(mode) {
15724
15846
  return mode === "plan" ? ["read", "grep", "find", "ls"] : void 0;
15725
15847
  }
@@ -15751,6 +15873,12 @@ function readAgentsFile(path) {
15751
15873
  return null;
15752
15874
  }
15753
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
+ }
15754
15882
  function agentsMdBlock(repoRoot, globalDir) {
15755
15883
  const global = readAgentsFile(join27(globalDir, "agents.md"));
15756
15884
  const project = readAgentsFile(join27(repoRoot, "AGENTS.md"));
@@ -15769,6 +15897,7 @@ function buildAppendPrompt(mode, skills = [], agentsMd, hasWebTools = false) {
15769
15897
  if (mode !== "plan") {
15770
15898
  blocks.push(editReliabilityGuidance());
15771
15899
  blocks.push(lspGuidance());
15900
+ blocks.push(todoTrackerGuidance());
15772
15901
  }
15773
15902
  if (hasWebTools) {
15774
15903
  blocks.push(antiFallbackGuidance());
@@ -16040,12 +16169,9 @@ var COMPACTION_SUFFIX = "\n\n(End of summary \u2014 the actual conversation cont
16040
16169
  function resolveEffectiveHistory(d, convId, sessionId) {
16041
16170
  const all = d.db.getConversation(convId, true)?.messages ?? [];
16042
16171
  const run = d.db.getAgentRun(sessionId);
16043
- const compactionIdx = run?.compactionUpToMessageId ? all.findIndex((m) => m.id === run.compactionUpToMessageId) : -1;
16044
- const clearedIdx = run?.clearedUpToMessageId ? all.findIndex((m) => m.id === run.clearedUpToMessageId) : -1;
16045
- if (clearedIdx !== -1 && clearedIdx >= compactionIdx) {
16046
- return { summaryText: null, messages: all.slice(clearedIdx + 1) };
16047
- }
16172
+ if (run?.clearedUpToMessageId) return { summaryText: null, messages: all };
16048
16173
  if (!run?.compactionUpToMessageId) return { summaryText: null, messages: all };
16174
+ const compactionIdx = all.findIndex((m) => m.id === run.compactionUpToMessageId);
16049
16175
  const rest = compactionIdx === -1 ? all : all.slice(compactionIdx + 1);
16050
16176
  return { summaryText: `${COMPACTION_PREFIX}${run.compactionSummary}${COMPACTION_SUFFIX}`, messages: rest };
16051
16177
  }
@@ -16074,6 +16200,7 @@ function keepRecentTokensFor(messages, baseKeepRecentTokens) {
16074
16200
  var MUTATING_TOOLS = /* @__PURE__ */ new Set(["edit", "write", "bash"]);
16075
16201
  var PATH_TOOLS = /* @__PURE__ */ new Set(["read", "edit", "write", "grep", "find", "ls"]);
16076
16202
  var LOOP_BREAK_AFTER = 3;
16203
+ var LOOP_ABORT_AFTER = LOOP_BREAK_AFTER + 3;
16077
16204
  function toolCallSignature(name, input) {
16078
16205
  const stable = (v) => {
16079
16206
  if (v === null || typeof v !== "object") return JSON.stringify(v) ?? "undefined";
@@ -16122,6 +16249,99 @@ function disposeLspClientsForConv(convId) {
16122
16249
  for (const c of clients.values()) c.dispose();
16123
16250
  lspClientsByConv.delete(convId);
16124
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
+ }
16125
16345
  async function runCodeSession(params) {
16126
16346
  const { d, convId, sessionId, repoRoot, mode, thinkingBudget, task: rawTask, signal, sink } = params;
16127
16347
  const ms = d.manager.status();
@@ -16321,12 +16541,192 @@ async function runCodeSession(params) {
16321
16541
  skillSession.dispose();
16322
16542
  return finalText2.trim() || "(the skill produced no output)";
16323
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
+ }
16324
16687
  const mcpToolDefs = d.tools ? (await d.tools.buildToolDefinitions()).filter((t) => t.function.name.startsWith("mcp__")) : [];
16325
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
+ };
16326
16725
  const extension = (pi) => {
16327
16726
  pi.on("before_provider_request", async (event) => {
16328
16727
  releaseGate();
16329
16728
  if (d.gate) heldGate = await d.gate.acquire("bg", { signal });
16729
+ startPrefillPoll();
16330
16730
  const payload = { ...event.payload };
16331
16731
  delete payload.max_tokens;
16332
16732
  delete payload.max_completion_tokens;
@@ -16343,6 +16743,7 @@ async function runCodeSession(params) {
16343
16743
  return payload;
16344
16744
  });
16345
16745
  pi.on("after_provider_response", () => {
16746
+ stopPrefillPoll();
16346
16747
  releaseGate();
16347
16748
  });
16348
16749
  pi.on("tool_call", async (event) => {
@@ -16350,7 +16751,14 @@ async function runCodeSession(params) {
16350
16751
  const toolCallId = event.toolCallId;
16351
16752
  const input = event.input;
16352
16753
  await sink({ event: "tool_call", data: { id: toolCallId, name: toolName, args: input, status: "pending" } });
16353
- 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) {
16354
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.]`;
16355
16763
  await sink({ event: "tool_call", data: { id: toolCallId, name: toolName, args: input, status: "error", result: reason } });
16356
16764
  return { block: true, reason };
@@ -16473,6 +16881,50 @@ ${lines.join("\n")}${more}]`
16473
16881
  }
16474
16882
  }
16475
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
+ });
16476
16928
  if (d.tools) {
16477
16929
  const tools2 = d.tools;
16478
16930
  pi.registerTool({
@@ -16592,24 +17044,24 @@ ${lines.join("\n")}${more}]`
16592
17044
  };
16593
17045
  if (signal.aborted) onAbort();
16594
17046
  else signal.addEventListener("abort", onAbort, { once: true });
17047
+ const turnCounter = { index: 0 };
16595
17048
  const unsubscribe = session.subscribe((ev) => {
16596
- if (ev.type === "message_update") {
16597
- const ame = ev.assistantMessageEvent;
16598
- if (ame.type === "text_delta") {
16599
- void sink({ event: "delta", data: { delta: ame.delta } });
16600
- } else if (ame.type === "thinking_delta") {
16601
- void sink({ event: "reasoning", data: { delta: ame.delta } });
16602
- }
16603
- } else if (ev.type === "compaction_start") {
16604
- void sink({ event: "compaction", data: { phase: "start", reason: ev.reason } });
16605
- } else if (ev.type === "compaction_end") {
16606
- void sink({ event: "compaction", data: { phase: "end", reason: ev.reason, aborted: ev.aborted, tokensBefore: ev.result?.tokensBefore } });
16607
- }
17049
+ if (ev.type === "message_update") stopPrefillPoll();
17050
+ const frame = codeEventToFrame(ev, turnCounter);
17051
+ if (frame) void sink(frame);
16608
17052
  });
17053
+ const steerHandle = async (text) => {
17054
+ if (!session.isStreaming) return false;
17055
+ await session.steer(text);
17056
+ return true;
17057
+ };
16609
17058
  d.manager.generationStart();
17059
+ params.onSteerable?.(steerHandle);
16610
17060
  try {
16611
17061
  await session.prompt(task);
16612
17062
  } finally {
17063
+ params.onSteerable?.(null);
17064
+ stopPrefillPoll();
16613
17065
  unsubscribe();
16614
17066
  releaseGate();
16615
17067
  d.manager.generationEnd();
@@ -16769,7 +17221,12 @@ var CodeRunManager = class {
16769
17221
  * (not just index) identifies each entry so a per-chip action (sendNow) can target one
16770
17222
  * specific queued turn even if two queued tasks have identical text. */
16771
17223
  queued(sessionId) {
16772
- 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 ?? [];
16773
17230
  }
16774
17231
  /** The in-flight turn's message ids, so a reconnecting stream can synthesize a `meta` frame
16775
17232
  * even if the real one has already aged out of the ring buffer. */
@@ -16782,7 +17239,7 @@ var CodeRunManager = class {
16782
17239
  if (!s) {
16783
17240
  const emitter = new EventEmitter();
16784
17241
  emitter.setMaxListeners(0);
16785
- 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: [] };
16786
17243
  this.sessions.set(sessionId, s);
16787
17244
  }
16788
17245
  if (s.cleanupTimer) {
@@ -16801,11 +17258,36 @@ var CodeRunManager = class {
16801
17258
  enqueue(sessionId, params) {
16802
17259
  const s = this.ensure(sessionId, params.convId, params.repoRoot);
16803
17260
  const willQueue = s.active !== null;
16804
- 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" });
16805
17262
  this.emitQueue(sessionId);
16806
17263
  void this.pump(sessionId);
16807
17264
  return { queued: willQueue };
16808
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
+ }
16809
17291
  /** The most recent real ctxUsed/ctxMax on record for this conversation (the last assistant
16810
17292
  * turn that actually completed and got a genuine stats.ctxUsed from runCodeSession), or
16811
17293
  * undefined if none exists yet. Used when a turn ABORTS before runCodeSession returns a
@@ -16849,8 +17331,9 @@ var CodeRunManager = class {
16849
17331
  this.emitQueue(sessionId);
16850
17332
  const assistantMsg = this.d.db.addMessage(s.convId, "assistant", "", { stats: { aborted: false } });
16851
17333
  const ac = new AbortController();
16852
- s.active = { ac, assistantMsgId: assistantMsg.id, userMsgId: turn.userMsgId };
17334
+ s.active = { ac, assistantMsgId: assistantMsg.id, userMsgId: turn.userMsgId, steer: null };
16853
17335
  s.replayFloor = s.buffer.head();
17336
+ s.todos = [];
16854
17337
  const push = (event, data) => {
16855
17338
  const ss = this.sessions.get(sessionId);
16856
17339
  if (!ss) return;
@@ -16872,7 +17355,10 @@ var CodeRunManager = class {
16872
17355
  if (last && last.type === "text") last.text += delta;
16873
17356
  else timeline.push({ type: "text", text: delta });
16874
17357
  } else if (ev.event === "reasoning") reasoning += String(data.delta ?? "");
16875
- 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")) {
16876
17362
  const id = String(data.id ?? "");
16877
17363
  toolCalls.push({
16878
17364
  id,
@@ -16904,7 +17390,14 @@ var CodeRunManager = class {
16904
17390
  thinkingBudget: turn.thinkingBudget,
16905
17391
  task: turn.task,
16906
17392
  signal: ac.signal,
16907
- 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
+ }
16908
17401
  });
16909
17402
  const finalContent = content.trim() || result.finalText;
16910
17403
  if (!content.trim() && result.finalText.trim()) timeline.push({ type: "text", text: result.finalText });
@@ -17130,6 +17623,297 @@ function revertFileEdits(messages, repoRoot) {
17130
17623
  return { reverted, failed };
17131
17624
  }
17132
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
+
17133
17917
  // src/code/code-routes.ts
17134
17918
  function err5(c, s, code, msg2) {
17135
17919
  return c.json({ error: { code, message: msg2 } }, s);
@@ -17194,6 +17978,8 @@ function toSidebarRow(run) {
17194
17978
  del: run.linesRemoved ?? 0,
17195
17979
  mode: void 0,
17196
17980
  // filled from conv below when available
17981
+ running: void 0,
17982
+ // filled from CodeRunManager below when available
17197
17983
  createdAt: run.createdAt,
17198
17984
  repoRoot: run.repoRoot ?? "",
17199
17985
  error: run.error,
@@ -17239,16 +18025,17 @@ function registerCodeRoutes(app2, d) {
17239
18025
  });
17240
18026
  app2.get("/api/v1/code/sessions", (c) => {
17241
18027
  const filter = c.req.query("filter") ?? "active";
17242
- const runs2 = db2.listAgentRuns().filter((r) => {
18028
+ const agentRuns = db2.listAgentRuns().filter((r) => {
17243
18029
  const conv = db2.getConversation(r.convId);
17244
18030
  if (conv?.kind !== "code") return false;
17245
18031
  if (filter === "active") return !r.archivedAt;
17246
18032
  if (filter === "archived") return !!r.archivedAt;
17247
18033
  return true;
17248
18034
  });
17249
- const rows = runs2.map((r) => {
18035
+ const rows = agentRuns.map((r) => {
17250
18036
  const row = toSidebarRow(r);
17251
18037
  row.mode = db2.getConversation(r.convId)?.agentMode;
18038
+ row.running = runs.isActive(r.id);
17252
18039
  return row;
17253
18040
  });
17254
18041
  return c.json({ sessions: rows });
@@ -17268,9 +18055,31 @@ function registerCodeRoutes(app2, d) {
17268
18055
  running: runs.isActive(run.id),
17269
18056
  // Tasks waiting behind the active turn — the server-side message queue, so its chips
17270
18057
  // survive a page reload / reconnect.
17271
- 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 }
17272
18067
  });
17273
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
+ });
17274
18083
  app2.patch("/api/v1/code/sessions/:id/mode", async (c) => {
17275
18084
  const id = c.req.param("id");
17276
18085
  const run = db2.getAgentRun(id);
@@ -17346,6 +18155,7 @@ function registerCodeRoutes(app2, d) {
17346
18155
  const lastMsg = (conv?.messages ?? []).at(-1);
17347
18156
  if (!lastMsg) return err5(c, 400, "nothing_to_clear", "Nothing to clear yet.");
17348
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);
17349
18159
  db2.setClearedUpToMessageId(id, lastMsg.id);
17350
18160
  return c.json({ ok: true, clearedUpToMessageId: lastMsg.id });
17351
18161
  });
@@ -17360,6 +18170,7 @@ function registerCodeRoutes(app2, d) {
17360
18170
  return c.json({ ok: true });
17361
18171
  }
17362
18172
  if (!run.clearedUpToMessageId) return err5(c, 400, "not_cleared", "This session has not been cleared.");
18173
+ db2.reactivateMessagesUpTo(run.convId, run.clearedUpToMessageId);
17363
18174
  db2.setClearedUpToMessageId(id, null);
17364
18175
  return c.json({ ok: true });
17365
18176
  });
@@ -17395,6 +18206,93 @@ function registerCodeRoutes(app2, d) {
17395
18206
  db2.setRevertedFromMessageId(id, messageId);
17396
18207
  return c.json({ ok: true, revertedFromMessageId: messageId, revertText: cut.revertText, revertedFiles, failedFiles });
17397
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
+ });
17398
18296
  app2.post("/api/v1/code/sessions/:id/stop", (c) => {
17399
18297
  const id = c.req.param("id");
17400
18298
  runs.stop(id);
@@ -17409,6 +18307,7 @@ function registerCodeRoutes(app2, d) {
17409
18307
  app2.post("/api/v1/code/sessions/:id/messages", async (c) => {
17410
18308
  const id = c.req.param("id");
17411
18309
  const b = await body5(c);
18310
+ const kind = b.kind === "steer" ? "steer" : "followUp";
17412
18311
  const run = db2.getAgentRun(id);
17413
18312
  if (!run) return err5(c, 404, "not_found", "Session not found.");
17414
18313
  const conv = db2.getConversation(run.convId, true);
@@ -17417,20 +18316,25 @@ function registerCodeRoutes(app2, d) {
17417
18316
  const ms = d.manager.status();
17418
18317
  if (ms.state !== "running" || !ms.model) return err5(c, 409, "model_not_loaded", "Load a model first.");
17419
18318
  if (!d.manager.target()) return err5(c, 409, "model_not_loaded", "Engine not running.");
17420
- const followUp = (b.content ?? "").trim();
18319
+ const newContent = (b.content ?? "").trim();
17421
18320
  let userMsgId;
17422
18321
  let task;
17423
- if (followUp) {
17424
- userMsgId = db2.addMessage(run.convId, "user", followUp, { textAttachments: b.contextFiles }).id;
17425
- 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);
17426
18325
  } else {
17427
18326
  const lastUser = (conv.messages ?? []).filter((m) => m.role === "user").at(-1);
17428
18327
  if (!lastUser) return err5(c, 400, "no_task", "No task to run.");
17429
18328
  userMsgId = lastUser.id;
17430
18329
  task = contextFilesBlock(lastUser.textAttachments) + lastUser.content;
17431
18330
  }
17432
- const { queued } = runs.enqueue(id, { convId: run.convId, repoRoot: run.repoRoot, task, userMsgId, thinkingBudget: b.thinkingBudget });
17433
- 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);
17434
18338
  });
17435
18339
  app2.get("/api/v1/code/sessions/:id/stream", (c) => {
17436
18340
  const id = c.req.param("id");
@@ -17446,6 +18350,8 @@ function registerCodeRoutes(app2, d) {
17446
18350
  const meta = runs.activeMeta(id);
17447
18351
  if (meta) await stream.writeSSE({ event: "meta", data: JSON.stringify(meta) });
17448
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 }) });
17449
18355
  for await (const ev of sub) {
17450
18356
  await stream.writeSSE({ id: String(ev.seq), event: ev.event, data: JSON.stringify(ev.data) });
17451
18357
  }
@@ -18131,7 +19037,7 @@ function contentType(file) {
18131
19037
  }
18132
19038
 
18133
19039
  // src/tunnel/manager.ts
18134
- 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";
18135
19041
  import { mkdirSync as mkdirSync15, readFileSync as readFileSync14, readdirSync as readdirSync9, rmSync as rmSync14, writeFileSync as writeFileSync13 } from "fs";
18136
19042
  import { join as join31 } from "path";
18137
19043
 
@@ -18211,13 +19117,13 @@ function sleep4(ms) {
18211
19117
  }
18212
19118
  function signalTerm2(child) {
18213
19119
  if (!child.pid) return;
18214
- if (process.platform === "win32") execFile12("taskkill", ["/PID", String(child.pid), "/T"], () => {
19120
+ if (process.platform === "win32") execFile13("taskkill", ["/PID", String(child.pid), "/T"], () => {
18215
19121
  });
18216
19122
  else child.kill("SIGTERM");
18217
19123
  }
18218
19124
  function forceKill2(child) {
18219
19125
  if (!child.pid) return;
18220
- 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"], () => {
18221
19127
  });
18222
19128
  else child.kill("SIGKILL");
18223
19129
  }