chainlesschain 0.162.60 → 0.162.65

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 (166) hide show
  1. package/package.json +2 -2
  2. package/src/assets/web-panel/assets/{AIOps-a2cSbSEu.js → AIOps-DjJf_QIn.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-DwvSB5Pp.js → ActionButton-BT45g-KL.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-BqaRaBDD.js → Analytics-CRaTHble.js} +3 -3
  5. package/src/assets/web-panel/assets/{AppLayout-Beck7v8t.js → AppLayout-72r5TM1u.js} +5 -5
  6. package/src/assets/web-panel/assets/{Audit-UtJhPdXJ.js → Audit-BNlvJ3Yc.js} +1 -1
  7. package/src/assets/web-panel/assets/{Backup-HVZhcdll.js → Backup-Kuj0-vBg.js} +1 -1
  8. package/src/assets/web-panel/assets/{BaseInput-DRY_ZGmj.js → BaseInput-_pKOPRf4.js} +1 -1
  9. package/src/assets/web-panel/assets/{Chat-D7Vuwfe2.js → Chat-CMNhGWK5.js} +5 -5
  10. package/src/assets/web-panel/assets/ChatBubbleRenderer-DxJmwLv8.js +1 -0
  11. package/src/assets/web-panel/assets/{Checkbox-B406i7N1.js → Checkbox-B5R2TdAI.js} +1 -1
  12. package/src/assets/web-panel/assets/{Codegen-BvTCqHi3.js → Codegen-69RAQ0Gi.js} +1 -1
  13. package/src/assets/web-panel/assets/{Col-DRiyxTQP.js → Col-DlbssQEY.js} +1 -1
  14. package/src/assets/web-panel/assets/{Community-DWmhxHQa.js → Community-DU3SAZIS.js} +1 -1
  15. package/src/assets/web-panel/assets/{Compact-DO1HBZEz.js → Compact-BqdNnAZv.js} +1 -1
  16. package/src/assets/web-panel/assets/{Compliance-D4j-VHwS.js → Compliance-D9a9-ihS.js} +1 -1
  17. package/src/assets/web-panel/assets/{Cowork-BGBkWtat.js → Cowork-DWBtOBbU.js} +3 -3
  18. package/src/assets/web-panel/assets/{Cron-Xa9PtMUQ.js → Cron-ClSuf90k.js} +2 -2
  19. package/src/assets/web-panel/assets/{Crosschain-wVWs4lqN.js → Crosschain-BFjRKvpa.js} +1 -1
  20. package/src/assets/web-panel/assets/{DID-DTkqiRuT.js → DID-BwBGRlMm.js} +2 -2
  21. package/src/assets/web-panel/assets/{Dashboard-d9STUbrr.js → Dashboard-CHrXGmQ3.js} +2 -2
  22. package/src/assets/web-panel/assets/{Dropdown-CrdxS-C8.js → Dropdown-C24B5sk2.js} +1 -1
  23. package/src/assets/web-panel/assets/{EmailListRenderer-D78XHUEp.js → EmailListRenderer-DaXTSK5p.js} +1 -1
  24. package/src/assets/web-panel/assets/{FamilyGuardDashboard-iAeSETIP.js → FamilyGuardDashboard-65d89G5t.js} +1 -1
  25. package/src/assets/web-panel/assets/{Federation-CTV1Sxqs.js → Federation-CkWdqmVs.js} +1 -1
  26. package/src/assets/web-panel/assets/{FormItemContext-BtwNuQKK.js → FormItemContext-BV4W2nrT.js} +1 -1
  27. package/src/assets/web-panel/assets/{GenericCardRenderer-CdEgHjkl.js → GenericCardRenderer-D60KJ0_b.js} +1 -1
  28. package/src/assets/web-panel/assets/{Git-BTo-PJr_.js → Git-BIKuoGvW.js} +2 -2
  29. package/src/assets/web-panel/assets/{Governance-DquOG94r.js → Governance-CKnJpq5X.js} +1 -1
  30. package/src/assets/web-panel/assets/{Inference-DDtcBxRB.js → Inference-C7G3YGeg.js} +1 -1
  31. package/src/assets/web-panel/assets/{KnowledgeGraph-KUOmNj5C.js → KnowledgeGraph-D7fCUd4B.js} +1 -1
  32. package/src/assets/web-panel/assets/{Logs-HKm7kRs7.js → Logs-C0unjcbC.js} +2 -2
  33. package/src/assets/web-panel/assets/{Marketplace-IxrOcbFB.js → Marketplace-BzLlnyI8.js} +1 -1
  34. package/src/assets/web-panel/assets/{McpTools-D6a1LM3S.js → McpTools-DSKFRB1-.js} +4 -4
  35. package/src/assets/web-panel/assets/{Memory-lFkD2ZuM.js → Memory-C_QrLAnt.js} +2 -2
  36. package/src/assets/web-panel/assets/{MobileBridge-xwuQTps5.js → MobileBridge-DBeaFERD.js} +2 -2
  37. package/src/assets/web-panel/assets/MobileProjects-C2L_RttC.js +1 -0
  38. package/src/assets/web-panel/assets/{Mtc-BXpJGrjm.js → Mtc-B3Tdh6-l.js} +5 -5
  39. package/src/assets/web-panel/assets/{MtcAudit-CWttaim1.js → MtcAudit-B3O_EUvt.js} +4 -4
  40. package/src/assets/web-panel/assets/{Multisig-jKgTuVLS.js → Multisig--60rVmDj.js} +3 -3
  41. package/src/assets/web-panel/assets/{NLProgramming-xl4RDzQj.js → NLProgramming-D60vxATf.js} +1 -1
  42. package/src/assets/web-panel/assets/{Notes-DPBOvscE.js → Notes-D2gj2uFI.js} +3 -3
  43. package/src/assets/web-panel/assets/{NotificationSettings-8TkIkRo3.js → NotificationSettings-D0DWHNlF.js} +1 -1
  44. package/src/assets/web-panel/assets/{OrderTableRenderer-Dwa-XtoE.js → OrderTableRenderer-CNi1B7fH.js} +1 -1
  45. package/src/assets/web-panel/assets/{Organization-CJ0xVwZM.js → Organization-BRcdFgAd.js} +3 -3
  46. package/src/assets/web-panel/assets/{Overflow-V7VuUslt.js → Overflow-C3_Oap7v.js} +1 -1
  47. package/src/assets/web-panel/assets/{P2P-BxuccEGq.js → P2P-DEbZ93QW.js} +2 -2
  48. package/src/assets/web-panel/assets/{PdhVaultBrowser-DaP2Q5kU.js → PdhVaultBrowser-DN_pmo2N.js} +3 -3
  49. package/src/assets/web-panel/assets/{Permissions-CPJFF0zU.js → Permissions-CPj3C9o2.js} +4 -4
  50. package/src/assets/web-panel/assets/{PersonalDataHub-Cmn2uiuw.js → PersonalDataHub-BZGupZzh.js} +4 -4
  51. package/src/assets/web-panel/assets/{Pipeline-0zX89_iz.js → Pipeline-SMLW1BG7.js} +1 -1
  52. package/src/assets/web-panel/assets/{Privacy-DROUg3XE.js → Privacy-o24SJ2no.js} +1 -1
  53. package/src/assets/web-panel/assets/{ProjectInit-c5KESOK4.js → ProjectInit-DxjAXD8f.js} +2 -2
  54. package/src/assets/web-panel/assets/{ProjectSettings-BfiCcnb_.js → ProjectSettings-DipynlqL.js} +2 -2
  55. package/src/assets/web-panel/assets/{Projects-BtZH5-Eh.js → Projects-CZ9egQ8r.js} +1 -1
  56. package/src/assets/web-panel/assets/{Providers-C9Rr_dOk.js → Providers-x3p-wcab.js} +1 -1
  57. package/src/assets/web-panel/assets/{QuickAsk-Du4p90W6.js → QuickAsk-CZ7beKFC.js} +1 -1
  58. package/src/assets/web-panel/assets/{Recommend-B-DQenTl.js → Recommend-VJCd2i9_.js} +1 -1
  59. package/src/assets/web-panel/assets/{Reputation-DvwlAVRZ.js → Reputation-pl12NmBF.js} +1 -1
  60. package/src/assets/web-panel/assets/{Row-rTnbvkP-.js → Row-NkSeo4Tb.js} +1 -1
  61. package/src/assets/web-panel/assets/{RssFeed-DwvsqWbB.js → RssFeed-B17vp67R.js} +3 -3
  62. package/src/assets/web-panel/assets/{Search-U_Xj5SvF.js → Search-Dij0_m6W.js} +1 -1
  63. package/src/assets/web-panel/assets/{Security-CdjsVDQ8.js → Security-n9CSBX-9.js} +4 -4
  64. package/src/assets/web-panel/assets/{Services-DUd3mFXk.js → Services-bZOzqHdK.js} +2 -2
  65. package/src/assets/web-panel/assets/{Skeleton-CA2gCJmY.js → Skeleton-B23D5vJ-.js} +1 -1
  66. package/src/assets/web-panel/assets/{Skills-BIw7Rb-m.js → Skills-IXh-0mk0.js} +1 -1
  67. package/src/assets/web-panel/assets/{Sla-vySPesC0.js → Sla-QEofxmdK.js} +1 -1
  68. package/src/assets/web-panel/assets/{SpeechSettings-EniuTjBJ.js → SpeechSettings-u68R59ft.js} +1 -1
  69. package/src/assets/web-panel/assets/{SyncSettings-DkrqbzYS.js → SyncSettings-B3tc986U.js} +2 -2
  70. package/src/assets/web-panel/assets/Tasks-Cp2QxGrr.js +1 -0
  71. package/src/assets/web-panel/assets/{Templates-C2Kvn60U.js → Templates-CMWiWxiH.js} +1 -1
  72. package/src/assets/web-panel/assets/{Tenant-x6jVMx4D.js → Tenant-M8aPJ3C7.js} +1 -1
  73. package/src/assets/web-panel/assets/Terminal-CK3zKjIE.js +3 -0
  74. package/src/assets/web-panel/assets/{TimelineRenderer-BF6HAETd.js → TimelineRenderer-DF6aIS-d.js} +1 -1
  75. package/src/assets/web-panel/assets/{Tokens-B-KcVAin.js → Tokens-BcfMMw_e.js} +1 -1
  76. package/src/assets/web-panel/assets/{Trigger-B-Caiptm.js → Trigger-jIbNmxvm.js} +1 -1
  77. package/src/assets/web-panel/assets/{Trust-_8sq7pJp.js → Trust-ChLil6CZ.js} +1 -1
  78. package/src/assets/web-panel/assets/{UkeySign-CLveUEgo.js → UkeySign-B1WzYAon.js} +1 -1
  79. package/src/assets/web-panel/assets/{VideoEditing-iVc9jxt9.js → VideoEditing-Dwm0LyCc.js} +1 -1
  80. package/src/assets/web-panel/assets/{Wallet-D1n5pidD.js → Wallet-DVyxsX-O.js} +3 -3
  81. package/src/assets/web-panel/assets/{WebAuthn-CA8kubXb.js → WebAuthn-WYPNy2Q7.js} +4 -4
  82. package/src/assets/web-panel/assets/{WorkflowEditor-BXWwd_fB.js → WorkflowEditor-Br3dCsmv.js} +1 -1
  83. package/src/assets/web-panel/assets/{chat-DUNkQr1A.js → chat-fAKHY2HK.js} +1 -1
  84. package/src/assets/web-panel/assets/{colors-Dz5ozTcp.js → colors-BXqS-Bwi.js} +1 -1
  85. package/src/assets/web-panel/assets/{compact-item-CZ5-JSLh.js → compact-item-BgCQhtW3.js} +1 -1
  86. package/src/assets/web-panel/assets/{createContext-CFxlcPug.js → createContext-weZBwqHy.js} +1 -1
  87. package/src/assets/web-panel/assets/devWarning-BpXdFCJ4.js +1 -0
  88. package/src/assets/web-panel/assets/{hasIn-CqWIkHJm.js → hasIn-Dx68UNFL.js} +1 -1
  89. package/src/assets/web-panel/assets/{index-C5zhjact.js → index-5e-OAZOb.js} +1 -1
  90. package/src/assets/web-panel/assets/{index-xaZX6ZDL.js → index-B0rgvjX8.js} +1 -1
  91. package/src/assets/web-panel/assets/{index-DUyhvh0L.js → index-B8-2rQdr.js} +1 -1
  92. package/src/assets/web-panel/assets/{index-3elHm6lI.js → index-B8zNZ_oH.js} +1 -1
  93. package/src/assets/web-panel/assets/{index-CD7UjlnE.js → index-B9NeNwHP.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-CE-gpaY9.js → index-BC-4la9j.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-CLu3Oyef.js → index-BSQEQCft.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-CBeASfAD.js → index-BawcE_zG.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-Dc-5Ulgt.js → index-Bazltj8w.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-DO6mf95c.js → index-BehvfmYd.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-rR060KAF.js → index-BjsidvP5.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-BiAcVeea.js → index-Bo4UTTla.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-CDq8IVvv.js → index-BqcH_mKR.js} +1 -1
  102. package/src/assets/web-panel/assets/index-BrPKR2RZ.js +1 -0
  103. package/src/assets/web-panel/assets/{index-8l5LLWxH.js → index-Bwv_UrNF.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-BLNgGXeg.js → index-C0ZjD3Ac.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-GVbsyUQm.js → index-CEjLe8FJ.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-BLLSLAC3.js → index-CJXZDwkf.js} +1 -1
  107. package/src/assets/web-panel/assets/index-CMEfvACO.js +1 -0
  108. package/src/assets/web-panel/assets/{index-CL6wt2JN.js → index-CX9cxRnU.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-BBq1ySIt.js → index-CfqzwaAV.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-DW18L-o6.js → index-ChdeuOni.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-FsYDYVUk.js → index-ClfP1Yax.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-CbcHBDYj.js → index-Co5cQnlv.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-BI2EU3hC.js → index-CxfVwfub.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-noQc_RpT.js → index-Cxsfc5Ou.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-DF0hXW5L.js → index-DCYJDUab.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-BO-yo7Jv.js → index-DG2KCc8h.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-DEzYXMgc.js → index-DNF3aCJF.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-CvMZxZOn.js → index-DXuz90bX.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-Ci1-8q-g.js → index-Dbv5btEU.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-BJt6sNTF.js → index-DgUC575c.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-D6Nkerss.js → index-G_wPnPoA.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-CguUaiiY.js → index-HIN85jl7.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-C5XUilwu.js → index-LxcdLeFj.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-C_WWTpLE.js → index-loaP_41H.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-C2SoMbLc.js → index-ohVNy7ua.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-DvUlrMy-.js → index-qUBHSW_3.js} +3 -3
  127. package/src/assets/web-panel/assets/{index-DGAjS_D9.js → index-u2U9t07r.js} +1 -1
  128. package/src/assets/web-panel/assets/{initDefaultProps-PIetywTX.js → initDefaultProps-M7xH4eUK.js} +1 -1
  129. package/src/assets/web-panel/assets/{motion-gQJEK3wO.js → motion-BrJP4mFE.js} +1 -1
  130. package/src/assets/web-panel/assets/{move-ML1nRxts.js → move-BufxEuU9.js} +1 -1
  131. package/src/assets/web-panel/assets/{omit-wUWsw3YL.js → omit-s6dtQtFP.js} +1 -1
  132. package/src/assets/web-panel/assets/{pickAttrs-A0Wlomih.js → pickAttrs-BcYdIZqz.js} +1 -1
  133. package/src/assets/web-panel/assets/{placementArrow-C5RKYdxT.js → placementArrow-Ds-3Hw3n.js} +1 -1
  134. package/src/assets/web-panel/assets/{responsiveObserve-DIxNVSJl.js → responsiveObserve-BRtrRTxl.js} +1 -1
  135. package/src/assets/web-panel/assets/{slide-B-tNesVu.js → slide-RJzqMQM4.js} +1 -1
  136. package/src/assets/web-panel/assets/{statusUtils-CftzO200.js → statusUtils-BuBhJXvr.js} +1 -1
  137. package/src/assets/web-panel/assets/{styleChecker-CMgvWu90.js → styleChecker-so8acGHq.js} +1 -1
  138. package/src/assets/web-panel/assets/{useFlexGapSupport-sxqoDNhZ.js → useFlexGapSupport-BpkV467K.js} +1 -1
  139. package/src/assets/web-panel/assets/{useFs-CowEXz4v.js → useFs-lES1RctZ.js} +1 -1
  140. package/src/assets/web-panel/assets/{usePersonalDataHub-6ISRG7V-.js → usePersonalDataHub-DdCNi4bA.js} +1 -1
  141. package/src/assets/web-panel/assets/{vnode-C2mnXfmw.js → vnode-nAeEg_3h.js} +1 -1
  142. package/src/assets/web-panel/assets/{zoom-DMsur0jx.js → zoom-BWjRAfRy.js} +1 -1
  143. package/src/assets/web-panel/index.html +1 -1
  144. package/src/commands/agent.js +40 -1
  145. package/src/commands/review.js +463 -0
  146. package/src/commands/terminal-setup.js +127 -0
  147. package/src/index.js +4 -0
  148. package/src/lib/cost-budget.js +109 -0
  149. package/src/lib/ide-context.js +50 -0
  150. package/src/lib/image-input.js +8 -2
  151. package/src/lib/llm-pricing.js +6 -0
  152. package/src/lib/personal-data-hub-wiring.js +16 -0
  153. package/src/lib/terminal-setup.js +209 -0
  154. package/src/repl/agent-repl.js +58 -1
  155. package/src/repl/tasks-status.js +82 -0
  156. package/src/runtime/agent-core.js +38 -3
  157. package/src/runtime/headless-runner.js +51 -3
  158. package/src/runtime/headless-stream.js +62 -4
  159. package/src/runtime/mcp-config.js +6 -0
  160. package/src/assets/web-panel/assets/ChatBubbleRenderer-BS2q_hPX.js +0 -1
  161. package/src/assets/web-panel/assets/MobileProjects-BYr1D3WO.js +0 -1
  162. package/src/assets/web-panel/assets/Tasks-oyPnWRbw.js +0 -1
  163. package/src/assets/web-panel/assets/Terminal-C2nZbPBs.js +0 -3
  164. package/src/assets/web-panel/assets/devWarning-BMRVR8Xp.js +0 -1
  165. package/src/assets/web-panel/assets/index-BT2s_zxU.js +0 -1
  166. package/src/assets/web-panel/assets/index-DOTL6NDc.js +0 -1
@@ -0,0 +1,109 @@
1
+ /**
2
+ * cost-budget — a hard USD spend cap for unattended agent runs
3
+ * (Claude-Code `--max-budget-usd` parity).
4
+ *
5
+ * Where IterationBudget caps the number of agent-loop turns, CostBudget caps the
6
+ * estimated dollar cost: it accumulates the per-call cost (via llm-pricing) as
7
+ * token-usage events arrive and reports when the cap is reached, so the runner
8
+ * can stop BEFORE making another paid LLM call. Because a call's cost is only
9
+ * known after it returns, a run may overshoot by at most one call — it never
10
+ * starts a new turn once over budget.
11
+ *
12
+ * Local/free providers (ollama, …) and unpriced models cost $0 here, so a cap
13
+ * can never trigger for them; `shouldWarnInactive()` lets the caller surface a
14
+ * one-time "cap inactive" notice instead of silently doing nothing.
15
+ *
16
+ * Pure + dependency-light (only llm-pricing) so it is unit-testable without a
17
+ * real agent loop.
18
+ */
19
+
20
+ import { estimateCost } from "./llm-pricing.js";
21
+
22
+ const round = (n, dp = 6) => {
23
+ const f = Math.pow(10, dp);
24
+ return Math.round((Number(n) + Number.EPSILON) * f) / f;
25
+ };
26
+
27
+ /** Parse a `--max-budget-usd` value into a positive number, or null when unset. */
28
+ export function parseBudgetUsd(value) {
29
+ if (value == null || value === "") return null;
30
+ const n = Number(value);
31
+ if (!Number.isFinite(n) || n <= 0) {
32
+ throw new Error(
33
+ `Invalid --max-budget-usd "${value}". Expected a positive number of US dollars.`,
34
+ );
35
+ }
36
+ return n;
37
+ }
38
+
39
+ export class CostBudget {
40
+ /**
41
+ * @param {object} opts
42
+ * @param {number|null} [opts.limitUsd] cap in USD; null/≤0 → disabled
43
+ * @param {object} [opts.table] merged price table (mergePricing output)
44
+ */
45
+ constructor({ limitUsd = null, table = undefined } = {}) {
46
+ const lim = Number(limitUsd);
47
+ this.limitUsd = Number.isFinite(lim) && lim > 0 ? lim : null;
48
+ this.table = table;
49
+ this.spentUsd = 0;
50
+ this.priced = false; // priced ≥1 non-free usage record
51
+ this.sawUnpriced = false; // saw tokens we couldn't price
52
+ this.sawFree = false; // saw a free/local provider
53
+ this._warned = false;
54
+ }
55
+
56
+ enabled() {
57
+ return this.limitUsd != null;
58
+ }
59
+
60
+ /**
61
+ * Fold one token-usage record into the running spend.
62
+ * @returns {object} the estimateCost() result for this record
63
+ */
64
+ add({ provider, model, usage } = {}) {
65
+ const est = estimateCost({
66
+ provider,
67
+ model,
68
+ inputTokens: usage?.input_tokens || 0,
69
+ outputTokens: usage?.output_tokens || 0,
70
+ table: this.table,
71
+ });
72
+ const tokens = (usage?.input_tokens || 0) + (usage?.output_tokens || 0);
73
+ if (est.free) {
74
+ this.sawFree = true;
75
+ } else if (est.matched) {
76
+ this.spentUsd = round(this.spentUsd + est.totalCost);
77
+ this.priced = true;
78
+ } else if (tokens > 0) {
79
+ this.sawUnpriced = true;
80
+ }
81
+ return est;
82
+ }
83
+
84
+ /** True once the running spend has reached/passed the cap. */
85
+ exceeded() {
86
+ return this.limitUsd != null && this.spentUsd >= this.limitUsd;
87
+ }
88
+
89
+ /** USD left under the cap (Infinity when disabled). */
90
+ remaining() {
91
+ return this.limitUsd == null
92
+ ? Infinity
93
+ : Math.max(0, round(this.limitUsd - this.spentUsd));
94
+ }
95
+
96
+ /**
97
+ * True the FIRST time we can tell the cap can't bite — a cap was set but every
98
+ * usage so far has been free/local or unpriced, so spend stays $0. Lets the
99
+ * caller print a one-time "cap inactive" warning instead of a silent no-op.
100
+ */
101
+ shouldWarnInactive() {
102
+ if (!this.enabled() || this._warned || this.priced) return false;
103
+ if (this.sawUnpriced || this.sawFree) {
104
+ this._warned = true;
105
+ return true;
106
+ }
107
+ return false;
108
+ }
109
+ }
@@ -278,6 +278,13 @@ export function hasIdeOpenDiff(mcp) {
278
278
  * Run one blocking openDiff review in the connected IDE. Returns
279
279
  * { outcome:"accepted", finalText|null } — the IDE wrote the file itself
280
280
  * { outcome:"rejected" } — nothing was written
281
+ * { outcome:"changes-requested", comments, reviewedText }
282
+ * — the user annotated the diff with
283
+ * revision notes instead of
284
+ * accepting/rejecting; nothing was
285
+ * written and the caller should
286
+ * feed `comments` back to the agent
287
+ * so it revises and re-proposes.
281
288
  * null — IDE unavailable / transport
282
289
  * error / malformed reply → the
283
290
  * caller falls back to its normal
@@ -309,10 +316,53 @@ export async function requestIdeDiffApproval(mcp, req = {}) {
309
316
  finalText: typeof data.finalText === "string" ? data.finalText : null,
310
317
  };
311
318
  }
319
+ if (data?.outcome === "changes-requested") {
320
+ return {
321
+ outcome: "changes-requested",
322
+ comments: Array.isArray(data.comments) ? data.comments : [],
323
+ reviewedText:
324
+ typeof data.reviewedText === "string" ? data.reviewedText : null,
325
+ };
326
+ }
312
327
  if (data?.outcome === "rejected") return { outcome: "rejected" };
313
328
  return null; // anything else is not a verdict — fail safe to fallback
314
329
  }
315
330
 
331
+ /**
332
+ * Render line-anchored review comments (from an openDiff "changes-requested"
333
+ * verdict) into a compact feedback block the agent can act on. Each comment is
334
+ * `{ line?, endLine?, lineText?, note }` with 0-based editor lines. Returns
335
+ * null when there is no actionable note. Pure — safe to unit-test.
336
+ */
337
+ export function formatReviewComments(comments, { path: filePath } = {}) {
338
+ if (!Array.isArray(comments) || comments.length === 0) return null;
339
+ const lines = comments
340
+ .map((c) => {
341
+ if (!c || typeof c.note !== "string" || c.note.trim().length === 0) {
342
+ return null;
343
+ }
344
+ const start = Number.isInteger(c.line) ? c.line + 1 : null; // 0→1-based
345
+ const end = Number.isInteger(c.endLine) ? c.endLine + 1 : start;
346
+ const where =
347
+ start != null
348
+ ? end != null && end !== start
349
+ ? `lines ${start}-${end}`
350
+ : `line ${start}`
351
+ : "(general)";
352
+ const anchor =
353
+ typeof c.lineText === "string" && c.lineText.trim().length > 0
354
+ ? ` ⟪${c.lineText.trim().slice(0, 120)}⟫`
355
+ : "";
356
+ return ` • ${where}: ${c.note.trim()}${anchor}`;
357
+ })
358
+ .filter(Boolean);
359
+ if (lines.length === 0) return null;
360
+ const header = filePath
361
+ ? `Review comments on ${filePath}:`
362
+ : "Review comments:";
363
+ return `${header}\n${lines.join("\n")}`;
364
+ }
365
+
316
366
  // ─── Explicit @selection / @diagnostics at-mentions (Claude-Code parity) ────
317
367
  //
318
368
  // The ambient `<ide-context>` block above shares the selection on every turn.
@@ -115,8 +115,14 @@ export function imageUrlBlockToAnthropic(block) {
115
115
  };
116
116
  }
117
117
 
118
- /** Default vision model (Volcengine Ark Doubao-Seed-1.6 Vision) when none configured. */
119
- export const DEFAULT_VISION_MODEL = "doubao-seed-1-6-vision-250815";
118
+ /**
119
+ * Default vision model when none is configured — Volcengine Ark
120
+ * Doubao-Seed-2.0-lite (id `doubao-seed-2-0-lite-260215`, dated YYMMDD =
121
+ * 2026-02-15), a natively multimodal model (text/image/video; 2.0 has no
122
+ * separate "-vision-" SKU). Override via --vision-model or config.llm.visionModel
123
+ * to pin a different snapshot.
124
+ */
125
+ export const DEFAULT_VISION_MODEL = "doubao-seed-2-0-lite-260215";
120
126
 
121
127
  /**
122
128
  * Resolve the effective LLM config for a run. When an image is attached, default
@@ -58,6 +58,12 @@ export const PRICE_TABLE = Object.freeze({
58
58
  ],
59
59
  // Volcengine Doubao — rough USD conversion of public RMB list pricing.
60
60
  volcengine: [
61
+ // Doubao Seed 2.0 family (2026, e.g. doubao-seed-2-0-lite-260215) is
62
+ // natively multimodal. Rates ≈ official CNY ÷ ~7.2 (lite ≤32k is 0.6/3.6
63
+ // CNY → $0.08/$0.50); the generic "seed" rate below underprices 2.0 output.
64
+ { match: "seed-2-0-pro", in: 0.5, out: 2.5 },
65
+ { match: "seed-2-0-lite", in: 0.08, out: 0.5 },
66
+ { match: "seed-2-0-mini", in: 0.03, out: 0.3 },
61
67
  { match: "seed-1-6", in: 0.11, out: 0.28 },
62
68
  { match: "seed", in: 0.11, out: 0.28 },
63
69
  { match: "pro", in: 0.11, out: 0.28 },
@@ -57,6 +57,8 @@ const {
57
57
  ZhihuAdapter,
58
58
  BossZhipinAdapter,
59
59
  CsdnAdapter,
60
+ DongchediAdapter,
61
+ TianyanchaAdapter,
60
62
  DouyinAdapter,
61
63
  XiaohongshuAdapter,
62
64
  ToutiaoAdapter,
@@ -74,12 +76,18 @@ const {
74
76
  KugouMusicAdapter,
75
77
  IqiyiVideoAdapter,
76
78
  TencentVideoAdapter,
79
+ XiguaVideoAdapter,
77
80
  WeReadAdapter,
78
81
  WpsDocAdapter,
79
82
  TencentDocsAdapter,
80
83
  BaiduNetdiskAdapter,
84
+ CamScannerDocAdapter,
85
+ IXiamenAdapter,
86
+ MeiyouAdapter,
87
+ TaxAdapter,
81
88
  DingTalkPcAdapter,
82
89
  FeishuPcAdapter,
90
+ WeWorkPcAdapter,
83
91
  BaiduMapAdapter,
84
92
  TencentMapAdapter,
85
93
  JdAdapter,
@@ -529,6 +537,8 @@ async function initHub() {
529
537
  ZhihuAdapter,
530
538
  BossZhipinAdapter,
531
539
  CsdnAdapter,
540
+ DongchediAdapter,
541
+ TianyanchaAdapter,
532
542
  DouyinAdapter,
533
543
  XiaohongshuAdapter,
534
544
  ToutiaoAdapter,
@@ -546,12 +556,18 @@ async function initHub() {
546
556
  KugouMusicAdapter,
547
557
  IqiyiVideoAdapter,
548
558
  TencentVideoAdapter,
559
+ XiguaVideoAdapter,
549
560
  WeReadAdapter,
550
561
  WpsDocAdapter,
551
562
  TencentDocsAdapter,
552
563
  BaiduNetdiskAdapter,
564
+ CamScannerDocAdapter,
565
+ IXiamenAdapter,
566
+ MeiyouAdapter,
567
+ TaxAdapter,
553
568
  DingTalkPcAdapter,
554
569
  FeishuPcAdapter,
570
+ WeWorkPcAdapter,
555
571
  BaiduMapAdapter,
556
572
  TencentMapAdapter,
557
573
  JdAdapter,
@@ -0,0 +1,209 @@
1
+ /**
2
+ * terminal-setup — make Shift+Enter insert a newline in the agent REPL
3
+ * (Claude-Code `/terminal-setup` parity).
4
+ *
5
+ * cc's REPL is readline-based, so a bare Enter always submits. Multiline input
6
+ * is reached by ending a line with a continuation backslash (see
7
+ * repl-multiline.js). This module configures the terminal so that pressing
8
+ * Shift+Enter emits the byte sequence `<space>\<CR>` — which cc's
9
+ * backslash-continuation turns into a soft newline — giving the familiar
10
+ * "Shift+Enter = newline, Enter = send" editing without any REPL raw-mode hacks.
11
+ *
12
+ * Everything here is pure (detection + keybindings JSON shaping); the command
13
+ * (commands/terminal-setup.js) does the file I/O.
14
+ */
15
+
16
+ import path from "node:path";
17
+ import os from "node:os";
18
+
19
+ /**
20
+ * The bytes Shift+Enter should send: space + backslash + carriage return. The
21
+ * leading space makes the trailing backslash satisfy repl-multiline's
22
+ * whitespace-gated continuation rule (so it is never mistaken for a Windows
23
+ * path), and the backslash is stripped back out when the line is joined.
24
+ */
25
+ export const SHIFT_ENTER_SEQUENCE = " \\\r";
26
+
27
+ /** Identify the host terminal from environment variables. */
28
+ export function detectTerminal(env = process.env) {
29
+ const tp = String(env.TERM_PROGRAM || "").toLowerCase();
30
+ if (tp === "vscode" || env.VSCODE_PID || env.VSCODE_INJECTION) {
31
+ return { id: "vscode", name: "VS Code integrated terminal" };
32
+ }
33
+ if (tp === "iterm.app") return { id: "iterm2", name: "iTerm2" };
34
+ if (tp === "apple_terminal")
35
+ return { id: "apple-terminal", name: "Apple Terminal" };
36
+ if (tp === "wezterm") return { id: "wezterm", name: "WezTerm" };
37
+ if (env.WT_SESSION)
38
+ return { id: "windows-terminal", name: "Windows Terminal" };
39
+ return {
40
+ id: "unknown",
41
+ name: env.TERM_PROGRAM || env.TERM || "your terminal",
42
+ };
43
+ }
44
+
45
+ /** The VS Code keybinding that sends the Shift+Enter sequence in the terminal. */
46
+ export function vscodeKeybinding() {
47
+ return {
48
+ key: "shift+enter",
49
+ command: "workbench.action.terminal.sendSequence",
50
+ when: "terminalFocus",
51
+ args: { text: SHIFT_ENTER_SEQUENCE },
52
+ };
53
+ }
54
+
55
+ /** Per-OS path to VS Code's user keybindings.json. */
56
+ export function vscodeKeybindingsPath(
57
+ platform = process.platform,
58
+ env = process.env,
59
+ ) {
60
+ const home = os.homedir();
61
+ if (platform === "win32") {
62
+ const appData = env.APPDATA || path.join(home, "AppData", "Roaming");
63
+ return path.join(appData, "Code", "User", "keybindings.json");
64
+ }
65
+ if (platform === "darwin") {
66
+ return path.join(
67
+ home,
68
+ "Library",
69
+ "Application Support",
70
+ "Code",
71
+ "User",
72
+ "keybindings.json",
73
+ );
74
+ }
75
+ return path.join(home, ".config", "Code", "User", "keybindings.json");
76
+ }
77
+
78
+ /**
79
+ * Strip JSONC (line + block comments, trailing commas) so a keybindings.json
80
+ * with comments can be parsed. Best-effort and string-aware enough for the
81
+ * common case; callers must tolerate a null parse.
82
+ */
83
+ export function stripJsonc(text) {
84
+ let out = "";
85
+ const s = String(text || "");
86
+ let inStr = false;
87
+ let strCh = "";
88
+ let i = 0;
89
+ while (i < s.length) {
90
+ const c = s[i];
91
+ const n = s[i + 1];
92
+ if (inStr) {
93
+ out += c;
94
+ if (c === "\\") {
95
+ out += n ?? "";
96
+ i += 2;
97
+ continue;
98
+ }
99
+ if (c === strCh) inStr = false;
100
+ i += 1;
101
+ continue;
102
+ }
103
+ if (c === '"' || c === "'") {
104
+ inStr = true;
105
+ strCh = c;
106
+ out += c;
107
+ i += 1;
108
+ continue;
109
+ }
110
+ if (c === "/" && n === "/") {
111
+ while (i < s.length && s[i] !== "\n") i += 1;
112
+ continue;
113
+ }
114
+ if (c === "/" && n === "*") {
115
+ i += 2;
116
+ while (i < s.length && !(s[i] === "*" && s[i + 1] === "/")) i += 1;
117
+ i += 2;
118
+ continue;
119
+ }
120
+ out += c;
121
+ i += 1;
122
+ }
123
+ // Drop trailing commas before } or ].
124
+ return out.replace(/,(\s*[}\]])/g, "$1");
125
+ }
126
+
127
+ /** Parse a JSONC keybindings array → array, or null when it cannot be parsed. */
128
+ export function parseKeybindings(text) {
129
+ const src = String(text || "").trim();
130
+ if (!src) return [];
131
+ try {
132
+ const v = JSON.parse(stripJsonc(src));
133
+ return Array.isArray(v) ? v : null;
134
+ } catch {
135
+ return null;
136
+ }
137
+ }
138
+
139
+ /** True when an equivalent keybinding (key+command+when) already exists. */
140
+ export function hasKeybinding(arr, binding) {
141
+ return (Array.isArray(arr) ? arr : []).some(
142
+ (b) =>
143
+ b &&
144
+ b.key === binding.key &&
145
+ b.command === binding.command &&
146
+ (b.when || "") === (binding.when || ""),
147
+ );
148
+ }
149
+
150
+ /**
151
+ * Textually append a keybinding before the closing `]`, preserving the file's
152
+ * existing comments/formatting. Returns the new file text, or null when `text`
153
+ * is non-empty but not a JSON array (caller should not overwrite blindly).
154
+ */
155
+ export function appendKeybindingText(text, binding) {
156
+ const bindingJson = JSON.stringify(binding, null, 2)
157
+ .split("\n")
158
+ .map((l) => " " + l)
159
+ .join("\n")
160
+ .trimStart();
161
+ const src = String(text || "").trim();
162
+ if (!src || src === "[]") {
163
+ return `[\n ${bindingJson}\n]\n`;
164
+ }
165
+ const close = src.lastIndexOf("]");
166
+ if (close === -1) return null; // not an array
167
+ const before = src.slice(0, close).replace(/\s*$/, "");
168
+ const sep = before.endsWith("[") ? "" : ",";
169
+ return `${before}${sep}\n ${bindingJson}\n]\n`;
170
+ }
171
+
172
+ /** Manual instructions for terminals cc cannot auto-configure. */
173
+ export function instructionsFor(id) {
174
+ const seq = "a space, then a backslash, then Enter";
175
+ const common = `Bind Shift+Enter to send ${seq} (cc turns a trailing "\\" into a newline).`;
176
+ switch (id) {
177
+ case "iterm2":
178
+ return [
179
+ common,
180
+ "iTerm2 → Settings → Profiles → Keys → Key Mappings → + :",
181
+ ' Shortcut: Shift+Return Action: "Send Text" Text: \\ (then a literal Return)',
182
+ ];
183
+ case "apple-terminal":
184
+ return [
185
+ common,
186
+ "Terminal → Settings → Profiles → Keyboard → + :",
187
+ ' Key: Shift+Return Action: send string " \\015" (space, backslash, CR)',
188
+ ];
189
+ case "windows-terminal":
190
+ return [
191
+ common,
192
+ "Windows Terminal → Settings → Actions (settings.json) → add:",
193
+ ' { "command": { "action": "sendInput", "input": " \\\\\\r" }, "keys": "shift+enter" }',
194
+ ];
195
+ case "wezterm":
196
+ return [
197
+ common,
198
+ "WezTerm (~/.wezterm.lua) keys:",
199
+ ' { key="Enter", mods="SHIFT", action=wezterm.action.SendString(" \\\\\\r") }',
200
+ ];
201
+ default:
202
+ return [
203
+ common,
204
+ "Most terminals let you remap a key to send custom text/bytes — point",
205
+ "Shift+Enter at the 3 bytes: space (0x20), backslash (0x5C), CR (0x0D).",
206
+ "Or just type a trailing \\ yourself to continue onto the next line.",
207
+ ];
208
+ }
209
+ }
@@ -75,7 +75,10 @@ import {
75
75
  agentLoop as coreAgentLoop,
76
76
  formatToolArgs,
77
77
  killAllBackgroundShellTasks,
78
+ killBackgroundShellTask,
79
+ listBackgroundShellTasks,
78
80
  } from "../runtime/agent-core.js";
81
+ import { formatBackgroundTasks } from "./tasks-status.js";
79
82
  import { expandFileRefs } from "../runtime/file-ref-expander.js";
80
83
  import { composeSystemPrompt } from "../runtime/system-prompt.js";
81
84
  import {
@@ -833,6 +836,8 @@ export async function startAgentRepl(options = {}) {
833
836
  "/statusline",
834
837
  "/sub-agents",
835
838
  "/task",
839
+ "/tasks",
840
+ "/terminal-setup",
836
841
  "/vim",
837
842
  ],
838
843
  getIdeOpenFiles: async () => {
@@ -1176,6 +1181,9 @@ export async function startAgentRepl(options = {}) {
1176
1181
  logger.log(
1177
1182
  ` ${chalk.cyan("/vim")} Toggle vim-mode line editing (/vim [on|off]; Esc → NORMAL)`,
1178
1183
  );
1184
+ logger.log(
1185
+ ` ${chalk.cyan("/terminal-setup")} Bind Shift+Enter → newline (--apply for VS Code)`,
1186
+ );
1179
1187
  logger.log(
1180
1188
  ` ${chalk.cyan("/statusline")} Context-usage line on/off (/statusline [on|off])`,
1181
1189
  );
@@ -1249,6 +1257,9 @@ export async function startAgentRepl(options = {}) {
1249
1257
  logger.log(
1250
1258
  ` ${chalk.cyan("/sub-agents")} Show active/completed sub-agents`,
1251
1259
  );
1260
+ logger.log(
1261
+ ` ${chalk.cyan("/tasks")} Show background shell tasks (kill <id> · kill-all)`,
1262
+ );
1252
1263
  logger.log(
1253
1264
  ` ${chalk.cyan("/ide")} IDE bridge status (connected editor, tools, or why not)`,
1254
1265
  );
@@ -1288,6 +1299,35 @@ export async function startAgentRepl(options = {}) {
1288
1299
  return;
1289
1300
  }
1290
1301
 
1302
+ // `/tasks` — user-facing view of the agent's background shell tasks
1303
+ // (run_shell run_in_background). Must precede the `/task` handler below,
1304
+ // which matches with startsWith("/task") and would otherwise swallow it.
1305
+ if (trimmed === "/tasks" || trimmed.startsWith("/tasks ")) {
1306
+ const rest = trimmed.slice("/tasks".length).trim();
1307
+ if (rest === "kill-all") {
1308
+ const n = killAllBackgroundShellTasks();
1309
+ logger.log(chalk.dim(`Killed ${n} background shell task(s).`));
1310
+ } else if (rest.startsWith("kill ")) {
1311
+ const id = rest.slice("kill ".length).trim();
1312
+ const ok = id ? killBackgroundShellTask(id) : false;
1313
+ logger.log(
1314
+ ok
1315
+ ? chalk.dim(`Killed background shell task ${id}.`)
1316
+ : chalk.dim(`No running background shell task with id "${id}".`),
1317
+ );
1318
+ } else if (rest === "kill") {
1319
+ logger.log(chalk.dim("Usage: /tasks kill <id> · /tasks kill-all"));
1320
+ } else {
1321
+ logger.log(
1322
+ "
1323
+ " + formatBackgroundTasks(listBackgroundShellTasks()) + "
1324
+ ",
1325
+ );
1326
+ }
1327
+ prompt();
1328
+ return;
1329
+ }
1330
+
1291
1331
  if (trimmed === "/sub-agents" || trimmed === "/subagents") {
1292
1332
  try {
1293
1333
  const { SubAgentRegistry } =
@@ -1384,6 +1424,23 @@ export async function startAgentRepl(options = {}) {
1384
1424
  return;
1385
1425
  }
1386
1426
 
1427
+ if (
1428
+ trimmed === "/terminal-setup" ||
1429
+ trimmed.startsWith("/terminal-setup ")
1430
+ ) {
1431
+ try {
1432
+ const arg = trimmed.slice("/terminal-setup".length).trim();
1433
+ const { runTerminalSetup } =
1434
+ await import("../commands/terminal-setup.js");
1435
+ const res = runTerminalSetup({ apply: arg === "--apply" });
1436
+ for (const l of res.lines) logger.log(l);
1437
+ } catch (err) {
1438
+ logger.error(`/terminal-setup failed: ${err.message}`);
1439
+ }
1440
+ prompt();
1441
+ return;
1442
+ }
1443
+
1387
1444
  if (trimmed === "/vim" || trimmed.startsWith("/vim ")) {
1388
1445
  const arg = trimmed.slice("/vim".length).trim().toLowerCase();
1389
1446
  const turnOn = arg === "on" || (arg === "" && !_vimEnabled);
@@ -2507,7 +2564,7 @@ export async function startAgentRepl(options = {}) {
2507
2564
  const roles = {
2508
2565
  mainProvider: provider,
2509
2566
  mainModel: _curModel || model,
2510
- visionModel: visionModel || "doubao-seed-1-6-vision-250815",
2567
+ visionModel: visionModel || "doubao-seed-2-0-lite-260215",
2511
2568
  fallbackModels: _fallbackModels || [],
2512
2569
  };
2513
2570
  const { renderSessionCost } = await import("./session-cost.js");
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Pure renderer for the `/tasks` REPL command — a user-facing view of the
3
+ * agent's background shell tasks (the ones it starts with
4
+ * run_shell { run_in_background: true }). The data comes from agent-core's
5
+ * listBackgroundShellTasks(); this module only formats it, so it stays
6
+ * deterministic and unit-testable (inject `now` for stable elapsed time).
7
+ *
8
+ * Background shells are otherwise only visible to the agent (via its
9
+ * check_shell tool) — this surfaces them to the human, the way Claude Code's
10
+ * background-tasks view does. Sub-agents live under a separate registry and
11
+ * are shown by `/sub-agents`.
12
+ */
13
+
14
+ /** Coerce an ISO-string or epoch-ms timestamp to epoch ms, or null. */
15
+ function toMs(v) {
16
+ if (typeof v === "number" && Number.isFinite(v)) return v;
17
+ if (typeof v === "string") {
18
+ const t = Date.parse(v);
19
+ return Number.isFinite(t) ? t : null;
20
+ }
21
+ return null;
22
+ }
23
+
24
+ /** Human-friendly elapsed duration. */
25
+ function fmtElapsed(ms) {
26
+ if (!Number.isFinite(ms) || ms < 0) return "?";
27
+ const s = Math.floor(ms / 1000);
28
+ if (s < 60) return `${s}s`;
29
+ const m = Math.floor(s / 60);
30
+ if (m < 60) return `${m}m${s % 60}s`;
31
+ const h = Math.floor(m / 60);
32
+ return `${h}h${m % 60}m`;
33
+ }
34
+
35
+ /** A short status badge for one task. */
36
+ export function taskStatusLabel(t) {
37
+ switch (t?.status) {
38
+ case "running":
39
+ return "● running";
40
+ case "exited":
41
+ return `✓ exited${t.exitCode != null ? ` (${t.exitCode})` : ""}`;
42
+ case "failed":
43
+ return `✗ failed${t.exitCode != null ? ` (${t.exitCode})` : ""}`;
44
+ case "error":
45
+ return "✗ error";
46
+ default:
47
+ return t?.status || "?";
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Render the background-shell task list as a plain-text block.
53
+ * @param {Array} tasks from listBackgroundShellTasks()
54
+ * @param {object} opts { now?: epoch-ms } — defaults to Date.now()
55
+ */
56
+ export function formatBackgroundTasks(tasks, { now = Date.now() } = {}) {
57
+ const list = Array.isArray(tasks) ? tasks : [];
58
+ if (list.length === 0) {
59
+ return (
60
+ "No background shell tasks.\n" +
61
+ " (The agent starts these with run_shell run_in_background:true; " +
62
+ "sub-agents are under /sub-agents.)"
63
+ );
64
+ }
65
+ const running = list.filter((t) => t?.status === "running").length;
66
+ const lines = [
67
+ `Background shell tasks (${list.length}, ${running} running):`,
68
+ ];
69
+ for (const t of list) {
70
+ const started = toMs(t.startedAt);
71
+ const ended = toMs(t.endedAt);
72
+ const elapsed =
73
+ started != null ? fmtElapsed((ended ?? now) - started) : "?";
74
+ const cmd = String(t.command || "")
75
+ .replace(/\s+/g, " ")
76
+ .trim();
77
+ const cmdShort = cmd.length > 70 ? cmd.slice(0, 70) + "…" : cmd;
78
+ lines.push(` ${taskStatusLabel(t)} ${t.id} ${elapsed} ${cmdShort}`);
79
+ }
80
+ lines.push("Manage: /tasks kill <id> · /tasks kill-all");
81
+ return lines.join("\n");
82
+ }